Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import type {TupleToUnion} from 'type-fest';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import Checkbox from '@components/Checkbox';
import * as Expensicons from '@components/Icon/Expensicons';

Check warning on line 8 in src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Expensicons' import is restricted from being used by a pattern. Direct imports from Icon/Expensicons are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyExpensifyIcons` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details

Check warning on line 8 in src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'@components/Icon/Expensicons' import is restricted from being used. Direct imports from @components/Icon/Expensicons are deprecated. Please use lazy loading hooks instead. Use `useMemoizedLazyExpensifyIcons` from @hooks/useLazyAsset. See docs/LAZY_ICONS_AND_ILLUSTRATIONS.md for details
import MenuItem from '@components/MenuItem';
import Modal from '@components/Modal';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
Expand Down Expand Up @@ -272,8 +272,7 @@
} as ReportScreenNavigationProps;

if (!reportIDToNavigate) {
const transaction = sortedTransactions.find((t) => t.transactionID === activeTransactionID);
const transactionThreadReport = createTransactionThreadReport(report, iouAction, transaction);
const transactionThreadReport = createTransactionThreadReport(report, iouAction);
if (transactionThreadReport) {
reportIDToNavigate = transactionThreadReport.reportID;
routeParams.reportID = reportIDToNavigate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe

const {transactions: reportTransactions, violations: allReportViolations} = useTransactionsAndViolationsForReport(reportID);
const hasPendingDeletionTransaction = Object.values(reportTransactions ?? {}).some((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const transactions = useMemo(() => getAllNonDeletedTransactions(reportTransactions, reportActions, isOffline, true), [reportTransactions, reportActions, isOffline]);
const transactions = useMemo(() => getAllNonDeletedTransactions(reportTransactions, reportActions), [reportTransactions, reportActions]);

const visibleTransactions = transactions?.filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const reportTransactionIDs = visibleTransactions?.map((transaction) => transaction.transactionID);
Expand Down
5 changes: 1 addition & 4 deletions src/libs/MoneyRequestReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function getReportIDForTransaction(transactionItem: TransactionListItemType) {
/**
* Filters all available transactions and returns the ones that belong to not removed action and not removed parent action.
*/
function getAllNonDeletedTransactions(transactions: OnyxCollection<Transaction>, reportActions: ReportAction[], isOffline = false, includeOrphanedTransactions = false) {
function getAllNonDeletedTransactions(transactions: OnyxCollection<Transaction>, reportActions: ReportAction[], isOffline = false) {
return Object.values(transactions ?? {}).filter((transaction): transaction is Transaction => {
if (!transaction) {
return false;
Expand All @@ -82,9 +82,6 @@ function getAllNonDeletedTransactions(transactions: OnyxCollection<Transaction>,
}

const action = getIOUActionForTransactionID(reportActions, transaction.transactionID);
if (!action && includeOrphanedTransactions) {
return true;
}
if (action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isOffline) {
return true;
}
Expand Down
67 changes: 6 additions & 61 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1371,69 +1371,14 @@ function getTaskSections(
);
}

/**
* @private
*
* Extracts the core Transaction fields from a TransactionListItemType
* This removes UI-specific fields like formattedFrom, hash, violations, etc.
*/
function getTransactionFromTransactionListItem(item: TransactionListItemType): OnyxTypes.Transaction {
// Extract only the core Transaction fields, excluding UI-specific and search-specific fields
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const {
// Remove UI-specific fields
keyForList,
action,
allActions,
report,
from,
to,
formattedFrom,
formattedTo,
formattedTotal,
formattedMerchant,
date,
shouldShowMerchant,
shouldShowYear: shouldTransactionShowYear,
isAmountColumnWide,
isTaxAmountColumnWide,
violations,
hash,
moneyRequestReportActionID,
canDelete,
convertedAmount,
convertedCurrency,
transactionThreadReportID,
isFromOneTransactionReport,
accountID,
policyID,
transactionType,
// Keep all other fields (core Transaction fields)
...transaction
} = item;

return transaction as OnyxTypes.Transaction;
}

/** Creates transaction thread report and navigates to it from the search page */
function createAndOpenSearchTransactionThread(item: TransactionListItemType, hash: number, backTo: string, transactionPreviewData?: TransactionPreviewData, shouldNavigate = true) {
// Treat '0' as empty for reportActionID (0 means no IOU action exists in the backend)
const reportActionID = item.moneyRequestReportActionID === '0' ? '' : item.moneyRequestReportActionID;

// If the IOU action exists in the backend, populate Onyx with data from the search snapshot
// This shows the transaction thread immediately while waiting for OpenReport to return the real data
if (reportActionID) {
const previewData = transactionPreviewData
? {...transactionPreviewData, hasTransactionThreadReport: true}
: {hasTransaction: false, hasParentReport: false, hasParentReportAction: false, hasTransactionThreadReport: true};
setOptimisticDataForTransactionThreadPreview(item, previewData);
}

// For legacy transactions without an IOU action in the backend, pass transaction data
// This allows OpenReport to create the IOU action and transaction thread on the backend
const transaction = !reportActionID ? getTransactionFromTransactionListItem(item) : undefined;
const transactionViolations = !reportActionID ? item.violations : undefined;
const transactionThreadReport = createTransactionThreadReport(item.report, {reportActionID} as OnyxTypes.ReportAction, transaction, transactionViolations);
const previewData = transactionPreviewData
? {...transactionPreviewData, hasTransactionThreadReport: true}
: {hasTransaction: false, hasParentReport: false, hasParentReportAction: false, hasTransactionThreadReport: true};
setOptimisticDataForTransactionThreadPreview(item, previewData);

const transactionThreadReport = createTransactionThreadReport(item.report, {reportActionID: item.moneyRequestReportActionID} as OnyxTypes.ReportAction);
if (transactionThreadReport?.reportID) {
updateSearchResultsWithTransactionThreadReportID(hash, item.transactionID, transactionThreadReport?.reportID);
}
Expand Down
175 changes: 55 additions & 120 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@
let currentUserAccountID = -1;
let currentUserEmail: string | undefined;

Onyx.connect({

Check warning on line 271 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -281,13 +281,13 @@
},
});

Onyx.connect({

Check warning on line 284 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => (conciergeReportID = value),
});

let preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE;
Onyx.connect({

Check warning on line 290 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
callback: (value) => {
preferredSkinTone = EmojiUtils.getPreferredSkinToneIndex(value);
Expand All @@ -297,7 +297,7 @@
// map of reportID to all reportActions for that report
const allReportActions: OnyxCollection<ReportActions> = {};

Onyx.connect({

Check warning on line 300 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
if (!key || !actions) {
Expand All @@ -309,14 +309,14 @@
});

let allTransactionViolations: OnyxCollection<TransactionViolations> = {};
Onyx.connect({

Check warning on line 312 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => (allTransactionViolations = value),
});

let allReports: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 319 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -325,7 +325,7 @@
});

let isNetworkOffline = false;
Onyx.connect({

Check warning on line 328 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NETWORK,
callback: (value) => {
isNetworkOffline = value?.isOffline ?? false;
Expand All @@ -333,7 +333,7 @@
});

let allPersonalDetails: OnyxEntry<PersonalDetailsList> = {};
Onyx.connect({

Check warning on line 336 in src/libs/actions/Report.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -974,13 +974,7 @@
* @param parentReportActionID The parent report action that a thread was created from (only passed for new threads)
* @param isFromDeepLink Whether or not this report is being opened from a deep link
* @param participantAccountIDList The list of accountIDs that are included in a new chat, not including the user creating it
* @param avatar The avatar file to upload for a new group chat
* @param isNewThread Whether this is a new thread being created
* @param transaction The transaction object for legacy transactions that don't have a transaction thread or money request preview yet
* @param transactionViolations The violations for the transaction, if any
* @param parentReportID The parent report ID for the transaction thread (optional, defaults to transaction.reportID)
*/
// eslint-disable-next-line @typescript-eslint/max-params
function openReport(
reportID: string | undefined,
reportActionID?: string,
Expand All @@ -990,10 +984,8 @@
isFromDeepLink = false,
participantAccountIDList: number[] = [],
avatar?: File | CustomRNImageManipulatorResult,
transactionID?: string,
isNewThread = false,
transaction?: Transaction,
transactionViolations?: TransactionViolations,
parentReportID?: string,
) {
if (!reportID) {
return;
Expand Down Expand Up @@ -1074,92 +1066,59 @@
emailList: participantLoginList ? participantLoginList.join(',') : '',
accountIDList: participantAccountIDList ? participantAccountIDList.join(',') : '',
parentReportActionID,
transactionID: transaction?.transactionID,
transactionID,
};

// This is a legacy transaction that doesn't have either a transaction thread or a money request preview
if (transaction && !parentReportActionID) {
const transactionParentReportID = parentReportID ?? transaction?.reportID;
const iouReportActionID = rand64();

const optimisticIOUAction = buildOptimisticIOUReportAction({
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
amount: Math.abs(transaction.amount),
currency: transaction.currency,
comment: transaction.comment?.comment ?? '',
participants: [{accountID: currentUserAccountID, login: currentUserEmail ?? ''}],
transactionID: transaction.transactionID,
isOwnPolicyExpenseChat: true,
reportActionID: iouReportActionID,
iouReportID: transactionParentReportID,
});

// We have a case where the transaction data is only available from the snapshot
// So we need to add the transaction data to Onyx so it's available when opening the report
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: transaction,
});

// Add violations if they exist. This is needed when opening from search results where
// violations are in the snapshot but not yet synced to the main collections, so we need
// to add them to Onyx to ensure they show up in the transaction thread
if (transactionViolations) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
value: transactionViolations,
});
}
// This is a legacy transactions that doesn't have either a transaction thread or a money request preview
if (transactionID && !parentReportActionID) {
const transaction = allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];

if (transaction) {
const selfDMReportID = findSelfDMReportID();

if (selfDMReportID) {
const generatedReportActionID = rand64();
const optimisticParentAction = buildOptimisticIOUReportAction({
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
amount: Math.abs(transaction.amount),
currency: transaction.currency,
comment: transaction.comment?.comment ?? '',
participants: [{accountID: currentUserAccountID, login: currentUserEmail ?? ''}],
transactionID,
isOwnPolicyExpenseChat: true,
});

// Attach the optimistic IOU report action created for the transaction to the transaction thread
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
parentReportActionID: iouReportActionID,
},
});
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: {
parentReportID: selfDMReportID,
parentReportActionID: generatedReportActionID,
},
});

optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionParentReportID}`,
value: {
[iouReportActionID]: {
...optimisticIOUAction,
childReportID: reportID,
},
},
});
// Log how often the legacy transaction fallback path is taken
Log.info('[Report] Legacy transaction fallback: creating money request preview in self DM', false, {
selfDMReportID,
transactionID,
reportID,
});

// Update the snapshot with the new transactionThreadReportID and moneyRequestReportActionID if we're coming from search
// preventing duplicate reportActionID when moneyRequestReportActionID still empty
const currentSearchQueryJSON = getCurrentSearchQueryJSON();
if (currentSearchQueryJSON?.hash) {
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`,
value: {
data: {
[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: {
transactionThreadReportID: reportID,
moneyRequestReportActionID: iouReportActionID,
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${selfDMReportID}`,
value: {
[generatedReportActionID]: {
...optimisticParentAction,
reportActionID: generatedReportActionID,
childReportID: reportID,
},
},
},
});
}

parameters.moneyRequestPreviewReportActionID = iouReportActionID;
});

// Log how often the legacy transaction fallback path is taken
Log.info('[Report] Legacy transaction fallback: creating money request preview', false, {
transactionParentReportID,
transactionID: transaction.transactionID,
reportID,
});
parameters.moneyRequestPreviewReportActionID = generatedReportActionID;
}
}
}

const isInviteOnboardingComplete = introSelected?.isInviteOnboardingComplete ?? false;
Expand Down Expand Up @@ -1396,20 +1355,16 @@
});
}

function createTransactionThreadReport(
iouReport?: OnyxEntry<Report>,
iouReportAction?: OnyxEntry<ReportAction>,
transaction?: Transaction,
transactionViolations?: TransactionViolations,
): OptimisticChatReport | undefined {
// Determine if we need selfDM report (for track expenses or unreported transactions)
const isTrackExpense = !iouReport && ReportActionsUtils.isTrackExpenseAction(iouReportAction);
const isUnreportedTransaction = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID;
const selfDMReportID = isTrackExpense || isUnreportedTransaction ? findSelfDMReportID() : undefined;
function createTransactionThreadReport(iouReport: OnyxEntry<Report>, iouReportAction: OnyxEntry<ReportAction>): OptimisticChatReport | undefined {
if (!iouReportAction) {
Log.warn('Cannot build transaction thread report without iouReportAction parameter');
return;
}

let reportToUse = iouReport;
// For track expenses without iouReport, get the selfDM report
if (isTrackExpense && selfDMReportID) {
if (!iouReport && ReportActionsUtils.isTrackExpenseAction(iouReportAction)) {
const selfDMReportID = findSelfDMReportID();
reportToUse = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`];
}

Expand All @@ -1418,29 +1373,9 @@
return;
}

// Sync fresh report data from snapshot to allReports. When navigating from search,
// allReports may be stale and missing parentReportID/parentReportActionID, causing
// getAllAncestorReportActionIDs() to fail when adding comments optimistically.
if (iouReport?.reportID && reportToUse.reportID === iouReport.reportID) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport);
}

const optimisticTransactionThreadReportID = generateReportID();
const optimisticTransactionThread = buildTransactionThread(iouReportAction, reportToUse, undefined, optimisticTransactionThreadReportID);
openReport(
optimisticTransactionThreadReportID,
undefined,
currentUserEmail ? [currentUserEmail] : [],
optimisticTransactionThread,
iouReportAction?.reportActionID,
false,
[],
undefined,
false,
transaction,
transactionViolations,
selfDMReportID,
);
openReport(optimisticTransactionThreadReportID, undefined, currentUserEmail ? [currentUserEmail] : [], optimisticTransactionThread, iouReportAction?.reportActionID);
return optimisticTransactionThread;
}

Expand Down Expand Up @@ -1555,7 +1490,7 @@

if (!childReportID) {
const participantLogins = PersonalDetailsUtils.getLoginsByAccountIDs(Object.keys(newChat.participants ?? {}).map(Number));
openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID, undefined, undefined, undefined, true);
openReport(newChat.reportID, '', participantLogins, newChat, parentReportAction.reportActionID, undefined, undefined, undefined, undefined, true);
} else {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`, newChat);
}
Expand Down
7 changes: 3 additions & 4 deletions src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const {transactions: allReportTransactions, violations: allReportViolations} = useTransactionsAndViolationsForReport(reportIDFromRoute);
const hasPendingDeletionTransaction = Object.values(allReportTransactions ?? {}).some((transaction) => transaction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);

const reportTransactions = useMemo(() => getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true), [allReportTransactions, reportActions, isOffline]);
const reportTransactions = useMemo(() => getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline), [allReportTransactions, reportActions, isOffline]);
// wrapping in useMemo because this is array operation and can cause performance issues
const visibleTransactions = useMemo(
() => reportTransactions?.filter((transaction) => isOffline || transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE),
Expand Down Expand Up @@ -507,7 +507,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
const currentReportTransaction = getReportTransactions(reportID).filter((transaction) => transaction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE);
const oneTransactionID = currentReportTransaction.at(0)?.transactionID;
const iouAction = getIOUActionForReportID(reportID, oneTransactionID);
createTransactionThreadReport(report, iouAction, currentReportTransaction.at(0));
createTransactionThreadReport(report, iouAction);
}, [report, reportID]);

const isInviteOnboardingComplete = introSelected?.isInviteOnboardingComplete ?? false;
Expand All @@ -523,7 +523,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
}

// If there is one transaction thread that has not yet been created, we should create it.
if ((transactionThreadReportID === CONST.FAKE_REPORT_ID && !transactionThreadReport) || (visibleTransactions.length === 1 && !transactionThreadReportID)) {
if (transactionThreadReportID === CONST.FAKE_REPORT_ID && !transactionThreadReport) {
createOneTransactionThreadReport();
return;
}
Expand All @@ -549,7 +549,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) {
isOffline,
transactionThreadReportID,
transactionThreadReport,
visibleTransactions.length,
reportIDFromRoute,
reportActionIDFromRoute,
createOneTransactionThreadReport,
Expand Down
Loading
Loading