From 124ecab449b199ad2ee4a91624ccf612ab9487c4 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 19 Jan 2026 22:42:48 +0700 Subject: [PATCH 1/3] Refactor ConfirmModal usage to useConfirmModal in Search pages 2 --- .../SearchTypeMenuPopover.tsx | 6 +- src/components/Search/index.tsx | 45 ++++++------ src/hooks/useDeleteSavedSearch.tsx | 61 +++++++--------- src/hooks/useSearchTypeMenu.tsx | 3 +- src/pages/Search/EmptySearchView.tsx | 73 +++++++++++-------- src/pages/Search/SearchTypeMenu.tsx | 10 +-- 6 files changed, 96 insertions(+), 102 deletions(-) diff --git a/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx b/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx index df5af069a91ce..44a453e4e4ffe 100644 --- a/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx +++ b/src/components/Search/SearchPageHeader/SearchTypeMenuPopover.tsx @@ -13,7 +13,7 @@ type SearchTypeMenuNarrowProps = { function SearchTypeMenuPopover({queryJSON}: SearchTypeMenuNarrowProps) { const styles = useThemeStyles(); - const {isPopoverVisible, delayPopoverMenuFirstRender, openMenu, closeMenu, allMenuItems, DeleteConfirmModal, windowHeight} = useSearchTypeMenu(queryJSON); + const {isPopoverVisible, delayPopoverMenuFirstRender, openMenu, closeMenu, allMenuItems, windowHeight} = useSearchTypeMenu(queryJSON); const buttonRef = useRef(null); const {unmodifiedPaddings} = useSafeAreaPaddings(); @@ -41,10 +41,6 @@ function SearchTypeMenuPopover({queryJSON}: SearchTypeMenuNarrowProps) { scrollContainerStyle={styles.pv0} /> )} - {/* DeleteConfirmModal is a stable JSX element returned by the hook. - Returning the element directly keeps the component identity across re-renders so React - can play its exit animation instead of removing it instantly. */} - {DeleteConfirmModal} ); } diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index f7348a64ddbe9..95633361e4550 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -7,13 +7,14 @@ import type {OnyxEntry} from 'react-native-onyx'; import Animated, {FadeIn, FadeOut, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import FullPageErrorView from '@components/BlockingViews/FullPageErrorView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmModal from '@components/ConfirmModal'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import SearchTableHeader from '@components/SelectionListWithSections/SearchTableHeader'; import type {ReportActionListItemType, SearchListItem, SelectionListHandle, TransactionGroupListItemType, TransactionListItemType} from '@components/SelectionListWithSections/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import {WideRHPContext} from '@components/WideRHPContextProvider'; import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet'; import useCardFeedsForDisplay from '@hooks/useCardFeedsForDisplay'; +import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useMultipleSnapshots from '@hooks/useMultipleSnapshots'; @@ -203,17 +204,9 @@ function Search({ const prevIsOffline = usePrevious(isOffline); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); - const [isDEWModalVisible, setIsDEWModalVisible] = useState(false); + const {showConfirmModal} = useConfirmModal(); const {isBetaEnabled} = usePermissions(); const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); - - const handleDEWModalOpen = useCallback(() => { - if (onDEWModalOpen) { - onDEWModalOpen(); - } else { - setIsDEWModalVisible(true); - } - }, [onDEWModalOpen]); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for enabling the selection mode on small screens only // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, isLargeScreenWidth} = useResponsiveLayout(); @@ -274,6 +267,22 @@ function Search({ const {translate, localeCompare, formatPhoneNumber} = useLocalize(); const searchListRef = useRef(null); + const handleDEWModalOpen = useCallback(async () => { + if (onDEWModalOpen) { + onDEWModalOpen(); + } else { + const result = await showConfirmModal({ + title: translate('customApprovalWorkflow.title'), + prompt: translate('customApprovalWorkflow.description'), + confirmText: translate('customApprovalWorkflow.goToExpensifyClassic'), + shouldShowCancelButton: false, + }); + if (result.action === ModalActions.CONFIRM) { + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + } + } + }, [onDEWModalOpen, showConfirmModal, translate]); + const clearTransactionsAndSetHashAndKey = useCallback(() => { clearSelectedTransactions(hash); setCurrentSearchHashAndKey(hash, searchKey); @@ -1107,7 +1116,9 @@ function Search({ canSelectMultiple={canSelectMultiple} selectedTransactions={selectedTransactions} shouldPreventLongPressRow={isChat || isTask} - onDEWModalOpen={handleDEWModalOpen} + onDEWModalOpen={() => { + handleDEWModalOpen(); + }} isDEWBetaEnabled={isDEWBetaEnabled} SearchTableHeader={ !shouldShowTableHeader ? undefined : ( @@ -1156,18 +1167,6 @@ function Search({ hasLoadedAllTransactions={hasLoadedAllTransactions} customCardNames={customCardNames} /> - { - setIsDEWModalVisible(false); - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }} - onCancel={() => setIsDEWModalVisible(false)} - prompt={translate('customApprovalWorkflow.description')} - confirmText={translate('customApprovalWorkflow.goToExpensifyClassic')} - shouldShowCancelButton={false} - /> ); diff --git a/src/hooks/useDeleteSavedSearch.tsx b/src/hooks/useDeleteSavedSearch.tsx index f8c8d17537844..9bcb6fca2d517 100644 --- a/src/hooks/useDeleteSavedSearch.tsx +++ b/src/hooks/useDeleteSavedSearch.tsx @@ -1,48 +1,43 @@ -import React, {useState} from 'react'; -import ConfirmModal from '@components/ConfirmModal'; +import {useCallback} from 'react'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import {useSearchContext} from '@components/Search/SearchContext'; import {deleteSavedSearch} from '@libs/actions/Search'; import Navigation from '@libs/Navigation/Navigation'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import ROUTES from '@src/ROUTES'; +import useConfirmModal from './useConfirmModal'; import useLocalize from './useLocalize'; export default function useDeleteSavedSearch() { - const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); - const [hashToDelete, setHashToDelete] = useState(0); const {translate} = useLocalize(); const {currentSearchHash} = useSearchContext(); + const {showConfirmModal} = useConfirmModal(); - const showDeleteModal = (hash: number) => { - setIsDeleteModalVisible(true); - setHashToDelete(hash); - }; + const handleDeleteSavedSearch = useCallback( + (hash: number) => { + showConfirmModal({ + title: translate('search.deleteSavedSearch'), + prompt: translate('search.deleteSavedSearchConfirm'), + confirmText: translate('common.delete'), + cancelText: translate('common.cancel'), + danger: true, + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } + deleteSavedSearch(hash); - const handleDelete = () => { - deleteSavedSearch(hashToDelete); - setIsDeleteModalVisible(false); - - if (hashToDelete === currentSearchHash) { - Navigation.navigate( - ROUTES.SEARCH_ROOT.getRoute({ - query: buildCannedSearchQuery(), - }), - ); - } - }; - - const DeleteConfirmModal = ( - setIsDeleteModalVisible(false)} - isVisible={isDeleteModalVisible} - prompt={translate('search.deleteSavedSearchConfirm')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> + if (hash === currentSearchHash) { + Navigation.navigate( + ROUTES.SEARCH_ROOT.getRoute({ + query: buildCannedSearchQuery(), + }), + ); + } + }); + }, + [showConfirmModal, translate, currentSearchHash], ); - return {showDeleteModal, DeleteConfirmModal}; + return {showDeleteModal: handleDeleteSavedSearch}; } diff --git a/src/hooks/useSearchTypeMenu.tsx b/src/hooks/useSearchTypeMenu.tsx index d4bcf3326b203..b9e54c0587efd 100644 --- a/src/hooks/useSearchTypeMenu.tsx +++ b/src/hooks/useSearchTypeMenu.tsx @@ -41,7 +41,7 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { const {translate} = useLocalize(); const {typeMenuSections, shouldShowSuggestedSearchSkeleton} = useSearchTypeMenuSections(); const {clearSelectedTransactions} = useSearchContext(); - const {showDeleteModal, DeleteConfirmModal} = useDeleteSavedSearch(); + const {showDeleteModal} = useDeleteSavedSearch(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const personalDetails = usePersonalDetails(); const [reports = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); @@ -215,7 +215,6 @@ export default function useSearchTypeMenu(queryJSON: SearchQueryJSON) { openMenu, closeMenu, allMenuItems: popoverMenuItems, - DeleteConfirmModal, theme, styles, windowHeight, diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 838cd6c121c5e..25427b1cb5d61 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -7,12 +7,12 @@ import type {GestureResponderEvent, ImageStyle, Text as RNText, TextStyle, ViewS import {Linking, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import BookTravelButton from '@components/BookTravelButton'; -import ConfirmModal from '@components/ConfirmModal'; import GenericEmptyStateComponent from '@components/EmptyStateComponent/GenericEmptyStateComponent'; import type {EmptyStateButton, HeaderMedia, MediaTypes} from '@components/EmptyStateComponent/types'; import type {FeatureListItem} from '@components/FeatureList'; import LottieAnimations from '@components/LottieAnimations'; import MenuItem from '@components/MenuItem'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction'; import ScrollView from '@components/ScrollView'; import {SearchScopeProvider} from '@components/Search/SearchScopeProvider'; @@ -20,6 +20,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useConfirmModal from '@hooks/useConfirmModal'; import useCreateEmptyReportConfirmation from '@hooks/useCreateEmptyReportConfirmation'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; @@ -172,7 +173,7 @@ function EmptySearchViewContent({ const handleContextMenuAnchorRef = useCallback((node: RNText | null) => { setContextMenuAnchor(node); }, []); - const [modalVisible, setModalVisible] = useState(false); + const {showConfirmModal} = useConfirmModal(); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); @@ -246,6 +247,40 @@ function EmptySearchViewContent({ } }, [handleCreateWorkspaceReport, openCreateReportFromSearch, shouldShowEmptyReportConfirmation]); + const handleRedirectToExpensifyClassic = useCallback(() => { + showConfirmModal({ + prompt: translate('sidebarScreen.redirectToExpensifyClassicModal.description'), + title: translate('sidebarScreen.redirectToExpensifyClassicModal.title'), + confirmText: translate('exitSurvey.goToExpensifyClassic'), + cancelText: translate('common.cancel'), + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + }); + }, [showConfirmModal, translate]); + + const handleCreateExpense = useCallback(() => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + handleRedirectToExpensifyClassic(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID()); + }); + }, [shouldRedirectToExpensifyClassic, handleRedirectToExpensifyClassic]); + + const handleCreateInvoice = useCallback(() => { + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + handleRedirectToExpensifyClassic(); + return; + } + startMoneyRequest(CONST.IOU.TYPE.INVOICE, generateReportID()); + }); + }, [shouldRedirectToExpensifyClassic, handleRedirectToExpensifyClassic]); + const typeMenuItems = useMemo(() => { return typeMenuSections.map((section) => section.menuItems).flat(); }, [typeMenuSections]); @@ -319,6 +354,7 @@ function EmptySearchViewContent({ // Default 'Folder' lottie animation, along with its background styles const defaultViewItemHeader = useSearchEmptyStateIllustration(); + // eslint-disable-next-line react-hooks/preserve-manual-memoization const content: EmptySearchViewItem = useMemo(() => { // Begin by going through all of our To-do searches, and returning their empty state // if it exists @@ -430,14 +466,7 @@ function EmptySearchViewContent({ : []), { buttonText: translate('iou.createExpense'), - buttonAction: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); - return; - } - startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID()); - }), + buttonAction: handleCreateExpense, success: true, }, ], @@ -461,14 +490,7 @@ function EmptySearchViewContent({ : []), { buttonText: translate('workspace.invoices.sendInvoice'), - buttonAction: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); - return; - } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, generateReportID()); - }), + buttonAction: handleCreateInvoice, success: true, }, ], @@ -504,11 +526,12 @@ function EmptySearchViewContent({ groupPoliciesWithChatEnabled.length, tripViewChildren, hasTransactions, - shouldRedirectToExpensifyClassic, hasExpenseReports, defaultChatEnabledPolicyID, handleCreateReportClick, queryJSON, + handleCreateExpense, + handleCreateInvoice, ]); return ( @@ -534,18 +557,6 @@ function EmptySearchViewContent({ {CreateReportConfirmationModal} - { - setModalVisible(false); - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }} - onCancel={() => setModalVisible(false)} - title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')} - confirmText={translate('exitSurvey.goToExpensifyClassic')} - cancelText={translate('common.cancel')} - /> ); } diff --git a/src/pages/Search/SearchTypeMenu.tsx b/src/pages/Search/SearchTypeMenu.tsx index eeba8905da81f..6cc1062c53a82 100644 --- a/src/pages/Search/SearchTypeMenu.tsx +++ b/src/pages/Search/SearchTypeMenu.tsx @@ -70,7 +70,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { 'CreditCardHourglass', 'Bank', ] as const); - const {showDeleteModal, DeleteConfirmModal} = useDeleteSavedSearch(); + const {showDeleteModal} = useDeleteSavedSearch(); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {canBeMissing: true}); @@ -240,13 +240,7 @@ function SearchTypeMenu({queryJSON}: SearchTypeMenuProps) { {translate(section.translationPath)} {section.translationPath === 'search.savedSearchesMenuItemTitle' ? ( - <> - {renderSavedSearchesSection(savedSearchesMenuItems)} - {/* DeleteConfirmModal is a stable JSX element returned by the hook. - Returning the element directly keeps the component identity across re-renders so React - can play its exit animation instead of removing it instantly. */} - {DeleteConfirmModal} - + <>{renderSavedSearchesSection(savedSearchesMenuItems)} ) : ( <> {section.menuItems.map((item, itemIndex) => { From e01e18dbcc967f06309004fdc02f41186b17bb8b Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 23 Jan 2026 15:56:29 +0700 Subject: [PATCH 2/3] merge two function into one --- src/pages/Search/EmptySearchView.tsx | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index dd1003b90f491..04b97dc986715 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -252,23 +252,13 @@ function EmptySearchViewContent({ }); }; - const handleCreateExpense = () => { + const handleCreateMoneyRequest = (iouType: typeof CONST.IOU.TYPE.CREATE | typeof CONST.IOU.TYPE.INVOICE) => { interceptAnonymousUser(() => { if (shouldRedirectToExpensifyClassic) { handleRedirectToExpensifyClassic(); return; } - startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID()); - }); - }; - - const handleCreateInvoice = () => { - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - handleRedirectToExpensifyClassic(); - return; - } - startMoneyRequest(CONST.IOU.TYPE.INVOICE, generateReportID()); + startMoneyRequest(iouType, generateReportID()); }); }; @@ -467,7 +457,7 @@ function EmptySearchViewContent({ : []), { buttonText: translate('iou.createExpense'), - buttonAction: handleCreateExpense, + buttonAction: () => handleCreateMoneyRequest(CONST.IOU.TYPE.CREATE), success: true, }, ], @@ -493,7 +483,7 @@ function EmptySearchViewContent({ : []), { buttonText: translate('workspace.invoices.sendInvoice'), - buttonAction: handleCreateInvoice, + buttonAction: () => handleCreateMoneyRequest(CONST.IOU.TYPE.INVOICE), success: true, }, ], From c106798e034b9b93306974b8a8037203a1dada3a Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 28 Jan 2026 00:41:16 +0700 Subject: [PATCH 3/3] change handleDEWModalOpen to use promise --- src/components/Search/index.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index 7832db4fc4b65..ee0b9c3e322a5 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -273,19 +273,21 @@ function Search({ const {translate, localeCompare, formatPhoneNumber} = useLocalize(); const searchListRef = useRef(null); - const handleDEWModalOpen = useCallback(async () => { + const handleDEWModalOpen = useCallback(() => { if (onDEWModalOpen) { onDEWModalOpen(); } else { - const result = await showConfirmModal({ + showConfirmModal({ title: translate('customApprovalWorkflow.title'), prompt: translate('customApprovalWorkflow.description'), confirmText: translate('customApprovalWorkflow.goToExpensifyClassic'), shouldShowCancelButton: false, - }); - if (result.action === ModalActions.CONFIRM) { + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } openOldDotLink(CONST.OLDDOT_URLS.INBOX); - } + }); } }, [onDEWModalOpen, showConfirmModal, translate]); @@ -1154,9 +1156,7 @@ function Search({ canSelectMultiple={canSelectMultiple} selectedTransactions={selectedTransactions} shouldPreventLongPressRow={isChat || isTask} - onDEWModalOpen={() => { - handleDEWModalOpen(); - }} + onDEWModalOpen={handleDEWModalOpen} isDEWBetaEnabled={isDEWBetaEnabled} SearchTableHeader={ !shouldShowTableHeader ? undefined : (