diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 8384faff98b1..e0cc175eb321 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -1165,13 +1165,13 @@ "../../src/pages/TransactionDuplicate/Confirmation.tsx" "react-hooks/refs" 12 "../../src/pages/TransactionDuplicate/DuplicateTransactionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2 "../../src/pages/TransactionDuplicate/Review.tsx" "no-restricted-imports" 1 -"../../src/pages/TransactionMerge/ConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../../src/pages/TransactionMerge/ConfirmationPage.tsx" "no-restricted-imports" 1 -"../../src/pages/TransactionMerge/DetailsReviewPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../../src/pages/TransactionMerge/DetailsReviewPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/DynamicConfirmationPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TransactionMerge/DynamicConfirmationPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 +"../../src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx" "no-restricted-imports" 1 "../../src/pages/TransactionMerge/MergeTransactionItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/TransactionMerge/MergeTransactionsListContent.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 -"../../src/pages/TransactionMerge/ReceiptReviewPage.tsx" "no-restricted-imports" 1 +"../../src/pages/TransactionMerge/DynamicReceiptReviewPage.tsx" "no-restricted-imports" 1 "../../src/pages/TransactionMerge/TransactionMergeReceipts.tsx" "no-restricted-imports" 1 "../../src/pages/Travel/DynamicDomainPermissionInfoPage.tsx" "no-restricted-imports" 1 "../../src/pages/Travel/DynamicDomainSelectorPage.tsx" "no-restricted-imports" 1 diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4dba1f7c8238..b4718fe5d95e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -227,6 +227,30 @@ const DYNAMIC_ROUTES = { getRoute: (isMovingExpenses?: boolean) => `new-report-workspace-selection${isMovingExpenses ? '?isMovingExpenses=true' : ''}` as const, queryParams: ['isMovingExpenses'], }, + MERGE_TRANSACTION_LIST_PAGE: { + path: 'merge/:transactionID', + entryScreens: [SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.SEARCH.ROOT], + getRoute: (transactionID: string, isOnSearch?: boolean) => getUrlWithParams(`merge/${transactionID}`, {isOnSearch: isOnSearch ? 'true' : undefined}), + queryParams: ['isOnSearch'], + }, + MERGE_TRANSACTION_RECEIPT_PAGE: { + path: 'merge/:transactionID/receipt', + entryScreens: [SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.SEARCH.ROOT], + getRoute: (transactionID: string, isOnSearch?: boolean) => getUrlWithParams(`merge/${transactionID}/receipt`, {isOnSearch: isOnSearch ? 'true' : undefined}), + queryParams: ['isOnSearch'], + }, + MERGE_TRANSACTION_DETAILS_PAGE: { + path: 'merge/:transactionID/details', + entryScreens: [SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.SEARCH.ROOT], + getRoute: (transactionID: string, isOnSearch?: boolean) => getUrlWithParams(`merge/${transactionID}/details`, {isOnSearch: isOnSearch ? 'true' : undefined}), + queryParams: ['isOnSearch'], + }, + MERGE_TRANSACTION_CONFIRMATION_PAGE: { + path: 'merge/:transactionID/confirmation', + entryScreens: [SCREENS.REPORT, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT, SCREENS.SEARCH.ROOT], + getRoute: (transactionID: string, isOnSearch?: boolean) => getUrlWithParams(`merge/${transactionID}/confirmation`, {isOnSearch: isOnSearch ? 'true' : undefined}), + queryParams: ['isOnSearch'], + }, NETSUITE_AUTO_SYNC: { path: 'netsuite-autosync', entryScreens: [SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_ADVANCED, SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION], @@ -3519,38 +3543,6 @@ const ROUTES = { getRoute: (threadReportID: string, backTo?: string) => getUrlWithBackToParam(`r/${threadReportID}/duplicates/confirm` as const, backTo), }, - MERGE_TRANSACTION_LIST_PAGE: { - route: 'merge/:transactionID', - - getRoute: (transactionID: string, backTo: string, isOnSearch = false) => { - const url = getUrlWithBackToParam(`merge/${transactionID}` as const, backTo); - return isOnSearch ? (`${url}&isOnSearch=true` as const) : url; - }, - }, - MERGE_TRANSACTION_RECEIPT_PAGE: { - route: 'merge/:transactionID/receipt', - - getRoute: (transactionID: string, backTo: string, isOnSearch = false) => { - const url = getUrlWithBackToParam(`merge/${transactionID}/receipt` as const, backTo); - return isOnSearch ? (`${url}&isOnSearch=true` as const) : url; - }, - }, - MERGE_TRANSACTION_DETAILS_PAGE: { - route: 'merge/:transactionID/details', - - getRoute: (transactionID: string, backTo: string, isOnSearch = false) => { - const url = getUrlWithBackToParam(`merge/${transactionID}/details` as const, backTo); - return isOnSearch ? (`${url}&isOnSearch=true` as const) : url; - }, - }, - MERGE_TRANSACTION_CONFIRMATION_PAGE: { - route: 'merge/:transactionID/confirmation', - - getRoute: (transactionID: string, backTo: string, isOnSearch = false) => { - const url = getUrlWithBackToParam(`merge/${transactionID}/confirmation` as const, backTo); - return isOnSearch ? (`${url}&isOnSearch=true` as const) : url; - }, - }, POLICY_ACCOUNTING_XERO_SETUP: { route: 'workspaces/:policyID/accounting/xero/setup', getRoute: (policyID: string) => `workspaces/${policyID}/accounting/xero/setup` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ad60f8a1f754..ab9d36824924 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -393,10 +393,10 @@ const SCREENS = { }, MERGE_TRANSACTION: { - LIST_PAGE: 'Merge_Transaction_List_Page', - RECEIPT_PAGE: 'Merge_Transaction_Receipt_Page', - DETAILS_PAGE: 'Merge_Transaction_Details_Page', - CONFIRMATION_PAGE: 'Merge_Transaction_Confirmation_Page', + DYNAMIC_LIST_PAGE: 'Dynamic_Merge_Transaction_List_Page', + DYNAMIC_RECEIPT_PAGE: 'Dynamic_Merge_Transaction_Receipt_Page', + DYNAMIC_DETAILS_PAGE: 'Dynamic_Merge_Transaction_Details_Page', + DYNAMIC_CONFIRMATION_PAGE: 'Dynamic_Merge_Transaction_Confirmation_Page', }, IOU_SEND: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 98fda2e2ce2c..a856207e21b2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1152,10 +1152,10 @@ const TransactionDuplicateStackNavigator = createModalStackNavigator({ - [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: () => require('../../../../pages/TransactionMerge/MergeTransactionsListPage').default, - [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: () => require('../../../../pages/TransactionMerge/ReceiptReviewPage').default, - [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: () => require('../../../../pages/TransactionMerge/DetailsReviewPage').default, - [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: () => require('../../../../pages/TransactionMerge/ConfirmationPage').default, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_LIST_PAGE]: () => require('../../../../pages/TransactionMerge/DynamicMergeTransactionsListPage').default, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_RECEIPT_PAGE]: () => require('../../../../pages/TransactionMerge/DynamicReceiptReviewPage').default, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_DETAILS_PAGE]: () => require('../../../../pages/TransactionMerge/DynamicDetailsReviewPage').default, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_CONFIRMATION_PAGE]: () => require('../../../../pages/TransactionMerge/DynamicConfirmationPage').default, }); const SearchReportActionsModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 326c2056a5da..d496d40c2a68 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1859,10 +1859,10 @@ const config: LinkingOptions['config'] = { }, [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: { screens: { - [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: ROUTES.MERGE_TRANSACTION_LIST_PAGE.route, - [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.route, - [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.route, - [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.route, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_LIST_PAGE]: DYNAMIC_ROUTES.MERGE_TRANSACTION_LIST_PAGE.path, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_RECEIPT_PAGE]: DYNAMIC_ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.path, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_DETAILS_PAGE]: DYNAMIC_ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.path, + [SCREENS.MERGE_TRANSACTION.DYNAMIC_CONFIRMATION_PAGE]: DYNAMIC_ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.path, }, }, [SCREENS.RIGHT_MODAL.SPLIT_DETAILS]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 81d103fbc9d6..328fbcf6f823 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2549,28 +2549,20 @@ type TransactionDuplicateNavigatorParamList = { }; type MergeTransactionNavigatorParamList = { - [SCREENS.MERGE_TRANSACTION.LIST_PAGE]: { + [SCREENS.MERGE_TRANSACTION.DYNAMIC_LIST_PAGE]: { transactionID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; isOnSearch?: boolean; }; - [SCREENS.MERGE_TRANSACTION.RECEIPT_PAGE]: { + [SCREENS.MERGE_TRANSACTION.DYNAMIC_RECEIPT_PAGE]: { transactionID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; isOnSearch?: boolean; }; - [SCREENS.MERGE_TRANSACTION.DETAILS_PAGE]: { + [SCREENS.MERGE_TRANSACTION.DYNAMIC_DETAILS_PAGE]: { transactionID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; isOnSearch?: boolean; }; - [SCREENS.MERGE_TRANSACTION.CONFIRMATION_PAGE]: { + [SCREENS.MERGE_TRANSACTION.DYNAMIC_CONFIRMATION_PAGE]: { transactionID: string; - // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md - backTo?: Routes; isOnSearch?: boolean; }; }; diff --git a/src/libs/actions/MergeTransaction.ts b/src/libs/actions/MergeTransaction.ts index d6e50d76c154..3b9603c27830 100644 --- a/src/libs/actions/MergeTransaction.ts +++ b/src/libs/actions/MergeTransaction.ts @@ -14,6 +14,7 @@ import { shouldNavigateToReceiptReview, } from '@libs/MergeTransactionUtils'; import type {MergeFieldKey, MergeTransactionUpdateValues} from '@libs/MergeTransactionUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import {isPaidGroupPolicy, isPolicyAdmin} from '@libs/PolicyUtils'; import {getIOUActionForReportID, getReportAction, getTrackExpenseActionableWhisper} from '@libs/ReportActionsUtils'; @@ -30,7 +31,7 @@ import { import CONST from '@src/CONST'; import {isDistanceRequest, isTransactionPendingDelete} from '@src/libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {CardList, MergeTransaction, Policy, PolicyCategories, PolicyTagLists, Report, ReportNextStepDeprecated, Transaction, TransactionViolations} from '@src/types/onyx'; import type {OnyxCollection, OnyxEntry, OnyxMergeInput, OnyxUpdate} from 'react-native-onyx'; @@ -68,16 +69,22 @@ function setupMergeTransactionDataAndNavigate( isSelectingSourceTransaction?: boolean, isOnSearch?: boolean, policies?: Array>, + basePath?: string, ) { if (!transactions.length || transactions.length > 2) { return; } + // The dynamic merge routes are appended to the screen the flow started from. When advancing between steps + // (e.g. from the list page after selecting a source transaction) the caller passes the base path so the + // suffix isn't appended to the current merge URL. Otherwise fall back to the active route. + const base = basePath ?? Navigation.getActiveRoute(); + if (transactions.length === 1) { const transaction = transactions.at(0); if (transaction) { setupMergeTransactionData(navigationTransactionID, {targetTransactionID: transaction.transactionID}); - Navigation.navigate(ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(transaction.transactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(transaction.transactionID, isOnSearch), base)); return; } } @@ -98,9 +105,16 @@ function setupMergeTransactionDataAndNavigate( } else { setupMergeTransactionData(navigationTransactionID, setupData); } + + // When advancing from the list page (after selecting a source), the merge RHP modal is already open, so we must + // replace the current dynamic screen rather than pushing a new one. Pushing a second dynamic merge screen makes + // getPathFromState concatenate both suffixes into an invalid doubled URL (e.g. .../merge/ID/merge/ID/receipt). + // Fresh entries (bulk selection) open the modal normally. + const navigationOptions = isSelectingSourceTransaction ? {forceReplace: true} : undefined; + if (shouldNavigateToReceiptReview([targetTransaction, sourceTransaction])) { // Navigate to the receipt review page if both transactions have a receipt - Navigation.navigate(ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.getRoute(navigationTransactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.getRoute(navigationTransactionID, isOnSearch), base), navigationOptions); } else { const receipt = targetTransaction.receipt?.receiptID ? targetTransaction.receipt : sourceTransaction.receipt; if (receipt) { @@ -120,11 +134,11 @@ function setupMergeTransactionDataAndNavigate( if (!conflictFields.length) { // If there are no conflict fields, we should set mergeable data and navigate to the confirmation page setMergeTransactionKey(navigationTransactionID, mergeableData); - Navigation.navigate(ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.getRoute(navigationTransactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.getRoute(navigationTransactionID, isOnSearch), base), navigationOptions); return; } - Navigation.navigate(ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(navigationTransactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(navigationTransactionID, isOnSearch), base), navigationOptions); } } diff --git a/src/pages/TransactionMerge/ConfirmationPage.tsx b/src/pages/TransactionMerge/DynamicConfirmationPage.tsx similarity index 69% rename from src/pages/TransactionMerge/ConfirmationPage.tsx rename to src/pages/TransactionMerge/DynamicConfirmationPage.tsx index 0dc3301bdab4..803f17d5cd97 100644 --- a/src/pages/TransactionMerge/ConfirmationPage.tsx +++ b/src/pages/TransactionMerge/DynamicConfirmationPage.tsx @@ -8,8 +8,10 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; +import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useDelegateAccountID from '@hooks/useDelegateAccountID'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useMergeTransactions from '@hooks/useMergeTransactions'; import useOnyx from '@hooks/useOnyx'; @@ -19,7 +21,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {mergeTransactionRequest} from '@libs/actions/MergeTransaction'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import {buildMergedTransactionData, getTransactionThreadReportID} from '@libs/MergeTransactionUtils'; +import {buildMergedTransactionData, getMergeableDataAndConflictFields, getTransactionThreadReportID, shouldNavigateToReceiptReview} from '@libs/MergeTransactionUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -29,26 +32,30 @@ import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Transaction} from '@src/types/onyx'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type {OnyxEntry} from 'react-native-onyx'; -import React, {useState} from 'react'; +import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; -type ConfirmationPageProps = PlatformStackScreenProps; +type DynamicConfirmationPageProps = PlatformStackScreenProps; -function ConfirmationPage({route}: ConfirmationPageProps) { - const {translate} = useLocalize(); +function DynamicConfirmationPage({route}: DynamicConfirmationPageProps) { + const {translate, localeCompare} = useLocalize(); const styles = useThemeStyles(); + const {getCurrencyDecimals} = useCurrencyListActions(); const [isMergingExpenses, setIsMergingExpenses] = useState(false); - const {transactionID, isOnSearch, backTo} = route.params; + const {transactionID, isOnSearch} = route.params; + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.path); const [mergeTransaction, mergeTransactionMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); - const {targetTransaction, sourceTransaction, targetTransactionReport, targetTransactionPolicy} = useMergeTransactions({mergeTransaction}); + const {targetTransaction, sourceTransaction, targetTransactionReport, sourceTransactionReport, targetTransactionPolicy, sourceTransactionPolicy} = useMergeTransactions({ + mergeTransaction, + }); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetTransactionPolicy?.id}`); @@ -71,6 +78,52 @@ function ConfirmationPage({route}: ConfirmationPageProps) { // Build the merged transaction data for display const mergedTransactionData = buildMergedTransactionData(targetTransaction, mergeTransaction); + // The confirmation page can be reached after the details step (when the transactions had conflicting fields), + // after the receipt step (both had a receipt but no conflicts), directly from the list page (single-expense merge), + // or directly from the entry screen (merging two selected expenses). Recompute the same branching so back + // navigation returns to the exact step that preceded it. The list is only part of the flow when it populated + // `eligibleTransactions`; otherwise the flow was entered directly and back must exit to the entry screen. + const cameFromList = mergeTransaction?.eligibleTransactions !== undefined; + const backRoute = useMemo(() => { + if (targetTransaction && sourceTransaction) { + const {conflictFields} = getMergeableDataAndConflictFields( + targetTransaction, + sourceTransaction, + localeCompare, + getCurrencyDecimals, + [targetTransactionReport, sourceTransactionReport], + targetTransactionPolicy, + sourceTransactionPolicy, + ); + if (conflictFields.length > 0) { + return createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(transactionID, isOnSearch), backPath); + } + } + + if (shouldNavigateToReceiptReview([targetTransaction, sourceTransaction])) { + return createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.getRoute(transactionID, isOnSearch), backPath); + } + + if (cameFromList) { + return createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(transactionID, isOnSearch), backPath); + } + + return backPath; + }, [ + targetTransaction, + sourceTransaction, + localeCompare, + getCurrencyDecimals, + targetTransactionReport, + sourceTransactionReport, + targetTransactionPolicy, + sourceTransactionPolicy, + cameFromList, + transactionID, + isOnSearch, + backPath, + ]); + const handleMergeExpenses = () => { if (!targetTransaction || !mergeTransaction || !sourceTransaction) { return; @@ -128,7 +181,7 @@ function ConfirmationPage({route}: ConfirmationPageProps) { return ( @@ -136,7 +189,7 @@ function ConfirmationPage({route}: ConfirmationPageProps) { { - Navigation.goBack(backTo); + Navigation.goBack(backRoute); }} /> @@ -165,4 +218,4 @@ function ConfirmationPage({route}: ConfirmationPageProps) { ); } -export default ConfirmationPage; +export default DynamicConfirmationPage; diff --git a/src/pages/TransactionMerge/DetailsReviewPage.tsx b/src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx similarity index 83% rename from src/pages/TransactionMerge/DetailsReviewPage.tsx rename to src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx index ea1cfbd92617..fda6cd8165c7 100644 --- a/src/pages/TransactionMerge/DetailsReviewPage.tsx +++ b/src/pages/TransactionMerge/DynamicDetailsReviewPage.tsx @@ -9,6 +9,7 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import useLocalize from '@hooks/useLocalize'; import useMergeTransactions from '@hooks/useMergeTransactions'; import useOnyx from '@hooks/useOnyx'; @@ -24,8 +25,10 @@ import { getMergeFieldUpdatedValues, getMergeFieldValue, isEmptyMergeValue, + shouldNavigateToReceiptReview, } from '@libs/MergeTransactionUtils'; import type {MergeFieldKey} from '@libs/MergeTransactionUtils'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {MergeTransactionNavigatorParamList} from '@libs/Navigation/types'; @@ -33,7 +36,7 @@ import type {TransactionDetails} from '@libs/ReportUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Transaction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -44,13 +47,14 @@ import {View} from 'react-native'; import MergeFieldReview from './MergeFieldReview'; -type DetailsReviewPageProps = PlatformStackScreenProps; +type DynamicDetailsReviewPageProps = PlatformStackScreenProps; -function DetailsReviewPage({route}: DetailsReviewPageProps) { +function DynamicDetailsReviewPage({route}: DynamicDetailsReviewPageProps) { const {translate, localeCompare} = useLocalize(); const styles = useThemeStyles(); const {getCurrencyDecimals, convertToDisplayString} = useCurrencyListActions(); - const {transactionID, isOnSearch, backTo} = route.params; + const {transactionID, isOnSearch} = route.params; + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.path); const [mergeTransaction, mergeTransactionMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); const {targetTransaction, sourceTransaction, targetTransactionReport, sourceTransactionReport, targetTransactionPolicy, sourceTransactionPolicy} = useMergeTransactions({ @@ -151,9 +155,23 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { setHasErrors(newHasErrors); if (isEmptyObject(newHasErrors)) { - Navigation.navigate(ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.getRoute(transactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.getRoute(transactionID, isOnSearch), backPath), {forceReplace: true}); } - }, [mergeTransaction, conflictFields, transactionID, isOnSearch]); + }, [mergeTransaction, conflictFields, transactionID, isOnSearch, backPath]); + + // The details page is reached after the receipt review step (when both transactions have a receipt), or directly + // from the list page (single-expense merge), or directly from the entry screen (merging two selected expenses). + // Point back navigation at whichever step actually preceded it so the wizard steps back one page at a time. + const cameFromList = mergeTransaction?.eligibleTransactions !== undefined; + const backRoute = useMemo(() => { + if (shouldNavigateToReceiptReview([targetTransaction, sourceTransaction])) { + return createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.getRoute(transactionID, isOnSearch), backPath); + } + if (cameFromList) { + return createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(transactionID, isOnSearch), backPath); + } + return backPath; + }, [targetTransaction, sourceTransaction, cameFromList, transactionID, isOnSearch, backPath]); // Build merge fields array with all necessary information const mergeFields = useMemo( @@ -202,7 +220,7 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { return ( @@ -210,7 +228,7 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { { - Navigation.goBack(backTo); + Navigation.goBack(backRoute); }} /> @@ -247,4 +265,4 @@ function DetailsReviewPage({route}: DetailsReviewPageProps) { ); } -export default DetailsReviewPage; +export default DynamicDetailsReviewPage; diff --git a/src/pages/TransactionMerge/MergeTransactionsListPage.tsx b/src/pages/TransactionMerge/DynamicMergeTransactionsListPage.tsx similarity index 82% rename from src/pages/TransactionMerge/MergeTransactionsListPage.tsx rename to src/pages/TransactionMerge/DynamicMergeTransactionsListPage.tsx index 76cfaf2fe1c4..be713a779cc7 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListPage.tsx +++ b/src/pages/TransactionMerge/DynamicMergeTransactionsListPage.tsx @@ -19,11 +19,11 @@ import React from 'react'; import MergeTransactionsListContent from './MergeTransactionsListContent'; -type MergeTransactionsListPageProps = PlatformStackScreenProps; +type DynamicMergeTransactionsListPageProps = PlatformStackScreenProps; -function MergeTransactionsListPage({route}: MergeTransactionsListPageProps) { +function DynamicMergeTransactionsListPage({route}: DynamicMergeTransactionsListPageProps) { const {translate} = useLocalize(); - const {transactionID, backTo} = route.params; + const {transactionID} = route.params; const [mergeTransaction, mergeTransactionMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${transactionID}`); @@ -37,7 +37,7 @@ function MergeTransactionsListPage({route}: MergeTransactionsListPageProps) { return ( @@ -45,7 +45,7 @@ function MergeTransactionsListPage({route}: MergeTransactionsListPageProps) { { - Navigation.goBack(backTo); + Navigation.goBack(); }} /> ; +type DynamicReceiptReviewPageProps = PlatformStackScreenProps; -function ReceiptReviewPage({route}: ReceiptReviewPageProps) { +function DynamicReceiptReviewPage({route}: DynamicReceiptReviewPageProps) { const {translate, localeCompare} = useLocalize(); const styles = useThemeStyles(); const {getCurrencyDecimals} = useCurrencyListActions(); - const {transactionID, isOnSearch, backTo} = route.params; + const {transactionID, isOnSearch} = route.params; + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MERGE_TRANSACTION_RECEIPT_PAGE.path); const [mergeTransaction, mergeTransactionMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.MERGE_TRANSACTION}${getNonEmptyStringOnyxID(transactionID)}`); const {targetTransaction, sourceTransaction, targetTransactionPolicy, sourceTransactionPolicy} = useMergeTransactions({mergeTransaction}); + // The list page is only part of the flow when merging starts from a single expense (the list is where the source + // is picked, which populates `eligibleTransactions`). When merging two selected expenses directly, the list is + // skipped, so back navigation from the first step must exit to the entry screen instead of the list. + const cameFromList = mergeTransaction?.eligibleTransactions !== undefined; + const backRoute = cameFromList ? createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_LIST_PAGE.getRoute(transactionID, isOnSearch), backPath) : backPath; + const transactions = [targetTransaction, sourceTransaction].filter((transaction): transaction is Transaction => !!transaction); const handleSelect = (receipt: Receipt | undefined) => { @@ -67,10 +76,10 @@ function ReceiptReviewPage({route}: ReceiptReviewPageProps) { if (!conflictFields.length) { // If there are no conflict fields, we should set mergeable data and navigate to the confirmation page setMergeTransactionKey(transactionID, mergeableData); - Navigation.navigate(ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.getRoute(transactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_CONFIRMATION_PAGE.getRoute(transactionID, isOnSearch), backPath), {forceReplace: true}); return; } - Navigation.navigate(ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(transactionID, Navigation.getActiveRoute(), isOnSearch)); + Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.MERGE_TRANSACTION_DETAILS_PAGE.getRoute(transactionID, isOnSearch), backPath), {forceReplace: true}); }; if (isLoadingOnyxValue(mergeTransactionMetadata)) { @@ -83,7 +92,7 @@ function ReceiptReviewPage({route}: ReceiptReviewPageProps) { return ( @@ -91,7 +100,7 @@ function ReceiptReviewPage({route}: ReceiptReviewPageProps) { { - Navigation.goBack(backTo); + Navigation.goBack(backRoute); }} /> @@ -119,4 +128,4 @@ function ReceiptReviewPage({route}: ReceiptReviewPageProps) { ); } -export default ReceiptReviewPage; +export default DynamicReceiptReviewPage; diff --git a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx index bd299a0b1f60..ef63af96b4fe 100644 --- a/src/pages/TransactionMerge/MergeTransactionsListContent.tsx +++ b/src/pages/TransactionMerge/MergeTransactionsListContent.tsx @@ -7,6 +7,7 @@ import MergeExpensesSkeleton from '@components/Skeletons/MergeExpensesSkeleton'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useDebouncedState from '@hooks/useDebouncedState'; +import useDynamicBackPath from '@hooks/useDynamicBackPath'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useMergeTransactions from '@hooks/useMergeTransactions'; @@ -23,6 +24,7 @@ import {getAmount, getCreated, getCurrency, getDescription, getMerchant, isExpen import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type {MergeTransaction} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type Transaction from '@src/types/onyx/Transaction'; @@ -52,6 +54,7 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const {isOffline} = useNetwork(); const {convertToDisplayString, getCurrencyDecimals} = useCurrencyListActions(); + const backPath = useDynamicBackPath(DYNAMIC_ROUTES.MERGE_TRANSACTION_LIST_PAGE.path); const eligibleTransactions = mergeTransaction?.eligibleTransactions; const {targetTransaction, sourceTransaction, targetTransactionReport, sourceTransactionReport, targetTransactionPolicy, sourceTransactionPolicy} = useMergeTransactions({ @@ -163,10 +166,17 @@ function MergeTransactionsListContent({transactionID, mergeTransaction}: MergeTr } const reports = targetTransactionReport && sourceTransactionReport ? [targetTransactionReport, sourceTransactionReport] : undefined; - setupMergeTransactionDataAndNavigate(transactionID, [targetTransaction, sourceTransaction], localeCompare, getCurrencyDecimals, reports, true, undefined, [ - targetTransactionPolicy, - sourceTransactionPolicy, - ]); + setupMergeTransactionDataAndNavigate( + transactionID, + [targetTransaction, sourceTransaction], + localeCompare, + getCurrencyDecimals, + reports, + true, + undefined, + [targetTransactionPolicy, sourceTransactionPolicy], + backPath, + ); }; const confirmButtonOptions = {