From 17917f82aaca2e5a63e62fdabaded4d7a13d833e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 15 Oct 2025 12:02:55 +0200 Subject: [PATCH 01/26] refactor: move getPolicyTagsData to IOU and Transaction files --- src/libs/actions/IOU.ts | 52 ++++++++++++++++++++++++++++----- src/libs/actions/Policy/Tag.ts | 5 ---- src/libs/actions/Transaction.ts | 29 +++++++++++++++++- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c0e797db5ad4..4f1273d2bcfb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -255,7 +255,7 @@ import {clearByKey as clearPdfByOnyxKey} from './CachedPDFPaths'; import {buildOptimisticPolicyRecentlyUsedCategories} from './Policy/Category'; import {buildAddMembersToWorkspaceOnyxData, buildUpdateWorkspaceMembersRoleOnyxData} from './Policy/Member'; import {buildOptimisticRecentlyUsedCurrencies, buildPolicyData, generatePolicyID} from './Policy/Policy'; -import {buildOptimisticPolicyRecentlyUsedTags, getPolicyTagsData} from './Policy/Tag'; +import {buildOptimisticPolicyRecentlyUsedTags} from './Policy/Tag'; import type {GuidedSetupData} from './Report'; import {buildInviteToRoomOnyxData, completeOnboarding, getCurrentUserAccountID, notifyNewAction} from './Report'; import {clearAllRelatedReportActionErrors} from './ReportActions'; @@ -831,6 +831,23 @@ Onyx.connect({ }, }); +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/71491) +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + // TODO: remove `allRecentlyUsedTags` from this file (https://github.com/Expensify/App/issues/71491) // `allRecentlyUsedTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. @@ -910,6 +927,14 @@ Onyx.connectWithoutView({ callback: (value) => (recentAttendees = value), }); +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} + /** * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. * All usages of this function should be replaced with useOnyx hook in React components. @@ -3267,8 +3292,9 @@ function getSendInvoiceInformation( const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(optimisticInvoiceReport.policyID), - // TODO: Replace getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyRecentlyUsedTags: getPolicyRecentlyUsedTagsData(optimisticInvoiceReport.policyID), transactionTags: tag, @@ -3505,8 +3531,9 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(iouReport.policyID), - // TODO: Replace getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyRecentlyUsedTags: getPolicyRecentlyUsedTagsData(iouReport.policyID), transactionTags: tag, @@ -3769,8 +3796,9 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI optimisticTransaction.hasEReceipt = true; const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(iouReport.policyID), - // TODO: Replace getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyRecentlyUsedTags: getPolicyRecentlyUsedTagsData(iouReport.policyID), transactionTags: tag, @@ -4491,8 +4519,9 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U const hasModifiedTag = 'tag' in transactionChanges; if (hasModifiedTag) { const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(iouReport?.policyID), - // TODO: Replace getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyRecentlyUsedTags: getPolicyRecentlyUsedTagsData(iouReport?.policyID), transactionTags: transactionChanges.tag, @@ -6962,8 +6991,9 @@ function createSplitsAndOnyxData({ // Add tag to optimistic policy recently used tags when a participant is a workspace const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? buildOptimisticPolicyRecentlyUsedTags({ + // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(participant.policyID), - // TODO: Replace getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyRecentlyUsedTags: getPolicyRecentlyUsedTagsData(participant.policyID), transactionTags: tag, @@ -7506,8 +7536,9 @@ function startSplitBill({ // eslint-disable-next-line deprecation/deprecation const optimisticPolicyRecentlyUsedCategories = buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(participant.policyID), - // TODO: Replace getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyRecentlyUsedTags: getPolicyRecentlyUsedTagsData(participant.policyID), transactionTags: tag, @@ -11202,6 +11233,8 @@ function detachReceipt(transactionID: string | undefined) { if (policy && isPaidGroupPolicy(policy) && newTransaction) { const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`]; + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( @@ -11331,6 +11364,8 @@ function replaceReceipt({transactionID, file, source}: ReplaceReceipt) { if (policy && isPaidGroupPolicy(policy) && newTransaction) { const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`]; + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( @@ -13317,6 +13352,8 @@ function saveSplitTransactions( const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; const originalTransactionDetails = getTransactionDetails(originalTransaction); + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport); const splitExpenses = draftTransaction?.comment?.splitExpenses ?? []; @@ -13917,6 +13954,7 @@ export { getUpdateMoneyRequestParams, getUpdateTrackExpenseParams, getReportPreviewAction, + getPolicyTagsData, }; export type { GPSPoint as GpsPoint, diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 69171310f946..ae8e36c06c83 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1198,10 +1198,6 @@ function downloadMultiLevelIndependentTagsCSV(policyID: string, onDownloadFailed fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_MULTI_LEVEL_TAGS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); } -function getPolicyTagsData(policyID: string | undefined) { - return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - export { buildOptimisticPolicyRecentlyUsedTags, setPolicyRequiresTag, @@ -1221,7 +1217,6 @@ export { setPolicyTagApprover, importPolicyTags, downloadTagsCSV, - getPolicyTagsData, downloadMultiLevelIndependentTagsCSV, cleanPolicyTags, setImportedSpreadsheetIsImportingMultiLevelTags, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 5350d4c73d67..8ce63133fbef 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -34,6 +34,7 @@ import type { PersonalDetails, Policy, PolicyCategories, + PolicyTagLists, RecentWaypoint, Report, ReportAction, @@ -47,7 +48,6 @@ import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from '@src/type import type {OnyxData} from '@src/types/onyx/Request'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; import type TransactionState from '@src/types/utils/TransactionStateType'; -import {getPolicyTagsData} from './Policy/Tag'; let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ @@ -106,6 +106,31 @@ Onyx.connect({ callback: (val) => (allTransactionViolations = val ?? []), }); +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/71491) +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} + // Helper to safely check for a string 'name' property function isViolationWithName(violation: unknown): violation is {name: string} { return !!(violation && typeof violation === 'object' && typeof (violation as {name?: unknown}).name === 'string'); @@ -752,6 +777,8 @@ function changeTransactionsReport( let transactionsMoved = false; let shouldFixViolations = false; + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy?.id); const policyHasDependentTags = hasDependentTags(policy, policyTagList); From b08a9a55a381c0807b0951d0572a50f9b5906cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 15 Oct 2025 12:59:34 +0200 Subject: [PATCH 02/26] fix: move Onyx.connect temporarily to TransactionUtils --- src/libs/TransactionUtils/index.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 59950398f4a3..8dd1c82fb30f 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -7,7 +7,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {UnreportedExpenseListItemType} from '@components/SelectionListWithSections/types'; -import {getPolicyTagsData} from '@libs/actions/Policy/Tag'; import type {MergeDuplicatesParams} from '@libs/API/parameters'; import {getCategoryDefaultTaxRate} from '@libs/CategoryUtils'; import {convertToBackendAmount, getCurrencyDecimals} from '@libs/CurrencyUtils'; @@ -50,6 +49,7 @@ import type { OnyxInputOrEntry, Policy, PolicyCategories, + PolicyTagLists, RecentWaypoint, Report, ReviewDuplicates, @@ -152,6 +152,31 @@ Onyx.connect({ }, }); +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/66462) +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} + function hasDistanceCustomUnit(transaction: OnyxEntry): boolean { const type = transaction?.comment?.type; const customUnitName = transaction?.comment?.customUnit?.name; @@ -1796,6 +1821,8 @@ function compareDuplicateTransactionFields( keep[fieldName] = firstTransaction?.[keys[0]] ?? firstTransaction?.[keys[1]]; } } else if (fieldName === 'tag') { + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/66462) + // eslint-disable-next-line deprecation/deprecation const policyTags = report?.policyID ? getPolicyTagsData(report?.policyID) : {}; const isMultiLevelTags = isMultiLevelTagsPolicyUtils(policyTags); if (isMultiLevelTags) { From d293bdf35fefd8211576e5372bce70e604bcf40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 15 Oct 2025 13:00:37 +0200 Subject: [PATCH 03/26] fix: update link to the issues in TODO comments in Transaction.ts --- src/libs/actions/Transaction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 8ce63133fbef..9755bf763013 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -106,7 +106,7 @@ Onyx.connect({ callback: (val) => (allTransactionViolations = val ?? []), }); -// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/71491) +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/67774) // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; @@ -777,7 +777,7 @@ function changeTransactionsReport( let transactionsMoved = false; let shouldFixViolations = false; - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/67774) // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy?.id); const policyHasDependentTags = hasDependentTags(policy, policyTagList); From b2fae7c65ffb59a3f2e8ce2bdf2e27d8e2b97ebf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 15 Oct 2025 13:24:04 +0200 Subject: [PATCH 04/26] fix: remove getPolicyTagsData export --- src/libs/actions/IOU.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4f1273d2bcfb..9d8c625abcac 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -13954,7 +13954,6 @@ export { getUpdateMoneyRequestParams, getUpdateTrackExpenseParams, getReportPreviewAction, - getPolicyTagsData, }; export type { GPSPoint as GpsPoint, From 5978bbfd7d8797e7d538618596fe568cc0f31f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 16 Oct 2025 10:29:32 +0200 Subject: [PATCH 05/26] refactor: update issue urls --- src/libs/TransactionUtils/index.ts | 4 ++-- src/libs/actions/IOU.ts | 20 ++++++++++---------- src/libs/actions/Transaction.ts | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 8dd1c82fb30f..8aab50f67813 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -152,7 +152,7 @@ Onyx.connect({ }, }); -// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/66462) +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72719) // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; @@ -1821,7 +1821,7 @@ function compareDuplicateTransactionFields( keep[fieldName] = firstTransaction?.[keys[0]] ?? firstTransaction?.[keys[1]]; } } else if (fieldName === 'tag') { - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/66462) + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/72719) // eslint-disable-next-line deprecation/deprecation const policyTags = report?.policyID ? getPolicyTagsData(report?.policyID) : {}; const isMultiLevelTags = isMultiLevelTagsPolicyUtils(policyTags); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9d8c625abcac..27bddef740ec 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -831,7 +831,7 @@ Onyx.connect({ }, }); -// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/71491) +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72721) // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; @@ -3292,7 +3292,7 @@ function getSendInvoiceInformation( const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(optimisticInvoiceReport.policyID), // eslint-disable-next-line deprecation/deprecation @@ -3531,7 +3531,7 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(iouReport.policyID), // eslint-disable-next-line deprecation/deprecation @@ -3796,7 +3796,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI optimisticTransaction.hasEReceipt = true; const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(iouReport.policyID), // eslint-disable-next-line deprecation/deprecation @@ -4519,7 +4519,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U const hasModifiedTag = 'tag' in transactionChanges; if (hasModifiedTag) { const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(iouReport?.policyID), // eslint-disable-next-line deprecation/deprecation @@ -6991,7 +6991,7 @@ function createSplitsAndOnyxData({ // Add tag to optimistic policy recently used tags when a participant is a workspace const optimisticPolicyRecentlyUsedTags = isPolicyExpenseChat ? buildOptimisticPolicyRecentlyUsedTags({ - // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(participant.policyID), // eslint-disable-next-line deprecation/deprecation @@ -7536,7 +7536,7 @@ function startSplitBill({ // eslint-disable-next-line deprecation/deprecation const optimisticPolicyRecentlyUsedCategories = buildOptimisticPolicyRecentlyUsedCategories(participant.policyID, category); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: Replace getPolicyTagsData and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation policyTags: getPolicyTagsData(participant.policyID), // eslint-disable-next-line deprecation/deprecation @@ -11233,7 +11233,7 @@ function detachReceipt(transactionID: string | undefined) { if (policy && isPaidGroupPolicy(policy) && newTransaction) { const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`]; - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; @@ -11364,7 +11364,7 @@ function replaceReceipt({transactionID, file, source}: ReplaceReceipt) { if (policy && isPaidGroupPolicy(policy) && newTransaction) { const policyCategories = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`]; - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; @@ -13352,7 +13352,7 @@ function saveSplitTransactions( const originalTransaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; const originalTransactionDetails = getTransactionDetails(originalTransaction); - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/71491) + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) with useOnyx hook (https://github.com/Expensify/App/issues/71491) // eslint-disable-next-line deprecation/deprecation const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 9755bf763013..28f51c0df4f4 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -106,7 +106,7 @@ Onyx.connect({ callback: (val) => (allTransactionViolations = val ?? []), }); -// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/67774) +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72720) // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; @@ -777,7 +777,7 @@ function changeTransactionsReport( let transactionsMoved = false; let shouldFixViolations = false; - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/67774) + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/72720) // eslint-disable-next-line deprecation/deprecation const policyTagList = getPolicyTagsData(policy?.id); const policyHasDependentTags = hasDependentTags(policy, policyTagList); From 0efdb55384885bd2207d97e7a09386fc5effc81f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 23 Oct 2025 10:38:42 +0200 Subject: [PATCH 06/26] fix: fix eslint errors --- src/libs/TransactionUtils/index.ts | 2 +- src/libs/actions/Transaction.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 4c09cfb757b9..1b990f76ae1c 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1845,7 +1845,7 @@ function compareDuplicateTransactionFields( } } else if (fieldName === 'tag') { // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/72719) - // eslint-disable-next-line deprecation/deprecation + // eslint-disable-next-line @typescript-eslint/no-deprecated const policyTags = report?.policyID ? getPolicyTagsData(report?.policyID) : {}; const isMultiLevelTags = isMultiLevelTagsPolicyUtils(policyTags); if (isMultiLevelTags) { diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 3b1260b83538..84c551e6e625 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -778,8 +778,8 @@ function changeTransactionsReport( let transactionsMoved = false; let shouldFixViolations = false; - // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/72720) - // eslint-disable-next-line deprecation/deprecation + // TODO: Replace getPolicyTagsData with useOnyx hook (https://github.com/Expensify/App/issues/72720) + // eslint-disable-next-line @typescript-eslint/no-deprecated const policyTagList = getPolicyTagsData(policy?.id); const policyHasDependentTags = hasDependentTags(policy, policyTagList); From bdf606c9199482651d174474c9c158ce3e96ddbc Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Tue, 18 Nov 2025 15:14:46 +0100 Subject: [PATCH 07/26] feat: removes Onyx.connect for ONYXKEYS.COLLECTION.POLICY_TAGS in src/libs/actions/Policy/Tag.ts --- src/libs/actions/Policy/Tag.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 61b31dcfe1c8..3effd4cb7c5a 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -35,20 +35,6 @@ import type {OnyxValueWithOfflineFeedback} from '@src/types/onyx/OnyxCommon'; import type {ApprovalRule} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; -let allPolicyTags: OnyxCollection = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - function openPolicyTagsPage(policyID: string) { if (!policyID) { Log.warn('openPolicyTagsPage invalid params', {policyID}); From f2b062dd69dcc3359973490b1a2812975472967e Mon Sep 17 00:00:00 2001 From: Dariusz Biela Date: Tue, 18 Nov 2025 15:30:48 +0100 Subject: [PATCH 08/26] fix: removes unused import --- src/libs/actions/Policy/Tag.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 3effd4cb7c5a..2a4250d1b473 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -1,5 +1,5 @@ import lodashCloneDeep from 'lodash/cloneDeep'; -import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type PolicyData from '@hooks/usePolicyData/types'; import * as API from '@libs/API'; From a95a1b0422082e3f877b3376d19842db1f08d8a0 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 17 Jan 2026 00:48:12 +0100 Subject: [PATCH 09/26] dont push mobile exp --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 3ca4d93727b6..c77d4a04585b 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 3ca4d93727b6e4a0982c5fdc6bae7c9f0bc84ec6 +Subproject commit c77d4a04585bb4c3986b1731a0d0580b55f09c45 From 7a690abc7133fcefa43cf05a8ed5e02a8c2a113e Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 17 Jan 2026 01:25:42 +0100 Subject: [PATCH 10/26] fix merge --- src/libs/actions/IOU/index.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 3a361510b732..47709309b323 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -229,7 +229,7 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import {clearByKey as clearPdfByOnyxKey} from '@userActions/CachedPDFPaths'; import {buildAddMembersToWorkspaceOnyxData, buildUpdateWorkspaceMembersRoleOnyxData} from '@userActions/Policy/Member'; import {buildPolicyData, generatePolicyID} from '@userActions/Policy/Policy'; -import {buildOptimisticPolicyRecentlyUsedTags, getPolicyTagsData} from '@userActions/Policy/Tag'; +import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import type {GuidedSetupData} from '@userActions/Report'; import {buildInviteToRoomOnyxData, completeOnboarding, getCurrentUserAccountID, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions'; @@ -874,6 +874,20 @@ Onyx.connect({ }, }); +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + let allReports: OnyxCollection; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -943,6 +957,13 @@ Onyx.connect({ key: ONYXKEYS.NVP_RECENT_WAYPOINTS, callback: (val) => (recentWaypoints = val ?? []), }); +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} function getAllPersonalDetails(): OnyxTypes.PersonalDetailsList { return allPersonalDetails; @@ -3351,7 +3372,12 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma function computePerDiemExpenseAmount(customUnit: TransactionCustomUnit) { const subRates = customUnit.subRates ?? []; return subRates.reduce((total, subRate) => total + subRate.quantity * subRate.rate, 0); - return ''; +} + +function computePerDiemExpenseMerchant(customUnit: TransactionCustomUnit, policy: OnyxEntry) { + if (!customUnit.customUnitRateID) { + return ''; + } const policyCustomUnit = getPerDiemCustomUnit(policy); const rate = policyCustomUnit?.rates?.[customUnit.customUnitRateID]; const locationName = rate?.name ?? ''; From 7b439fe3666b4e07b070e123651626305a011568 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 17 Jan 2026 01:39:15 +0100 Subject: [PATCH 11/26] adding allPolicyTags Onyx.connect to necessary files --- src/libs/actions/IOU/Duplicate.ts | 28 ++++++++++++++++++++++++++-- src/libs/actions/IOU/SendInvoice.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 0d52f42439a1..8e9229fee459 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -1,5 +1,5 @@ import {format} from 'date-fns'; -import type {NullishDeep, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {PartialDeep} from 'type-fest'; import * as API from '@libs/API'; @@ -18,7 +18,6 @@ import { buildTransactionThread, getTransactionDetails, } from '@libs/ReportUtils'; -import {getPolicyTagsData} from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -37,6 +36,31 @@ import { trackExpense, } from '.'; +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72719) +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} + function getIOUActionForTransactions(transactionIDList: Array, iouReportID: string | undefined): Array> { const allReportActions = getAllReportActionsFromIOU(); return Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`] ?? {})?.filter( diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index bea275ab32b4..82a9bc8153f3 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -1,5 +1,5 @@ import {InteractionManager} from 'react-native'; -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {SendInvoiceParams} from '@libs/API/parameters'; @@ -23,7 +23,7 @@ import { } from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import {buildOptimisticTransaction} from '@libs/TransactionUtils'; -import {buildOptimisticPolicyRecentlyUsedTags, getPolicyTagsData} from '@userActions/Policy/Tag'; +import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import {notifyNewAction} from '@userActions/Report'; import {removeDraftTransaction} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; @@ -108,6 +108,31 @@ type BuildOnyxDataForInvoiceParams = { participant?: Participant; }; +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72719) +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} + /** Builds the Onyx data for an invoice */ function buildOnyxDataForInvoice( invoiceParams: BuildOnyxDataForInvoiceParams, From 7ef57dbcbd7c4a62c10434cf1ebca6217f6db05a Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 17 Jan 2026 02:06:48 +0100 Subject: [PATCH 12/26] fixing comments about deprecation --- src/libs/actions/IOU/Duplicate.ts | 2 +- src/libs/actions/IOU/SendInvoice.ts | 2 +- src/libs/actions/IOU/index.ts | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 8e9229fee459..d54117ac2be4 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -36,7 +36,7 @@ import { trackExpense, } from '.'; -// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72719) +// TODO: remove `allPolicyTags` from this file // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 82a9bc8153f3..09c4df2159b1 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -108,7 +108,7 @@ type BuildOnyxDataForInvoiceParams = { participant?: Participant; }; -// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72719) +// TODO: remove `allPolicyTags` from this file // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 47709309b323..a6757ea11e71 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -11721,6 +11721,8 @@ function detachReceipt(transactionID: string | undefined, transactionPolicy: Ony ]; if (transactionPolicy && isPaidGroupPolicy(transactionPolicy) && newTransaction) { + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData (https://github.com/Expensify/App/issues/71491) with useOnyx hook + // eslint-disable-next-line @typescript-eslint/no-deprecated const policyTagList = getPolicyTagsData(transactionPolicy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( @@ -11846,6 +11848,8 @@ function replaceReceipt({transactionID, file, source, transactionPolicy, transac ]; if (transactionPolicy && isPaidGroupPolicy(transactionPolicy) && newTransaction) { + // TODO: Replace getPolicyTagsData (https://github.com/Expensify/App/issues/72721) and getPolicyRecentlyUsedTagsData (https://github.com/Expensify/App/issues/71491) with useOnyx hook + // eslint-disable-next-line @typescript-eslint/no-deprecated const policyTagList = getPolicyTagsData(transactionPolicy.id); const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; const violationsOnyxData = ViolationsUtils.getViolationsOnyxData( From 7289d0b0bdd263d9b9a02f443a847cce236afaca Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 20 Jan 2026 08:26:15 +0100 Subject: [PATCH 13/26] Apply suggestion from @dariusz-biela Co-authored-by: Dariusz Biela --- src/libs/actions/IOU/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index a6757ea11e71..fe624dcf0191 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -874,6 +874,9 @@ Onyx.connect({ }, }); +// TODO: remove `allPolicyTags` from this file (https://github.com/Expensify/App/issues/72721) +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY_TAGS, From 8d4bf20c4dfb4e0e203ce6b0757b53d02fac0d27 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 21 Jan 2026 00:49:54 +0100 Subject: [PATCH 14/26] update issues links --- src/libs/actions/IOU/Duplicate.ts | 2 +- src/libs/actions/IOU/SendInvoice.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 07303de05756..79211ef6791f 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -36,7 +36,7 @@ import { trackExpense, } from '.'; -// TODO: remove `allPolicyTags` from this file +// TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80049 // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 09c4df2159b1..2d0cb3f5f174 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -108,7 +108,7 @@ type BuildOnyxDataForInvoiceParams = { participant?: Participant; }; -// TODO: remove `allPolicyTags` from this file +// TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80048 // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; From 238dc4ff4107ddcc7527cb4c96668aeef41f7465 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Thu, 22 Jan 2026 12:05:30 +0100 Subject: [PATCH 15/26] add no-deprecated annotations --- src/libs/actions/IOU/Duplicate.ts | 2 ++ src/libs/actions/IOU/SendInvoice.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index 07f1d600da71..1c4b021e1aaf 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -579,6 +579,8 @@ function duplicateExpenseTransaction({ params.policyParams = { policy: targetPolicy, + // // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80049 + // eslint-disable-next-line @typescript-eslint/no-deprecated policyTagList: getPolicyTagsData(targetPolicy.id) ?? {}, policyCategories: targetPolicyCategories ?? {}, }; diff --git a/src/libs/actions/IOU/SendInvoice.ts b/src/libs/actions/IOU/SendInvoice.ts index 2d0cb3f5f174..860cfcc319c6 100644 --- a/src/libs/actions/IOU/SendInvoice.ts +++ b/src/libs/actions/IOU/SendInvoice.ts @@ -660,6 +660,8 @@ function getSendInvoiceInformation({ const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80048 + // eslint-disable-next-line @typescript-eslint/no-deprecated policyTags: getPolicyTagsData(optimisticInvoiceReport.policyID), policyRecentlyUsedTags, transactionTags: tag, From bfc5b632547454f33808457cf0069870625a3be5 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Thu, 22 Jan 2026 12:17:29 +0100 Subject: [PATCH 16/26] update max-warnings --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 59dc6a51835a..2ec7c41ca61a 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=383 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=386 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", From 73d245bc263432a83815f8ca04e4b7e40b82f9a9 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Fri, 23 Jan 2026 00:29:21 +0100 Subject: [PATCH 17/26] Mobile --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 8a56ffa0d93b..ca95b80075b8 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 8a56ffa0d93b5c159697aaeabe5e0bda9670d6f0 +Subproject commit ca95b80075b8791bc458b47cd1b12a69936e8670 From 6e70379d947a6e68c96c100e1002bcfc27b7840a Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Fri, 23 Jan 2026 00:46:47 +0100 Subject: [PATCH 18/26] migrating also to Split.ts --- src/libs/actions/IOU/Split.ts | 120 ++++++++++++++++------------------ 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index e95036ee241a..ad5260b6b269 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -1,77 +1,43 @@ -import {InteractionManager} from 'react-native'; -import type {OnyxCollection, OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx'; +import { InteractionManager } from 'react-native'; +import type { OnyxCollection, OnyxEntry, OnyxKey, OnyxUpdate } from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; -import type {SearchContextProps} from '@components/Search/types'; +import type { ValueOf } from 'type-fest'; +import type { SearchContextProps } from '@components/Search/types'; import * as API from '@libs/API'; -import type {CompleteSplitBillParams, RevertSplitTransactionParams, SplitBillParams, SplitTransactionParams, SplitTransactionSplitsParam, StartSplitBillParams} from '@libs/API/parameters'; -import {WRITE_COMMANDS} from '@libs/API/types'; +import type { CompleteSplitBillParams, RevertSplitTransactionParams, SplitBillParams, SplitTransactionParams, SplitTransactionSplitsParam, StartSplitBillParams } from '@libs/API/parameters'; +import { WRITE_COMMANDS } from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; -import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; -import {calculateAmount as calculateIOUAmount, updateIOUOwnerAndTotal} from '@libs/IOUUtils'; -import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; +import { getMicroSecondOnyxErrorWithTranslationKey } from '@libs/ErrorUtils'; +import { calculateAmount as calculateIOUAmount, updateIOUOwnerAndTotal } from '@libs/IOUUtils'; +import { formatPhoneNumber } from '@libs/LocalePhoneNumber'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; -import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; +import Navigation, { navigationRef } from '@libs/Navigation/Navigation'; import * as NumberUtils from '@libs/NumberUtils'; import Parser from '@libs/Parser'; -import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; -import {getDistanceRateCustomUnitRate} from '@libs/PolicyUtils'; -import {getAllReportActions, getOriginalMessage, getReportAction, getReportActionHtml, getReportActionText, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import { - buildOptimisticChatReport, - buildOptimisticCreatedReportAction, - buildOptimisticExpenseReport, - buildOptimisticIOUReport, - buildOptimisticIOUReportAction, - buildOptimisticMoneyRequestEntities, - buildOptimisticReportPreview, - generateReportID, - getChatByParticipants, - getParsedComment, - getReportOrDraftReport, - getTransactionDetails, - hasViolations as hasViolationsReportUtils, - isArchivedReport, - isPolicyExpenseChat as isPolicyExpenseChatReportUtil, - shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, - updateReportPreview, -} from '@libs/ReportUtils'; -import playSound, {SOUNDS} from '@libs/Sound'; -import {buildOptimisticTransaction, getChildTransactions, isOnHold, isPerDiemRequest as isPerDiemRequestTransactionUtils} from '@libs/TransactionUtils'; -import {buildOptimisticPolicyRecentlyUsedTags, getPolicyTagsData} from '@userActions/Policy/Tag'; -import {notifyNewAction} from '@userActions/Report'; -import {removeDraftSplitTransaction, removeDraftTransaction} from '@userActions/TransactionEdit'; +import { addSMSDomainIfPhoneNumber } from '@libs/PhoneNumber'; +import { getDistanceRateCustomUnitRate } from '@libs/PolicyUtils'; +import { getAllReportActions, getOriginalMessage, getReportAction, getReportActionHtml, getReportActionText, isMoneyRequestAction } from '@libs/ReportActionsUtils'; +import { buildOptimisticChatReport, buildOptimisticCreatedReportAction, buildOptimisticExpenseReport, buildOptimisticIOUReport, buildOptimisticIOUReportAction, buildOptimisticMoneyRequestEntities, buildOptimisticReportPreview, generateReportID, getChatByParticipants, getParsedComment, getReportOrDraftReport, getTransactionDetails, hasViolations as hasViolationsReportUtils, isArchivedReport, isPolicyExpenseChat as isPolicyExpenseChatReportUtil, shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, updateReportPreview } from '@libs/ReportUtils'; +import playSound, { SOUNDS } from '@libs/Sound'; +import { buildOptimisticTransaction, getChildTransactions, isOnHold, isPerDiemRequest as isPerDiemRequestTransactionUtils } from '@libs/TransactionUtils'; +import { buildOptimisticPolicyRecentlyUsedTags } from '@userActions/Policy/Tag'; +import { notifyNewAction } from '@userActions/Report'; +import { removeDraftSplitTransaction, removeDraftTransaction } from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import type {Attendee, Participant, Split, SplitExpense} from '@src/types/onyx/IOU'; -import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; +import type { Attendee, Participant, Split, SplitExpense } from '@src/types/onyx/IOU'; +import type { CurrentUserPersonalDetails } from '@src/types/onyx/PersonalDetails'; import type RecentlyUsedTags from '@src/types/onyx/RecentlyUsedTags'; import type ReportAction from '@src/types/onyx/ReportAction'; -import type {OnyxData} from '@src/types/onyx/Request'; -import type {SplitShares, TransactionChanges} from '@src/types/onyx/Transaction'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import { - buildMinimalTransactionForFormula, - buildOnyxDataForMoneyRequest, - createSplitsAndOnyxData, - dismissModalAndOpenReportInInboxTab, - getAllPersonalDetails, - getAllReports, - getAllTransactions, - getDeleteTrackExpenseInformation, - getMoneyRequestInformation, - getMoneyRequestParticipantsFromReport, - getOrCreateOptimisticSplitChatReport, - getReceiptError, - getReportPreviewAction, - getUpdateMoneyRequestParams, - mergePolicyRecentlyUsedCategories, - mergePolicyRecentlyUsedCurrencies, -} from './index'; -import type {MoneyRequestInformationParams, OneOnOneIOUReport, StartSplitBilActionParams} from './index'; +import type { OnyxData } from '@src/types/onyx/Request'; +import type { SplitShares, TransactionChanges } from '@src/types/onyx/Transaction'; +import { isEmptyObject } from '@src/types/utils/EmptyObject'; +import { buildMinimalTransactionForFormula, buildOnyxDataForMoneyRequest, createSplitsAndOnyxData, dismissModalAndOpenReportInInboxTab, getAllPersonalDetails, getAllReports, getAllTransactions, getDeleteTrackExpenseInformation, getMoneyRequestInformation, getMoneyRequestParticipantsFromReport, getOrCreateOptimisticSplitChatReport, getReceiptError, getReportPreviewAction, getUpdateMoneyRequestParams, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies } from './index'; +import type { MoneyRequestInformationParams, OneOnOneIOUReport, StartSplitBilActionParams } from './index'; + type IOURequestType = ValueOf; @@ -304,6 +270,31 @@ function splitBillAndOpenReport({ notifyNewAction(splitData.chatReportID, currentUserAccountID); } +// TODO: remove `allPolicyTags` from this file [ISSUE YET TO BE CREATED] +// `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. +// All uses of this variable should be replaced with `useOnyx`. +let allPolicyTags: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + + allPolicyTags = value; + }, +}); + +/** + * @deprecated This function uses Onyx.connect and should be replaced with useOnyx for reactive data access. + * All usages of this function should be replaced with useOnyx hook in React components. + */ +function getPolicyTagsData(policyID: string | undefined) { + return allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; +} + /** Used exclusively for starting a split expense request that contains a receipt, the split request will be completed once the receipt is scanned * or user enters details manually. * @@ -592,6 +583,8 @@ function startSplitBill({ } const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ + // TODO: remove `allPolicyTags` from this file [ISSUE YET TO BE CREATED] + // eslint-disable-next-line @typescript-eslint/no-deprecated policyTags: getPolicyTagsData(participant.policyID), policyRecentlyUsedTags, transactionTags: tag, @@ -996,7 +989,8 @@ function updateSplitTransactions({ const originalTransactionID = transactionData?.originalTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID; const originalTransaction = allTransactionsList?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; const originalTransactionDetails = getTransactionDetails(originalTransaction); - + // TODO: remove `allPolicyTags` from this file [ISSUE YET TO BE CREATED] + // eslint-disable-next-line @typescript-eslint/no-deprecated const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport, currentUserPersonalDetails.accountID); const splitExpenses = transactionData?.splitExpenses ?? []; @@ -1552,4 +1546,4 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac export {completeSplitBill, splitBill, splitBillAndOpenReport, startSplitBill, updateSplitTransactions, updateSplitTransactionsFromSplitExpensesFlow}; -export type {SplitBillActionsParams, UpdateSplitTransactionsParams}; +export type {SplitBillActionsParams, UpdateSplitTransactionsParams}; \ No newline at end of file From 13d34d83c610d533bd8de9365f071986b1c2c343 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Fri, 23 Jan 2026 00:50:16 +0100 Subject: [PATCH 19/26] prettier --- src/libs/actions/IOU/Split.ts | 90 ++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 28 deletions(-) diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index ad5260b6b269..dbc429354ea9 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -1,43 +1,77 @@ -import { InteractionManager } from 'react-native'; -import type { OnyxCollection, OnyxEntry, OnyxKey, OnyxUpdate } from 'react-native-onyx'; +import {InteractionManager} from 'react-native'; +import type {OnyxCollection, OnyxEntry, OnyxKey, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type { ValueOf } from 'type-fest'; -import type { SearchContextProps } from '@components/Search/types'; +import type {ValueOf} from 'type-fest'; +import type {SearchContextProps} from '@components/Search/types'; import * as API from '@libs/API'; -import type { CompleteSplitBillParams, RevertSplitTransactionParams, SplitBillParams, SplitTransactionParams, SplitTransactionSplitsParam, StartSplitBillParams } from '@libs/API/parameters'; -import { WRITE_COMMANDS } from '@libs/API/types'; +import type {CompleteSplitBillParams, RevertSplitTransactionParams, SplitBillParams, SplitTransactionParams, SplitTransactionSplitsParam, StartSplitBillParams} from '@libs/API/parameters'; +import {WRITE_COMMANDS} from '@libs/API/types'; import DateUtils from '@libs/DateUtils'; -import { getMicroSecondOnyxErrorWithTranslationKey } from '@libs/ErrorUtils'; -import { calculateAmount as calculateIOUAmount, updateIOUOwnerAndTotal } from '@libs/IOUUtils'; -import { formatPhoneNumber } from '@libs/LocalePhoneNumber'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import {calculateAmount as calculateIOUAmount, updateIOUOwnerAndTotal} from '@libs/IOUUtils'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; -import Navigation, { navigationRef } from '@libs/Navigation/Navigation'; +import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; import * as NumberUtils from '@libs/NumberUtils'; import Parser from '@libs/Parser'; -import { addSMSDomainIfPhoneNumber } from '@libs/PhoneNumber'; -import { getDistanceRateCustomUnitRate } from '@libs/PolicyUtils'; -import { getAllReportActions, getOriginalMessage, getReportAction, getReportActionHtml, getReportActionText, isMoneyRequestAction } from '@libs/ReportActionsUtils'; -import { buildOptimisticChatReport, buildOptimisticCreatedReportAction, buildOptimisticExpenseReport, buildOptimisticIOUReport, buildOptimisticIOUReportAction, buildOptimisticMoneyRequestEntities, buildOptimisticReportPreview, generateReportID, getChatByParticipants, getParsedComment, getReportOrDraftReport, getTransactionDetails, hasViolations as hasViolationsReportUtils, isArchivedReport, isPolicyExpenseChat as isPolicyExpenseChatReportUtil, shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, updateReportPreview } from '@libs/ReportUtils'; -import playSound, { SOUNDS } from '@libs/Sound'; -import { buildOptimisticTransaction, getChildTransactions, isOnHold, isPerDiemRequest as isPerDiemRequestTransactionUtils } from '@libs/TransactionUtils'; -import { buildOptimisticPolicyRecentlyUsedTags } from '@userActions/Policy/Tag'; -import { notifyNewAction } from '@userActions/Report'; -import { removeDraftSplitTransaction, removeDraftTransaction } from '@userActions/TransactionEdit'; +import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber'; +import {getDistanceRateCustomUnitRate} from '@libs/PolicyUtils'; +import {getAllReportActions, getOriginalMessage, getReportAction, getReportActionHtml, getReportActionText, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import { + buildOptimisticChatReport, + buildOptimisticCreatedReportAction, + buildOptimisticExpenseReport, + buildOptimisticIOUReport, + buildOptimisticIOUReportAction, + buildOptimisticMoneyRequestEntities, + buildOptimisticReportPreview, + generateReportID, + getChatByParticipants, + getParsedComment, + getReportOrDraftReport, + getTransactionDetails, + hasViolations as hasViolationsReportUtils, + isArchivedReport, + isPolicyExpenseChat as isPolicyExpenseChatReportUtil, + shouldCreateNewMoneyRequestReport as shouldCreateNewMoneyRequestReportReportUtils, + updateReportPreview, +} from '@libs/ReportUtils'; +import playSound, {SOUNDS} from '@libs/Sound'; +import {buildOptimisticTransaction, getChildTransactions, isOnHold, isPerDiemRequest as isPerDiemRequestTransactionUtils} from '@libs/TransactionUtils'; +import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; +import {notifyNewAction} from '@userActions/Report'; +import {removeDraftSplitTransaction, removeDraftTransaction} from '@userActions/TransactionEdit'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; -import type { Attendee, Participant, Split, SplitExpense } from '@src/types/onyx/IOU'; -import type { CurrentUserPersonalDetails } from '@src/types/onyx/PersonalDetails'; +import type {Attendee, Participant, Split, SplitExpense} from '@src/types/onyx/IOU'; +import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; import type RecentlyUsedTags from '@src/types/onyx/RecentlyUsedTags'; import type ReportAction from '@src/types/onyx/ReportAction'; -import type { OnyxData } from '@src/types/onyx/Request'; -import type { SplitShares, TransactionChanges } from '@src/types/onyx/Transaction'; -import { isEmptyObject } from '@src/types/utils/EmptyObject'; -import { buildMinimalTransactionForFormula, buildOnyxDataForMoneyRequest, createSplitsAndOnyxData, dismissModalAndOpenReportInInboxTab, getAllPersonalDetails, getAllReports, getAllTransactions, getDeleteTrackExpenseInformation, getMoneyRequestInformation, getMoneyRequestParticipantsFromReport, getOrCreateOptimisticSplitChatReport, getReceiptError, getReportPreviewAction, getUpdateMoneyRequestParams, mergePolicyRecentlyUsedCategories, mergePolicyRecentlyUsedCurrencies } from './index'; -import type { MoneyRequestInformationParams, OneOnOneIOUReport, StartSplitBilActionParams } from './index'; - +import type {OnyxData} from '@src/types/onyx/Request'; +import type {SplitShares, TransactionChanges} from '@src/types/onyx/Transaction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import { + buildMinimalTransactionForFormula, + buildOnyxDataForMoneyRequest, + createSplitsAndOnyxData, + dismissModalAndOpenReportInInboxTab, + getAllPersonalDetails, + getAllReports, + getAllTransactions, + getDeleteTrackExpenseInformation, + getMoneyRequestInformation, + getMoneyRequestParticipantsFromReport, + getOrCreateOptimisticSplitChatReport, + getReceiptError, + getReportPreviewAction, + getUpdateMoneyRequestParams, + mergePolicyRecentlyUsedCategories, + mergePolicyRecentlyUsedCurrencies, +} from './index'; +import type {MoneyRequestInformationParams, OneOnOneIOUReport, StartSplitBilActionParams} from './index'; type IOURequestType = ValueOf; @@ -1546,4 +1580,4 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac export {completeSplitBill, splitBill, splitBillAndOpenReport, startSplitBill, updateSplitTransactions, updateSplitTransactionsFromSplitExpensesFlow}; -export type {SplitBillActionsParams, UpdateSplitTransactionsParams}; \ No newline at end of file +export type {SplitBillActionsParams, UpdateSplitTransactionsParams}; From 766ea1b60245753361a4bd7e2721d32543da2bc0 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 24 Jan 2026 12:53:29 +0100 Subject: [PATCH 20/26] Split issue update --- src/libs/actions/IOU/Duplicate.ts | 2 +- src/libs/actions/IOU/Split.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU/Duplicate.ts b/src/libs/actions/IOU/Duplicate.ts index f619c6681c6a..6d6dcb26dd8a 100644 --- a/src/libs/actions/IOU/Duplicate.ts +++ b/src/libs/actions/IOU/Duplicate.ts @@ -576,7 +576,7 @@ function duplicateExpenseTransaction({ params.policyParams = { policy: targetPolicy, - // // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80049 + // TODO: remove `allPolicyTags` from this file https://github.com/Expensify/App/issues/80049 // eslint-disable-next-line @typescript-eslint/no-deprecated policyTagList: getPolicyTagsData(targetPolicy.id) ?? {}, policyCategories: targetPolicyCategories ?? {}, diff --git a/src/libs/actions/IOU/Split.ts b/src/libs/actions/IOU/Split.ts index dbc429354ea9..f7572070316d 100644 --- a/src/libs/actions/IOU/Split.ts +++ b/src/libs/actions/IOU/Split.ts @@ -304,7 +304,7 @@ function splitBillAndOpenReport({ notifyNewAction(splitData.chatReportID, currentUserAccountID); } -// TODO: remove `allPolicyTags` from this file [ISSUE YET TO BE CREATED] +// TODO: remove `allPolicyTags` from this file [https://github.com/Expensify/App/issues/80401] // `allPolicyTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor. // All uses of this variable should be replaced with `useOnyx`. let allPolicyTags: OnyxCollection = {}; @@ -617,7 +617,7 @@ function startSplitBill({ } const optimisticPolicyRecentlyUsedCategories = mergePolicyRecentlyUsedCategories(category, policyRecentlyUsedCategories); const optimisticPolicyRecentlyUsedTags = buildOptimisticPolicyRecentlyUsedTags({ - // TODO: remove `allPolicyTags` from this file [ISSUE YET TO BE CREATED] + // TODO: remove `allPolicyTags` from this file [https://github.com/Expensify/App/issues/80401] // eslint-disable-next-line @typescript-eslint/no-deprecated policyTags: getPolicyTagsData(participant.policyID), policyRecentlyUsedTags, @@ -1023,7 +1023,7 @@ function updateSplitTransactions({ const originalTransactionID = transactionData?.originalTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID; const originalTransaction = allTransactionsList?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]; const originalTransactionDetails = getTransactionDetails(originalTransaction); - // TODO: remove `allPolicyTags` from this file [ISSUE YET TO BE CREATED] + // TODO: remove `allPolicyTags` from this file [https://github.com/Expensify/App/issues/80401] // eslint-disable-next-line @typescript-eslint/no-deprecated const policyTags = getPolicyTagsData(expenseReport?.policyID); const participants = getMoneyRequestParticipantsFromReport(expenseReport, currentUserPersonalDetails.accountID); From 2f63889c18d15705f0e3d3f5e97cab7a83081b4e Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Sat, 24 Jan 2026 12:58:10 +0100 Subject: [PATCH 21/26] update Mobile --- android/app/build.gradle | 4 +- contributingGuides/NAVIGATION.md | 229 ++++++- ios/NewExpensify/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- ios/ShareViewController/Info.plist | 2 +- package-lock.json | 13 +- package.json | 6 +- src/CONST/index.ts | 15 + src/ONYXKEYS.ts | 2 - src/ROUTES.ts | 15 +- src/SCREENS.ts | 3 +- .../AttachmentCarousel/extractAttachments.ts | 14 +- src/components/EReceipt.tsx | 3 +- .../InteractiveStepSubPageHeader.tsx | 90 +++ .../LHNOptionsList/LHNOptionsList.tsx | 86 +-- .../LHNOptionsList/OptionRowLHNData.tsx | 3 - src/components/MoneyReportHeader.tsx | 23 +- .../MoneyRequestConfirmationListFooter.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 22 +- .../MoneyRequestReportActionsList.tsx | 34 +- src/components/ParentNavigationSubtitle.tsx | 5 +- .../ReportActionItem/IssueCardMessage.tsx | 2 +- .../FilterDropdowns/UserSelectPopup.tsx | 3 +- .../Search/SearchAutocompleteList.tsx | 12 +- src/components/Search/SearchContext.tsx | 59 +- .../Search/SearchFiltersChatsSelector.tsx | 22 +- .../SearchFiltersParticipantsSelector.tsx | 3 +- src/components/Search/index.tsx | 19 +- src/components/Search/types.ts | 7 +- .../SelectionList/BaseSelectionList.tsx | 77 +-- .../ListItem/ListItemRenderer.tsx | 4 +- .../BaseSelectionListWithSections.tsx | 328 +++++++++ .../index.native.tsx | 17 + .../SelectionListWithSections/index.tsx | 23 + .../SelectionListWithSections/types.ts | 47 ++ .../SelectionList/components/TextInput.tsx | 6 +- .../hooks/useFlattenedSections.ts | 95 +++ .../SelectionList/hooks/useSearchFocusSync.ts | 101 +++ .../hooks/useSelectedItemFocusSync.ts | 52 ++ .../useWebSelectionListBehavior/index.ts | 20 + .../useWebSelectionListBehavior/index.web.ts | 104 +++ .../useWebSelectionListBehavior/types.ts | 20 + src/components/SelectionList/index.native.tsx | 3 +- src/components/SelectionList/index.tsx | 88 +-- src/components/SelectionList/types.ts | 255 +++---- .../SubStepForms/DateOfBirthStep.tsx | 4 +- src/components/TaxPicker.tsx | 22 +- src/hooks/useOnyx.ts | 6 +- src/hooks/useSearchHighlightAndScroll.ts | 6 +- src/hooks/useSearchSelector.base.ts | 12 +- src/hooks/useSubPage/index.tsx | 118 ++++ src/hooks/useSubPage/types.ts | 52 ++ src/hooks/useTodos.ts | 178 ++++- src/languages/de.ts | 2 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/languages/fr.ts | 2 + src/languages/it.ts | 2 + src/languages/ja.ts | 2 + src/languages/nl.ts | 2 + src/languages/pl.ts | 4 +- src/languages/pt-BR.ts | 2 + src/languages/zh-hans.ts | 2 + .../API/parameters/CreateAppReportParams.ts | 1 + .../parameters/CreateDistanceRequestParams.ts | 1 + src/libs/CardFeedUtils.ts | 8 +- src/libs/CardUtils.ts | 14 +- src/libs/IOUUtils.ts | 32 +- .../ModalStackNavigators/index.tsx | 3 +- src/libs/Navigation/linkingConfig/config.ts | 6 +- src/libs/Navigation/types.ts | 12 +- src/libs/OptionsListUtils/index.ts | 249 +++---- src/libs/OptionsListUtils/types.ts | 3 +- src/libs/PolicyUtils.ts | 5 + src/libs/ReportActionsUtils.ts | 130 +--- src/libs/ReportSecondaryActionUtils.ts | 3 +- src/libs/ReportUtils.ts | 39 +- src/libs/SearchQueryUtils.ts | 8 +- src/libs/SearchUIUtils.ts | 43 +- src/libs/SidebarUtils.ts | 32 +- src/libs/SubPageUtils.ts | 20 + src/libs/TaxOptionsListUtils.ts | 6 +- src/libs/TransactionUtils/index.ts | 8 + src/libs/WorkspaceReportFieldUtils.ts | 18 +- src/libs/actions/IOU/Duplicate.ts | 36 +- src/libs/actions/IOU/MoneyRequest.ts | 2 +- src/libs/actions/IOU/index.ts | 12 +- .../OnyxDerived/ONYX_DERIVED_VALUES.ts | 2 - .../configs/visibleReportActions.ts | 136 ---- src/libs/actions/Report.ts | 119 +++- src/libs/actions/TaxRate.ts | 25 +- .../MissingPersonalDetailsContent.tsx | 99 ++- .../MissingPersonalDetailsMagicCodePage.tsx | 6 +- .../{substeps => subPages}/Address.tsx | 4 +- .../{substeps => subPages}/Confirmation.tsx | 14 +- .../{substeps => subPages}/DateOfBirth.tsx | 4 +- .../{substeps => subPages}/LegalName.tsx | 4 +- .../{substeps => subPages}/PhoneNumber.tsx | 4 +- src/pages/MissingPersonalDetails/types.ts | 6 +- src/pages/MissingPersonalDetails/utils.ts | 20 +- src/pages/NewChatPage.tsx | 6 +- src/pages/RoomInvitePage.tsx | 16 +- src/pages/Search/AdvancedSearchFilters.tsx | 2 +- src/pages/Search/SearchPage.tsx | 11 +- .../Search/SearchTransactionsChangeReport.tsx | 32 +- src/pages/Share/ShareTab.tsx | 4 +- src/pages/Share/SubmitDetailsPage.tsx | 4 +- src/pages/home/ReportScreen.tsx | 17 +- .../PopoverReportActionContextMenu.tsx | 4 +- .../report/ReportActionItemMessageEdit.tsx | 3 - src/pages/home/report/ReportActionsView.tsx | 37 +- src/pages/iou/SplitBillDetailsPage.tsx | 2 +- src/pages/iou/request/IOURequestStartPage.tsx | 27 +- .../MoneyRequestAccountantSelector.tsx | 4 +- .../request/MoneyRequestAttendeeSelector.tsx | 2 +- .../MoneyRequestParticipantsSelector.tsx | 2 +- .../request/step/BaseRequestStepWorkspace.tsx | 106 +++ .../iou/request/step/IOURequestEditReport.tsx | 17 +- .../step/IOURequestEditReportCommon.tsx | 4 +- .../iou/request/step/IOURequestStepAmount.tsx | 20 +- .../step/IOURequestStepConfirmation.tsx | 2 +- .../step/IOURequestStepDistanceOdometer.tsx | 2 +- .../iou/request/step/IOURequestStepHours.tsx | 51 +- .../request/step/IOURequestStepMerchant.tsx | 16 +- .../step/IOURequestStepParticipants.tsx | 35 +- .../step/IOURequestStepPerDiemWorkspace.tsx | 160 +---- .../iou/request/step/IOURequestStepReport.tsx | 29 +- .../IOURequestStepScan/ReceiptView/index.tsx | 12 +- .../step/IOURequestStepTimeWorkspace.tsx | 51 ++ .../step/withFullTransactionOrNotFound.tsx | 3 +- .../step/withWritableReportOrNotFound.tsx | 3 +- ...pensifyCardMissingDetailsMagicCodePage.tsx | 4 +- .../Wallet/ExpensifyCardPage/index.tsx | 2 +- .../WorkspaceCompanyCardFeedSelectorPage.tsx | 2 +- ...kspaceCompanyCardsSettingsFeedNamePage.tsx | 2 +- .../WorkspaceCompanyCardsSettingsPage.tsx | 2 +- ...orkspaceCompanyCardsTableHeaderButtons.tsx | 2 +- .../reports/ReportFieldsAddListValuePage.tsx | 4 +- .../reports/ReportFieldsEditValuePage.tsx | 4 +- .../reports/ReportFieldsInitialValuePage.tsx | 2 +- .../reports/ReportFieldsSettingsPage.tsx | 2 +- src/pages/workspace/taxes/NamePage.tsx | 4 +- .../taxes/WorkspaceCreateTaxPage.tsx | 4 +- .../workspace/taxes/WorkspaceTaxCodePage.tsx | 4 +- src/types/onyx/DerivedValues.ts | 14 +- src/types/onyx/index.ts | 3 +- tests/actions/IOUTest/DuplicateTest.ts | 60 +- tests/actions/ReportTest.ts | 74 ++- tests/ui/components/LHNOptionsListTest.tsx | 24 +- tests/unit/CardUtilsTest.ts | 14 +- tests/unit/OptionsListUtilsTest.tsx | 553 ++++++++++++++-- tests/unit/ReportActionsUtilsTest.ts | 23 - tests/unit/ReportUtilsTest.ts | 1 + tests/unit/SidebarUtilsTest.ts | 22 +- tests/unit/TaxOptionsListUtilsTest.ts | 8 +- tests/unit/TransactionGroupListItemTest.tsx | 1 + tests/unit/useSearchHighlightAndScrollTest.ts | 1 + tests/unit/useSubPageTest.tsx | 622 ++++++++++++++++++ 158 files changed, 4087 insertions(+), 1551 deletions(-) create mode 100644 src/components/InteractiveStepSubPageHeader.tsx create mode 100644 src/components/SelectionList/SelectionListWithSections/BaseSelectionListWithSections.tsx create mode 100644 src/components/SelectionList/SelectionListWithSections/index.native.tsx create mode 100644 src/components/SelectionList/SelectionListWithSections/index.tsx create mode 100644 src/components/SelectionList/SelectionListWithSections/types.ts create mode 100644 src/components/SelectionList/hooks/useFlattenedSections.ts create mode 100644 src/components/SelectionList/hooks/useSearchFocusSync.ts create mode 100644 src/components/SelectionList/hooks/useSelectedItemFocusSync.ts create mode 100644 src/components/SelectionList/hooks/useWebSelectionListBehavior/index.ts create mode 100644 src/components/SelectionList/hooks/useWebSelectionListBehavior/index.web.ts create mode 100644 src/components/SelectionList/hooks/useWebSelectionListBehavior/types.ts create mode 100644 src/hooks/useSubPage/index.tsx create mode 100644 src/hooks/useSubPage/types.ts create mode 100644 src/libs/SubPageUtils.ts delete mode 100644 src/libs/actions/OnyxDerived/configs/visibleReportActions.ts rename src/pages/MissingPersonalDetails/{substeps => subPages}/Address.tsx (99%) rename src/pages/MissingPersonalDetails/{substeps => subPages}/Confirmation.tsx (80%) rename src/pages/MissingPersonalDetails/{substeps => subPages}/DateOfBirth.tsx (92%) rename src/pages/MissingPersonalDetails/{substeps => subPages}/LegalName.tsx (93%) rename src/pages/MissingPersonalDetails/{substeps => subPages}/PhoneNumber.tsx (96%) create mode 100644 src/pages/iou/request/step/BaseRequestStepWorkspace.tsx create mode 100644 src/pages/iou/request/step/IOURequestStepTimeWorkspace.tsx create mode 100644 tests/unit/useSubPageTest.tsx diff --git a/android/app/build.gradle b/android/app/build.gradle index d2d4a6b9d95f..6f4e6cc2f12d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -114,8 +114,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009030700 - versionName "9.3.7-0" + versionCode 1009030701 + versionName "9.3.7-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 2fd461eb2060..35b18e8b173b 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -13,6 +13,9 @@ The navigation in the app is built on top of the `react-navigation` library. To - [Dismissing modals with opening a report](#dismissing-modals-with-opening-a-report) - [Summary](#summary) - [Adding new screens](#adding-new-screens) + - [Multi-step flows with URL synchronization](#multi-step-flows-with-url-synchronization) + - [When to use](#when-to-use) + - [Implementation pattern](#implementation-pattern) - [Debugging](#debugging) - [Reading state when it changes](#reading-state-when-it-changes) - [Finding the code that calls the navigation function](#finding-the-code-that-calls-the-navigation-function) @@ -396,7 +399,7 @@ export default NewSettingsScreen; }); ``` - Let's assume that we want to have PreferencesPage below our new Settings RHP screen. + Let's assume that we want to have PreferencesPage below our new Settings RHP screen. ```ts // src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -415,6 +418,230 @@ export default NewSettingsScreen; export default SETTINGS_TO_RHP; ``` +## Multi-step flows with URL synchronization + +Multi-step flows (wizards, forms with multiple screens) should use URL-based navigation via the `useSubPage` hook or via basic navigation between plain static routes. This approach ensures browser navigation works correctly and page refreshes preserve the current position. + +### When to use + +You can use `useSubPage` hook for any multi-step flow where: +- Users progress through a series of screens to complete a task +- Each step collects or displays different information +- The flow has a final confirmation or summary step +- You need proper browser back/forward button support +- Page refresh should preserve the user's current position + +Common examples include: +- Account setup wizards +- Form flows with multiple sections +- Settings configuration flows + +### Implementation pattern + +#### 1. Define your routes + +Add routes with a `subPage` parameter to `ROUTES.ts`. The `action` parameter is used for edit mode: + +```ts +MY_FLOW: { + route: 'my-flow/:subPage?/:action?', + getRoute: (subPage?: string, action?: 'edit') => { + if (!subPage) { + return 'my-flow' as const; + } + return `my-flow/${subPage}${action ? `/${action}` : ''}` as const; + }, +}, +``` + +#### 2. Add navigation config + +Register the screen in `linkingConfig/config.ts`: + +```ts +[SCREENS.MY_FLOW]: { + path: ROUTES.MY_FLOW.route, + exact: true, +}, +``` + +#### 3. Define page constants + +Add page name constants to `CONST.ts`: + +```ts +MY_FLOW: { + STEP_INDEX_LIST: ['1', '2', '3'], + PAGE_NAME: { + STEP_ONE: 'step-one', + STEP_TWO: 'step-two', + CONFIRMATION: 'confirmation', + }, +}, +``` + +#### 4. Create sub-page components + +Each sub-page receives `SubPageProps` and any custom props you define: + +```ts +import type {SubPageProps} from '@hooks/useSubPage/types'; + +type CustomSubPageProps = SubPageProps & { + // Add custom props specific to your flow + formValues: MyFormType; +}; + +function StepOne({isEditing, onNext, onMove, formValues}: CustomSubPageProps) { + const handleSubmit = (data: FormData) => { + saveData(data); + onNext(); + }; + + return ( + + {/* Form fields */} + + ); +} +``` + +#### 5. Implement the main flow component + +```ts +import useSubPage from '@hooks/useSubPage'; +import type {SubPageProps} from '@hooks/useSubPage/types'; +import InteractiveStepSubPageHeader from '@components/InteractiveStepSubPageHeader'; + +type CustomSubPageProps = SubPageProps & { + formValues: MyFormType; +}; + +const pages = [ + {pageName: CONST.MY_FLOW.PAGE_NAME.STEP_ONE, component: StepOne}, + {pageName: CONST.MY_FLOW.PAGE_NAME.STEP_TWO, component: StepTwo}, + {pageName: CONST.MY_FLOW.PAGE_NAME.CONFIRMATION, component: Confirmation}, +]; + +function MyFlowContent() { + const { + CurrentPage, + isEditing, + pageIndex, + prevPage, + nextPage, + lastPageIndex, + moveTo, + resetToPage, + } = useSubPage({ + pages, + startFrom: 0, + onFinished: () => { + // Handle flow completion + Navigation.goBack(); + }, + buildRoute: (pageName, action) => ROUTES.MY_FLOW.getRoute(pageName, action), + }); + + const handleBackButtonPress = () => { + if (isEditing) { + Navigation.goBack(); + return; + } + + if (pageIndex === 0) { + Navigation.closeRHPFlow(); + return; + } + + prevPage(); + }; + + return ( + + + + + + + + ); +} +``` + +### Using InteractiveStepSubPageHeader + +The `InteractiveStepSubPageHeader` component is designed to work with the `useSubPage` hook for URL-based multi-step flows. + +Key features: +- Displays numbered step indicators with connecting lines +- Shows completed steps with a checkmark icon +- Allows users to tap on completed steps to navigate back (entering edit mode) +- Locked (future) steps are disabled +- Automatically syncs with the current page index from the URL + +```ts + +``` + +> **Note**: The `stepNames` array determines the number of steps displayed. The `currentStepIndex` is 0-based. When a user taps a completed step, `onStepSelected` is called with that step's index, which triggers edit mode navigation. + +#### 6. Handle edit mode + +The hook automatically manages edit mode via the `action=edit` URL parameter. When users navigate to a previous step using `moveTo()`, they enter edit mode. In edit mode, calling `nextPage()` returns them to the last page (typically confirmation) instead of advancing sequentially: + +```ts +// In Confirmation component - allow editing previous steps +function Confirmation({onMove, formValues}: CustomSubPageProps) { + return ( + + onMove(0)} + /> + {/* More review items */} + + ); +} +``` + +#### 7. Skip pages conditionally + +Use `skipPages` to conditionally skip steps based on user input or feature flags: + +```ts +const skipPages = useMemo(() => { + const pagesToSkip: string[] = []; + if (!requiresAdditionalInfo) { + pagesToSkip.push(CONST.MY_FLOW.PAGE_NAME.STEP_TWO); + } + return pagesToSkip; +}, [requiresAdditionalInfo]); + +const {...} = useSubPage({ + pages, + skipPages, + // ...other props +}); +``` + ## Debugging ### Reading state when it changes diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 797b1106d997..b9492ea49a9d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.3.7.0 + 9.3.7.1 FullStory OrgId diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 48285a740ee6..94d5939cf3c3 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.3.7 CFBundleVersion - 9.3.7.0 + 9.3.7.1 NSExtension NSExtensionPointIdentifier diff --git a/ios/ShareViewController/Info.plist b/ios/ShareViewController/Info.plist index 6a5966faa65e..3f8247b8269f 100644 --- a/ios/ShareViewController/Info.plist +++ b/ios/ShareViewController/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.3.7 CFBundleVersion - 9.3.7.0 + 9.3.7.1 NSExtension NSExtensionAttributes diff --git a/package-lock.json b/package-lock.json index 31ce4d3a5ed9..889428e866d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.3.7-0", + "version": "9.3.7-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.3.7-0", + "version": "9.3.7-1", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -119,7 +119,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.2.0", - "react-native-onyx": "3.0.30", + "react-native-onyx": "3.0.31", "react-native-pager-view": "7.0.2", "react-native-pdf": "7.0.2", "react-native-performance": "^6.0.0", @@ -33885,9 +33885,9 @@ } }, "node_modules/react-native-onyx": { - "version": "3.0.30", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.30.tgz", - "integrity": "sha512-qlaK6UJTdoO26oPWTGCvii3U9vITrBXWaam7pkP+W9tiB56DbTcVVC/nWvJlg7+CENa2NlGwuPBB2DtodpBt8A==", + "version": "3.0.31", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-3.0.31.tgz", + "integrity": "sha512-QWAuJIjVjOycobk1eUsvRB0SgBTnHOowHPxQTDtC53ayaxoeN/f10ziKIe6on7VEKzzP7GWN3CJuBwR4Uqbijg==", "license": "MIT", "dependencies": { "ascii-table": "0.0.9", @@ -33905,7 +33905,6 @@ "peerDependencies": { "idb-keyval": "^6.2.1", "react": ">=18.1.0", - "react-dom": ">=18.1.0", "react-native": ">=0.75.0", "react-native-device-info": "^10.3.0", "react-native-nitro-modules": ">=0.27.2", diff --git a/package.json b/package.json index ecd3662483e6..3598b3c5b29e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.3.7-0", + "version": "9.3.7-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -43,7 +43,7 @@ "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", - "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=386 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", + "lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=387 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto", "lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh", "check-lazy-loading": "ts-node scripts/checkLazyLoading.ts", "lint-watch": "npx eslint-watch --watch --changed", @@ -188,7 +188,7 @@ "react-native-localize": "^3.5.4", "react-native-nitro-modules": "0.29.4", "react-native-nitro-sqlite": "9.2.0", - "react-native-onyx": "3.0.30", + "react-native-onyx": "3.0.31", "react-native-pager-view": "7.0.2", "react-native-pdf": "7.0.2", "react-native-performance": "^6.0.0", diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 2b6e2f223bae..3bc77c33513e 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -2684,6 +2684,16 @@ const CONST = { VENDOR_BILL: 'VENDOR_BILL', }, + MISSING_PERSONAL_DETAILS: { + STEP_INDEX_LIST: ['1', '2', '3', '4'], + PAGE_NAME: { + LEGAL_NAME: 'legal-name', + DATE_OF_BIRTH: 'date-of-birth', + ADDRESS: 'address', + PHONE_NUMBER: 'phone-number', + CONFIRM: 'confirm', + }, + }, MISSING_PERSONAL_DETAILS_INDEXES: { MAPPING: { LEGAL_NAME: 0, @@ -8052,6 +8062,11 @@ const CONST = { /** Onyx prefix for domain security groups */ DOMAIN_SECURITY_GROUP_PREFIX: 'domain_securityGroup_', }, + + SECTION_LIST_ITEM_TYPE: { + HEADER: 'header', + ROW: 'row', + }, } as const; const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [ diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 3205cb49523a..97d06c697306 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -986,7 +986,6 @@ const ONYXKEYS = { REPORT_ATTRIBUTES: 'reportAttributes', REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations', OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID', - VISIBLE_REPORT_ACTIONS: 'visibleReportActions', }, /** Stores HybridApp specific state required to interoperate with OldDot */ @@ -1395,7 +1394,6 @@ type OnyxDerivedValuesMapping = { [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue; [ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue; [ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue; - [ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS]: OnyxTypes.VisibleReportActionsDerivedValue; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 50d6749e940f..cfcfe09941df 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1335,6 +1335,11 @@ const ROUTES = { getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => `${action as string}/${iouType as string}/hours/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}` as const, }, + MONEY_REQUEST_STEP_HOURS_EDIT: { + route: ':action/:iouType/hours-edit/:transactionID/:reportID/:reportActionID?', + getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, reportActionID?: string) => + `${action as string}/${iouType as string}/hours-edit/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}` as const, + }, DISTANCE_REQUEST_CREATE: { route: ':action/:iouType/start/:transactionID/:reportID/distance-new/:backToReport?', getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) => @@ -3153,7 +3158,15 @@ const ROUTES = { route: 'restricted-action/workspace/:policyID', getRoute: (policyID: string) => `restricted-action/workspace/${policyID}` as const, }, - MISSING_PERSONAL_DETAILS: 'missing-personal-details', + MISSING_PERSONAL_DETAILS: { + route: 'missing-personal-details/:subPage?/:action?', + getRoute: (subPage?: string, action?: 'edit') => { + if (!subPage) { + return 'missing-personal-details' as const; + } + return `missing-personal-details/${subPage}${action ? `/${action}` : ''}` as const; + }, + }, MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'missing-personal-details/confirm-magic-code', POLICY_ACCOUNTING_NETSUITE_SUBSIDIARY_SELECTOR: { route: 'workspaces/:policyID/accounting/netsuite/subsidiary-selector', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 739c5ac47279..617ab3c4d9a6 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -355,6 +355,7 @@ const SCREENS = { RECEIPT_PREVIEW: 'Money_Request_Receipt_preview', STEP_TIME_RATE: 'Money_Request_Step_Time_Rate', STEP_HOURS: 'Money_Request_Step_Hours', + STEP_HOURS_EDIT: 'Money_Request_Step_Hours_Edit', }, TRANSACTION_DUPLICATE: { @@ -860,7 +861,7 @@ const SCREENS = { TRANSACTION_RECEIPT: 'TransactionReceipt', FEATURE_TRAINING_ROOT: 'FeatureTraining_Root', RESTRICTED_ACTION_ROOT: 'RestrictedAction_Root', - MISSING_PERSONAL_DETAILS_ROOT: 'MissingPersonalDetails_Root', + MISSING_PERSONAL_DETAILS: 'MissingPersonalDetails', MISSING_PERSONAL_DETAILS_CONFIRM_MAGIC_CODE: 'MissingPersonalDetails_ConfirmMagicCode', ADD_UNREPORTED_EXPENSES_ROOT: 'AddUnreportedExpenses_Root', DEBUG: { diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index df0d08946c19..6dffd994d6a7 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -3,11 +3,11 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import {getFileName, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; -import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, isReportActionVisible} from '@libs/ReportActionsUtils'; +import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; -import type {Report, ReportAction, ReportActions, VisibleReportActionsDerivedValue} from '@src/types/onyx'; +import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; /** @@ -22,7 +22,6 @@ function extractAttachments( reportActions, report, isReportArchived, - visibleReportActionsData, }: { privateNotes?: Record; accountID?: number; @@ -30,7 +29,6 @@ function extractAttachments( reportActions?: OnyxEntry; report: OnyxEntry; isReportArchived: boolean | undefined; - visibleReportActionsData?: VisibleReportActionsDerivedValue; }, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; @@ -117,13 +115,9 @@ function extractAttachments( return attachments.reverse(); } - const reportID = report?.reportID; - if (!reportID) { - return attachments.reverse(); - } const actions = [...(parentReportAction ? [parentReportAction] : []), ...getSortedReportActions(Object.values(reportActions ?? {}))]; - for (const action of actions) { - if (!isReportActionVisible(action, reportID, canUserPerformAction, visibleReportActionsData) || isMoneyRequestAction(action)) { + for (const [key, action] of actions.entries()) { + if (!shouldReportActionBeVisible(action, key, canUserPerformAction) || isMoneyRequestAction(action)) { continue; } diff --git a/src/components/EReceipt.tsx b/src/components/EReceipt.tsx index f9ae0027eaa4..d4cde4306b30 100644 --- a/src/components/EReceipt.tsx +++ b/src/components/EReceipt.tsx @@ -62,7 +62,8 @@ function EReceipt({transactionID, transactionItem, onLoad, isThumbnail = false}: const formattedAmount = convertToDisplayString(transactionAmount, transactionCurrency); const currency = getCurrencySymbol(transactionCurrency ?? ''); const amount = currency ? formattedAmount.replace(currency, '') : formattedAmount; - const cardDescription = getCompanyCardDescription(transactionCardName, transactionCardID, cardList) ?? (transactionCardID ? getCardDescription(cardList?.[transactionCardID]) : ''); + const cardDescription = + getCompanyCardDescription(transactionCardName, transactionCardID, cardList) ?? (transactionCardID ? getCardDescription(cardList?.[transactionCardID], translate) : ''); const secondaryBgcolorStyle = secondaryColor ? StyleUtils.getBackgroundColorStyle(secondaryColor) : undefined; const primaryTextColorStyle = primaryColor ? StyleUtils.getColorStyle(primaryColor) : undefined; const titleTextColorStyle = titleColor ? StyleUtils.getColorStyle(titleColor) : undefined; diff --git a/src/components/InteractiveStepSubPageHeader.tsx b/src/components/InteractiveStepSubPageHeader.tsx new file mode 100644 index 000000000000..66b5762490c6 --- /dev/null +++ b/src/components/InteractiveStepSubPageHeader.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import type {ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useThemeStyles from '@hooks/useThemeStyles'; +import colors from '@styles/theme/colors'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import Icon from './Icon'; +import PressableWithFeedback from './Pressable/PressableWithFeedback'; +import Text from './Text'; + +type InteractiveStepSubPageHeaderProps = { + /** List of step names to display */ + stepNames: readonly string[]; + + /** Current step index (0-based) */ + currentStepIndex: number; + + /** Function to call when a step is selected */ + onStepSelected?: (stepIndex: number) => void; +}; + +const MIN_AMOUNT_FOR_EXPANDING = 3; +const MIN_AMOUNT_OF_STEPS = 2; + +function InteractiveStepSubPageHeader({stepNames, currentStepIndex, onStepSelected}: InteractiveStepSubPageHeaderProps) { + const styles = useThemeStyles(); + const icons = useMemoizedLazyExpensifyIcons(['Checkmark']); + const containerWidthStyle: ViewStyle = stepNames.length < MIN_AMOUNT_FOR_EXPANDING ? styles.mnw60 : styles.mnw100; + + if (stepNames.length < MIN_AMOUNT_OF_STEPS) { + throw new Error(`stepNames list must have at least ${MIN_AMOUNT_OF_STEPS} elements.`); + } + + const lastStepIndex = stepNames.length - 1; + + const handleStepPress = (isLockedStep: boolean, index: number) => { + if (isLockedStep || !onStepSelected) { + return; + } + onStepSelected(index); + }; + + return ( + + {stepNames.map((stepName, index) => { + const isCompletedStep = currentStepIndex > index; + const isLockedStep = currentStepIndex < index; + const isLockedLine = currentStepIndex < index + 1; + const hasConnectingLine = index < lastStepIndex; + + return ( + + handleStepPress(isLockedStep, index)} + accessible + accessibilityLabel={stepName} + role={CONST.ROLE.BUTTON} + > + {isCompletedStep ? ( + + ) : ( + {index + 1} + )} + + {hasConnectingLine ? : null} + + ); + })} + + ); +} + +export default InteractiveStepSubPageHeader; diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 308bb1599c61..8159e1072648 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -29,19 +29,20 @@ import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; import { - getLastVisibleAction, getOneTransactionThreadReportID, getOriginalMessage, - getReportActionActorAccountID, + getSortedReportActions, + getSortedReportActionsForDisplay, isInviteOrRemovedAction, isMoneyRequestAction, + shouldReportActionBeVisibleAsLastAction, } from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report} from '@src/types/onyx'; +import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import OptionRowLHNData from './OptionRowLHNData'; import OptionRowRendererComponent from './OptionRowRendererComponent'; @@ -71,7 +72,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY, {canBeMissing: true}); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); @@ -192,6 +192,9 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio } const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]; + const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item); + const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`]; const transactionID = isMoneyRequestAction(itemParentReportAction) ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) @@ -203,57 +206,55 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived); - const lastAction = getLastVisibleAction( - reportID, - canUserPerformWrite, - {}, - itemReportActions ? {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: itemReportActions} : undefined, - visibleReportActionsData, - ); + const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite); + const lastReportAction = sortedReportActions.at(0); - const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, visibleReportActionsData, lastAction); - const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; - - const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; + // Get the transaction for the last report action + const lastReportActionTransactionID = isMoneyRequestAction(lastReportAction) + ? (getOriginalMessage(lastReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) + : CONST.DEFAULT_NUMBER_ID; const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`]; - const lastActorAccountID = getReportActionActorAccountID(lastAction, undefined, item) ?? item.lastActorAccountID; - let lastActorDetails: Partial | null = lastActorAccountID && personalDetails?.[lastActorAccountID] ? personalDetails[lastActorAccountID] : null; - - if (!lastActorDetails && lastAction) { - const lastActorDisplayName = lastAction?.person?.[0]?.text; + // SidebarUtils.getOptionData in OptionRowLHNData does not get re-evaluated when the linked task report changes, so we have the lastMessageTextFromReport evaluation logic here + let lastActorDetails: Partial | null = item?.lastActorAccountID && personalDetails?.[item.lastActorAccountID] ? personalDetails[item.lastActorAccountID] : null; + if (!lastActorDetails && lastReportAction) { + const lastActorDisplayName = lastReportAction?.person?.[0]?.text; lastActorDetails = lastActorDisplayName ? { displayName: lastActorDisplayName, - accountID: lastActorAccountID, + accountID: item?.lastActorAccountID, } : null; } - - const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; - const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`]; + const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; + const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; const itemReportMetadata = reportMetadataCollection?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`]; - - // For archived reports, always call getLastMessageTextForReport to get the archive reason message - // instead of using lastMessageText from the report - const lastMessageTextFromReport = - (isReportArchived ? undefined : item.lastMessageText) ?? - getLastMessageTextForReport({ - translate, - report: item, - lastActorDetails, - movedFromReport, - movedToReport, - policy: itemPolicy, - isReportArchived, - policyForMovingExpensesID, - reportMetadata: itemReportMetadata, - visibleReportActionsDataParam: visibleReportActionsData, - lastAction, - }); + const lastMessageTextFromReport = getLastMessageTextForReport({ + translate, + report: item, + lastActorDetails, + movedFromReport, + movedToReport, + policy: itemPolicy, + isReportArchived: !!itemReportNameValuePairs?.private_isArchived, + policyForMovingExpensesID, + reportMetadata: itemReportMetadata, + }); const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; + let lastAction: ReportAction | undefined; + if (!itemReportActions || !item) { + lastAction = undefined; + } else { + const canUserPerformWriteAction = canUserPerformWriteActionUtil(item, isReportArchived); + const actionsArray = getSortedReportActions(Object.values(itemReportActions)); + const reportActionsForDisplay = actionsArray.filter( + (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + ); + lastAction = reportActionsForDisplay.at(-1); + } + let lastActionReport: OnyxEntry | undefined; if (isInviteOrRemovedAction(lastAction)) { const lastActionOriginalMessage = lastAction?.actionName ? getOriginalMessage(lastAction) : null; @@ -326,7 +327,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio isScreenFocused, localeCompare, translate, - visibleReportActionsData, currentUserAccountID, ], ); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index b20daa3bc918..0fd1032c2628 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -51,7 +51,6 @@ function OptionRowLHNData({ const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`, {canBeMissing: true}); const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`, {canBeMissing: true}); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); // Check the report errors equality to avoid re-rendering when there are no changes const prevReportErrors = usePrevious(reportAttributes?.reportErrors); const areReportErrorsEqual = useMemo(() => deepEqual(prevReportErrors, reportAttributes?.reportErrors), [prevReportErrors, reportAttributes?.reportErrors]); @@ -79,7 +78,6 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, - visibleReportActionsData, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -116,7 +114,6 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, - visibleReportActionsData, ]); return ( diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 5ecd5a62ce26..40444b3125ef 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -91,6 +91,7 @@ import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; import { allHavePendingRTERViolation, getOriginalTransactionWithSplitInfo, + hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils, hasDuplicateTransactions, isDuplicate, isExpensifyCardTransaction, @@ -132,6 +133,7 @@ import BrokenConnectionDescription from './BrokenConnectionDescription'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import type {DropdownOption} from './ButtonWithDropdownMenu/types'; +import ConfirmModal from './ConfirmModal'; import DecisionModal from './DecisionModal'; import {DelegateNoAccessContext} from './DelegateNoAccessModalProvider'; import Header from './Header'; @@ -326,6 +328,7 @@ function MoneyReportHeader({ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW); const hasViolations = hasViolationsReportUtils(moneyRequestReport?.reportID, allTransactionViolations, accountID, email ?? ''); + const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations); const [exportModalStatus, setExportModalStatus] = useState(null); const {showConfirmModal} = useConfirmModal(); @@ -395,6 +398,7 @@ function MoneyReportHeader({ const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isHoldEducationalModalVisible, setIsHoldEducationalModalVisible] = useState(false); + const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [rejectModalAction, setRejectModalAction] = useState | null>(null); @@ -649,13 +653,14 @@ function MoneyReportHeader({ activePolicyID, quickAction, policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], + customUnitPolicyID: policy?.id, targetPolicy: defaultExpensePolicy ?? undefined, targetPolicyCategories: activePolicyCategories, targetReport: activePolicyExpenseChat, }); } }, - [activePolicyExpenseChat, activePolicyID, allPolicyCategories, defaultExpensePolicy, introSelected, isASAPSubmitBetaEnabled, quickAction, policyRecentlyUsedCurrencies], + [activePolicyExpenseChat, activePolicyID, allPolicyCategories, defaultExpensePolicy, introSelected, isASAPSubmitBetaEnabled, quickAction, policyRecentlyUsedCurrencies, policy?.id], ); const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( @@ -1319,6 +1324,11 @@ function MoneyReportHeader({ icon: isDuplicateActive ? expensifyIcons.ReceiptMultiple : expensifyIcons.CheckmarkCircle, value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE, onSelected: () => { + if (hasCustomUnitOutOfPolicyViolation) { + setDuplicateDistanceErrorModalVisible(true); + return; + } + if (!isDuplicateActive || !transaction) { return; } @@ -1327,7 +1337,7 @@ function MoneyReportHeader({ duplicateExpenseTransaction([transaction]); }, - shouldCloseModalOnSelect: activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, + shouldCloseModalOnSelect: hasCustomUnitOutOfPolicyViolation || activePolicyExpenseChat?.iouReportID === moneyRequestReport?.reportID, }, [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE]: { text: translate('iou.changeWorkspace'), @@ -1791,6 +1801,15 @@ function MoneyReportHeader({ isVisible={isDownloadErrorModalVisible} onClose={() => setIsDownloadErrorModalVisible(false)} /> + setDuplicateDistanceErrorModalVisible(false)} + onCancel={() => setDuplicateDistanceErrorModalVisible(false)} + confirmText={translate('common.buttonConfirm')} + prompt={translate('iou.correctDistanceRateError')} + shouldShowCancelButton={false} + /> {!!rejectModalAction && ( | null>(null); + const [duplicateDistanceErrorModalVisible, setDuplicateDistanceErrorModalVisible] = useState(false); const [isDuplicateActive, temporarilyDisableDuplicateAction] = useThrottledButtonState(); const [dismissedRejectUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_REJECT_USE_EXPLANATION, {canBeMissing: true}); const [dismissedHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {canBeMissing: true}); @@ -159,6 +161,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const {iouReport, chatReport: chatIOUReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(parentReportAction); const hasPendingRTERViolation = hasPendingRTERViolationTransactionUtils(transactionViolations); + const hasCustomUnitOutOfPolicyViolation = hasCustomUnitOutOfPolicyViolationTransactionUtils(transactionViolations); const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(parentReport, policy, transactionViolations); const isReportSubmitter = isCurrentUserSubmitter(chatIOUReport); @@ -196,13 +199,14 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre activePolicyID, quickAction, policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [], + customUnitPolicyID: policy?.id, targetPolicy: defaultExpensePolicy ?? undefined, targetPolicyCategories: activePolicyCategories, targetReport: activePolicyExpenseChat, }); } }, - [activePolicyExpenseChat, allPolicyCategories, defaultExpensePolicy, isASAPSubmitBetaEnabled, introSelected, activePolicyID, quickAction, policyRecentlyUsedCurrencies], + [activePolicyExpenseChat, allPolicyCategories, defaultExpensePolicy, isASAPSubmitBetaEnabled, introSelected, activePolicyID, quickAction, policyRecentlyUsedCurrencies, policy?.id], ); const getStatusIcon: (src: IconAsset) => ReactNode = (src) => ( @@ -427,6 +431,11 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre icon: isDuplicateActive ? Expensicons.ReceiptMultiple : Expensicons.CheckmarkCircle, value: CONST.REPORT.SECONDARY_ACTIONS.DUPLICATE, onSelected: () => { + if (hasCustomUnitOutOfPolicyViolation) { + setDuplicateDistanceErrorModalVisible(true); + return; + } + if (!isDuplicateActive || !transaction) { return; } @@ -435,7 +444,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre duplicateTransaction([transaction]); }, - shouldCloseModalOnSelect: false, + shouldCloseModalOnSelect: hasCustomUnitOutOfPolicyViolation, }, [CONST.REPORT.TRANSACTION_SECONDARY_ACTIONS.VIEW_DETAILS]: { value: CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS, @@ -596,6 +605,15 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre danger shouldEnableNewFocusManagement /> + setDuplicateDistanceErrorModalVisible(false)} + onCancel={() => setDuplicateDistanceErrorModalVisible(false)} + confirmText={translate('common.buttonConfirm')} + prompt={translate('iou.correctDistanceRateError')} + shouldShowCancelButton={false} + /> {!!rejectModalAction && ( { const filteredActions = reportActions.filter((reportAction) => { const isActionVisibleOnMoneyReport = isActionVisibleOnMoneyRequestReport(reportAction, shouldShowHarvestCreatedAction); - if (!isActionVisibleOnMoneyReport) { - return false; - } - - const passesOfflineCheck = isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; - if (!passesOfflineCheck) { - return false; - } - const actionReportID = reportAction.reportID ?? reportID; - const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID] ?? true; - if (!isStaticallyVisible) { - return false; - } - - if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { - return false; - } - - if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { - return false; - } - - return true; + return ( + isActionVisibleOnMoneyReport && + (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) && + isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs) + ); }); return filteredActions.toReversed(); - }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction, visibleReportActionsData, reportID]); + }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction]); const reportActionSize = useRef(visibleReportActions.length); const lastAction = visibleReportActions.at(-1); diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index e04d280a1628..cb1d3ea4939c 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; -import {getReportAction, isReportActionVisible} from '@libs/ReportActionsUtils'; +import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; @@ -88,7 +88,6 @@ function ParentNavigationSubtitle({ const {translate} = useLocalize(); const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, {canBeMissing: false}); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const isReportArchived = useReportIsArchived(report?.reportID); const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived); const isReportInRHP = currentRoute.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; @@ -117,7 +116,7 @@ function ParentNavigationSubtitle({ const onPress = () => { const parentAction = getReportAction(parentReportID, parentReportActionID); - const isVisibleAction = isReportActionVisible(parentAction, parentReportID, canUserPerformWriteAction, visibleReportActionsData); + const isVisibleAction = shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? CONST.DEFAULT_NUMBER_ID, canUserPerformWriteAction); const focusedNavigatorState = currentFocusedNavigator?.state; const currentReportIndex = focusedNavigatorState?.index; diff --git a/src/components/ReportActionItem/IssueCardMessage.tsx b/src/components/ReportActionItem/IssueCardMessage.tsx index 215038f2d9c3..9adb3619602b 100644 --- a/src/components/ReportActionItem/IssueCardMessage.tsx +++ b/src/components/ReportActionItem/IssueCardMessage.tsx @@ -45,7 +45,7 @@ function IssueCardMessage({action, policyID}: IssueCardMessageProps) { ${getCardIssuedMessage({reportAction: action, shouldRenderHTML: true, policyID, expensifyCard, companyCard, translate})}`} /> {shouldShowAddMissingDetailsButton && (