diff --git a/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts b/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts index 51c047d84576..f6ca47493498 100644 --- a/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts +++ b/src/pages/home/RecentlyAddedSection/useRecentlyAddedData.ts @@ -83,23 +83,8 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} { const hash = queryJSON?.hash; const [searchResults] = useOnyx(`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`); - - // The Search snapshot omits each transaction's `inserted` timestamp, so recency ordering must come from the - // local `transactions_` collection, which carries `inserted` for expenses the user has recently added. const [localTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - // Maps transactionID -> local `inserted` timestamp, used as the recency key the snapshot can't provide. - const insertedByTransactionID = useMemo(() => { - const map = new Map(); - for (const [key, transaction] of Object.entries(localTransactions ?? {})) { - const transactionID = transaction?.transactionID ?? key.slice(ONYXKEYS.COLLECTION.TRANSACTION.length); - if (transaction?.inserted) { - map.set(transactionID, transaction.inserted); - } - } - return map; - }, [localTransactions]); - // Maps transactionID -> local `transactions_` copy. When present it carries the freshest optimistic state // (edited values and `pendingFields`) for an offline edit, which the server-backed snapshot doesn't yet reflect. const localTransactionByID = useMemo(() => { @@ -194,19 +179,15 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} { // only the splits. Prefer the local copy's reportID, which reflects the split even before the snapshot refreshes. .filter((transaction) => (localTransactionByID.get(transaction.transactionID)?.reportID ?? transaction.reportID) !== CONST.REPORT.SPLIT_REPORT_ID); - // Recency key: prefer the local `inserted` timestamp (full precision, present for recently added expenses), - // then any snapshot `inserted`, then fall back to the expense date. Newest first. - const getRecencyKey = (transaction: Transaction & {reportID: string}) => - insertedByTransactionID.get(transaction.transactionID) ?? transaction.inserted ?? getCreated(transaction) ?? ''; - + // Order by the transaction's `inserted` timestamp (the immutable insertion time), most recent first. return combined .sort((firstTransaction, secondTransaction) => { - const firstKey = getRecencyKey(firstTransaction); - const secondKey = getRecencyKey(secondTransaction); - if (firstKey === secondKey) { - return 0; + const firstInserted = firstTransaction.inserted ?? ''; + const secondInserted = secondTransaction.inserted ?? ''; + if (firstInserted !== secondInserted) { + return firstInserted < secondInserted ? 1 : -1; } - return firstKey < secondKey ? 1 : -1; + return firstTransaction.transactionID < secondTransaction.transactionID ? 1 : -1; }) .slice(0, CONST.HOME.SECTION_VISIBLE_LIMIT) .map((transaction) => { @@ -238,7 +219,7 @@ function useRecentlyAddedData(): {transactions: RecentlyAddedExpense[]} { transaction: sourceTransaction, }; }); - }, [snapshotData, accountID, insertedByTransactionID, localPendingTransactions, localTransactionByID, translate]); + }, [snapshotData, accountID, localPendingTransactions, localTransactionByID, translate]); return {transactions}; } diff --git a/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts b/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts index 35d688fe5494..28cd8fcc6e3b 100644 --- a/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts +++ b/tests/unit/HomePage/RecentlyAddedSection/useRecentlyAddedDataTest.ts @@ -4,7 +4,6 @@ * not the on-demand `transactions_` Onyx collection * - returns the current user's expenses, most recent first * - sorts strictly by the `inserted` (creation/insertion) timestamp, never by `created` (expense date); - * falls back to `created` only when `inserted` is missing * - caps the list at CONST.HOME.SECTION_VISIBLE_LIMIT (5) rows * - includes expenses regardless of report status (no recency-window / draft-only filter) * - defensively excludes expenses owned by another account when the snapshot carries the parent report @@ -140,19 +139,21 @@ describe('useRecentlyAddedData — ordering', () => { expect(resultTransactionIDs(result.current.transactions)).toEqual(['t3', 't2', 't1']); }); - it('falls back to the created date for ordering when an expense is missing its inserted timestamp', () => { + it('breaks ties between equal inserted timestamps deterministically by transactionID', () => { setupSnapshot( [ - makeTransaction({transactionID: 'withCreatedOnly', created: '2026-06-05', inserted: undefined}), - makeTransaction({transactionID: 'withInserted', created: '2026-01-01', inserted: '2026-06-04 10:00:00'}), + makeTransaction({transactionID: 'aaa', created: '2026-06-01', inserted: '2026-06-05 10:00:00'}), + makeTransaction({transactionID: 'ccc', created: '2026-06-09', inserted: '2026-06-05 10:00:00'}), + makeTransaction({transactionID: 'bbb', created: '2026-06-05', inserted: '2026-06-05 10:00:00'}), ], [makeReport('report_owned', ACCOUNT_ID)], ); const {result} = renderHook(() => useRecentlyAddedData()); - // withCreatedOnly has no inserted, so its created date (2026-06-05) is used and outranks withInserted (2026-06-04). - expect(resultTransactionIDs(result.current.transactions)).toEqual(['withCreatedOnly', 'withInserted']); + // Equal `inserted` timestamps tie-break on transactionID (descending) rather than the differing created dates, + // giving a stable order that never silently reshuffles across renders. + expect(resultTransactionIDs(result.current.transactions)).toEqual(['ccc', 'bbb', 'aaa']); }); it('ranks an old-dated expense first when it was inserted most recently', () => {