diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx index 792122cc11a9..3193f0e8d29f 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/MoneyRequestReportPreviewContent.tsx @@ -57,7 +57,7 @@ import {startSpan} from '@libs/telemetry/activeSpans'; import {getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import {compareByRBR} from '@libs/TransactionPreviewUtils'; -import {hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils'; +import {getCreated, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils'; import colors from '@styles/theme/colors'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -160,7 +160,7 @@ function MoneyRequestReportPreviewContent({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const currentUserDetails = useCurrentUserPersonalDetails(); - const {translate, formatPhoneNumber} = useLocalize(); + const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const {convertToDisplayString} = useCurrencyListActions(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); @@ -357,11 +357,16 @@ function MoneyRequestReportPreviewContent({ if (shouldShowAccessPlaceHolder) { return []; } - const sorted = [...transactions].sort((a, b) => - compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy), - ); + const sorted = [...transactions].sort((a, b) => { + const rbrComparison = compareByRBR(a, b, transactionViolations, currentUserDetails?.login ?? '', currentUserDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID, iouReport, policy); + if (rbrComparison !== 0) { + return rbrComparison; + } + // Tiebreak by date (ascending — oldest first) so position is stable across RBR state changes + return localeCompare(getCreated(a), getCreated(b)); + }); return sorted.slice(0, 11); - }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy]); + }, [shouldShowAccessPlaceHolder, transactions, transactionViolations, currentUserDetails?.login, currentUserDetails?.accountID, iouReport, policy, localeCompare]); const prevCarouselTransactionLength = useRef(0); useEffect(() => { diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index c41e65cd11f3..59c9dc9c5af0 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -429,7 +429,9 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T created = '', merchant = '', receipt, - category = '', + // Prevent RBR flip and transaction jump: initialize category to 'Uncategorized' instead of + // empty string so optimistic missing category violation isn't added then removed during backend sync + category = CONST.SEARCH.CATEGORY_DEFAULT_VALUE, tag = '', taxCode = '', taxAmount = 0,