From bf90d6cb31f47776233a86cd1df15067fd7da9ec Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 16 Feb 2026 19:45:12 +0100 Subject: [PATCH 01/40] Migration navigation from InteractionManager to TransitionTracker --- .../Navigators/InteractionManagerLayout.tsx | 32 +++++++++++ src/libs/Navigation/Navigation.ts | 35 +++++++++--- .../index.tsx | 3 ++ src/libs/Navigation/TransitionTracker.ts | 54 +++++++++++++++++++ src/libs/Navigation/helpers/linkTo/types.ts | 4 +- 5 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx create mode 100644 src/libs/Navigation/TransitionTracker.ts diff --git a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx b/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx new file mode 100644 index 0000000000000..55bec365582d0 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx @@ -0,0 +1,32 @@ +import type {ParamListBase, ScreenLayoutArgs} from '@react-navigation/native'; +import {StackNavigationOptions} from '@react-navigation/stack'; +import {useLayoutEffect} from 'react'; +import {PlatformStackNavigationOptions, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import {endTransition, startTransition} from '@libs/Navigation/TransitionTracker'; + +function InteractionManagerLayout({ + children, + navigation, + options, + route, +}: ScreenLayoutArgs>) { + useLayoutEffect(() => { + const transitionStartListener = navigation.addListener('transitionStart', () => { + console.log('transitionStart', route?.name); + // startTransition(); + }); + const transitionEndListener = navigation.addListener('transitionEnd', () => { + console.log('transitionEnd', route?.name); + // endTransition(); + }); + + return () => { + transitionStartListener(); + transitionEndListener(); + }; + }, [navigation, options.animation, route?.name]); + + return children; +} + +export default InteractionManagerLayout; diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index b1e897f7eb4e7..c6660119fcb29 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -4,7 +4,7 @@ import {CommonActions, getPathFromState, StackActions} from '@react-navigation/n import {Str} from 'expensify-common'; // eslint-disable-next-line you-dont-need-lodash-underscore/omit import omit from 'lodash/omit'; -import {DeviceEventEmitter, Dimensions, InteractionManager} from 'react-native'; +import {DeviceEventEmitter, Dimensions} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {Writable} from 'type-fest'; @@ -39,6 +39,7 @@ import setNavigationActionToMicrotaskQueue from './helpers/setNavigationActionTo import {linkingConfig} from './linkingConfig'; import {SPLIT_TO_SIDEBAR} from './linkingConfig/RELATIONS'; import navigationRef from './navigationRef'; +import {runAfterTransition} from './TransitionTracker'; import type { NavigationPartialRoute, NavigationRef, @@ -313,6 +314,10 @@ function navigate(route: Route, options?: LinkToOptions) { } linkTo(navigationRef.current, route, options); closeSidePanelOnNarrowScreen(); + + if (options?.afterTransition) { + runAfterTransition(options.afterTransition); + } } /** * When routes are compared to determine whether the fallback route passed to the goUp function is in the state, @@ -377,10 +382,13 @@ type GoBackOptions = { * In that case we want to goUp to a country picker with any params so we don't compare them. */ compareParams?: boolean; + // Callback to execute after the navigation transition animation completes. + afterTransition?: () => void | undefined; }; const defaultGoBackOptions: Required = { compareParams: true, + afterTransition: () => {}, }; /** @@ -455,6 +463,9 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { if (backToRoute) { goUp(backToRoute, options); + if (options?.afterTransition) { + runAfterTransition(options.afterTransition); + } return; } @@ -464,6 +475,9 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { } navigationRef.current?.goBack(); + if (options?.afterTransition) { + runAfterTransition(options.afterTransition); + } } /** @@ -696,13 +710,13 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * * @param options - Configuration object * @param options.ref - Navigation ref to use (defaults to navigationRef) - * @param options.callback - Optional callback to execute after the modal has finished closing. - * The callback fires when RightModalNavigator unmounts. + * @param options.callback - Optional callback to execute when the modal unmounts (fires on MODAL_EVENTS.CLOSED). + * @param options.afterTransition - Optional callback to execute after the navigation transition animation completes. * * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -const dismissModal = ({ref = navigationRef, callback}: {ref?: NavigationRef; callback?: () => void} = {}) => { +const dismissModal = ({ref = navigationRef, callback, afterTransition}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void} = {}) => { clearSelectedText(); isNavigationReady().then(() => { if (callback) { @@ -713,6 +727,10 @@ const dismissModal = ({ref = navigationRef, callback}: {ref?: NavigationRef; cal } ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + + if (afterTransition) { + runAfterTransition(afterTransition); + } }); }; @@ -743,10 +761,10 @@ const dismissModalWithReport = ({reportID, reportActionID, referrer, backTo}: Re navigate(reportRoute, {forceReplace: true}); return; } - dismissModal(); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - navigate(reportRoute); + dismissModal({ + afterTransition: () => { + navigate(reportRoute); + }, }); }); }; @@ -952,6 +970,7 @@ export default { getTopmostSuperWideRHPReportID, getTopmostSearchReportRouteParams, navigateBackToLastSuperWideRHPScreen, + runAfterTransition, }; export {navigationRef, getDeepestFocusedScreenName, isTwoFactorSetupScreen, shouldShowRequire2FAPage}; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index 730e269d507af..610ecd9556e78 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -13,6 +13,7 @@ import type { PlatformStackNavigatorProps, PlatformStackRouterOptions, } from '@libs/Navigation/PlatformStackNavigation/types'; +import InteractionManagerLayout from '@libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout'; function createPlatformStackNavigatorComponent( displayName: string, @@ -35,6 +36,7 @@ function createPlatformStackNavigatorComponent) { const { @@ -62,6 +64,7 @@ function createPlatformStackNavigatorComponent , }, convertToWebNavigationOptions, ); diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts new file mode 100644 index 0000000000000..65948cfa4a82a --- /dev/null +++ b/src/libs/Navigation/TransitionTracker.ts @@ -0,0 +1,54 @@ +type CancelHandle = {cancel: () => void}; + +let activeTransitionCount = 0; +const pendingCallbacks: Array<() => void> = []; + +function startTransition(): void { + activeTransitionCount++; +} + +function endTransition(): void { + activeTransitionCount = Math.max(0, activeTransitionCount - 1); + if (activeTransitionCount === 0) { + flushCallbacks(); + } +} + +function flushCallbacks(): void { + while (pendingCallbacks.length > 0) { + const cb = pendingCallbacks.shift(); + cb?.(); + } +} + +function runAfterTransition(callback: () => void): CancelHandle { + if (activeTransitionCount === 0) { + let cancelled = false; + queueMicrotask(() => { + if (!cancelled) { + callback(); + } + }); + return { + cancel: () => { + cancelled = true; + }, + }; + } + + pendingCallbacks.push(callback); + return { + cancel: () => { + const idx = pendingCallbacks.indexOf(callback); + if (idx !== -1) { + pendingCallbacks.splice(idx, 1); + } + }, + }; +} + +function isTransitioning(): boolean { + return activeTransitionCount > 0; +} + +export {startTransition, endTransition, runAfterTransition, isTransitioning}; diff --git a/src/libs/Navigation/helpers/linkTo/types.ts b/src/libs/Navigation/helpers/linkTo/types.ts index 26217f561b9e9..9c7bd4e1cc40a 100644 --- a/src/libs/Navigation/helpers/linkTo/types.ts +++ b/src/libs/Navigation/helpers/linkTo/types.ts @@ -10,7 +10,9 @@ type ActionPayload = { type LinkToOptions = { // To explicitly set the action type to replace. - forceReplace: boolean; + forceReplace?: boolean; + // Callback to execute after the navigation transition animation completes. + afterTransition?: () => void; }; export type {ActionPayload, LinkToOptions}; From e4d30a7944786039f4352f56ad22eff0acba085a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 15:56:31 +0100 Subject: [PATCH 02/40] Migrate dismissModal to TransitionTracker --- .../Navigators/InteractionManagerLayout.tsx | 14 +- src/libs/Navigation/Navigation.ts | 21 +-- src/libs/Navigation/TransitionTracker.ts | 131 +++++++++++++----- src/libs/actions/Report/index.ts | 2 +- src/pages/NewChatPage.tsx | 2 +- .../routes/TransactionReceiptModalContent.tsx | 2 +- .../WorkspaceInviteMessageComponent.tsx | 8 +- 7 files changed, 121 insertions(+), 59 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx b/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx index 55bec365582d0..4667680cb6855 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx @@ -1,8 +1,8 @@ import type {ParamListBase, ScreenLayoutArgs} from '@react-navigation/native'; -import {StackNavigationOptions} from '@react-navigation/stack'; +import type {StackNavigationOptions} from '@react-navigation/stack'; import {useLayoutEffect} from 'react'; -import {PlatformStackNavigationOptions, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import {endTransition, startTransition} from '@libs/Navigation/TransitionTracker'; +import type {PlatformStackNavigationOptions, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; function InteractionManagerLayout({ children, @@ -12,12 +12,12 @@ function InteractionManagerLayout({ }: ScreenLayoutArgs>) { useLayoutEffect(() => { const transitionStartListener = navigation.addListener('transitionStart', () => { - console.log('transitionStart', route?.name); - // startTransition(); + console.log('xdd transitionStart', route?.name); + TransitionTracker.startTransition('navigation'); }); const transitionEndListener = navigation.addListener('transitionEnd', () => { - console.log('transitionEnd', route?.name); - // endTransition(); + console.log('xdd transitionEnd', route?.name); + TransitionTracker.endTransition('navigation'); }); return () => { diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index c6660119fcb29..783b1579f75f5 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -39,7 +39,7 @@ import setNavigationActionToMicrotaskQueue from './helpers/setNavigationActionTo import {linkingConfig} from './linkingConfig'; import {SPLIT_TO_SIDEBAR} from './linkingConfig/RELATIONS'; import navigationRef from './navigationRef'; -import {runAfterTransition} from './TransitionTracker'; +import TransitionTracker from './TransitionTracker'; import type { NavigationPartialRoute, NavigationRef, @@ -316,7 +316,7 @@ function navigate(route: Route, options?: LinkToOptions) { closeSidePanelOnNarrowScreen(); if (options?.afterTransition) { - runAfterTransition(options.afterTransition); + TransitionTracker.runAfterTransitions(options.afterTransition); } } /** @@ -464,7 +464,7 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { if (backToRoute) { goUp(backToRoute, options); if (options?.afterTransition) { - runAfterTransition(options.afterTransition); + TransitionTracker.runAfterTransitions(options.afterTransition); } return; } @@ -476,7 +476,7 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { navigationRef.current?.goBack(); if (options?.afterTransition) { - runAfterTransition(options.afterTransition); + TransitionTracker.runAfterTransitions(options.afterTransition); } } @@ -710,26 +710,18 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * * @param options - Configuration object * @param options.ref - Navigation ref to use (defaults to navigationRef) - * @param options.callback - Optional callback to execute when the modal unmounts (fires on MODAL_EVENTS.CLOSED). * @param options.afterTransition - Optional callback to execute after the navigation transition animation completes. * * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -const dismissModal = ({ref = navigationRef, callback, afterTransition}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void} = {}) => { +const dismissModal = ({ref = navigationRef, afterTransition}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void} = {}) => { clearSelectedText(); isNavigationReady().then(() => { - if (callback) { - const subscription = DeviceEventEmitter.addListener(CONST.MODAL_EVENTS.CLOSED, () => { - subscription.remove(); - callback(); - }); - } - ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); if (afterTransition) { - runAfterTransition(afterTransition); + TransitionTracker.runAfterTransitions(afterTransition); } }); }; @@ -970,7 +962,6 @@ export default { getTopmostSuperWideRHPReportID, getTopmostSearchReportRouteParams, navigateBackToLastSuperWideRHPScreen, - runAfterTransition, }; export {navigationRef, getDeepestFocusedScreenName, isTwoFactorSetupScreen, shouldShowRequire2FAPage}; diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index 65948cfa4a82a..da17da924d313 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -1,45 +1,111 @@ type CancelHandle = {cancel: () => void}; -let activeTransitionCount = 0; -const pendingCallbacks: Array<() => void> = []; +type TransitionType = 'keyboard' | 'navigation' | 'modal' | 'focus'; -function startTransition(): void { - activeTransitionCount++; +type PendingEntry = {callback: () => void; type?: TransitionType}; + +const MAX_TRANSITION_DURATION_MS = 1000; + +const activeTransitions = new Map(); + +const activeTimeouts: Array<{type: TransitionType; timeout: ReturnType}> = []; + +let pendingCallbacks: PendingEntry[] = []; + +/** + * Invokes and removes pending callbacks. + * + * @param type - When provided, only flushes entries scoped to that type. + * When omitted, flushes all remaining entries. + */ +function flushCallbacks(type?: TransitionType): void { + const remaining: PendingEntry[] = []; + for (const entry of pendingCallbacks) { + if (type === undefined || entry.type === type) { + entry.callback(); + } else { + remaining.push(entry); + } + } + pendingCallbacks = remaining; } -function endTransition(): void { - activeTransitionCount = Math.max(0, activeTransitionCount - 1); - if (activeTransitionCount === 0) { +/** + * Decrements the active count for the given transition type and flushes matching callbacks. + * Shared by {@link endTransition} (manual) and the auto-timeout. + */ +function decrementAndFlush(type: TransitionType): void { + const current = activeTransitions.get(type) ?? 0; + const next = Math.max(0, current - 1); + + if (next === 0) { + activeTransitions.delete(type); + } else { + activeTransitions.set(type, next); + } + + // Flush callbacks scoped to this specific type + flushCallbacks(type); + + // When all transitions end, flush remaining unscoped callbacks + if (activeTransitions.size === 0) { flushCallbacks(); } } -function flushCallbacks(): void { - while (pendingCallbacks.length > 0) { - const cb = pendingCallbacks.shift(); - cb?.(); - } +/** + * Increments the active count for the given transition type. + * Multiple overlapping transitions of the same type are counted. + * Each transition automatically ends after {@link MAX_TRANSITION_DURATION_MS} as a safety net. + */ +function startTransition(type: TransitionType): void { + const current = activeTransitions.get(type) ?? 0; + activeTransitions.set(type, current + 1); + + const timeout = setTimeout(() => { + const idx = activeTimeouts.findIndex((entry) => entry.timeout === timeout); + if (idx !== -1) { + activeTimeouts.splice(idx, 1); + } + decrementAndFlush(type); + }, MAX_TRANSITION_DURATION_MS); + + activeTimeouts.push({type, timeout}); } -function runAfterTransition(callback: () => void): CancelHandle { - if (activeTransitionCount === 0) { - let cancelled = false; - queueMicrotask(() => { - if (!cancelled) { - callback(); - } - }); - return { - cancel: () => { - cancelled = true; - }, - }; +/** + * Decrements the active count for the given transition type. + * Clears the corresponding auto-timeout since the transition ended normally. + * When the count reaches zero, flushes callbacks scoped to that type. + * When all transition types are idle, flushes remaining unscoped callbacks. + */ +function endTransition(type: TransitionType): void { + // Clear the oldest timeout for this type (FIFO order matches startTransition order) + const timeoutIdx = activeTimeouts.findIndex((entry) => entry.type === type); + if (timeoutIdx !== -1) { + clearTimeout(activeTimeouts.at(timeoutIdx)?.timeout); + activeTimeouts.splice(timeoutIdx, 1); } - pendingCallbacks.push(callback); + decrementAndFlush(type); +} + +/** + * Schedules a callback to run after transitions complete. If no transitions are active + * (or the specified type is idle), the callback fires on the next microtask. + * + * @param callback - The function to invoke once transitions finish. + * @param type - Optional transition type to scope the wait. When provided, the callback + * fires as soon as that specific type finishes, even if other types are still active. + * When omitted, waits for all transition types to be idle. + * @returns A handle with a `cancel` method to prevent the callback from firing. + */ +function runAfterTransitions(callback: () => void, type?: TransitionType): CancelHandle { + const entry: PendingEntry = {callback, type}; + pendingCallbacks.push(entry); return { cancel: () => { - const idx = pendingCallbacks.indexOf(callback); + const idx = pendingCallbacks.indexOf(entry); if (idx !== -1) { pendingCallbacks.splice(idx, 1); } @@ -47,8 +113,11 @@ function runAfterTransition(callback: () => void): CancelHandle { }; } -function isTransitioning(): boolean { - return activeTransitionCount > 0; -} +const TransitionTracker = { + startTransition, + endTransition, + runAfterTransitions, +}; -export {startTransition, endTransition, runAfterTransition, isTransitioning}; +export default TransitionTracker; +export type {TransitionType, CancelHandle}; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index b0244a7a3ae78..53c12405893c7 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1630,7 +1630,7 @@ function navigateToAndOpenReport( if (shouldDismissModal) { Navigation.dismissModal({ - callback: () => { + afterTransition: () => { if (!report?.reportID) { return; } diff --git a/src/pages/NewChatPage.tsx b/src/pages/NewChatPage.tsx index f2c2661c408f6..82425bccfc65c 100755 --- a/src/pages/NewChatPage.tsx +++ b/src/pages/NewChatPage.tsx @@ -374,7 +374,7 @@ function NewChatPage({ref}: NewChatPageProps) { if (option?.reportID) { Navigation.dismissModal({ - callback: () => { + afterTransition: () => { Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option?.reportID)); }, }); diff --git a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx index accf772c046a6..1815711a9c56d 100644 --- a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx +++ b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx @@ -255,7 +255,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre text: translate('common.replace'), onSelected: () => { Navigation.dismissModal({ - callback: () => + afterTransition: () => Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( action ?? CONST.IOU.ACTION.EDIT, diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index 4f9151d5fa253..aa626a1979a92 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -173,7 +173,7 @@ function WorkspaceInviteMessageComponent({ } if ((backTo as string)?.endsWith('members')) { - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal()); + Navigation.dismissModal(); return; } @@ -182,8 +182,10 @@ function WorkspaceInviteMessageComponent({ return; } - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.dismissModal({callback: () => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID))}); + Navigation.dismissModal({ + afterTransition: () => { + return Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); + }, }); }; From 8d17a6d30004bf8ae665294be35368375f7dd23d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 16:07:42 +0100 Subject: [PATCH 03/40] Remove unused DeviceEventEmitter --- src/CONST/index.ts | 4 ---- src/components/Modal/BaseModal.tsx | 4 +--- .../AppNavigator/Navigators/RightModalNavigator.tsx | 6 ++---- src/libs/Navigation/Navigation.ts | 2 +- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d7c9c26401434..28f73c5ce2947 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7982,10 +7982,6 @@ const CONST = { ADD_EXPENSE_APPROVALS: 'addExpenseApprovals', }, - MODAL_EVENTS: { - CLOSED: 'modalClosed', - }, - LIST_BEHAVIOR: { REGULAR: 'regular', INVERTED: 'inverted', diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 3d5751b53dd45..41969094e153e 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -2,7 +2,7 @@ import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} fr import type {LayoutChangeEvent} from 'react-native'; // Animated required for side panel navigation // eslint-disable-next-line no-restricted-imports -import {Animated, DeviceEventEmitter, View} from 'react-native'; +import {Animated, View} from 'react-native'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import NavigationBar from '@components/NavigationBar'; import ScreenWrapperOfflineIndicatorContext from '@components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext'; @@ -167,8 +167,6 @@ function BaseModal({ [], ); - useEffect(() => () => DeviceEventEmitter.emit(CONST.MODAL_EVENTS.CLOSED), []); - const handleShowModal = useCallback(() => { if (shouldSetModalVisibility) { setModalVisibility(true, type); diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 4b27936a29b1c..17a5f82776215 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -1,8 +1,8 @@ import type {NavigatorScreenParams} from '@react-navigation/native'; import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useMemo, useRef} from 'react'; // eslint-disable-next-line no-restricted-imports -import {Animated, DeviceEventEmitter, InteractionManager} from 'react-native'; +import {Animated, InteractionManager} from 'react-native'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; import { animatedWideRHPWidth, @@ -168,8 +168,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { }, [syncRHPKeys, clearWideRHPKeysAfterTabChanged]), ); - useEffect(() => () => DeviceEventEmitter.emit(CONST.MODAL_EVENTS.CLOSED), []); - return ( diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 783b1579f75f5..078b194f1a6d4 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -4,7 +4,7 @@ import {CommonActions, getPathFromState, StackActions} from '@react-navigation/n import {Str} from 'expensify-common'; // eslint-disable-next-line you-dont-need-lodash-underscore/omit import omit from 'lodash/omit'; -import {DeviceEventEmitter, Dimensions} from 'react-native'; +import {Dimensions} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {Writable} from 'type-fest'; From bae4b6e74b5d482caa39368c297a466f3d9fec38 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 16:13:44 +0100 Subject: [PATCH 04/40] Refactor dismissModal and dismissModalWithReport to use async/await for improved readability and error handling --- src/libs/Navigation/Navigation.ts | 67 +++++++++++++++---------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 078b194f1a6d4..e332379815fc8 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -715,51 +715,50 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -const dismissModal = ({ref = navigationRef, afterTransition}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void} = {}) => { +async function dismissModal({ref = navigationRef, afterTransition}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void} = {}) { clearSelectedText(); - isNavigationReady().then(() => { - ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + await isNavigationReady(); - if (afterTransition) { - TransitionTracker.runAfterTransitions(afterTransition); - } - }); -}; + ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + + if (afterTransition) { + TransitionTracker.runAfterTransitions(afterTransition); + } +} /** * Dismisses the modal and opens the given report. * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -const dismissModalWithReport = ({reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef) => { - isNavigationReady().then(() => { - const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); - let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; +async function dismissModalWithReport({reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef) { + await isNavigationReady(); + const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); + let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; - if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { - dismissToSuperWideRHP(); - return; - } + if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { + dismissToSuperWideRHP(); + return; + } - const topmostReportID = getTopmostReportId(); - areReportsIDsDefined = !!topmostReportID && !!reportID; - const isReportsSplitTopmostFullScreen = ref.getRootState().routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; - if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { - dismissModal(); - return; - } - const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, referrer, backTo); - if (getIsNarrowLayout()) { - navigate(reportRoute, {forceReplace: true}); - return; - } - dismissModal({ - afterTransition: () => { - navigate(reportRoute); - }, - }); + const topmostReportID = getTopmostReportId(); + areReportsIDsDefined = !!topmostReportID && !!reportID; + const isReportsSplitTopmostFullScreen = ref.getRootState().routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; + if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { + dismissModal(); + return; + } + const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, referrer, backTo); + if (getIsNarrowLayout()) { + navigate(reportRoute, {forceReplace: true}); + return; + } + dismissModal({ + afterTransition: () => { + navigate(reportRoute); + }, }); -}; +} /** * Returns to the first screen in the stack, dismissing all the others, only if the global variable shouldPopToSidebar is set to true. From 4880c05713a7b28409828a90fa2fde7c5faa9a19 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 16:13:53 +0100 Subject: [PATCH 05/40] Refactor afterTransition callback in WorkspaceInviteMessageComponent for improved readability --- .../workspace/members/WorkspaceInviteMessageComponent.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index aa626a1979a92..7e2bcca5b326a 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -183,9 +183,7 @@ function WorkspaceInviteMessageComponent({ } Navigation.dismissModal({ - afterTransition: () => { - return Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); - }, + afterTransition: () => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)), }); }; From 4dceb994b8c33e67ce8474a0e924215d1da11439 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 16:30:45 +0100 Subject: [PATCH 06/40] Fix typecheck --- .../Navigators/InteractionManagerLayout.tsx | 2 -- .../createPlatformStackNavigatorComponent/index.tsx | 13 ++++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx b/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx index 4667680cb6855..ec3dc03798a78 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx @@ -12,11 +12,9 @@ function InteractionManagerLayout({ }: ScreenLayoutArgs>) { useLayoutEffect(() => { const transitionStartListener = navigation.addListener('transitionStart', () => { - console.log('xdd transitionStart', route?.name); TransitionTracker.startTransition('navigation'); }); const transitionEndListener = navigation.addListener('transitionEnd', () => { - console.log('xdd transitionEnd', route?.name); TransitionTracker.endTransition('navigation'); }); diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index 610ecd9556e78..bcf234113f439 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -1,6 +1,6 @@ -import type {ParamListBase, StackActionHelpers} from '@react-navigation/native'; +import type {ParamListBase, ScreenLayoutArgs, StackActionHelpers} from '@react-navigation/native'; import {StackRouter, useNavigationBuilder} from '@react-navigation/native'; -import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; +import type {StackNavigationEventMap, StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack'; import {StackView} from '@react-navigation/stack'; import React, {useMemo} from 'react'; import {addCustomHistoryRouterExtension} from '@libs/Navigation/AppNavigator/customHistory'; @@ -64,7 +64,14 @@ function createPlatformStackNavigatorComponent , + // eslint-disable-next-line react/no-unstable-nested-components + screenLayout: ({navigation: navigationFromScreenLayout, ...layoutProps}) => ( + } + /> + ), }, convertToWebNavigationOptions, ); From aab188b64c3ad58a9c55c7a4d42ad18e0634eda0 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 16:32:04 +0100 Subject: [PATCH 07/40] Replace InteractionManagerLayout with ScreenLayout --- .../ScreenLayout.tsx} | 6 +++--- .../createPlatformStackNavigatorComponent/index.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/libs/Navigation/{AppNavigator/Navigators/InteractionManagerLayout.tsx => PlatformStackNavigation/ScreenLayout.tsx} (88%) diff --git a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx similarity index 88% rename from src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx rename to src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx index ec3dc03798a78..17561a7450c55 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx @@ -1,10 +1,10 @@ import type {ParamListBase, ScreenLayoutArgs} from '@react-navigation/native'; import type {StackNavigationOptions} from '@react-navigation/stack'; import {useLayoutEffect} from 'react'; -import type {PlatformStackNavigationOptions, PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; +import type {PlatformStackNavigationOptions, PlatformStackNavigationProp} from './types'; -function InteractionManagerLayout({ +function ScreenLayout({ children, navigation, options, @@ -27,4 +27,4 @@ function InteractionManagerLayout({ return children; } -export default InteractionManagerLayout; +export default ScreenLayout; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index bcf234113f439..3349de27e2f03 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -1,4 +1,4 @@ -import type {ParamListBase, ScreenLayoutArgs, StackActionHelpers} from '@react-navigation/native'; +import type {ParamListBase, StackActionHelpers} from '@react-navigation/native'; import {StackRouter, useNavigationBuilder} from '@react-navigation/native'; import type {StackNavigationEventMap, StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack'; import {StackView} from '@react-navigation/stack'; @@ -13,7 +13,7 @@ import type { PlatformStackNavigatorProps, PlatformStackRouterOptions, } from '@libs/Navigation/PlatformStackNavigation/types'; -import InteractionManagerLayout from '@libs/Navigation/AppNavigator/Navigators/InteractionManagerLayout'; +import ScreenLayout from '@libs/Navigation/PlatformStackNavigation/ScreenLayout'; function createPlatformStackNavigatorComponent( displayName: string, @@ -66,7 +66,7 @@ function createPlatformStackNavigatorComponent ( - } From eef1d2bcbeac8428ec9063cd630fb98d3193d8a4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 16:44:18 +0100 Subject: [PATCH 08/40] Add TransitionTracker to ReanimatedModal --- src/components/Modal/ReanimatedModal/index.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/Modal/ReanimatedModal/index.tsx b/src/components/Modal/ReanimatedModal/index.tsx index d43aae9bc6046..f22adfb24ee47 100644 --- a/src/components/Modal/ReanimatedModal/index.tsx +++ b/src/components/Modal/ReanimatedModal/index.tsx @@ -11,6 +11,7 @@ import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import Backdrop from './Backdrop'; import Container from './Container'; import type ReanimatedModalProps from './types'; @@ -102,6 +103,7 @@ function ReanimatedModal({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.clearInteractionHandle(handleRef.current); } + TransitionTracker.endTransition('modal'); setIsVisibleState(false); setIsContainerOpen(false); @@ -114,6 +116,7 @@ function ReanimatedModal({ if (isVisible && !isContainerOpen && !isTransitioning) { // eslint-disable-next-line @typescript-eslint/no-deprecated handleRef.current = InteractionManager.createInteractionHandle(); + TransitionTracker.startTransition('modal'); onModalWillShow(); setIsVisibleState(true); @@ -121,6 +124,7 @@ function ReanimatedModal({ } else if (!isVisible && isContainerOpen && !isTransitioning) { // eslint-disable-next-line @typescript-eslint/no-deprecated handleRef.current = InteractionManager.createInteractionHandle(); + TransitionTracker.startTransition('modal'); onModalWillHide(); blurActiveElement(); @@ -141,6 +145,7 @@ function ReanimatedModal({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.clearInteractionHandle(handleRef.current); } + TransitionTracker.endTransition('modal'); onModalShow(); }, [onModalShow]); @@ -151,6 +156,7 @@ function ReanimatedModal({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.clearInteractionHandle(handleRef.current); } + TransitionTracker.endTransition('modal'); // Because on Android, the Modal's onDismiss callback does not work reliably. There's a reported issue at: // https://stackoverflow.com/questions/58937956/react-native-modal-ondismiss-not-invoked From a050b00e19f51ef25a47673f438418bf2c8f8f27 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 17:05:21 +0100 Subject: [PATCH 09/40] Refactor keyboard dismiss function to accept options for transition management --- src/components/EmojiPicker/EmojiPicker.tsx | 2 +- src/utils/keyboard/index.android.ts | 9 ++++++++- src/utils/keyboard/index.ts | 10 +++++++++- src/utils/keyboard/index.website.ts | 11 +++++++++-- src/utils/keyboard/types.ts | 7 +++++++ 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/utils/keyboard/types.ts diff --git a/src/components/EmojiPicker/EmojiPicker.tsx b/src/components/EmojiPicker/EmojiPicker.tsx index 1fec2354705b4..4a147f34c4d44 100644 --- a/src/components/EmojiPicker/EmojiPicker.tsx +++ b/src/components/EmojiPicker/EmojiPicker.tsx @@ -116,7 +116,7 @@ function EmojiPicker({viewportOffsetTop, ref}: EmojiPickerProps) { // It's possible that the anchor is inside an active modal (e.g., add emoji reaction in report context menu). // So, we need to get the anchor position first before closing the active modal which will also destroy the anchor. - KeyboardUtils.dismiss(true).then(() => + KeyboardUtils.dismiss({shouldSkipSafari: true}).then(() => calculateAnchorPosition(emojiPopoverAnchor?.current, anchorOriginValue).then((value) => { close(() => { onWillShow?.(); diff --git a/src/utils/keyboard/index.android.ts b/src/utils/keyboard/index.android.ts index b15d81367e302..b3460dd2317ef 100644 --- a/src/utils/keyboard/index.android.ts +++ b/src/utils/keyboard/index.android.ts @@ -1,5 +1,7 @@ import {Keyboard} from 'react-native'; import {KeyboardEvents} from 'react-native-keyboard-controller'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; +import type {DismissKeyboardOptions} from './types'; type SimplifiedKeyboardEvent = { height?: number; @@ -21,7 +23,7 @@ const subscribeKeyboardVisibilityChange = (cb: (isVisible: boolean) => void) => }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const dismiss = (shouldSkipSafari?: boolean): Promise => { +const dismiss = (options?: DismissKeyboardOptions): Promise => { return new Promise((resolve) => { if (!isVisible) { resolve(); @@ -31,10 +33,15 @@ const dismiss = (shouldSkipSafari?: boolean): Promise => { const subscription = Keyboard.addListener('keyboardDidHide', () => { resolve(); + TransitionTracker.endTransition('keyboard'); subscription.remove(); }); Keyboard.dismiss(); + TransitionTracker.startTransition('keyboard'); + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } }); }; diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts index 3ff8096680c62..4c0f1c03202e3 100644 --- a/src/utils/keyboard/index.ts +++ b/src/utils/keyboard/index.ts @@ -1,4 +1,6 @@ +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {Keyboard} from 'react-native'; +import type {DismissKeyboardOptions} from './types'; type SimplifiedKeyboardEvent = { height?: number; @@ -20,7 +22,7 @@ const subscribeKeyboardVisibilityChange = (cb: (isVisible: boolean) => void) => }; // eslint-disable-next-line @typescript-eslint/no-unused-vars -const dismiss = (shouldSkipSafari?: boolean): Promise => { +const dismiss = (options?: DismissKeyboardOptions): Promise => { return new Promise((resolve) => { if (!isVisible) { resolve(); @@ -30,10 +32,16 @@ const dismiss = (shouldSkipSafari?: boolean): Promise => { const subscription = Keyboard.addListener('keyboardDidHide', () => { resolve(); + TransitionTracker.endTransition('keyboard'); subscription.remove(); }); + TransitionTracker.startTransition('keyboard'); Keyboard.dismiss(); + + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } }); }; diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts index 3c40a2eced2ea..3c49a40873387 100644 --- a/src/utils/keyboard/index.website.ts +++ b/src/utils/keyboard/index.website.ts @@ -1,6 +1,8 @@ import {Keyboard} from 'react-native'; import {isMobile, isMobileSafari} from '@libs/Browser'; import CONST from '@src/CONST'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; +import type {DismissKeyboardOptions} from './types'; let isVisible = false; const initialViewportHeight = window?.visualViewport?.height; @@ -34,9 +36,9 @@ const handleResize = () => { window.visualViewport?.addEventListener('resize', handleResize); -const dismiss = (shouldSkipSafari = false): Promise => { +const dismiss = (options?: DismissKeyboardOptions): Promise => { return new Promise((resolve) => { - if (shouldSkipSafari && isMobileSafari()) { + if (options?.shouldSkipSafari && isMobileSafari()) { resolve(); return; } @@ -58,11 +60,16 @@ const dismiss = (shouldSkipSafari = false): Promise => { } window.visualViewport?.removeEventListener('resize', handleDismissResize); + TransitionTracker.endTransition('keyboard'); return resolve(); }; window.visualViewport?.addEventListener('resize', handleDismissResize); Keyboard.dismiss(); + TransitionTracker.startTransition('keyboard'); + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } }); }; diff --git a/src/utils/keyboard/types.ts b/src/utils/keyboard/types.ts new file mode 100644 index 0000000000000..374054755852c --- /dev/null +++ b/src/utils/keyboard/types.ts @@ -0,0 +1,7 @@ +type DismissKeyboardOptions = { + shouldSkipSafari?: boolean; + afterTransition?: () => void; +}; + +// eslint-disable-next-line import/prefer-default-export +export type {DismissKeyboardOptions}; From 8a670c9039e716f358479c5122b782804518d8d5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 17:22:53 +0100 Subject: [PATCH 10/40] Fix keyboard behaviour --- .../categories/WorkspaceCategoriesSettingsPage.tsx | 13 ++++++++----- src/pages/workspace/tags/WorkspaceCreateTagPage.tsx | 7 ++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 103b2251afe27..9afb88691ae0c 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -1,5 +1,5 @@ import React, {useCallback, useMemo, useState} from 'react'; -import {InteractionManager, Keyboard, View} from 'react-native'; +import {View} from 'react-native'; import CategorySelectorModal from '@components/CategorySelector/CategorySelectorModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -26,6 +26,7 @@ import {clearPolicyErrorField, setWorkspaceDefaultSpendCategory} from '@userActi import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import KeyboardUtils from '@src/utils/keyboard'; type WorkspaceCategoriesSettingsPageProps = WithPolicyConnectionsProps & ( @@ -80,10 +81,12 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet setWorkspaceDefaultSpendCategory(policyID, currentGroupID, selectedCategory.keyForList); } - Keyboard.dismiss(); - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - setIsSelectorModalVisible(false); + console.log('test: dismissing keyboard'); + KeyboardUtils.dismiss({ + afterTransition: () => { + console.log('test: after interactions'); + return setIsSelectorModalVisible(false); + }, }); }; diff --git a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx index ec0b14d9a995f..774e43e3aa52f 100644 --- a/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx +++ b/src/pages/workspace/tags/WorkspaceCreateTagPage.tsx @@ -1,5 +1,4 @@ import React, {useCallback} from 'react'; -import {Keyboard} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -24,6 +23,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceTagForm'; +import KeyboardUtils from '@src/utils/keyboard'; type WorkspaceCreateTagPageProps = | PlatformStackScreenProps @@ -70,8 +70,9 @@ function WorkspaceCreateTagPage({route}: WorkspaceCreateTagPageProps) { const createTag = useCallback( (values: FormOnyxValues) => { createPolicyTag(policyID, values.tagName.trim(), policyTags, setupTagsTaskReport, setupCategoriesAndTagsTaskReport, policyHasCustomCategories); - Keyboard.dismiss(); - Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined); + KeyboardUtils.dismiss({ + afterTransition: () => Navigation.goBack(isQuickSettingsFlow ? ROUTES.SETTINGS_TAGS_ROOT.getRoute(policyID, backTo) : undefined), + }); }, [policyID, policyTags, isQuickSettingsFlow, backTo, setupTagsTaskReport, setupCategoriesAndTagsTaskReport, policyHasCustomCategories], ); From 28033aee1d29c0ead5050692901bd9af3b088771 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 18:59:23 +0100 Subject: [PATCH 11/40] Remove unnecessary code from keyboard dismiss logic --- .../categories/WorkspaceCategoriesSettingsPage.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx index 9afb88691ae0c..e4a47219d7884 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesSettingsPage.tsx @@ -81,12 +81,8 @@ function WorkspaceCategoriesSettingsPage({policy, route}: WorkspaceCategoriesSet setWorkspaceDefaultSpendCategory(policyID, currentGroupID, selectedCategory.keyForList); } - console.log('test: dismissing keyboard'); KeyboardUtils.dismiss({ - afterTransition: () => { - console.log('test: after interactions'); - return setIsSelectorModalVisible(false); - }, + afterTransition: () => setIsSelectorModalVisible(false), }); }; From 4d7fd9b3a8458f69f08fedbfb6bd3f629b5cfc26 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 20:56:34 +0100 Subject: [PATCH 12/40] Cluster usages into first groups --- .../InputFocusManagement.md | 31 ++++++++++++++++ .../InteractionManager/TaskAssignee.md | 14 ++++++++ .../WorkspaceConfirmModal.md | 35 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/InputFocusManagement.md create mode 100644 contributingGuides/migrations/InteractionManager/TaskAssignee.md create mode 100644 contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md diff --git a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md new file mode 100644 index 0000000000000..dddab85933a59 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md @@ -0,0 +1,31 @@ +# Cluster 3: Input Focus Management (~16 usages) + +## Strategy + +**Skip for now — focus utilities not coded yet** + +Document each usage but mark as TODO. When the focus utility is implemented, it should follow the same pattern as keyboard (`TransitionTracker` called internally, expose `afterTransition` option). + +`TransitionTracker.runAfterTransitions()` should **never** be called directly in application code. + +## Usages + +| File | Line | Description | PR | +| -------------------------------------------- | ---- | ------------------------------------------ | ------------------------------------------------------ | +| `InputFocus/index.website.ts` | 25 | Focus composer after modal | [#60073](https://github.com/Expensify/App/pull/60073) | +| `focusEditAfterCancelDelete/index.native.ts` | 6 | Focus text input after cancel/delete | [#47780](https://github.com/Expensify/App/pull/47780) | +| `useRestoreInputFocus/index.android.ts` | 15 | `KeyboardController.setFocusTo('current')` | [#54187](https://github.com/Expensify/App/pull/54187) | +| `useAutoFocusInput.ts` | 37 | Auto-focus input after interactions | [#47780](https://github.com/Expensify/App/pull/47780) | +| `FormProvider.tsx` | 427 | Set blur state in Safari | [#55494](https://github.com/Expensify/App/pull/55494) | +| `ContactPermissionModal/index.native.tsx` | 41 | Permission + focus after modal | [#54459](https://github.com/Expensify/App/pull/54459) | +| `ContactPermissionModal/index.native.tsx` | 59 | Permission + focus after modal | [#64207](https://github.com/Expensify/App/pull/64207) | +| `SearchRouter.tsx` | 346 | Focus search input after route | [#65183](https://github.com/Expensify/App/pull/65183) | +| `ShareRootPage.tsx` | 162 | Focus input after tab animation | [#63741](https://github.com/Expensify/App/pull/63741) | +| `EmojiPickerMenu/index.native.tsx` | 51 | Focus emoji search input | [#52009](https://github.com/Expensify/App/pull/52009) | +| `ReportActionItemMessageEdit.tsx` | 291 | Focus composer | [#28238](https://github.com/Expensify/App/pull/28238) | +| `ReportActionItemMessageEdit.tsx` | 545 | Focus composer | [#42965](https://github.com/Expensify/App/pull/42965) | +| `ComposerWithSuggestions.tsx` | 594 | Focus composer | [#74921](https://github.com/Expensify/App/pull/74921) | +| `MoneyRequestConfirmationList.tsx` | 1071 | `blurActiveElement()` after confirm | [#45873](https://github.com/Expensify/App/pull/45873) | +| `SplitListItem.tsx` | 75 | Focus input after screen transition | [#77657](https://github.com/Expensify/App/pull/77657) | +| `ContactMethodDetailsPage.tsx` | 215 | Focus after modal hide | [#54784](https://github.com/Expensify/App/pull/54784) | +| `ContactMethodDetailsPage.tsx` | 279 | Focus on entry transition end | [#55588](https://github.com/Expensify/App/pull/55588) | diff --git a/contributingGuides/migrations/InteractionManager/TaskAssignee.md b/contributingGuides/migrations/InteractionManager/TaskAssignee.md new file mode 100644 index 0000000000000..f5b487812f4f0 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/TaskAssignee.md @@ -0,0 +1,14 @@ +# Cluster 16: Task Assignee (2 usages) + +## Strategy + +**Use `requestAnimationFrame`** + +Both usages defer navigation after synchronous Onyx state updates (`setAssigneeValue`, `editTaskAssignee`). The deferral gives the optimistic updates a frame to flush before navigating. Replace with `requestAnimationFrame`. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------- | ---- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------ | +| `TaskAssigneeSelectorModal.tsx` | 181 | Edit task assignee via `setAssigneeValue` + `editTaskAssignee`, then defer `dismissModalWithReport` | `requestAnimationFrame(() => Navigation.dismissModalWithReport({reportID}))` | [#81320](https://github.com/Expensify/App/pull/81320) | +| `TaskAssigneeSelectorModal.tsx` | 194 | Set assignee for new task via `setAssigneeValue`, then defer `goBack` to `NEW_TASK` route | `requestAnimationFrame(() => Navigation.goBack(ROUTES.NEW_TASK.getRoute(backTo)))` | [#81320](https://github.com/Expensify/App/pull/81320) | diff --git a/contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md b/contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md new file mode 100644 index 0000000000000..dedbee53b0711 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md @@ -0,0 +1,35 @@ +# Cluster 1: ConfirmModal Patterns + +## Strategy + +**ConfirmModal pattern (ref + afterTransition)** + +Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. + +For the few cases involving navigation (Plaid, NetSuite), use `Navigation.afterTransition` instead. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------------------- | ---- | ----------------------------------- | ---------------------------- | ----------------------------------------------------- | +| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | ConfirmModal afterTransition | [#54178](https://github.com/Expensify/App/pull/54178) | +| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `ReportParticipantsPage.tsx` | 446 | Same pattern | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | ConfirmModal afterTransition | [#56792](https://github.com/Expensify/App/pull/56792) | +| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | ConfirmModal afterTransition | [#82523](https://github.com/Expensify/App/pull/82523) | +| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | ConfirmModal afterTransition | [#74530](https://github.com/Expensify/App/pull/74530) | +| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | ConfirmModal afterTransition | [#57373](https://github.com/Expensify/App/pull/57373) | +| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | ConfirmModal afterTransition | [#56932](https://github.com/Expensify/App/pull/56932) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | ConfirmModal afterTransition | [#52163](https://github.com/Expensify/App/pull/52163) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | ConfirmModal afterTransition | [#48618](https://github.com/Expensify/App/pull/48618) | +| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | ConfirmModal afterTransition | [#69146](https://github.com/Expensify/App/pull/69146) | +| `PlaidConnectionStep.tsx` | 138 | Plaid connection navigation | Navigation afterTransition | [#64741](https://github.com/Expensify/App/pull/64741) | +| `NetSuiteImportAddCustomSegmentContent.tsx` | 51 | NetSuite segment navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | +| `NetSuiteImportAddCustomListContent.tsx` | 48 | NetSuite list navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | From 1a913e750b7c019027e7a55723879a0bd68ec96a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:05:40 +0100 Subject: [PATCH 13/40] Add migration guide for Settings Pages in InteractionManager --- .../InteractionManager/SettingsPages.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/SettingsPages.md diff --git a/contributingGuides/migrations/InteractionManager/SettingsPages.md b/contributingGuides/migrations/InteractionManager/SettingsPages.md new file mode 100644 index 0000000000000..5495fdb14f2df --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/SettingsPages.md @@ -0,0 +1,16 @@ +# Cluster 14: Settings Pages (~4 usages) + +## Strategy + +**Mixed — Navigation afterTransition, KeyboardUtils.dismiss** + +Settings pages combine navigation-dependent cleanup and keyboard dismissal patterns. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------ | ---- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `StatusPage.tsx` | 136 | `clearDraftCustomStatus()` + navigate | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition: () => clearDraft()})` | [#40364](https://github.com/Expensify/App/pull/40364) | +| `StatusPage.tsx` | 157 | Navigate back after status action | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition})` | [#40364](https://github.com/Expensify/App/pull/40364) | +| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | `KeyboardUtils.dismiss({afterTransition: () => setIsDeleteModalOpen(true)})` | [#35305](https://github.com/Expensify/App/pull/35305) | +| `TwoFactorAuth/VerifyPage.tsx` | 79 | 2FA verify navigation | Investigate if InteractionManager is needed | [#67762](https://github.com/Expensify/App/pull/67762) | From 2520f6744f626f869c3a54b5bccac97217a1a145 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:10:46 +0100 Subject: [PATCH 14/40] Add migration guide for Search API operations in InteractionManager --- .../InteractionManager/SearchAPIOperations.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/SearchAPIOperations.md diff --git a/contributingGuides/migrations/InteractionManager/SearchAPIOperations.md b/contributingGuides/migrations/InteractionManager/SearchAPIOperations.md new file mode 100644 index 0000000000000..3c85f85ebf0dc --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/SearchAPIOperations.md @@ -0,0 +1,15 @@ +# Cluster 9: Search API Operations (~3 usages) + +## Strategy + +**Use `requestIdleCallback`** + +Search API calls and contact imports are non-urgent background work that should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. + +## Usages + +| File | Line | Current | Migration | PR | +| -------------------------------------- | ---- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `useSearchHighlightAndScroll.ts` | 126 | InteractionManager deferring search API call | Try to use `requestIdleCallback(() => search(...))` or startTransition to defer the search API call | [#69713](https://github.com/Expensify/App/pull/69713) | +| `useSearchSelector.native.ts` | 27 | InteractionManager deferring contact import | Try to use `requestIdleCallback(importAndSaveContacts)` or startTransition to defer the contact import | [#70700](https://github.com/Expensify/App/pull/70700) | +| `MoneyRequestParticipantsSelector.tsx` | 431 | InteractionManager deferring contact import | InteractionManager was used as a workaround to defer the contact import, now use `requestIdleCallback(importAndSaveContacts)` or startTransition to defer the contact import | [#54459](https://github.com/Expensify/App/pull/54459) | From a6a26edd2f55d68a168c8a1b097561c11cd4b358 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:32:00 +0100 Subject: [PATCH 15/40] Update InputFocusManagement documentation to include additional usages and improve formatting --- .../InputFocusManagement.md | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md index dddab85933a59..3b35e548438ff 100644 --- a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md +++ b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md @@ -10,22 +10,23 @@ Document each usage but mark as TODO. When the focus utility is implemented, it ## Usages -| File | Line | Description | PR | -| -------------------------------------------- | ---- | ------------------------------------------ | ------------------------------------------------------ | -| `InputFocus/index.website.ts` | 25 | Focus composer after modal | [#60073](https://github.com/Expensify/App/pull/60073) | -| `focusEditAfterCancelDelete/index.native.ts` | 6 | Focus text input after cancel/delete | [#47780](https://github.com/Expensify/App/pull/47780) | -| `useRestoreInputFocus/index.android.ts` | 15 | `KeyboardController.setFocusTo('current')` | [#54187](https://github.com/Expensify/App/pull/54187) | -| `useAutoFocusInput.ts` | 37 | Auto-focus input after interactions | [#47780](https://github.com/Expensify/App/pull/47780) | -| `FormProvider.tsx` | 427 | Set blur state in Safari | [#55494](https://github.com/Expensify/App/pull/55494) | -| `ContactPermissionModal/index.native.tsx` | 41 | Permission + focus after modal | [#54459](https://github.com/Expensify/App/pull/54459) | -| `ContactPermissionModal/index.native.tsx` | 59 | Permission + focus after modal | [#64207](https://github.com/Expensify/App/pull/64207) | -| `SearchRouter.tsx` | 346 | Focus search input after route | [#65183](https://github.com/Expensify/App/pull/65183) | -| `ShareRootPage.tsx` | 162 | Focus input after tab animation | [#63741](https://github.com/Expensify/App/pull/63741) | -| `EmojiPickerMenu/index.native.tsx` | 51 | Focus emoji search input | [#52009](https://github.com/Expensify/App/pull/52009) | -| `ReportActionItemMessageEdit.tsx` | 291 | Focus composer | [#28238](https://github.com/Expensify/App/pull/28238) | -| `ReportActionItemMessageEdit.tsx` | 545 | Focus composer | [#42965](https://github.com/Expensify/App/pull/42965) | -| `ComposerWithSuggestions.tsx` | 594 | Focus composer | [#74921](https://github.com/Expensify/App/pull/74921) | -| `MoneyRequestConfirmationList.tsx` | 1071 | `blurActiveElement()` after confirm | [#45873](https://github.com/Expensify/App/pull/45873) | -| `SplitListItem.tsx` | 75 | Focus input after screen transition | [#77657](https://github.com/Expensify/App/pull/77657) | -| `ContactMethodDetailsPage.tsx` | 215 | Focus after modal hide | [#54784](https://github.com/Expensify/App/pull/54784) | -| `ContactMethodDetailsPage.tsx` | 279 | Focus on entry transition end | [#55588](https://github.com/Expensify/App/pull/55588) | +| File | Line | Description | PR | +| -------------------------------------------- | ---- | --------------------------------------------- | ----------------------------------------------------- | +| `InputFocus/index.website.ts` | 25 | Focus composer after modal | [#60073](https://github.com/Expensify/App/pull/60073) | +| `focusEditAfterCancelDelete/index.native.ts` | 6 | Focus text input after cancel/delete | [#47780](https://github.com/Expensify/App/pull/47780) | +| `useRestoreInputFocus/index.android.ts` | 15 | `KeyboardController.setFocusTo('current')` | [#54187](https://github.com/Expensify/App/pull/54187) | +| `useAutoFocusInput.ts` | 37 | Auto-focus input after interactions | [#47780](https://github.com/Expensify/App/pull/47780) | +| `FormProvider.tsx` | 427 | Set blur state in Safari | [#55494](https://github.com/Expensify/App/pull/55494) | +| `ContactPermissionModal/index.native.tsx` | 41 | Permission + focus after modal | [#54459](https://github.com/Expensify/App/pull/54459) | +| `ContactPermissionModal/index.native.tsx` | 59 | Permission + focus after modal | [#64207](https://github.com/Expensify/App/pull/64207) | +| `SearchRouter.tsx` | 346 | Focus search input after route | [#65183](https://github.com/Expensify/App/pull/65183) | +| `ShareRootPage.tsx` | 162 | Focus input after tab animation | [#63741](https://github.com/Expensify/App/pull/63741) | +| `EmojiPickerMenu/index.native.tsx` | 51 | Focus emoji search input | [#52009](https://github.com/Expensify/App/pull/52009) | +| `ReportActionItemMessageEdit.tsx` | 291 | Focus composer | [#28238](https://github.com/Expensify/App/pull/28238) | +| `ReportActionItemMessageEdit.tsx` | 545 | Focus composer | [#42965](https://github.com/Expensify/App/pull/42965) | +| `ComposerWithSuggestions.tsx` | 594 | Focus composer | [#74921](https://github.com/Expensify/App/pull/74921) | +| `MoneyRequestConfirmationList.tsx` | 1071 | `blurActiveElement()` after confirm | [#45873](https://github.com/Expensify/App/pull/45873) | +| `SplitListItem.tsx` | 75 | Focus input after screen transition | [#77657](https://github.com/Expensify/App/pull/77657) | +| `ContactMethodDetailsPage.tsx` | 215 | Focus after modal hide | [#54784](https://github.com/Expensify/App/pull/54784) | +| `ContactMethodDetailsPage.tsx` | 279 | Focus on entry transition end | [#55588](https://github.com/Expensify/App/pull/55588) | +| `BaseLoginForm.tsx` | 221 | InteractionManager deferring login navigation | [#47780](https://github.com/Expensify/App/pull/47780) | From 556c13f04297a1bf51c90c1c3a31f5f39e428a4c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:35:45 +0100 Subject: [PATCH 16/40] Add migration guide for Onboarding Tours in InteractionManager --- .../InteractionManager/OnboardingTours.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/OnboardingTours.md diff --git a/contributingGuides/migrations/InteractionManager/OnboardingTours.md b/contributingGuides/migrations/InteractionManager/OnboardingTours.md new file mode 100644 index 0000000000000..ce22a7923860e --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/OnboardingTours.md @@ -0,0 +1,14 @@ +# Cluster 8: Onboarding Tours (~3 usages) + +## Strategy + +**Use `Navigation.navigate({afterTransition})`** + +All onboarding usages defer navigation to the next onboarding step until after the current screen transition completes. Replace with the `afterTransition` option on `Navigation.navigate`. + +## Usages + +| File | Line | Current | Migration | PR | +| -------------------------------------- | ---- | ---------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | +| `useOnboardingFlow.ts` | 58 | InteractionManager deferring onboarding start | Use TransitionTracker.runAfterTransitions | [#77874](https://github.com/Expensify/App/pull/77874) | +| `BaseOnboardingInterestedFeatures.tsx` | 217 | InteractionManager deferring feature step navigation | Add afterTransition to navigateAfterOnboardingWithMicrotaskQueue | [#79122](https://github.com/Expensify/App/pull/79122) | From 4c33b650930268b7d2bf47ce4bd2e7a73b5964ca Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:38:29 +0100 Subject: [PATCH 17/40] Add migration guide for Files Validation in InteractionManager --- .../InteractionManager/FilesValidation.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/FilesValidation.md diff --git a/contributingGuides/migrations/InteractionManager/FilesValidation.md b/contributingGuides/migrations/InteractionManager/FilesValidation.md new file mode 100644 index 0000000000000..14d6b5ae6e92b --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/FilesValidation.md @@ -0,0 +1,14 @@ +# Cluster 17: Files Validation (2 usages) + +## Strategy + +**ConfirmModal pattern (ref + afterTransition)** + +User confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------ | ---- | -------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | +| `useFilesValidation.tsx` | 108 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | +| `useFilesValidation.tsx` | 365 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | From 7018507d3f0abcc95211cf9f14061fb0ea89be29 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:41:29 +0100 Subject: [PATCH 18/40] Add migration guide for Execution Control in InteractionManager --- .../InteractionManager/ExecutionControl.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/ExecutionControl.md diff --git a/contributingGuides/migrations/InteractionManager/ExecutionControl.md b/contributingGuides/migrations/InteractionManager/ExecutionControl.md new file mode 100644 index 0000000000000..b4fad9d207df7 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/ExecutionControl.md @@ -0,0 +1,14 @@ +# Cluster 7: Execution Control (~3 usages) + +## Strategy + +**Case-by-case** + +These usages control execution timing for UI state resets and navigation animation detection. Each requires a different primitive. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------------ | ---- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `useSingleExecution/index.native.ts` | 27 | InteractionManager resetting `isExecuting` | Replace with `requestAnimationFrame(() => setIsExecuting(false))` — this just needs to yield to allow UI updates before resetting state, if it doesnt work use TransitionTracker.runAfterTransitions | [#47780](https://github.com/Expensify/App/pull/47780) | +| `TopLevelNavigationTabBar/index.tsx` | 54 | InteractionManager detecting animation finish | use TransitionTracker.runAfterTransitions | [#49539](https://github.com/Expensify/App/pull/49539) | From e7bf207892925454e29fbf6536bfebcc07c18e8d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:42:38 +0100 Subject: [PATCH 19/40] Add migration guide for Realtime Subscriptions in InteractionManager --- .../RealtimeSubscriptions.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md diff --git a/contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md b/contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md new file mode 100644 index 0000000000000..fa8aaba77b554 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md @@ -0,0 +1,18 @@ +# Cluster 5: Realtime Subscriptions (~6 usages) + +## Strategy + +**Use `requestIdleCallback`** + +Pusher subscriptions and typing event listeners are non-urgent background work. They should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. + +## Usages + +| File | Line | Current | Migration | PR | +| ----------------------------- | ---- | ------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------- | +| `Pusher/index.ts` | 206 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#53751](https://github.com/Expensify/App/pull/53751) | +| `Pusher/index.native.ts` | 211 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#56610](https://github.com/Expensify/App/pull/56610) | +| `UserTypingEventListener.tsx` | 35 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#47780](https://github.com/Expensify/App/pull/47780) | +| `UserTypingEventListener.tsx` | 49 | Store ref for InteractionManager handle | Update to store `requestIdleCallback` handle ref | [#47780](https://github.com/Expensify/App/pull/47780) | +| `UserTypingEventListener.tsx` | 59 | InteractionManager wrapping subscribe | Replace with `requestIdleCallback(() => subscribe())` | [#47780](https://github.com/Expensify/App/pull/47780) | +| `UserTypingEventListener.tsx` | 70 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#47780](https://github.com/Expensify/App/pull/47780) | From 0d81c53fe5a999fbd18ffd9e0d2e2f39a0f7f73f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:44:05 +0100 Subject: [PATCH 20/40] Add migration guide for Navigate After Focus in InteractionManager --- .../InteractionManager/NavigateAfterFocus.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md diff --git a/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md b/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md new file mode 100644 index 0000000000000..b69f872886dd0 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md @@ -0,0 +1,16 @@ +# Cluster 18: Navigate After Focus (Skipped for now) + +## Strategy + +**Skip — address separately** + +These usages involve `useFocusEffect` combined with navigation, which requires a different approach that hasn't been designed yet. They should be addressed in a separate effort. + +## Usages + +| File | Line | Description | PR | +| ------------------------- | ------------ | -------------------------------------------------------- | ----------------------------------------------------- | +| `AccountDetailsPage.tsx` | 87 | Navigate after `useFocusEffect` triggers | [#59911](https://github.com/Expensify/App/pull/59911) | +| `AccountDetailsPage.tsx` | 116 | Navigate after `useFocusEffect` triggers | [#65834](https://github.com/Expensify/App/pull/65834) | +| `AccountValidatePage.tsx` | 128 | Navigate after `useFocusEffect` triggers | [#79597](https://github.com/Expensify/App/pull/79597) | +| `Report/index.ts` | 5910 | `navigateToTrainingModal()` — focus-dependent navigation | [#66890](https://github.com/Expensify/App/pull/66890) | From eb133d730bc8eab542f4132f561a42558abea2bb Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Feb 2026 21:46:23 +0100 Subject: [PATCH 21/40] Add migration guide for Performance and App Lifecycle in InteractionManager --- .../PerformanceAppLifecycle.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md diff --git a/contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md b/contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md new file mode 100644 index 0000000000000..d3b18de1014c7 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md @@ -0,0 +1,16 @@ +# Cluster 6: Performance / App Lifecycle (~6 usages) + +## Strategy + +**Use `TransitionTracker.runAfterTransitions`** + +Performance measurements, splash screen timing, Lottie animation gating, and background image loading are all non-urgent work that should not block the main thread during transitions. + +## Usages + +| File | Line | Current | Migration | PR | +| ---------------------------------- | ---- | -------------------------------------------- | ----------------------------------------- | ----------------------------------------------------- | +| `Performance.tsx` | 49 | InteractionManager wrapping TTI measurement | use TransitionTracker.runAfterTransitions | [#54412](https://github.com/Expensify/App/pull/54412) | +| `Lottie/index.tsx` | 44 | InteractionManager gating Lottie rendering | use TransitionTracker.runAfterTransitions | [#48143](https://github.com/Expensify/App/pull/48143) | +| `BackgroundImage/index.native.tsx` | 38 | InteractionManager deferring background load | use TransitionTracker.runAfterTransitions | [#48143](https://github.com/Expensify/App/pull/48143) | +| `BackgroundImage/index.tsx` | 38 | InteractionManager deferring background load | use TransitionTracker.runAfterTransitions | [#48143](https://github.com/Expensify/App/pull/48143) | From 2163e79ecbd6853a6bdf87fd4d2834e62e57a30d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 18 Feb 2026 18:26:24 +0100 Subject: [PATCH 22/40] Add migration guides for various InteractionManager patterns --- .../InteractionManager/ExecutionControl.md | 14 -- .../InteractionManager/FilesValidation.md | 2 +- .../InputFocusManagement.md | 8 +- .../KeyboardDismissAfterTransition.md | 21 +++ .../ModalAfterTransition.md | 49 +++++ .../InteractionManager/NavigateAfterFocus.md | 2 +- .../NavigationAfterTransition.md | 170 ++++++++++++++++++ .../InteractionManager/NeedsInvestigation.md | 14 ++ .../InteractionManager/OnboardingTours.md | 2 +- .../PerformanceAppLifecycle.md | 16 -- .../RealtimeSubscriptions.md | 18 -- .../RequestAnimationFrame.md | 18 ++ .../InteractionManager/RequestIdleCallback.md | 30 ++++ .../InteractionManager/ScrollOperations.md | 21 +++ .../InteractionManager/SearchAPIOperations.md | 15 -- .../InteractionManager/SettingsPages.md | 16 -- .../InteractionManager/TaskAssignee.md | 14 -- .../TransitionTrackerDirect.md | 17 ++ .../WorkspaceConfirmModal.md | 35 ---- 19 files changed, 347 insertions(+), 135 deletions(-) delete mode 100644 contributingGuides/migrations/InteractionManager/ExecutionControl.md create mode 100644 contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md create mode 100644 contributingGuides/migrations/InteractionManager/ModalAfterTransition.md create mode 100644 contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md create mode 100644 contributingGuides/migrations/InteractionManager/NeedsInvestigation.md delete mode 100644 contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md delete mode 100644 contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md create mode 100644 contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md create mode 100644 contributingGuides/migrations/InteractionManager/RequestIdleCallback.md create mode 100644 contributingGuides/migrations/InteractionManager/ScrollOperations.md delete mode 100644 contributingGuides/migrations/InteractionManager/SearchAPIOperations.md delete mode 100644 contributingGuides/migrations/InteractionManager/SettingsPages.md delete mode 100644 contributingGuides/migrations/InteractionManager/TaskAssignee.md create mode 100644 contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md delete mode 100644 contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md diff --git a/contributingGuides/migrations/InteractionManager/ExecutionControl.md b/contributingGuides/migrations/InteractionManager/ExecutionControl.md deleted file mode 100644 index b4fad9d207df7..0000000000000 --- a/contributingGuides/migrations/InteractionManager/ExecutionControl.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cluster 7: Execution Control (~3 usages) - -## Strategy - -**Case-by-case** - -These usages control execution timing for UI state resets and navigation animation detection. Each requires a different primitive. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------------ | ---- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `useSingleExecution/index.native.ts` | 27 | InteractionManager resetting `isExecuting` | Replace with `requestAnimationFrame(() => setIsExecuting(false))` — this just needs to yield to allow UI updates before resetting state, if it doesnt work use TransitionTracker.runAfterTransitions | [#47780](https://github.com/Expensify/App/pull/47780) | -| `TopLevelNavigationTabBar/index.tsx` | 54 | InteractionManager detecting animation finish | use TransitionTracker.runAfterTransitions | [#49539](https://github.com/Expensify/App/pull/49539) | diff --git a/contributingGuides/migrations/InteractionManager/FilesValidation.md b/contributingGuides/migrations/InteractionManager/FilesValidation.md index 14d6b5ae6e92b..0e77a88a6ccbf 100644 --- a/contributingGuides/migrations/InteractionManager/FilesValidation.md +++ b/contributingGuides/migrations/InteractionManager/FilesValidation.md @@ -1,4 +1,4 @@ -# Cluster 17: Files Validation (2 usages) +# Files Validation (2 usages) ## Strategy diff --git a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md index 3b35e548438ff..26a98626ade3b 100644 --- a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md +++ b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md @@ -1,4 +1,4 @@ -# Cluster 3: Input Focus Management (~16 usages) +# Input Focus Management (~16 usages) ## Strategy @@ -13,9 +13,9 @@ Document each usage but mark as TODO. When the focus utility is implemented, it | File | Line | Description | PR | | -------------------------------------------- | ---- | --------------------------------------------- | ----------------------------------------------------- | | `InputFocus/index.website.ts` | 25 | Focus composer after modal | [#60073](https://github.com/Expensify/App/pull/60073) | -| `focusEditAfterCancelDelete/index.native.ts` | 6 | Focus text input after cancel/delete | [#47780](https://github.com/Expensify/App/pull/47780) | +| `focusEditAfterCancelDelete/index.native.ts` | 6 | Focus text input after cancel/delete | [#36195](https://github.com/Expensify/App/pull/36195) | | `useRestoreInputFocus/index.android.ts` | 15 | `KeyboardController.setFocusTo('current')` | [#54187](https://github.com/Expensify/App/pull/54187) | -| `useAutoFocusInput.ts` | 37 | Auto-focus input after interactions | [#47780](https://github.com/Expensify/App/pull/47780) | +| `useAutoFocusInput.ts` | 37 | Auto-focus input after interactions | [#31063](https://github.com/Expensify/App/pull/31063) | | `FormProvider.tsx` | 427 | Set blur state in Safari | [#55494](https://github.com/Expensify/App/pull/55494) | | `ContactPermissionModal/index.native.tsx` | 41 | Permission + focus after modal | [#54459](https://github.com/Expensify/App/pull/54459) | | `ContactPermissionModal/index.native.tsx` | 59 | Permission + focus after modal | [#64207](https://github.com/Expensify/App/pull/64207) | @@ -29,4 +29,4 @@ Document each usage but mark as TODO. When the focus utility is implemented, it | `SplitListItem.tsx` | 75 | Focus input after screen transition | [#77657](https://github.com/Expensify/App/pull/77657) | | `ContactMethodDetailsPage.tsx` | 215 | Focus after modal hide | [#54784](https://github.com/Expensify/App/pull/54784) | | `ContactMethodDetailsPage.tsx` | 279 | Focus on entry transition end | [#55588](https://github.com/Expensify/App/pull/55588) | -| `BaseLoginForm.tsx` | 221 | InteractionManager deferring login navigation | [#47780](https://github.com/Expensify/App/pull/47780) | +| `BaseLoginForm.tsx` | 221 | InteractionManager deferring login navigation | [#42603](https://github.com/Expensify/App/pull/42603) | diff --git a/contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md b/contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md new file mode 100644 index 0000000000000..93e38c051261d --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md @@ -0,0 +1,21 @@ +# Keyboard Dismiss After Transition (~7 usages) + +## Strategy + +**Use `KeyboardUtils.dismiss({afterTransition})`** + +These usages dismiss the keyboard and then perform an action (usually navigation) after the keyboard close animation completes. Replace with `KeyboardUtils.dismiss({afterTransition: callback})`. + +`TransitionTracker.runAfterTransitions()` should **never** be called directly — it is already wired into `KeyboardUtils` internally. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------ | ---- | ------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------- | +| `IOURequestStepDestination.tsx` | 201 | Keyboard dismiss + navigate | `KeyboardUtils.dismiss({afterTransition: () => Navigation.goBack()})` | [#66747](https://github.com/Expensify/App/pull/66747) | +| `IOURequestStepCategory.tsx` | 210 | Category selection + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#53316](https://github.com/Expensify/App/pull/53316) | +| `IOURequestStepSubrate.tsx` | 234 | Subrate selection + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#56347](https://github.com/Expensify/App/pull/56347) | +| `IOURequestStepDescription.tsx` | 226 | Description submit + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#67010](https://github.com/Expensify/App/pull/67010) | +| `IOURequestStepMerchant.tsx` | 181 | Merchant submit + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#67010](https://github.com/Expensify/App/pull/67010) | +| `NewTaskPage.tsx` | 63 | `blurActiveElement()` on focus | `KeyboardUtils.dismiss({afterTransition})` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | `KeyboardUtils.dismiss({afterTransition: () => setIsDeleteModalOpen(true)})` | [#35305](https://github.com/Expensify/App/pull/35305) | diff --git a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md new file mode 100644 index 0000000000000..7cefccee50b83 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md @@ -0,0 +1,49 @@ +# Modal After Transition (~24 usages) + +## Strategy + +**ConfirmModal ref + `afterTransition` AND ReanimatedModal `afterTransition`** + +Two modal patterns for deferring work until after a modal close animation completes. + +--- + +## 1. ConfirmModal Pattern (ref + afterTransition) + +Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. + +| File | Line | Current | Migration | PR | +| ----------------------------------------- | ---- | ----------------------------------- | ---------------------------- | ----------------------------------------------------- | +| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | ConfirmModal afterTransition | [#54178](https://github.com/Expensify/App/pull/54178) | +| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `ReportParticipantsPage.tsx` | 446 | Same pattern | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | ConfirmModal afterTransition | [#56792](https://github.com/Expensify/App/pull/56792) | +| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | ConfirmModal afterTransition | [#82523](https://github.com/Expensify/App/pull/82523) | +| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | ConfirmModal afterTransition | [#74530](https://github.com/Expensify/App/pull/74530) | +| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | ConfirmModal afterTransition | [#57373](https://github.com/Expensify/App/pull/57373) | +| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | ConfirmModal afterTransition | [#56932](https://github.com/Expensify/App/pull/56932) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | ConfirmModal afterTransition | [#52163](https://github.com/Expensify/App/pull/52163) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | ConfirmModal afterTransition | [#48618](https://github.com/Expensify/App/pull/48618) | +| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | ConfirmModal afterTransition | [#69146](https://github.com/Expensify/App/pull/69146) | +| `PopoverReportActionContextMenu.tsx` | 374 | `deleteReportComment()` after confirm | ConfirmModal ref + afterTransition | [#66791](https://github.com/Expensify/App/pull/66791) | +| `AutoSubmitModal.tsx` | 43 | `dismissASAPSubmitExplanation()` on modal close | Use modal's `onModalHide` with afterTransition (ConfirmModal pattern) | [#63893](https://github.com/Expensify/App/pull/63893) | + +--- + +## 2. ReanimatedModal Pattern (afterTransition callback) + +These usages involve ReanimatedModal's built-in `afterTransition` callback to defer work until after the modal close animation. + +| File | Line | Current | Migration | PR | +| ------------------------------------- | ---- | --------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | +| `FeatureTrainingModal.tsx` | 224 | `setIsModalVisible` based on disabled state | Use ReanimatedModal's afterTransition | [#57649](https://github.com/Expensify/App/pull/57649) | +| `FeatureTrainingModal.tsx` | 352 | goBack + onClose after setting invisible | Use modal's afterTransition callback | [#53225](https://github.com/Expensify/App/pull/53225) | +| `AvatarCropModal/AvatarCropModal.tsx` | 324 | InteractionManager deferring onClose | Use modal's `afterTransition` callback (ReanimatedModal pattern) | [#66890](https://github.com/Expensify/App/pull/66890) | +| `AttachmentModalHandler/index.ios.ts` | 12 | iOS: execute close callback after interaction | Use ReanimatedModal's `afterTransition` on the attachment modal | [#53108](https://github.com/Expensify/App/pull/53108) | diff --git a/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md b/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md index b69f872886dd0..86643bd6c2f3f 100644 --- a/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md +++ b/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md @@ -1,4 +1,4 @@ -# Cluster 18: Navigate After Focus (Skipped for now) +# Navigate After Focus (Skipped for now) ## Strategy diff --git a/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md b/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md new file mode 100644 index 0000000000000..449c52b562a14 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md @@ -0,0 +1,170 @@ +# Navigation After Transition (~60 usages) + +## Strategy + +**Use `Navigation.navigate/goBack/dismissModal({afterTransition: callback})`** + +Every usage in this file defers work until after a screen/modal transition completes. Replace each `InteractionManager.runAfterInteractions()` call with the `afterTransition` option available on Navigation methods. + +`TransitionTracker.runAfterTransitions()` should **never** be called directly — it is already wired into `Navigation.ts` internally. + +--- + +## 1. Navigation Utility Files + +| File | Line | Current | Migration | PR | +| --------------------------------------- | ---------- | -------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `navigateAfterInteraction/index.ios.ts` | 10 | iOS wrapper deferring navigation | Replace with `Navigation.navigate({afterTransition})` — remove this file entirely | [#56865](https://github.com/Expensify/App/pull/56865) | +| `navigateAfterInteraction/index.ts` | 1 | Non-iOS passthrough | Remove file (merged into Navigation API) | [#56865](https://github.com/Expensify/App/pull/56865) | + +--- + +## 2. Direct Navigation Calls + +| File | Line | Current | Migration | PR | +| ----------------------------------------- | ---------- | -------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------- | +| `SubmitDetailsPage.tsx` | 263 | `navigateAfterInteraction` call | Use `Navigation.navigate(route, {afterTransition})` | [#58834](https://github.com/Expensify/App/pull/58834) | +| `TestToolsModalPage.tsx` | 73 | `navigateAfterInteraction` call | Use `Navigation.navigate(route, {afterTransition})` | [#64717](https://github.com/Expensify/App/pull/64717) | +| `IOURequestStepConfirmation.tsx` | 1390, 1396 | `navigateAfterInteraction` calls | Use `Navigation.navigate(route, {afterTransition})` | [#58422](https://github.com/Expensify/App/pull/58422) | +| `DiscardChangesConfirmation/index.tsx` | 32, 62 | Toggle visibility after discard | Use `Navigation.navigate(route, {afterTransition})` | File removed | +| `WorkspaceMemberDetailsPage.tsx` | 170 | Go back after member action | Use `Navigation.goBack({afterTransition})` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `FloatingActionButtonAndPopover.tsx` | 702 | FAB menu item action | Use `Navigation.navigate(route, {afterTransition})` | [#56865](https://github.com/Expensify/App/pull/56865) | +| `Session/index.ts` | 1248 | Navigate after sign-in | Use `Navigation.navigate(route, {afterTransition})` | [#30269](https://github.com/Expensify/App/pull/30269) | +| `Link.ts` | 305 | Deep link navigation | Use `Navigation.navigate(route, {afterTransition})` | [#74237](https://github.com/Expensify/App/pull/74237) | +| `Tour.ts` | 9 | Tour navigation | Use `Navigation.navigate(route, {afterTransition})` | [#67348](https://github.com/Expensify/App/pull/67348) | +| `AdminTestDriveModal.tsx` | 21 | Navigate to test drive | Use `Navigation.navigate(route, {afterTransition})` | [#60997](https://github.com/Expensify/App/pull/60997) | +| `EmployeeTestDriveModal.tsx` | 108 | Navigate to money request | Use `Navigation.navigate(route, {afterTransition})` | [#63260](https://github.com/Expensify/App/pull/63260) | +| `TestDriveDemo.tsx` | 68, 76 | Set visibility / go back | Use `Navigation.goBack({afterTransition})` | [#60085](https://github.com/Expensify/App/pull/60085) | +| `ImportedMembersPage.tsx` | 204 | Navigate back after import | Use `Navigation.goBack({afterTransition})` | [#69436](https://github.com/Expensify/App/pull/69436) | +| `ImportedMembersConfirmationPage.tsx` | 210 | Navigate back | Use `Navigation.goBack({afterTransition})` | [#75515](https://github.com/Expensify/App/pull/75515) | +| `WorkspaceDowngradePage.tsx` | 70, 84 | Navigate after downgrade | Use `Navigation.navigate/dismissModal({afterTransition})` | [#71333](https://github.com/Expensify/App/pull/71333) | +| `DebugReportActionPage.tsx` | 69 | Defer deletion during nav | Use `Navigation.goBack({afterTransition: () => deleteAction()})` | [#53655](https://github.com/Expensify/App/pull/53655) | +| `DebugTransactionPage.tsx` | 65 | Defer transaction deletion | Use `Navigation.goBack({afterTransition: () => deleteTx()})` | [#53655](https://github.com/Expensify/App/pull/53655) | +| `DebugTransactionViolationPage.tsx` | 53 | Defer violation deletion | Use `Navigation.goBack({afterTransition: () => deleteViolation()})` | [#53969](https://github.com/Expensify/App/pull/53969) | + +--- + +## 3. IOU/Transaction Draft Cleanup + +**Pattern: Thread through `Navigation.afterTransition` in `handleNavigateAfterExpenseCreate`** + +| File | Line | Current | Migration | PR | +| -------------------- | ----- | -------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| `IOU/index.ts` | 6374 | `removeDraftTransactions()` after split bill | Pass as `afterTransition` to the navigation call in `handleNavigateAfterExpenseCreate` | [#61574](https://github.com/Expensify/App/pull/61574) | +| `IOU/index.ts` | 6501 | `removeDraftTransaction()` after per diem | Same — thread through navigation | [#54760](https://github.com/Expensify/App/pull/54760) | +| `IOU/index.ts` | 6834 | `removeDraftTransactions()` after distance request | Same | [#78109](https://github.com/Expensify/App/pull/78109) | +| `IOU/index.ts` | 7522 | `removeDraftTransaction()` | Same | [#53852](https://github.com/Expensify/App/pull/53852) | +| `IOU/index.ts` | 7613 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 8281 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 8539 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 9059 | Clear draft transaction data | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 14208 | `removeDraftSplitTransaction()` | Same | [#79648](https://github.com/Expensify/App/pull/79648) | +| `IOU/SendInvoice.ts` | 787 | `removeDraftTransaction()` after invoice | Same | [#78512](https://github.com/Expensify/App/pull/78512) | +| `IOU/index.ts` | 1154 | Navigate after `dismissModal()` | Use `Navigation.dismissModal({afterTransition: () => navigate()})` | [#81580](https://github.com/Expensify/App/pull/81580) | + +--- + +## 4. Task State Cleanup + +| File | Line | Current | Migration | PR | +| --------- | ---- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `Task.ts` | 319 | `clearOutTaskInfo()` + `dismissModalWithReport` | Use `dismissModalWithReport({afterTransition: () => clearOutTaskInfo()})` | [#57864](https://github.com/Expensify/App/pull/57864) | + +--- + +## 5. Card Assignment Cleanup + +| File | Line | Current | Migration | PR | +| ---------------------- | ---- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | +| `ConfirmationStep.tsx` | 76 | `clearAssignCardStepAndData()` after `dismissModal()` | Use `Navigation.dismissModal({afterTransition: () => clearAssignCardStepAndData()})` | [#58630](https://github.com/Expensify/App/pull/58630) | + +--- + +## 6. Two-Factor Auth Cleanup + +| File | Line | Current | Migration | PR | +| ------------------------- | ---- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `TwoFactorAuthActions.ts` | 28 | `clearTwoFactorAuthData()` after `goBack()` | Use `Navigation.goBack({afterTransition: () => clearTwoFactorAuthData()})` | [#54404](https://github.com/Expensify/App/pull/54404) | + +--- + +## 7. Report State Cleanup + +| File | Line | Current | Migration | PR | +| ---------------------------- | ---- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `Report/index.ts` | 1176 | `clearGroupChat()` during creation | Use `Navigation.afterTransition` on the subsequent navigation | [#57864](https://github.com/Expensify/App/pull/57864) | +| `Report/index.ts` | 1909 | Clear report data | Navigation afterTransition | [#47033](https://github.com/Expensify/App/pull/47033) | +| `Report/index.ts` | 3328 | `deleteReport()` after `goBack()/popToSidebar()` | `Navigation.goBack({afterTransition: () => deleteReport()})` | [#66890](https://github.com/Expensify/App/pull/66890) | +| `Report/index.ts` | 5814 | Clear report data | Navigation afterTransition | [#66890](https://github.com/Expensify/App/pull/66890) | +| `MoneyRequestReportView.tsx` | 137 | `removeFailedReport()` after `goBackFromSearchMoneyRequest()` | Thread afterTransition through goBack | [#59386](https://github.com/Expensify/App/pull/59386) | + +--- + +## 8. Report Screen Actions + +| File | Line | Current | Migration | PR | +| --------------------------------- | ---- | ------------------------------------------------ | ------------------------------------------------------------------ | ----------------------------------------------------- | +| `ReportScreen.tsx` | 493 | Clear `deleteTransactionNavigateBackUrl` | `Navigation.afterTransition` (post-navigation cleanup) | [#52740](https://github.com/Expensify/App/pull/52740) | +| `ReportScreen.tsx` | 681 | `setShouldShowComposeInput(true)` on mount | `Navigation.afterTransition` (show after screen entry) | [#38255](https://github.com/Expensify/App/pull/38255) | +| `ReportScreen.tsx` | 884 | Subscribe to report leaving events | `Navigation.afterTransition` (after report screen loads) | [#30269](https://github.com/Expensify/App/pull/30269) | +| `ReportActionsView.tsx` | 286 | Set `navigatingToLinkedMessage` state | `Navigation.afterTransition` then `setTimeout(10)` | [#30269](https://github.com/Expensify/App/pull/30269) | +| `BaseReportActionContextMenu.tsx` | 317 | `signOutAndRedirectToSignIn()` after hiding menu | Use context menu's `hideContextMenu` callback with afterTransition | [#33715](https://github.com/Expensify/App/pull/33715) | +| `PureReportActionItem.tsx` | 1740 | Sign out and redirect | `Navigation.navigate({afterTransition})` or `requestAnimationFrame` | [#52948](https://github.com/Expensify/App/pull/52948) | + +--- + +## 9. Component-Specific Navigation + +| File | Line | Current | Migration | PR | +| ---------------------- | ---- | ---------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | +| `MoneyReportHeader.tsx` | 1503 | Delete transaction after nav setup | `Navigation.goBack({afterTransition: () => deleteTransactions()})` | [#74605](https://github.com/Expensify/App/pull/74605) | +| `MoneyReportHeader.tsx` | 1526 | Delete report after goBack | `Navigation.goBack({afterTransition: () => deleteAppReport()})` | [#79539](https://github.com/Expensify/App/pull/79539) | + +--- + +## 10. Settings Pages + +| File | Line | Current | Migration | PR | +| ------------------------------ | ---- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `StatusPage.tsx` | 136 | `clearDraftCustomStatus()` + navigate | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition: () => clearDraft()})` | [#40364](https://github.com/Expensify/App/pull/40364) | +| `StatusPage.tsx` | 157 | Navigate back after status action | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition})` | [#40364](https://github.com/Expensify/App/pull/40364) | +| `TwoFactorAuth/VerifyPage.tsx` | 79 | 2FA verify navigation | Investigate if InteractionManager is needed | [#67762](https://github.com/Expensify/App/pull/67762) | + +--- + +## 11. Workspace Navigation (from WorkspaceConfirmModal) + +| File | Line | Current | Migration | PR | +| ------------------------------------------- | ---- | ------------------------------- | ------------------------------ | ----------------------------------------------------- | +| `PlaidConnectionStep.tsx` | 138 | Plaid connection navigation | Navigation afterTransition | [#64741](https://github.com/Expensify/App/pull/64741) | +| `NetSuiteImportAddCustomSegmentContent.tsx` | 51 | NetSuite segment navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | +| `NetSuiteImportAddCustomListContent.tsx` | 48 | NetSuite list navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | + +--- + +## 12. IOU Request Steps Navigation + +| File | Line | Current | Migration | PR | +| ------------------------------------------ | ---- | ------------------------- | ---------------------------------------- | ----------------------------------------------------- | +| `IOURequestStepReport.tsx` | 156 | Report selection + navigate | `Navigation.goBack({afterTransition})` | [#67048](https://github.com/Expensify/App/pull/67048) | +| `IOURequestStepReport.tsx` | 211 | Report selection + navigate | `Navigation.goBack({afterTransition})` | [#67925](https://github.com/Expensify/App/pull/67925) | +| `IOURequestStepScan/index.native.tsx` | 374 | Scan step navigation | `Navigation.navigate({afterTransition})` | [#63451](https://github.com/Expensify/App/pull/63451) | +| `IOURequestStepScan/ReceiptView/index.tsx` | 70 | Receipt view navigation | `Navigation.navigate({afterTransition})` | [#63352](https://github.com/Expensify/App/pull/63352) | +| `IOURequestStepScan/index.tsx` | 624 | Scan step navigation | `Navigation.navigate({afterTransition})` | [#63451](https://github.com/Expensify/App/pull/63451) | +| `SplitExpensePage.tsx` | 392 | Split expense navigation | `Navigation.navigate({afterTransition})` | [#79597](https://github.com/Expensify/App/pull/79597) | + +--- + +## 13. Modal/Component State (Navigation-based) + +| File | Line | Current | Migration | PR | +| -------------------------------- | ---- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `AddUnreportedExpenseFooter.tsx` | 57 | Bulk convert after `dismissToSuperWideRHP()` | Use `Navigation.dismissToSuperWideRHP({afterTransition: () => convert()})` | [#79328](https://github.com/Expensify/App/pull/79328) | + +--- + +## 14. Right Modal Navigator + +| File | Line | Current | Migration | PR | +| ----------------------- | -------- | -------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `RightModalNavigator.tsx` | 130, 198 | Save scroll offsets / clear 2FA | Use `Navigation.afterTransition` in the navigator | [#69531](https://github.com/Expensify/App/pull/69531), [#79473](https://github.com/Expensify/App/pull/79473) | diff --git a/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md b/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md new file mode 100644 index 0000000000000..a77f40cf6840e --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md @@ -0,0 +1,14 @@ +# Needs Investigation (~2 usages) + +## Strategy + +**Requires deeper investigation before choosing a migration approach** + +These usages don't clearly fit into any of the standard migration patterns and need further analysis. + +## Usages + +| File | Line | Current | Migration | PR | +| ---------------------- | ---- | --------------------------------------------- | ---------------------- | ----------------------------------------------------- | +| `DatePicker/index.tsx` | 107 | InteractionManager deferring popover position | Need deeper investigation | [#62354](https://github.com/Expensify/App/pull/62354) | +| `DatePicker/index.tsx` | 118 | InteractionManager deferring handlePress | Need deeper investigation | [#56068](https://github.com/Expensify/App/pull/56068) | diff --git a/contributingGuides/migrations/InteractionManager/OnboardingTours.md b/contributingGuides/migrations/InteractionManager/OnboardingTours.md index ce22a7923860e..cd6c558c1e4e2 100644 --- a/contributingGuides/migrations/InteractionManager/OnboardingTours.md +++ b/contributingGuides/migrations/InteractionManager/OnboardingTours.md @@ -1,4 +1,4 @@ -# Cluster 8: Onboarding Tours (~3 usages) +# Onboarding Tours (~3 usages) ## Strategy diff --git a/contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md b/contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md deleted file mode 100644 index d3b18de1014c7..0000000000000 --- a/contributingGuides/migrations/InteractionManager/PerformanceAppLifecycle.md +++ /dev/null @@ -1,16 +0,0 @@ -# Cluster 6: Performance / App Lifecycle (~6 usages) - -## Strategy - -**Use `TransitionTracker.runAfterTransitions`** - -Performance measurements, splash screen timing, Lottie animation gating, and background image loading are all non-urgent work that should not block the main thread during transitions. - -## Usages - -| File | Line | Current | Migration | PR | -| ---------------------------------- | ---- | -------------------------------------------- | ----------------------------------------- | ----------------------------------------------------- | -| `Performance.tsx` | 49 | InteractionManager wrapping TTI measurement | use TransitionTracker.runAfterTransitions | [#54412](https://github.com/Expensify/App/pull/54412) | -| `Lottie/index.tsx` | 44 | InteractionManager gating Lottie rendering | use TransitionTracker.runAfterTransitions | [#48143](https://github.com/Expensify/App/pull/48143) | -| `BackgroundImage/index.native.tsx` | 38 | InteractionManager deferring background load | use TransitionTracker.runAfterTransitions | [#48143](https://github.com/Expensify/App/pull/48143) | -| `BackgroundImage/index.tsx` | 38 | InteractionManager deferring background load | use TransitionTracker.runAfterTransitions | [#48143](https://github.com/Expensify/App/pull/48143) | diff --git a/contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md b/contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md deleted file mode 100644 index fa8aaba77b554..0000000000000 --- a/contributingGuides/migrations/InteractionManager/RealtimeSubscriptions.md +++ /dev/null @@ -1,18 +0,0 @@ -# Cluster 5: Realtime Subscriptions (~6 usages) - -## Strategy - -**Use `requestIdleCallback`** - -Pusher subscriptions and typing event listeners are non-urgent background work. They should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. - -## Usages - -| File | Line | Current | Migration | PR | -| ----------------------------- | ---- | ------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------- | -| `Pusher/index.ts` | 206 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#53751](https://github.com/Expensify/App/pull/53751) | -| `Pusher/index.native.ts` | 211 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#56610](https://github.com/Expensify/App/pull/56610) | -| `UserTypingEventListener.tsx` | 35 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#47780](https://github.com/Expensify/App/pull/47780) | -| `UserTypingEventListener.tsx` | 49 | Store ref for InteractionManager handle | Update to store `requestIdleCallback` handle ref | [#47780](https://github.com/Expensify/App/pull/47780) | -| `UserTypingEventListener.tsx` | 59 | InteractionManager wrapping subscribe | Replace with `requestIdleCallback(() => subscribe())` | [#47780](https://github.com/Expensify/App/pull/47780) | -| `UserTypingEventListener.tsx` | 70 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#47780](https://github.com/Expensify/App/pull/47780) | diff --git a/contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md b/contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md new file mode 100644 index 0000000000000..e43abbb72dc90 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md @@ -0,0 +1,18 @@ +# requestAnimationFrame (~6 usages) + +## Strategy + +**Use `requestAnimationFrame` for non-scroll UI yields** + +These usages need to yield to the UI thread for a single frame before performing an action (state update, navigation after synchronous Onyx flush, etc.). `requestAnimationFrame` is the correct primitive — it ensures the callback runs after the current frame is painted. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------------ | ---- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `OptionRow.tsx` | 195 | InteractionManager re-enabling row | `requestAnimationFrame(() => setIsDisabled(false))` (yield to UI) | [#14426](https://github.com/Expensify/App/pull/14426) | +| `MoneyReportHeader.tsx` | 574 | iOS-only: show hold menu after interaction | `requestAnimationFrame(() => setIsHoldMenuVisible(true))` (iOS animation workaround) | [#66790](https://github.com/Expensify/App/pull/66790) | +| `TaskAssigneeSelectorModal.tsx` | 181 | Edit task assignee via `setAssigneeValue` + `editTaskAssignee`, then defer `dismissModalWithReport` | `requestAnimationFrame(() => Navigation.dismissModalWithReport({reportID}))` | [#81320](https://github.com/Expensify/App/pull/81320) | +| `TaskAssigneeSelectorModal.tsx` | 194 | Set assignee for new task via `setAssigneeValue`, then defer `goBack` to `NEW_TASK` route | `requestAnimationFrame(() => Navigation.goBack(ROUTES.NEW_TASK.getRoute(backTo)))` | [#81320](https://github.com/Expensify/App/pull/81320) | +| `useSingleExecution/index.native.ts` | 27 | InteractionManager resetting `isExecuting` | `requestAnimationFrame(() => setIsExecuting(false))` — yield to allow UI updates before resetting state, if it doesn't work use `TransitionTracker.runAfterTransitions` | [#24173](https://github.com/Expensify/App/pull/24173) | +| `WorkspaceNewRoomPage.tsx` | 136 | `addPolicyReport()` deferred | `requestAnimationFrame(() => addPolicyReport())` (no navigation involved) | [#59207](https://github.com/Expensify/App/pull/59207) | diff --git a/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md b/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md new file mode 100644 index 0000000000000..9ca2cc1e8d6e9 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md @@ -0,0 +1,30 @@ +# requestIdleCallback (~9 usages) + +## Strategy + +**Use `requestIdleCallback` for non-urgent background work** + +Pusher subscriptions, typing event listeners, search API calls, and contact imports are non-urgent background work. They should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. + +--- + +## 1. Realtime Subscriptions + +| File | Line | Current | Migration | PR | +| ----------------------------- | ---- | ------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------- | +| `Pusher/index.ts` | 206 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#53751](https://github.com/Expensify/App/pull/53751) | +| `Pusher/index.native.ts` | 211 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#56610](https://github.com/Expensify/App/pull/56610) | +| `UserTypingEventListener.tsx` | 35 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | +| `UserTypingEventListener.tsx` | 49 | Store ref for InteractionManager handle | Update to store `requestIdleCallback` handle ref | [#39347](https://github.com/Expensify/App/pull/39347) | +| `UserTypingEventListener.tsx` | 59 | InteractionManager wrapping subscribe | Replace with `requestIdleCallback(() => subscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | +| `UserTypingEventListener.tsx` | 70 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | + +--- + +## 2. Search API Operations + +| File | Line | Current | Migration | PR | +| -------------------------------------- | ---- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `useSearchHighlightAndScroll.ts` | 126 | InteractionManager deferring search API call | Try to use `requestIdleCallback(() => search(...))` or `startTransition` to defer the search API call | [#69713](https://github.com/Expensify/App/pull/69713) | +| `useSearchSelector.native.ts` | 27 | InteractionManager deferring contact import | Try to use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#70700](https://github.com/Expensify/App/pull/70700) | +| `MoneyRequestParticipantsSelector.tsx` | 431 | InteractionManager deferring contact import | Use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#54459](https://github.com/Expensify/App/pull/54459) | diff --git a/contributingGuides/migrations/InteractionManager/ScrollOperations.md b/contributingGuides/migrations/InteractionManager/ScrollOperations.md new file mode 100644 index 0000000000000..03d647ab6490c --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/ScrollOperations.md @@ -0,0 +1,21 @@ +# Scroll Operations (~8 usages) + +## Strategy + +**Use `requestAnimationFrame`** + +Scroll operations are layout-dependent and need the current frame's layout to be committed before executing. `requestAnimationFrame` is the correct primitive here — it ensures the scroll happens after the browser/native has painted the current frame. + +## Usages + +| File | Line | Current | Migration | PR | +| ----------------------------------------------- | -------- | ---------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `ReportActionItemEventHandler/index.android.ts` | 7 | `InteractionManager.runAfterInteractions(() => rAF)` | Verify if this is still needed | [#44428](https://github.com/Expensify/App/pull/44428) | +| `FormWrapper.tsx` | 199 | Nested `InteractionManager + rAF` | Replace with just `requestAnimationFrame(() => scrollToEnd())` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `MoneyRequestReportActionsList.tsx` | 293 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadOlderChats())` | [#59664](https://github.com/Expensify/App/pull/59664) | +| `MoneyRequestReportActionsList.tsx` | 514 | InteractionManager wrapping scroll | Replace with `requestAnimationFrame(() => scrollToBottom())` | [#59664](https://github.com/Expensify/App/pull/59664) | +| `ReportActionsList.tsx` | 837 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadNewerChats())` | [#49477](https://github.com/Expensify/App/pull/49477) | +| `ReportActionsList.tsx` | 494 | Hide counter + scroll to bottom on mount | `requestAnimationFrame` (scroll operation) | [#55350](https://github.com/Expensify/App/pull/55350) | +| `ReportActionsList.tsx` | 513 | Safari scroll to bottom for whisper | `requestAnimationFrame` (scroll) | [#55350](https://github.com/Expensify/App/pull/55350) | +| `ReportActionsList.tsx` | 526 | Scroll to bottom for current user action | `requestAnimationFrame` (scroll) | [#52955](https://github.com/Expensify/App/pull/52955) | +| `ReportActionsList.tsx` | 617 | Scroll to bottom for IOU error | `requestAnimationFrame` (scroll) | [#58793](https://github.com/Expensify/App/pull/58793) | \ No newline at end of file diff --git a/contributingGuides/migrations/InteractionManager/SearchAPIOperations.md b/contributingGuides/migrations/InteractionManager/SearchAPIOperations.md deleted file mode 100644 index 3c85f85ebf0dc..0000000000000 --- a/contributingGuides/migrations/InteractionManager/SearchAPIOperations.md +++ /dev/null @@ -1,15 +0,0 @@ -# Cluster 9: Search API Operations (~3 usages) - -## Strategy - -**Use `requestIdleCallback`** - -Search API calls and contact imports are non-urgent background work that should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. - -## Usages - -| File | Line | Current | Migration | PR | -| -------------------------------------- | ---- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `useSearchHighlightAndScroll.ts` | 126 | InteractionManager deferring search API call | Try to use `requestIdleCallback(() => search(...))` or startTransition to defer the search API call | [#69713](https://github.com/Expensify/App/pull/69713) | -| `useSearchSelector.native.ts` | 27 | InteractionManager deferring contact import | Try to use `requestIdleCallback(importAndSaveContacts)` or startTransition to defer the contact import | [#70700](https://github.com/Expensify/App/pull/70700) | -| `MoneyRequestParticipantsSelector.tsx` | 431 | InteractionManager deferring contact import | InteractionManager was used as a workaround to defer the contact import, now use `requestIdleCallback(importAndSaveContacts)` or startTransition to defer the contact import | [#54459](https://github.com/Expensify/App/pull/54459) | diff --git a/contributingGuides/migrations/InteractionManager/SettingsPages.md b/contributingGuides/migrations/InteractionManager/SettingsPages.md deleted file mode 100644 index 5495fdb14f2df..0000000000000 --- a/contributingGuides/migrations/InteractionManager/SettingsPages.md +++ /dev/null @@ -1,16 +0,0 @@ -# Cluster 14: Settings Pages (~4 usages) - -## Strategy - -**Mixed — Navigation afterTransition, KeyboardUtils.dismiss** - -Settings pages combine navigation-dependent cleanup and keyboard dismissal patterns. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------ | ---- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `StatusPage.tsx` | 136 | `clearDraftCustomStatus()` + navigate | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition: () => clearDraft()})` | [#40364](https://github.com/Expensify/App/pull/40364) | -| `StatusPage.tsx` | 157 | Navigate back after status action | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition})` | [#40364](https://github.com/Expensify/App/pull/40364) | -| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | `KeyboardUtils.dismiss({afterTransition: () => setIsDeleteModalOpen(true)})` | [#35305](https://github.com/Expensify/App/pull/35305) | -| `TwoFactorAuth/VerifyPage.tsx` | 79 | 2FA verify navigation | Investigate if InteractionManager is needed | [#67762](https://github.com/Expensify/App/pull/67762) | diff --git a/contributingGuides/migrations/InteractionManager/TaskAssignee.md b/contributingGuides/migrations/InteractionManager/TaskAssignee.md deleted file mode 100644 index f5b487812f4f0..0000000000000 --- a/contributingGuides/migrations/InteractionManager/TaskAssignee.md +++ /dev/null @@ -1,14 +0,0 @@ -# Cluster 16: Task Assignee (2 usages) - -## Strategy - -**Use `requestAnimationFrame`** - -Both usages defer navigation after synchronous Onyx state updates (`setAssigneeValue`, `editTaskAssignee`). The deferral gives the optimistic updates a frame to flush before navigating. Replace with `requestAnimationFrame`. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------- | ---- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------ | -| `TaskAssigneeSelectorModal.tsx` | 181 | Edit task assignee via `setAssigneeValue` + `editTaskAssignee`, then defer `dismissModalWithReport` | `requestAnimationFrame(() => Navigation.dismissModalWithReport({reportID}))` | [#81320](https://github.com/Expensify/App/pull/81320) | -| `TaskAssigneeSelectorModal.tsx` | 194 | Set assignee for new task via `setAssigneeValue`, then defer `goBack` to `NEW_TASK` route | `requestAnimationFrame(() => Navigation.goBack(ROUTES.NEW_TASK.getRoute(backTo)))` | [#81320](https://github.com/Expensify/App/pull/81320) | diff --git a/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md b/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md new file mode 100644 index 0000000000000..68e4816c2edcc --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md @@ -0,0 +1,17 @@ +# TransitionTracker Direct Usage (~5 usages) + +## Strategy + +**Use `TransitionTracker.runAfterTransitions` (direct usage in utilities/infra code only)** + +These are infrastructure-level usages where `TransitionTracker.runAfterTransitions` should be called directly. This is the exception to the general rule — application code should use `Navigation.afterTransition` or `KeyboardUtils.dismiss({afterTransition})` instead, but these utility/infrastructure files need the direct API. + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------------ | ---- | --------------------------------------------- | ------------------------------------------ | ----------------------------------------------------- | +| `Performance.tsx` | 49 | InteractionManager wrapping TTI measurement | Use `TransitionTracker.runAfterTransitions` | [#54412](https://github.com/Expensify/App/pull/54412) | +| `Lottie/index.tsx` | 44 | InteractionManager gating Lottie rendering | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | +| `BackgroundImage/index.native.tsx` | 38 | InteractionManager deferring background load | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | +| `BackgroundImage/index.tsx` | 38 | InteractionManager deferring background load | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | +| `TopLevelNavigationTabBar/index.tsx` | 54 | InteractionManager detecting animation finish | Use `TransitionTracker.runAfterTransitions` | [#49539](https://github.com/Expensify/App/pull/49539) | diff --git a/contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md b/contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md deleted file mode 100644 index dedbee53b0711..0000000000000 --- a/contributingGuides/migrations/InteractionManager/WorkspaceConfirmModal.md +++ /dev/null @@ -1,35 +0,0 @@ -# Cluster 1: ConfirmModal Patterns - -## Strategy - -**ConfirmModal pattern (ref + afterTransition)** - -Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. - -For the few cases involving navigation (Plaid, NetSuite), use `Navigation.afterTransition` instead. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------------------- | ---- | ----------------------------------- | ---------------------------- | ----------------------------------------------------- | -| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | ConfirmModal afterTransition | [#54178](https://github.com/Expensify/App/pull/54178) | -| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `ReportParticipantsPage.tsx` | 446 | Same pattern | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | ConfirmModal afterTransition | [#56792](https://github.com/Expensify/App/pull/56792) | -| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | ConfirmModal afterTransition | [#82523](https://github.com/Expensify/App/pull/82523) | -| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | ConfirmModal afterTransition | [#74530](https://github.com/Expensify/App/pull/74530) | -| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | ConfirmModal afterTransition | [#57373](https://github.com/Expensify/App/pull/57373) | -| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | ConfirmModal afterTransition | [#56932](https://github.com/Expensify/App/pull/56932) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | ConfirmModal afterTransition | [#52163](https://github.com/Expensify/App/pull/52163) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | ConfirmModal afterTransition | [#48618](https://github.com/Expensify/App/pull/48618) | -| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | ConfirmModal afterTransition | [#69146](https://github.com/Expensify/App/pull/69146) | -| `PlaidConnectionStep.tsx` | 138 | Plaid connection navigation | Navigation afterTransition | [#64741](https://github.com/Expensify/App/pull/64741) | -| `NetSuiteImportAddCustomSegmentContent.tsx` | 51 | NetSuite segment navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | -| `NetSuiteImportAddCustomListContent.tsx` | 48 | NetSuite list navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | From 20293976ca67a0ed7e27bd94e34e63b825999fd5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 18 Feb 2026 21:27:36 +0100 Subject: [PATCH 23/40] Remove outdated InteractionManager migration documentation and add new guidelines for using Navigation with waitForTransition. Update various files to reflect the new categorization --- .../InteractionManager/FilesValidation.md | 14 -- .../InputFocusManagement.md | 10 +- .../KeyboardDismissAfterTransition.md | 21 -- .../ModalAfterTransition.md | 95 ++++++--- .../InteractionManager/NavigateAfterFocus.md | 16 -- .../NavigationAfterTransition.md | 200 ++++-------------- .../NavigationWaitForTransition.md | 81 +++++++ .../InteractionManager/NeedsInvestigation.md | 19 +- .../InteractionManager/OnboardingTours.md | 8 +- .../migrations/InteractionManager/README.md | 43 ++++ .../RequestAnimationFrame.md | 18 -- .../InteractionManager/RequestIdleCallback.md | 32 +-- .../InteractionManager/ScrollOperations.md | 30 +-- .../TransitionTrackerDirect.md | 12 +- src/libs/Navigation/Navigation.ts | 96 +++++---- src/libs/Navigation/TransitionTracker.ts | 56 +++-- src/libs/Navigation/helpers/linkTo/types.ts | 2 + 17 files changed, 371 insertions(+), 382 deletions(-) delete mode 100644 contributingGuides/migrations/InteractionManager/FilesValidation.md delete mode 100644 contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md delete mode 100644 contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md create mode 100644 contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md create mode 100644 contributingGuides/migrations/InteractionManager/README.md delete mode 100644 contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md diff --git a/contributingGuides/migrations/InteractionManager/FilesValidation.md b/contributingGuides/migrations/InteractionManager/FilesValidation.md deleted file mode 100644 index 0e77a88a6ccbf..0000000000000 --- a/contributingGuides/migrations/InteractionManager/FilesValidation.md +++ /dev/null @@ -1,14 +0,0 @@ -# Files Validation (2 usages) - -## Strategy - -**ConfirmModal pattern (ref + afterTransition)** - -User confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------ | ---- | -------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | -| `useFilesValidation.tsx` | 108 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | -| `useFilesValidation.tsx` | 365 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | diff --git a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md index 26a98626ade3b..96187abd672ec 100644 --- a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md +++ b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md @@ -1,12 +1,10 @@ -# Input Focus Management (~16 usages) +# Input Focus Management -## Strategy - -**Skip for now — focus utilities not coded yet** +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. -Document each usage but mark as TODO. When the focus utility is implemented, it should follow the same pattern as keyboard (`TransitionTracker` called internally, expose `afterTransition` option). +## Strategy -`TransitionTracker.runAfterTransitions()` should **never** be called directly in application code. +Needs investigation. We need to figure out a way to handle this without using `InteractionManager.runAfterInteractions`. ## Usages diff --git a/contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md b/contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md deleted file mode 100644 index 93e38c051261d..0000000000000 --- a/contributingGuides/migrations/InteractionManager/KeyboardDismissAfterTransition.md +++ /dev/null @@ -1,21 +0,0 @@ -# Keyboard Dismiss After Transition (~7 usages) - -## Strategy - -**Use `KeyboardUtils.dismiss({afterTransition})`** - -These usages dismiss the keyboard and then perform an action (usually navigation) after the keyboard close animation completes. Replace with `KeyboardUtils.dismiss({afterTransition: callback})`. - -`TransitionTracker.runAfterTransitions()` should **never** be called directly — it is already wired into `KeyboardUtils` internally. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------ | ---- | ------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------- | -| `IOURequestStepDestination.tsx` | 201 | Keyboard dismiss + navigate | `KeyboardUtils.dismiss({afterTransition: () => Navigation.goBack()})` | [#66747](https://github.com/Expensify/App/pull/66747) | -| `IOURequestStepCategory.tsx` | 210 | Category selection + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#53316](https://github.com/Expensify/App/pull/53316) | -| `IOURequestStepSubrate.tsx` | 234 | Subrate selection + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#56347](https://github.com/Expensify/App/pull/56347) | -| `IOURequestStepDescription.tsx` | 226 | Description submit + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#67010](https://github.com/Expensify/App/pull/67010) | -| `IOURequestStepMerchant.tsx` | 181 | Merchant submit + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#67010](https://github.com/Expensify/App/pull/67010) | -| `NewTaskPage.tsx` | 63 | `blurActiveElement()` on focus | `KeyboardUtils.dismiss({afterTransition})` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | `KeyboardUtils.dismiss({afterTransition: () => setIsDeleteModalOpen(true)})` | [#35305](https://github.com/Expensify/App/pull/35305) | diff --git a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md index 7cefccee50b83..0b26a25a8db2e 100644 --- a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md +++ b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md @@ -1,49 +1,76 @@ -# Modal After Transition (~24 usages) +# Modal After Transition + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy **ConfirmModal ref + `afterTransition` AND ReanimatedModal `afterTransition`** -Two modal patterns for deferring work until after a modal close animation completes. +Implement ref pattern for both ReanimatedModal and ConfirmModal. + +``` +const modalRef = useRef(null); + +modalRef.current.close({afterTransition: () => { + // cleanup here +}}); ---- +return ( + +); +``` + +Instead of: + +``` +setModalVisible(false); +InteractionManager.runAfterInteractions(() => { + // cleanup here +}); +``` ## 1. ConfirmModal Pattern (ref + afterTransition) Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. -| File | Line | Current | Migration | PR | -| ----------------------------------------- | ---- | ----------------------------------- | ---------------------------- | ----------------------------------------------------- | -| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | ConfirmModal afterTransition | [#54178](https://github.com/Expensify/App/pull/54178) | -| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `ReportParticipantsPage.tsx` | 446 | Same pattern | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | ConfirmModal afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | ConfirmModal afterTransition | [#56792](https://github.com/Expensify/App/pull/56792) | -| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | ConfirmModal afterTransition | [#82523](https://github.com/Expensify/App/pull/82523) | -| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | ConfirmModal afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | ConfirmModal afterTransition | [#74530](https://github.com/Expensify/App/pull/74530) | -| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | ConfirmModal afterTransition | [#57373](https://github.com/Expensify/App/pull/57373) | -| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | ConfirmModal afterTransition | [#56932](https://github.com/Expensify/App/pull/56932) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | ConfirmModal afterTransition | [#52163](https://github.com/Expensify/App/pull/52163) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | ConfirmModal afterTransition | [#48618](https://github.com/Expensify/App/pull/48618) | -| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | ConfirmModal afterTransition | [#69146](https://github.com/Expensify/App/pull/69146) | -| `PopoverReportActionContextMenu.tsx` | 374 | `deleteReportComment()` after confirm | ConfirmModal ref + afterTransition | [#66791](https://github.com/Expensify/App/pull/66791) | -| `AutoSubmitModal.tsx` | 43 | `dismissASAPSubmitExplanation()` on modal close | Use modal's `onModalHide` with afterTransition (ConfirmModal pattern) | [#63893](https://github.com/Expensify/App/pull/63893) | - ---- +| File | Line | Current | Migration | PR | +| ----------------------------------------- | ---- | ----------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | +| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | Use ConfirmModal ref approach with afterTransition | [#54178](https://github.com/Expensify/App/pull/54178) | +| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | Use ConfirmModal ref approach with afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `ReportParticipantsPage.tsx` | 446 | Same pattern | Use ConfirmModal ref approach with afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | Use ConfirmModal ref approach with afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | +| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | Use ConfirmModal ref approach with afterTransition | [#56792](https://github.com/Expensify/App/pull/56792) | +| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | Use ConfirmModal ref approach with afterTransition | [#82523](https://github.com/Expensify/App/pull/82523) | +| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | Use ConfirmModal ref approach with afterTransition | [#74530](https://github.com/Expensify/App/pull/74530) | +| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | Use ConfirmModal ref approach with afterTransition | [#57373](https://github.com/Expensify/App/pull/57373) | +| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | Use ConfirmModal ref approach with afterTransition | [#56932](https://github.com/Expensify/App/pull/56932) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | Use ConfirmModal ref approach with afterTransition | [#52163](https://github.com/Expensify/App/pull/52163) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | Use ConfirmModal ref approach with afterTransition | [#48618](https://github.com/Expensify/App/pull/48618) | +| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | Use ConfirmModal ref approach with afterTransition | [#69146](https://github.com/Expensify/App/pull/69146) | +| `PopoverReportActionContextMenu.tsx` | 374 | `deleteReportComment()` after confirm | Use ConfirmModal ref approach with afterTransition | [#66791](https://github.com/Expensify/App/pull/66791) | +| `AutoSubmitModal.tsx` | 43 | `dismissASAPSubmitExplanation()` on modal close | Use ConfirmModal ref approach with afterTransition | [#63893](https://github.com/Expensify/App/pull/63893) | +| `useFilesValidation.tsx` | 108 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | +| `useFilesValidation.tsx` | 365 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | +| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | Use ConfirmModal ref approach with afterTransition | [#35305](https://github.com/Expensify/App/pull/35305) | +| `IOURequestStepMerchant.tsx` | 181 | Merchant submit + keyboard dismiss | Use ConfirmModal ref approach with afterTransition | [#67010](https://github.com/Expensify/App/pull/67010) | +| `IOURequestStepDescription.tsx` | 226 | Description submit + keyboard dismiss | Use ConfirmModal ref approach with afterTransition | [#67010](https://github.com/Expensify/App/pull/67010) | + ## 2. ReanimatedModal Pattern (afterTransition callback) -These usages involve ReanimatedModal's built-in `afterTransition` callback to defer work until after the modal close animation. +These usages involve ReanimatedModal's ref pattern to defer work until after the modal close animation. -| File | Line | Current | Migration | PR | -| ------------------------------------- | ---- | --------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | -| `FeatureTrainingModal.tsx` | 224 | `setIsModalVisible` based on disabled state | Use ReanimatedModal's afterTransition | [#57649](https://github.com/Expensify/App/pull/57649) | -| `FeatureTrainingModal.tsx` | 352 | goBack + onClose after setting invisible | Use modal's afterTransition callback | [#53225](https://github.com/Expensify/App/pull/53225) | -| `AvatarCropModal/AvatarCropModal.tsx` | 324 | InteractionManager deferring onClose | Use modal's `afterTransition` callback (ReanimatedModal pattern) | [#66890](https://github.com/Expensify/App/pull/66890) | -| `AttachmentModalHandler/index.ios.ts` | 12 | iOS: execute close callback after interaction | Use ReanimatedModal's `afterTransition` on the attachment modal | [#53108](https://github.com/Expensify/App/pull/53108) | +| File | Line | Current | Migration | PR | +| ------------------------------------- | ---- | --------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | +| `FeatureTrainingModal.tsx` | 224 | `setIsModalVisible` based on disabled state | Use ReanimatedModal ref approach with afterTransition | [#57649](https://github.com/Expensify/App/pull/57649) | +| `FeatureTrainingModal.tsx` | 352 | goBack + onClose after setting invisible | Use ReanimatedModal ref approach with afterTransition | [#53225](https://github.com/Expensify/App/pull/53225) | +| `AvatarCropModal/AvatarCropModal.tsx` | 324 | InteractionManager deferring onClose | Use ReanimatedModal ref approach with afterTransition | [#66890](https://github.com/Expensify/App/pull/66890) | +| `AttachmentModalHandler/index.ios.ts` | 12 | iOS: execute close callback after interaction | Use ReanimatedModal ref approach with afterTransition | [#53108](https://github.com/Expensify/App/pull/53108) | diff --git a/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md b/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md deleted file mode 100644 index 86643bd6c2f3f..0000000000000 --- a/contributingGuides/migrations/InteractionManager/NavigateAfterFocus.md +++ /dev/null @@ -1,16 +0,0 @@ -# Navigate After Focus (Skipped for now) - -## Strategy - -**Skip — address separately** - -These usages involve `useFocusEffect` combined with navigation, which requires a different approach that hasn't been designed yet. They should be addressed in a separate effort. - -## Usages - -| File | Line | Description | PR | -| ------------------------- | ------------ | -------------------------------------------------------- | ----------------------------------------------------- | -| `AccountDetailsPage.tsx` | 87 | Navigate after `useFocusEffect` triggers | [#59911](https://github.com/Expensify/App/pull/59911) | -| `AccountDetailsPage.tsx` | 116 | Navigate after `useFocusEffect` triggers | [#65834](https://github.com/Expensify/App/pull/65834) | -| `AccountValidatePage.tsx` | 128 | Navigate after `useFocusEffect` triggers | [#79597](https://github.com/Expensify/App/pull/79597) | -| `Report/index.ts` | 5910 | `navigateToTrainingModal()` — focus-dependent navigation | [#66890](https://github.com/Expensify/App/pull/66890) | diff --git a/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md b/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md index 449c52b562a14..300bd4c1de13c 100644 --- a/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md +++ b/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md @@ -1,4 +1,6 @@ -# Navigation After Transition (~60 usages) +# Navigation After Transition + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy @@ -8,163 +10,43 @@ Every usage in this file defers work until after a screen/modal transition compl `TransitionTracker.runAfterTransitions()` should **never** be called directly — it is already wired into `Navigation.ts` internally. ---- - -## 1. Navigation Utility Files - -| File | Line | Current | Migration | PR | -| --------------------------------------- | ---------- | -------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `navigateAfterInteraction/index.ios.ts` | 10 | iOS wrapper deferring navigation | Replace with `Navigation.navigate({afterTransition})` — remove this file entirely | [#56865](https://github.com/Expensify/App/pull/56865) | -| `navigateAfterInteraction/index.ts` | 1 | Non-iOS passthrough | Remove file (merged into Navigation API) | [#56865](https://github.com/Expensify/App/pull/56865) | - ---- - -## 2. Direct Navigation Calls - -| File | Line | Current | Migration | PR | -| ----------------------------------------- | ---------- | -------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------- | -| `SubmitDetailsPage.tsx` | 263 | `navigateAfterInteraction` call | Use `Navigation.navigate(route, {afterTransition})` | [#58834](https://github.com/Expensify/App/pull/58834) | -| `TestToolsModalPage.tsx` | 73 | `navigateAfterInteraction` call | Use `Navigation.navigate(route, {afterTransition})` | [#64717](https://github.com/Expensify/App/pull/64717) | -| `IOURequestStepConfirmation.tsx` | 1390, 1396 | `navigateAfterInteraction` calls | Use `Navigation.navigate(route, {afterTransition})` | [#58422](https://github.com/Expensify/App/pull/58422) | -| `DiscardChangesConfirmation/index.tsx` | 32, 62 | Toggle visibility after discard | Use `Navigation.navigate(route, {afterTransition})` | File removed | -| `WorkspaceMemberDetailsPage.tsx` | 170 | Go back after member action | Use `Navigation.goBack({afterTransition})` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `FloatingActionButtonAndPopover.tsx` | 702 | FAB menu item action | Use `Navigation.navigate(route, {afterTransition})` | [#56865](https://github.com/Expensify/App/pull/56865) | -| `Session/index.ts` | 1248 | Navigate after sign-in | Use `Navigation.navigate(route, {afterTransition})` | [#30269](https://github.com/Expensify/App/pull/30269) | -| `Link.ts` | 305 | Deep link navigation | Use `Navigation.navigate(route, {afterTransition})` | [#74237](https://github.com/Expensify/App/pull/74237) | -| `Tour.ts` | 9 | Tour navigation | Use `Navigation.navigate(route, {afterTransition})` | [#67348](https://github.com/Expensify/App/pull/67348) | -| `AdminTestDriveModal.tsx` | 21 | Navigate to test drive | Use `Navigation.navigate(route, {afterTransition})` | [#60997](https://github.com/Expensify/App/pull/60997) | -| `EmployeeTestDriveModal.tsx` | 108 | Navigate to money request | Use `Navigation.navigate(route, {afterTransition})` | [#63260](https://github.com/Expensify/App/pull/63260) | -| `TestDriveDemo.tsx` | 68, 76 | Set visibility / go back | Use `Navigation.goBack({afterTransition})` | [#60085](https://github.com/Expensify/App/pull/60085) | -| `ImportedMembersPage.tsx` | 204 | Navigate back after import | Use `Navigation.goBack({afterTransition})` | [#69436](https://github.com/Expensify/App/pull/69436) | -| `ImportedMembersConfirmationPage.tsx` | 210 | Navigate back | Use `Navigation.goBack({afterTransition})` | [#75515](https://github.com/Expensify/App/pull/75515) | -| `WorkspaceDowngradePage.tsx` | 70, 84 | Navigate after downgrade | Use `Navigation.navigate/dismissModal({afterTransition})` | [#71333](https://github.com/Expensify/App/pull/71333) | -| `DebugReportActionPage.tsx` | 69 | Defer deletion during nav | Use `Navigation.goBack({afterTransition: () => deleteAction()})` | [#53655](https://github.com/Expensify/App/pull/53655) | -| `DebugTransactionPage.tsx` | 65 | Defer transaction deletion | Use `Navigation.goBack({afterTransition: () => deleteTx()})` | [#53655](https://github.com/Expensify/App/pull/53655) | -| `DebugTransactionViolationPage.tsx` | 53 | Defer violation deletion | Use `Navigation.goBack({afterTransition: () => deleteViolation()})` | [#53969](https://github.com/Expensify/App/pull/53969) | - ---- - -## 3. IOU/Transaction Draft Cleanup - -**Pattern: Thread through `Navigation.afterTransition` in `handleNavigateAfterExpenseCreate`** - -| File | Line | Current | Migration | PR | -| -------------------- | ----- | -------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------ | -| `IOU/index.ts` | 6374 | `removeDraftTransactions()` after split bill | Pass as `afterTransition` to the navigation call in `handleNavigateAfterExpenseCreate` | [#61574](https://github.com/Expensify/App/pull/61574) | -| `IOU/index.ts` | 6501 | `removeDraftTransaction()` after per diem | Same — thread through navigation | [#54760](https://github.com/Expensify/App/pull/54760) | -| `IOU/index.ts` | 6834 | `removeDraftTransactions()` after distance request | Same | [#78109](https://github.com/Expensify/App/pull/78109) | -| `IOU/index.ts` | 7522 | `removeDraftTransaction()` | Same | [#53852](https://github.com/Expensify/App/pull/53852) | -| `IOU/index.ts` | 7613 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 8281 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 8539 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 9059 | Clear draft transaction data | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 14208 | `removeDraftSplitTransaction()` | Same | [#79648](https://github.com/Expensify/App/pull/79648) | -| `IOU/SendInvoice.ts` | 787 | `removeDraftTransaction()` after invoice | Same | [#78512](https://github.com/Expensify/App/pull/78512) | -| `IOU/index.ts` | 1154 | Navigate after `dismissModal()` | Use `Navigation.dismissModal({afterTransition: () => navigate()})` | [#81580](https://github.com/Expensify/App/pull/81580) | - ---- - -## 4. Task State Cleanup - -| File | Line | Current | Migration | PR | -| --------- | ---- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `Task.ts` | 319 | `clearOutTaskInfo()` + `dismissModalWithReport` | Use `dismissModalWithReport({afterTransition: () => clearOutTaskInfo()})` | [#57864](https://github.com/Expensify/App/pull/57864) | - ---- - -## 5. Card Assignment Cleanup - -| File | Line | Current | Migration | PR | -| ---------------------- | ---- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | -| `ConfirmationStep.tsx` | 76 | `clearAssignCardStepAndData()` after `dismissModal()` | Use `Navigation.dismissModal({afterTransition: () => clearAssignCardStepAndData()})` | [#58630](https://github.com/Expensify/App/pull/58630) | - ---- - -## 6. Two-Factor Auth Cleanup - -| File | Line | Current | Migration | PR | -| ------------------------- | ---- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `TwoFactorAuthActions.ts` | 28 | `clearTwoFactorAuthData()` after `goBack()` | Use `Navigation.goBack({afterTransition: () => clearTwoFactorAuthData()})` | [#54404](https://github.com/Expensify/App/pull/54404) | - ---- - -## 7. Report State Cleanup - -| File | Line | Current | Migration | PR | -| ---------------------------- | ---- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `Report/index.ts` | 1176 | `clearGroupChat()` during creation | Use `Navigation.afterTransition` on the subsequent navigation | [#57864](https://github.com/Expensify/App/pull/57864) | -| `Report/index.ts` | 1909 | Clear report data | Navigation afterTransition | [#47033](https://github.com/Expensify/App/pull/47033) | -| `Report/index.ts` | 3328 | `deleteReport()` after `goBack()/popToSidebar()` | `Navigation.goBack({afterTransition: () => deleteReport()})` | [#66890](https://github.com/Expensify/App/pull/66890) | -| `Report/index.ts` | 5814 | Clear report data | Navigation afterTransition | [#66890](https://github.com/Expensify/App/pull/66890) | -| `MoneyRequestReportView.tsx` | 137 | `removeFailedReport()` after `goBackFromSearchMoneyRequest()` | Thread afterTransition through goBack | [#59386](https://github.com/Expensify/App/pull/59386) | - ---- - -## 8. Report Screen Actions - -| File | Line | Current | Migration | PR | -| --------------------------------- | ---- | ------------------------------------------------ | ------------------------------------------------------------------ | ----------------------------------------------------- | -| `ReportScreen.tsx` | 493 | Clear `deleteTransactionNavigateBackUrl` | `Navigation.afterTransition` (post-navigation cleanup) | [#52740](https://github.com/Expensify/App/pull/52740) | -| `ReportScreen.tsx` | 681 | `setShouldShowComposeInput(true)` on mount | `Navigation.afterTransition` (show after screen entry) | [#38255](https://github.com/Expensify/App/pull/38255) | -| `ReportScreen.tsx` | 884 | Subscribe to report leaving events | `Navigation.afterTransition` (after report screen loads) | [#30269](https://github.com/Expensify/App/pull/30269) | -| `ReportActionsView.tsx` | 286 | Set `navigatingToLinkedMessage` state | `Navigation.afterTransition` then `setTimeout(10)` | [#30269](https://github.com/Expensify/App/pull/30269) | -| `BaseReportActionContextMenu.tsx` | 317 | `signOutAndRedirectToSignIn()` after hiding menu | Use context menu's `hideContextMenu` callback with afterTransition | [#33715](https://github.com/Expensify/App/pull/33715) | -| `PureReportActionItem.tsx` | 1740 | Sign out and redirect | `Navigation.navigate({afterTransition})` or `requestAnimationFrame` | [#52948](https://github.com/Expensify/App/pull/52948) | - ---- - -## 9. Component-Specific Navigation - -| File | Line | Current | Migration | PR | -| ---------------------- | ---- | ---------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | -| `MoneyReportHeader.tsx` | 1503 | Delete transaction after nav setup | `Navigation.goBack({afterTransition: () => deleteTransactions()})` | [#74605](https://github.com/Expensify/App/pull/74605) | -| `MoneyReportHeader.tsx` | 1526 | Delete report after goBack | `Navigation.goBack({afterTransition: () => deleteAppReport()})` | [#79539](https://github.com/Expensify/App/pull/79539) | - ---- - -## 10. Settings Pages - -| File | Line | Current | Migration | PR | -| ------------------------------ | ---- | ------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `StatusPage.tsx` | 136 | `clearDraftCustomStatus()` + navigate | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition: () => clearDraft()})` | [#40364](https://github.com/Expensify/App/pull/40364) | -| `StatusPage.tsx` | 157 | Navigate back after status action | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition})` | [#40364](https://github.com/Expensify/App/pull/40364) | -| `TwoFactorAuth/VerifyPage.tsx` | 79 | 2FA verify navigation | Investigate if InteractionManager is needed | [#67762](https://github.com/Expensify/App/pull/67762) | - ---- - -## 11. Workspace Navigation (from WorkspaceConfirmModal) - -| File | Line | Current | Migration | PR | -| ------------------------------------------- | ---- | ------------------------------- | ------------------------------ | ----------------------------------------------------- | -| `PlaidConnectionStep.tsx` | 138 | Plaid connection navigation | Navigation afterTransition | [#64741](https://github.com/Expensify/App/pull/64741) | -| `NetSuiteImportAddCustomSegmentContent.tsx` | 51 | NetSuite segment navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | -| `NetSuiteImportAddCustomListContent.tsx` | 48 | NetSuite list navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | - ---- - -## 12. IOU Request Steps Navigation - -| File | Line | Current | Migration | PR | -| ------------------------------------------ | ---- | ------------------------- | ---------------------------------------- | ----------------------------------------------------- | -| `IOURequestStepReport.tsx` | 156 | Report selection + navigate | `Navigation.goBack({afterTransition})` | [#67048](https://github.com/Expensify/App/pull/67048) | -| `IOURequestStepReport.tsx` | 211 | Report selection + navigate | `Navigation.goBack({afterTransition})` | [#67925](https://github.com/Expensify/App/pull/67925) | -| `IOURequestStepScan/index.native.tsx` | 374 | Scan step navigation | `Navigation.navigate({afterTransition})` | [#63451](https://github.com/Expensify/App/pull/63451) | -| `IOURequestStepScan/ReceiptView/index.tsx` | 70 | Receipt view navigation | `Navigation.navigate({afterTransition})` | [#63352](https://github.com/Expensify/App/pull/63352) | -| `IOURequestStepScan/index.tsx` | 624 | Scan step navigation | `Navigation.navigate({afterTransition})` | [#63451](https://github.com/Expensify/App/pull/63451) | -| `SplitExpensePage.tsx` | 392 | Split expense navigation | `Navigation.navigate({afterTransition})` | [#79597](https://github.com/Expensify/App/pull/79597) | - ---- - -## 13. Modal/Component State (Navigation-based) - -| File | Line | Current | Migration | PR | -| -------------------------------- | ---- | ------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `AddUnreportedExpenseFooter.tsx` | 57 | Bulk convert after `dismissToSuperWideRHP()` | Use `Navigation.dismissToSuperWideRHP({afterTransition: () => convert()})` | [#79328](https://github.com/Expensify/App/pull/79328) | - ---- +| File | Line | Current | Migration | PR | +| ------------------------------------------- | -------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| `IOU/index.ts` | 6374 | `removeDraftTransactions()` after split bill | Pass as `afterTransition` to the navigation call in `handleNavigateAfterExpenseCreate` | [#61574](https://github.com/Expensify/App/pull/61574) | +| `IOU/index.ts` | 6501 | `removeDraftTransaction()` after per diem | Same — thread through navigation | [#54760](https://github.com/Expensify/App/pull/54760) | +| `IOU/index.ts` | 6834 | `removeDraftTransactions()` after distance request | Same | [#78109](https://github.com/Expensify/App/pull/78109) | +| `IOU/index.ts` | 7522 | `removeDraftTransaction()` | Same | [#53852](https://github.com/Expensify/App/pull/53852) | +| `IOU/index.ts` | 7613 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 8281 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 8539 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 9059 | Clear draft transaction data | Same | [#51940](https://github.com/Expensify/App/pull/51940) | +| `IOU/index.ts` | 14208 | `removeDraftSplitTransaction()` | Same | [#79648](https://github.com/Expensify/App/pull/79648) | +| `IOU/SendInvoice.ts` | 787 | `removeDraftTransaction()` after invoice | Same | [#78512](https://github.com/Expensify/App/pull/78512) | +| `Task.ts` | 319 | `clearOutTaskInfo()` + `dismissModalWithReport` | Use `dismissModalWithReport({afterTransition: () => clearOutTaskInfo()})` | [#57864](https://github.com/Expensify/App/pull/57864) | +| `ConfirmationStep.tsx` | 76 | `clearAssignCardStepAndData()` after `dismissModal()` | Use `Navigation.dismissModal({afterTransition: () => clearAssignCardStepAndData()})` | [#58630](https://github.com/Expensify/App/pull/58630) | +| `TwoFactorAuthActions.ts` | 28 | `clearTwoFactorAuthData()` after `goBack()` | Use `Navigation.goBack({afterTransition: () => clearTwoFactorAuthData()})` | [#54404](https://github.com/Expensify/App/pull/54404) | +| `Report/index.ts` | 1176 | `clearGroupChat()` during creation | Use `Navigation.afterTransition` on the subsequent navigation | [#57864](https://github.com/Expensify/App/pull/57864) | +| `Report/index.ts` | 1909 | Clear report data | Navigation afterTransition | [#47033](https://github.com/Expensify/App/pull/47033) | +| `Report/index.ts` | 3328 | `deleteReport()` after `goBack()/popToSidebar()` | `Navigation.goBack({afterTransition: () => deleteReport()})` | [#66890](https://github.com/Expensify/App/pull/66890) | +| `Report/index.ts` | 5814 | Clear report data | Navigation afterTransition | [#66890](https://github.com/Expensify/App/pull/66890) | +| `MoneyRequestReportView.tsx` | 137 | `removeFailedReport()` after `goBackFromSearchMoneyRequest()` | Thread afterTransition through goBack | [#59386](https://github.com/Expensify/App/pull/59386) | +| `ReportScreen.tsx` | 493 | Clear `deleteTransactionNavigateBackUrl` | `Navigation.afterTransition` (post-navigation cleanup) | [#52740](https://github.com/Expensify/App/pull/52740) | +| `ReportScreen.tsx` | 681 | `setShouldShowComposeInput(true)` on mount | `Navigation.afterTransition` (show after screen entry) | [#38255](https://github.com/Expensify/App/pull/38255) | +| `ReportScreen.tsx` | 884 | Subscribe to report leaving events | `Navigation.afterTransition` (after report screen loads) | [#30269](https://github.com/Expensify/App/pull/30269) | +| `ReportActionsView.tsx` | 286 | Set `navigatingToLinkedMessage` state | `Navigation.afterTransition` then `setTimeout(10)` | [#30269](https://github.com/Expensify/App/pull/30269) | +| `BaseReportActionContextMenu.tsx` | 317 | `signOutAndRedirectToSignIn()` after hiding menu | Use context menu's `hideContextMenu` callback with afterTransition | [#33715](https://github.com/Expensify/App/pull/33715) | +| `PureReportActionItem.tsx` | 1740 | Sign out and redirect | `Navigation.navigate({afterTransition})` or `requestAnimationFrame` | [#52948](https://github.com/Expensify/App/pull/52948) | +| `MoneyReportHeader.tsx` | 1503 | Delete transaction after nav setup | `Navigation.goBack({afterTransition: () => deleteTransactions()})` | [#74605](https://github.com/Expensify/App/pull/74605) | +| `MoneyReportHeader.tsx` | 1526 | Delete report after goBack | `Navigation.goBack({afterTransition: () => deleteAppReport()})` | [#79539](https://github.com/Expensify/App/pull/79539) | +| `StatusPage.tsx` | 136 | `clearDraftCustomStatus()` + navigate | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition: () => clearDraft()})` | [#40364](https://github.com/Expensify/App/pull/40364) | +| `StatusPage.tsx` | 157 | Navigate back after status action | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition})` | [#40364](https://github.com/Expensify/App/pull/40364) | +| `TwoFactorAuth/VerifyPage.tsx` | 79 | 2FA verify navigation | Investigate if InteractionManager is needed | [#67762](https://github.com/Expensify/App/pull/67762) | +| `DebugReportActionPage.tsx` | 69 | Defer deletion during nav | Use `Navigation.goBack({afterTransition: () => deleteAction()})` | [#53655](https://github.com/Expensify/App/pull/53655) | +| `DebugTransactionPage.tsx` | 65 | Defer transaction deletion | Use `Navigation.goBack({afterTransition: () => deleteTx()})` | [#53655](https://github.com/Expensify/App/pull/53655) | +| `DebugTransactionViolationPage.tsx` | 53 | Defer violation deletion | Use `Navigation.goBack({afterTransition: () => deleteViolation()})` | [#53969](https://github.com/Expensify/App/pull/53969) | +| `AddUnreportedExpenseFooter.tsx` | 57 | Bulk convert after `dismissToSuperWideRHP()` | Use `Navigation.dismissToSuperWideRHP({afterTransition: () => convert()})` | [#79328](https://github.com/Expensify/App/pull/79328) | +| `RightModalNavigator.tsx` | 130, 198 | Save scroll offsets / clear 2FA | Use `Navigation.afterTransition` in the navigator | [#69531](https://github.com/Expensify/App/pull/69531), [#79473](https://github.com/Expensify/App/pull/79473) | +| `NetSuiteImportAddCustomSegmentContent.tsx` | 51 | NetSuite segment navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | +| `NetSuiteImportAddCustomListContent.tsx` | 48 | NetSuite list navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | -## 14. Right Modal Navigator -| File | Line | Current | Migration | PR | -| ----------------------- | -------- | -------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | -| `RightModalNavigator.tsx` | 130, 198 | Save scroll offsets / clear 2FA | Use `Navigation.afterTransition` in the navigator | [#69531](https://github.com/Expensify/App/pull/69531), [#79473](https://github.com/Expensify/App/pull/79473) | diff --git a/contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md b/contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md new file mode 100644 index 0000000000000..a41770258bd6a --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md @@ -0,0 +1,81 @@ +# Navigation waitForTransition + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. + +## Strategy + +**Use `Navigation.navigate/goBack/dismissModal` with `{ waitForTransition: true }`** + +These call sites wrap `Navigation.navigate`, `Navigation.goBack`, or `Navigation.dismissModal` in `InteractionManager.runAfterInteractions`. Since `navigate`, `goBack`, and `dismissModal` now accept a `waitForTransition` option that internally defers through `TransitionTracker.runAfterTransitions`, the `InteractionManager` wrapper can be removed and replaced with the option. + +Instead of: + +```ts +InteractionManager.runAfterInteractions(() => { + Navigation.navigate(ROUTES.SOME_ROUTE); +}); +``` + +Use: + +```ts +Navigation.navigate(ROUTES.SOME_ROUTE, { waitForTransition: true }); +``` + +Instead of: + +```ts +InteractionManager.runAfterInteractions(() => { + Navigation.goBack(ROUTES.SOME_ROUTE); +}); +``` + +Use: + +```ts +Navigation.goBack(ROUTES.SOME_ROUTE, { waitForTransition: true }); +``` + +## Navigation Utility Files + +These utility files exist solely to wrap navigation calls in `InteractionManager` and should be removed entirely once the migration is complete. + +| File | Line | Current | Migration | PR | +| --------------------------------------- | ---- | -------------------------------- | ----------------------------------------------- | ----------------------------------------------------- | +| `navigateAfterInteraction/index.ios.ts` | 10 | iOS wrapper deferring navigation | Remove file — `waitForTransition` replaces this | [#56865](https://github.com/Expensify/App/pull/56865) | +| `navigateAfterInteraction/index.ts` | 1 | Non-iOS passthrough | Remove file | [#56865](https://github.com/Expensify/App/pull/56865) | + +## Usages + +| File | Line | Current | Migration | PR | +| ------------------------------------------ | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `ImportedMembersPage.tsx` | 204 | `InteractionManager` wrapping `Navigation.goBack` in `onModalHide` | `Navigation.goBack(route, { waitForTransition: true })` | [#69436](https://github.com/Expensify/App/pull/69436) | +| `ImportedMembersConfirmationPage.tsx` | 210 | `InteractionManager` wrapping `Navigation.goBack` in `onModalHide` | `Navigation.goBack(route, { waitForTransition: true })` | [#75515](https://github.com/Expensify/App/pull/75515) | +| `AdminTestDriveModal.tsx` | 21 | `InteractionManager` wrapping `Navigation.navigate` | `Navigation.navigate(route, { waitForTransition: true })` | [#60997](https://github.com/Expensify/App/pull/60997) | +| `EmployeeTestDriveModal.tsx` | 110 | `InteractionManager` wrapping `Navigation.navigate` after `goBack` | `Navigation.navigate(route, { waitForTransition: true })` | [#63260](https://github.com/Expensify/App/pull/63260) | +| `TaskAssigneeSelectorModal.tsx` | 181 | `InteractionManager` wrapping `Navigation.dismissModalWithReport` | Remove `InteractionManager` wrapper (delegates to `navigate`/`dismissModal` internally) | [#81320](https://github.com/Expensify/App/pull/81320) | +| `TaskAssigneeSelectorModal.tsx` | 194 | `InteractionManager` wrapping `Navigation.goBack` | `Navigation.goBack(route, { waitForTransition: true })` | [#81320](https://github.com/Expensify/App/pull/81320) | +| `IOU/index.ts` | 1154 | `InteractionManager` wrapping `Navigation.navigate` after `dismissModal` | `Navigation.navigate(route, { waitForTransition: true })` | [#81580](https://github.com/Expensify/App/pull/81580) | +| `Report/index.ts` | 5912 | `InteractionManager` wrapping `Navigation.navigate` | `Navigation.navigate(route, { waitForTransition: true })` | [#66890](https://github.com/Expensify/App/pull/66890) | +| `SubmitDetailsPage.tsx` | 263 | `navigateAfterInteraction` call | `Navigation.navigate(route, { waitForTransition: true })` | [#58834](https://github.com/Expensify/App/pull/58834) | +| `TestToolsModalPage.tsx` | 73 | `navigateAfterInteraction` call | `Navigation.navigate(route, { waitForTransition: true })` | [#64717](https://github.com/Expensify/App/pull/64717) | +| `IOURequestStepConfirmation.tsx` | 1390, 1396 | `navigateAfterInteraction` calls | `Navigation.navigate(route, { waitForTransition: true })` | [#58422](https://github.com/Expensify/App/pull/58422) | +| `DiscardChangesConfirmation/index.tsx` | 32, 62 | Toggle visibility after discard | `Navigation.navigate(route, { waitForTransition: true })` | [#58422](https://github.com/Expensify/App/pull/58422) | +| `WorkspaceMemberDetailsPage.tsx` | 170 | Go back after member action | `Navigation.goBack(route, { waitForTransition: true })` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `FloatingActionButtonAndPopover.tsx` | 702 | FAB menu item action | `Navigation.navigate(route, { waitForTransition: true })` | [#56865](https://github.com/Expensify/App/pull/56865) | +| `Session/index.ts` | 1248 | Navigate after sign-in | `Navigation.navigate(route, { waitForTransition: true })` | [#30269](https://github.com/Expensify/App/pull/30269) | +| `Link.ts` | 305 | Deep link navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#74237](https://github.com/Expensify/App/pull/74237) | +| `Tour.ts` | 9 | Tour navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#67348](https://github.com/Expensify/App/pull/67348) | +| `TestDriveDemo.tsx` | 68, 76 | Set visibility / go back | `Navigation.goBack({ waitForTransition: true })` | [#60085](https://github.com/Expensify/App/pull/60085) | +| `WorkspaceDowngradePage.tsx` | 70, 84 | Navigate after downgrade | `Navigation.navigate/dismissModal({ waitForTransition: true })` | [#71333](https://github.com/Expensify/App/pull/71333) | +| `AccountDetailsPage.tsx` | 87 | Navigate after `useFocusEffect` triggers | `Navigation.navigate(route, { waitForTransition: true })` | [#59911](https://github.com/Expensify/App/pull/59911) | +| `AccountDetailsPage.tsx` | 116 | Navigate after `useFocusEffect` triggers | `Navigation.navigate(route, { waitForTransition: true })` | [#65834](https://github.com/Expensify/App/pull/65834) | +| `AccountValidatePage.tsx` | 128 | Navigate after `useFocusEffect` triggers | `Navigation.navigate(route, { waitForTransition: true })` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `IOURequestStepReport.tsx` | 156 | Report selection + navigate | `Navigation.goBack(route, { waitForTransition: true })` | [#67048](https://github.com/Expensify/App/pull/67048) | +| `IOURequestStepReport.tsx` | 211 | Report selection + navigate | `Navigation.goBack(route, { waitForTransition: true })` | [#67925](https://github.com/Expensify/App/pull/67925) | +| `IOURequestStepScan/index.native.tsx` | 374 | Scan step navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#63451](https://github.com/Expensify/App/pull/63451) | +| `IOURequestStepScan/ReceiptView/index.tsx` | 70 | Receipt view navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#63352](https://github.com/Expensify/App/pull/63352) | +| `IOURequestStepScan/index.tsx` | 624 | Scan step navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#63451](https://github.com/Expensify/App/pull/63451) | +| `SplitExpensePage.tsx` | 392 | Split expense navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `IOURequestStepCategory.tsx` | 210 | Category selection + keyboard dismiss | `Navigation.navigate(route, { waitForTransition: true })` | [#53316](https://github.com/Expensify/App/pull/53316) | +| `IOURequestStepDestination.tsx` | 201 | Keyboard dismiss + navigate | `KeyboardUtils.dismiss({afterTransition: () => Navigation.goBack()})` | [#66747](https://github.com/Expensify/App/pull/66747) | diff --git a/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md b/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md index a77f40cf6840e..af5a9f619ff9e 100644 --- a/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md +++ b/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md @@ -1,4 +1,6 @@ -# Needs Investigation (~2 usages) +# Needs Investigation + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy @@ -8,7 +10,14 @@ These usages don't clearly fit into any of the standard migration patterns and n ## Usages -| File | Line | Current | Migration | PR | -| ---------------------- | ---- | --------------------------------------------- | ---------------------- | ----------------------------------------------------- | -| `DatePicker/index.tsx` | 107 | InteractionManager deferring popover position | Need deeper investigation | [#62354](https://github.com/Expensify/App/pull/62354) | -| `DatePicker/index.tsx` | 118 | InteractionManager deferring handlePress | Need deeper investigation | [#56068](https://github.com/Expensify/App/pull/56068) | +| File | Line | Current | Migration | PR | +| ------------------------------------ | ---- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `DatePicker/index.tsx` | 107 | InteractionManager deferring popover position | Need deeper investigation | [#62354](https://github.com/Expensify/App/pull/62354) | +| `DatePicker/index.tsx` | 118 | InteractionManager deferring handlePress | Need deeper investigation | [#56068](https://github.com/Expensify/App/pull/56068) | +| `PlaidConnectionStep.tsx` | 138 | Plaid connection navigation | Navigation afterTransition | [#64741](https://github.com/Expensify/App/pull/64741) | +| `OptionRow.tsx` | 195 | InteractionManager re-enabling row | Need deeper investigation / `requestAnimationFrame(() => setIsDisabled(false))` (yield to UI) | [#14426](https://github.com/Expensify/App/pull/14426) | +| `MoneyReportHeader.tsx` | 574 | iOS-only: show hold menu after interaction | Need deeper investigation / `requestAnimationFrame(() => setIsHoldMenuVisible(true))` (iOS animation workaround) | [#66790](https://github.com/Expensify/App/pull/66790) | +| `useSingleExecution/index.native.ts` | 27 | InteractionManager resetting `isExecuting` | Need deeper investigation / `requestAnimationFrame(() => setIsExecuting(false))` — yield to allow UI updates before resetting state, if it doesn't work use `TransitionTracker.runAfterTransitions` | [#24173](https://github.com/Expensify/App/pull/24173) | +| `WorkspaceNewRoomPage.tsx` | 136 | `addPolicyReport()` deferred | Need deeper investigation / `requestAnimationFrame(() => addPolicyReport())` (no navigation involved) | [#59207](https://github.com/Expensify/App/pull/59207) | +| `NewTaskPage.tsx` | 63 | `blurActiveElement()` on focus | Need deeper investigation | [#79597](https://github.com/Expensify/App/pull/79597) | +| `IOURequestStepSubrate.tsx` | 234 | Subrate selection + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#56347](https://github.com/Expensify/App/pull/56347) | diff --git a/contributingGuides/migrations/InteractionManager/OnboardingTours.md b/contributingGuides/migrations/InteractionManager/OnboardingTours.md index cd6c558c1e4e2..9b7bbcca0cdf1 100644 --- a/contributingGuides/migrations/InteractionManager/OnboardingTours.md +++ b/contributingGuides/migrations/InteractionManager/OnboardingTours.md @@ -1,10 +1,10 @@ -# Onboarding Tours (~3 usages) +# Onboarding Tours -## Strategy +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. -**Use `Navigation.navigate({afterTransition})`** +## Strategy -All onboarding usages defer navigation to the next onboarding step until after the current screen transition completes. Replace with the `afterTransition` option on `Navigation.navigate`. +Onboarding usages defer navigation to the next onboarding step until after the current screen transition completes. ## Usages diff --git a/contributingGuides/migrations/InteractionManager/README.md b/contributingGuides/migrations/InteractionManager/README.md new file mode 100644 index 0000000000000..f1a5049eff836 --- /dev/null +++ b/contributingGuides/migrations/InteractionManager/README.md @@ -0,0 +1,43 @@ +# InteractionManager Migration Guide + +## Why we're migrating away from `InteractionManager.runAfterInteractions` + +`runAfterInteractions` conflates three different needs into one global queue: + +1. "Wait for this navigation transition to finish" +2. "Yield to the UI thread for one frame" +3. "Defer non-urgent background work" + +Each need has a better, more precise primitive. This guide explains the replacements. + +--- + +## Primitives comparison + +### `requestAnimationFrame` (rAF) + +- Fires **before the next paint** (~16ms at 60fps) +- Guaranteed to run every frame if the thread isn't blocked +- Use for: UI updates that need to happen on the next frame (scroll, layout measurement, enabling a button after a state flush) + +### `requestIdleCallback` + +- Fires when the runtime has **idle time** — no pending frames, no urgent work +- May be delayed indefinitely if the main thread stays busy +- Accepts a `timeout` option to force execution after a deadline +- Use for: Non-urgent background work (Pusher subscriptions, search API calls, contact imports) + +### `InteractionManager.runAfterInteractions` (legacy — do not use) + +- React Native-specific. Fires after all **ongoing interactions** (animations, touches) complete +- Tracks interactions via `createInteractionHandle()` — anything that calls `handle.done()` unblocks the queue +- In practice, this means "run after the current navigation transition finishes" +- Problem: it's a global queue with no granularity — you can't say "after _this specific_ transition" + +### Summary + +| | Timing | Granularity | Platform | +| ---------------------- | ------------------------- | ------------------------- | --------------------- | +| `rAF` | Next frame (~16ms) | None — just "next paint" | Web + RN | +| `requestIdleCallback` | When idle (unpredictable) | None — "whenever free" | Web + RN (polyfilled) | +| `runAfterInteractions` | After animations finish | Global — all interactions | RN only | diff --git a/contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md b/contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md deleted file mode 100644 index e43abbb72dc90..0000000000000 --- a/contributingGuides/migrations/InteractionManager/RequestAnimationFrame.md +++ /dev/null @@ -1,18 +0,0 @@ -# requestAnimationFrame (~6 usages) - -## Strategy - -**Use `requestAnimationFrame` for non-scroll UI yields** - -These usages need to yield to the UI thread for a single frame before performing an action (state update, navigation after synchronous Onyx flush, etc.). `requestAnimationFrame` is the correct primitive — it ensures the callback runs after the current frame is painted. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------------ | ---- | --------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `OptionRow.tsx` | 195 | InteractionManager re-enabling row | `requestAnimationFrame(() => setIsDisabled(false))` (yield to UI) | [#14426](https://github.com/Expensify/App/pull/14426) | -| `MoneyReportHeader.tsx` | 574 | iOS-only: show hold menu after interaction | `requestAnimationFrame(() => setIsHoldMenuVisible(true))` (iOS animation workaround) | [#66790](https://github.com/Expensify/App/pull/66790) | -| `TaskAssigneeSelectorModal.tsx` | 181 | Edit task assignee via `setAssigneeValue` + `editTaskAssignee`, then defer `dismissModalWithReport` | `requestAnimationFrame(() => Navigation.dismissModalWithReport({reportID}))` | [#81320](https://github.com/Expensify/App/pull/81320) | -| `TaskAssigneeSelectorModal.tsx` | 194 | Set assignee for new task via `setAssigneeValue`, then defer `goBack` to `NEW_TASK` route | `requestAnimationFrame(() => Navigation.goBack(ROUTES.NEW_TASK.getRoute(backTo)))` | [#81320](https://github.com/Expensify/App/pull/81320) | -| `useSingleExecution/index.native.ts` | 27 | InteractionManager resetting `isExecuting` | `requestAnimationFrame(() => setIsExecuting(false))` — yield to allow UI updates before resetting state, if it doesn't work use `TransitionTracker.runAfterTransitions` | [#24173](https://github.com/Expensify/App/pull/24173) | -| `WorkspaceNewRoomPage.tsx` | 136 | `addPolicyReport()` deferred | `requestAnimationFrame(() => addPolicyReport())` (no navigation involved) | [#59207](https://github.com/Expensify/App/pull/59207) | diff --git a/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md b/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md index 9ca2cc1e8d6e9..71711b68b53ce 100644 --- a/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md +++ b/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md @@ -1,4 +1,6 @@ -# requestIdleCallback (~9 usages) +# requestIdleCallback + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy @@ -6,25 +8,23 @@ Pusher subscriptions, typing event listeners, search API calls, and contact imports are non-urgent background work. They should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. ---- - ## 1. Realtime Subscriptions -| File | Line | Current | Migration | PR | -| ----------------------------- | ---- | ------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------- | -| `Pusher/index.ts` | 206 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#53751](https://github.com/Expensify/App/pull/53751) | -| `Pusher/index.native.ts` | 211 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#56610](https://github.com/Expensify/App/pull/56610) | -| `UserTypingEventListener.tsx` | 35 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | -| `UserTypingEventListener.tsx` | 49 | Store ref for InteractionManager handle | Update to store `requestIdleCallback` handle ref | [#39347](https://github.com/Expensify/App/pull/39347) | -| `UserTypingEventListener.tsx` | 59 | InteractionManager wrapping subscribe | Replace with `requestIdleCallback(() => subscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | -| `UserTypingEventListener.tsx` | 70 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | +| File | Line | Current | Migration | PR | +| ----------------------------- | ---- | ------------------------------------------ | ------------------------------------------------------- | ----------------------------------------------------- | +| `Pusher/index.ts` | 206 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#53751](https://github.com/Expensify/App/pull/53751) | +| `Pusher/index.native.ts` | 211 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#56610](https://github.com/Expensify/App/pull/56610) | +| `UserTypingEventListener.tsx` | 35 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | +| `UserTypingEventListener.tsx` | 49 | Store ref for InteractionManager handle | Update to store `requestIdleCallback` handle ref | [#39347](https://github.com/Expensify/App/pull/39347) | +| `UserTypingEventListener.tsx` | 59 | InteractionManager wrapping subscribe | Replace with `requestIdleCallback(() => subscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | +| `UserTypingEventListener.tsx` | 70 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | --- ## 2. Search API Operations -| File | Line | Current | Migration | PR | -| -------------------------------------- | ---- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `useSearchHighlightAndScroll.ts` | 126 | InteractionManager deferring search API call | Try to use `requestIdleCallback(() => search(...))` or `startTransition` to defer the search API call | [#69713](https://github.com/Expensify/App/pull/69713) | -| `useSearchSelector.native.ts` | 27 | InteractionManager deferring contact import | Try to use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#70700](https://github.com/Expensify/App/pull/70700) | -| `MoneyRequestParticipantsSelector.tsx` | 431 | InteractionManager deferring contact import | Use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#54459](https://github.com/Expensify/App/pull/54459) | +| File | Line | Current | Migration | PR | +| -------------------------------------- | ---- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `useSearchHighlightAndScroll.ts` | 126 | InteractionManager deferring search API call | Try to use `requestIdleCallback(() => search(...))` or `startTransition` to defer the search API call | [#69713](https://github.com/Expensify/App/pull/69713) | +| `useSearchSelector.native.ts` | 27 | InteractionManager deferring contact import | Try to use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#70700](https://github.com/Expensify/App/pull/70700) | +| `MoneyRequestParticipantsSelector.tsx` | 431 | InteractionManager deferring contact import | Use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#54459](https://github.com/Expensify/App/pull/54459) | diff --git a/contributingGuides/migrations/InteractionManager/ScrollOperations.md b/contributingGuides/migrations/InteractionManager/ScrollOperations.md index 03d647ab6490c..5f3aa0f5aa5eb 100644 --- a/contributingGuides/migrations/InteractionManager/ScrollOperations.md +++ b/contributingGuides/migrations/InteractionManager/ScrollOperations.md @@ -1,21 +1,23 @@ -# Scroll Operations (~8 usages) +# Scroll Operations + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy -**Use `requestAnimationFrame`** +**Use `requestAnimationFrame(() => callback())`** -Scroll operations are layout-dependent and need the current frame's layout to be committed before executing. `requestAnimationFrame` is the correct primitive here — it ensures the scroll happens after the browser/native has painted the current frame. +Scroll operations are layout-dependent and need the current frame's layout to be committed before executing. `requestAnimationFrame(() => callback())` is the correct primitive here — it ensures the scroll happens after the browser/native has painted the current frame. ## Usages -| File | Line | Current | Migration | PR | -| ----------------------------------------------- | -------- | ---------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `ReportActionItemEventHandler/index.android.ts` | 7 | `InteractionManager.runAfterInteractions(() => rAF)` | Verify if this is still needed | [#44428](https://github.com/Expensify/App/pull/44428) | -| `FormWrapper.tsx` | 199 | Nested `InteractionManager + rAF` | Replace with just `requestAnimationFrame(() => scrollToEnd())` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `MoneyRequestReportActionsList.tsx` | 293 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadOlderChats())` | [#59664](https://github.com/Expensify/App/pull/59664) | -| `MoneyRequestReportActionsList.tsx` | 514 | InteractionManager wrapping scroll | Replace with `requestAnimationFrame(() => scrollToBottom())` | [#59664](https://github.com/Expensify/App/pull/59664) | -| `ReportActionsList.tsx` | 837 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadNewerChats())` | [#49477](https://github.com/Expensify/App/pull/49477) | -| `ReportActionsList.tsx` | 494 | Hide counter + scroll to bottom on mount | `requestAnimationFrame` (scroll operation) | [#55350](https://github.com/Expensify/App/pull/55350) | -| `ReportActionsList.tsx` | 513 | Safari scroll to bottom for whisper | `requestAnimationFrame` (scroll) | [#55350](https://github.com/Expensify/App/pull/55350) | -| `ReportActionsList.tsx` | 526 | Scroll to bottom for current user action | `requestAnimationFrame` (scroll) | [#52955](https://github.com/Expensify/App/pull/52955) | -| `ReportActionsList.tsx` | 617 | Scroll to bottom for IOU error | `requestAnimationFrame` (scroll) | [#58793](https://github.com/Expensify/App/pull/58793) | \ No newline at end of file +| File | Line | Current | Migration | PR | +| ----------------------------------------------- | ---- | ---------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------- | +| `ReportActionItemEventHandler/index.android.ts` | 7 | `InteractionManager.runAfterInteractions(() => rAF)` | Verify if this is still needed | [#44428](https://github.com/Expensify/App/pull/44428) | +| `FormWrapper.tsx` | 199 | Nested `InteractionManager + rAF` | Replace with just `requestAnimationFrame(() => scrollToEnd())` | [#79597](https://github.com/Expensify/App/pull/79597) | +| `MoneyRequestReportActionsList.tsx` | 293 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadOlderChats())` | [#59664](https://github.com/Expensify/App/pull/59664) | +| `MoneyRequestReportActionsList.tsx` | 514 | InteractionManager wrapping scroll | Replace with `requestAnimationFrame(() => scrollToBottom())` | [#59664](https://github.com/Expensify/App/pull/59664) | +| `ReportActionsList.tsx` | 837 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadNewerChats())` | [#49477](https://github.com/Expensify/App/pull/49477) | +| `ReportActionsList.tsx` | 494 | Hide counter + scroll to bottom on mount | Replace with `requestAnimationFrame` | [#55350](https://github.com/Expensify/App/pull/55350) | +| `ReportActionsList.tsx` | 513 | Safari scroll to bottom for whisper | Just remove, no longer needed | [#55350](https://github.com/Expensify/App/pull/55350) | +| `ReportActionsList.tsx` | 526 | Scroll to bottom for current user action | Replace with `requestAnimationFrame` | [#52955](https://github.com/Expensify/App/pull/52955) | +| `ReportActionsList.tsx` | 617 | Scroll to bottom for IOU error | Replace with `requestAnimationFrame` | [#58793](https://github.com/Expensify/App/pull/58793) | \ No newline at end of file diff --git a/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md b/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md index 68e4816c2edcc..00df17417a790 100644 --- a/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md +++ b/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md @@ -1,15 +1,17 @@ -# TransitionTracker Direct Usage (~5 usages) +# TransitionTracker Direct Usage + +Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy -**Use `TransitionTracker.runAfterTransitions` (direct usage in utilities/infra code only)** +**Use `TransitionTracker.runAfterTransitions` ** -These are infrastructure-level usages where `TransitionTracker.runAfterTransitions` should be called directly. This is the exception to the general rule — application code should use `Navigation.afterTransition` or `KeyboardUtils.dismiss({afterTransition})` instead, but these utility/infrastructure files need the direct API. +These are utility files where `TransitionTracker.runAfterTransitions` should be called directly. This is the exception to the general rule — application code should use `Navigation.afterTransition` or `KeyboardUtils.dismiss({afterTransition})` instead, but these utility files need the direct API. ## Usages -| File | Line | Current | Migration | PR | -| ------------------------------------ | ---- | --------------------------------------------- | ------------------------------------------ | ----------------------------------------------------- | +| File | Line | Current | Migration | PR | +| ------------------------------------ | ---- | --------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | | `Performance.tsx` | 49 | InteractionManager wrapping TTI measurement | Use `TransitionTracker.runAfterTransitions` | [#54412](https://github.com/Expensify/App/pull/54412) | | `Lottie/index.tsx` | 44 | InteractionManager gating Lottie rendering | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | | `BackgroundImage/index.native.tsx` | 38 | InteractionManager deferring background load | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index eeac63b7187c8..c238494030266 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -320,13 +320,16 @@ function navigate(route: Route, options?: LinkToOptions) { } } - const targetRoute = route.startsWith(CONST.SAML_REDIRECT_URL) ? ROUTES.HOME : route; - linkTo(navigationRef.current, targetRoute, options); - closeSidePanelOnNarrowScreen(route); + const runImmediately = !options?.waitForTransition; + TransitionTracker.runAfterTransitions(() => { + const targetRoute = route.startsWith(CONST.SAML_REDIRECT_URL) ? ROUTES.HOME : route; + linkTo(navigationRef.current, targetRoute, options); + closeSidePanelOnNarrowScreen(route); - if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); - } + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } + }, runImmediately); } /** * When routes are compared to determine whether the fallback route passed to the goUp function is in the state, @@ -393,11 +396,14 @@ type GoBackOptions = { compareParams?: boolean; // Callback to execute after the navigation transition animation completes. afterTransition?: () => void | undefined; + // If true, waits for ongoing transitions to finish before going back. Defaults to false (goes back immediately). + waitForTransition?: boolean; }; const defaultGoBackOptions: Required = { compareParams: true, afterTransition: () => {}, + waitForTransition: false, }; /** @@ -470,28 +476,31 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { return; } - if (backToRoute) { - goUp(backToRoute, options); - if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); + const runImmediately = !options?.waitForTransition; + TransitionTracker.runAfterTransitions(() => { + if (backToRoute) { + goUp(backToRoute, options); + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } + return; } - return; - } - if (shouldPopToSidebar) { - popToSidebar(); - return; - } + if (shouldPopToSidebar) { + popToSidebar(); + return; + } - if (!navigationRef.current?.canGoBack()) { - Log.hmmm('[Navigation] Unable to go back'); - return; - } + if (!navigationRef.current?.canGoBack()) { + Log.hmmm('[Navigation] Unable to go back'); + return; + } - navigationRef.current?.goBack(); - if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); - } + navigationRef.current?.goBack(); + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } + }, runImmediately); } /** @@ -729,15 +738,22 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -async function dismissModal({ref = navigationRef, afterTransition}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void} = {}) { +async function dismissModal({ + ref = navigationRef, + afterTransition, + waitForTransition, +}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void; waitForTransition?: boolean} = {}) { clearSelectedText(); await isNavigationReady(); - ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + const runImmediately = !waitForTransition; + TransitionTracker.runAfterTransitions(() => { + ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); - if (afterTransition) { - TransitionTracker.runAfterTransitions(afterTransition); - } + if (afterTransition) { + TransitionTracker.runAfterTransitions(afterTransition); + } + }, runImmediately); } /** @@ -854,7 +870,7 @@ function clearPreloadedRoutes() { * * @param modalStackNames - names of the modal stacks we want to dismiss to */ -function dismissToModalStack(modalStackNames: Set) { +function dismissToModalStack(modalStackNames: Set, options: {afterTransition?: () => void} = {}) { const rootState = navigationRef.getRootState(); if (!rootState) { return; @@ -870,32 +886,36 @@ function dismissToModalStack(modalStackNames: Set) { const routesToPop = rhpState.routes.length - lastFoundModalStackIndex - 1; if (routesToPop <= 0 || lastFoundModalStackIndex === -1) { - dismissModal(); + dismissModal(options); return; } navigationRef.dispatch({...StackActions.pop(routesToPop), target: rhpState.key}); + + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions(options.afterTransition); + } } /** * Dismiss top layer modal and go back to the Wide/Super Wide RHP. */ -function dismissToPreviousRHP() { - return dismissToModalStack(ALL_WIDE_RIGHT_MODALS); +function dismissToPreviousRHP(options: {afterTransition?: () => void} = {}) { + return dismissToModalStack(ALL_WIDE_RIGHT_MODALS, options); } -function navigateBackToLastSuperWideRHPScreen() { - return dismissToModalStack(SUPER_WIDE_RIGHT_MODALS); +function navigateBackToLastSuperWideRHPScreen(options: {afterTransition?: () => void} = {}) { + return dismissToModalStack(SUPER_WIDE_RIGHT_MODALS, options); } -function dismissToSuperWideRHP() { +function dismissToSuperWideRHP(options: {afterTransition?: () => void} = {}) { // On narrow layouts (mobile), Super Wide RHP doesn't exist, so just dismiss the modal completely if (getIsNarrowLayout()) { - dismissModal(); + dismissModal(options); return; } // On wide layouts, dismiss back to the Super Wide RHP modal stack - navigateBackToLastSuperWideRHPScreen(); + navigateBackToLastSuperWideRHPScreen(options); } function getTopmostSearchReportRouteParams(state = navigationRef.getRootState()): RightModalNavigatorParamList[typeof SCREENS.RIGHT_MODAL.SEARCH_REPORT] | undefined { diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index da17da924d313..433e30b1b4efe 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -2,36 +2,27 @@ type CancelHandle = {cancel: () => void}; type TransitionType = 'keyboard' | 'navigation' | 'modal' | 'focus'; -type PendingEntry = {callback: () => void; type?: TransitionType}; - const MAX_TRANSITION_DURATION_MS = 1000; const activeTransitions = new Map(); const activeTimeouts: Array<{type: TransitionType; timeout: ReturnType}> = []; -let pendingCallbacks: PendingEntry[] = []; +let pendingCallbacks: Array<() => void> = []; /** - * Invokes and removes pending callbacks. - * - * @param type - When provided, only flushes entries scoped to that type. - * When omitted, flushes all remaining entries. + * Invokes and removes all pending callbacks. */ -function flushCallbacks(type?: TransitionType): void { - const remaining: PendingEntry[] = []; - for (const entry of pendingCallbacks) { - if (type === undefined || entry.type === type) { - entry.callback(); - } else { - remaining.push(entry); - } +function flushCallbacks(): void { + const callbacks = pendingCallbacks; + pendingCallbacks = []; + for (const callback of callbacks) { + callback(); } - pendingCallbacks = remaining; } /** - * Decrements the active count for the given transition type and flushes matching callbacks. + * Decrements the active count for the given transition type and flushes callbacks when all transitions are idle. * Shared by {@link endTransition} (manual) and the auto-timeout. */ function decrementAndFlush(type: TransitionType): void { @@ -44,10 +35,7 @@ function decrementAndFlush(type: TransitionType): void { activeTransitions.set(type, next); } - // Flush callbacks scoped to this specific type - flushCallbacks(type); - - // When all transitions end, flush remaining unscoped callbacks + // When all transitions end, flush all pending callbacks if (activeTransitions.size === 0) { flushCallbacks(); } @@ -76,8 +64,7 @@ function startTransition(type: TransitionType): void { /** * Decrements the active count for the given transition type. * Clears the corresponding auto-timeout since the transition ended normally. - * When the count reaches zero, flushes callbacks scoped to that type. - * When all transition types are idle, flushes remaining unscoped callbacks. + * When all transition types are idle, flushes all pending callbacks. */ function endTransition(type: TransitionType): void { // Clear the oldest timeout for this type (FIFO order matches startTransition order) @@ -91,21 +78,24 @@ function endTransition(type: TransitionType): void { } /** - * Schedules a callback to run after transitions complete. If no transitions are active - * (or the specified type is idle), the callback fires on the next microtask. + * Schedules a callback to run after all transitions complete. If no transitions are active + * or `runImmediately` is true, the callback fires synchronously. * * @param callback - The function to invoke once transitions finish. - * @param type - Optional transition type to scope the wait. When provided, the callback - * fires as soon as that specific type finishes, even if other types are still active. - * When omitted, waits for all transition types to be idle. + * @param runImmediately - If true, the callback fires synchronously regardless of active transitions. Defaults to false. * @returns A handle with a `cancel` method to prevent the callback from firing. */ -function runAfterTransitions(callback: () => void, type?: TransitionType): CancelHandle { - const entry: PendingEntry = {callback, type}; - pendingCallbacks.push(entry); +function runAfterTransitions(callback: () => void, runImmediately = false): CancelHandle { + if (activeTransitions.size === 0 || runImmediately) { + callback(); + return {cancel: () => {}}; + } + + pendingCallbacks.push(callback); + return { cancel: () => { - const idx = pendingCallbacks.indexOf(entry); + const idx = pendingCallbacks.indexOf(callback); if (idx !== -1) { pendingCallbacks.splice(idx, 1); } @@ -117,6 +107,8 @@ const TransitionTracker = { startTransition, endTransition, runAfterTransitions, + activeTransitions, + pendingCallbacks, }; export default TransitionTracker; diff --git a/src/libs/Navigation/helpers/linkTo/types.ts b/src/libs/Navigation/helpers/linkTo/types.ts index 9c7bd4e1cc40a..0cb3945405052 100644 --- a/src/libs/Navigation/helpers/linkTo/types.ts +++ b/src/libs/Navigation/helpers/linkTo/types.ts @@ -13,6 +13,8 @@ type LinkToOptions = { forceReplace?: boolean; // Callback to execute after the navigation transition animation completes. afterTransition?: () => void; + // If true, waits for ongoing transitions to finish before navigating. Defaults to false (navigates immediately). + waitForTransition?: boolean; }; export type {ActionPayload, LinkToOptions}; From 869728b666da4d6cc0858cecfeeb9574f0c77eb8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 19 Feb 2026 08:30:44 +0100 Subject: [PATCH 24/40] Revise InteractionManager migration documentation to clarify the removal of `runAfterInteractions` --- .../migrations/InteractionManager/README.md | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/contributingGuides/migrations/InteractionManager/README.md b/contributingGuides/migrations/InteractionManager/README.md index f1a5049eff836..e4cfd79ba6d77 100644 --- a/contributingGuides/migrations/InteractionManager/README.md +++ b/contributingGuides/migrations/InteractionManager/README.md @@ -1,19 +1,54 @@ -# InteractionManager Migration Guide +# InteractionManager Migration -## Why we're migrating away from `InteractionManager.runAfterInteractions` +## Why -`runAfterInteractions` conflates three different needs into one global queue: +`InteractionManager` is being removed from React Native. We currently maintain a patch to keep it working, but that's a temporary measure — upstream libraries will also drop support over time. -1. "Wait for this navigation transition to finish" -2. "Yield to the UI thread for one frame" -3. "Defer non-urgent background work" +Rather than keep patching, we're replacing `InteractionManager.runAfterInteractions` with purpose-built alternatives that are more precise and self-descriptive. -Each need has a better, more precise primitive. This guide explains the replacements. +## Current state ---- +`runAfterInteractions` is used across the codebase for a wide range of reasons: waiting for navigation transitions, deferring work after modals close, managing input focus, delaying scroll operations, and many other cases that are harder to classify. + +## The problem + +`runAfterInteractions` is a global queue with no granularity. You can't say "after *this specific* transition" — it just means "after all current interactions finish." This made it a convenient catch-all, but the intent behind each call is often unclear. Many usages exist simply because it "just worked" as a timing workaround, not because it was the right tool for the job. + +This makes the migration non-trivial: you have to understand *what each call is actually waiting for* before you can pick the right replacement. + +## The approach + +**TransitionTracker** is the backbone. It tracks navigation transitions explicitly, so other APIs can hook into transition lifecycle without relying on a global queue. + +On top of TransitionTracker, existing APIs gain transition-aware callbacks: + +- Navigation methods accept `afterTransition` — a callback that runs after the triggered navigation transition completes +- Navigation methods accept `waitForTransition` — the call waits for any ongoing transition to finish before navigating +- Keyboard methods accept `afterTransition` — a callback that runs after the keyboard transition completes +- `ConfirmModal` and similar modal APIs will accept `afterTransition` callback to defer work until the modal close transition finishes (will be implemented in a separate PR) + +This makes the code self-descriptive: instead of a generic `runAfterInteractions`, each call site says exactly what it's waiting for and why. + +> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `ConfirmModal`, etc.) rather than importing TransitionTracker directly. + +## How + +The migration is split into 9 subcategories. Each is worked on by a single person, with multiple PRs per category. The subcategory docs serve as sub-issues tracking the specific call sites and their replacement strategy: + +1. [NavigationWaitForTransition](./NavigationWaitForTransition.md) — Navigation calls that need to wait for an ongoing transition before proceeding +2. [NavigationAfterTransition](./NavigationAfterTransition.md) — Callbacks that should run after a navigation transition completes +3. [ModalAfterTransition](./ModalAfterTransition.md) — Work deferred until a modal close transition finishes +4. [ScrollOperations](./ScrollOperations.md) — Scroll-related operations that were waiting on transitions +5. [RequestIdleCallback](./RequestIdleCallback.md) — Non-urgent background work that should use `requestIdleCallback` instead +6. [TransitionTrackerDirect](./TransitionTrackerDirect.md) — Cases that need `TransitionTracker.runAfterTransitions` directly +7. [OnboardingTours](./OnboardingTours.md) — Onboarding and guided tour timing +8. [InputFocusManagement](./InputFocusManagement.md) — Input focus operations deferred until transitions settle +9. [NeedsInvestigation](./NeedsInvestigation.md) — Call sites that need further analysis to determine the right replacement ## Primitives comparison +For reference, here's how the available timing primitives compare: + ### `requestAnimationFrame` (rAF) - Fires **before the next paint** (~16ms at 60fps) From 3ada8cd214fd1508bf23d956437461b011f70bd2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 19 Feb 2026 12:00:56 +0100 Subject: [PATCH 25/40] Refactor TransitionTracker to remove transition type --- .../Modal/ReanimatedModal/index.tsx | 10 ++-- .../PlatformStackNavigation/ScreenLayout.tsx | 4 +- src/libs/Navigation/TransitionTracker.ts | 59 +++++++------------ src/utils/keyboard/index.android.ts | 4 +- src/utils/keyboard/index.ts | 4 +- src/utils/keyboard/index.website.ts | 4 +- 6 files changed, 35 insertions(+), 50 deletions(-) diff --git a/src/components/Modal/ReanimatedModal/index.tsx b/src/components/Modal/ReanimatedModal/index.tsx index f22adfb24ee47..3280344957bd8 100644 --- a/src/components/Modal/ReanimatedModal/index.tsx +++ b/src/components/Modal/ReanimatedModal/index.tsx @@ -103,7 +103,7 @@ function ReanimatedModal({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.clearInteractionHandle(handleRef.current); } - TransitionTracker.endTransition('modal'); + TransitionTracker.endTransition(); setIsVisibleState(false); setIsContainerOpen(false); @@ -116,7 +116,7 @@ function ReanimatedModal({ if (isVisible && !isContainerOpen && !isTransitioning) { // eslint-disable-next-line @typescript-eslint/no-deprecated handleRef.current = InteractionManager.createInteractionHandle(); - TransitionTracker.startTransition('modal'); + TransitionTracker.startTransition(); onModalWillShow(); setIsVisibleState(true); @@ -124,7 +124,7 @@ function ReanimatedModal({ } else if (!isVisible && isContainerOpen && !isTransitioning) { // eslint-disable-next-line @typescript-eslint/no-deprecated handleRef.current = InteractionManager.createInteractionHandle(); - TransitionTracker.startTransition('modal'); + TransitionTracker.startTransition(); onModalWillHide(); blurActiveElement(); @@ -145,7 +145,7 @@ function ReanimatedModal({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.clearInteractionHandle(handleRef.current); } - TransitionTracker.endTransition('modal'); + TransitionTracker.endTransition(); onModalShow(); }, [onModalShow]); @@ -156,7 +156,7 @@ function ReanimatedModal({ // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.clearInteractionHandle(handleRef.current); } - TransitionTracker.endTransition('modal'); + TransitionTracker.endTransition(); // Because on Android, the Modal's onDismiss callback does not work reliably. There's a reported issue at: // https://stackoverflow.com/questions/58937956/react-native-modal-ondismiss-not-invoked diff --git a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx index 17561a7450c55..ca53fd53a1324 100644 --- a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx @@ -12,10 +12,10 @@ function ScreenLayout({ }: ScreenLayoutArgs>) { useLayoutEffect(() => { const transitionStartListener = navigation.addListener('transitionStart', () => { - TransitionTracker.startTransition('navigation'); + TransitionTracker.startTransition(); }); const transitionEndListener = navigation.addListener('transitionEnd', () => { - TransitionTracker.endTransition('navigation'); + TransitionTracker.endTransition(); }); return () => { diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index 433e30b1b4efe..699dff51a4ffb 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -1,12 +1,10 @@ type CancelHandle = {cancel: () => void}; -type TransitionType = 'keyboard' | 'navigation' | 'modal' | 'focus'; - const MAX_TRANSITION_DURATION_MS = 1000; -const activeTransitions = new Map(); +let activeCount = 0; -const activeTimeouts: Array<{type: TransitionType; timeout: ReturnType}> = []; +const activeTimeouts: Array> = []; let pendingCallbacks: Array<() => void> = []; @@ -22,59 +20,48 @@ function flushCallbacks(): void { } /** - * Decrements the active count for the given transition type and flushes callbacks when all transitions are idle. + * Decrements the active count and flushes callbacks when all transitions are idle. * Shared by {@link endTransition} (manual) and the auto-timeout. */ -function decrementAndFlush(type: TransitionType): void { - const current = activeTransitions.get(type) ?? 0; - const next = Math.max(0, current - 1); - - if (next === 0) { - activeTransitions.delete(type); - } else { - activeTransitions.set(type, next); - } +function decrementAndFlush(): void { + activeCount = Math.max(0, activeCount - 1); - // When all transitions end, flush all pending callbacks - if (activeTransitions.size === 0) { + if (activeCount === 0) { flushCallbacks(); } } /** - * Increments the active count for the given transition type. - * Multiple overlapping transitions of the same type are counted. + * Increments the active transition count. + * Multiple overlapping transitions are counted. * Each transition automatically ends after {@link MAX_TRANSITION_DURATION_MS} as a safety net. */ -function startTransition(type: TransitionType): void { - const current = activeTransitions.get(type) ?? 0; - activeTransitions.set(type, current + 1); +function startTransition(): void { + activeCount += 1; const timeout = setTimeout(() => { - const idx = activeTimeouts.findIndex((entry) => entry.timeout === timeout); + const idx = activeTimeouts.indexOf(timeout); if (idx !== -1) { activeTimeouts.splice(idx, 1); } - decrementAndFlush(type); + decrementAndFlush(); }, MAX_TRANSITION_DURATION_MS); - activeTimeouts.push({type, timeout}); + activeTimeouts.push(timeout); } /** - * Decrements the active count for the given transition type. + * Decrements the active transition count. * Clears the corresponding auto-timeout since the transition ended normally. - * When all transition types are idle, flushes all pending callbacks. + * When the count reaches zero, flushes all pending callbacks. */ -function endTransition(type: TransitionType): void { - // Clear the oldest timeout for this type (FIFO order matches startTransition order) - const timeoutIdx = activeTimeouts.findIndex((entry) => entry.type === type); - if (timeoutIdx !== -1) { - clearTimeout(activeTimeouts.at(timeoutIdx)?.timeout); - activeTimeouts.splice(timeoutIdx, 1); +function endTransition(): void { + const timeout = activeTimeouts.shift(); + if (timeout !== undefined) { + clearTimeout(timeout); } - decrementAndFlush(type); + decrementAndFlush(); } /** @@ -86,7 +73,7 @@ function endTransition(type: TransitionType): void { * @returns A handle with a `cancel` method to prevent the callback from firing. */ function runAfterTransitions(callback: () => void, runImmediately = false): CancelHandle { - if (activeTransitions.size === 0 || runImmediately) { + if (activeCount === 0 || runImmediately) { callback(); return {cancel: () => {}}; } @@ -107,9 +94,7 @@ const TransitionTracker = { startTransition, endTransition, runAfterTransitions, - activeTransitions, - pendingCallbacks, }; export default TransitionTracker; -export type {TransitionType, CancelHandle}; +export type {CancelHandle}; diff --git a/src/utils/keyboard/index.android.ts b/src/utils/keyboard/index.android.ts index b3460dd2317ef..ecaf4bfc55403 100644 --- a/src/utils/keyboard/index.android.ts +++ b/src/utils/keyboard/index.android.ts @@ -33,12 +33,12 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { const subscription = Keyboard.addListener('keyboardDidHide', () => { resolve(); - TransitionTracker.endTransition('keyboard'); + TransitionTracker.endTransition(); subscription.remove(); }); Keyboard.dismiss(); - TransitionTracker.startTransition('keyboard'); + TransitionTracker.startTransition(); if (options?.afterTransition) { TransitionTracker.runAfterTransitions(options.afterTransition); } diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts index 4c0f1c03202e3..fb9d01216c4d8 100644 --- a/src/utils/keyboard/index.ts +++ b/src/utils/keyboard/index.ts @@ -32,11 +32,11 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { const subscription = Keyboard.addListener('keyboardDidHide', () => { resolve(); - TransitionTracker.endTransition('keyboard'); + TransitionTracker.endTransition(); subscription.remove(); }); - TransitionTracker.startTransition('keyboard'); + TransitionTracker.startTransition(); Keyboard.dismiss(); if (options?.afterTransition) { diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts index 3c49a40873387..9b69d504b483d 100644 --- a/src/utils/keyboard/index.website.ts +++ b/src/utils/keyboard/index.website.ts @@ -60,13 +60,13 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { } window.visualViewport?.removeEventListener('resize', handleDismissResize); - TransitionTracker.endTransition('keyboard'); + TransitionTracker.endTransition(); return resolve(); }; window.visualViewport?.addEventListener('resize', handleDismissResize); Keyboard.dismiss(); - TransitionTracker.startTransition('keyboard'); + TransitionTracker.startTransition(); if (options?.afterTransition) { TransitionTracker.runAfterTransitions(options.afterTransition); } From 1005d3800304ca622f165d2e42bca433ac206a2b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 20 Feb 2026 13:46:03 +0100 Subject: [PATCH 26/40] Update InteractionManager migration documentation to clarify that `waitForTransition` waits for all ongoing transitions to finish before navigating. --- contributingGuides/migrations/InteractionManager/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributingGuides/migrations/InteractionManager/README.md b/contributingGuides/migrations/InteractionManager/README.md index e4cfd79ba6d77..156fc326616fa 100644 --- a/contributingGuides/migrations/InteractionManager/README.md +++ b/contributingGuides/migrations/InteractionManager/README.md @@ -23,7 +23,7 @@ This makes the migration non-trivial: you have to understand *what each call is On top of TransitionTracker, existing APIs gain transition-aware callbacks: - Navigation methods accept `afterTransition` — a callback that runs after the triggered navigation transition completes -- Navigation methods accept `waitForTransition` — the call waits for any ongoing transition to finish before navigating +- Navigation methods accept `waitForTransition` — the call waits for all ongoing transitions to finish before navigating - Keyboard methods accept `afterTransition` — a callback that runs after the keyboard transition completes - `ConfirmModal` and similar modal APIs will accept `afterTransition` callback to defer work until the modal close transition finishes (will be implemented in a separate PR) From 79ae3d2d3c15a50a6dcabe0975b1385337e2aca5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 20 Feb 2026 13:48:08 +0100 Subject: [PATCH 27/40] Refactor ScreenLayout to improve type handling for navigation prop and simplify screen layout integration in createPlatformStackNavigatorComponent. --- .../PlatformStackNavigation/ScreenLayout.tsx | 14 ++++++-------- .../index.tsx | 11 ++--------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx index ca53fd53a1324..7d0792dea635d 100644 --- a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx @@ -1,15 +1,13 @@ import type {ParamListBase, ScreenLayoutArgs} from '@react-navigation/native'; -import type {StackNavigationOptions} from '@react-navigation/stack'; +import type {StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack'; import {useLayoutEffect} from 'react'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; -import type {PlatformStackNavigationOptions, PlatformStackNavigationProp} from './types'; +import type {PlatformStackNavigationOptions} from './types'; + +function ScreenLayout({children, navigation: navigationProp, options, route}: ScreenLayoutArgs) { + // useNavigationBuilder hardcodes the Navigation generic to `string`, but the actual runtime value is a full navigation object. + const navigation = navigationProp as unknown as StackNavigationProp; -function ScreenLayout({ - children, - navigation, - options, - route, -}: ScreenLayoutArgs>) { useLayoutEffect(() => { const transitionStartListener = navigation.addListener('transitionStart', () => { TransitionTracker.startTransition(); diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index 3349de27e2f03..635d97cd2ed01 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -1,6 +1,6 @@ import type {ParamListBase, StackActionHelpers} from '@react-navigation/native'; import {StackRouter, useNavigationBuilder} from '@react-navigation/native'; -import type {StackNavigationEventMap, StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack'; +import type {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; import {StackView} from '@react-navigation/stack'; import React, {useMemo} from 'react'; import {addCustomHistoryRouterExtension} from '@libs/Navigation/AppNavigator/customHistory'; @@ -64,14 +64,7 @@ function createPlatformStackNavigatorComponent ( - } - /> - ), + screenLayout: ScreenLayout, }, convertToWebNavigationOptions, ); From 58e70f4b6f22da2eb4cae04eb2f7d7aa3f7b1575 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 20 Feb 2026 14:18:43 +0100 Subject: [PATCH 28/40] Refactor ScreenLayout to use a wrapper function --- .../PlatformStackNavigation/ScreenLayout.tsx | 26 ++++++++++++++----- .../index.tsx | 5 ++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx index 7d0792dea635d..1115008e84bc9 100644 --- a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx @@ -1,13 +1,27 @@ +import React, {useLayoutEffect} from 'react'; import type {ParamListBase, ScreenLayoutArgs} from '@react-navigation/native'; import type {StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack'; -import {useLayoutEffect} from 'react'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; -import type {PlatformStackNavigationOptions} from './types'; +import type {PlatformStackNavigationOptions, PlatformStackNavigationProp} from './types'; -function ScreenLayout({children, navigation: navigationProp, options, route}: ScreenLayoutArgs) { - // useNavigationBuilder hardcodes the Navigation generic to `string`, but the actual runtime value is a full navigation object. - const navigation = navigationProp as unknown as StackNavigationProp; +// screenLayout is invoked as a render function (not JSX), so we need this wrapper to create a proper React component boundary for hooks. +function screenLayoutWrapper({navigation, ...rest}: ScreenLayoutArgs) { + return ( + } + /> + ); +} +function ScreenLayout({ + children, + navigation, + options, + route, +}: ScreenLayoutArgs>) { useLayoutEffect(() => { const transitionStartListener = navigation.addListener('transitionStart', () => { TransitionTracker.startTransition(); @@ -25,4 +39,4 @@ function ScreenLayout({children, navigation: navigationProp, options, route}: Sc return children; } -export default ScreenLayout; +export default screenLayoutWrapper; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index 635d97cd2ed01..f0dcc9e016efd 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -13,7 +13,7 @@ import type { PlatformStackNavigatorProps, PlatformStackRouterOptions, } from '@libs/Navigation/PlatformStackNavigation/types'; -import ScreenLayout from '@libs/Navigation/PlatformStackNavigation/ScreenLayout'; +import screenLayout from '@libs/Navigation/PlatformStackNavigation/ScreenLayout'; function createPlatformStackNavigatorComponent( displayName: string, @@ -36,7 +36,6 @@ function createPlatformStackNavigatorComponent) { const { @@ -64,7 +63,7 @@ function createPlatformStackNavigatorComponent Date: Fri, 20 Feb 2026 15:41:45 +0100 Subject: [PATCH 29/40] Update ModalAfterTransition documentation to reflect migration to useConfirmModal hook, clarifying that showConfirmModal resolves after modal close transition completes, eliminating the need for explicit afterTransition callbacks. --- .../ModalAfterTransition.md | 97 ++++++++++--------- .../migrations/InteractionManager/README.md | 6 +- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md index 0b26a25a8db2e..ec1b0b0a97c96 100644 --- a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md +++ b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md @@ -1,26 +1,31 @@ -# Modal After Transition +# Modal After Transition Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. ## Strategy -**ConfirmModal ref + `afterTransition` AND ReanimatedModal `afterTransition`** +**Migrate to `useConfirmModal` hook** -Implement ref pattern for both ReanimatedModal and ConfirmModal. +The `showConfirmModal` function returned by `useConfirmModal` returns a Promise that resolves **after the modal close transition completes** (the underlying `closeModal` is called inside `onModalHide`, which fires after the animation finishes). This means any code that `await`s the result already runs post-transition — no explicit `afterTransition` callback is needed. -``` -const modalRef = useRef(null); +```typescript +const {showConfirmModal} = useConfirmModal(); -modalRef.current.close({afterTransition: () => { - // cleanup here -}}); +const {action} = await showConfirmModal({ + title: 'Delete items?', + prompt: 'This cannot be undone.', + confirmText: 'Delete', + danger: true, +}); -return ( - -); -``` +if (action !== ModalActions.CONFIRM) { + return; +} + +// This runs after the modal close transition — cleanup here +setSelectedItems([]); +deleteItems(); +``` Instead of: @@ -31,37 +36,39 @@ InteractionManager.runAfterInteractions(() => { }); ``` -## 1. ConfirmModal Pattern (ref + afterTransition) - -Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Use a ConfirmModal ref with `afterTransition` to defer the cleanup. - -| File | Line | Current | Migration | PR | -| ----------------------------------------- | ---- | ----------------------------------------------- | -------------------------------------------------- | ----------------------------------------------------- | -| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | Use ConfirmModal ref approach with afterTransition | [#54178](https://github.com/Expensify/App/pull/54178) | -| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | Use ConfirmModal ref approach with afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `ReportParticipantsPage.tsx` | 446 | Same pattern | Use ConfirmModal ref approach with afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | Use ConfirmModal ref approach with afterTransition | [#50488](https://github.com/Expensify/App/pull/50488) | -| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | Use ConfirmModal ref approach with afterTransition | [#56792](https://github.com/Expensify/App/pull/56792) | -| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | Use ConfirmModal ref approach with afterTransition | [#82523](https://github.com/Expensify/App/pull/82523) | -| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | Use ConfirmModal ref approach with afterTransition | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | Use ConfirmModal ref approach with afterTransition | [#74530](https://github.com/Expensify/App/pull/74530) | -| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | Use ConfirmModal ref approach with afterTransition | [#57373](https://github.com/Expensify/App/pull/57373) | -| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | Use ConfirmModal ref approach with afterTransition | [#56932](https://github.com/Expensify/App/pull/56932) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | Use ConfirmModal ref approach with afterTransition | [#52163](https://github.com/Expensify/App/pull/52163) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | Use ConfirmModal ref approach with afterTransition | [#48618](https://github.com/Expensify/App/pull/48618) | -| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | Use ConfirmModal ref approach with afterTransition | [#69146](https://github.com/Expensify/App/pull/69146) | -| `PopoverReportActionContextMenu.tsx` | 374 | `deleteReportComment()` after confirm | Use ConfirmModal ref approach with afterTransition | [#66791](https://github.com/Expensify/App/pull/66791) | -| `AutoSubmitModal.tsx` | 43 | `dismissASAPSubmitExplanation()` on modal close | Use ConfirmModal ref approach with afterTransition | [#63893](https://github.com/Expensify/App/pull/63893) | -| `useFilesValidation.tsx` | 108 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | -| `useFilesValidation.tsx` | 365 | InteractionManager deferring file validation | Use ConfirmModal ref approach with afterTransition | [#65316](https://github.com/Expensify/App/pull/65316) | -| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | Use ConfirmModal ref approach with afterTransition | [#35305](https://github.com/Expensify/App/pull/35305) | -| `IOURequestStepMerchant.tsx` | 181 | Merchant submit + keyboard dismiss | Use ConfirmModal ref approach with afterTransition | [#67010](https://github.com/Expensify/App/pull/67010) | -| `IOURequestStepDescription.tsx` | 226 | Description submit + keyboard dismiss | Use ConfirmModal ref approach with afterTransition | [#67010](https://github.com/Expensify/App/pull/67010) | +For **ReanimatedModal** usages, use ref pattern (ref.current.open({afterTransition: callback})) with `afterTransition` to defer work until after the modal close animation + +## 1. ConfirmModal Pattern (migrate to `useConfirmModal` hook) + +Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Migrate from inline `` component + `InteractionManager` to the `useConfirmModal` hook, whose promise resolves after the close transition. + +| File | Line | Current | Migration | PR | +| ----------------------------------------- | ---- | ----------------------------------------------- | --------------------------------- | ----------------------------------------------------- | +| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | Migrate to `useConfirmModal` hook | [#54178](https://github.com/Expensify/App/pull/54178) | +| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | Migrate to `useConfirmModal` hook | [#50488](https://github.com/Expensify/App/pull/50488) | +| `ReportParticipantsPage.tsx` | 446 | Same pattern | Migrate to `useConfirmModal` hook | [#50488](https://github.com/Expensify/App/pull/50488) | +| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | Migrate to `useConfirmModal` hook | [#50488](https://github.com/Expensify/App/pull/50488) | +| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | Migrate to `useConfirmModal` hook | [#56792](https://github.com/Expensify/App/pull/56792) | +| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | Migrate to `useConfirmModal` hook | [#82523](https://github.com/Expensify/App/pull/82523) | +| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | +| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | +| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | +| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | Migrate to `useConfirmModal` hook | [#74530](https://github.com/Expensify/App/pull/74530) | +| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | Migrate to `useConfirmModal` hook | [#57373](https://github.com/Expensify/App/pull/57373) | +| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | Migrate to `useConfirmModal` hook | [#56932](https://github.com/Expensify/App/pull/56932) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | Migrate to `useConfirmModal` hook | [#52163](https://github.com/Expensify/App/pull/52163) | +| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | Migrate to `useConfirmModal` hook | [#48618](https://github.com/Expensify/App/pull/48618) | +| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | Migrate to `useConfirmModal` hook | [#69146](https://github.com/Expensify/App/pull/69146) | +| `PopoverReportActionContextMenu.tsx` | 374 | `deleteReportComment()` after confirm | Migrate to `useConfirmModal` hook | [#66791](https://github.com/Expensify/App/pull/66791) | +| `AutoSubmitModal.tsx` | 43 | `dismissASAPSubmitExplanation()` on modal close | Migrate to `useConfirmModal` hook | [#63893](https://github.com/Expensify/App/pull/63893) | +| `useFilesValidation.tsx` | 108 | InteractionManager deferring file validation | Migrate to `useConfirmModal` hook | [#65316](https://github.com/Expensify/App/pull/65316) | +| `useFilesValidation.tsx` | 365 | InteractionManager deferring file validation | Migrate to `useConfirmModal` hook | [#65316](https://github.com/Expensify/App/pull/65316) | +| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | Migrate to `useConfirmModal` hook | [#35305](https://github.com/Expensify/App/pull/35305) | +| `IOURequestStepMerchant.tsx` | 181 | Merchant submit + keyboard dismiss | Migrate to `useConfirmModal` hook | [#67010](https://github.com/Expensify/App/pull/67010) | +| `IOURequestStepDescription.tsx` | 226 | Description submit + keyboard dismiss | Migrate to `useConfirmModal` hook | [#67010](https://github.com/Expensify/App/pull/67010) | ## 2. ReanimatedModal Pattern (afterTransition callback) diff --git a/contributingGuides/migrations/InteractionManager/README.md b/contributingGuides/migrations/InteractionManager/README.md index 156fc326616fa..373f117a343dc 100644 --- a/contributingGuides/migrations/InteractionManager/README.md +++ b/contributingGuides/migrations/InteractionManager/README.md @@ -25,11 +25,11 @@ On top of TransitionTracker, existing APIs gain transition-aware callbacks: - Navigation methods accept `afterTransition` — a callback that runs after the triggered navigation transition completes - Navigation methods accept `waitForTransition` — the call waits for all ongoing transitions to finish before navigating - Keyboard methods accept `afterTransition` — a callback that runs after the keyboard transition completes -- `ConfirmModal` and similar modal APIs will accept `afterTransition` callback to defer work until the modal close transition finishes (will be implemented in a separate PR) +- `useConfirmModal` hook's `showConfirmModal` returns a Promise that resolves **after the modal close transition completes**, so any work awaited after it naturally runs post-transition — no explicit `afterTransition` callback needed This makes the code self-descriptive: instead of a generic `runAfterInteractions`, each call site says exactly what it's waiting for and why. -> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `ConfirmModal`, etc.) rather than importing TransitionTracker directly. +> **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `useConfirmModal`, etc.) rather than importing TransitionTracker directly. ## How @@ -37,7 +37,7 @@ The migration is split into 9 subcategories. Each is worked on by a single perso 1. [NavigationWaitForTransition](./NavigationWaitForTransition.md) — Navigation calls that need to wait for an ongoing transition before proceeding 2. [NavigationAfterTransition](./NavigationAfterTransition.md) — Callbacks that should run after a navigation transition completes -3. [ModalAfterTransition](./ModalAfterTransition.md) — Work deferred until a modal close transition finishes +3. [ModalAfterTransition](./ModalAfterTransition.md) — Work deferred until a modal close transition finishes (migrate to `useConfirmModal` hook) 4. [ScrollOperations](./ScrollOperations.md) — Scroll-related operations that were waiting on transitions 5. [RequestIdleCallback](./RequestIdleCallback.md) — Non-urgent background work that should use `requestIdleCallback` instead 6. [TransitionTrackerDirect](./TransitionTrackerDirect.md) — Cases that need `TransitionTracker.runAfterTransitions` directly From aeb0f6aff8d28ef2aac9f0cea4db5f14a9811c9b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 20 Feb 2026 15:50:17 +0100 Subject: [PATCH 30/40] Add InteractionManager migration documentation, remove other docs --- .../README.md => INTERACTION_MANAGER.md} | 0 .../InputFocusManagement.md | 30 ------- .../ModalAfterTransition.md | 83 ------------------- .../NavigationAfterTransition.md | 52 ------------ .../NavigationWaitForTransition.md | 81 ------------------ .../InteractionManager/NeedsInvestigation.md | 23 ----- .../InteractionManager/OnboardingTours.md | 14 ---- .../InteractionManager/RequestIdleCallback.md | 30 ------- .../InteractionManager/ScrollOperations.md | 23 ----- .../TransitionTrackerDirect.md | 19 ----- 10 files changed, 355 deletions(-) rename contributingGuides/{migrations/InteractionManager/README.md => INTERACTION_MANAGER.md} (100%) delete mode 100644 contributingGuides/migrations/InteractionManager/InputFocusManagement.md delete mode 100644 contributingGuides/migrations/InteractionManager/ModalAfterTransition.md delete mode 100644 contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md delete mode 100644 contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md delete mode 100644 contributingGuides/migrations/InteractionManager/NeedsInvestigation.md delete mode 100644 contributingGuides/migrations/InteractionManager/OnboardingTours.md delete mode 100644 contributingGuides/migrations/InteractionManager/RequestIdleCallback.md delete mode 100644 contributingGuides/migrations/InteractionManager/ScrollOperations.md delete mode 100644 contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md diff --git a/contributingGuides/migrations/InteractionManager/README.md b/contributingGuides/INTERACTION_MANAGER.md similarity index 100% rename from contributingGuides/migrations/InteractionManager/README.md rename to contributingGuides/INTERACTION_MANAGER.md diff --git a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md b/contributingGuides/migrations/InteractionManager/InputFocusManagement.md deleted file mode 100644 index 96187abd672ec..0000000000000 --- a/contributingGuides/migrations/InteractionManager/InputFocusManagement.md +++ /dev/null @@ -1,30 +0,0 @@ -# Input Focus Management - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -Needs investigation. We need to figure out a way to handle this without using `InteractionManager.runAfterInteractions`. - -## Usages - -| File | Line | Description | PR | -| -------------------------------------------- | ---- | --------------------------------------------- | ----------------------------------------------------- | -| `InputFocus/index.website.ts` | 25 | Focus composer after modal | [#60073](https://github.com/Expensify/App/pull/60073) | -| `focusEditAfterCancelDelete/index.native.ts` | 6 | Focus text input after cancel/delete | [#36195](https://github.com/Expensify/App/pull/36195) | -| `useRestoreInputFocus/index.android.ts` | 15 | `KeyboardController.setFocusTo('current')` | [#54187](https://github.com/Expensify/App/pull/54187) | -| `useAutoFocusInput.ts` | 37 | Auto-focus input after interactions | [#31063](https://github.com/Expensify/App/pull/31063) | -| `FormProvider.tsx` | 427 | Set blur state in Safari | [#55494](https://github.com/Expensify/App/pull/55494) | -| `ContactPermissionModal/index.native.tsx` | 41 | Permission + focus after modal | [#54459](https://github.com/Expensify/App/pull/54459) | -| `ContactPermissionModal/index.native.tsx` | 59 | Permission + focus after modal | [#64207](https://github.com/Expensify/App/pull/64207) | -| `SearchRouter.tsx` | 346 | Focus search input after route | [#65183](https://github.com/Expensify/App/pull/65183) | -| `ShareRootPage.tsx` | 162 | Focus input after tab animation | [#63741](https://github.com/Expensify/App/pull/63741) | -| `EmojiPickerMenu/index.native.tsx` | 51 | Focus emoji search input | [#52009](https://github.com/Expensify/App/pull/52009) | -| `ReportActionItemMessageEdit.tsx` | 291 | Focus composer | [#28238](https://github.com/Expensify/App/pull/28238) | -| `ReportActionItemMessageEdit.tsx` | 545 | Focus composer | [#42965](https://github.com/Expensify/App/pull/42965) | -| `ComposerWithSuggestions.tsx` | 594 | Focus composer | [#74921](https://github.com/Expensify/App/pull/74921) | -| `MoneyRequestConfirmationList.tsx` | 1071 | `blurActiveElement()` after confirm | [#45873](https://github.com/Expensify/App/pull/45873) | -| `SplitListItem.tsx` | 75 | Focus input after screen transition | [#77657](https://github.com/Expensify/App/pull/77657) | -| `ContactMethodDetailsPage.tsx` | 215 | Focus after modal hide | [#54784](https://github.com/Expensify/App/pull/54784) | -| `ContactMethodDetailsPage.tsx` | 279 | Focus on entry transition end | [#55588](https://github.com/Expensify/App/pull/55588) | -| `BaseLoginForm.tsx` | 221 | InteractionManager deferring login navigation | [#42603](https://github.com/Expensify/App/pull/42603) | diff --git a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md b/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md deleted file mode 100644 index ec1b0b0a97c96..0000000000000 --- a/contributingGuides/migrations/InteractionManager/ModalAfterTransition.md +++ /dev/null @@ -1,83 +0,0 @@ -# Modal After Transition - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Migrate to `useConfirmModal` hook** - -The `showConfirmModal` function returned by `useConfirmModal` returns a Promise that resolves **after the modal close transition completes** (the underlying `closeModal` is called inside `onModalHide`, which fires after the animation finishes). This means any code that `await`s the result already runs post-transition — no explicit `afterTransition` callback is needed. - -```typescript -const {showConfirmModal} = useConfirmModal(); - -const {action} = await showConfirmModal({ - title: 'Delete items?', - prompt: 'This cannot be undone.', - confirmText: 'Delete', - danger: true, -}); - -if (action !== ModalActions.CONFIRM) { - return; -} - -// This runs after the modal close transition — cleanup here -setSelectedItems([]); -deleteItems(); -``` - -Instead of: - -``` -setModalVisible(false); -InteractionManager.runAfterInteractions(() => { - // cleanup here -}); -``` - -For **ReanimatedModal** usages, use ref pattern (ref.current.open({afterTransition: callback})) with `afterTransition` to defer work until after the modal close animation - -## 1. ConfirmModal Pattern (migrate to `useConfirmModal` hook) - -Nearly all workspace pages follow the same pattern: user confirms an action via a modal, then selections are cleared after the modal closes. Migrate from inline `` component + `InteractionManager` to the `useConfirmModal` hook, whose promise resolves after the close transition. - -| File | Line | Current | Migration | PR | -| ----------------------------------------- | ---- | ----------------------------------------------- | --------------------------------- | ----------------------------------------------------- | -| `WorkspaceMembersPage.tsx` | 272 | `setSelectedEmployees([])` + remove | Migrate to `useConfirmModal` hook | [#54178](https://github.com/Expensify/App/pull/54178) | -| `ReportParticipantsPage.tsx` | 251 | `setSelectedMembers([])` | Migrate to `useConfirmModal` hook | [#50488](https://github.com/Expensify/App/pull/50488) | -| `ReportParticipantsPage.tsx` | 446 | Same pattern | Migrate to `useConfirmModal` hook | [#50488](https://github.com/Expensify/App/pull/50488) | -| `RoomMembersPage.tsx` | 155 | `setSelectedMembers([])` | Migrate to `useConfirmModal` hook | [#50488](https://github.com/Expensify/App/pull/50488) | -| `WorkspaceCategoriesPage.tsx` | 346 | Category bulk action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceCategoriesSettingsPage.tsx` | 346 | Category settings cleanup | Migrate to `useConfirmModal` hook | [#56792](https://github.com/Expensify/App/pull/56792) | -| `WorkspaceTagsPage.tsx` | 437 | Tag bulk action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceViewTagsPage.tsx` | 221 | Tag view action cleanup | Migrate to `useConfirmModal` hook | [#82523](https://github.com/Expensify/App/pull/82523) | -| `WorkspaceTaxesPage.tsx` | 250 | Tax bulk action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | -| `PolicyDistanceRatesPage.tsx` | 317 | Distance rate action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspacePerDiemPage.tsx` | 267 | Per diem action cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | -| `ReportFieldsListValuesPage.tsx` | 195 | Report fields cleanup | Migrate to `useConfirmModal` hook | [#60023](https://github.com/Expensify/App/pull/60023) | -| `WorkspaceExpensifyCardDetailsPage.tsx` | 94 | Card details cleanup | Migrate to `useConfirmModal` hook | [#74530](https://github.com/Expensify/App/pull/74530) | -| `WorkspaceCompanyCardsSettingsPage.tsx` | 88 | Card settings cleanup | Migrate to `useConfirmModal` hook | [#57373](https://github.com/Expensify/App/pull/57373) | -| `WorkspaceWorkflowsPage.tsx` | 141 | Workflows action cleanup | Migrate to `useConfirmModal` hook | [#56932](https://github.com/Expensify/App/pull/56932) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 59 | Approvals edit cleanup | Migrate to `useConfirmModal` hook | [#52163](https://github.com/Expensify/App/pull/52163) | -| `WorkspaceWorkflowsApprovalsEditPage.tsx` | 73 | Approvals edit cleanup | Migrate to `useConfirmModal` hook | [#48618](https://github.com/Expensify/App/pull/48618) | -| `WorkspacesListPage.tsx` | 639 | Workspaces list action cleanup | Migrate to `useConfirmModal` hook | [#69146](https://github.com/Expensify/App/pull/69146) | -| `PopoverReportActionContextMenu.tsx` | 374 | `deleteReportComment()` after confirm | Migrate to `useConfirmModal` hook | [#66791](https://github.com/Expensify/App/pull/66791) | -| `AutoSubmitModal.tsx` | 43 | `dismissASAPSubmitExplanation()` on modal close | Migrate to `useConfirmModal` hook | [#63893](https://github.com/Expensify/App/pull/63893) | -| `useFilesValidation.tsx` | 108 | InteractionManager deferring file validation | Migrate to `useConfirmModal` hook | [#65316](https://github.com/Expensify/App/pull/65316) | -| `useFilesValidation.tsx` | 365 | InteractionManager deferring file validation | Migrate to `useConfirmModal` hook | [#65316](https://github.com/Expensify/App/pull/65316) | -| `ContactMethodDetailsPage.tsx` | 130 | Open delete modal after keyboard dismiss | Migrate to `useConfirmModal` hook | [#35305](https://github.com/Expensify/App/pull/35305) | -| `IOURequestStepMerchant.tsx` | 181 | Merchant submit + keyboard dismiss | Migrate to `useConfirmModal` hook | [#67010](https://github.com/Expensify/App/pull/67010) | -| `IOURequestStepDescription.tsx` | 226 | Description submit + keyboard dismiss | Migrate to `useConfirmModal` hook | [#67010](https://github.com/Expensify/App/pull/67010) | - - -## 2. ReanimatedModal Pattern (afterTransition callback) - -These usages involve ReanimatedModal's ref pattern to defer work until after the modal close animation. - -| File | Line | Current | Migration | PR | -| ------------------------------------- | ---- | --------------------------------------------- | ----------------------------------------------------- | ----------------------------------------------------- | -| `FeatureTrainingModal.tsx` | 224 | `setIsModalVisible` based on disabled state | Use ReanimatedModal ref approach with afterTransition | [#57649](https://github.com/Expensify/App/pull/57649) | -| `FeatureTrainingModal.tsx` | 352 | goBack + onClose after setting invisible | Use ReanimatedModal ref approach with afterTransition | [#53225](https://github.com/Expensify/App/pull/53225) | -| `AvatarCropModal/AvatarCropModal.tsx` | 324 | InteractionManager deferring onClose | Use ReanimatedModal ref approach with afterTransition | [#66890](https://github.com/Expensify/App/pull/66890) | -| `AttachmentModalHandler/index.ios.ts` | 12 | iOS: execute close callback after interaction | Use ReanimatedModal ref approach with afterTransition | [#53108](https://github.com/Expensify/App/pull/53108) | diff --git a/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md b/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md deleted file mode 100644 index 300bd4c1de13c..0000000000000 --- a/contributingGuides/migrations/InteractionManager/NavigationAfterTransition.md +++ /dev/null @@ -1,52 +0,0 @@ -# Navigation After Transition - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Use `Navigation.navigate/goBack/dismissModal({afterTransition: callback})`** - -Every usage in this file defers work until after a screen/modal transition completes. Replace each `InteractionManager.runAfterInteractions()` call with the `afterTransition` option available on Navigation methods. - -`TransitionTracker.runAfterTransitions()` should **never** be called directly — it is already wired into `Navigation.ts` internally. - -| File | Line | Current | Migration | PR | -| ------------------------------------------- | -------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | -| `IOU/index.ts` | 6374 | `removeDraftTransactions()` after split bill | Pass as `afterTransition` to the navigation call in `handleNavigateAfterExpenseCreate` | [#61574](https://github.com/Expensify/App/pull/61574) | -| `IOU/index.ts` | 6501 | `removeDraftTransaction()` after per diem | Same — thread through navigation | [#54760](https://github.com/Expensify/App/pull/54760) | -| `IOU/index.ts` | 6834 | `removeDraftTransactions()` after distance request | Same | [#78109](https://github.com/Expensify/App/pull/78109) | -| `IOU/index.ts` | 7522 | `removeDraftTransaction()` | Same | [#53852](https://github.com/Expensify/App/pull/53852) | -| `IOU/index.ts` | 7613 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 8281 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 8539 | `removeDraftTransaction()` | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 9059 | Clear draft transaction data | Same | [#51940](https://github.com/Expensify/App/pull/51940) | -| `IOU/index.ts` | 14208 | `removeDraftSplitTransaction()` | Same | [#79648](https://github.com/Expensify/App/pull/79648) | -| `IOU/SendInvoice.ts` | 787 | `removeDraftTransaction()` after invoice | Same | [#78512](https://github.com/Expensify/App/pull/78512) | -| `Task.ts` | 319 | `clearOutTaskInfo()` + `dismissModalWithReport` | Use `dismissModalWithReport({afterTransition: () => clearOutTaskInfo()})` | [#57864](https://github.com/Expensify/App/pull/57864) | -| `ConfirmationStep.tsx` | 76 | `clearAssignCardStepAndData()` after `dismissModal()` | Use `Navigation.dismissModal({afterTransition: () => clearAssignCardStepAndData()})` | [#58630](https://github.com/Expensify/App/pull/58630) | -| `TwoFactorAuthActions.ts` | 28 | `clearTwoFactorAuthData()` after `goBack()` | Use `Navigation.goBack({afterTransition: () => clearTwoFactorAuthData()})` | [#54404](https://github.com/Expensify/App/pull/54404) | -| `Report/index.ts` | 1176 | `clearGroupChat()` during creation | Use `Navigation.afterTransition` on the subsequent navigation | [#57864](https://github.com/Expensify/App/pull/57864) | -| `Report/index.ts` | 1909 | Clear report data | Navigation afterTransition | [#47033](https://github.com/Expensify/App/pull/47033) | -| `Report/index.ts` | 3328 | `deleteReport()` after `goBack()/popToSidebar()` | `Navigation.goBack({afterTransition: () => deleteReport()})` | [#66890](https://github.com/Expensify/App/pull/66890) | -| `Report/index.ts` | 5814 | Clear report data | Navigation afterTransition | [#66890](https://github.com/Expensify/App/pull/66890) | -| `MoneyRequestReportView.tsx` | 137 | `removeFailedReport()` after `goBackFromSearchMoneyRequest()` | Thread afterTransition through goBack | [#59386](https://github.com/Expensify/App/pull/59386) | -| `ReportScreen.tsx` | 493 | Clear `deleteTransactionNavigateBackUrl` | `Navigation.afterTransition` (post-navigation cleanup) | [#52740](https://github.com/Expensify/App/pull/52740) | -| `ReportScreen.tsx` | 681 | `setShouldShowComposeInput(true)` on mount | `Navigation.afterTransition` (show after screen entry) | [#38255](https://github.com/Expensify/App/pull/38255) | -| `ReportScreen.tsx` | 884 | Subscribe to report leaving events | `Navigation.afterTransition` (after report screen loads) | [#30269](https://github.com/Expensify/App/pull/30269) | -| `ReportActionsView.tsx` | 286 | Set `navigatingToLinkedMessage` state | `Navigation.afterTransition` then `setTimeout(10)` | [#30269](https://github.com/Expensify/App/pull/30269) | -| `BaseReportActionContextMenu.tsx` | 317 | `signOutAndRedirectToSignIn()` after hiding menu | Use context menu's `hideContextMenu` callback with afterTransition | [#33715](https://github.com/Expensify/App/pull/33715) | -| `PureReportActionItem.tsx` | 1740 | Sign out and redirect | `Navigation.navigate({afterTransition})` or `requestAnimationFrame` | [#52948](https://github.com/Expensify/App/pull/52948) | -| `MoneyReportHeader.tsx` | 1503 | Delete transaction after nav setup | `Navigation.goBack({afterTransition: () => deleteTransactions()})` | [#74605](https://github.com/Expensify/App/pull/74605) | -| `MoneyReportHeader.tsx` | 1526 | Delete report after goBack | `Navigation.goBack({afterTransition: () => deleteAppReport()})` | [#79539](https://github.com/Expensify/App/pull/79539) | -| `StatusPage.tsx` | 136 | `clearDraftCustomStatus()` + navigate | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition: () => clearDraft()})` | [#40364](https://github.com/Expensify/App/pull/40364) | -| `StatusPage.tsx` | 157 | Navigate back after status action | Remove navigateBackToPreviousScreenTask and use `Navigation.goBack({afterTransition})` | [#40364](https://github.com/Expensify/App/pull/40364) | -| `TwoFactorAuth/VerifyPage.tsx` | 79 | 2FA verify navigation | Investigate if InteractionManager is needed | [#67762](https://github.com/Expensify/App/pull/67762) | -| `DebugReportActionPage.tsx` | 69 | Defer deletion during nav | Use `Navigation.goBack({afterTransition: () => deleteAction()})` | [#53655](https://github.com/Expensify/App/pull/53655) | -| `DebugTransactionPage.tsx` | 65 | Defer transaction deletion | Use `Navigation.goBack({afterTransition: () => deleteTx()})` | [#53655](https://github.com/Expensify/App/pull/53655) | -| `DebugTransactionViolationPage.tsx` | 53 | Defer violation deletion | Use `Navigation.goBack({afterTransition: () => deleteViolation()})` | [#53969](https://github.com/Expensify/App/pull/53969) | -| `AddUnreportedExpenseFooter.tsx` | 57 | Bulk convert after `dismissToSuperWideRHP()` | Use `Navigation.dismissToSuperWideRHP({afterTransition: () => convert()})` | [#79328](https://github.com/Expensify/App/pull/79328) | -| `RightModalNavigator.tsx` | 130, 198 | Save scroll offsets / clear 2FA | Use `Navigation.afterTransition` in the navigator | [#69531](https://github.com/Expensify/App/pull/69531), [#79473](https://github.com/Expensify/App/pull/79473) | -| `NetSuiteImportAddCustomSegmentContent.tsx` | 51 | NetSuite segment navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | -| `NetSuiteImportAddCustomListContent.tsx` | 48 | NetSuite list navigation | Navigation afterTransition | [#51109](https://github.com/Expensify/App/pull/51109) | - - diff --git a/contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md b/contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md deleted file mode 100644 index a41770258bd6a..0000000000000 --- a/contributingGuides/migrations/InteractionManager/NavigationWaitForTransition.md +++ /dev/null @@ -1,81 +0,0 @@ -# Navigation waitForTransition - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Use `Navigation.navigate/goBack/dismissModal` with `{ waitForTransition: true }`** - -These call sites wrap `Navigation.navigate`, `Navigation.goBack`, or `Navigation.dismissModal` in `InteractionManager.runAfterInteractions`. Since `navigate`, `goBack`, and `dismissModal` now accept a `waitForTransition` option that internally defers through `TransitionTracker.runAfterTransitions`, the `InteractionManager` wrapper can be removed and replaced with the option. - -Instead of: - -```ts -InteractionManager.runAfterInteractions(() => { - Navigation.navigate(ROUTES.SOME_ROUTE); -}); -``` - -Use: - -```ts -Navigation.navigate(ROUTES.SOME_ROUTE, { waitForTransition: true }); -``` - -Instead of: - -```ts -InteractionManager.runAfterInteractions(() => { - Navigation.goBack(ROUTES.SOME_ROUTE); -}); -``` - -Use: - -```ts -Navigation.goBack(ROUTES.SOME_ROUTE, { waitForTransition: true }); -``` - -## Navigation Utility Files - -These utility files exist solely to wrap navigation calls in `InteractionManager` and should be removed entirely once the migration is complete. - -| File | Line | Current | Migration | PR | -| --------------------------------------- | ---- | -------------------------------- | ----------------------------------------------- | ----------------------------------------------------- | -| `navigateAfterInteraction/index.ios.ts` | 10 | iOS wrapper deferring navigation | Remove file — `waitForTransition` replaces this | [#56865](https://github.com/Expensify/App/pull/56865) | -| `navigateAfterInteraction/index.ts` | 1 | Non-iOS passthrough | Remove file | [#56865](https://github.com/Expensify/App/pull/56865) | - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------------------ | ---------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `ImportedMembersPage.tsx` | 204 | `InteractionManager` wrapping `Navigation.goBack` in `onModalHide` | `Navigation.goBack(route, { waitForTransition: true })` | [#69436](https://github.com/Expensify/App/pull/69436) | -| `ImportedMembersConfirmationPage.tsx` | 210 | `InteractionManager` wrapping `Navigation.goBack` in `onModalHide` | `Navigation.goBack(route, { waitForTransition: true })` | [#75515](https://github.com/Expensify/App/pull/75515) | -| `AdminTestDriveModal.tsx` | 21 | `InteractionManager` wrapping `Navigation.navigate` | `Navigation.navigate(route, { waitForTransition: true })` | [#60997](https://github.com/Expensify/App/pull/60997) | -| `EmployeeTestDriveModal.tsx` | 110 | `InteractionManager` wrapping `Navigation.navigate` after `goBack` | `Navigation.navigate(route, { waitForTransition: true })` | [#63260](https://github.com/Expensify/App/pull/63260) | -| `TaskAssigneeSelectorModal.tsx` | 181 | `InteractionManager` wrapping `Navigation.dismissModalWithReport` | Remove `InteractionManager` wrapper (delegates to `navigate`/`dismissModal` internally) | [#81320](https://github.com/Expensify/App/pull/81320) | -| `TaskAssigneeSelectorModal.tsx` | 194 | `InteractionManager` wrapping `Navigation.goBack` | `Navigation.goBack(route, { waitForTransition: true })` | [#81320](https://github.com/Expensify/App/pull/81320) | -| `IOU/index.ts` | 1154 | `InteractionManager` wrapping `Navigation.navigate` after `dismissModal` | `Navigation.navigate(route, { waitForTransition: true })` | [#81580](https://github.com/Expensify/App/pull/81580) | -| `Report/index.ts` | 5912 | `InteractionManager` wrapping `Navigation.navigate` | `Navigation.navigate(route, { waitForTransition: true })` | [#66890](https://github.com/Expensify/App/pull/66890) | -| `SubmitDetailsPage.tsx` | 263 | `navigateAfterInteraction` call | `Navigation.navigate(route, { waitForTransition: true })` | [#58834](https://github.com/Expensify/App/pull/58834) | -| `TestToolsModalPage.tsx` | 73 | `navigateAfterInteraction` call | `Navigation.navigate(route, { waitForTransition: true })` | [#64717](https://github.com/Expensify/App/pull/64717) | -| `IOURequestStepConfirmation.tsx` | 1390, 1396 | `navigateAfterInteraction` calls | `Navigation.navigate(route, { waitForTransition: true })` | [#58422](https://github.com/Expensify/App/pull/58422) | -| `DiscardChangesConfirmation/index.tsx` | 32, 62 | Toggle visibility after discard | `Navigation.navigate(route, { waitForTransition: true })` | [#58422](https://github.com/Expensify/App/pull/58422) | -| `WorkspaceMemberDetailsPage.tsx` | 170 | Go back after member action | `Navigation.goBack(route, { waitForTransition: true })` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `FloatingActionButtonAndPopover.tsx` | 702 | FAB menu item action | `Navigation.navigate(route, { waitForTransition: true })` | [#56865](https://github.com/Expensify/App/pull/56865) | -| `Session/index.ts` | 1248 | Navigate after sign-in | `Navigation.navigate(route, { waitForTransition: true })` | [#30269](https://github.com/Expensify/App/pull/30269) | -| `Link.ts` | 305 | Deep link navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#74237](https://github.com/Expensify/App/pull/74237) | -| `Tour.ts` | 9 | Tour navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#67348](https://github.com/Expensify/App/pull/67348) | -| `TestDriveDemo.tsx` | 68, 76 | Set visibility / go back | `Navigation.goBack({ waitForTransition: true })` | [#60085](https://github.com/Expensify/App/pull/60085) | -| `WorkspaceDowngradePage.tsx` | 70, 84 | Navigate after downgrade | `Navigation.navigate/dismissModal({ waitForTransition: true })` | [#71333](https://github.com/Expensify/App/pull/71333) | -| `AccountDetailsPage.tsx` | 87 | Navigate after `useFocusEffect` triggers | `Navigation.navigate(route, { waitForTransition: true })` | [#59911](https://github.com/Expensify/App/pull/59911) | -| `AccountDetailsPage.tsx` | 116 | Navigate after `useFocusEffect` triggers | `Navigation.navigate(route, { waitForTransition: true })` | [#65834](https://github.com/Expensify/App/pull/65834) | -| `AccountValidatePage.tsx` | 128 | Navigate after `useFocusEffect` triggers | `Navigation.navigate(route, { waitForTransition: true })` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `IOURequestStepReport.tsx` | 156 | Report selection + navigate | `Navigation.goBack(route, { waitForTransition: true })` | [#67048](https://github.com/Expensify/App/pull/67048) | -| `IOURequestStepReport.tsx` | 211 | Report selection + navigate | `Navigation.goBack(route, { waitForTransition: true })` | [#67925](https://github.com/Expensify/App/pull/67925) | -| `IOURequestStepScan/index.native.tsx` | 374 | Scan step navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#63451](https://github.com/Expensify/App/pull/63451) | -| `IOURequestStepScan/ReceiptView/index.tsx` | 70 | Receipt view navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#63352](https://github.com/Expensify/App/pull/63352) | -| `IOURequestStepScan/index.tsx` | 624 | Scan step navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#63451](https://github.com/Expensify/App/pull/63451) | -| `SplitExpensePage.tsx` | 392 | Split expense navigation | `Navigation.navigate(route, { waitForTransition: true })` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `IOURequestStepCategory.tsx` | 210 | Category selection + keyboard dismiss | `Navigation.navigate(route, { waitForTransition: true })` | [#53316](https://github.com/Expensify/App/pull/53316) | -| `IOURequestStepDestination.tsx` | 201 | Keyboard dismiss + navigate | `KeyboardUtils.dismiss({afterTransition: () => Navigation.goBack()})` | [#66747](https://github.com/Expensify/App/pull/66747) | diff --git a/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md b/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md deleted file mode 100644 index af5a9f619ff9e..0000000000000 --- a/contributingGuides/migrations/InteractionManager/NeedsInvestigation.md +++ /dev/null @@ -1,23 +0,0 @@ -# Needs Investigation - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Requires deeper investigation before choosing a migration approach** - -These usages don't clearly fit into any of the standard migration patterns and need further analysis. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------------ | ---- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `DatePicker/index.tsx` | 107 | InteractionManager deferring popover position | Need deeper investigation | [#62354](https://github.com/Expensify/App/pull/62354) | -| `DatePicker/index.tsx` | 118 | InteractionManager deferring handlePress | Need deeper investigation | [#56068](https://github.com/Expensify/App/pull/56068) | -| `PlaidConnectionStep.tsx` | 138 | Plaid connection navigation | Navigation afterTransition | [#64741](https://github.com/Expensify/App/pull/64741) | -| `OptionRow.tsx` | 195 | InteractionManager re-enabling row | Need deeper investigation / `requestAnimationFrame(() => setIsDisabled(false))` (yield to UI) | [#14426](https://github.com/Expensify/App/pull/14426) | -| `MoneyReportHeader.tsx` | 574 | iOS-only: show hold menu after interaction | Need deeper investigation / `requestAnimationFrame(() => setIsHoldMenuVisible(true))` (iOS animation workaround) | [#66790](https://github.com/Expensify/App/pull/66790) | -| `useSingleExecution/index.native.ts` | 27 | InteractionManager resetting `isExecuting` | Need deeper investigation / `requestAnimationFrame(() => setIsExecuting(false))` — yield to allow UI updates before resetting state, if it doesn't work use `TransitionTracker.runAfterTransitions` | [#24173](https://github.com/Expensify/App/pull/24173) | -| `WorkspaceNewRoomPage.tsx` | 136 | `addPolicyReport()` deferred | Need deeper investigation / `requestAnimationFrame(() => addPolicyReport())` (no navigation involved) | [#59207](https://github.com/Expensify/App/pull/59207) | -| `NewTaskPage.tsx` | 63 | `blurActiveElement()` on focus | Need deeper investigation | [#79597](https://github.com/Expensify/App/pull/79597) | -| `IOURequestStepSubrate.tsx` | 234 | Subrate selection + keyboard dismiss | `KeyboardUtils.dismiss({afterTransition})` | [#56347](https://github.com/Expensify/App/pull/56347) | diff --git a/contributingGuides/migrations/InteractionManager/OnboardingTours.md b/contributingGuides/migrations/InteractionManager/OnboardingTours.md deleted file mode 100644 index 9b7bbcca0cdf1..0000000000000 --- a/contributingGuides/migrations/InteractionManager/OnboardingTours.md +++ /dev/null @@ -1,14 +0,0 @@ -# Onboarding Tours - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -Onboarding usages defer navigation to the next onboarding step until after the current screen transition completes. - -## Usages - -| File | Line | Current | Migration | PR | -| -------------------------------------- | ---- | ---------------------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------- | -| `useOnboardingFlow.ts` | 58 | InteractionManager deferring onboarding start | Use TransitionTracker.runAfterTransitions | [#77874](https://github.com/Expensify/App/pull/77874) | -| `BaseOnboardingInterestedFeatures.tsx` | 217 | InteractionManager deferring feature step navigation | Add afterTransition to navigateAfterOnboardingWithMicrotaskQueue | [#79122](https://github.com/Expensify/App/pull/79122) | diff --git a/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md b/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md deleted file mode 100644 index 71711b68b53ce..0000000000000 --- a/contributingGuides/migrations/InteractionManager/RequestIdleCallback.md +++ /dev/null @@ -1,30 +0,0 @@ -# requestIdleCallback - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Use `requestIdleCallback` for non-urgent background work** - -Pusher subscriptions, typing event listeners, search API calls, and contact imports are non-urgent background work. They should not block rendering or animations. `requestIdleCallback` schedules them during idle periods. - -## 1. Realtime Subscriptions - -| File | Line | Current | Migration | PR | -| ----------------------------- | ---- | ------------------------------------------ | ------------------------------------------------------- | ----------------------------------------------------- | -| `Pusher/index.ts` | 206 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#53751](https://github.com/Expensify/App/pull/53751) | -| `Pusher/index.native.ts` | 211 | InteractionManager wrapping subscribe call | Replace with `requestIdleCallback(() => subscribe())` | [#56610](https://github.com/Expensify/App/pull/56610) | -| `UserTypingEventListener.tsx` | 35 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | -| `UserTypingEventListener.tsx` | 49 | Store ref for InteractionManager handle | Update to store `requestIdleCallback` handle ref | [#39347](https://github.com/Expensify/App/pull/39347) | -| `UserTypingEventListener.tsx` | 59 | InteractionManager wrapping subscribe | Replace with `requestIdleCallback(() => subscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | -| `UserTypingEventListener.tsx` | 70 | InteractionManager wrapping unsubscribe | Replace with `requestIdleCallback(() => unsubscribe())` | [#39347](https://github.com/Expensify/App/pull/39347) | - ---- - -## 2. Search API Operations - -| File | Line | Current | Migration | PR | -| -------------------------------------- | ---- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| `useSearchHighlightAndScroll.ts` | 126 | InteractionManager deferring search API call | Try to use `requestIdleCallback(() => search(...))` or `startTransition` to defer the search API call | [#69713](https://github.com/Expensify/App/pull/69713) | -| `useSearchSelector.native.ts` | 27 | InteractionManager deferring contact import | Try to use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#70700](https://github.com/Expensify/App/pull/70700) | -| `MoneyRequestParticipantsSelector.tsx` | 431 | InteractionManager deferring contact import | Use `requestIdleCallback(importAndSaveContacts)` or `startTransition` to defer the contact import | [#54459](https://github.com/Expensify/App/pull/54459) | diff --git a/contributingGuides/migrations/InteractionManager/ScrollOperations.md b/contributingGuides/migrations/InteractionManager/ScrollOperations.md deleted file mode 100644 index 5f3aa0f5aa5eb..0000000000000 --- a/contributingGuides/migrations/InteractionManager/ScrollOperations.md +++ /dev/null @@ -1,23 +0,0 @@ -# Scroll Operations - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Use `requestAnimationFrame(() => callback())`** - -Scroll operations are layout-dependent and need the current frame's layout to be committed before executing. `requestAnimationFrame(() => callback())` is the correct primitive here — it ensures the scroll happens after the browser/native has painted the current frame. - -## Usages - -| File | Line | Current | Migration | PR | -| ----------------------------------------------- | ---- | ---------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------- | -| `ReportActionItemEventHandler/index.android.ts` | 7 | `InteractionManager.runAfterInteractions(() => rAF)` | Verify if this is still needed | [#44428](https://github.com/Expensify/App/pull/44428) | -| `FormWrapper.tsx` | 199 | Nested `InteractionManager + rAF` | Replace with just `requestAnimationFrame(() => scrollToEnd())` | [#79597](https://github.com/Expensify/App/pull/79597) | -| `MoneyRequestReportActionsList.tsx` | 293 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadOlderChats())` | [#59664](https://github.com/Expensify/App/pull/59664) | -| `MoneyRequestReportActionsList.tsx` | 514 | InteractionManager wrapping scroll | Replace with `requestAnimationFrame(() => scrollToBottom())` | [#59664](https://github.com/Expensify/App/pull/59664) | -| `ReportActionsList.tsx` | 837 | InteractionManager wrapping load call | Replace with `requestAnimationFrame(() => loadNewerChats())` | [#49477](https://github.com/Expensify/App/pull/49477) | -| `ReportActionsList.tsx` | 494 | Hide counter + scroll to bottom on mount | Replace with `requestAnimationFrame` | [#55350](https://github.com/Expensify/App/pull/55350) | -| `ReportActionsList.tsx` | 513 | Safari scroll to bottom for whisper | Just remove, no longer needed | [#55350](https://github.com/Expensify/App/pull/55350) | -| `ReportActionsList.tsx` | 526 | Scroll to bottom for current user action | Replace with `requestAnimationFrame` | [#52955](https://github.com/Expensify/App/pull/52955) | -| `ReportActionsList.tsx` | 617 | Scroll to bottom for IOU error | Replace with `requestAnimationFrame` | [#58793](https://github.com/Expensify/App/pull/58793) | \ No newline at end of file diff --git a/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md b/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md deleted file mode 100644 index 00df17417a790..0000000000000 --- a/contributingGuides/migrations/InteractionManager/TransitionTrackerDirect.md +++ /dev/null @@ -1,19 +0,0 @@ -# TransitionTracker Direct Usage - -Refer to [README.md](./README.md) for more information what's the overall strategy and why we're migrating away from `InteractionManager.runAfterInteractions`. - -## Strategy - -**Use `TransitionTracker.runAfterTransitions` ** - -These are utility files where `TransitionTracker.runAfterTransitions` should be called directly. This is the exception to the general rule — application code should use `Navigation.afterTransition` or `KeyboardUtils.dismiss({afterTransition})` instead, but these utility files need the direct API. - -## Usages - -| File | Line | Current | Migration | PR | -| ------------------------------------ | ---- | --------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | -| `Performance.tsx` | 49 | InteractionManager wrapping TTI measurement | Use `TransitionTracker.runAfterTransitions` | [#54412](https://github.com/Expensify/App/pull/54412) | -| `Lottie/index.tsx` | 44 | InteractionManager gating Lottie rendering | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | -| `BackgroundImage/index.native.tsx` | 38 | InteractionManager deferring background load | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | -| `BackgroundImage/index.tsx` | 38 | InteractionManager deferring background load | Use `TransitionTracker.runAfterTransitions` | [#48143](https://github.com/Expensify/App/pull/48143) | -| `TopLevelNavigationTabBar/index.tsx` | 54 | InteractionManager detecting animation finish | Use `TransitionTracker.runAfterTransitions` | [#49539](https://github.com/Expensify/App/pull/49539) | From 4acf8215e61aced32c6e63781783cb5107019f95 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 20 Feb 2026 16:00:06 +0100 Subject: [PATCH 31/40] Refine InteractionManager migration documentation --- contributingGuides/INTERACTION_MANAGER.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/contributingGuides/INTERACTION_MANAGER.md b/contributingGuides/INTERACTION_MANAGER.md index 373f117a343dc..08008b0b42ada 100644 --- a/contributingGuides/INTERACTION_MANAGER.md +++ b/contributingGuides/INTERACTION_MANAGER.md @@ -2,17 +2,17 @@ ## Why -`InteractionManager` is being removed from React Native. We currently maintain a patch to keep it working, but that's a temporary measure — upstream libraries will also drop support over time. +`InteractionManager` is being removed from React Native. We currently maintain a patch to keep it working, but that's a temporary measure and upstream libraries will also drop support over time. -Rather than keep patching, we're replacing `InteractionManager.runAfterInteractions` with purpose-built alternatives that are more precise and self-descriptive. +Rather than keep patching, we're replacing `InteractionManager.runAfterInteractions` with purpose-built alternatives that are more precise. ## Current state -`runAfterInteractions` is used across the codebase for a wide range of reasons: waiting for navigation transitions, deferring work after modals close, managing input focus, delaying scroll operations, and many other cases that are harder to classify. +`runAfterInteractions` is used across the codebase for a wide range of reasons: waiting for navigation transitions, deferring work after modals close, managing input focus, delaying scroll operations, and many other cases that are hard to classify. ## The problem -`runAfterInteractions` is a global queue with no granularity. You can't say "after *this specific* transition" — it just means "after all current interactions finish." This made it a convenient catch-all, but the intent behind each call is often unclear. Many usages exist simply because it "just worked" as a timing workaround, not because it was the right tool for the job. +`runAfterInteractions` is a global queue with no granularity. This made it a convenient catch-all, but the intent behind each call is often unclear. Many usages exist simply because it "just worked" as a timing workaround, not because it was the right tool for the job. This makes the migration non-trivial: you have to understand *what each call is actually waiting for* before you can pick the right replacement. @@ -32,18 +32,7 @@ This makes the code self-descriptive: instead of a generic `runAfterInteractions > **Note:** `TransitionTracker.runAfterTransitions` is an internal primitive. Application code should use the higher-level APIs (`Navigation`, `useConfirmModal`, etc.) rather than importing TransitionTracker directly. ## How - -The migration is split into 9 subcategories. Each is worked on by a single person, with multiple PRs per category. The subcategory docs serve as sub-issues tracking the specific call sites and their replacement strategy: - -1. [NavigationWaitForTransition](./NavigationWaitForTransition.md) — Navigation calls that need to wait for an ongoing transition before proceeding -2. [NavigationAfterTransition](./NavigationAfterTransition.md) — Callbacks that should run after a navigation transition completes -3. [ModalAfterTransition](./ModalAfterTransition.md) — Work deferred until a modal close transition finishes (migrate to `useConfirmModal` hook) -4. [ScrollOperations](./ScrollOperations.md) — Scroll-related operations that were waiting on transitions -5. [RequestIdleCallback](./RequestIdleCallback.md) — Non-urgent background work that should use `requestIdleCallback` instead -6. [TransitionTrackerDirect](./TransitionTrackerDirect.md) — Cases that need `TransitionTracker.runAfterTransitions` directly -7. [OnboardingTours](./OnboardingTours.md) — Onboarding and guided tour timing -8. [InputFocusManagement](./InputFocusManagement.md) — Input focus operations deferred until transitions settle -9. [NeedsInvestigation](./NeedsInvestigation.md) — Call sites that need further analysis to determine the right replacement +The migration is split into 9 issues. Current status of the migration can be found in the parent Github issue [here](https://github.com/Expensify/App/issues/71913). ## Primitives comparison From b2186f37b132c26e55c0994fe09e2ab9ff4258d5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 23 Feb 2026 10:40:22 +0100 Subject: [PATCH 32/40] Refactor dismissModal and dismissModalWithReport functions to use promises for isNavigationReady --- src/libs/Navigation/Navigation.ts | 69 ++++++++++++++++--------------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index c238494030266..99fd138de4f20 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -738,22 +738,22 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -async function dismissModal({ +function dismissModal({ ref = navigationRef, afterTransition, waitForTransition, }: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void; waitForTransition?: boolean} = {}) { clearSelectedText(); - await isNavigationReady(); - const runImmediately = !waitForTransition; - TransitionTracker.runAfterTransitions(() => { - ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + isNavigationReady().then(() => { + TransitionTracker.runAfterTransitions(() => { + ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); - if (afterTransition) { - TransitionTracker.runAfterTransitions(afterTransition); - } - }, runImmediately); + if (afterTransition) { + TransitionTracker.runAfterTransitions(afterTransition); + } + }, runImmediately); + }); } /** @@ -761,32 +761,35 @@ async function dismissModal({ * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -async function dismissModalWithReport({reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef) { - await isNavigationReady(); - const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); - let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; +function dismissModalWithReport({reportID, reportActionID, referrer, backTo}: ReportsSplitNavigatorParamList[typeof SCREENS.REPORT], ref = navigationRef) { + isNavigationReady().then(() => { + const topmostSuperWideRHPReportID = getTopmostSuperWideRHPReportID(); + let areReportsIDsDefined = !!topmostSuperWideRHPReportID && !!reportID; - if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { - dismissToSuperWideRHP(); - return; - } + if (topmostSuperWideRHPReportID === reportID && areReportsIDsDefined) { + dismissToSuperWideRHP(); + return; + } - const topmostReportID = getTopmostReportId(); - areReportsIDsDefined = !!topmostReportID && !!reportID; - const isReportsSplitTopmostFullScreen = ref.getRootState().routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; - if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { - dismissModal(); - return; - } - const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, referrer, backTo); - if (getIsNarrowLayout()) { - navigate(reportRoute, {forceReplace: true}); - return; - } - dismissModal({ - afterTransition: () => { - navigate(reportRoute); - }, + const topmostReportID = getTopmostReportId(); + areReportsIDsDefined = !!topmostReportID && !!reportID; + const isReportsSplitTopmostFullScreen = ref.getRootState().routes.findLast((route) => isFullScreenName(route.name))?.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR; + if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) { + dismissModal(); + return; + } + + const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, referrer, backTo); + if (getIsNarrowLayout()) { + navigate(reportRoute, {forceReplace: true}); + return; + } + + dismissModal({ + afterTransition: () => { + navigate(reportRoute); + }, + }); }); } From 620b48a0ff21736ade588bd0bb130a687aa3ebdb Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 23 Feb 2026 17:03:16 +0100 Subject: [PATCH 33/40] Fix prettier --- src/components/ConnectToNetSuiteFlow/index.tsx | 2 +- src/components/Modal/ReanimatedModal/index.tsx | 2 +- src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx | 2 +- .../createPlatformStackNavigatorComponent/index.tsx | 2 +- src/pages/workspace/accounting/netsuite/utils.ts | 4 ++-- src/utils/keyboard/index.ts | 2 +- src/utils/keyboard/index.website.ts | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ConnectToNetSuiteFlow/index.tsx b/src/components/ConnectToNetSuiteFlow/index.tsx index 28a87da7e8ced..52824a331fa6c 100644 --- a/src/components/ConnectToNetSuiteFlow/index.tsx +++ b/src/components/ConnectToNetSuiteFlow/index.tsx @@ -8,11 +8,11 @@ import {isAuthenticationError} from '@libs/actions/connections'; import {getAdminPoliciesConnectedToNetSuite} from '@libs/actions/Policy/Policy'; import Navigation from '@libs/Navigation/Navigation'; import {useAccountingContext} from '@pages/workspace/accounting/AccountingContext'; +import {getInitialSubPageForNetsuiteTokenInput} from '@pages/workspace/accounting/netsuite/utils'; import type {AnchorPosition} from '@styles/index'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {getInitialSubPageForNetsuiteTokenInput} from '@pages/workspace/accounting/netsuite/utils'; import type {ConnectToNetSuiteFlowProps} from './types'; function ConnectToNetSuiteFlow({policyID}: ConnectToNetSuiteFlowProps) { diff --git a/src/components/Modal/ReanimatedModal/index.tsx b/src/components/Modal/ReanimatedModal/index.tsx index 3280344957bd8..cf79323627305 100644 --- a/src/components/Modal/ReanimatedModal/index.tsx +++ b/src/components/Modal/ReanimatedModal/index.tsx @@ -9,9 +9,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import blurActiveElement from '@libs/Accessibility/blurActiveElement'; import getPlatform from '@libs/getPlatform'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import Backdrop from './Backdrop'; import Container from './Container'; import type ReanimatedModalProps from './types'; diff --git a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx index 1115008e84bc9..5d2c219708781 100644 --- a/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/ScreenLayout.tsx @@ -1,6 +1,6 @@ -import React, {useLayoutEffect} from 'react'; import type {ParamListBase, ScreenLayoutArgs} from '@react-navigation/native'; import type {StackNavigationOptions, StackNavigationProp} from '@react-navigation/stack'; +import React, {useLayoutEffect} from 'react'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {PlatformStackNavigationOptions, PlatformStackNavigationProp} from './types'; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx index f0dcc9e016efd..2a8c36c868d9e 100644 --- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx @@ -5,6 +5,7 @@ import {StackView} from '@react-navigation/stack'; import React, {useMemo} from 'react'; import {addCustomHistoryRouterExtension} from '@libs/Navigation/AppNavigator/customHistory'; import convertToWebNavigationOptions from '@libs/Navigation/PlatformStackNavigation/navigationOptions/convertToWebNavigationOptions'; +import screenLayout from '@libs/Navigation/PlatformStackNavigation/ScreenLayout'; import type { CreatePlatformStackNavigatorComponentOptions, CustomCodeProps, @@ -13,7 +14,6 @@ import type { PlatformStackNavigatorProps, PlatformStackRouterOptions, } from '@libs/Navigation/PlatformStackNavigation/types'; -import screenLayout from '@libs/Navigation/PlatformStackNavigation/ScreenLayout'; function createPlatformStackNavigatorComponent( displayName: string, diff --git a/src/pages/workspace/accounting/netsuite/utils.ts b/src/pages/workspace/accounting/netsuite/utils.ts index 1e47998a41f70..ada9c3b5122f5 100644 --- a/src/pages/workspace/accounting/netsuite/utils.ts +++ b/src/pages/workspace/accounting/netsuite/utils.ts @@ -1,10 +1,10 @@ +import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import {isAuthenticationError} from '@libs/actions/connections'; import {canUseProvincialTaxNetSuite, canUseTaxNetSuite} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import type {NetSuiteConnectionConfig, NetSuiteSubsidiary} from '@src/types/onyx/Policy'; -import {isAuthenticationError} from '@libs/actions/connections'; import type Policy from '@src/types/onyx/Policy'; -import type {OnyxEntry} from 'react-native-onyx'; function shouldHideReimbursedReportsSection(config?: NetSuiteConnectionConfig) { return config?.reimbursableExpensesExportDestination === CONST.NETSUITE_EXPORT_DESTINATION.JOURNAL_ENTRY; diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts index fb9d01216c4d8..6f40bd6455479 100644 --- a/src/utils/keyboard/index.ts +++ b/src/utils/keyboard/index.ts @@ -1,5 +1,5 @@ -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import {Keyboard} from 'react-native'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {DismissKeyboardOptions} from './types'; type SimplifiedKeyboardEvent = { diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts index 9b69d504b483d..b37f296382606 100644 --- a/src/utils/keyboard/index.website.ts +++ b/src/utils/keyboard/index.website.ts @@ -1,7 +1,7 @@ import {Keyboard} from 'react-native'; import {isMobile, isMobileSafari} from '@libs/Browser'; -import CONST from '@src/CONST'; import TransitionTracker from '@libs/Navigation/TransitionTracker'; +import CONST from '@src/CONST'; import type {DismissKeyboardOptions} from './types'; let isVisible = false; From 69ff82db7eb77f6b1527e5c2d04ee84cf3e07f01 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 23 Feb 2026 17:09:40 +0100 Subject: [PATCH 34/40] Refactor dismissModal function parameters and fix keyboard dismiss logic --- src/libs/Navigation/Navigation.ts | 6 +----- src/utils/keyboard/index.website.ts | 10 +++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 99fd138de4f20..1550c98936d6e 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -738,11 +738,7 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -function dismissModal({ - ref = navigationRef, - afterTransition, - waitForTransition, -}: {ref?: NavigationRef; callback?: () => void; afterTransition?: () => void; waitForTransition?: boolean} = {}) { +function dismissModal({ref = navigationRef, afterTransition, waitForTransition}: {ref?: NavigationRef; afterTransition?: () => void; waitForTransition?: boolean} = {}) { clearSelectedText(); const runImmediately = !waitForTransition; isNavigationReady().then(() => { diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts index b37f296382606..480baa23b2777 100644 --- a/src/utils/keyboard/index.website.ts +++ b/src/utils/keyboard/index.website.ts @@ -38,11 +38,11 @@ window.visualViewport?.addEventListener('resize', handleResize); const dismiss = (options?: DismissKeyboardOptions): Promise => { return new Promise((resolve) => { - if (options?.shouldSkipSafari && isMobileSafari()) { - resolve(); - return; - } - if (!isVisible || !isMobile()) { + const shouldSkipSafari = options?.shouldSkipSafari && isMobileSafari(); + const shouldDismiss = !isVisible || !isMobile(); + + if (shouldDismiss || shouldSkipSafari) { + options?.afterTransition?.(); resolve(); return; } From 57cc57d9b44d2d79889cea82d04ddb3ea8014e9f Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Wed, 25 Feb 2026 16:29:23 +0100 Subject: [PATCH 35/40] small cleanup + fixes of transitionTracker --- src/CONST/index.ts | 1 + src/libs/Navigation/Navigation.ts | 19 +++++-------------- src/libs/Navigation/TransitionTracker.ts | 6 +++--- src/utils/keyboard/index.android.ts | 2 +- 4 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a4ccf7c70e602..43b87b1da300f 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -229,6 +229,7 @@ const CONST = { ANIMATED_PROGRESS_BAR_DURATION: 750, ANIMATION_IN_TIMING: 100, COMPOSER_FOCUS_DELAY: 150, + MAX_TRANSITION_DURATION_MS: 1000, ANIMATION_DIRECTION: { IN: 'in', OUT: 'out', diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 1550c98936d6e..5c9dbfdf0e164 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -400,9 +400,8 @@ type GoBackOptions = { waitForTransition?: boolean; }; -const defaultGoBackOptions: Required = { +const defaultGoBackOptions: Required> = { compareParams: true, - afterTransition: () => {}, waitForTransition: false, }; @@ -480,23 +479,15 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { TransitionTracker.runAfterTransitions(() => { if (backToRoute) { goUp(backToRoute, options); - if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); - } - return; - } - - if (shouldPopToSidebar) { + } else if (shouldPopToSidebar) { popToSidebar(); - return; - } - - if (!navigationRef.current?.canGoBack()) { + } else if (!navigationRef.current?.canGoBack()) { Log.hmmm('[Navigation] Unable to go back'); return; + } else { + navigationRef.current?.goBack(); } - navigationRef.current?.goBack(); if (options?.afterTransition) { TransitionTracker.runAfterTransitions(options.afterTransition); } diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index 699dff51a4ffb..42509403c7829 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -1,6 +1,6 @@ -type CancelHandle = {cancel: () => void}; +import CONST from '@src/CONST'; -const MAX_TRANSITION_DURATION_MS = 1000; +type CancelHandle = {cancel: () => void}; let activeCount = 0; @@ -45,7 +45,7 @@ function startTransition(): void { activeTimeouts.splice(idx, 1); } decrementAndFlush(); - }, MAX_TRANSITION_DURATION_MS); + }, CONST.MAX_TRANSITION_DURATION_MS); activeTimeouts.push(timeout); } diff --git a/src/utils/keyboard/index.android.ts b/src/utils/keyboard/index.android.ts index ecaf4bfc55403..f837d71488a40 100644 --- a/src/utils/keyboard/index.android.ts +++ b/src/utils/keyboard/index.android.ts @@ -37,8 +37,8 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { subscription.remove(); }); - Keyboard.dismiss(); TransitionTracker.startTransition(); + Keyboard.dismiss(); if (options?.afterTransition) { TransitionTracker.runAfterTransitions(options.afterTransition); } From fdf6bec2001e8e33f91aaa551cb7f1d9a60feb4f Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 26 Feb 2026 11:11:44 +0100 Subject: [PATCH 36/40] add tests for transitionTracker --- .../unit/Navigation/TransitionTrackerTest.ts | 76 +++++++++++++++++++ .../unit/keyboard/AndroidKeyboardUtilsTest.ts | 1 + tests/unit/keyboard/KeyboardUtilsTest.ts | 23 +++++- 3 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/unit/Navigation/TransitionTrackerTest.ts diff --git a/tests/unit/Navigation/TransitionTrackerTest.ts b/tests/unit/Navigation/TransitionTrackerTest.ts new file mode 100644 index 0000000000000..731be3744009b --- /dev/null +++ b/tests/unit/Navigation/TransitionTrackerTest.ts @@ -0,0 +1,76 @@ +import TransitionTracker from '@libs/Navigation/TransitionTracker'; +import CONST from '@src/CONST'; + +describe('TransitionTracker', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + function drainTransitions(): void { + jest.runAllTimers(); + } + + describe('runAfterTransitions', () => { + it('runs callback immediately when no transition is active', () => { + const callback = jest.fn(); + TransitionTracker.runAfterTransitions(callback); + expect(callback).toHaveBeenCalledTimes(1); + drainTransitions(); + }); + + it('runs callback immediately when runImmediately is true even with active transition', () => { + TransitionTracker.startTransition(); + const callback = jest.fn(); + TransitionTracker.runAfterTransitions(callback, true); + expect(callback).toHaveBeenCalledTimes(1); + TransitionTracker.endTransition(); + drainTransitions(); + }); + + it('queues callback when transition is active and runs it after endTransition', () => { + const callback = jest.fn(); + TransitionTracker.startTransition(); + TransitionTracker.runAfterTransitions(callback); + expect(callback).not.toHaveBeenCalled(); + TransitionTracker.endTransition(); + expect(callback).toHaveBeenCalledTimes(1); + drainTransitions(); + }); + + it('runs queued callbacks only when all overlapping transitions end', () => { + const callback = jest.fn(); + TransitionTracker.startTransition(); + TransitionTracker.startTransition(); + TransitionTracker.runAfterTransitions(callback); + TransitionTracker.endTransition(); + expect(callback).not.toHaveBeenCalled(); + TransitionTracker.endTransition(); + expect(callback).toHaveBeenCalledTimes(1); + drainTransitions(); + }); + + it('cancel prevents queued callback from running', () => { + const callback = jest.fn(); + TransitionTracker.startTransition(); + const handle = TransitionTracker.runAfterTransitions(callback); + handle.cancel(); + TransitionTracker.endTransition(); + expect(callback).not.toHaveBeenCalled(); + drainTransitions(); + }); + + it('safety timeout flushes callbacks when endTransition is never called', () => { + const callback = jest.fn(); + TransitionTracker.startTransition(); + TransitionTracker.runAfterTransitions(callback); + expect(callback).not.toHaveBeenCalled(); + jest.advanceTimersByTime(CONST.MAX_TRANSITION_DURATION_MS); + expect(callback).toHaveBeenCalledTimes(1); + jest.useRealTimers(); + }); + }); +}); diff --git a/tests/unit/keyboard/AndroidKeyboardUtilsTest.ts b/tests/unit/keyboard/AndroidKeyboardUtilsTest.ts index 0b4a8f0452907..07e199298c27e 100644 --- a/tests/unit/keyboard/AndroidKeyboardUtilsTest.ts +++ b/tests/unit/keyboard/AndroidKeyboardUtilsTest.ts @@ -20,6 +20,7 @@ jest.mock('react-native', () => ({ Platform: { Version: 35, }, + PixelRatio: {getFontScale: () => 1}, })); // Mock react-native-keyboard-controller diff --git a/tests/unit/keyboard/KeyboardUtilsTest.ts b/tests/unit/keyboard/KeyboardUtilsTest.ts index 9482f047b6b19..ccecd318846d3 100644 --- a/tests/unit/keyboard/KeyboardUtilsTest.ts +++ b/tests/unit/keyboard/KeyboardUtilsTest.ts @@ -1,3 +1,4 @@ +import type {DismissKeyboardOptions} from '@src/utils/keyboard/types'; import type {SimplifiedKeyboardEvent} from '@src/utils/keyboard'; const mockKeyboardListeners: Record void>> = {}; @@ -17,6 +18,7 @@ jest.mock('react-native', () => ({ }; }), }, + PixelRatio: {getFontScale: () => 1}, })); // Mock react-native-keyboard-controller @@ -51,7 +53,7 @@ const clearListeners = () => { describe('Keyboard utils: general native', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - let utils: {dismiss: () => Promise; dismissKeyboardAndExecute: (cb: () => void) => Promise}; + let utils: {dismiss: (options?: DismissKeyboardOptions) => Promise; dismissKeyboardAndExecute: (cb: () => void) => Promise}; beforeEach(() => { // Clear all mocks @@ -61,7 +63,10 @@ describe('Keyboard utils: general native', () => { jest.resetModules(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - utils = require('@src/utils/keyboard').default as {dismiss: () => Promise; dismissKeyboardAndExecute: (cb: () => void) => Promise}; + utils = require('@src/utils/keyboard').default as { + dismiss: (options?: DismissKeyboardOptions) => Promise; + dismissKeyboardAndExecute: (cb: () => void) => Promise; + }; }); describe('dismiss', () => { @@ -114,6 +119,20 @@ describe('Keyboard utils: general native', () => { await expect(Promise.all([promise1, promise2])).resolves.toEqual([undefined, undefined]); }); + + it('schedules afterTransition with TransitionTracker when keyboard is visible and runs it after keyboardDidHide', async () => { + triggerKeyboardEvent('keyboardDidShow'); + + const afterTransition = jest.fn(); + const dismissPromise = utils.dismiss({afterTransition}); + + expect(afterTransition).not.toHaveBeenCalled(); + + triggerKeyboardEvent('keyboardDidHide'); + await dismissPromise; + + expect(afterTransition).toHaveBeenCalledTimes(1); + }); }); describe('dismissKeyboardAndExecute', () => { From 16d2d693b453e1d1a0c2f66b8871be19b7afab17 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 26 Feb 2026 14:09:57 +0100 Subject: [PATCH 37/40] add waitForUpcomingTransaction argument, to prevent executing callbaks before navigation transition --- src/libs/Navigation/Navigation.ts | 82 +++++++++++-------- src/libs/Navigation/TransitionTracker.ts | 53 +++++++++++- src/utils/keyboard/index.android.ts | 2 +- src/utils/keyboard/index.ts | 2 +- src/utils/keyboard/index.website.ts | 2 +- .../unit/Navigation/TransitionTrackerTest.ts | 44 ++++++++-- 6 files changed, 140 insertions(+), 45 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 5c9dbfdf0e164..cc6cf9111671d 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -321,15 +321,17 @@ function navigate(route: Route, options?: LinkToOptions) { } const runImmediately = !options?.waitForTransition; - TransitionTracker.runAfterTransitions(() => { - const targetRoute = route.startsWith(CONST.SAML_REDIRECT_URL) ? ROUTES.HOME : route; - linkTo(navigationRef.current, targetRoute, options); - closeSidePanelOnNarrowScreen(route); - - if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); - } - }, runImmediately); + TransitionTracker.runAfterTransitions({ + callback: () => { + const targetRoute = route.startsWith(CONST.SAML_REDIRECT_URL) ? ROUTES.HOME : route; + linkTo(navigationRef.current, targetRoute, options); + closeSidePanelOnNarrowScreen(route); + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions({callback: options.afterTransition, waitForUpcomingTransition: true}); + } + }, + runImmediately, + }); } /** * When routes are compared to determine whether the fallback route passed to the goUp function is in the state, @@ -476,22 +478,25 @@ function goBack(backToRoute?: Route, options?: GoBackOptions) { } const runImmediately = !options?.waitForTransition; - TransitionTracker.runAfterTransitions(() => { - if (backToRoute) { - goUp(backToRoute, options); - } else if (shouldPopToSidebar) { - popToSidebar(); - } else if (!navigationRef.current?.canGoBack()) { - Log.hmmm('[Navigation] Unable to go back'); - return; - } else { - navigationRef.current?.goBack(); - } + TransitionTracker.runAfterTransitions({ + callback: () => { + if (backToRoute) { + goUp(backToRoute, options); + } else if (shouldPopToSidebar) { + popToSidebar(); + } else if (!navigationRef.current?.canGoBack()) { + Log.hmmm('[Navigation] Unable to go back'); + return; + } else { + navigationRef.current?.goBack(); + } - if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); - } - }, runImmediately); + if (options?.afterTransition) { + TransitionTracker.runAfterTransitions({callback: options.afterTransition, waitForUpcomingTransition: true}); + } + }, + runImmediately, + }); } /** @@ -729,17 +734,28 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -function dismissModal({ref = navigationRef, afterTransition, waitForTransition}: {ref?: NavigationRef; afterTransition?: () => void; waitForTransition?: boolean} = {}) { +function dismissModal({ + ref = navigationRef, + afterTransition = () => { + console.log('This is in callback'); + }, + waitForTransition, +}: {ref?: NavigationRef; afterTransition?: () => void; waitForTransition?: boolean} = {}) { clearSelectedText(); const runImmediately = !waitForTransition; isNavigationReady().then(() => { - TransitionTracker.runAfterTransitions(() => { - ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); - - if (afterTransition) { - TransitionTracker.runAfterTransitions(afterTransition); - } - }, runImmediately); + TransitionTracker.runAfterTransitions({ + callback: () => { + ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); + + if (afterTransition) { + console.log('before callback'); + TransitionTracker.runAfterTransitions({callback: afterTransition, waitForUpcomingTransition: true}); + console.log('after runAfterTransitions callback'); + } + }, + runImmediately, + }); }); } @@ -883,7 +899,7 @@ function dismissToModalStack(modalStackNames: Set, options: {afterTransi navigationRef.dispatch({...StackActions.pop(routesToPop), target: rhpState.key}); if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); + TransitionTracker.runAfterTransitions({callback: options.afterTransition, waitForUpcomingTransition: true}); } } diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index 42509403c7829..ad8dae4891dbd 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -2,12 +2,23 @@ import CONST from '@src/CONST'; type CancelHandle = {cancel: () => void}; +type RunAfterTransitionsOptions = { + callback: () => void; + runImmediately?: boolean; + waitForUpcomingTransition?: boolean; +}; + let activeCount = 0; const activeTimeouts: Array> = []; let pendingCallbacks: Array<() => void> = []; +let nextTransitionStartResolve: (() => void) | null = null; +let promiseForNextTransitionStart = new Promise((resolve) => { + nextTransitionStartResolve = resolve; +}); + /** * Invokes and removes all pending callbacks. */ @@ -39,6 +50,15 @@ function decrementAndFlush(): void { function startTransition(): void { activeCount += 1; + const resolve = nextTransitionStartResolve; + if (resolve) { + nextTransitionStartResolve = null; + promiseForNextTransitionStart = new Promise((r) => { + nextTransitionStartResolve = r; + }); + resolve(); + } + const timeout = setTimeout(() => { const idx = activeTimeouts.indexOf(timeout); if (idx !== -1) { @@ -64,15 +84,42 @@ function endTransition(): void { decrementAndFlush(); } +/** + * Returns a promise that resolves when the next transition starts (when startTransition is called). + * Used to wait for a navigation transition to be registered before scheduling runAfterTransitions. + */ +function getPromiseForNextTransitionStart(): Promise { + return promiseForNextTransitionStart; +} + /** * Schedules a callback to run after all transitions complete. If no transitions are active * or `runImmediately` is true, the callback fires synchronously. * - * @param callback - The function to invoke once transitions finish. - * @param runImmediately - If true, the callback fires synchronously regardless of active transitions. Defaults to false. + * @param options - Options object. + * @param options.callback - The function to invoke once transitions finish. + * @param options.runImmediately - If true, the callback fires synchronously regardless of active transitions. Defaults to false. + * @param options.waitForUpcomingTransition - If true, waits for the next transition to start before queuing the callback, so it runs after that transition ends. Use when navigation happens just before this call and the transition is not yet registered. Defaults to false. * @returns A handle with a `cancel` method to prevent the callback from firing. */ -function runAfterTransitions(callback: () => void, runImmediately = false): CancelHandle { +function runAfterTransitions({callback, runImmediately = false, waitForUpcomingTransition = false}: RunAfterTransitionsOptions): CancelHandle { + if (waitForUpcomingTransition) { + let cancelled = false; + let innerHandle: CancelHandle | null = null; + getPromiseForNextTransitionStart().then(() => { + if (cancelled) { + return; + } + innerHandle = runAfterTransitions({callback}); + }); + return { + cancel: () => { + cancelled = true; + innerHandle?.cancel(); + }, + }; + } + if (activeCount === 0 || runImmediately) { callback(); return {cancel: () => {}}; diff --git a/src/utils/keyboard/index.android.ts b/src/utils/keyboard/index.android.ts index f837d71488a40..80846d1e88181 100644 --- a/src/utils/keyboard/index.android.ts +++ b/src/utils/keyboard/index.android.ts @@ -40,7 +40,7 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { TransitionTracker.startTransition(); Keyboard.dismiss(); if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); + TransitionTracker.runAfterTransitions({callback: options.afterTransition}); } }); }; diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts index 6f40bd6455479..87f5d0e65290c 100644 --- a/src/utils/keyboard/index.ts +++ b/src/utils/keyboard/index.ts @@ -40,7 +40,7 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { Keyboard.dismiss(); if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); + TransitionTracker.runAfterTransitions({callback: options.afterTransition}); } }); }; diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts index 480baa23b2777..47d2b7026e933 100644 --- a/src/utils/keyboard/index.website.ts +++ b/src/utils/keyboard/index.website.ts @@ -68,7 +68,7 @@ const dismiss = (options?: DismissKeyboardOptions): Promise => { Keyboard.dismiss(); TransitionTracker.startTransition(); if (options?.afterTransition) { - TransitionTracker.runAfterTransitions(options.afterTransition); + TransitionTracker.runAfterTransitions({callback: options.afterTransition}); } }); }; diff --git a/tests/unit/Navigation/TransitionTrackerTest.ts b/tests/unit/Navigation/TransitionTrackerTest.ts index 731be3744009b..3173b6f8803e2 100644 --- a/tests/unit/Navigation/TransitionTrackerTest.ts +++ b/tests/unit/Navigation/TransitionTrackerTest.ts @@ -17,7 +17,7 @@ describe('TransitionTracker', () => { describe('runAfterTransitions', () => { it('runs callback immediately when no transition is active', () => { const callback = jest.fn(); - TransitionTracker.runAfterTransitions(callback); + TransitionTracker.runAfterTransitions({callback}); expect(callback).toHaveBeenCalledTimes(1); drainTransitions(); }); @@ -25,7 +25,7 @@ describe('TransitionTracker', () => { it('runs callback immediately when runImmediately is true even with active transition', () => { TransitionTracker.startTransition(); const callback = jest.fn(); - TransitionTracker.runAfterTransitions(callback, true); + TransitionTracker.runAfterTransitions({callback, runImmediately: true}); expect(callback).toHaveBeenCalledTimes(1); TransitionTracker.endTransition(); drainTransitions(); @@ -34,7 +34,7 @@ describe('TransitionTracker', () => { it('queues callback when transition is active and runs it after endTransition', () => { const callback = jest.fn(); TransitionTracker.startTransition(); - TransitionTracker.runAfterTransitions(callback); + TransitionTracker.runAfterTransitions({callback}); expect(callback).not.toHaveBeenCalled(); TransitionTracker.endTransition(); expect(callback).toHaveBeenCalledTimes(1); @@ -45,7 +45,7 @@ describe('TransitionTracker', () => { const callback = jest.fn(); TransitionTracker.startTransition(); TransitionTracker.startTransition(); - TransitionTracker.runAfterTransitions(callback); + TransitionTracker.runAfterTransitions({callback}); TransitionTracker.endTransition(); expect(callback).not.toHaveBeenCalled(); TransitionTracker.endTransition(); @@ -56,7 +56,7 @@ describe('TransitionTracker', () => { it('cancel prevents queued callback from running', () => { const callback = jest.fn(); TransitionTracker.startTransition(); - const handle = TransitionTracker.runAfterTransitions(callback); + const handle = TransitionTracker.runAfterTransitions({callback}); handle.cancel(); TransitionTracker.endTransition(); expect(callback).not.toHaveBeenCalled(); @@ -66,11 +66,43 @@ describe('TransitionTracker', () => { it('safety timeout flushes callbacks when endTransition is never called', () => { const callback = jest.fn(); TransitionTracker.startTransition(); - TransitionTracker.runAfterTransitions(callback); + TransitionTracker.runAfterTransitions({callback}); expect(callback).not.toHaveBeenCalled(); jest.advanceTimersByTime(CONST.MAX_TRANSITION_DURATION_MS); expect(callback).toHaveBeenCalledTimes(1); jest.useRealTimers(); }); + + it('waitForUpcomingTransition queues callback after next transition starts and runs it after transition ends', async () => { + const callback = jest.fn(); + TransitionTracker.runAfterTransitions({callback, waitForUpcomingTransition: true}); + expect(callback).not.toHaveBeenCalled(); + TransitionTracker.startTransition(); + await Promise.resolve(); + expect(callback).not.toHaveBeenCalled(); + TransitionTracker.endTransition(); + expect(callback).toHaveBeenCalledTimes(1); + drainTransitions(); + }); + + it('cancel prevents waitForUpcomingTransition callback from running after transition starts', () => { + const callback = jest.fn(); + const handle = TransitionTracker.runAfterTransitions({callback, waitForUpcomingTransition: true}); + TransitionTracker.startTransition(); + handle.cancel(); + TransitionTracker.endTransition(); + expect(callback).not.toHaveBeenCalled(); + drainTransitions(); + }); + + it('cancel before transition starts prevents waitForUpcomingTransition callback from running', () => { + const callback = jest.fn(); + const handle = TransitionTracker.runAfterTransitions({callback, waitForUpcomingTransition: true}); + handle.cancel(); + TransitionTracker.startTransition(); + TransitionTracker.endTransition(); + expect(callback).not.toHaveBeenCalled(); + drainTransitions(); + }); }); }); From 688e0a00177dd3788f370cb11e2dd1c7a313a0fd Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 26 Feb 2026 16:50:03 +0100 Subject: [PATCH 38/40] fix after merge --- src/libs/actions/Report/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 3b98fdcdef542..9efd3fe5f52e9 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1858,7 +1858,7 @@ function createTransactionThreadReport( function navigateToReport(reportID: string | undefined, shouldDismissModal = true) { if (shouldDismissModal) { Navigation.dismissModal({ - callback: () => { + afterTransition: () => { if (!reportID) { return; } From daf120c39c21e092bfdbd9ef91a9a547581220f7 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 26 Feb 2026 17:08:18 +0100 Subject: [PATCH 39/40] change promise.then to async await and add JSDoc --- src/libs/Navigation/TransitionTracker.ts | 27 ++++++++++++------------ 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/libs/Navigation/TransitionTracker.ts b/src/libs/Navigation/TransitionTracker.ts index ad8dae4891dbd..53995a1bf3766 100644 --- a/src/libs/Navigation/TransitionTracker.ts +++ b/src/libs/Navigation/TransitionTracker.ts @@ -3,8 +3,15 @@ import CONST from '@src/CONST'; type CancelHandle = {cancel: () => void}; type RunAfterTransitionsOptions = { + /** The function to invoke once all active transitions have completed. */ callback: () => void; + + /** If true, the callback fires synchronously regardless of any active transitions. Defaults to false. */ runImmediately?: boolean; + + /** If true, waits for the next transition to start before queuing the callback, so it runs after that transition ends. + * Useful when a navigation action has just been dispatched but the transition has not yet been registered. + * Defaults to false. */ waitForUpcomingTransition?: boolean; }; @@ -84,14 +91,6 @@ function endTransition(): void { decrementAndFlush(); } -/** - * Returns a promise that resolves when the next transition starts (when startTransition is called). - * Used to wait for a navigation transition to be registered before scheduling runAfterTransitions. - */ -function getPromiseForNextTransitionStart(): Promise { - return promiseForNextTransitionStart; -} - /** * Schedules a callback to run after all transitions complete. If no transitions are active * or `runImmediately` is true, the callback fires synchronously. @@ -106,12 +105,14 @@ function runAfterTransitions({callback, runImmediately = false, waitForUpcomingT if (waitForUpcomingTransition) { let cancelled = false; let innerHandle: CancelHandle | null = null; - getPromiseForNextTransitionStart().then(() => { - if (cancelled) { - return; + + (async () => { + await promiseForNextTransitionStart; + if (!cancelled) { + innerHandle = runAfterTransitions({callback}); } - innerHandle = runAfterTransitions({callback}); - }); + })(); + return { cancel: () => { cancelled = true; From a2d5fe54a4f51da90091603a3645eaab29090b86 Mon Sep 17 00:00:00 2001 From: Yehor Kharchenko Date: Thu, 26 Feb 2026 17:19:50 +0100 Subject: [PATCH 40/40] small cleanup --- src/libs/Navigation/Navigation.ts | 10 +--------- tests/unit/keyboard/KeyboardUtilsTest.ts | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index b28a7a8cd260d..ee202d2c30a32 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -735,13 +735,7 @@ function getTopmostSuperWideRHPReportID(state: NavigationState = navigationRef.g * For detailed information about dismissing modals, * see the NAVIGATION.md documentation. */ -function dismissModal({ - ref = navigationRef, - afterTransition = () => { - console.log('This is in callback'); - }, - waitForTransition, -}: {ref?: NavigationRef; afterTransition?: () => void; waitForTransition?: boolean} = {}) { +function dismissModal({ref = navigationRef, afterTransition, waitForTransition}: {ref?: NavigationRef; afterTransition?: () => void; waitForTransition?: boolean} = {}) { clearSelectedText(); const runImmediately = !waitForTransition; isNavigationReady().then(() => { @@ -750,9 +744,7 @@ function dismissModal({ ref.dispatch({type: CONST.NAVIGATION.ACTION_TYPE.DISMISS_MODAL}); if (afterTransition) { - console.log('before callback'); TransitionTracker.runAfterTransitions({callback: afterTransition, waitForUpcomingTransition: true}); - console.log('after runAfterTransitions callback'); } }, runImmediately, diff --git a/tests/unit/keyboard/KeyboardUtilsTest.ts b/tests/unit/keyboard/KeyboardUtilsTest.ts index ccecd318846d3..fc9af0ca0f455 100644 --- a/tests/unit/keyboard/KeyboardUtilsTest.ts +++ b/tests/unit/keyboard/KeyboardUtilsTest.ts @@ -1,5 +1,5 @@ -import type {DismissKeyboardOptions} from '@src/utils/keyboard/types'; import type {SimplifiedKeyboardEvent} from '@src/utils/keyboard'; +import type {DismissKeyboardOptions} from '@src/utils/keyboard/types'; const mockKeyboardListeners: Record void>> = {}; const mockKeyboardControllerListeners: Record void>> = {};