From 7ca73ea213c875363dab266e3d340104d71a8ecc Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 29 May 2026 03:54:52 +0530 Subject: [PATCH 1/8] Add Trip row in MoneyRequestView linking to trip room --- .../ReportActionItem/MoneyRequestView.tsx | 19 +++++++++++++++++++ src/types/onyx/Transaction.ts | 9 +++++++++ 2 files changed, 28 insertions(+) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 7d1ade75020..d734753d955 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -453,6 +453,10 @@ function MoneyRequestView({ const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; + const tripRoomReportID = transaction?.comment?.tripChatReportID; + const tripRoomName = transaction?.comment?.tripName; + const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName; + const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isPaidGroupPolicy(transactionThreadReport)); const hasViolations = (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean => getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0; @@ -1331,6 +1335,21 @@ function MoneyRequestView({ /> )} + {shouldShowTripRoomLink && ( + <> + { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(tripRoomReportID)); + }} + interactive + /> + + + )} {/* Note: "View trip details" should be always the last item */} {shouldShowViewTripDetails && ( Date: Fri, 29 May 2026 21:13:41 +0530 Subject: [PATCH 2/8] Preserve active route as backTo when opening trip room --- src/components/ReportActionItem/MoneyRequestView.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index b6cc7b04a86..076e8702b5c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -1352,9 +1352,9 @@ function MoneyRequestView({ description={translate('travel.trip')} style={[styles.moneyRequestMenuItem]} titleStyle={[styles.flex1, styles.textBlue]} - onPress={() => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(tripRoomReportID)); - }} + onPress={() => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(tripRoomReportID, undefined, undefined, Navigation.getActiveRoute())); + }} interactive /> From 429a4da7bbc1cf94de3f045e7816e48f994be9df Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 May 2026 00:22:08 +0530 Subject: [PATCH 3/8] Resolve trip room via reports collection lookup instead of denormalized NVPs --- src/components/ReportActionItem/MoneyRequestView.tsx | 12 ++++++++++-- src/types/onyx/Transaction.ts | 6 ------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 076e8702b5c..8e8385d10b0 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -455,8 +455,16 @@ function MoneyRequestView({ const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; - const tripRoomReportID = transaction?.comment?.tripChatReportID; - const tripRoomName = transaction?.comment?.tripName; + const transactionTripID = transaction?.comment?.tripID; + const tripRoomReportSelector = (reports: OnyxCollection) => { + if (!transactionTripID || !reports) { + return undefined; + } + return Object.values(reports).find((candidateReport) => candidateReport?.tripData?.tripID === transactionTripID); + }; + const [tripRoomReport] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID]); + const tripRoomReportID = tripRoomReport?.reportID; + const tripRoomName = tripRoomReport ? getReportName(tripRoomReport, reportAttributes) || tripRoomReport.reportName : undefined; const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName; const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isPaidGroupPolicy(transactionThreadReport)); diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index a77ecd1ca27..2d5023afe7b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -155,12 +155,6 @@ type Comment = { /** Spotnana trip ID, set on travel transactions and used to link the expense to its trip room */ tripID?: string; - - /** Trip room report ID, set on travel transactions so the expense can deep-link directly to its trip room */ - tripChatReportID?: string; - - /** Trip room display name, set on travel transactions for rendering the trip room link label */ - tripName?: string; }; /** Model of transaction custom unit */ From c43954e38ef9c3b4f234e7db5b996c7910cae09d Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 May 2026 02:31:09 +0530 Subject: [PATCH 4/8] Try O(1) grandparent lookup before O(n) reports scan, and add tripID to DebugUtils validators --- .../ReportActionItem/MoneyRequestView.tsx | 14 ++++++++++---- src/libs/DebugUtils.ts | 2 ++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 8e8385d10b0..65495bdefee 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -456,13 +456,19 @@ function MoneyRequestView({ const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; const transactionTripID = transaction?.comment?.tripID; + // Spotnana expense reports are parented under the trip room, so try that O(1) hop before scanning. + const grandparentReportID = parentReport?.parentReportID; const tripRoomReportSelector = (reports: OnyxCollection) => { if (!transactionTripID || !reports) { return undefined; } + const grandparent = grandparentReportID ? reports[`${ONYXKEYS.COLLECTION.REPORT}${grandparentReportID}`] : undefined; + if (grandparent?.tripData?.tripID === transactionTripID) { + return grandparent; + } return Object.values(reports).find((candidateReport) => candidateReport?.tripData?.tripID === transactionTripID); }; - const [tripRoomReport] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID]); + const [tripRoomReport] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID, grandparentReportID]); const tripRoomReportID = tripRoomReport?.reportID; const tripRoomName = tripRoomReport ? getReportName(tripRoomReport, reportAttributes) || tripRoomReport.reportName : undefined; const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName; @@ -1360,9 +1366,9 @@ function MoneyRequestView({ description={translate('travel.trip')} style={[styles.moneyRequestMenuItem]} titleStyle={[styles.flex1, styles.textBlue]} - onPress={() => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(tripRoomReportID, undefined, undefined, Navigation.getActiveRoute())); - }} + onPress={() => { + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(tripRoomReportID, undefined, undefined, Navigation.getActiveRoute())); + }} interactive /> diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index e1823b26703..926dbc758ce 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1072,6 +1072,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) odometerEnd: CONST.RED_BRICK_ROAD_PENDING_ACTION, odometerStartImage: CONST.RED_BRICK_ROAD_PENDING_ACTION, odometerEndImage: CONST.RED_BRICK_ROAD_PENDING_ACTION, + tripID: CONST.RED_BRICK_ROAD_PENDING_ACTION, attendees: CONST.RED_BRICK_ROAD_PENDING_ACTION, amount: CONST.RED_BRICK_ROAD_PENDING_ACTION, taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION, @@ -1194,6 +1195,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string) odometerEnd: 'number', odometerStartImage: 'object', odometerEndImage: 'object', + tripID: 'string', }); case 'accountant': return validateObject>(value, { From da554d256bd2e48712251db88107464842845f34 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 May 2026 02:39:39 +0530 Subject: [PATCH 5/8] fix style --- src/components/ReportActionItem/MoneyRequestView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 65495bdefee..d98b309306a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -456,6 +456,7 @@ function MoneyRequestView({ const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; const transactionTripID = transaction?.comment?.tripID; + // Spotnana expense reports are parented under the trip room, so try that O(1) hop before scanning. const grandparentReportID = parentReport?.parentReportID; const tripRoomReportSelector = (reports: OnyxCollection) => { From dc97a972f70f60b24a54c8461c0d150242fc59b7 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 May 2026 02:49:23 +0530 Subject: [PATCH 6/8] Narrow trip room selector to return only reportID and name so Onyx deepEqual stays cheap --- .../ReportActionItem/MoneyRequestView.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d98b309306a..60f59cb2373 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -458,20 +458,27 @@ function MoneyRequestView({ const transactionTripID = transaction?.comment?.tripID; // Spotnana expense reports are parented under the trip room, so try that O(1) hop before scanning. + // Narrow the selector output to just the two primitives we render so Onyx's deepEqual stays cheap + // on every report collection mutation (PERF-11). const grandparentReportID = parentReport?.parentReportID; const tripRoomReportSelector = (reports: OnyxCollection) => { if (!transactionTripID || !reports) { return undefined; } const grandparent = grandparentReportID ? reports[`${ONYXKEYS.COLLECTION.REPORT}${grandparentReportID}`] : undefined; - if (grandparent?.tripData?.tripID === transactionTripID) { - return grandparent; + const match = + grandparent?.tripData?.tripID === transactionTripID ? grandparent : Object.values(reports).find((candidateReport) => candidateReport?.tripData?.tripID === transactionTripID); + if (!match?.reportID) { + return undefined; } - return Object.values(reports).find((candidateReport) => candidateReport?.tripData?.tripID === transactionTripID); + return { + reportID: match.reportID, + name: getReportName(match, reportAttributes) || match.reportName, + }; }; - const [tripRoomReport] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID, grandparentReportID]); - const tripRoomReportID = tripRoomReport?.reportID; - const tripRoomName = tripRoomReport ? getReportName(tripRoomReport, reportAttributes) || tripRoomReport.reportName : undefined; + const [tripRoomInfo] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID, grandparentReportID, reportAttributes]); + const tripRoomReportID = tripRoomInfo?.reportID; + const tripRoomName = tripRoomInfo?.name; const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName; const {getViolationsForField} = useViolations(transactionViolations ?? [], isTransactionScanning || !isPaidGroupPolicy(transactionThreadReport)); From 7b4fe96e318cdff8c0d8c8eb5e740a2f9a8f71a1 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 May 2026 02:53:41 +0530 Subject: [PATCH 7/8] remove bad comment --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 60f59cb2373..6541494c962 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -458,8 +458,6 @@ function MoneyRequestView({ const transactionTripID = transaction?.comment?.tripID; // Spotnana expense reports are parented under the trip room, so try that O(1) hop before scanning. - // Narrow the selector output to just the two primitives we render so Onyx's deepEqual stays cheap - // on every report collection mutation (PERF-11). const grandparentReportID = parentReport?.parentReportID; const tripRoomReportSelector = (reports: OnyxCollection) => { if (!transactionTripID || !reports) { From 9765c4576f038cd75c8092b0e334a0b5d1295db5 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sat, 30 May 2026 03:05:27 +0530 Subject: [PATCH 8/8] Use originalUseOnyx so trip room lookup hits live data instead of Search snapshot --- src/components/ReportActionItem/MoneyRequestView.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 6541494c962..7d7cdf2a20c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -2,6 +2,11 @@ import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +// Use the original useOnyx hook to scan the live report collection when resolving the trip room. +// @hooks/useOnyx redirects collection reads to the search snapshot under Search routes, which +// typically does not include the grandparent trip room and would leave the Trip row hidden. +// eslint-disable-next-line no-restricted-imports +import {useOnyx as originalUseOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import Icon from '@components/Icon'; @@ -458,6 +463,8 @@ function MoneyRequestView({ const transactionTripID = transaction?.comment?.tripID; // Spotnana expense reports are parented under the trip room, so try that O(1) hop before scanning. + // Use originalUseOnyx so the lookup hits the live report collection - the grandparent trip room + // is typically not in the Search snapshot. const grandparentReportID = parentReport?.parentReportID; const tripRoomReportSelector = (reports: OnyxCollection) => { if (!transactionTripID || !reports) { @@ -474,7 +481,7 @@ function MoneyRequestView({ name: getReportName(match, reportAttributes) || match.reportName, }; }; - const [tripRoomInfo] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID, grandparentReportID, reportAttributes]); + const [tripRoomInfo] = originalUseOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: tripRoomReportSelector}, [transactionTripID, grandparentReportID, reportAttributes]); const tripRoomReportID = tripRoomInfo?.reportID; const tripRoomName = tripRoomInfo?.name; const shouldShowTripRoomLink = !!tripRoomReportID && !!tripRoomName;