From 1a370aee4befa126a989b01b7e5b452d05b55568 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Mon, 2 Mar 2026 17:30:29 +0100 Subject: [PATCH 01/12] migrate to useReceiptScan --- src/libs/actions/IOU/MoneyRequest.ts | 19 ++++++++++++++++--- .../step/IOURequestStepScan/useReceiptScan.ts | 2 ++ tests/actions/IOU/MoneyRequestTest.ts | 13 +++++++++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/IOU/MoneyRequest.ts b/src/libs/actions/IOU/MoneyRequest.ts index 9d122915dbf0..ae96b9721e40 100644 --- a/src/libs/actions/IOU/MoneyRequest.ts +++ b/src/libs/actions/IOU/MoneyRequest.ts @@ -17,7 +17,19 @@ import CONST from '@src/CONST'; import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type {Beta, IntroSelected, LastSelectedDistanceRates, PersonalDetailsList, Policy, QuickAction, RecentWaypoint, Report, Transaction, TransactionViolation} from '@src/types/onyx'; +import type { + Beta, + IntroSelected, + LastSelectedDistanceRates, + PersonalDetailsList, + Policy, + PolicyTagLists, + QuickAction, + RecentWaypoint, + Report, + Transaction, + TransactionViolation, +} from '@src/types/onyx'; import type {ReportAttributes, ReportAttributesDerivedValue} from '@src/types/onyx/DerivedValues'; import type {Participant} from '@src/types/onyx/IOU'; import type {Unit} from '@src/types/onyx/Policy'; @@ -26,7 +38,6 @@ import type {GpsPoint} from './index'; import { createDistanceRequest, getMoneyRequestParticipantsFromReport, - getPolicyTags, requestMoney, setCustomUnitRateID, setMoneyRequestDistance, @@ -110,6 +121,7 @@ type MoneyRequestStepScanParticipantsFlowParams = { allTransactionDrafts: OnyxCollection; betas: OnyxEntry; recentWaypoints: OnyxEntry; + allPolicyTags: OnyxCollection; }; type MoneyRequestStepDistanceNavigationParams = { @@ -309,6 +321,7 @@ function handleMoneyRequestStepScanParticipants({ allTransactionDrafts, betas, recentWaypoints, + allPolicyTags, }: MoneyRequestStepScanParticipantsFlowParams) { if (backTo) { Navigation.goBack(backTo); @@ -367,7 +380,7 @@ function handleMoneyRequestStepScanParticipants({ policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], // No need to update recently used tags because no tags are used when the confirmation step is skipped policyRecentlyUsedTags: undefined, - allPolicyTags: getPolicyTags(), + allPolicyTags, }); return; } diff --git a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts index 06555dcbae25..6081a74234bc 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts @@ -127,6 +127,7 @@ function useReceiptScan({ } const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS); + const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); function navigateToConfirmationStep(files: ReceiptFile[], locationPermissionGranted = false, isTestTransaction = false) { handleMoneyRequestStepScanParticipants({ @@ -171,6 +172,7 @@ function useReceiptScan({ betas, recentWaypoints, allTransactionDrafts, + allPolicyTags, }); } diff --git a/tests/actions/IOU/MoneyRequestTest.ts b/tests/actions/IOU/MoneyRequestTest.ts index 59ef83449892..229f14dd74b1 100644 --- a/tests/actions/IOU/MoneyRequestTest.ts +++ b/tests/actions/IOU/MoneyRequestTest.ts @@ -1,5 +1,6 @@ -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import {getOnyxData} from 'tests/utils/TestHelper'; import type {MoneyRequestStepScanParticipantsFlowParams} from '@libs/actions/IOU/MoneyRequest'; import {createTransaction, handleMoneyRequestStepDistanceNavigation, handleMoneyRequestStepScanParticipants} from '@libs/actions/IOU/MoneyRequest'; import getCurrentPosition from '@libs/getCurrentPosition'; @@ -9,7 +10,7 @@ import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, QuickAction, RecentWaypoint} from '@src/types/onyx'; +import type {Policy, PolicyTagLists, QuickAction, RecentWaypoint} from '@src/types/onyx'; import type {SplitShares} from '@src/types/onyx/Transaction'; import * as IOU from '../../../src/libs/actions/IOU'; import * as Split from '../../../src/libs/actions/IOU/Split'; @@ -413,6 +414,7 @@ describe('MoneyRequest', () => { betas: [], recentWaypoints: [] as RecentWaypoint[], allTransactionDrafts: {}, + allPolicyTags: {} as OnyxCollection, }; beforeEach(async () => { @@ -426,6 +428,13 @@ describe('MoneyRequest', () => { }, }); baseParams.recentWaypoints = (await getOnyxValue(ONYXKEYS.NVP_RECENT_WAYPOINTS)) ?? []; + await getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}`, + waitForCollectionCallback: true, + callback: (value) => { + baseParams.allPolicyTags = value; + }, + }); }); afterEach(async () => { From ccecdf5be38eda0c97658c284184ef16bd53fcb8 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 3 Mar 2026 08:49:30 +0100 Subject: [PATCH 02/12] tests fix, merge main --- tests/actions/IOU/MoneyRequestTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/IOU/MoneyRequestTest.ts b/tests/actions/IOU/MoneyRequestTest.ts index 229f14dd74b1..91d97ad6315f 100644 --- a/tests/actions/IOU/MoneyRequestTest.ts +++ b/tests/actions/IOU/MoneyRequestTest.ts @@ -1,6 +1,5 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import {getOnyxData} from 'tests/utils/TestHelper'; import type {MoneyRequestStepScanParticipantsFlowParams} from '@libs/actions/IOU/MoneyRequest'; import {createTransaction, handleMoneyRequestStepDistanceNavigation, handleMoneyRequestStepScanParticipants} from '@libs/actions/IOU/MoneyRequest'; import getCurrentPosition from '@libs/getCurrentPosition'; @@ -20,6 +19,7 @@ import createRandomPolicy from '../../utils/collections/policies'; import {createRandomReport, createSelfDM} from '../../utils/collections/reports'; import createRandomTransaction from '../../utils/collections/transaction'; import getOnyxValue from '../../utils/getOnyxValue'; +import {getOnyxData} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; jest.mock('@libs/actions/IOU', () => { From 6bad412890481ae058debaf11e32784836e9d064 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 4 Mar 2026 15:57:22 +0100 Subject: [PATCH 03/12] calculate participants and participantsPolicyTags before handleMoneyRequestStepScanParticipants --- src/libs/actions/IOU/MoneyRequest.ts | 22 +++------- .../step/IOURequestStepScan/useReceiptScan.ts | 23 +++++++++-- tests/actions/IOU/MoneyRequestTest.ts | 40 ++++++++++++++----- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/libs/actions/IOU/MoneyRequest.ts b/src/libs/actions/IOU/MoneyRequest.ts index 3e832837a7ae..22b071f6ef1a 100644 --- a/src/libs/actions/IOU/MoneyRequest.ts +++ b/src/libs/actions/IOU/MoneyRequest.ts @@ -16,7 +16,6 @@ import {setTransactionReport} from '@userActions/Transaction'; import type {IOUType} from '@src/CONST'; import CONST from '@src/CONST'; import type {TranslationParameters, TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type { @@ -94,7 +93,6 @@ type MoneyRequestStepScanParticipantsFlowParams = { policy: OnyxEntry; report: OnyxEntry; reportID: string; - reportAttributesDerived?: Record; transactions: Transaction[]; initialTransaction: InitialTransactionParams; policyForMovingExpenses?: OnyxEntry; @@ -113,7 +111,6 @@ type MoneyRequestStepScanParticipantsFlowParams = { policyRecentlyUsedCurrencies?: string[]; introSelected?: IntroSelected; activePolicyID?: string; - privateIsArchived?: string; files: ReceiptFile[]; isTestTransaction?: boolean; locationPermissionGranted?: boolean; @@ -123,7 +120,8 @@ type MoneyRequestStepScanParticipantsFlowParams = { allTransactionDrafts: OnyxCollection; betas: OnyxEntry; recentWaypoints: OnyxEntry; - allPolicyTags: OnyxCollection; + participants: Participant[]; + participantsPolicyTags: Record; }; type MoneyRequestStepDistanceNavigationParams = { @@ -294,7 +292,6 @@ function handleMoneyRequestStepScanParticipants({ policy, report, reportID, - reportAttributesDerived, transactions, initialTransaction, policyForMovingExpenses, @@ -314,7 +311,6 @@ function handleMoneyRequestStepScanParticipants({ policyRecentlyUsedCurrencies, introSelected, activePolicyID, - privateIsArchived, files, isTestTransaction = false, locationPermissionGranted = false, @@ -323,7 +319,8 @@ function handleMoneyRequestStepScanParticipants({ allTransactionDrafts, betas, recentWaypoints, - allPolicyTags, + participants, + participantsPolicyTags, }: MoneyRequestStepScanParticipantsFlowParams) { if (backTo) { Navigation.goBack(backTo); @@ -357,8 +354,6 @@ function handleMoneyRequestStepScanParticipants({ // to the confirmation step. // If the user is started this flow using the Create expense option (combined submit/track flow), they should be redirected to the participants page. if (!initialTransaction?.isFromGlobalCreate && !isArchivedExpenseReport && iouType !== CONST.IOU.TYPE.CREATE) { - const participants = getMoneyRequestParticipantOptions(currentUserAccountID, report, policy, personalDetails, privateIsArchived, reportAttributesDerived); - if (shouldSkipConfirmation) { cancelSpan(CONST.TELEMETRY.SPAN_SCAN_PROCESS_AND_NAVIGATE); cancelSpan(CONST.TELEMETRY.SPAN_CONFIRMATION_MOUNT); @@ -370,12 +365,7 @@ function handleMoneyRequestStepScanParticipants({ const splitReceipt: Receipt = firstReceiptFile.file ?? {}; splitReceipt.source = firstReceiptFile.source; splitReceipt.state = CONST.IOU.RECEIPT_STATE.SCAN_READY; - const participantsPolicyTags = participants.reduce>((acc, participant) => { - if (participant.policyID) { - acc[participant.policyID] = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`] ?? {}; - } - return acc; - }, {}); + startSplitBill({ participants, currentUserLogin: currentUserLogin ?? '', @@ -754,5 +744,5 @@ function handleMoneyRequestStepDistanceNavigation({ } } -export {createTransaction, handleMoneyRequestStepScanParticipants, handleMoneyRequestStepDistanceNavigation}; +export {createTransaction, handleMoneyRequestStepScanParticipants, handleMoneyRequestStepDistanceNavigation, getMoneyRequestParticipantOptions}; export type {MoneyRequestStepScanParticipantsFlowParams, MoneyRequestStepDistanceNavigationParams}; diff --git a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts index 1aa15e4f16cf..acb0c8495042 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts @@ -14,7 +14,7 @@ import usePolicy from '@hooks/usePolicy'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportAttributes from '@hooks/useReportAttributes'; import useSelfDMReport from '@hooks/useSelfDMReport'; -import {handleMoneyRequestStepScanParticipants} from '@libs/actions/IOU/MoneyRequest'; +import {getMoneyRequestParticipantOptions, handleMoneyRequestStepScanParticipants} from '@libs/actions/IOU/MoneyRequest'; import setTestReceipt from '@libs/actions/setTestReceipt'; import {dismissProductTraining} from '@libs/actions/Welcome'; import DateUtils from '@libs/DateUtils'; @@ -27,6 +27,7 @@ import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactions, remov import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {validTransactionDraftsSelector} from '@src/selectors/TransactionDraft'; +import type {PolicyTagLists} from '@src/types/onyx'; import type Transaction from '@src/types/onyx/Transaction'; import type {FileObject} from '@src/types/utils/Attachment'; import type {ReceiptFile, UseReceiptScanParams} from './types'; @@ -137,12 +138,26 @@ function useReceiptScan({ parentSpan: getSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION), attributes: {[CONST.TELEMETRY.ATTRIBUTE_IS_MULTI_SCAN]: isMultiScanEnabled}, }); + const participants = getMoneyRequestParticipantOptions( + currentUserPersonalDetails.accountID, + report, + policy, + personalDetails, + reportNameValuePairs?.private_isArchived, + reportAttributesDerived, + ); + const participantsPolicyTags = participants.reduce>((acc, participant) => { + if (participant.policyID) { + acc[participant.policyID] = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`] ?? {}; + } + return acc; + }, {}); + handleMoneyRequestStepScanParticipants({ iouType, policy, report, reportID, - reportAttributesDerived, transactions, initialTransaction: { transactionID: initialTransactionID, @@ -169,7 +184,6 @@ function useReceiptScan({ policyRecentlyUsedCurrencies, introSelected, activePolicyID, - privateIsArchived: reportNameValuePairs?.private_isArchived, files, isTestTransaction, locationPermissionGranted, @@ -179,7 +193,8 @@ function useReceiptScan({ betas, recentWaypoints, allTransactionDrafts, - allPolicyTags, + participants, + participantsPolicyTags, }); } diff --git a/tests/actions/IOU/MoneyRequestTest.ts b/tests/actions/IOU/MoneyRequestTest.ts index 91d97ad6315f..d9479b5e3a8a 100644 --- a/tests/actions/IOU/MoneyRequestTest.ts +++ b/tests/actions/IOU/MoneyRequestTest.ts @@ -1,7 +1,7 @@ -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {MoneyRequestStepScanParticipantsFlowParams} from '@libs/actions/IOU/MoneyRequest'; -import {createTransaction, handleMoneyRequestStepDistanceNavigation, handleMoneyRequestStepScanParticipants} from '@libs/actions/IOU/MoneyRequest'; +import {createTransaction, getMoneyRequestParticipantOptions, handleMoneyRequestStepDistanceNavigation, handleMoneyRequestStepScanParticipants} from '@libs/actions/IOU/MoneyRequest'; import getCurrentPosition from '@libs/getCurrentPosition'; import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types'; import Navigation from '@libs/Navigation/Navigation'; @@ -10,6 +10,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, PolicyTagLists, QuickAction, RecentWaypoint} from '@src/types/onyx'; +import type {Participant} from '@src/types/onyx/IOU'; import type {SplitShares} from '@src/types/onyx/Transaction'; import * as IOU from '../../../src/libs/actions/IOU'; import * as Split from '../../../src/libs/actions/IOU/Split'; @@ -384,7 +385,6 @@ describe('MoneyRequest', () => { policy: fakePolicy, report: fakeReport, reportID: '1', - reportAttributesDerived: {}, transactions: [fakeTransaction], initialTransaction: { transactionID: '1', @@ -414,7 +414,8 @@ describe('MoneyRequest', () => { betas: [], recentWaypoints: [] as RecentWaypoint[], allTransactionDrafts: {}, - allPolicyTags: {} as OnyxCollection, + participants: [] as Participant[], + participantsPolicyTags: {} as Record, }; beforeEach(async () => { @@ -428,11 +429,17 @@ describe('MoneyRequest', () => { }, }); baseParams.recentWaypoints = (await getOnyxValue(ONYXKEYS.NVP_RECENT_WAYPOINTS)) ?? []; + baseParams.participants = getMoneyRequestParticipantOptions(baseParams.currentUserAccountID, baseParams.report, baseParams.policy, baseParams.personalDetails, undefined, {}); await getOnyxData({ key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}`, waitForCollectionCallback: true, callback: (value) => { - baseParams.allPolicyTags = value; + baseParams.participantsPolicyTags = baseParams.participants.reduce>((acc, participant) => { + if (participant.policyID) { + acc[participant.policyID] = value?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`] ?? {}; + } + return acc; + }, {}); }, }); }); @@ -526,13 +533,28 @@ describe('MoneyRequest', () => { }); it('should return if no participants found for non-SPLIT iouType when not from global create menu and skipping confirmation', async () => { + const report = { + ...fakeReport, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, + }; + baseParams.participants = getMoneyRequestParticipantOptions(baseParams.currentUserAccountID, report, baseParams.policy, baseParams.personalDetails, undefined, {}); + + await getOnyxData({ + key: `${ONYXKEYS.COLLECTION.POLICY_TAGS}`, + waitForCollectionCallback: true, + callback: (value) => { + baseParams.participantsPolicyTags = baseParams.participants.reduce>((acc, participant) => { + if (participant.policyID) { + acc[participant.policyID] = value?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`] ?? {}; + } + return acc; + }, {}); + }, + }); handleMoneyRequestStepScanParticipants({ ...baseParams, iouType: CONST.IOU.TYPE.TRACK, - report: { - ...fakeReport, - chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM, - }, + report, shouldSkipConfirmation: true, initialTransaction: { ...baseParams.initialTransaction, From d6dcf0a9861a9b692513490efad5238da4e06238 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 4 Mar 2026 17:48:49 +0100 Subject: [PATCH 04/12] add mockGetMoneyRequestParticipantOptions mocks --- tests/unit/hooks/useReceiptScan.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/unit/hooks/useReceiptScan.test.ts b/tests/unit/hooks/useReceiptScan.test.ts index f44a7c28ee6b..9391d378a601 100644 --- a/tests/unit/hooks/useReceiptScan.test.ts +++ b/tests/unit/hooks/useReceiptScan.test.ts @@ -9,6 +9,7 @@ import type {Report, Transaction} from '@src/types/onyx'; import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithAct'; const mockHandleMoneyRequestStepScanParticipants = jest.fn(); +const mockGetMoneyRequestParticipantOptions = jest.fn().mockReturnValue([]); const mockDismissProductTraining = jest.fn(); const mockRemoveDraftTransactions = jest.fn(); const mockRemoveTransactionReceipt = jest.fn(); @@ -39,6 +40,7 @@ jest.mock('@libs/TransactionUtils', () => ({ jest.mock('@libs/actions/IOU/MoneyRequest', () => ({ handleMoneyRequestStepScanParticipants: (...args: unknown[]) => mockHandleMoneyRequestStepScanParticipants(...args), + getMoneyRequestParticipantOptions: (...args: unknown[]) => mockGetMoneyRequestParticipantOptions(...args), })); jest.mock('@libs/actions/Welcome', () => ({ From e3f542063fd69a7caeaa28e07779bbbde235e80d Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 7 Mar 2026 17:49:27 +0100 Subject: [PATCH 05/12] add participantsPolicyTags selector --- .../step/IOURequestStepConfirmation.tsx | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 749ca5052be9..83d3fc00b48e 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,6 +1,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; @@ -110,7 +111,7 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {InvoiceReceiver} from '@src/types/onyx/Report'; import type {Receipt} from '@src/types/onyx/Transaction'; import type {FileObject} from '@src/types/utils/Attachment'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -207,7 +208,34 @@ function IOURequestStepConfirmation({ const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${draftPolicyID}`); const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`); - const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); + const participantPolicyIDs = useMemo(() => { + const ids = new Set(); + for (const p of transaction?.participants ?? []) { + if (p.policyID) { + ids.add(p.policyID); + } + } + return Array.from(ids).sort().join(','); + }, [transaction?.participants]); + + const policyTagsSelector = useCallback( + (allTags: OnyxCollection) => { + if (!participantPolicyIDs) { + return {}; + } + const policyIDs = participantPolicyIDs.split(','); + return policyIDs.reduce>((acc, participantPolicyID) => { + const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participantPolicyID}`; + if (allTags?.[key]) { + acc[participantPolicyID] = allTags[key]; + } + return acc; + }, {}); + }, + [participantPolicyIDs], + ); + + const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const [policyRecentlyUsedTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`); const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES); @@ -1085,13 +1113,6 @@ function IOURequestStepConfirmation({ } const itemTrimmedComment = item?.comment?.comment?.trim() ?? ''; - const participantsPolicyTags = selectedParticipants.reduce>((acc, participant) => { - if (participant.policyID) { - acc[participant.policyID] = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`] ?? {}; - } - return acc; - }, {}); - // If we have a receipt let's start the split expense by creating only the action, the transaction, and the group DM if needed startSplitBill({ participants: selectedParticipants, @@ -1336,7 +1357,7 @@ function IOURequestStepConfirmation({ reportID, requestType, betas, - allPolicyTags, + participantsPolicyTags, ], ); From 36030f739733a7fff55fc0eff98cf3b9c889bbd6 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 7 Mar 2026 20:48:32 +0100 Subject: [PATCH 06/12] extract logic to useParticipantsPolicyTags --- src/hooks/useParticipantsPolicyTags.ts | 52 +++++++++++++++++++ .../step/IOURequestStepConfirmation.tsx | 35 ++----------- .../step/IOURequestStepScan/useReceiptScan.ts | 25 +++------ 3 files changed, 64 insertions(+), 48 deletions(-) create mode 100644 src/hooks/useParticipantsPolicyTags.ts diff --git a/src/hooks/useParticipantsPolicyTags.ts b/src/hooks/useParticipantsPolicyTags.ts new file mode 100644 index 000000000000..eeb09c358cb6 --- /dev/null +++ b/src/hooks/useParticipantsPolicyTags.ts @@ -0,0 +1,52 @@ +import {useCallback, useMemo} from 'react'; +import type {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PolicyTagLists} from '@src/types/onyx'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; +import useOnyx from './useOnyx'; + +type ParticipantWithPolicyID = { + policyID?: string; +}; + +/** + * Hook that extracts policy tags only for participants' policies. + * + * @param participants - Array of participants with optional policyID + * @returns Record mapping policyID to PolicyTagLists + */ +function useParticipantsPolicyTags(participants: ParticipantWithPolicyID[]): Record { + + const participantPolicyIDs = useMemo(() => { + const ids = new Set(); + for (const p of participants) { + if (p.policyID) { + ids.add(p.policyID); + } + } + return Array.from(ids).sort().join(','); + }, [participants]); + + const policyTagsSelector = useCallback( + (allTags: OnyxCollection) => { + if (!participantPolicyIDs) { + return {}; + } + const policyIDs = participantPolicyIDs.split(','); + return policyIDs.reduce>((acc, participantPolicyID) => { + const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participantPolicyID}`; + if (allTags?.[key]) { + acc[participantPolicyID] = allTags[key]; + } + return acc; + }, {}); + }, + [participantPolicyIDs], + ); + + const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); + + return participantsPolicyTags; +} + +export default useParticipantsPolicyTags; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 83d3fc00b48e..fe9545507e08 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -1,7 +1,6 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxCollection} from 'react-native-onyx'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import DropZoneUI from '@components/DropZone/DropZoneUI'; @@ -25,6 +24,7 @@ import useOnyx from '@hooks/useOnyx'; import useOptimisticDraftTransactions from '@hooks/useOptimisticDraftTransactions'; import useParentReportAction from '@hooks/useParentReportAction'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import useParticipantsPolicyTags from '@hooks/useParticipantsPolicyTags'; import usePermissions from '@hooks/usePermissions'; import usePolicyForTransaction from '@hooks/usePolicyForTransaction'; import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap'; @@ -105,13 +105,13 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PolicyTagLists, RecentlyUsedCategories, Report} from '@src/types/onyx'; +import type {RecentlyUsedCategories, Report} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {InvoiceReceiver} from '@src/types/onyx/Report'; import type {Receipt} from '@src/types/onyx/Transaction'; import type {FileObject} from '@src/types/utils/Attachment'; -import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -208,34 +208,7 @@ function IOURequestStepConfirmation({ const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${draftPolicyID}`); const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`); - const participantPolicyIDs = useMemo(() => { - const ids = new Set(); - for (const p of transaction?.participants ?? []) { - if (p.policyID) { - ids.add(p.policyID); - } - } - return Array.from(ids).sort().join(','); - }, [transaction?.participants]); - - const policyTagsSelector = useCallback( - (allTags: OnyxCollection) => { - if (!participantPolicyIDs) { - return {}; - } - const policyIDs = participantPolicyIDs.split(','); - return policyIDs.reduce>((acc, participantPolicyID) => { - const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participantPolicyID}`; - if (allTags?.[key]) { - acc[participantPolicyID] = allTags[key]; - } - return acc; - }, {}); - }, - [participantPolicyIDs], - ); - - const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); + const participantsPolicyTags = useParticipantsPolicyTags(transaction?.participants ?? []); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const [policyRecentlyUsedTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`); const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES); diff --git a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts index bfdc5a714fc6..0f7084c27ada 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/useReceiptScan.ts @@ -1,5 +1,5 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; -import {useEffect, useState} from 'react'; +import {useEffect, useMemo, useState} from 'react'; import {InteractionManager} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; @@ -8,6 +8,7 @@ import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy'; import useFilesValidation from '@hooks/useFilesValidation'; import useOnyx from '@hooks/useOnyx'; import useOptimisticDraftTransactions from '@hooks/useOptimisticDraftTransactions'; +import useParticipantsPolicyTags from '@hooks/useParticipantsPolicyTags'; import usePermissions from '@hooks/usePermissions'; import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; @@ -27,7 +28,6 @@ import {buildOptimisticTransactionAndCreateDraft, removeDraftTransactions, remov import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {validTransactionDraftsSelector} from '@src/selectors/TransactionDraft'; -import type {PolicyTagLists} from '@src/types/onyx'; import type Transaction from '@src/types/onyx/Transaction'; import type {FileObject} from '@src/types/utils/Attachment'; import type {ReceiptFile, UseReceiptScanParams} from './types'; @@ -130,8 +130,13 @@ function useReceiptScan({ } const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS); - const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); + const participants = useMemo( + () => getMoneyRequestParticipantOptions(currentUserPersonalDetails.accountID, report, policy, personalDetails, reportNameValuePairs?.private_isArchived, reportAttributesDerived), + [currentUserPersonalDetails.accountID, report, policy, personalDetails, reportNameValuePairs?.private_isArchived, reportAttributesDerived], + ); + + const participantsPolicyTags = useParticipantsPolicyTags(participants); function navigateToConfirmationStep(files: ReceiptFile[], locationPermissionGranted = false, isTestTransaction = false) { startSpan(CONST.TELEMETRY.SPAN_SCAN_PROCESS_AND_NAVIGATE, { name: CONST.TELEMETRY.SPAN_SCAN_PROCESS_AND_NAVIGATE, @@ -139,20 +144,6 @@ function useReceiptScan({ parentSpan: getSpan(CONST.TELEMETRY.SPAN_SHUTTER_TO_CONFIRMATION), attributes: {[CONST.TELEMETRY.ATTRIBUTE_IS_MULTI_SCAN]: isMultiScanEnabled}, }); - const participants = getMoneyRequestParticipantOptions( - currentUserPersonalDetails.accountID, - report, - policy, - personalDetails, - reportNameValuePairs?.private_isArchived, - reportAttributesDerived, - ); - const participantsPolicyTags = participants.reduce>((acc, participant) => { - if (participant.policyID) { - acc[participant.policyID] = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`] ?? {}; - } - return acc; - }, {}); handleMoneyRequestStepScanParticipants({ iouType, From 3fde27f2d5506ea2ecde84f1b02de96b92c223a2 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sun, 8 Mar 2026 13:04:35 +0100 Subject: [PATCH 07/12] prettier fix --- src/hooks/useParticipantsPolicyTags.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/hooks/useParticipantsPolicyTags.ts b/src/hooks/useParticipantsPolicyTags.ts index eeb09c358cb6..b14ddd14d9da 100644 --- a/src/hooks/useParticipantsPolicyTags.ts +++ b/src/hooks/useParticipantsPolicyTags.ts @@ -16,7 +16,6 @@ type ParticipantWithPolicyID = { * @returns Record mapping policyID to PolicyTagLists */ function useParticipantsPolicyTags(participants: ParticipantWithPolicyID[]): Record { - const participantPolicyIDs = useMemo(() => { const ids = new Set(); for (const p of participants) { From de43a8423c972512fc4e0c8356ea00e0d8c42757 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Mon, 9 Mar 2026 23:30:48 +0100 Subject: [PATCH 08/12] selector uses participants array --- src/hooks/useParticipantsPolicyTags.ts | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/hooks/useParticipantsPolicyTags.ts b/src/hooks/useParticipantsPolicyTags.ts index b14ddd14d9da..33dc96b510c8 100644 --- a/src/hooks/useParticipantsPolicyTags.ts +++ b/src/hooks/useParticipantsPolicyTags.ts @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react'; +import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists} from '@src/types/onyx'; @@ -16,31 +16,20 @@ type ParticipantWithPolicyID = { * @returns Record mapping policyID to PolicyTagLists */ function useParticipantsPolicyTags(participants: ParticipantWithPolicyID[]): Record { - const participantPolicyIDs = useMemo(() => { - const ids = new Set(); - for (const p of participants) { - if (p.policyID) { - ids.add(p.policyID); - } - } - return Array.from(ids).sort().join(','); - }, [participants]); - const policyTagsSelector = useCallback( (allTags: OnyxCollection) => { - if (!participantPolicyIDs) { + if (!participants) { return {}; } - const policyIDs = participantPolicyIDs.split(','); - return policyIDs.reduce>((acc, participantPolicyID) => { - const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participantPolicyID}`; - if (allTags?.[key]) { - acc[participantPolicyID] = allTags[key]; + return participants.reduce>((acc, participant) => { + const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`; + if (allTags?.[key] && participant.policyID) { + acc[participant.policyID] = allTags[key]; } return acc; }, {}); }, - [participantPolicyIDs], + [participants], ); const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); From 20574541beb27076297bcacdf463d13bf23c7569 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 10 Mar 2026 15:30:18 +0100 Subject: [PATCH 09/12] add participants to useOnyx deps --- src/hooks/useParticipantsPolicyTags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useParticipantsPolicyTags.ts b/src/hooks/useParticipantsPolicyTags.ts index 33dc96b510c8..f9b00dd422a3 100644 --- a/src/hooks/useParticipantsPolicyTags.ts +++ b/src/hooks/useParticipantsPolicyTags.ts @@ -32,7 +32,7 @@ function useParticipantsPolicyTags(participants: ParticipantWithPolicyID[]): Rec [participants], ); - const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); + const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}, [participants]); return participantsPolicyTags; } From 6b7d203cd19dfc42f1ef48ade517760b24428736 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 10 Mar 2026 15:56:43 +0100 Subject: [PATCH 10/12] revert deps addition --- src/hooks/useParticipantsPolicyTags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useParticipantsPolicyTags.ts b/src/hooks/useParticipantsPolicyTags.ts index f9b00dd422a3..33dc96b510c8 100644 --- a/src/hooks/useParticipantsPolicyTags.ts +++ b/src/hooks/useParticipantsPolicyTags.ts @@ -32,7 +32,7 @@ function useParticipantsPolicyTags(participants: ParticipantWithPolicyID[]): Rec [participants], ); - const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}, [participants]); + const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); return participantsPolicyTags; } From 51b073bd35f9477bc0f6ff7522cec7db2c9c9c44 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 11 Mar 2026 13:32:23 +0100 Subject: [PATCH 11/12] use memoized participants --- src/hooks/useParticipantsPolicyTags.ts | 36 +++++++++---------- .../step/IOURequestStepConfirmation.tsx | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/hooks/useParticipantsPolicyTags.ts b/src/hooks/useParticipantsPolicyTags.ts index 33dc96b510c8..e7012e6c2384 100644 --- a/src/hooks/useParticipantsPolicyTags.ts +++ b/src/hooks/useParticipantsPolicyTags.ts @@ -1,4 +1,3 @@ -import {useCallback} from 'react'; import type {OnyxCollection} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists} from '@src/types/onyx'; @@ -9,6 +8,21 @@ type ParticipantWithPolicyID = { policyID?: string; }; +function getPolicyTagsSelector(participants: ParticipantWithPolicyID[]): (allTags: OnyxCollection) => Record { + return (allTags: OnyxCollection) => { + if (!participants) { + return {}; + } + return participants.reduce>((acc, participant) => { + const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`; + if (allTags?.[key] && participant.policyID) { + acc[participant.policyID] = allTags[key]; + } + return acc; + }, {}); + }; +} + /** * Hook that extracts policy tags only for participants' policies. * @@ -16,23 +30,9 @@ type ParticipantWithPolicyID = { * @returns Record mapping policyID to PolicyTagLists */ function useParticipantsPolicyTags(participants: ParticipantWithPolicyID[]): Record { - const policyTagsSelector = useCallback( - (allTags: OnyxCollection) => { - if (!participants) { - return {}; - } - return participants.reduce>((acc, participant) => { - const key = `${ONYXKEYS.COLLECTION.POLICY_TAGS}${participant.policyID}`; - if (allTags?.[key] && participant.policyID) { - acc[participant.policyID] = allTags[key]; - } - return acc; - }, {}); - }, - [participants], - ); - - const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: policyTagsSelector}); + const [participantsPolicyTags = getEmptyObject>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS, {selector: getPolicyTagsSelector(participants)}, [ + participants, + ]); return participantsPolicyTags; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 2e33548fe5e2..9ad634357a24 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -208,7 +208,6 @@ function IOURequestStepConfirmation({ const [policyCategoriesDraft] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT}${draftPolicyID}`); const [policyRecentlyUsedCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_CATEGORIES}${policyID}`); - const participantsPolicyTags = useParticipantsPolicyTags(transaction?.participants ?? []); const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); const [policyRecentlyUsedTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS}${policyID}`); const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES); @@ -318,6 +317,7 @@ function IOURequestStepConfirmation({ }) ?? [], [transaction?.participants, iouType, personalDetails, reportAttributesDerived, reportDrafts, privateIsArchivedMap, policy, currentUserPersonalDetails.accountID], ); + const participantsPolicyTags = useParticipantsPolicyTags(participants ?? []); const isPolicyExpenseChat = useMemo(() => participants?.some((participant) => participant.isPolicyExpenseChat), [participants]); const shouldGenerateTransactionThreadReport = !isBetaEnabled(CONST.BETAS.NO_OPTIMISTIC_TRANSACTION_THREADS); const formHasBeenSubmitted = useRef(false); From 0a2086ace9bf6a6cd7c7ab4862a58feb2133a850 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Thu, 12 Mar 2026 14:49:54 +0100 Subject: [PATCH 12/12] mobile exp --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index d04040c9f51f..56b3c41ffcc1 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit d04040c9f51f31226fb0d48bc1513dd7b15ea526 +Subproject commit 56b3c41ffcc17e76ee067daf27e9309d0eb2fcd9