From 4659765a670ba9f129c194338452727c038e1208 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 18 Dec 2025 05:46:25 +0500 Subject: [PATCH 01/25] fixed prettier issues --- src/pages/ReportDetailsPage.tsx | 2 +- src/pages/ReportParticipantsPage.tsx | 4 ++-- src/pages/RoomMembersPage.tsx | 4 ++-- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 84b86265ccc0d..35ac59cd32978 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -6,12 +6,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index abe10995475fb..35b2d0b35df80 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -9,16 +9,16 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; // eslint-disable-next-line no-restricted-imports import {FallbackAvatar, Plus} from '@components/Icon/Expensicons'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import ScreenWrapper from '@components/ScreenWrapper'; -import useConfirmModal from '@hooks/useConfirmModal'; import SelectionListWithModal from '@components/SelectionListWithModal'; import TableListItem from '@components/SelectionListWithSections/TableListItem'; import type {ListItem, SelectionListHandle} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; +import useConfirmModal from '@hooks/useConfirmModal'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index a25ee64725e31..fc2e2719d8a43 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -5,12 +5,11 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, RoomMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; // eslint-disable-next-line no-restricted-imports import {FallbackAvatar, Plus} from '@components/Icon/Expensicons'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; -import useConfirmModal from '@hooks/useConfirmModal'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionListWithModal from '@components/SelectionListWithModal'; import TableListItem from '@components/SelectionListWithSections/TableListItem'; @@ -18,6 +17,7 @@ import type {ListItem} from '@components/SelectionListWithSections/types'; import Text from '@components/Text'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useConfirmModal from '@hooks/useConfirmModal'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 9438d107463b8..da21969940a33 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -8,11 +8,11 @@ import React, {useCallback, useContext, useEffect, useImperativeHandle, useMemo, import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; // eslint-disable-next-line no-restricted-imports import * as Expensicons from '@components/Icon/Expensicons'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import useConfirmModal from '@hooks/useConfirmModal'; From 7981b0b518e58face710477dee983687dcb09f07 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 23 Dec 2025 05:22:44 +0500 Subject: [PATCH 02/25] Fixed lint issues --- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 39ae6b6a0f396..0315f4cb3ca16 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -10,8 +10,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; -// eslint-disable-next-line no-restricted-imports -import * as Expensicons from '@components/Icon/Expensicons'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; From 76ae50f0313c1aef76ee605135dc2c3cbb435314 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 6 Jan 2026 19:47:51 +0500 Subject: [PATCH 03/25] fixed lint issues --- src/pages/ReportParticipantsPage.tsx | 4 +--- src/pages/RoomMembersPage.tsx | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 41a1eef3c3612..e3fef13fb922a 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -11,9 +11,8 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; // eslint-disable-next-line no-restricted-imports -import {FallbackAvatar, Plus} from '@components/Icon/Expensicons'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import {Plus} from '@components/Icon/Expensicons'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionListWithModal from '@components/SelectionListWithModal'; import TableListItem from '@components/SelectionListWithSections/TableListItem'; @@ -67,7 +66,6 @@ type ReportParticipantsPageProps = WithReportOrNotFoundProps & PlatformStackScre function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const backTo = route.params.backTo; const icons = useMemoizedLazyExpensifyIcons(['User', 'MakeAdmin', 'RemoveMembers', 'FallbackAvatar']); - const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index 3f733151c204b..502c7605bc4e5 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -7,9 +7,8 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, RoomMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; // eslint-disable-next-line no-restricted-imports -import {FallbackAvatar, Plus} from '@components/Icon/Expensicons'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import {Plus} from '@components/Icon/Expensicons'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionListWithModal from '@components/SelectionListWithModal'; From ff4f5e1b6d1d8ea03dd4a16f301bf3e352fe9d8a Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 7 Jan 2026 18:00:58 +0500 Subject: [PATCH 04/25] Fixed confirmation modal actions --- src/components/Modal/Global/ModalContext.tsx | 79 ++++++------ .../helpers/isSearchTopmostFullScreenRoute.ts | 5 + src/pages/ReportDetailsPage.tsx | 115 ++++++++---------- 3 files changed, 100 insertions(+), 99 deletions(-) diff --git a/src/components/Modal/Global/ModalContext.tsx b/src/components/Modal/Global/ModalContext.tsx index 91bbcf6799691..0db22fb50d031 100644 --- a/src/components/Modal/Global/ModalContext.tsx +++ b/src/components/Modal/Global/ModalContext.tsx @@ -1,7 +1,5 @@ import noop from 'lodash/noop'; -import React, {useCallback, useContext, useMemo, useState} from 'react'; -import Log from '@libs/Log'; -import CONST from '@src/CONST'; +import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; const ModalActions = { CONFIRM: 'CONFIRM', @@ -40,48 +38,53 @@ type ModalInfo = { function ModalProvider({children}: {children: React.ReactNode}) { const [modalStack, setModalStack] = useState<{modals: ModalInfo[]}>({modals: []}); + // Use a ref to track modals synchronously for duplicate ID checks + const modalStackRef = useRef([]); const showModal = useCallback(({component, props, id, isCloseable = true}) => { - // This is a promise that will resolve when the modal is closed - let closeModalPromise: Promise | null = null; - - setModalStack((prevState) => { - // Check current state for existing modal - const existingModal = id ? prevState.modals.find((modal: ModalInfo) => modal.id === id) : undefined; - if (existingModal) { - // There is already a modal with this ID. Return the existing promise and don't modify state. - closeModalPromise = existingModal.promiseWithResolvers.promise; - return prevState; // No state change needed - } - - // Create a new promise with resolvers to be resolved when the modal is closed - const promiseWithResolvers = Promise.withResolvers(); - closeModalPromise = promiseWithResolvers.promise; - - return { - ...prevState, - modals: [...prevState.modals, {component: component as React.FunctionComponent, props, promiseWithResolvers, isCloseable, id: id ?? String(modalID++)}], - }; - }); - - // At this point, closeModalPromise should always be assigned - if (!closeModalPromise) { - Log.alert(`${CONST.ERROR.ENSURE_BUG_BOT} Failed to create modal promise. This should never happen.`); - throw new Error('Failed to create modal promise'); + const modalId = id ?? String(modalID++); + + // Check for existing modal with the same ID using the ref (synchronous) + const existingModal = id ? modalStackRef.current.find((modal: ModalInfo) => modal.id === id) : undefined; + if (existingModal) { + // There is already a modal with this ID. Return the existing promise. + return existingModal.promiseWithResolvers.promise; } - return closeModalPromise; + // Create the promise outside of the state setter to avoid React batching issues + const promiseWithResolvers = Promise.withResolvers(); + + const newModal: ModalInfo = { + component: component as React.FunctionComponent, + props, + promiseWithResolvers, + isCloseable, + id: modalId, + }; + + // Update the ref synchronously + modalStackRef.current = [...modalStackRef.current, newModal]; + + // Update the state (may be batched by React) + setModalStack((prevState) => ({ + ...prevState, + modals: [...prevState.modals, newModal], + })); + + return promiseWithResolvers.promise; }, []); const closeModal = useCallback((data = {action: 'CLOSE'}) => { - setModalStack((prevState) => { - const lastModal = prevState.modals.at(-1); - lastModal?.promiseWithResolvers.resolve(data); - return { - ...prevState, - modals: prevState.modals.slice(0, -1), - }; - }); + // Update the ref synchronously + const lastModal = modalStackRef.current.at(-1); + lastModal?.promiseWithResolvers.resolve(data); + modalStackRef.current = modalStackRef.current.slice(0, -1); + + // Update the state + setModalStack((prevState) => ({ + ...prevState, + modals: prevState.modals.slice(0, -1), + })); }, []); const contextValue = useMemo(() => ({showModal, closeModal}), [closeModal, showModal]); diff --git a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts index b324cc99718f3..70e15aae65bae 100644 --- a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts +++ b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts @@ -4,6 +4,11 @@ import NAVIGATORS from '@src/NAVIGATORS'; import {isFullScreenName} from './isNavigatorName'; const isSearchTopmostFullScreenRoute = (): boolean => { + // Check if navigation is ready before accessing state to avoid "navigation not initialized" errors + if (!navigationRef.isReady()) { + return false; + } + const rootState = navigationRef.getRootState() as State; if (!rootState) { diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 70a3a70b7317f..e60ed41ad5879 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,6 +1,6 @@ import reportsSelector from '@selectors/Attributes'; import {Str} from 'expensify-common'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -352,29 +352,11 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail if (action !== ModalActions.CONFIRM) { return; } + // leaveChat already handles Navigation.isNavigationReady() leaveChat(); }); }, [showConfirmModal, translate, leaveChat]); - // A flag to indicate whether the user chose to delete the transaction or not - const isTransactionDeleted = useRef(false); - - const showDeleteModal = useCallback(() => { - showConfirmModal({ - title: caseID === CASES.DEFAULT ? translate('task.deleteTask') : translate('iou.deleteExpense', {count: 1}), - prompt: caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1}), - confirmText: translate('common.delete'), - cancelText: translate('common.cancel'), - danger: true, - shouldEnableNewFocusManagement: true, - }).then(({action}) => { - if (action !== ModalActions.CONFIRM) { - return; - } - isTransactionDeleted.current = true; - }); - }, [showConfirmModal, translate, caseID]); - const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived); const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, currentUserPersonalDetails?.email) && !policy?.isJoinRequestPending; @@ -922,53 +904,64 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail // Where to navigate back to after deleting the transaction and its report. const navigateToTargetUrl = useCallback(() => { - let urlToNavigateBack: string | undefined; - - // Only proceed with navigation logic if transaction was actually deleted - if (!isEmptyObject(requestParentReportAction)) { - const isTrackExpense = isTrackExpenseAction(requestParentReportAction); - if (isTrackExpense) { - urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( - moneyRequestReport?.reportID, - moneyRequestReport, - iouTransactionID, - requestParentReportAction, - iouReport, - chatIOUReport, - isChatIOUReportArchived, - isSingleTransactionView, - ); - } else { - urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete( - iouTransactionID, - requestParentReportAction, - iouReport, - chatIOUReport, - isChatIOUReportArchived, - isSingleTransactionView, - ); + // Ensure navigation is ready before proceeding + Navigation.isNavigationReady().then(() => { + let urlToNavigateBack: string | undefined; + + // Only proceed with navigation logic if transaction was actually deleted + if (!isEmptyObject(requestParentReportAction)) { + const isTrackExpense = isTrackExpenseAction(requestParentReportAction); + if (isTrackExpense) { + urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( + moneyRequestReport?.reportID, + moneyRequestReport, + iouTransactionID, + requestParentReportAction, + iouReport, + chatIOUReport, + isChatIOUReportArchived, + isSingleTransactionView, + ); + } else { + urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete( + iouTransactionID, + requestParentReportAction, + iouReport, + chatIOUReport, + isChatIOUReportArchived, + isSingleTransactionView, + ); + } } - } - if (!urlToNavigateBack) { - Navigation.dismissModal(); - } else { - setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); - } + if (!urlToNavigateBack) { + Navigation.dismissModal(); + } else { + setDeleteTransactionNavigateBackUrl(urlToNavigateBack); + navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); + } + }); }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, moneyRequestReport, isChatIOUReportArchived, iouReport, chatIOUReport]); - useEffect(() => { - return () => { - // Perform the actual deletion after the details page is unmounted. This prevents the [Deleted ...] text from briefly appearing when dismissing the modal. - if (!isTransactionDeleted.current) { + const showDeleteModal = useCallback(() => { + showConfirmModal({ + title: caseID === CASES.DEFAULT ? translate('task.deleteTask') : translate('iou.deleteExpense', {count: 1}), + prompt: caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1}), + confirmText: translate('common.delete'), + cancelText: translate('common.cancel'), + danger: true, + shouldEnableNewFocusManagement: true, + }).then(({action}) => { + if (action !== ModalActions.CONFIRM) { return; } - isTransactionDeleted.current = false; - navigateToTargetUrl(); - deleteTransaction(); - }; - }, [deleteTransaction, navigateToTargetUrl]); + // Wait for navigation to be ready before navigating + Navigation.isNavigationReady().then(() => { + navigateToTargetUrl(); + deleteTransaction(); + }); + }); + }, [showConfirmModal, translate, caseID, navigateToTargetUrl, deleteTransaction]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); From 8e96ddc2e444948ba2017d5ae4405429671b314f Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 7 Jan 2026 18:04:14 +0500 Subject: [PATCH 05/25] remove redundent code --- src/pages/ReportDetailsPage.tsx | 67 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index e60ed41ad5879..853c056320c49 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -904,43 +904,40 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail // Where to navigate back to after deleting the transaction and its report. const navigateToTargetUrl = useCallback(() => { - // Ensure navigation is ready before proceeding - Navigation.isNavigationReady().then(() => { - let urlToNavigateBack: string | undefined; - - // Only proceed with navigation logic if transaction was actually deleted - if (!isEmptyObject(requestParentReportAction)) { - const isTrackExpense = isTrackExpenseAction(requestParentReportAction); - if (isTrackExpense) { - urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( - moneyRequestReport?.reportID, - moneyRequestReport, - iouTransactionID, - requestParentReportAction, - iouReport, - chatIOUReport, - isChatIOUReportArchived, - isSingleTransactionView, - ); - } else { - urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete( - iouTransactionID, - requestParentReportAction, - iouReport, - chatIOUReport, - isChatIOUReportArchived, - isSingleTransactionView, - ); - } - } - - if (!urlToNavigateBack) { - Navigation.dismissModal(); + let urlToNavigateBack: string | undefined; + + // Only proceed with navigation logic if transaction was actually deleted + if (!isEmptyObject(requestParentReportAction)) { + const isTrackExpense = isTrackExpenseAction(requestParentReportAction); + if (isTrackExpense) { + urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete( + moneyRequestReport?.reportID, + moneyRequestReport, + iouTransactionID, + requestParentReportAction, + iouReport, + chatIOUReport, + isChatIOUReportArchived, + isSingleTransactionView, + ); } else { - setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); + urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete( + iouTransactionID, + requestParentReportAction, + iouReport, + chatIOUReport, + isChatIOUReportArchived, + isSingleTransactionView, + ); } - }); + } + + if (!urlToNavigateBack) { + Navigation.dismissModal(); + } else { + setDeleteTransactionNavigateBackUrl(urlToNavigateBack); + navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); + } }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, moneyRequestReport, isChatIOUReportArchived, iouReport, chatIOUReport]); const showDeleteModal = useCallback(() => { From 5b13c8af7f5f1029a4b56a154fa97b33449db094 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 8 Jan 2026 02:13:08 +0500 Subject: [PATCH 06/25] reverted code in Globl modal --- package-lock.json | 2 +- src/components/Modal/Global/ModalContext.tsx | 79 ++++++++++---------- 2 files changed, 39 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 073cf72d6b795..ff963ea47629f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12578,7 +12578,7 @@ }, "node_modules/@react-native-community/cli/node_modules/universalify": { "version": "0.1.2", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4.0.0" diff --git a/src/components/Modal/Global/ModalContext.tsx b/src/components/Modal/Global/ModalContext.tsx index 0db22fb50d031..91bbcf6799691 100644 --- a/src/components/Modal/Global/ModalContext.tsx +++ b/src/components/Modal/Global/ModalContext.tsx @@ -1,5 +1,7 @@ import noop from 'lodash/noop'; -import React, {useCallback, useContext, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useContext, useMemo, useState} from 'react'; +import Log from '@libs/Log'; +import CONST from '@src/CONST'; const ModalActions = { CONFIRM: 'CONFIRM', @@ -38,53 +40,48 @@ type ModalInfo = { function ModalProvider({children}: {children: React.ReactNode}) { const [modalStack, setModalStack] = useState<{modals: ModalInfo[]}>({modals: []}); - // Use a ref to track modals synchronously for duplicate ID checks - const modalStackRef = useRef([]); const showModal = useCallback(({component, props, id, isCloseable = true}) => { - const modalId = id ?? String(modalID++); - - // Check for existing modal with the same ID using the ref (synchronous) - const existingModal = id ? modalStackRef.current.find((modal: ModalInfo) => modal.id === id) : undefined; - if (existingModal) { - // There is already a modal with this ID. Return the existing promise. - return existingModal.promiseWithResolvers.promise; + // This is a promise that will resolve when the modal is closed + let closeModalPromise: Promise | null = null; + + setModalStack((prevState) => { + // Check current state for existing modal + const existingModal = id ? prevState.modals.find((modal: ModalInfo) => modal.id === id) : undefined; + if (existingModal) { + // There is already a modal with this ID. Return the existing promise and don't modify state. + closeModalPromise = existingModal.promiseWithResolvers.promise; + return prevState; // No state change needed + } + + // Create a new promise with resolvers to be resolved when the modal is closed + const promiseWithResolvers = Promise.withResolvers(); + closeModalPromise = promiseWithResolvers.promise; + + return { + ...prevState, + modals: [...prevState.modals, {component: component as React.FunctionComponent, props, promiseWithResolvers, isCloseable, id: id ?? String(modalID++)}], + }; + }); + + // At this point, closeModalPromise should always be assigned + if (!closeModalPromise) { + Log.alert(`${CONST.ERROR.ENSURE_BUG_BOT} Failed to create modal promise. This should never happen.`); + throw new Error('Failed to create modal promise'); } - // Create the promise outside of the state setter to avoid React batching issues - const promiseWithResolvers = Promise.withResolvers(); - - const newModal: ModalInfo = { - component: component as React.FunctionComponent, - props, - promiseWithResolvers, - isCloseable, - id: modalId, - }; - - // Update the ref synchronously - modalStackRef.current = [...modalStackRef.current, newModal]; - - // Update the state (may be batched by React) - setModalStack((prevState) => ({ - ...prevState, - modals: [...prevState.modals, newModal], - })); - - return promiseWithResolvers.promise; + return closeModalPromise; }, []); const closeModal = useCallback((data = {action: 'CLOSE'}) => { - // Update the ref synchronously - const lastModal = modalStackRef.current.at(-1); - lastModal?.promiseWithResolvers.resolve(data); - modalStackRef.current = modalStackRef.current.slice(0, -1); - - // Update the state - setModalStack((prevState) => ({ - ...prevState, - modals: prevState.modals.slice(0, -1), - })); + setModalStack((prevState) => { + const lastModal = prevState.modals.at(-1); + lastModal?.promiseWithResolvers.resolve(data); + return { + ...prevState, + modals: prevState.modals.slice(0, -1), + }; + }); }, []); const contextValue = useMemo(() => ({showModal, closeModal}), [closeModal, showModal]); From c4e1d84002aa9d445c6cdea51a2d0ed5e1821fe3 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 8 Jan 2026 03:12:48 +0500 Subject: [PATCH 07/25] reverted pacage.json file --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index ff963ea47629f..073cf72d6b795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12578,7 +12578,7 @@ }, "node_modules/@react-native-community/cli/node_modules/universalify": { "version": "0.1.2", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" From 549067ca3c497a26425b0c9a2da3172615641e77 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 8 Jan 2026 20:41:47 +0500 Subject: [PATCH 08/25] reverted the navigation code --- .../Navigation/helpers/isSearchTopmostFullScreenRoute.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts index 70e15aae65bae..b324cc99718f3 100644 --- a/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts +++ b/src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts @@ -4,11 +4,6 @@ import NAVIGATORS from '@src/NAVIGATORS'; import {isFullScreenName} from './isNavigatorName'; const isSearchTopmostFullScreenRoute = (): boolean => { - // Check if navigation is ready before accessing state to avoid "navigation not initialized" errors - if (!navigationRef.isReady()) { - return false; - } - const rootState = navigationRef.getRootState() as State; if (!rootState) { From db923f8750cee0a40da4a929b317c1414fd58ca4 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 8 Jan 2026 20:51:19 +0500 Subject: [PATCH 09/25] refactor: use async/await for showConfirmModal and move ref check inside callback --- src/pages/ReportDetailsPage.tsx | 35 +++++++++++++--------------- src/pages/ReportParticipantsPage.tsx | 24 +++++++++---------- src/pages/RoomMembersPage.tsx | 13 +++++------ 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 3210384d3fe11..e390cc8f1891f 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -328,20 +328,19 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail }); }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report]); - const showLastMemberLeavingModal = useCallback(() => { - showConfirmModal({ + const showLastMemberLeavingModal = useCallback(async () => { + const {action} = await showConfirmModal({ title: translate('groupChat.lastMemberTitle'), prompt: translate('groupChat.lastMemberWarning'), confirmText: translate('common.leave'), cancelText: translate('common.cancel'), danger: true, - }).then(({action}) => { - if (action !== ModalActions.CONFIRM) { - return; - } - // leaveChat already handles Navigation.isNavigationReady() - leaveChat(); }); + if (action !== ModalActions.CONFIRM) { + return; + } + // leaveChat already handles Navigation.isNavigationReady() + leaveChat(); }, [showConfirmModal, translate, leaveChat]); const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived); @@ -927,24 +926,22 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail } }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, moneyRequestReport, isChatIOUReportArchived, iouReport, chatIOUReport]); - const showDeleteModal = useCallback(() => { - showConfirmModal({ + const showDeleteModal = useCallback(async () => { + const {action} = await showConfirmModal({ title: caseID === CASES.DEFAULT ? translate('task.deleteTask') : translate('iou.deleteExpense', {count: 1}), prompt: caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1}), confirmText: translate('common.delete'), cancelText: translate('common.cancel'), danger: true, shouldEnableNewFocusManagement: true, - }).then(({action}) => { - if (action !== ModalActions.CONFIRM) { - return; - } - // Wait for navigation to be ready before navigating - Navigation.isNavigationReady().then(() => { - navigateToTargetUrl(); - deleteTransaction(); - }); }); + if (action !== ModalActions.CONFIRM) { + return; + } + // Wait for navigation to be ready before navigating + await Navigation.isNavigationReady(); + navigateToTargetUrl(); + deleteTransaction(); }, [showConfirmModal, translate, caseID, navigateToTargetUrl, deleteTransaction]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index c63b8b35bd935..7cd1b708a5020 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -251,8 +251,8 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { }); }, [selectedMembers, currentUserAccountID, report.reportID, setSelectedMembers]); - const showRemoveMembersModal = useCallback(() => { - showConfirmModal({ + const showRemoveMembersModal = useCallback(async () => { + const {action} = await showConfirmModal({ title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), prompt: translate('workspace.people.removeMembersPrompt', { count: selectedMembers.length, @@ -261,20 +261,20 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { confirmText: translate('common.remove'), cancelText: translate('common.cancel'), danger: true, - }).then(({action}) => { - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - if (!textInputRef.current) { - return; - } - textInputRef.current.focus(); - }); + }); - if (action !== ModalActions.CONFIRM) { + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + if (!textInputRef.current) { return; } - removeUsers(); + textInputRef.current.focus(); }); + + if (action !== ModalActions.CONFIRM) { + return; + } + removeUsers(); }, [showConfirmModal, translate, selectedMembers, formatPhoneNumber, currentUserAccountID, removeUsers]); const changeUserRole = useCallback( diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index e85eb26373011..dc953bbcc3d34 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -143,8 +143,8 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { }); }, [report, selectedMembers, setSelectedMembers]); - const showRemoveMembersModal = useCallback(() => { - showConfirmModal({ + const showRemoveMembersModal = useCallback(async () => { + const {action} = await showConfirmModal({ title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), prompt: translate('roomMembersPage.removeMembersPrompt', { count: selectedMembers.length, @@ -153,12 +153,11 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { confirmText: translate('common.remove'), cancelText: translate('common.cancel'), danger: true, - }).then(({action}) => { - if (action !== ModalActions.CONFIRM) { - return; - } - removeUsers(); }); + if (action !== ModalActions.CONFIRM) { + return; + } + removeUsers(); }, [showConfirmModal, translate, selectedMembers, formatPhoneNumber, currentUserAccountID, removeUsers]); /** From 9b794cbbfc0d3761f0d73a543f6cd8d4d2426b48 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 14 Jan 2026 14:45:45 +0500 Subject: [PATCH 10/25] resolved feedbacks --- src/pages/ReportDetailsPage.tsx | 20 +++++++++++--------- src/pages/ReportParticipantsPage.tsx | 9 --------- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index ce3ab6b0db795..fbc0611609382 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -324,16 +324,18 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail getReportPrivateNote(report?.reportID); }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered, isSelfDM]); - const leaveChat = useCallback(() => { + const leaveChat = useCallback(async () => { Navigation.dismissModal(); - Navigation.isNavigationReady().then(() => { - if (isRootGroupChat) { - leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID); - return; - } - const isWorkspaceMemberLeavingWorkspaceRoom = isWorkspaceMemberLeavingWorkspaceRoomUtil(report, isPolicyEmployee, isPolicyAdmin); - leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom); - }); + + await Navigation.isNavigationReady(); + + if (isRootGroupChat) { + leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID); + return; + } + + const isWorkspaceMemberLeavingWorkspaceRoom = isWorkspaceMemberLeavingWorkspaceRoomUtil(report, isPolicyEmployee, isPolicyAdmin); + leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom); }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report]); const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived); diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 33123c30d3487..072e5d29d2640 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -441,15 +441,6 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { })} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} - onModalHide={() => { - // eslint-disable-next-line @typescript-eslint/no-deprecated - InteractionManager.runAfterInteractions(() => { - if (!textInputRef.current) { - return; - } - textInputRef.current.focus(); - }); - }} /> Date: Wed, 14 Jan 2026 17:31:11 +0500 Subject: [PATCH 11/25] resolved conflicts with main --- src/pages/ReportDetailsPage.tsx | 76 +++++++++---------- src/pages/ReportParticipantsPage.tsx | 40 +++++----- src/pages/RoomMembersPage.tsx | 41 +++++----- .../FloatingActionButtonAndPopover.tsx | 48 ++++++------ 4 files changed, 104 insertions(+), 101 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index fbc0611609382..d3143980aad0b 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -6,12 +6,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import ConfirmModal from '@components/ConfirmModal'; import DisplayNames from '@components/DisplayNames'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -23,6 +23,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useSearchContext} from '@components/Search/SearchContext'; import useAncestors from '@hooks/useAncestors'; +import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDeleteTransactions from '@hooks/useDeleteTransactions'; import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactionsAndViolations'; @@ -182,8 +183,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const [isLastMemberLeavingGroupModalVisible, setIsLastMemberLeavingGroupModalVisible] = useState(false); - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + const {showConfirmModal} = useConfirmModal(); const isPolicyAdmin = useMemo(() => isPolicyAdminUtil(policy), [policy]); const isPolicyEmployee = useMemo(() => isPolicyEmployeeUtil(report?.policyID, policy), [report?.policyID, policy]); const isPolicyExpenseChat = useMemo(() => isPolicyExpenseChatUtil(report), [report]); @@ -307,14 +307,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: reportsSelector}); const isWorkspaceChat = useMemo(() => isWorkspaceChatUtil(report?.chatType ?? ''), [report?.chatType]); - useEffect(() => { - if (canDeleteRequest) { - return; - } - - setIsDeleteModalVisible(false); - }, [canDeleteRequest]); - useEffect(() => { // Do not fetch private notes if isLoadingPrivateNotes is already defined, or if the network is offline, or if the report is a self DM. if (isPrivateNotesFetchTriggered || isOffline || isSelfDM) { @@ -338,6 +330,21 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom); }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report]); + const showLastMemberLeavingModal = useCallback(async () => { + const {action} = await showConfirmModal({ + title: translate('groupChat.lastMemberTitle'), + prompt: translate('groupChat.lastMemberWarning'), + confirmText: translate('common.leave'), + cancelText: translate('common.cancel'), + danger: true, + shouldHandleNavigationBack: false, + }); + if (action !== ModalActions.CONFIRM) { + return; + } + await leaveChat(); + }, [showConfirmModal, translate, leaveChat]); + const shouldShowLeaveButton = canLeaveChat(report, policy, !!reportNameValuePairs?.private_isArchived); const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, currentUserPersonalDetails?.email) && !policy?.isJoinRequestPending; @@ -528,7 +535,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail isAnonymousAction: true, action: () => { if (getParticipantsAccountIDsForDisplay(report, false, true).length === 1 && isRootGroupChat) { - setIsLastMemberLeavingGroupModalVisible(true); + showLastMemberLeavingModal(); return; } @@ -582,6 +589,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail isTaskActionable, isRootGroupChat, leaveChat, + showLastMemberLeavingModal, isSmallScreenWidth, isRestrictedToPreferredPolicy, preferredPolicyID, @@ -935,6 +943,21 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail }; }, [deleteTransaction, navigateToTargetUrl]); + const showDeleteModal = useCallback(async () => { + const {action} = await showConfirmModal({ + title: caseID === CASES.DEFAULT ? translate('task.deleteTask') : translate('iou.deleteExpense', {count: 1}), + prompt: caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1}), + confirmText: translate('common.delete'), + cancelText: translate('common.cancel'), + danger: true, + shouldEnableNewFocusManagement: true, + }); + if (action !== ModalActions.CONFIRM) { + return; + } + isTransactionDeleted.current = true; + }, [showConfirmModal, translate, caseID]); + const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); return ( @@ -1016,37 +1039,10 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail key={CONST.REPORT_DETAILS_MENU_ITEM.DELETE} icon={expensifyIcons.Trashcan} title={caseID === CASES.DEFAULT ? translate('common.delete') : translate('reportActionContextMenu.deleteAction', {action: requestParentReportAction})} - onPress={() => setIsDeleteModalVisible(true)} + onPress={showDeleteModal} /> )} - { - setIsLastMemberLeavingGroupModalVisible(false); - leaveChat(); - }} - onCancel={() => setIsLastMemberLeavingGroupModalVisible(false)} - prompt={translate('groupChat.lastMemberWarning')} - confirmText={translate('common.leave')} - cancelText={translate('common.cancel')} - /> - { - setIsDeleteModalVisible(false); - isTransactionDeleted.current = true; - }} - onCancel={() => setIsDeleteModalVisible(false)} - prompt={caseID === CASES.DEFAULT ? translate('task.deleteConfirmation') : translate('iou.deleteConfirmation', {count: 1})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - shouldEnableNewFocusManagement - /> ); diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 072e5d29d2640..4e53ec0e3c9fc 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -8,16 +8,17 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption, WorkspaceMemberBulkActionType} from '@components/ButtonWithDropdownMenu/types'; -import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; // eslint-disable-next-line no-restricted-imports import {Plus} from '@components/Icon/Expensicons'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import ScreenWrapper from '@components/ScreenWrapper'; import TableListItem from '@components/SelectionList/ListItem/TableListItem'; import type {ListItem, SelectionListHandle} from '@components/SelectionList/types'; import SelectionListWithModal from '@components/SelectionListWithModal'; import Text from '@components/Text'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; +import useConfirmModal from '@hooks/useConfirmModal'; import useFilteredSelection from '@hooks/useFilteredSelection'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -65,8 +66,8 @@ type ReportParticipantsPageProps = WithReportOrNotFoundProps & PlatformStackScre function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const backTo = route.params.backTo; const icons = useMemoizedLazyExpensifyIcons(['User', 'MakeAdmin', 'RemoveMembers', 'FallbackAvatar']); - const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const {translate, formatPhoneNumber, localeCompare} = useLocalize(); + const {showConfirmModal} = useConfirmModal(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -246,7 +247,6 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const accountIDsToRemove = selectedMembers.filter((id) => id !== currentUserAccountID); removeFromGroupChat(report.reportID, accountIDsToRemove); setSearchValue(''); - setRemoveMembersConfirmModalVisible(false); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { setSelectedMembers([]); @@ -254,6 +254,23 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { }); }; + const showRemoveMembersModal = useCallback(async () => { + const {action} = await showConfirmModal({ + title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), + prompt: translate('workspace.people.removeMembersPrompt', { + count: selectedMembers.length, + memberName: formatPhoneNumber(getPersonalDetailsByIDs({accountIDs: selectedMembers, currentUserAccountID}).at(0)?.displayName ?? ''), + }), + confirmText: translate('common.remove'), + cancelText: translate('common.cancel'), + danger: true, + }); + if (action !== ModalActions.CONFIRM) { + return; + } + removeUsers(); + }, [showConfirmModal, translate, selectedMembers, formatPhoneNumber, currentUserAccountID, removeUsers]); + const changeUserRole = useCallback( (role: ValueOf) => { const accountIDsToUpdate = selectedMembers.filter((id) => report.participants?.[id].role !== role); @@ -309,7 +326,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, icon: icons.RemoveMembers, - onSelected: () => setRemoveMembersConfirmModalVisible(true), + onSelected: showRemoveMembersModal, }, ]; @@ -336,7 +353,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { } return options; - }, [icons.RemoveMembers, icons.User, icons.MakeAdmin, changeUserRole, translate, setRemoveMembersConfirmModalVisible, selectedMembers, report.participants]); + }, [icons.RemoveMembers, icons.User, icons.MakeAdmin, changeUserRole, translate, showRemoveMembersModal, selectedMembers, report.participants]); const headerButtons = useMemo(() => { if (!isGroupChat) { @@ -429,19 +446,6 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { subtitle={StringUtils.lineBreaksToSpaces(getReportName(report, reportAttributes))} /> {headerButtons} - setRemoveMembersConfirmModalVisible(false)} - prompt={translate('workspace.people.removeMembersPrompt', { - count: selectedMembers.length, - memberName: formatPhoneNumber(getPersonalDetailsByIDs({accountIDs: selectedMembers, currentUserAccountID}).at(0)?.displayName ?? ''), - })} - confirmText={translate('common.remove')} - cancelText={translate('common.cancel')} - /> { setSelectedMembers([]); @@ -145,6 +145,23 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { }); }; + const showRemoveMembersModal = useCallback(async () => { + const {action} = await showConfirmModal({ + title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), + prompt: translate('roomMembersPage.removeMembersPrompt', { + count: selectedMembers.length, + memberName: formatPhoneNumber(getPersonalDetailsByIDs({accountIDs: selectedMembers, currentUserAccountID}).at(0)?.displayName ?? ''), + }), + confirmText: translate('common.remove'), + cancelText: translate('common.cancel'), + danger: true, + }); + if (action !== ModalActions.CONFIRM) { + return; + } + removeUsers(); + }, [showConfirmModal, translate, selectedMembers, formatPhoneNumber, currentUserAccountID, removeUsers]); + /** * Add user from the selectedMembers list */ @@ -318,11 +335,11 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, icon: icons.RemoveMembers, - onSelected: () => setRemoveMembersConfirmModalVisible(true), + onSelected: showRemoveMembersModal, }, ]; return options; - }, [icons.RemoveMembers, translate, selectedMembers.length]); + }, [icons.RemoveMembers, translate, selectedMembers.length, showRemoveMembersModal]); const headerButtons = useMemo(() => { return ( @@ -426,23 +443,9 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { }} /> {headerButtons} - setRemoveMembersConfirmModalVisible(false)} - prompt={translate('roomMembersPage.removeMembersPrompt', { - count: selectedMembers.length, - memberName: formatPhoneNumber(getPersonalDetailsByIDs({accountIDs: selectedMembers, currentUserAccountID}).at(0)?.displayName ?? ''), - })} - confirmText={translate('common.remove')} - cancelText={translate('common.cancel')} - /> (null); const {windowHeight} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -317,17 +317,33 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref [quickActionReport?.policyID], ); + const showRedirectToExpensifyClassicModal = useCallback(() => { + showConfirmModal({ + title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'), + prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'), + confirmText: translate('exitSurvey.goToExpensifyClassic'), + cancelText: translate('common.cancel'), + onConfirm: () => { + if (CONFIG.IS_HYBRID_APP) { + closeReactNativeApp({shouldSetNVP: true}); + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + }, + }); + }, [showConfirmModal, translate]); + const startScan = useCallback(() => { interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); + showRedirectToExpensifyClassicModal(); return; } // Start the scan flow directly startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts); }); - }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID]); + }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID, showRedirectToExpensifyClassicModal]); const startQuickScan = useCallback(() => { interceptAnonymousUser(() => { @@ -418,7 +434,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref onSelected: () => interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); + showRedirectToExpensifyClassicModal(); return; } startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts); @@ -563,7 +579,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref onSelected: () => { interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); + showRedirectToExpensifyClassicModal(); return; } // Start the flow to start tracking a distance request @@ -581,7 +597,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref onSelected: () => { interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); + showRedirectToExpensifyClassicModal(); return; } @@ -626,7 +642,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref onSelected: () => interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); + showRedirectToExpensifyClassicModal(); return; } @@ -703,22 +719,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref })} anchorRef={fabRef} /> - { - setModalVisible(false); - if (CONFIG.IS_HYBRID_APP) { - closeReactNativeApp({shouldSetNVP: true}); - return; - } - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }} - onCancel={() => setModalVisible(false)} - title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')} - confirmText={translate('exitSurvey.goToExpensifyClassic')} - cancelText={translate('common.cancel')} - /> {!shouldUseNarrowLayout && ( Date: Wed, 14 Jan 2026 18:20:05 +0500 Subject: [PATCH 12/25] fixed EsLint issues --- src/pages/ReportDetailsPage.tsx | 2 +- .../FloatingActionButtonAndPopover.tsx | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index d3143980aad0b..f2f680430c3c5 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,6 +1,6 @@ import reportsSelector from '@selectors/Attributes'; import {Str} from 'expensify-common'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index def9f0d2d1594..80af7b29da061 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -12,6 +12,7 @@ import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -317,20 +318,21 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref [quickActionReport?.policyID], ); - const showRedirectToExpensifyClassicModal = useCallback(() => { - showConfirmModal({ + const showRedirectToExpensifyClassicModal = useCallback(async () => { + const {action} = await showConfirmModal({ title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'), prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'), confirmText: translate('exitSurvey.goToExpensifyClassic'), cancelText: translate('common.cancel'), - onConfirm: () => { - if (CONFIG.IS_HYBRID_APP) { - closeReactNativeApp({shouldSetNVP: true}); - return; - } - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }, }); + if (action !== ModalActions.CONFIRM) { + return; + } + if (CONFIG.IS_HYBRID_APP) { + closeReactNativeApp({shouldSetNVP: true}); + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX); }, [showConfirmModal, translate]); const startScan = useCallback(() => { From 960a140018f258a088ab1e41b74353e75ef026c3 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 14 Jan 2026 18:29:33 +0500 Subject: [PATCH 13/25] fixed prettier issues --- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 80af7b29da061..77669d988c174 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -10,9 +10,9 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; import FloatingActionButton from '@components/FloatingActionButton'; import FloatingReceiptButton from '@components/FloatingReceiptButton'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; -import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; From a8cd81ee20174ceecadd37f33c11f89201de0e08 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 15 Jan 2026 02:45:30 +0500 Subject: [PATCH 14/25] fix: update leaveGroupChat and leaveRoom calls to match upstream signatures --- src/pages/ReportDetailsPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index f2f680430c3c5..fb4aa91add0d8 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -322,13 +322,13 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail await Navigation.isNavigationReady(); if (isRootGroupChat) { - leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID); + leaveGroupChat(report.reportID, quickAction?.chatReportID?.toString() === report.reportID, currentUserPersonalDetails.accountID); return; } const isWorkspaceMemberLeavingWorkspaceRoom = isWorkspaceMemberLeavingWorkspaceRoomUtil(report, isPolicyEmployee, isPolicyAdmin); - leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom); - }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report]); + leaveRoom(report.reportID, currentUserPersonalDetails.accountID, isWorkspaceMemberLeavingWorkspaceRoom); + }, [isRootGroupChat, isPolicyEmployee, isPolicyAdmin, quickAction?.chatReportID, report, currentUserPersonalDetails.accountID]); const showLastMemberLeavingModal = useCallback(async () => { const {action} = await showConfirmModal({ From 886feddc48e546d0df1eec25bf86499f3ecd242b Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Fri, 16 Jan 2026 16:19:05 +0500 Subject: [PATCH 15/25] fixed lint issue for react compiler --- src/pages/RoomMembersPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index e4456c0321a4f..0740082c6aa08 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -133,7 +133,7 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { * Remove selected users from the room * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ - const removeUsers = () => { + const removeUsers = useCallback(() => { if (report) { removeFromRoom(report.reportID, selectedMembers); } @@ -143,7 +143,7 @@ function RoomMembersPage({report, policy}: RoomMembersPageProps) { setSelectedMembers([]); clearUserSearchPhrase(); }); - }; + }, [report, selectedMembers, setSearchValue, setSelectedMembers]); const showRemoveMembersModal = useCallback(async () => { const {action} = await showConfirmModal({ From e02c483bf2010abd50a8a16766b2af7e2aca1c34 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Fri, 16 Jan 2026 16:31:42 +0500 Subject: [PATCH 16/25] fixed lint issue for react compiler --- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 77669d988c174..41c12be981657 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -444,7 +444,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref sentryLabel: CONST.SENTRY_LABEL.FAB_MENU.CREATE_EXPENSE, }, ]; - }, [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts, reportID, icons]); + }, [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts, reportID, icons, showRedirectToExpensifyClassicModal]); const quickActionMenuItems = useMemo(() => { // Define common properties in baseQuickAction From b23ebe448717bbbd3c9439055a8c9776d7ffc83b Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Sat, 17 Jan 2026 00:54:06 +0500 Subject: [PATCH 17/25] Added missing text focus logic --- src/pages/ReportParticipantsPage.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 4e53ec0e3c9fc..1b245f7a6305a 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -265,6 +265,12 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { cancelText: translate('common.cancel'), danger: true, }); + + // Focus the text input after the modal hides + if (textInputRef.current) { + textInputRef.current.focus(); + } + if (action !== ModalActions.CONFIRM) { return; } From beb48923e2bf50de6c4f945afe01a3109edf3d24 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 20 Jan 2026 03:15:30 +0500 Subject: [PATCH 18/25] fixed lint & delete task modal issue --- src/pages/ReportDetailsPage.tsx | 1 + src/pages/ReportParticipantsPage.tsx | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index c88f0e4d36191..872166a5fa13a 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1009,6 +1009,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail return; } isTransactionDeleted.current = true; + Navigation.dismissModal(); }, [showConfirmModal, translate, caseID]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index 1b245f7a6305a..e6626882ad0fd 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -242,7 +242,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { * Remove selected users from the workspace * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details */ - const removeUsers = () => { + const removeUsers = useCallback(() => { // Remove the admin from the list const accountIDsToRemove = selectedMembers.filter((id) => id !== currentUserAccountID); removeFromGroupChat(report.reportID, accountIDsToRemove); @@ -252,7 +252,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { setSelectedMembers([]); clearUserSearchPhrase(); }); - }; + }, [selectedMembers, currentUserAccountID, report.reportID]); const showRemoveMembersModal = useCallback(async () => { const {action} = await showConfirmModal({ From b6792cfb535753593f1f8d775004b54aa0a64754 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 21 Jan 2026 01:08:16 +0500 Subject: [PATCH 19/25] resolved feedback --- src/pages/ReportDetailsPage.tsx | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 298a2c8330ff8..528e3beea86c5 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,7 +1,7 @@ import {StackActions} from '@react-navigation/native'; import reportsSelector from '@selectors/Attributes'; import {Str} from 'expensify-common'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; @@ -989,21 +989,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail } }, [requestParentReportAction, route.params.reportID, moneyRequestReport, iouTransactionID, iouReport, chatIOUReport, isChatIOUReportArchived, isSingleTransactionView]); - // A flag to indicate whether the user chose to delete the transaction or not - const isTransactionDeleted = useRef(false); - - useEffect(() => { - return () => { - // Perform the actual deletion after the details page is unmounted. This prevents the [Deleted ...] text from briefly appearing when dismissing the modal. - if (!isTransactionDeleted.current) { - return; - } - isTransactionDeleted.current = false; - navigateToTargetUrl(); - deleteTransaction(); - }; - }, [deleteTransaction, navigateToTargetUrl]); - const showDeleteModal = useCallback(async () => { const {action} = await showConfirmModal({ title: caseID === CASES.DEFAULT ? translate('task.deleteTask') : translate('iou.deleteExpense', {count: 1}), @@ -1016,9 +1001,9 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail if (action !== ModalActions.CONFIRM) { return; } - isTransactionDeleted.current = true; - Navigation.dismissModal(); - }, [showConfirmModal, translate, caseID]); + navigateToTargetUrl(); + deleteTransaction(); + }, [showConfirmModal, translate, caseID, navigateToTargetUrl, deleteTransaction]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); From 681d5053440ae41be90e9e2574e9a84d794e6a86 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 21 Jan 2026 01:44:30 +0500 Subject: [PATCH 20/25] fixed lint issue --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 5b3007779a380..03f4b18e214f4 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -7,7 +7,6 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import usePrevious from '@hooks/usePrevious'; -import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; @@ -27,6 +26,7 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from './ReportUtils'; import {isCardPendingActivate} from './CardUtils'; import {getDecodedCategoryName} from './CategoryUtils'; import {convertAmountToDisplayString, convertToDisplayString, convertToShortDisplayString} from './CurrencyUtils'; From d4d1e566ad1328fb09eabf2561ff8b8b82837909 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 21 Jan 2026 01:49:38 +0500 Subject: [PATCH 21/25] fixed prettier issue --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 03f4b18e214f4..64d216c9a4c29 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -26,7 +26,6 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from './ReportUtils'; import {isCardPendingActivate} from './CardUtils'; import {getDecodedCategoryName} from './CategoryUtils'; import {convertAmountToDisplayString, convertToDisplayString, convertToShortDisplayString} from './CurrencyUtils'; @@ -43,6 +42,7 @@ import getReportURLForCurrentContext from './Navigation/helpers/getReportURLForC import Parser from './Parser'; import {arePersonalDetailsMissing, getEffectiveDisplayName, getPersonalDetailByEmail, getPersonalDetailsByIDs} from './PersonalDetailsUtils'; import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtils'; +import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from './ReportUtils'; import type {getReportName, OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; import {getReportFieldTypeTranslationKey} from './WorkspaceReportFieldUtils'; From f532c08cec6c7bce1eaf150ec5bd15af192f8b3e Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Wed, 21 Jan 2026 19:35:06 +0500 Subject: [PATCH 22/25] fixed lint issues --- src/libs/ReportActionsUtils.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 64d216c9a4c29..afe9546f67e00 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -42,7 +42,6 @@ import getReportURLForCurrentContext from './Navigation/helpers/getReportURLForC import Parser from './Parser'; import {arePersonalDetailsMissing, getEffectiveDisplayName, getPersonalDetailByEmail, getPersonalDetailsByIDs} from './PersonalDetailsUtils'; import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtils'; -import {isHarvestCreatedExpenseReport, isPolicyExpenseChat} from './ReportUtils'; import type {getReportName, OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; import {getReportFieldTypeTranslationKey} from './WorkspaceReportFieldUtils'; @@ -65,6 +64,14 @@ type MemberChangeMessageRoomReferenceElement = { type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; +function isPolicyExpenseChat(report: OnyxInputOrEntry): boolean { + return report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT || !!(report && typeof report === 'object' && 'isPolicyExpenseChat' in report && report.isPolicyExpenseChat); +} + +function isHarvestCreatedExpenseReport(origin?: string, originalID?: string): boolean { + return !!originalID && origin === 'harvest'; +} + let allReportActions: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, From 71bb9da95b361456f4151e60b1fa154987bec019 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Thu, 22 Jan 2026 01:59:56 +0500 Subject: [PATCH 23/25] Fixed lint issue --- src/pages/ReportDetailsPage.tsx | 1 + src/pages/ReportParticipantsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 528e3beea86c5..0b0c621a020f1 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -910,6 +910,7 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail isSingleTransactionView, moneyRequestReport, removeTransaction, + allTransactionViolations, isMoneyRequestReportArchived, iouReport, chatIOUReport, diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index e6626882ad0fd..d132dd303a918 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -252,7 +252,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { setSelectedMembers([]); clearUserSearchPhrase(); }); - }, [selectedMembers, currentUserAccountID, report.reportID]); + }, [selectedMembers, currentUserAccountID, report.reportID, setSelectedMembers]); const showRemoveMembersModal = useCallback(async () => { const {action} = await showConfirmModal({ From 84599bb163affb61fd1ef9338f954ed3323e3e69 Mon Sep 17 00:00:00 2001 From: Faizan Shoukat Abbasi Date: Tue, 27 Jan 2026 03:13:09 +0500 Subject: [PATCH 24/25] Fix React Compiler regressions in ReportParticipantsPage --- src/pages/ReportParticipantsPage.tsx | 403 +++++++++++---------------- 1 file changed, 163 insertions(+), 240 deletions(-) diff --git a/src/pages/ReportParticipantsPage.tsx b/src/pages/ReportParticipantsPage.tsx index d132dd303a918..fe7c6521554a9 100755 --- a/src/pages/ReportParticipantsPage.tsx +++ b/src/pages/ReportParticipantsPage.tsx @@ -1,6 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import reportsSelector from '@selectors/Attributes'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import Badge from '@components/Badge'; @@ -85,31 +85,24 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const currentUserAccountID = Number(session?.accountID); const isCurrentUserAdmin = isGroupChatAdmin(report, currentUserAccountID); - const isGroupChat = useMemo(() => isGroupChatUtils(report), [report]); + const isGroupChat = isGroupChatUtils(report); const isCurrentUserGroupChatAdmin = isGroupChat && isCurrentUserAdmin; const isFocused = useIsFocused(); const {isOffline} = useNetwork(); const canSelectMultiple = isGroupChat && isCurrentUserAdmin && (isSmallScreenWidth ? isMobileSelectionModeEnabled : true); const [searchValue, setSearchValue] = useState(''); - const {chatParticipants, personalDetailsParticipants} = useMemo( - () => getReportPersonalDetailsParticipants(report, personalDetails, reportMetadata), - [report, personalDetails, reportMetadata], - ); - - const filterParticipants = useCallback( - (participant?: PersonalDetails) => { - if (!participant) { - return false; - } - const isInParticipants = chatParticipants.includes(participant.accountID); - const pendingChatMember = reportMetadata?.pendingChatMembers?.find((member) => member.accountID === participant.accountID.toString()); + const {chatParticipants, personalDetailsParticipants} = getReportPersonalDetailsParticipants(report, personalDetails, reportMetadata); - const isPendingDelete = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - return isInParticipants && !isPendingDelete; - }, - [chatParticipants, reportMetadata?.pendingChatMembers], - ); + const filterParticipants = (participant?: PersonalDetails) => { + if (!participant) { + return false; + } + const isInParticipants = chatParticipants.includes(participant.accountID); + const pendingChatMember = reportMetadata?.pendingChatMembers?.find((member) => member.accountID === participant.accountID.toString()); + const isPendingDelete = pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + return isInParticipants && !isPendingDelete; + }; const [selectedMembers, setSelectedMembers] = useFilteredSelection(personalDetailsParticipants, filterParticipants); @@ -122,6 +115,7 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { if (!personalDetails?.[accountID]) { return false; } + // When offline, we want to include the pending members with delete action as they are displayed in the list as well return !pendingMember || isOffline || pendingMember.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; }); @@ -153,97 +147,29 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { }, }); - const getParticipants = () => { - let result: MemberOption[] = []; - - for (const accountID of chatParticipants) { - const role = reportParticipants?.[accountID].role; - const details = personalDetails?.[accountID]; - - // If search value is provided, filter out members that don't match the search value - if (!details || (searchValue.trim() && !isSearchStringMatchUserDetails(details, searchValue))) { - continue; - } - - const pendingChatMember = pendingChatMembers?.findLast((member) => member.accountID === accountID.toString()); - const isSelected = selectedMembers.includes(accountID) && canSelectMultiple; - const isAdmin = role === CONST.REPORT.ROLE.ADMIN; - let roleBadge = null; - if (isAdmin) { - roleBadge = ; - } - - const pendingAction = pendingChatMember?.pendingAction ?? reportParticipants?.[accountID]?.pendingAction; - - result.push({ - keyForList: `${accountID}`, - accountID, - isSelected, - isDisabledCheckbox: accountID === currentUserAccountID, - isDisabled: pendingChatMember?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || details?.isOptimisticPersonalDetail, - text: formatPhoneNumber(getDisplayNameOrDefault(details)), - alternateText: formatPhoneNumber(details?.login ?? ''), - rightElement: roleBadge, - pendingAction, - icons: [ - { - source: details?.avatar ?? icons.FallbackAvatar, - name: formatPhoneNumber(details?.login ?? ''), - type: CONST.ICON_TYPE_AVATAR, - id: accountID, - }, - ], - }); + const toggleUser = (user: MemberOption) => { + if (user.accountID === currentUserAccountID) { + return; } - result = result.sort((a, b) => localeCompare((a.text ?? '').toLowerCase(), (b.text ?? '').toLowerCase())); - return result; + if (selectedMembers.includes(user.accountID)) { + setSelectedMembers((prevSelected) => prevSelected.filter((id) => id !== user.accountID)); + } else { + setSelectedMembers((prevSelected) => [...prevSelected, user.accountID]); + } }; - const participants = getParticipants(); - - /** - * Add user from the selectedMembers list - */ - const addUser = useCallback((accountID: number) => setSelectedMembers((prevSelected) => [...prevSelected, accountID]), [setSelectedMembers]); - - /** - * Add or remove all users passed from the selectedEmployees list - */ const toggleAllUsers = (memberList: MemberOption[]) => { const enabledAccounts = memberList.filter((member) => !member.isDisabled && !member.isDisabledCheckbox); const someSelected = enabledAccounts.some((member) => selectedMembers.includes(member.accountID)); if (someSelected) { setSelectedMembers([]); } else { - const everyAccountId = enabledAccounts.map((member) => member.accountID); - setSelectedMembers(everyAccountId); + setSelectedMembers(enabledAccounts.map((member) => member.accountID)); } }; - /** - * Remove user from the selectedMembers list - */ - const removeUser = useCallback( - (accountID: number) => { - setSelectedMembers((prevSelected) => prevSelected.filter((id) => id !== accountID)); - }, - [setSelectedMembers], - ); - - /** - * Open the modal to invite a user - */ - const inviteUser = useCallback(() => { - Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_INVITE.getRoute(report.reportID, backTo)); - }, [report.reportID, backTo]); - - /** - * Remove selected users from the workspace - * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details - */ - const removeUsers = useCallback(() => { - // Remove the admin from the list + const removeUsers = () => { const accountIDsToRemove = selectedMembers.filter((id) => id !== currentUserAccountID); removeFromGroupChat(report.reportID, accountIDsToRemove); setSearchValue(''); @@ -252,9 +178,9 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { setSelectedMembers([]); clearUserSearchPhrase(); }); - }, [selectedMembers, currentUserAccountID, report.reportID, setSelectedMembers]); + }; - const showRemoveMembersModal = useCallback(async () => { + const showRemoveMembersModal = async () => { const {action} = await showConfirmModal({ title: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), prompt: translate('workspace.people.removeMembersPrompt', { @@ -266,167 +192,131 @@ function ReportParticipantsPage({report, route}: ReportParticipantsPageProps) { danger: true, }); - // Focus the text input after the modal hides if (textInputRef.current) { textInputRef.current.focus(); } - if (action !== ModalActions.CONFIRM) { - return; - } - removeUsers(); - }, [showConfirmModal, translate, selectedMembers, formatPhoneNumber, currentUserAccountID, removeUsers]); - - const changeUserRole = useCallback( - (role: ValueOf) => { - const accountIDsToUpdate = selectedMembers.filter((id) => report.participants?.[id].role !== role); - updateGroupChatMemberRoles(report.reportID, accountIDsToUpdate, role); - setSelectedMembers([]); - }, - [report, selectedMembers, setSelectedMembers], - ); - - /** - * Toggle user from the selectedMembers list - */ - const toggleUser = useCallback( - (user: MemberOption) => { - if (user.accountID === currentUserAccountID) { - return; - } - - // Add or remove the user if the checkbox is enabled - if (selectedMembers.includes(user.accountID)) { - removeUser(user.accountID); - } else { - addUser(user.accountID); - } - }, - [selectedMembers, addUser, removeUser, currentUserAccountID], - ); - - const customListHeader = useMemo(() => { - const header = ( - - - {translate('common.member')} - - {isGroupChat && ( - - {translate('common.role')} - - )} - - ); - - if (canSelectMultiple) { - return header; - } - - return {header}; - }, [styles, translate, isGroupChat, shouldShowTextInput, StyleUtils, canSelectMultiple]); - - const bulkActionsButtonOptions = useMemo(() => { - const options: Array> = [ - { - text: translate('workspace.people.removeMembersTitle', {count: selectedMembers.length}), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.REMOVE, - icon: icons.RemoveMembers, - onSelected: showRemoveMembersModal, - }, - ]; - - const isAtLeastOneAdminSelected = selectedMembers.some((accountId) => report.participants?.[accountId]?.role === CONST.REPORT.ROLE.ADMIN); - - if (isAtLeastOneAdminSelected) { - options.push({ - text: translate('workspace.people.makeMember', {count: selectedMembers.length}), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_MEMBER, - icon: icons.User, - onSelected: () => changeUserRole(CONST.REPORT.ROLE.MEMBER), - }); + if (action === ModalActions.CONFIRM) { + removeUsers(); } + }; - const isAtLeastOneMemberSelected = selectedMembers.some((accountId) => report.participants?.[accountId]?.role === CONST.REPORT.ROLE.MEMBER); + const changeUserRole = (role: ValueOf) => { + const accountIDsToUpdate = selectedMembers.filter((id) => report.participants?.[id].role !== role); + updateGroupChatMemberRoles(report.reportID, accountIDsToUpdate, role); + setSelectedMembers([]); + }; - if (isAtLeastOneMemberSelected) { - options.push({ - text: translate('workspace.people.makeAdmin', {count: selectedMembers.length}), - value: CONST.POLICY.MEMBERS_BULK_ACTION_TYPES.MAKE_ADMIN, - icon: icons.MakeAdmin, - onSelected: () => changeUserRole(CONST.REPORT.ROLE.ADMIN), - }); + const openMemberDetails = (item: MemberOption) => { + if (isGroupChat && isCurrentUserAdmin) { + Navigation.navigate(ROUTES.REPORT_PARTICIPANTS_DETAILS.getRoute(report.reportID, item.accountID, backTo)); + return; } + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID, Navigation.getActiveRoute())); + }; - return options; - }, [icons.RemoveMembers, icons.User, icons.MakeAdmin, changeUserRole, translate, showRemoveMembersModal, selectedMembers, report.participants]); + // Build participants list + let participants: MemberOption[] = []; + for (const accountID of chatParticipants) { + const role = reportParticipants?.[accountID].role; + const details = personalDetails?.[accountID]; - const headerButtons = useMemo(() => { - if (!isGroupChat) { - return; + if (!details || (searchValue.trim() && !isSearchStringMatchUserDetails(details, searchValue))) { + continue; } - return ( - - {(isSmallScreenWidth ? canSelectMultiple : selectedMembers.length > 0) ? ( - - shouldAlwaysShowDropdownMenu - pressOnEnter - customText={translate('workspace.common.selected', {count: selectedMembers.length})} - buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - onPress={() => null} - isSplitButton={false} - options={bulkActionsButtonOptions} - style={[shouldUseNarrowLayout && styles.flexGrow1]} - isDisabled={!selectedMembers.length} - /> - ) : ( -