Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0339c92
migrate allTransactionViolations from Transaction.ts
Guccio163 May 20, 2026
215e41e
move duplicated logic to function, pass all violations
Guccio163 May 20, 2026
95e2693
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 1, 2026
2174f18
fix test in new file
Guccio163 Jun 1, 2026
20ff69e
bug proof test, to be removed after assesment
Guccio163 Jun 2, 2026
845787d
extract duplicatesViolations logic out of changeTransactionsReport
Guccio163 Jun 3, 2026
0a94de6
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 3, 2026
fc9b8c5
remove redundant export
Guccio163 Jun 3, 2026
14054ee
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 9, 2026
238bd53
remove redundant changeTransactionsReport props
Guccio163 Jun 9, 2026
7e36993
remove unnecessary whole collection subscription
Guccio163 Jun 9, 2026
4e3989d
simplify transaction extraction
Guccio163 Jun 9, 2026
50d4342
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 9, 2026
6cafc6c
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 9, 2026
4ac4d2b
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 10, 2026
bdc2f23
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 10, 2026
b2fc9f0
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 15, 2026
fc6f7fc
add filter to useTransactionsByID
Guccio163 Jun 15, 2026
c8335b0
remove redundant helpers
Guccio163 Jun 15, 2026
286b32b
rename back transactionViolations in ChangeTransactionsReportProps
Guccio163 Jun 15, 2026
0c3bb90
remove proof test file
Guccio163 Jun 15, 2026
cc7e3b3
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 16, 2026
1b17bd9
extract transactionsby useTransactionsByID
Guccio163 Jun 16, 2026
9f35713
use passed transactions
Guccio163 Jun 16, 2026
e1cebdb
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 19, 2026
d9e896c
remove selectedTransactionsByID
Guccio163 Jun 19, 2026
bf4be2c
fix ts
Guccio163 Jun 19, 2026
c4d1846
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 19, 2026
d9b465b
Merge branch 'main' of github.com:software-mansion-labs/expensify-app…
Guccio163 Jun 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 7 additions & 15 deletions src/components/AddExistingExpenseFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React from 'react';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionsByID from '@hooks/useTransactionsByID';
import {isIOUReport} from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import {convertBulkTrackedExpensesToIOU} from '@userActions/IOU/TrackExpense';
import {changeTransactionsReport} from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, PolicyCategories, Report, ReportNextStepDeprecated, Transaction} from '@src/types/onyx';
import type {Policy, PolicyCategories, Report, ReportNextStepDeprecated} from '@src/types/onyx';
import Button from './Button';
import FormHelpMessage from './FormHelpMessage';
import {usePersonalDetails, useSession} from './OnyxListItemProvider';
Expand Down Expand Up @@ -41,17 +42,6 @@ function AddExistingExpenseFooter({selectedIds, report, reportToConfirm, reportN
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
const session = useSession();
const personalDetails = usePersonalDetails();
const getSelectedTransactions = (allTransactions: OnyxCollection<Transaction>) =>
!allTransactions
? {}
: Array.from(selectedIds).reduce<Record<string, Transaction>>((acc, id) => {
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`];
if (transaction) {
acc[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = transaction;
}
return acc;
}, {});
const [selectedTransactions = CONST.EMPTY_OBJECT] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {selector: getSelectedTransactions});
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES);
Expand All @@ -61,6 +51,8 @@ function AddExistingExpenseFooter({selectedIds, report, reportToConfirm, reportN
const [policyTagList] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`);
const [chatReportPolicyTagList] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${chatReport?.policyID}`);

const [transactions] = useTransactionsByID([...selectedIds]);

const handleConfirm = () => {
if (selectedIds.size === 0) {
setErrorMessage(translate('iou.selectExistingExpense'));
Expand All @@ -71,7 +63,7 @@ function AddExistingExpenseFooter({selectedIds, report, reportToConfirm, reportN
afterTransition: () => {
if (report && isIOUReport(report)) {
convertBulkTrackedExpensesToIOU({
transactions: Object.values(selectedTransactions),
transactions,
iouReport: report,
chatReport,
isASAPSubmitBetaEnabled,
Expand All @@ -94,8 +86,8 @@ function AddExistingExpenseFooter({selectedIds, report, reportToConfirm, reportN
policy,
reportNextStep,
policyCategories,
allTransactions: selectedTransactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useTransactionsByID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import {useCallback, useMemo} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Transaction} from '@src/types/onyx';
import getEmptyArray from '@src/types/utils/getEmptyArray';
import useOnyx from './useOnyx';

function useTransactionsByID(transactionIDs: string[] | undefined) {
const transactionIDsKey = transactionIDs?.join('|') ?? '';
const stableTransactionIDs = useMemo(() => (transactionIDsKey ? transactionIDsKey.split('|') : []), [transactionIDsKey]);

const transactionsSelector = useCallback(
(transactions: OnyxCollection<Transaction>) => stableTransactionIDs.map((id) => transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]),
(transactions: OnyxCollection<Transaction>) =>
stableTransactionIDs.map((id) => transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((transaction): transaction is Transaction => !!transaction),
[stableTransactionIDs],
);

const [transactions] = useOnyx(
const [transactions = getEmptyArray<Transaction>()] = useOnyx(
ONYXKEYS.COLLECTION.TRANSACTION,
{
selector: transactionsSelector,
Expand Down
3 changes: 1 addition & 2 deletions src/hooks/useUndeleteTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ function useUndeleteTransactions() {

return (transactions: Transaction[]) => {
const transactionIDs = transactions.map((transaction) => transaction.transactionID);
const allTransactions = Object.fromEntries(transactions.map((transaction) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction]));

changeTransactionsReport({
transactionIDs,
isASAPSubmitBetaEnabled,
accountID: currentUserPersonalDetails.accountID ?? CONST.DEFAULT_NUMBER_ID,
email: currentUserPersonalDetails.email ?? '',
policy,
allTransactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand Down
21 changes: 7 additions & 14 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,6 @@ Onyx.connect({
},
});

let allTransactionViolations: TransactionViolations = [];
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
callback: (val) => (allTransactionViolations = val ?? []),
});

type SaveWaypointProps = {
transactionID: string;
index: string;
Expand Down Expand Up @@ -836,8 +830,8 @@ type ChangeTransactionsReportProps = {
policy: OnyxEntry<Policy>;
reportNextStep?: OnyxEntry<ReportNextStepDeprecated>;
policyCategories?: OnyxEntry<PolicyCategories>;
allTransactions: OnyxCollection<Transaction>;
policyTagList: OnyxEntry<PolicyTagLists>;
transactions: Transaction[];
allTransactionViolation?: OnyxCollection<TransactionViolation[]>;
allReports: OnyxCollection<Report>;
};
Expand All @@ -851,15 +845,14 @@ function changeTransactionsReport({
policy,
reportNextStep,
policyCategories,
allTransactions,
policyTagList,
transactions,
allTransactionViolation = {},
allReports: allReportsParam,
}: ChangeTransactionsReportProps) {
const reports = allReportsParam ?? allReports;
const reportID = newReport?.reportID ?? CONST.REPORT.UNREPORTED_REPORT_ID;

const transactions = transactionIDs.map((id) => allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${id}`]).filter((t): t is NonNullable<typeof t> => t !== undefined);
const transactionIDToReportActionAndThreadData: Record<string, TransactionThreadInfo> = {};
const updatedReportTotals: Record<string, number> = {};
const updatedReportTransactionCounts: Record<string, number> = {};
Expand All @@ -871,7 +864,6 @@ function changeTransactionsReport({
const optimisticPendingFieldsByReport: Record<string, Partial<NonNullable<Report['pendingFields']>>> = {};
const targetReportCurrenciesByReport: Record<string, Set<string>> = {};

// Store current violations for each transaction to restore on failure
const currentTransactionViolations: Record<string, TransactionViolation[]> = {};
for (const id of transactionIDs) {
currentTransactionViolations[id] = allTransactionViolation?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
Expand Down Expand Up @@ -1155,14 +1147,15 @@ function changeTransactionsReport({

// Optimistically clear all violations for the transaction when moving to self DM report
if (isUnreported) {
const duplicateViolation = currentTransactionViolations?.[transaction.transactionID]?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION);
const duplicateTransactionIDs = duplicateViolation?.data?.duplicates;
const duplicateTransactionIDs = currentTransactionViolations[transaction.transactionID]?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data
?.duplicates;
if (duplicateTransactionIDs) {
for (const id of duplicateTransactionIDs) {
const siblingViolations = allTransactionViolation?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`] ?? [];
optimisticData.push({
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${id}`,
value: allTransactionViolations.filter((violation: TransactionViolation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION),
value: siblingViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.DUPLICATED_TRANSACTION),
});
}
}
Expand Down Expand Up @@ -1303,7 +1296,7 @@ function changeTransactionsReport({
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
value: allTransactionViolation?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`] ?? null,
value: currentTransactionViolations[transaction.transactionID] ?? null,
});
if (Array.isArray(violationData.value) && hasSubmissionBlockingViolationInList(violationData.value)) {
shouldFixViolations = true;
Expand Down
20 changes: 8 additions & 12 deletions src/pages/DynamicNewReportWorkspaceSelectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionsByID from '@hooks/useTransactionsByID';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import setNavigationActionToMicrotaskQueue from '@libs/Navigation/helpers/setNavigationActionToMicrotaskQueue';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -72,11 +73,14 @@ function DynamicNewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelec
const [userBillingGracePeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END);
const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
const [policies, fetchStatus] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION);
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS);
const currentUserPersonalDetails = useCurrentUserPersonalDetails();

const selectedTransactionsKeys = Object.keys(selectedTransactions);
const transactionIDs = selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs;
const [transactions] = useTransactionsByID(transactionIDs);

const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const shouldShowLoadingIndicator = isLoadingApp && !isOffline;
const [pendingPolicySelection, setPendingPolicySelection] = useState<{policy: WorkspaceListItem; shouldShowEmptyReportConfirmation: boolean} | null>(null);
Expand Down Expand Up @@ -109,23 +113,22 @@ function DynamicNewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelec

const createReport = (policyID: string, shouldDismissEmptyReportsConfirmation?: boolean) => {
const optimisticReport = createNewReport(policyID, shouldDismissEmptyReportsConfirmation);
const selectedTransactionsKeys = Object.keys(selectedTransactions);

if (isMovingExpenses && (!!selectedTransactionsKeys.length || !!selectedTransactionIDs.length)) {
const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${optimisticReport.reportID}`];
const policyTagList = policyID ? allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] : {};
setNavigationActionToMicrotaskQueue(() => {
changeTransactionsReport({
transactionIDs: selectedTransactionsKeys.length ? selectedTransactionsKeys : selectedTransactionIDs,
transactionIDs,
isASAPSubmitBetaEnabled,
accountID: currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID,
email: currentUserPersonalDetails?.email ?? '',
newReport: optimisticReport,
policy: policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`],
reportNextStep,
policyCategories: undefined,
allTransactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand Down Expand Up @@ -199,14 +202,7 @@ function DynamicNewReportWorkspaceSelectionPage({route}: NewReportWorkspaceSelec
});
};

const hasPerDiemTransactions =
selectedTransactionIDs &&
selectedTransactionIDs.length > 0 &&
allTransactions &&
selectedTransactionIDs.some((transactionID) => {
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
return transaction && isPerDiemRequest(transaction);
});
const hasPerDiemTransactions = transactions.length > 0 && transactions.some(isPerDiemRequest);

let usersWorkspaces: WorkspaceListItem[] = [];
if (policies && !isEmptyObject(policies)) {
Expand Down
23 changes: 7 additions & 16 deletions src/pages/Search/SearchTransactionsChangeReport.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, {useEffect, useMemo} from 'react';
// eslint-disable-next-line no-restricted-imports
import {InteractionManager} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider';
import {useSearchResultsContext, useSearchSelectionActions, useSearchSelectionContext} from '@components/Search/SearchContext';
Expand Down Expand Up @@ -34,18 +33,10 @@ function SearchTransactionsChangeReport() {
const {clearSelectedTransactions} = useSearchSelectionActions();
const {currentSearchResults} = useSearchResultsContext();
const selectedTransactionsKeys = useMemo(() => Object.keys(selectedTransactions), [selectedTransactions]);
const transactions = useMemo(
() =>
Object.values(selectedTransactions).reduce(
(transactionsCollection, transactionItem) => {
// eslint-disable-next-line no-param-reassign
transactionsCollection[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transaction?.transactionID}`] = transactionItem.transaction;
return transactionsCollection;
},
{} as NonNullable<OnyxCollection<Transaction>>,
),
[selectedTransactions],
);
// Search-selected transactions are not in COLLECTION.TRANSACTION — extract from `selectedTransactions` directly.
const transactions = Object.values(selectedTransactions)
.map((transactionItem) => transactionItem.transaction)
.filter((transaction): transaction is Transaction => !!transaction);
const [allReportNextSteps] = useOnyx(ONYXKEYS.COLLECTION.NEXT_STEP);
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
Expand Down Expand Up @@ -161,8 +152,8 @@ function SearchTransactionsChangeReport() {
policy: policyForMovingExpenses,
reportNextStep,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyForMovingExpensesID}`],
allTransactions: transactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand Down Expand Up @@ -243,8 +234,8 @@ function SearchTransactionsChangeReport() {
policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`],
reportNextStep,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${item.policyID}`],
allTransactions: transactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand All @@ -266,8 +257,8 @@ function SearchTransactionsChangeReport() {
accountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
email: session?.email ?? '',
policy: allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${personalPolicyID}`],
allTransactions: transactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand Down
25 changes: 6 additions & 19 deletions src/pages/iou/request/step/DynamicIOURequestStepUpgradePage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {hasSeenTourSelector} from '@selectors/Onboarding';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import type {OnyxCollection} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
Expand Down Expand Up @@ -94,22 +93,10 @@ function DynamicIOURequestStepUpgradePage({
const [allPolicyTags] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS);
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);

// Build transactions map from selectedTransactions (search results) instead of Onyx TRANSACTION collection
// This ensures that transactions selected from search are properly included in the map passed to changeTransactionsReport
const allTransactions = useMemo(
() =>
Object.values(selectedTransactions).reduce(
(transactionsCollection, transactionItem) => {
if (transactionItem.transaction) {
// eslint-disable-next-line no-param-reassign
transactionsCollection[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionItem.transaction.transactionID}`] = transactionItem.transaction;
}
return transactionsCollection;
},
{} as NonNullable<OnyxCollection<Transaction>>,
),
[selectedTransactions],
);
// Search-selected transactions are not in COLLECTION.TRANSACTION — extract from `selectedTransactions` directly.
const transactions = Object.values(selectedTransactions)
.map((transactionItem) => transactionItem.transaction)
.filter((item): item is Transaction => !!item);

const {isBetaEnabled} = usePermissions();
const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT);
Expand Down Expand Up @@ -153,8 +140,8 @@ function DynamicIOURequestStepUpgradePage({
policy: newPolicy,
reportNextStep,
policyCategories: allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`],
allTransactions,
policyTagList,
transactions,
allTransactionViolation: transactionViolations,
allReports,
});
Expand Down Expand Up @@ -247,7 +234,7 @@ function DynamicIOURequestStepUpgradePage({
session?.accountID,
session?.email,
ownerPersonalDetails,
allTransactions,
transactions,
betas,
iouType,
isTrack,
Expand Down
Loading
Loading