From 0700258610b32ea45dfb4c1920b758420e0478d6 Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Thu, 28 May 2026 17:01:05 +0200
Subject: [PATCH 01/12] Add review expenses row to ForYouSection

---
 src/ONYXKEYS.ts                               |   2 +
 src/languages/en.ts                           |   1 +
 .../OnyxDerived/ONYX_DERIVED_VALUES.ts        |   2 +
 .../OnyxDerived/configs/flaggedExpenses.ts    |  87 ++++++
 src/pages/home/ForYouSection/index.tsx        |  62 +++-
 src/selectors/Todos.ts                        |  47 +++-
 src/types/onyx/DerivedValues.ts               |  18 ++
 src/types/onyx/index.ts                       |   2 +
 tests/ui/ForYouSectionTest.tsx                | 193 ++++++++++++-
 tests/unit/OnyxDerived/flaggedExpensesTest.ts | 264 ++++++++++++++++++
 tests/unit/selectors/TodosTest.ts             |  70 ++++-
 11 files changed, 733 insertions(+), 15 deletions(-)
 create mode 100644 src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
 create mode 100644 tests/unit/OnyxDerived/flaggedExpensesTest.ts

diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 8e30de4e3f7b..9ac7bb02a226 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -1159,6 +1159,7 @@ const ONYXKEYS = {
         TODOS: 'todos',
         RAM_ONLY_SORTED_REPORT_ACTIONS: 'sortedReportActions',
         OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID: 'openAndSubmittedReportsByPolicyID',
+        FLAGGED_EXPENSES: 'flaggedExpenses',
     },
 
     /** Stores HybridApp specific state required to interoperate with OldDot */
@@ -1637,6 +1638,7 @@ type OnyxDerivedValuesMapping = {
     [ONYXKEYS.DERIVED.TODOS]: OnyxTypes.TodosDerivedValue;
     [ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: OnyxTypes.SortedReportActionsDerivedValue;
     [ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID]: OnyxTypes.OpenAndSubmittedReportsByPolicyIDDerivedValue;
+    [ONYXKEYS.DERIVED.FLAGGED_EXPENSES]: OnyxTypes.FlaggedExpensesDerivedValue;
 };
 
 type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 88e0bbe8b673..f30a79060920 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1037,6 +1037,7 @@ const translations = {
             menuItemDescription: 'See what Expensify can do in 2 min',
         },
         forYouSection: {
+            reviewExpenses: ({count}: {count: number}) => `Review ${count} ${count === 1 ? 'expense' : 'expenses'}`,
             submit: ({count}: {count: number}) => `Submit ${count} ${count === 1 ? 'report' : 'reports'}`,
             approve: ({count}: {count: number}) => `Approve ${count} ${count === 1 ? 'report' : 'reports'}`,
             pay: ({count}: {count: number}) => `Pay ${count} ${count === 1 ? 'report' : 'reports'}`,
diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
index 161c7f72ecf8..63d8bfd1478c 100644
--- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
+++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
@@ -1,6 +1,7 @@
 import type {ValueOf} from 'type-fest';
 import ONYXKEYS from '@src/ONYXKEYS';
 import cardFeedErrorsConfig from './configs/cardFeedErrors';
+import flaggedExpensesConfig from './configs/flaggedExpenses';
 import nonPersonalAndWorkspaceCardListConfig from './configs/nonPersonalAndWorkspaceCardList';
 import openAndSubmittedReportsByPolicyIDConfig from './configs/openAndSubmittedReportsByPolicyID';
 import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID';
@@ -27,6 +28,7 @@ const ONYX_DERIVED_VALUES = {
     [ONYXKEYS.DERIVED.TODOS]: todosConfig,
     [ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: sortedReportActionsConfig,
     [ONYXKEYS.DERIVED.OPEN_AND_SUBMITTED_REPORTS_BY_POLICY_ID]: openAndSubmittedReportsByPolicyIDConfig,
+    [ONYXKEYS.DERIVED.FLAGGED_EXPENSES]: flaggedExpensesConfig,
 } as const satisfies {
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     [Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
diff --git a/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts b/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
new file mode 100644
index 000000000000..e555f50f3107
--- /dev/null
+++ b/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
@@ -0,0 +1,87 @@
+import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {FlaggedExpensesDerivedValue} from '@src/types/onyx';
+import type Report from '@src/types/onyx/Report';
+import type TransactionViolations from '@src/types/onyx/TransactionViolation';
+
+type FlaggedExpenseEntry = FlaggedExpensesDerivedValue['flaggedExpenses'][number];
+
+const EMPTY_VALUE: FlaggedExpensesDerivedValue = {flaggedExpenses: []};
+
+/**
+ * Returns true when this report is an OPEN/OPEN expense report owned by the current user.
+ *
+ * `currentUserAccountID` is required. Callers should pass `session?.accountID ?? CONST.DEFAULT_NUMBER_ID`
+ * so that the ownership check fails closed when the session is not yet populated (no real ownerAccountID is 0).
+ */
+function isCurrentUserOpenExpenseReport(report: Report | null | undefined, currentUserAccountID: number): boolean {
+    if (!report) {
+        return false;
+    }
+    if (report.type !== CONST.REPORT.TYPE.EXPENSE) {
+        return false;
+    }
+    if (report.ownerAccountID !== currentUserAccountID) {
+        return false;
+    }
+    return report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS_NUM.OPEN;
+}
+
+/**
+ * Returns true when the given violation list has at least one entry that should surface in the
+ * `Review X expenses` row: a transaction-level violation that the user can act on. Violations
+ * marked `showInReview === false` and report-field violations (`fieldRequired`) are excluded.
+ */
+function hasReviewableViolation(violations: TransactionViolations | null | undefined): boolean {
+    if (!violations || violations.length === 0) {
+        return false;
+    }
+
+    return violations.some((violation) => {
+        if (!violation) {
+            return false;
+        }
+        if (violation.showInReview === false) {
+            return false;
+        }
+        if (violation.name === CONST.REPORT_VIOLATIONS.FIELD_REQUIRED) {
+            return false;
+        }
+        return true;
+    });
+}
+
+export default createOnyxDerivedValueConfig({
+    key: ONYXKEYS.DERIVED.FLAGGED_EXPENSES,
+    dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.TRANSACTION, ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, ONYXKEYS.SESSION],
+    compute: ([allReports, allTransactions, allTransactionViolations, session]) => {
+        if (!allReports || !allTransactions) {
+            return EMPTY_VALUE;
+        }
+
+        const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID;
+        const flaggedExpenses: FlaggedExpenseEntry[] = [];
+
+        for (const transactionKey of Object.keys(allTransactions)) {
+            const transaction = allTransactions[transactionKey];
+            if (!transaction?.transactionID || !transaction.reportID) {
+                continue;
+            }
+
+            const report = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transaction.reportID}`];
+            if (!isCurrentUserOpenExpenseReport(report, currentUserAccountID)) {
+                continue;
+            }
+
+            const violations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`];
+            if (!hasReviewableViolation(violations)) {
+                continue;
+            }
+
+            flaggedExpenses.push({transactionID: transaction.transactionID, reportID: transaction.reportID});
+        }
+
+        return {flaggedExpenses};
+    },
+});
diff --git a/src/pages/home/ForYouSection/index.tsx b/src/pages/home/ForYouSection/index.tsx
index ac4074b2224c..f4f7d537ffd7 100644
--- a/src/pages/home/ForYouSection/index.tsx
+++ b/src/pages/home/ForYouSection/index.tsx
@@ -15,7 +15,7 @@ import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import {accountIDSelector} from '@src/selectors/Session';
-import todosReportCountsSelector, {EMPTY_TODOS_SINGLE_REPORT_IDS, todosSingleReportIDsSelector} from '@src/selectors/Todos';
+import todosReportCountsSelector, {EMPTY_FLAGGED_EXPENSES_REVIEW, EMPTY_TODOS_SINGLE_REPORT_IDS, flaggedExpensesReviewSelector, todosSingleReportIDsSelector} from '@src/selectors/Todos';
 import EmptyState from './EmptyState';
 import ForYouSkeleton from './ForYouSkeleton';
 
@@ -29,24 +29,33 @@ function ForYouSection() {
     const [isLoadingReportData = false] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
     const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector});
     const [singleReportIDs = EMPTY_TODOS_SINGLE_REPORT_IDS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosSingleReportIDsSelector});
+    const [flaggedExpensesReview = EMPTY_FLAGGED_EXPENSES_REVIEW] = useOnyx(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {selector: flaggedExpensesReviewSelector});
 
-    const icons = useMemoizedLazyExpensifyIcons(['MoneyBag', 'Send', 'ThumbsUp', 'Export']);
+    const icons = useMemoizedLazyExpensifyIcons(['Exclamation', 'MoneyBag', 'Send', 'ThumbsUp', 'Export']);
 
     const submitCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.SUBMIT] ?? 0;
     const approveCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.APPROVE] ?? 0;
     const payCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.PAY] ?? 0;
     const exportCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.EXPORT] ?? 0;
+    const flaggedExpensesCount = flaggedExpensesReview.count;
 
-    const hasAnyTodos = submitCount > 0 || approveCount > 0 || payCount > 0 || exportCount > 0;
+    const hasAnyTodos = flaggedExpensesCount > 0 || submitCount > 0 || approveCount > 0 || payCount > 0 || exportCount > 0;
+
+    const navigateToReport = useCallback(
+        (reportID: string) => {
+            if (shouldUseNarrowLayout) {
+                Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, undefined, undefined, ROUTES.HOME));
+                return;
+            }
+            Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID, backTo: ROUTES.HOME}));
+        },
+        [shouldUseNarrowLayout],
+    );
 
     const createNavigationHandler = useCallback(
         (action: string, queryParams: Record<string, unknown>, reportID?: string) => () => {
             if (reportID) {
-                if (shouldUseNarrowLayout) {
-                    Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, undefined, undefined, ROUTES.HOME));
-                } else {
-                    Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID, backTo: ROUTES.HOME}));
-                }
+                navigateToReport(reportID);
                 return;
             }
 
@@ -60,12 +69,29 @@ function ForYouSection() {
                 }),
             );
         },
-        [shouldUseNarrowLayout],
+        [navigateToReport],
+    );
+
+    const createReviewExpensesHandler = useCallback(
+        (firstReportID: string | undefined) => () => {
+            if (!firstReportID) {
+                return;
+            }
+            navigateToReport(firstReportID);
+        },
+        [navigateToReport],
     );
 
     const todoItems = useMemo(
         () =>
             [
+                {
+                    key: 'reviewExpenses',
+                    count: flaggedExpensesCount,
+                    icon: icons.Exclamation,
+                    translationKey: 'homePage.forYouSection.reviewExpenses' as const,
+                    handler: createReviewExpensesHandler(flaggedExpensesReview.firstReportID),
+                },
                 {
                     key: 'submit',
                     count: submitCount,
@@ -103,7 +129,23 @@ function ForYouSection() {
                     ),
                 },
             ].filter((item) => item.count > 0),
-        [accountID, approveCount, createNavigationHandler, exportCount, icons.Export, icons.MoneyBag, icons.Send, icons.ThumbsUp, payCount, singleReportIDs, submitCount],
+        [
+            accountID,
+            approveCount,
+            createNavigationHandler,
+            createReviewExpensesHandler,
+            exportCount,
+            flaggedExpensesCount,
+            flaggedExpensesReview.firstReportID,
+            icons.Exclamation,
+            icons.Export,
+            icons.MoneyBag,
+            icons.Send,
+            icons.ThumbsUp,
+            payCount,
+            singleReportIDs,
+            submitCount,
+        ],
     );
 
     const renderTodoItems = () => (
diff --git a/src/selectors/Todos.ts b/src/selectors/Todos.ts
index a1108233c703..84ff216de5c0 100644
--- a/src/selectors/Todos.ts
+++ b/src/selectors/Todos.ts
@@ -1,7 +1,7 @@
 import {shallowEqual} from 'fast-equals';
 import type {OnyxEntry} from 'react-native-onyx';
 import CONST from '@src/CONST';
-import type {TodosDerivedValue} from '@src/types/onyx';
+import type {FlaggedExpensesDerivedValue, TodosDerivedValue} from '@src/types/onyx';
 
 const EMPTY_TODOS_SINGLE_REPORT_IDS = Object.freeze({
     [CONST.SEARCH.SEARCH_KEYS.SUBMIT]: undefined,
@@ -66,5 +66,48 @@ const todosSingleReportIDsSelector = (todos: OnyxEntry<TodosDerivedValue>) => {
     return newValue;
 };
 
+type FlaggedExpensesReview = {
+    /** Total number of flagged expenses */
+    count: number;
+    /** Transaction ID of the first flagged expense, used to seed the carousel handoff */
+    firstTransactionID: string | undefined;
+    /** Parent report ID of the first flagged expense */
+    firstReportID: string | undefined;
+};
+
+const EMPTY_FLAGGED_EXPENSES_REVIEW: FlaggedExpensesReview = Object.freeze({
+    count: 0,
+    firstTransactionID: undefined,
+    firstReportID: undefined,
+}) as FlaggedExpensesReview;
+
+// Manual memoization mirrors `todosSingleReportIDsSelector`: ForYouSection's useMemo dependency
+// list consumes the returned object as a whole, so we need referential stability across
+// equivalent derived snapshots to avoid cascading re-renders on every Onyx churn.
+let previousFlaggedExpensesReview: FlaggedExpensesReview = EMPTY_FLAGGED_EXPENSES_REVIEW;
+
+const flaggedExpensesReviewSelector = (flaggedExpensesValue: OnyxEntry<FlaggedExpensesDerivedValue>): FlaggedExpensesReview => {
+    const flaggedExpenses = flaggedExpensesValue?.flaggedExpenses;
+    if (!flaggedExpenses || flaggedExpenses.length === 0) {
+        previousFlaggedExpensesReview = EMPTY_FLAGGED_EXPENSES_REVIEW;
+        return EMPTY_FLAGGED_EXPENSES_REVIEW;
+    }
+
+    const first = flaggedExpenses.at(0);
+    const newValue: FlaggedExpensesReview = {
+        count: flaggedExpenses.length,
+        firstTransactionID: first?.transactionID,
+        firstReportID: first?.reportID,
+    };
+
+    if (shallowEqual(previousFlaggedExpensesReview, newValue)) {
+        return previousFlaggedExpensesReview;
+    }
+
+    previousFlaggedExpensesReview = newValue;
+    return newValue;
+};
+
 export default todosReportCountsSelector;
-export {todosSingleReportIDsSelector, EMPTY_TODOS_SINGLE_REPORT_IDS};
+export {EMPTY_FLAGGED_EXPENSES_REVIEW, EMPTY_TODOS_SINGLE_REPORT_IDS, flaggedExpensesReviewSelector, todosSingleReportIDsSelector};
+export type {FlaggedExpensesReview};
diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts
index bbcecb4f4d6f..46128996dce1 100644
--- a/src/types/onyx/DerivedValues.ts
+++ b/src/types/onyx/DerivedValues.ts
@@ -266,6 +266,23 @@ type TodosDerivedValue = {
     transactionsByReportID: Record<string, Transaction[]>;
 };
 
+/**
+ * The derived value for flagged expenses.
+ *
+ * Aggregates transactions on the current user's `OPEN`/`OPEN` expense reports that have
+ * at least one transaction-level violation (excluding `showInReview === false` entries and
+ * `REPORT_VIOLATIONS.FIELD_REQUIRED` entries that may slip into the collection).
+ */
+type FlaggedExpensesDerivedValue = {
+    /** Ordered list of flagged transactions with their parent report IDs */
+    flaggedExpenses: Array<{
+        /** ID of the flagged transaction */
+        transactionID: string;
+        /** ID of the parent expense report */
+        reportID: string;
+    }>;
+};
+
 /**
  * The derived value for sorted report actions, last report actions, and cached transaction thread report IDs.
  */
@@ -297,6 +314,7 @@ export type {
     CardFeedErrorsDerivedValue,
     TodosDerivedValue,
     TodoMetadata,
+    FlaggedExpensesDerivedValue,
     CardFeedErrorsObject,
     CardFeedErrorState,
     CardFeedErrors,
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index 574568fa4d17..4346973a81e2 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -46,6 +46,7 @@ import type {CurrencyList} from './Currency';
 import type CustomStatusDraft from './CustomStatusDraft';
 import type {
     CardFeedErrorsDerivedValue,
+    FlaggedExpensesDerivedValue,
     NonPersonalAndWorkspaceCardListDerivedValue,
     OpenAndSubmittedReportsByPolicyIDDerivedValue,
     OutstandingReportsByPolicyIDDerivedValue,
@@ -395,6 +396,7 @@ export type {
     CardFeedErrorsDerivedValue,
     TodosDerivedValue,
     TodoMetadata,
+    FlaggedExpensesDerivedValue,
     ScheduleCallDraft,
     ValidateUserAndGetAccessiblePolicies,
     VacationDelegate,
diff --git a/tests/ui/ForYouSectionTest.tsx b/tests/ui/ForYouSectionTest.tsx
index ebdcf1ce65ca..02792c2de4c5 100644
--- a/tests/ui/ForYouSectionTest.tsx
+++ b/tests/ui/ForYouSectionTest.tsx
@@ -6,7 +6,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import ForYouSection from '@pages/home/ForYouSection';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
-import type {TodosDerivedValue} from '@src/types/onyx';
+import type {FlaggedExpensesDerivedValue, TodosDerivedValue} from '@src/types/onyx';
 import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
 
 jest.mock('@libs/Navigation/Navigation', () => ({
@@ -44,12 +44,15 @@ jest.mock('@hooks/useThemeStyles', () =>
 
 jest.mock('@hooks/useTheme', () => jest.fn(() => ({})));
 
+const EXCLAMATION_ASSET = {testID: 'exclamation-icon'};
+
 jest.mock('@hooks/useLazyAsset', () => ({
     useMemoizedLazyExpensifyIcons: jest.fn(() => ({
         MoneyBag: null,
         Send: null,
         ThumbsUp: null,
         Export: null,
+        Exclamation: EXCLAMATION_ASSET,
     })),
     useMemoizedLazyIllustrations: jest.fn(() => ({
         ThumbsUpStars: null,
@@ -75,6 +78,8 @@ const BASE_TODOS: TodosDerivedValue = {
     transactionsByReportID: {},
 };
 
+const EMPTY_FLAGGED_EXPENSES: FlaggedExpensesDerivedValue = {flaggedExpenses: []};
+
 function renderForYouSection() {
     return render(<ForYouSection />);
 }
@@ -125,6 +130,7 @@ describe('ForYouSection', () => {
         it('renders EmptyState when there are no todos', async () => {
             await act(async () => {
                 await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, EMPTY_FLAGGED_EXPENSES);
             });
             await waitForBatchedUpdatesWithAct();
 
@@ -135,6 +141,191 @@ describe('ForYouSection', () => {
         });
     });
 
+    describe('review row', () => {
+        it('is not rendered when there are no flagged expenses', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, {
+                    ...BASE_TODOS,
+                    reportsToSubmit: [{reportID: '1'} as TodosDerivedValue['reportsToSubmit'][number]],
+                });
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, EMPTY_FLAGGED_EXPENSES);
+            });
+            await waitForBatchedUpdatesWithAct();
+
+            renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
+
+            expect(screen.queryByText(/homePage\.forYouSection\.reviewExpenses/)).not.toBeOnTheScreen();
+        });
+
+        it('renders with the count-1 string when exactly one expense is flagged', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                    flaggedExpenses: [{transactionID: 't1', reportID: 'r1'}],
+                });
+            });
+            await waitForBatchedUpdatesWithAct();
+
+            renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
+
+            expect(screen.getByText('homePage.forYouSection.reviewExpenses:{"count":1}')).toBeOnTheScreen();
+        });
+
+        it('renders with the count-N string when multiple expenses are flagged', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                    flaggedExpenses: [
+                        {transactionID: 't1', reportID: 'r1'},
+                        {transactionID: 't2', reportID: 'r2'},
+                        {transactionID: 't3', reportID: 'r3'},
+                    ],
+                });
+            });
+            await waitForBatchedUpdatesWithAct();
+
+            renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
+
+            expect(screen.getByText('homePage.forYouSection.reviewExpenses:{"count":3}')).toBeOnTheScreen();
+        });
+
+        it('renders the review row above submit/approve/pay/export rows', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, {
+                    ...BASE_TODOS,
+                    reportsToSubmit: [{reportID: 's1'} as TodosDerivedValue['reportsToSubmit'][number]],
+                    reportsToApprove: [{reportID: 'a1'} as TodosDerivedValue['reportsToApprove'][number]],
+                    reportsToPay: [{reportID: 'p1'} as TodosDerivedValue['reportsToPay'][number]],
+                    reportsToExport: [{reportID: 'e1'} as TodosDerivedValue['reportsToExport'][number]],
+                });
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                    flaggedExpenses: [{transactionID: 't1', reportID: 'r1'}],
+                });
+            });
+            await waitForBatchedUpdatesWithAct();
+
+            renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
+
+            const reviewTitle = screen.getByText('homePage.forYouSection.reviewExpenses:{"count":1}');
+            const submitTitle = screen.getByText('homePage.forYouSection.submit:{"count":1}');
+            const approveTitle = screen.getByText('homePage.forYouSection.approve:{"count":1}');
+            const payTitle = screen.getByText('homePage.forYouSection.pay:{"count":1}');
+            const exportTitle = screen.getByText('homePage.forYouSection.export:{"count":1}');
+
+            // jest-native renderer assigns sequential _nativeTag-like positions; we rely on
+            // the order returned by getAllByText to verify rendering order.
+            const allTitles = screen.getAllByText(/homePage\.forYouSection\.(reviewExpenses|submit|approve|pay|export):/);
+            const getChildren = (node: {props: {children?: unknown}}) => node.props.children;
+            const titleOrder = allTitles.map(getChildren);
+
+            expect(titleOrder.at(0)).toBe(getChildren(reviewTitle));
+            expect(titleOrder).toEqual([getChildren(reviewTitle), getChildren(submitTitle), getChildren(approveTitle), getChildren(payTitle), getChildren(exportTitle)]);
+        });
+
+        it('exposes a Begin CTA and uses the Exclamation icon asset', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                    flaggedExpenses: [{transactionID: 't1', reportID: 'r1'}],
+                });
+            });
+            await waitForBatchedUpdatesWithAct();
+
+            const {UNSAFE_root: unsafeRoot} = renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
+
+            expect(screen.getByText('Begin')).toBeOnTheScreen();
+
+            // The Exclamation asset should be passed as the `icon` prop on at least one BaseWidgetItem.
+            // We look for any rendered element whose `icon` prop is the EXCLAMATION_ASSET reference.
+            const matchingNodes = unsafeRoot.findAll((node) => node.props && (node.props as {icon?: unknown}).icon === EXCLAMATION_ASSET);
+            expect(matchingNodes.length).toBeGreaterThan(0);
+        });
+    });
+
+    describe('review row navigation', () => {
+        describe('wide layout', () => {
+            it('navigates to EXPENSE_REPORT_RHP with the first flagged expense parent report and backTo=HOME', async () => {
+                await act(async () => {
+                    await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                    await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                        flaggedExpenses: [
+                            {transactionID: 't1', reportID: 'r1'},
+                            {transactionID: 't2', reportID: 'r2'},
+                        ],
+                    });
+                });
+                await waitForBatchedUpdatesWithAct();
+
+                renderForYouSection();
+                await waitForBatchedUpdatesWithAct();
+
+                pressFirstBeginButton();
+
+                expect(mockNavigate).toHaveBeenCalledTimes(1);
+                expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: 'r1', backTo: ROUTES.HOME}));
+            });
+
+            it('still uses the per-report RHP route when only one expense is flagged', async () => {
+                await act(async () => {
+                    await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                    await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                        flaggedExpenses: [{transactionID: 't1', reportID: 'rOnly'}],
+                    });
+                });
+                await waitForBatchedUpdatesWithAct();
+
+                renderForYouSection();
+                await waitForBatchedUpdatesWithAct();
+
+                pressFirstBeginButton();
+
+                expect(mockNavigate).toHaveBeenCalledTimes(1);
+                expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: 'rOnly', backTo: ROUTES.HOME}));
+            });
+        });
+
+        describe('narrow layout', () => {
+            beforeEach(() => {
+                mockUseResponsiveLayout.mockReturnValue({
+                    shouldUseNarrowLayout: true,
+                    isSmallScreenWidth: true,
+                    isInNarrowPaneModal: false,
+                    isExtraSmallScreenHeight: false,
+                    isMediumScreenWidth: false,
+                    isLargeScreenWidth: false,
+                    isExtraLargeScreenWidth: false,
+                    isExtraSmallScreenWidth: false,
+                    isSmallScreen: true,
+                    onboardingIsMediumOrLargerScreenWidth: false,
+                    isInLandscapeMode: false,
+                });
+            });
+
+            it('navigates to REPORT_WITH_ID with the first flagged expense parent report and backTo=HOME', async () => {
+                await act(async () => {
+                    await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                    await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                        flaggedExpenses: [{transactionID: 't1', reportID: 'rNarrow'}],
+                    });
+                });
+                await waitForBatchedUpdatesWithAct();
+
+                renderForYouSection();
+                await waitForBatchedUpdatesWithAct();
+
+                pressFirstBeginButton();
+
+                expect(mockNavigate).toHaveBeenCalledTimes(1);
+                expect(mockNavigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute('rNarrow', undefined, undefined, ROUTES.HOME));
+            });
+        });
+    });
+
     describe('navigation with multiple reports (search route)', () => {
         it('navigates to SEARCH_ROOT when submit has multiple reports', async () => {
             await act(async () => {
diff --git a/tests/unit/OnyxDerived/flaggedExpensesTest.ts b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
new file mode 100644
index 000000000000..4b59525f2d10
--- /dev/null
+++ b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
@@ -0,0 +1,264 @@
+import type {OnyxCollection} from 'react-native-onyx';
+import flaggedExpensesConfig from '@libs/actions/OnyxDerived/configs/flaggedExpenses';
+import type {DerivedValueContext} from '@libs/actions/OnyxDerived/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report, Session, Transaction, TransactionViolations} from '@src/types/onyx';
+import {createMockReport} from '../../utils/ReportTestUtils';
+
+const CURRENT_USER_ACCOUNT_ID = 1;
+const OTHER_USER_ACCOUNT_ID = 2;
+const POLICY_ID_1 = 'policy1';
+const POLICY_ID_2 = 'policy2';
+
+function createExpenseReport(reportID: string, overrides: Partial<Report> = {}): Report {
+    return createMockReport({
+        reportID,
+        type: CONST.REPORT.TYPE.EXPENSE,
+        ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+        policyID: POLICY_ID_1,
+        stateNum: CONST.REPORT.STATE_NUM.OPEN,
+        statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+        ...overrides,
+    });
+}
+
+function createTransaction(transactionID: string, reportID: string, overrides: Partial<Transaction> = {}): Transaction {
+    return {
+        transactionID,
+        reportID,
+        amount: 100,
+        currency: 'USD',
+        created: '2024-01-01',
+        merchant: 'Test Merchant',
+        ...overrides,
+    } as Transaction;
+}
+
+function buildReports(...reports: Report[]): OnyxCollection<Report> {
+    const result: OnyxCollection<Report> = {};
+    for (const report of reports) {
+        result[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`] = report;
+    }
+    return result;
+}
+
+function buildTransactions(...transactions: Transaction[]): OnyxCollection<Transaction> {
+    const result: OnyxCollection<Transaction> = {};
+    for (const transaction of transactions) {
+        result[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = transaction;
+    }
+    return result;
+}
+
+function buildViolations(violationsByTransactionID: Record<string, TransactionViolations>): OnyxCollection<TransactionViolations> {
+    const result: OnyxCollection<TransactionViolations> = {};
+    for (const [transactionID, violations] of Object.entries(violationsByTransactionID)) {
+        result[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] = violations;
+    }
+    return result;
+}
+
+function createSession(accountID: number = CURRENT_USER_ACCOUNT_ID): Session {
+    return {accountID} as Session;
+}
+
+const {compute} = flaggedExpensesConfig;
+const emptyContext = {} as DerivedValueContext<
+    typeof ONYXKEYS.DERIVED.FLAGGED_EXPENSES,
+    [typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.COLLECTION.TRANSACTION, typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, typeof ONYXKEYS.SESSION]
+>;
+
+describe('flaggedExpenses derived value', () => {
+    describe('presence and count', () => {
+        it('reports zero flagged expenses and an empty list when no transactions have violations', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1');
+            const result = compute([buildReports(report), buildTransactions(transaction), {}, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toEqual([]);
+        });
+
+        it('counts two flagged transactions on OPEN/OPEN expense reports across two workspaces', () => {
+            const report1 = createExpenseReport('r1', {policyID: POLICY_ID_1});
+            const report2 = createExpenseReport('r2', {policyID: POLICY_ID_2});
+            const transaction1 = createTransaction('t1', 'r1');
+            const transaction2 = createTransaction('t2', 'r2');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+                t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const result = compute([buildReports(report1, report2), buildTransactions(transaction1, transaction2), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toHaveLength(2);
+            const ids = result.flaggedExpenses.map((entry) => entry.transactionID).sort();
+            expect(ids).toEqual(['t1', 't2']);
+            const reportIDs = result.flaggedExpenses.map((entry) => entry.reportID).sort();
+            expect(reportIDs).toEqual(['r1', 'r2']);
+        });
+
+        it('counts a single transaction with multiple violations as 1 expense', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1');
+            const violations = buildViolations({
+                t1: [
+                    {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY},
+                    {type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_TAG},
+                ],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toHaveLength(1);
+            expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
+        });
+
+        it('counts every non-report-field violation type that OpenApp returns', () => {
+            const report = createExpenseReport('r1');
+            const transactions = [createTransaction('tA', 'r1'), createTransaction('tB', 'r1'), createTransaction('tC', 'r1')];
+            const violations = buildViolations({
+                tA: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.OVER_CATEGORY_LIMIT}],
+                tB: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_ATTENDEES}],
+                tC: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_COMMENT}],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(...transactions), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toHaveLength(3);
+            expect(result.flaggedExpenses.map((entry) => entry.transactionID).sort()).toEqual(['tA', 'tB', 'tC']);
+        });
+
+        it('excludes violations with showInReview === false', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY, showInReview: false}],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toEqual([]);
+        });
+    });
+
+    describe('Draft-report scope and resolution', () => {
+        it('excludes flagged transactions on submitted expense reports', () => {
+            const draftReport = createExpenseReport('r1');
+            const submittedReport = createExpenseReport('r2', {
+                stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
+                statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
+            });
+            const draftTransaction = createTransaction('t1', 'r1');
+            const submittedTransaction = createTransaction('t2', 'r2');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+                t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const result = compute([buildReports(draftReport, submittedReport), buildTransactions(draftTransaction, submittedTransaction), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toHaveLength(1);
+            expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
+        });
+
+        it('excludes flagged transactions whose parent report is not an expense report', () => {
+            const expenseReport = createExpenseReport('r1');
+            const chatReport = createMockReport({
+                reportID: 'r2',
+                type: CONST.REPORT.TYPE.CHAT,
+                ownerAccountID: CURRENT_USER_ACCOUNT_ID,
+                stateNum: CONST.REPORT.STATE_NUM.OPEN,
+                statusNum: CONST.REPORT.STATUS_NUM.OPEN,
+            });
+            const t1 = createTransaction('t1', 'r1');
+            const t2 = createTransaction('t2', 'r2');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+                t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const result = compute([buildReports(expenseReport, chatReport), buildTransactions(t1, t2), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toHaveLength(1);
+            expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
+        });
+
+        it('does not count report-field violations as flagged expenses', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.REPORT_VIOLATIONS.FIELD_REQUIRED} as TransactionViolations[number]],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toEqual([]);
+        });
+
+        it('drops a transaction from the list when its last violation is cleared', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1');
+            const flaggedViolations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const flaggedResult = compute([buildReports(report), buildTransactions(transaction), flaggedViolations, createSession()], emptyContext);
+            expect(flaggedResult.flaggedExpenses).toHaveLength(1);
+
+            const clearedViolations = buildViolations({t1: []});
+            const clearedResult = compute([buildReports(report), buildTransactions(transaction), clearedViolations, createSession()], emptyContext);
+            expect(clearedResult.flaggedExpenses).toEqual([]);
+        });
+
+        it('restores a previously cleaned transaction once it is re-flagged', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1');
+
+            const clearedResult = compute([buildReports(report), buildTransactions(transaction), buildViolations({t1: []}), createSession()], emptyContext);
+            expect(clearedResult.flaggedExpenses).toEqual([]);
+
+            const reflaggedResult = compute(
+                [
+                    buildReports(report),
+                    buildTransactions(transaction),
+                    buildViolations({
+                        t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+                    }),
+                    createSession(),
+                ],
+                emptyContext,
+            );
+            expect(reflaggedResult.flaggedExpenses).toHaveLength(1);
+            expect(reflaggedResult.flaggedExpenses.at(0)?.transactionID).toBe('t1');
+        });
+
+        it('excludes flagged transactions on expense reports the current user does not own', () => {
+            const ownReport = createExpenseReport('r1');
+            const foreignReport = createExpenseReport('r2', {ownerAccountID: OTHER_USER_ACCOUNT_ID});
+            const ownTransaction = createTransaction('t1', 'r1');
+            const foreignTransaction = createTransaction('t2', 'r2');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+                t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const result = compute([buildReports(ownReport, foreignReport), buildTransactions(ownTransaction, foreignTransaction), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toHaveLength(1);
+            expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
+        });
+
+        it('excludes all flagged transactions when the session has no accountID', () => {
+            const report = createExpenseReport('r1', {ownerAccountID: OTHER_USER_ACCOUNT_ID});
+            const transaction = createTransaction('t1', 'r1');
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, {} as Session], emptyContext);
+
+            expect(result.flaggedExpenses).toEqual([]);
+        });
+    });
+});
diff --git a/tests/unit/selectors/TodosTest.ts b/tests/unit/selectors/TodosTest.ts
index 25e57801cd2a..9d90b76cb698 100644
--- a/tests/unit/selectors/TodosTest.ts
+++ b/tests/unit/selectors/TodosTest.ts
@@ -1,6 +1,6 @@
-import todosReportCountsSelector, {todosSingleReportIDsSelector} from '@selectors/Todos';
+import todosReportCountsSelector, {flaggedExpensesReviewSelector, todosSingleReportIDsSelector} from '@selectors/Todos';
 import CONST from '@src/CONST';
-import type {TodosDerivedValue} from '@src/types/onyx';
+import type {FlaggedExpensesDerivedValue, TodosDerivedValue} from '@src/types/onyx';
 
 describe('todosReportCountsSelector', () => {
     it('returns undefined when todos is undefined', () => {
@@ -212,3 +212,69 @@ describe('todosSingleReportIDsSelector', () => {
         expect(second[CONST.SEARCH.SEARCH_KEYS.SUBMIT]).toBe('21');
     });
 });
+
+describe('flaggedExpensesReviewSelector', () => {
+    it('returns count 0 and undefined identifiers when input is undefined', () => {
+        const result = flaggedExpensesReviewSelector(undefined);
+
+        expect(result.count).toBe(0);
+        expect(result.firstTransactionID).toBeUndefined();
+        expect(result.firstReportID).toBeUndefined();
+    });
+
+    it('returns count 0 and undefined identifiers when there are no flagged expenses', () => {
+        const value: FlaggedExpensesDerivedValue = {flaggedExpenses: []};
+
+        const result = flaggedExpensesReviewSelector(value);
+
+        expect(result.count).toBe(0);
+        expect(result.firstTransactionID).toBeUndefined();
+        expect(result.firstReportID).toBeUndefined();
+    });
+
+    it('returns aggregated count and stable first identifiers for multiple flagged expenses', () => {
+        const value: FlaggedExpensesDerivedValue = {
+            flaggedExpenses: [
+                {transactionID: 't1', reportID: 'r1'},
+                {transactionID: 't2', reportID: 'r2'},
+                {transactionID: 't3', reportID: 'r3'},
+            ],
+        };
+
+        const result = flaggedExpensesReviewSelector(value);
+
+        expect(result.count).toBe(3);
+        expect(result.firstTransactionID).toBe('t1');
+        expect(result.firstReportID).toBe('r1');
+    });
+
+    it('returns the same object reference when called twice with shallowly equal values', () => {
+        const value: FlaggedExpensesDerivedValue = {
+            flaggedExpenses: [{transactionID: 'tA', reportID: 'rA'}],
+        };
+        const first = flaggedExpensesReviewSelector(value);
+
+        const clone: FlaggedExpensesDerivedValue = {
+            flaggedExpenses: [{transactionID: 'tA', reportID: 'rA'}],
+        };
+        const second = flaggedExpensesReviewSelector(clone);
+
+        expect(second).toBe(first);
+    });
+
+    it('returns a new object reference when the first flagged expense changes', () => {
+        const value: FlaggedExpensesDerivedValue = {
+            flaggedExpenses: [{transactionID: 'tA', reportID: 'rA'}],
+        };
+        const first = flaggedExpensesReviewSelector(value);
+
+        const changed: FlaggedExpensesDerivedValue = {
+            flaggedExpenses: [{transactionID: 'tB', reportID: 'rB'}],
+        };
+        const second = flaggedExpensesReviewSelector(changed);
+
+        expect(second).not.toBe(first);
+        expect(second.firstTransactionID).toBe('tB');
+        expect(second.firstReportID).toBe('rB');
+    });
+});

From 41d1e9978223fab2ee4206c99390c94cb9b95c32 Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Mon, 1 Jun 2026 16:28:34 +0200
Subject: [PATCH 02/12] Replace Exclamation icon with ReceiptSearch for review
 expenses widget

---
 assets/images/receipt-search.svg               |  1 +
 .../Icon/chunks/expensify-icons.chunk.ts       |  2 ++
 src/pages/home/ForYouSection/index.tsx         | 18 +++++++++++-------
 tests/ui/ForYouSectionTest.tsx                 | 12 ++++++------
 4 files changed, 20 insertions(+), 13 deletions(-)
 create mode 100644 assets/images/receipt-search.svg

diff --git a/assets/images/receipt-search.svg b/assets/images/receipt-search.svg
new file mode 100644
index 000000000000..bbf36cf9b1df
--- /dev/null
+++ b/assets/images/receipt-search.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M3 1.82c0-.36.44-.54.69-.28l.77.81c.16.17.42.17.58 0l1.17-1.23c.16-.17.42-.17.58 0l1.17 1.23c.16.17.42.17.58 0l1.17-1.23c.16-.17.42-.17.58 0l1.17 1.23c.16.17.42.17.58 0l1.17-1.23c.16-.17.42-.17.58 0l1.17 1.23c.16.17.42.17.58 0l.77-.81c.25-.26.69-.08.69.28v5.24c0 .38-.52.57-.8.32a6.05 6.05 0 0 0-4.03-1.54c-3.38 0-6.12 2.79-6.12 6.24 0 2.08.74 3.12 1.47 4.16.2.28.4.56.58.86.09.15.07.34-.05.47l-1.26 1.32c-.16.17-.42.17-.58 0l-1.17-1.23a.394.394 0 0 0-.58 0l-.77.81a.4.4 0 0 1-.69-.28z"/><path fill-rule="evenodd" d="M12.08 16.16c.76 0 1.48-.21 2.09-.58l3.12 3.12 1.41-1.41-3.12-3.12c.37-.61.58-1.33.58-2.09 0-2.25-1.83-4.08-4.08-4.08S8 9.83 8 12.08s1.83 4.08 4.08 4.08m0-2c1.15 0 2.08-.93 2.08-2.08S13.23 10 12.08 10 10 10.93 10 12.08s.93 2.08 2.08 2.08"/></svg>
\ No newline at end of file
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index 821fb37ef966..38558ace6f6d 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -204,6 +204,7 @@ import ReceiptMultiple from '@assets/images/receipt-multiple.svg';
 import ReceiptPlaceholderPlus from '@assets/images/receipt-placeholder-plus.svg';
 import ReceiptPlus from '@assets/images/receipt-plus.svg';
 import ReceiptScan from '@assets/images/receipt-scan.svg';
+import ReceiptSearch from '@assets/images/receipt-search.svg';
 import ReceiptSlash from '@assets/images/receipt-slash.svg';
 import Receipt from '@assets/images/receipt.svg';
 import RemoveMembers from '@assets/images/remove-members.svg';
@@ -444,6 +445,7 @@ const Expensicons = {
     ReceiptPlaceholderPlus,
     ReceiptPlus,
     ReceiptScan,
+    ReceiptSearch,
     ReceiptSlash,
     RemoveMembers,
     ReportCopy,
diff --git a/src/pages/home/ForYouSection/index.tsx b/src/pages/home/ForYouSection/index.tsx
index f4f7d537ffd7..9e275e365a79 100644
--- a/src/pages/home/ForYouSection/index.tsx
+++ b/src/pages/home/ForYouSection/index.tsx
@@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import {buildQueryStringFromFilterFormValues} from '@libs/SearchQueryUtils';
 import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
+import colors from '@styles/theme/colors';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
@@ -31,7 +32,7 @@ function ForYouSection() {
     const [singleReportIDs = EMPTY_TODOS_SINGLE_REPORT_IDS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosSingleReportIDsSelector});
     const [flaggedExpensesReview = EMPTY_FLAGGED_EXPENSES_REVIEW] = useOnyx(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {selector: flaggedExpensesReviewSelector});
 
-    const icons = useMemoizedLazyExpensifyIcons(['Exclamation', 'MoneyBag', 'Send', 'ThumbsUp', 'Export']);
+    const icons = useMemoizedLazyExpensifyIcons(['ReceiptSearch', 'MoneyBag', 'Send', 'ThumbsUp', 'Export']);
 
     const submitCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.SUBMIT] ?? 0;
     const approveCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.APPROVE] ?? 0;
@@ -88,9 +89,12 @@ function ForYouSection() {
                 {
                     key: 'reviewExpenses',
                     count: flaggedExpensesCount,
-                    icon: icons.Exclamation,
+                    icon: icons.ReceiptSearch,
+                    iconBackgroundColor: colors.tangerine100,
+                    iconFill: colors.tangerine500,
                     translationKey: 'homePage.forYouSection.reviewExpenses' as const,
                     handler: createReviewExpensesHandler(flaggedExpensesReview.firstReportID),
+                    buttonProps: {danger: true} as const,
                 },
                 {
                     key: 'submit',
@@ -137,9 +141,9 @@ function ForYouSection() {
             exportCount,
             flaggedExpensesCount,
             flaggedExpensesReview.firstReportID,
-            icons.Exclamation,
             icons.Export,
             icons.MoneyBag,
+            icons.ReceiptSearch,
             icons.Send,
             icons.ThumbsUp,
             payCount,
@@ -150,16 +154,16 @@ function ForYouSection() {
 
     const renderTodoItems = () => (
         <View style={styles.getForYouSectionContainerStyle(shouldUseNarrowLayout)}>
-            {todoItems.map(({key, count, icon, translationKey, handler}) => (
+            {todoItems.map(({key, count, icon, iconBackgroundColor, iconFill, translationKey, handler, buttonProps}) => (
                 <BaseWidgetItem
                     key={key}
                     icon={icon}
-                    iconBackgroundColor={theme.widgetIconBG}
-                    iconFill={theme.widgetIconFill}
+                    iconBackgroundColor={iconBackgroundColor ?? theme.widgetIconBG}
+                    iconFill={iconFill ?? theme.widgetIconFill}
                     title={translate(translationKey, {count})}
                     ctaText={translate('homePage.forYouSection.begin')}
                     onCtaPress={handler}
-                    buttonProps={{success: true}}
+                    buttonProps={buttonProps ?? {success: true}}
                 />
             ))}
         </View>
diff --git a/tests/ui/ForYouSectionTest.tsx b/tests/ui/ForYouSectionTest.tsx
index 02792c2de4c5..196df710f2be 100644
--- a/tests/ui/ForYouSectionTest.tsx
+++ b/tests/ui/ForYouSectionTest.tsx
@@ -44,7 +44,7 @@ jest.mock('@hooks/useThemeStyles', () =>
 
 jest.mock('@hooks/useTheme', () => jest.fn(() => ({})));
 
-const EXCLAMATION_ASSET = {testID: 'exclamation-icon'};
+const RECEIPT_SEARCH_ASSET = {testID: 'receipt-search-icon'};
 
 jest.mock('@hooks/useLazyAsset', () => ({
     useMemoizedLazyExpensifyIcons: jest.fn(() => ({
@@ -52,7 +52,7 @@ jest.mock('@hooks/useLazyAsset', () => ({
         Send: null,
         ThumbsUp: null,
         Export: null,
-        Exclamation: EXCLAMATION_ASSET,
+        ReceiptSearch: RECEIPT_SEARCH_ASSET,
     })),
     useMemoizedLazyIllustrations: jest.fn(() => ({
         ThumbsUpStars: null,
@@ -226,7 +226,7 @@ describe('ForYouSection', () => {
             expect(titleOrder).toEqual([getChildren(reviewTitle), getChildren(submitTitle), getChildren(approveTitle), getChildren(payTitle), getChildren(exportTitle)]);
         });
 
-        it('exposes a Begin CTA and uses the Exclamation icon asset', async () => {
+        it('exposes a Begin CTA and uses the ReceiptSearch icon asset', async () => {
             await act(async () => {
                 await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
                 await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
@@ -240,9 +240,9 @@ describe('ForYouSection', () => {
 
             expect(screen.getByText('Begin')).toBeOnTheScreen();
 
-            // The Exclamation asset should be passed as the `icon` prop on at least one BaseWidgetItem.
-            // We look for any rendered element whose `icon` prop is the EXCLAMATION_ASSET reference.
-            const matchingNodes = unsafeRoot.findAll((node) => node.props && (node.props as {icon?: unknown}).icon === EXCLAMATION_ASSET);
+            // The ReceiptSearch asset should be passed as the `icon` prop on at least one BaseWidgetItem.
+            // We look for any rendered element whose `icon` prop is the RECEIPT_SEARCH_ASSET reference.
+            const matchingNodes = unsafeRoot.findAll((node) => node.props && (node.props as {icon?: unknown}).icon === RECEIPT_SEARCH_ASSET);
             expect(matchingNodes.length).toBeGreaterThan(0);
         });
     });

From 288c0a09545000af2b24a20963be31a61ec774fd Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Mon, 1 Jun 2026 18:53:35 +0200
Subject: [PATCH 03/12] Open transaction thread with review carousel from
 review expenses row

---
 .../MoneyRequestReportTransactionList.tsx     |  55 ++-------
 src/hooks/useNavigateToTransactionThread.ts   |  92 ++++++++++++++
 src/pages/home/ForYouSection/index.tsx        |  21 +---
 .../ForYouSection/useReviewFlaggedExpenses.ts |  52 ++++++++
 src/selectors/Todos.ts                        |  17 ++-
 tests/ui/ForYouSectionTest.tsx                | 116 ++++++++----------
 6 files changed, 227 insertions(+), 126 deletions(-)
 create mode 100644 src/hooks/useNavigateToTransactionThread.ts
 create mode 100644 src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts

diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index 06671b17d07f..d2df0138c1e8 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -20,7 +20,6 @@ import SelectionList from '@components/SelectionList';
 import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem';
 import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton';
 import Text from '@components/Text';
-import {useWideRHPActions} from '@components/WideRHPContextProvider';
 import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
 import {useCurrencyListActions} from '@hooks/useCurrencyList';
 import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
@@ -28,6 +27,7 @@ import useHandleSelectionMode from '@hooks/useHandleSelectionMode';
 import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
 import useLocalize from '@hooks/useLocalize';
 import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
+import useNavigateToTransactionThread from '@hooks/useNavigateToTransactionThread';
 import useNetwork from '@hooks/useNetwork';
 import useOnyx from '@hooks/useOnyx';
 import useReportIsArchived from '@hooks/useReportIsArchived';
@@ -38,14 +38,12 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import useWindowDimensions from '@hooks/useWindowDimensions';
 import {turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
-import {setOptimisticTransactionThread} from '@libs/actions/Report';
 import {getReportLayoutGroupBy, setReportLayoutGroupBy} from '@libs/actions/ReportLayout';
 import {clearActiveTransactionIDs, setActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
 import {resolveTransactionCardFields} from '@libs/CardUtils';
 import {hasNonReimbursableTransactions, isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
 import {navigationRef} from '@libs/Navigation/Navigation';
 import {isPolicyTaxEnabled} from '@libs/PolicyUtils';
-import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
 import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils';
 import {
     canAddTransaction,
@@ -67,9 +65,7 @@ import {getTransactionPendingAction, isTransactionPendingDelete, shouldShowExpen
 import shouldShowTransactionYear from '@libs/TransactionUtils/shouldShowTransactionYear';
 import isReportOpenInSuperWideRHP from '@navigation/helpers/isReportOpenInSuperWideRHP';
 import Navigation from '@navigation/Navigation';
-import type {ReportsSplitNavigatorParamList} from '@navigation/types';
 import variables from '@styles/variables';
-import {createTransactionThreadReport} from '@userActions/Report';
 import CONST from '@src/CONST';
 import type {TranslationPaths} from '@src/languages/types';
 import NAVIGATORS from '@src/NAVIGATORS';
@@ -123,8 +119,6 @@ type TransactionWithOptionalHighlight = OnyxTypes.Transaction & {
     shouldBeHighlighted?: boolean;
 };
 
-type ReportScreenNavigationProps = ReportsSplitNavigatorParamList[typeof SCREENS.REPORT];
-
 type SortedTransactions = {
     sortBy: SortableColumnName;
     sortOrder: SortOrder;
@@ -152,7 +146,7 @@ function MoneyRequestReportTransactionList({
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
     const {isSmallScreenWidth, isMediumScreenWidth, isInLandscapeMode} = useResponsiveLayout();
     const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP();
-    const {markReportIDAsExpense} = useWideRHPActions();
+    const navigateToTransactionThread = useNavigateToTransactionThread();
     const [isModalVisible, setIsModalVisible] = useState(false);
     const [selectedTransactionID, setSelectedTransactionID] = useState<string>('');
     const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report);
@@ -177,8 +171,6 @@ function MoneyRequestReportTransactionList({
     const [reportLayoutGroupBy] = useOnyx(ONYXKEYS.NVP_REPORT_LAYOUT_GROUP_BY);
     const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED);
     const [reportDetailsColumns] = useOnyx(ONYXKEYS.NVP_REPORT_DETAILS_COLUMNS);
-    const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
-    const [betas] = useOnyx(ONYXKEYS.BETAS);
     const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST);
     const [cardList] = useOnyx(ONYXKEYS.CARD_LIST);
     const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});
@@ -464,44 +456,15 @@ function MoneyRequestReportTransactionList({
      */
     const navigateToTransaction = useCallback(
         (activeTransactionID: string) => {
-            const iouAction = getIOUActionForTransactionID(reportActions, activeTransactionID);
-            const backTo = Navigation.getActiveRoute();
-            let reportIDToNavigate = iouAction?.childReportID;
-
-            const routeParams = {
-                reportID: reportIDToNavigate,
-                backTo,
-            } as ReportScreenNavigationProps;
-
-            if (!reportIDToNavigate) {
-                const transaction = sortedTransactions.find((t) => t.transactionID === activeTransactionID);
-                const transactionThreadReport = createTransactionThreadReport({
-                    introSelected,
-                    currentUserLogin: currentUserDetails.email ?? '',
-                    currentUserAccountID: currentUserDetails.accountID,
-                    betas,
-                    iouReport: report,
-                    iouReportAction: iouAction,
-                    transaction,
-                });
-                if (transactionThreadReport) {
-                    reportIDToNavigate = transactionThreadReport.reportID;
-                    routeParams.reportID = reportIDToNavigate;
-                }
-            } else {
-                setOptimisticTransactionThread(reportIDToNavigate, report?.reportID, iouAction?.reportActionID, report?.policyID);
-            }
-
-            // Single transaction report will open in RHP, and we need to find every other report ID for the rest of transactions
-            // to display prev/next arrows in RHP for navigation - use visual order from grouped transactions
-            setActiveTransactionIDs(visualOrderTransactionIDs).then(() => {
-                if (reportIDToNavigate) {
-                    markReportIDAsExpense(reportIDToNavigate);
-                }
-                Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
+            navigateToTransactionThread({
+                transactionID: activeTransactionID,
+                reportActions,
+                report,
+                transaction: sortedTransactions.find((t) => t.transactionID === activeTransactionID),
+                siblingTransactionIDs: visualOrderTransactionIDs,
             });
         },
-        [reportActions, visualOrderTransactionIDs, sortedTransactions, report, markReportIDAsExpense, introSelected, betas, currentUserDetails.email, currentUserDetails.accountID],
+        [navigateToTransactionThread, reportActions, sortedTransactions, report, visualOrderTransactionIDs],
     );
 
     const {amountColumnSize, dateColumnSize, taxAmountColumnSize} = useMemo(() => {
diff --git a/src/hooks/useNavigateToTransactionThread.ts b/src/hooks/useNavigateToTransactionThread.ts
new file mode 100644
index 000000000000..f6678e419216
--- /dev/null
+++ b/src/hooks/useNavigateToTransactionThread.ts
@@ -0,0 +1,92 @@
+import {useCallback} from 'react';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useWideRHPActions} from '@components/WideRHPContextProvider';
+import {createTransactionThreadReport, setOptimisticTransactionThread} from '@libs/actions/Report';
+import {setActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
+import Navigation from '@libs/Navigation/Navigation';
+import {getIOUActionForTransactionID} from '@libs/ReportActionsUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Report, ReportAction, Transaction} from '@src/types/onyx';
+import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
+import useOnyx from './useOnyx';
+
+type NavigateToTransactionThreadParams = {
+    /** The transaction whose thread should be opened */
+    transactionID: string;
+
+    /** Report actions of the parent (IOU/expense) report, used to resolve the IOU action and its thread */
+    reportActions: ReportAction[];
+
+    /** The parent (IOU/expense) report that owns the transaction */
+    report: OnyxEntry<Report>;
+
+    /** The transaction being opened, used to build the thread optimistically when it doesn't exist yet */
+    transaction: OnyxEntry<Transaction>;
+
+    /** Ordered list of sibling transaction IDs used to drive the prev/next carousel in the thread RHP */
+    siblingTransactionIDs: string[];
+
+    /** Route to return to when navigating back; defaults to the current active route */
+    backTo?: string;
+};
+
+/**
+ * Shared navigation algorithm for opening a transaction thread (single-expense RHP view with the
+ * prev/next carousel). It resolves the IOU action's `childReportID`, creates the thread optimistically
+ * when it doesn't exist yet, seeds the sibling transaction IDs for the carousel, and navigates to the
+ * SEARCH_REPORT route.
+ *
+ * Callers are responsible for gathering their own `reportActions`, `report`, `transaction`, and
+ * `siblingTransactionIDs` because the data sources differ per screen.
+ */
+function useNavigateToTransactionThread() {
+    const {markReportIDAsExpense} = useWideRHPActions();
+    const currentUserDetails = useCurrentUserPersonalDetails();
+    const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
+    const [betas] = useOnyx(ONYXKEYS.BETAS);
+
+    return useCallback(
+        ({transactionID, reportActions, report, transaction, siblingTransactionIDs, backTo}: NavigateToTransactionThreadParams) => {
+            const iouAction = getIOUActionForTransactionID(reportActions, transactionID);
+            const resolvedBackTo = backTo ?? Navigation.getActiveRoute();
+            let reportIDToNavigate = iouAction?.childReportID;
+
+            const routeParams: {reportID: string | undefined; reportActionID?: string; backTo?: string} = {
+                reportID: reportIDToNavigate,
+                backTo: resolvedBackTo,
+            };
+
+            if (!reportIDToNavigate) {
+                const transactionThreadReport = createTransactionThreadReport({
+                    introSelected,
+                    currentUserLogin: currentUserDetails.email ?? '',
+                    currentUserAccountID: currentUserDetails.accountID,
+                    betas,
+                    iouReport: report,
+                    iouReportAction: iouAction,
+                    transaction,
+                });
+                if (transactionThreadReport) {
+                    reportIDToNavigate = transactionThreadReport.reportID;
+                    routeParams.reportID = reportIDToNavigate;
+                }
+            } else {
+                setOptimisticTransactionThread(reportIDToNavigate, report?.reportID, iouAction?.reportActionID, report?.policyID);
+            }
+
+            // Single transaction report opens in RHP. We seed every sibling transaction ID so the RHP can
+            // display prev/next arrows for navigation between expenses.
+            setActiveTransactionIDs(siblingTransactionIDs).then(() => {
+                if (reportIDToNavigate) {
+                    markReportIDAsExpense(reportIDToNavigate);
+                }
+                Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
+            });
+        },
+        [markReportIDAsExpense, introSelected, betas, currentUserDetails.email, currentUserDetails.accountID],
+    );
+}
+
+export default useNavigateToTransactionThread;
+export type {NavigateToTransactionThreadParams};
diff --git a/src/pages/home/ForYouSection/index.tsx b/src/pages/home/ForYouSection/index.tsx
index 9e275e365a79..8062d2706f9a 100644
--- a/src/pages/home/ForYouSection/index.tsx
+++ b/src/pages/home/ForYouSection/index.tsx
@@ -16,9 +16,10 @@ import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import {accountIDSelector} from '@src/selectors/Session';
-import todosReportCountsSelector, {EMPTY_FLAGGED_EXPENSES_REVIEW, EMPTY_TODOS_SINGLE_REPORT_IDS, flaggedExpensesReviewSelector, todosSingleReportIDsSelector} from '@src/selectors/Todos';
+import todosReportCountsSelector, {EMPTY_TODOS_SINGLE_REPORT_IDS, todosSingleReportIDsSelector} from '@src/selectors/Todos';
 import EmptyState from './EmptyState';
 import ForYouSkeleton from './ForYouSkeleton';
+import useReviewFlaggedExpenses from './useReviewFlaggedExpenses';
 
 function ForYouSection() {
     const styles = useThemeStyles();
@@ -30,7 +31,7 @@ function ForYouSection() {
     const [isLoadingReportData = false] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA);
     const [reportCounts = CONST.EMPTY_TODOS_REPORT_COUNTS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosReportCountsSelector});
     const [singleReportIDs = EMPTY_TODOS_SINGLE_REPORT_IDS] = useOnyx(ONYXKEYS.DERIVED.TODOS, {selector: todosSingleReportIDsSelector});
-    const [flaggedExpensesReview = EMPTY_FLAGGED_EXPENSES_REVIEW] = useOnyx(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {selector: flaggedExpensesReviewSelector});
+    const {count: flaggedExpensesCount, reviewExpenses} = useReviewFlaggedExpenses();
 
     const icons = useMemoizedLazyExpensifyIcons(['ReceiptSearch', 'MoneyBag', 'Send', 'ThumbsUp', 'Export']);
 
@@ -38,7 +39,6 @@ function ForYouSection() {
     const approveCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.APPROVE] ?? 0;
     const payCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.PAY] ?? 0;
     const exportCount = reportCounts?.[CONST.SEARCH.SEARCH_KEYS.EXPORT] ?? 0;
-    const flaggedExpensesCount = flaggedExpensesReview.count;
 
     const hasAnyTodos = flaggedExpensesCount > 0 || submitCount > 0 || approveCount > 0 || payCount > 0 || exportCount > 0;
 
@@ -73,16 +73,6 @@ function ForYouSection() {
         [navigateToReport],
     );
 
-    const createReviewExpensesHandler = useCallback(
-        (firstReportID: string | undefined) => () => {
-            if (!firstReportID) {
-                return;
-            }
-            navigateToReport(firstReportID);
-        },
-        [navigateToReport],
-    );
-
     const todoItems = useMemo(
         () =>
             [
@@ -93,7 +83,7 @@ function ForYouSection() {
                     iconBackgroundColor: colors.tangerine100,
                     iconFill: colors.tangerine500,
                     translationKey: 'homePage.forYouSection.reviewExpenses' as const,
-                    handler: createReviewExpensesHandler(flaggedExpensesReview.firstReportID),
+                    handler: reviewExpenses,
                     buttonProps: {danger: true} as const,
                 },
                 {
@@ -137,10 +127,9 @@ function ForYouSection() {
             accountID,
             approveCount,
             createNavigationHandler,
-            createReviewExpensesHandler,
+            reviewExpenses,
             exportCount,
             flaggedExpensesCount,
-            flaggedExpensesReview.firstReportID,
             icons.Export,
             icons.MoneyBag,
             icons.ReceiptSearch,
diff --git a/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts b/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts
new file mode 100644
index 000000000000..c04331a02291
--- /dev/null
+++ b/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts
@@ -0,0 +1,52 @@
+import {useCallback} from 'react';
+import useNavigateToTransactionThread from '@hooks/useNavigateToTransactionThread';
+import useOnyx from '@hooks/useOnyx';
+import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import {EMPTY_FLAGGED_EXPENSES_REVIEW, flaggedExpensesReviewSelector} from '@src/selectors/Todos';
+
+type ReviewFlaggedExpenses = {
+    /** Number of flagged expenses awaiting review, used to decide whether to render the review row */
+    count: number;
+
+    /** Opens the first flagged expense's transaction thread with the full set of flagged expenses as the review carousel */
+    reviewExpenses: () => void;
+};
+
+/**
+ * Encapsulates the data plumbing for the "Review X expenses" row in the For You section: it reads the
+ * flagged-expenses review summary plus the first flagged expense's report, report actions, and transaction,
+ * and exposes a bound handler that navigates to the transaction thread (single-expense RHP + review carousel).
+ */
+function useReviewFlaggedExpenses(): ReviewFlaggedExpenses {
+    const [review = EMPTY_FLAGGED_EXPENSES_REVIEW] = useOnyx(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {selector: flaggedExpensesReviewSelector});
+
+    // Load the first flagged expense's parent report, its actions, and the transaction itself so the shared
+    // navigation hook can resolve (or optimistically create) the transaction thread.
+    const [firstFlaggedReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(review.firstReportID)}`);
+    const [firstFlaggedReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(review.firstReportID)}`);
+    const [firstFlaggedTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(review.firstTransactionID)}`);
+
+    const navigateToTransactionThread = useNavigateToTransactionThread();
+
+    const reviewExpenses = useCallback(() => {
+        const {firstTransactionID, firstReportID, transactionIDs} = review;
+        if (!firstTransactionID || !firstReportID) {
+            return;
+        }
+        navigateToTransactionThread({
+            transactionID: firstTransactionID,
+            reportActions: Object.values(firstFlaggedReportActions ?? {}),
+            report: firstFlaggedReport,
+            transaction: firstFlaggedTransaction,
+            siblingTransactionIDs: transactionIDs,
+            backTo: ROUTES.HOME,
+        });
+    }, [review, firstFlaggedReportActions, firstFlaggedReport, firstFlaggedTransaction, navigateToTransactionThread]);
+
+    return {count: review.count, reviewExpenses};
+}
+
+export default useReviewFlaggedExpenses;
+export type {ReviewFlaggedExpenses};
diff --git a/src/selectors/Todos.ts b/src/selectors/Todos.ts
index 84ff216de5c0..e0e8eb3565ad 100644
--- a/src/selectors/Todos.ts
+++ b/src/selectors/Todos.ts
@@ -73,12 +73,15 @@ type FlaggedExpensesReview = {
     firstTransactionID: string | undefined;
     /** Parent report ID of the first flagged expense */
     firstReportID: string | undefined;
+    /** Ordered list of every flagged transaction ID, used to drive the prev/next review carousel */
+    transactionIDs: string[];
 };
 
 const EMPTY_FLAGGED_EXPENSES_REVIEW: FlaggedExpensesReview = Object.freeze({
     count: 0,
     firstTransactionID: undefined,
     firstReportID: undefined,
+    transactionIDs: [],
 }) as FlaggedExpensesReview;
 
 // Manual memoization mirrors `todosSingleReportIDsSelector`: ForYouSection's useMemo dependency
@@ -94,13 +97,25 @@ const flaggedExpensesReviewSelector = (flaggedExpensesValue: OnyxEntry<FlaggedEx
     }
 
     const first = flaggedExpenses.at(0);
+    const transactionIDs = flaggedExpenses.map((flaggedExpense) => flaggedExpense.transactionID);
     const newValue: FlaggedExpensesReview = {
         count: flaggedExpenses.length,
         firstTransactionID: first?.transactionID,
         firstReportID: first?.reportID,
+        transactionIDs,
     };
 
-    if (shallowEqual(previousFlaggedExpensesReview, newValue)) {
+    // shallowEqual would always report a difference because `transactionIDs` is a fresh array each call,
+    // so we compare the scalar fields shallowly and the IDs element-by-element to preserve referential stability.
+    const previousTransactionIDs = previousFlaggedExpensesReview.transactionIDs;
+    const areTransactionIDsEqual =
+        previousTransactionIDs.length === transactionIDs.length && previousTransactionIDs.every((transactionID, index) => transactionID === transactionIDs.at(index));
+    if (
+        previousFlaggedExpensesReview.count === newValue.count &&
+        previousFlaggedExpensesReview.firstTransactionID === newValue.firstTransactionID &&
+        previousFlaggedExpensesReview.firstReportID === newValue.firstReportID &&
+        areTransactionIDsEqual
+    ) {
         return previousFlaggedExpensesReview;
     }
 
diff --git a/tests/ui/ForYouSectionTest.tsx b/tests/ui/ForYouSectionTest.tsx
index 196df710f2be..14877cc43c75 100644
--- a/tests/ui/ForYouSectionTest.tsx
+++ b/tests/ui/ForYouSectionTest.tsx
@@ -4,6 +4,7 @@ import Onyx from 'react-native-onyx';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import Navigation from '@libs/Navigation/Navigation';
 import ForYouSection from '@pages/home/ForYouSection';
+import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import type {FlaggedExpensesDerivedValue, TodosDerivedValue} from '@src/types/onyx';
@@ -17,6 +18,9 @@ jest.mock('@libs/Navigation/Navigation', () => ({
 
 jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
 
+const mockNavigateToTransactionThread = jest.fn();
+jest.mock('@hooks/useNavigateToTransactionThread', () => jest.fn(() => mockNavigateToTransactionThread));
+
 jest.mock('@hooks/useLocalize', () =>
     jest.fn(() => ({
         translate: jest.fn((key: string, params?: Record<string, unknown>) => {
@@ -248,81 +252,67 @@ describe('ForYouSection', () => {
     });
 
     describe('review row navigation', () => {
-        describe('wide layout', () => {
-            it('navigates to EXPENSE_REPORT_RHP with the first flagged expense parent report and backTo=HOME', async () => {
-                await act(async () => {
-                    await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
-                    await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
-                        flaggedExpenses: [
-                            {transactionID: 't1', reportID: 'r1'},
-                            {transactionID: 't2', reportID: 'r2'},
-                        ],
-                    });
+        it('delegates to useNavigateToTransactionThread with the first flagged expense and all sibling transaction IDs', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                    flaggedExpenses: [
+                        {transactionID: 't1', reportID: 'r1'},
+                        {transactionID: 't2', reportID: 'r2'},
+                    ],
                 });
-                await waitForBatchedUpdatesWithAct();
-
-                renderForYouSection();
-                await waitForBatchedUpdatesWithAct();
-
-                pressFirstBeginButton();
-
-                expect(mockNavigate).toHaveBeenCalledTimes(1);
-                expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: 'r1', backTo: ROUTES.HOME}));
-            });
-
-            it('still uses the per-report RHP route when only one expense is flagged', async () => {
-                await act(async () => {
-                    await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
-                    await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
-                        flaggedExpenses: [{transactionID: 't1', reportID: 'rOnly'}],
-                    });
+                await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}r1`, {reportID: 'r1'});
+                await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}r1`, {
+                    action1: {
+                        reportActionID: 'action1',
+                        actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+                        childReportID: 'thread-r1',
+                        message: {IOUTransactionID: 't1'},
+                    },
                 });
-                await waitForBatchedUpdatesWithAct();
+                await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}t1`, {transactionID: 't1', reportID: 'r1'});
+            });
+            await waitForBatchedUpdatesWithAct();
 
-                renderForYouSection();
-                await waitForBatchedUpdatesWithAct();
+            renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
 
-                pressFirstBeginButton();
+            pressFirstBeginButton();
 
-                expect(mockNavigate).toHaveBeenCalledTimes(1);
-                expect(mockNavigate).toHaveBeenCalledWith(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: 'rOnly', backTo: ROUTES.HOME}));
-            });
+            expect(mockNavigateToTransactionThread).toHaveBeenCalledTimes(1);
+            expect(mockNavigateToTransactionThread).toHaveBeenCalledWith(
+                expect.objectContaining({
+                    transactionID: 't1',
+                    report: expect.objectContaining({reportID: 'r1'}),
+                    transaction: expect.objectContaining({transactionID: 't1'}),
+                    reportActions: expect.arrayContaining([expect.objectContaining({reportActionID: 'action1', childReportID: 'thread-r1'})]),
+                    siblingTransactionIDs: ['t1', 't2'],
+                    backTo: ROUTES.HOME,
+                }),
+            );
+            // The standard report routes should not be used for the review row anymore.
+            expect(mockNavigate).not.toHaveBeenCalled();
         });
 
-        describe('narrow layout', () => {
-            beforeEach(() => {
-                mockUseResponsiveLayout.mockReturnValue({
-                    shouldUseNarrowLayout: true,
-                    isSmallScreenWidth: true,
-                    isInNarrowPaneModal: false,
-                    isExtraSmallScreenHeight: false,
-                    isMediumScreenWidth: false,
-                    isLargeScreenWidth: false,
-                    isExtraLargeScreenWidth: false,
-                    isExtraSmallScreenWidth: false,
-                    isSmallScreen: true,
-                    onboardingIsMediumOrLargerScreenWidth: false,
-                    isInLandscapeMode: false,
+        it('does not call the hook when there is no flagged transaction or report', async () => {
+            await act(async () => {
+                await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
+                // count > 0 keeps the row rendered, but the first transaction/report are missing
+                await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
+                    flaggedExpenses: [{transactionID: '', reportID: ''}],
                 });
             });
+            await waitForBatchedUpdatesWithAct();
 
-            it('navigates to REPORT_WITH_ID with the first flagged expense parent report and backTo=HOME', async () => {
-                await act(async () => {
-                    await Onyx.set(ONYXKEYS.DERIVED.TODOS, BASE_TODOS);
-                    await Onyx.set(ONYXKEYS.DERIVED.FLAGGED_EXPENSES, {
-                        flaggedExpenses: [{transactionID: 't1', reportID: 'rNarrow'}],
-                    });
-                });
-                await waitForBatchedUpdatesWithAct();
-
-                renderForYouSection();
-                await waitForBatchedUpdatesWithAct();
+            renderForYouSection();
+            await waitForBatchedUpdatesWithAct();
 
-                pressFirstBeginButton();
+            const beginButton = screen.queryByText('Begin');
+            if (beginButton) {
+                fireEvent.press(beginButton);
+            }
 
-                expect(mockNavigate).toHaveBeenCalledTimes(1);
-                expect(mockNavigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute('rNarrow', undefined, undefined, ROUTES.HOME));
-            });
+            expect(mockNavigateToTransactionThread).not.toHaveBeenCalled();
         });
     });
 

From 7ff6727fee47a9ea961821cca087a3798826a3ed Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Mon, 1 Jun 2026 19:05:09 +0200
Subject: [PATCH 04/12] Fix cspell errors by renaming reflaggedResult to
 reFlaggedResult

---
 tests/unit/OnyxDerived/flaggedExpensesTest.ts | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/tests/unit/OnyxDerived/flaggedExpensesTest.ts b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
index 4b59525f2d10..fdb04dfc6cde 100644
--- a/tests/unit/OnyxDerived/flaggedExpensesTest.ts
+++ b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
@@ -218,7 +218,7 @@ describe('flaggedExpenses derived value', () => {
             const clearedResult = compute([buildReports(report), buildTransactions(transaction), buildViolations({t1: []}), createSession()], emptyContext);
             expect(clearedResult.flaggedExpenses).toEqual([]);
 
-            const reflaggedResult = compute(
+            const reFlaggedResult = compute(
                 [
                     buildReports(report),
                     buildTransactions(transaction),
@@ -229,8 +229,8 @@ describe('flaggedExpenses derived value', () => {
                 ],
                 emptyContext,
             );
-            expect(reflaggedResult.flaggedExpenses).toHaveLength(1);
-            expect(reflaggedResult.flaggedExpenses.at(0)?.transactionID).toBe('t1');
+            expect(reFlaggedResult.flaggedExpenses).toHaveLength(1);
+            expect(reFlaggedResult.flaggedExpenses.at(0)?.transactionID).toBe('t1');
         });
 
         it('excludes flagged transactions on expense reports the current user does not own', () => {

From 751d7fcbc6976deea196024ec6220c6df7fdb67f Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Tue, 2 Jun 2026 13:38:32 +0200
Subject: [PATCH 05/12] Remove redundant useCallback in
 useNavigateToTransactionThread

---
 src/hooks/useNavigateToTransactionThread.ts | 70 ++++++++++-----------
 1 file changed, 33 insertions(+), 37 deletions(-)

diff --git a/src/hooks/useNavigateToTransactionThread.ts b/src/hooks/useNavigateToTransactionThread.ts
index f6678e419216..df7b648d48ab 100644
--- a/src/hooks/useNavigateToTransactionThread.ts
+++ b/src/hooks/useNavigateToTransactionThread.ts
@@ -1,4 +1,3 @@
-import {useCallback} from 'react';
 import type {OnyxEntry} from 'react-native-onyx';
 import {useWideRHPActions} from '@components/WideRHPContextProvider';
 import {createTransactionThreadReport, setOptimisticTransactionThread} from '@libs/actions/Report';
@@ -46,46 +45,43 @@ function useNavigateToTransactionThread() {
     const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED);
     const [betas] = useOnyx(ONYXKEYS.BETAS);
 
-    return useCallback(
-        ({transactionID, reportActions, report, transaction, siblingTransactionIDs, backTo}: NavigateToTransactionThreadParams) => {
-            const iouAction = getIOUActionForTransactionID(reportActions, transactionID);
-            const resolvedBackTo = backTo ?? Navigation.getActiveRoute();
-            let reportIDToNavigate = iouAction?.childReportID;
+    return ({transactionID, reportActions, report, transaction, siblingTransactionIDs, backTo}: NavigateToTransactionThreadParams) => {
+        const iouAction = getIOUActionForTransactionID(reportActions, transactionID);
+        const resolvedBackTo = backTo ?? Navigation.getActiveRoute();
+        let reportIDToNavigate = iouAction?.childReportID;
 
-            const routeParams: {reportID: string | undefined; reportActionID?: string; backTo?: string} = {
-                reportID: reportIDToNavigate,
-                backTo: resolvedBackTo,
-            };
+        const routeParams: {reportID: string | undefined; reportActionID?: string; backTo?: string} = {
+            reportID: reportIDToNavigate,
+            backTo: resolvedBackTo,
+        };
 
-            if (!reportIDToNavigate) {
-                const transactionThreadReport = createTransactionThreadReport({
-                    introSelected,
-                    currentUserLogin: currentUserDetails.email ?? '',
-                    currentUserAccountID: currentUserDetails.accountID,
-                    betas,
-                    iouReport: report,
-                    iouReportAction: iouAction,
-                    transaction,
-                });
-                if (transactionThreadReport) {
-                    reportIDToNavigate = transactionThreadReport.reportID;
-                    routeParams.reportID = reportIDToNavigate;
-                }
-            } else {
-                setOptimisticTransactionThread(reportIDToNavigate, report?.reportID, iouAction?.reportActionID, report?.policyID);
+        if (!reportIDToNavigate) {
+            const transactionThreadReport = createTransactionThreadReport({
+                introSelected,
+                currentUserLogin: currentUserDetails.email ?? '',
+                currentUserAccountID: currentUserDetails.accountID,
+                betas,
+                iouReport: report,
+                iouReportAction: iouAction,
+                transaction,
+            });
+            if (transactionThreadReport) {
+                reportIDToNavigate = transactionThreadReport.reportID;
+                routeParams.reportID = reportIDToNavigate;
             }
+        } else {
+            setOptimisticTransactionThread(reportIDToNavigate, report?.reportID, iouAction?.reportActionID, report?.policyID);
+        }
 
-            // Single transaction report opens in RHP. We seed every sibling transaction ID so the RHP can
-            // display prev/next arrows for navigation between expenses.
-            setActiveTransactionIDs(siblingTransactionIDs).then(() => {
-                if (reportIDToNavigate) {
-                    markReportIDAsExpense(reportIDToNavigate);
-                }
-                Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
-            });
-        },
-        [markReportIDAsExpense, introSelected, betas, currentUserDetails.email, currentUserDetails.accountID],
-    );
+        // Single transaction report opens in RHP. We seed every sibling transaction ID so the RHP can
+        // display prev/next arrows for navigation between expenses.
+        setActiveTransactionIDs(siblingTransactionIDs).then(() => {
+            if (reportIDToNavigate) {
+                markReportIDAsExpense(reportIDToNavigate);
+            }
+            Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute(routeParams));
+        });
+    };
 }
 
 export default useNavigateToTransactionThread;

From adfb3804c658d752d9744965e9f58d33753dbac1 Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Tue, 2 Jun 2026 13:46:17 +0200
Subject: [PATCH 06/12] Gate notice/warning violations on showInReview in
 flaggedExpenses

---
 .../actions/OnyxDerived/configs/flaggedExpenses.ts | 10 ++++++++--
 tests/unit/OnyxDerived/flaggedExpensesTest.ts      | 14 ++++++++++++++
 2 files changed, 22 insertions(+), 2 deletions(-)

diff --git a/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts b/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
index e555f50f3107..3f2163b3979d 100644
--- a/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
+++ b/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
@@ -30,8 +30,11 @@ function isCurrentUserOpenExpenseReport(report: Report | null | undefined, curre
 
 /**
  * Returns true when the given violation list has at least one entry that should surface in the
- * `Review X expenses` row: a transaction-level violation that the user can act on. Violations
- * marked `showInReview === false` and report-field violations (`fieldRequired`) are excluded.
+ * `Review X expenses` row: a transaction-level violation that the user can act on. Entries marked
+ * `showInReview === false` and report-field violations (`fieldRequired`) are excluded. `notice`/
+ * `warning` types only count when `showInReview` is explicitly true (omitting it is not enough),
+ * mirroring the review-visibility logic in `SearchUIUtils.hasVisibleViolations`; real `violation`
+ * types remain actionable.
  */
 function hasReviewableViolation(violations: TransactionViolations | null | undefined): boolean {
     if (!violations || violations.length === 0) {
@@ -48,6 +51,9 @@ function hasReviewableViolation(violations: TransactionViolations | null | undef
         if (violation.name === CONST.REPORT_VIOLATIONS.FIELD_REQUIRED) {
             return false;
         }
+        if (violation.type === CONST.VIOLATION_TYPES.NOTICE || violation.type === CONST.VIOLATION_TYPES.WARNING) {
+            return violation.showInReview === true;
+        }
         return true;
     });
 }
diff --git a/tests/unit/OnyxDerived/flaggedExpensesTest.ts b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
index fdb04dfc6cde..c2316893628c 100644
--- a/tests/unit/OnyxDerived/flaggedExpensesTest.ts
+++ b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
@@ -140,6 +140,20 @@ describe('flaggedExpenses derived value', () => {
 
             expect(result.flaggedExpenses).toEqual([]);
         });
+
+        it('excludes warning/notice violations unless showInReview is explicitly true', () => {
+            const report = createExpenseReport('r1');
+            const transactions = [createTransaction('tWarn', 'r1'), createTransaction('tNotice', 'r1'), createTransaction('tShown', 'r1')];
+            const violations = buildViolations({
+                tWarn: [{type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}],
+                tNotice: [{type: CONST.VIOLATION_TYPES.NOTICE, name: CONST.VIOLATIONS.RECEIPT_REQUIRED}],
+                tShown: [{type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER, showInReview: true}],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(...transactions), violations, createSession()], emptyContext);
+
+            expect(result.flaggedExpenses).toEqual([{transactionID: 'tShown', reportID: 'r1'}]);
+        });
     });
 
     describe('Draft-report scope and resolution', () => {

From 6315c15377c59b46d77cd8bb17f1dc8495fb28d5 Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Wed, 3 Jun 2026 11:47:40 +0200
Subject: [PATCH 07/12] Add translations

---
 src/languages/de.ts      | 1 +
 src/languages/es.ts      | 1 +
 src/languages/fr.ts      | 1 +
 src/languages/it.ts      | 1 +
 src/languages/ja.ts      | 1 +
 src/languages/nl.ts      | 1 +
 src/languages/pl.ts      | 1 +
 src/languages/pt-BR.ts   | 1 +
 src/languages/zh-hans.ts | 1 +
 9 files changed, 9 insertions(+)

diff --git a/src/languages/de.ts b/src/languages/de.ts
index 9a5d43843811..5f16a3ad188c 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1005,6 +1005,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Alles erledigt',
                 f1FlagsDescription: 'Sie haben alle offenen Aufgaben abgeschlossen.',
             },
+            reviewExpenses: ({count}: {count: number}) => `Überprüfen Sie ${count} ${count === 1 ? 'Ausgabe' : 'Spesen'}`,
         },
         upcomingTravel: 'Bevorstehende Reisen',
         upcomingTravelSection: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 9ed06f7e346a..471f0899f1a7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -985,6 +985,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Todo al día',
                 f1FlagsDescription: 'Has completado todas las tareas pendientes.',
             },
+            reviewExpenses: ({count}: {count: number}) => `Revisa ${count} ${count === 1 ? 'gasto' : 'gastos'}`,
         },
         gettingStartedSection: {
             title: 'Primeros pasos',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index b6b2a7801543..a4d1fd03f3c4 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1008,6 +1008,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Tout est à jour',
                 f1FlagsDescription: 'Vous avez terminé toutes les tâches en cours.',
             },
+            reviewExpenses: ({count}: {count: number}) => `Examiner ${count} ${count === 1 ? 'dépense' : 'dépenses'}`,
         },
         upcomingTravel: 'Voyages à venir',
         upcomingTravelSection: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 705ae2ec94d0..d18c7039ba51 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1006,6 +1006,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Tutto a posto',
                 f1FlagsDescription: 'Hai completato tutte le attività in sospeso.',
             },
+            reviewExpenses: ({count}: {count: number}) => `Esamina ${count} ${count === 1 ? 'spesa' : 'spese'}`,
         },
         upcomingTravel: 'Prossimi viaggi',
         upcomingTravelSection: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 22dff1932990..230674aef9a9 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -989,6 +989,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'すべて確認済みです',
                 f1FlagsDescription: 'すべての未処理の To-do が完了しました。',
             },
+            reviewExpenses: ({count}: {count: number}) => `${count} 件の${count === 1 ? '経費' : '経費'}を確認`,
         },
         upcomingTravel: '今後の出張',
         upcomingTravelSection: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index b6f95c44ed34..477ce90bbae6 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1004,6 +1004,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Helemaal bij',
                 f1FlagsDescription: "Je hebt alle openstaande to-do's afgerond.",
             },
+            reviewExpenses: ({count}: {count: number}) => `Beoordeel ${count} ${count === 1 ? 'uitgave' : 'uitgaven'}`,
         },
         upcomingTravel: 'Aankomende reizen',
         upcomingTravelSection: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 1fe87f4ec8c4..75b68ef80900 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1006,6 +1006,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Wszystko nadrobione',
                 f1FlagsDescription: 'Ukończyłeś wszystkie zaległe zadania.',
             },
+            reviewExpenses: ({count}: {count: number}) => `Przejrzyj ${count} ${count === 1 ? 'wydatek' : 'wydatki'}`,
         },
         upcomingTravel: 'Nadchodząca podróż',
         upcomingTravelSection: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 394be17dc887..5fb2e3a69dfb 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1004,6 +1004,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: 'Tudo em dia',
                 f1FlagsDescription: 'Você concluiu todas as tarefas pendentes.',
             },
+            reviewExpenses: ({count}: {count: number}) => `Revisar ${count} ${count === 1 ? 'despesa' : 'despesas'}`,
         },
         upcomingTravel: 'Próximas viagens',
         upcomingTravelSection: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 8013eff5912d..afe96f3cd947 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -972,6 +972,7 @@ const translations: TranslationDeepObject<typeof en> = {
                 f1FlagsTitle: '全部完成',
                 f1FlagsDescription: '你已完成所有未完成的待办事项。',
             },
+            reviewExpenses: ({count}: {count: number}) => `审核 ${count} ${count === 1 ? '费用' : '费用'}`,
         },
         upcomingTravel: '即将出行',
         upcomingTravelSection: {

From 3da501cba6674ebf2621db3eba027c817a1f1a7b Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Wed, 3 Jun 2026 12:45:49 +0200
Subject: [PATCH 08/12] Remove unused type exports flagged by knip

---
 src/hooks/useNavigateToTransactionThread.ts              | 1 -
 src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts | 1 -
 src/selectors/Todos.ts                                   | 1 -
 3 files changed, 3 deletions(-)

diff --git a/src/hooks/useNavigateToTransactionThread.ts b/src/hooks/useNavigateToTransactionThread.ts
index df7b648d48ab..914a27ffdc4d 100644
--- a/src/hooks/useNavigateToTransactionThread.ts
+++ b/src/hooks/useNavigateToTransactionThread.ts
@@ -85,4 +85,3 @@ function useNavigateToTransactionThread() {
 }
 
 export default useNavigateToTransactionThread;
-export type {NavigateToTransactionThreadParams};
diff --git a/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts b/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts
index c04331a02291..3957511ad012 100644
--- a/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts
+++ b/src/pages/home/ForYouSection/useReviewFlaggedExpenses.ts
@@ -49,4 +49,3 @@ function useReviewFlaggedExpenses(): ReviewFlaggedExpenses {
 }
 
 export default useReviewFlaggedExpenses;
-export type {ReviewFlaggedExpenses};
diff --git a/src/selectors/Todos.ts b/src/selectors/Todos.ts
index e0e8eb3565ad..b0a628f1ed5a 100644
--- a/src/selectors/Todos.ts
+++ b/src/selectors/Todos.ts
@@ -125,4 +125,3 @@ const flaggedExpensesReviewSelector = (flaggedExpensesValue: OnyxEntry<FlaggedEx
 
 export default todosReportCountsSelector;
 export {EMPTY_FLAGGED_EXPENSES_REVIEW, EMPTY_TODOS_SINGLE_REPORT_IDS, flaggedExpensesReviewSelector, todosSingleReportIDsSelector};
-export type {FlaggedExpensesReview};

From 4d49b7bc291947a335a96502331e63ad016f527a Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Mon, 8 Jun 2026 15:04:33 +0200
Subject: [PATCH 09/12] Fix flagged expenses to exclude dismissed violations

---
 .../OnyxDerived/configs/flaggedExpenses.ts    | 23 +++----
 tests/unit/OnyxDerived/flaggedExpensesTest.ts | 60 +++++++++++++------
 2 files changed, 53 insertions(+), 30 deletions(-)

diff --git a/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts b/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
index 3f2163b3979d..d69e39d75686 100644
--- a/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
+++ b/src/libs/actions/OnyxDerived/configs/flaggedExpenses.ts
@@ -1,3 +1,4 @@
+import {getVisibleTransactionViolations} from '@libs/TransactionUtils';
 import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -28,14 +29,7 @@ function isCurrentUserOpenExpenseReport(report: Report | null | undefined, curre
     return report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS_NUM.OPEN;
 }
 
-/**
- * Returns true when the given violation list has at least one entry that should surface in the
- * `Review X expenses` row: a transaction-level violation that the user can act on. Entries marked
- * `showInReview === false` and report-field violations (`fieldRequired`) are excluded. `notice`/
- * `warning` types only count when `showInReview` is explicitly true (omitting it is not enough),
- * mirroring the review-visibility logic in `SearchUIUtils.hasVisibleViolations`; real `violation`
- * types remain actionable.
- */
+/** Returns true when at least one visible violation should surface in the "Review X expenses" row. */
 function hasReviewableViolation(violations: TransactionViolations | null | undefined): boolean {
     if (!violations || violations.length === 0) {
         return false;
@@ -60,13 +54,14 @@ function hasReviewableViolation(violations: TransactionViolations | null | undef
 
 export default createOnyxDerivedValueConfig({
     key: ONYXKEYS.DERIVED.FLAGGED_EXPENSES,
-    dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.TRANSACTION, ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, ONYXKEYS.SESSION],
-    compute: ([allReports, allTransactions, allTransactionViolations, session]) => {
+    dependencies: [ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.TRANSACTION, ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, ONYXKEYS.COLLECTION.POLICY, ONYXKEYS.SESSION],
+    compute: ([allReports, allTransactions, allTransactionViolations, allPolicies, session]) => {
         if (!allReports || !allTransactions) {
             return EMPTY_VALUE;
         }
 
         const currentUserAccountID = session?.accountID ?? CONST.DEFAULT_NUMBER_ID;
+        const currentUserEmail = session?.email ?? '';
         const flaggedExpenses: FlaggedExpenseEntry[] = [];
 
         for (const transactionKey of Object.keys(allTransactions)) {
@@ -81,7 +76,13 @@ export default createOnyxDerivedValueConfig({
             }
 
             const violations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`];
-            if (!hasReviewableViolation(violations)) {
+            if (!violations || violations.length === 0) {
+                continue;
+            }
+
+            const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
+            const visibleViolations = getVisibleTransactionViolations(transaction, violations, currentUserEmail, currentUserAccountID, report, policy);
+            if (!hasReviewableViolation(visibleViolations)) {
                 continue;
             }
 
diff --git a/tests/unit/OnyxDerived/flaggedExpensesTest.ts b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
index c2316893628c..12628862c3c0 100644
--- a/tests/unit/OnyxDerived/flaggedExpensesTest.ts
+++ b/tests/unit/OnyxDerived/flaggedExpensesTest.ts
@@ -7,6 +7,7 @@ import type {Report, Session, Transaction, TransactionViolations} from '@src/typ
 import {createMockReport} from '../../utils/ReportTestUtils';
 
 const CURRENT_USER_ACCOUNT_ID = 1;
+const CURRENT_USER_EMAIL = 'user@example.com';
 const OTHER_USER_ACCOUNT_ID = 2;
 const POLICY_ID_1 = 'policy1';
 const POLICY_ID_2 = 'policy2';
@@ -59,14 +60,14 @@ function buildViolations(violationsByTransactionID: Record<string, TransactionVi
     return result;
 }
 
-function createSession(accountID: number = CURRENT_USER_ACCOUNT_ID): Session {
-    return {accountID} as Session;
+function createSession(accountID: number = CURRENT_USER_ACCOUNT_ID, email: string = CURRENT_USER_EMAIL): Session {
+    return {accountID, email} as Session;
 }
 
 const {compute} = flaggedExpensesConfig;
 const emptyContext = {} as DerivedValueContext<
     typeof ONYXKEYS.DERIVED.FLAGGED_EXPENSES,
-    [typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.COLLECTION.TRANSACTION, typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, typeof ONYXKEYS.SESSION]
+    [typeof ONYXKEYS.COLLECTION.REPORT, typeof ONYXKEYS.COLLECTION.TRANSACTION, typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, typeof ONYXKEYS.COLLECTION.POLICY, typeof ONYXKEYS.SESSION]
 >;
 
 describe('flaggedExpenses derived value', () => {
@@ -74,7 +75,7 @@ describe('flaggedExpenses derived value', () => {
         it('reports zero flagged expenses and an empty list when no transactions have violations', () => {
             const report = createExpenseReport('r1');
             const transaction = createTransaction('t1', 'r1');
-            const result = compute([buildReports(report), buildTransactions(transaction), {}, createSession()], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(transaction), {}, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toEqual([]);
         });
@@ -89,7 +90,7 @@ describe('flaggedExpenses derived value', () => {
                 t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
             });
 
-            const result = compute([buildReports(report1, report2), buildTransactions(transaction1, transaction2), violations, createSession()], emptyContext);
+            const result = compute([buildReports(report1, report2), buildTransactions(transaction1, transaction2), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toHaveLength(2);
             const ids = result.flaggedExpenses.map((entry) => entry.transactionID).sort();
@@ -108,7 +109,7 @@ describe('flaggedExpenses derived value', () => {
                 ],
             });
 
-            const result = compute([buildReports(report), buildTransactions(transaction), violations, createSession()], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toHaveLength(1);
             expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
@@ -119,11 +120,11 @@ describe('flaggedExpenses derived value', () => {
             const transactions = [createTransaction('tA', 'r1'), createTransaction('tB', 'r1'), createTransaction('tC', 'r1')];
             const violations = buildViolations({
                 tA: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.OVER_CATEGORY_LIMIT}],
-                tB: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_ATTENDEES}],
+                tB: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_TAG}],
                 tC: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_COMMENT}],
             });
 
-            const result = compute([buildReports(report), buildTransactions(...transactions), violations, createSession()], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(...transactions), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toHaveLength(3);
             expect(result.flaggedExpenses.map((entry) => entry.transactionID).sort()).toEqual(['tA', 'tB', 'tC']);
@@ -136,7 +137,7 @@ describe('flaggedExpenses derived value', () => {
                 t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY, showInReview: false}],
             });
 
-            const result = compute([buildReports(report), buildTransactions(transaction), violations, createSession()], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toEqual([]);
         });
@@ -147,10 +148,10 @@ describe('flaggedExpenses derived value', () => {
             const violations = buildViolations({
                 tWarn: [{type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER}],
                 tNotice: [{type: CONST.VIOLATION_TYPES.NOTICE, name: CONST.VIOLATIONS.RECEIPT_REQUIRED}],
-                tShown: [{type: CONST.VIOLATION_TYPES.WARNING, name: CONST.VIOLATIONS.RTER, showInReview: true}],
+                tShown: [{type: CONST.VIOLATION_TYPES.NOTICE, name: CONST.VIOLATIONS.RECEIPT_REQUIRED, showInReview: true}],
             });
 
-            const result = compute([buildReports(report), buildTransactions(...transactions), violations, createSession()], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(...transactions), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toEqual([{transactionID: 'tShown', reportID: 'r1'}]);
         });
@@ -170,7 +171,7 @@ describe('flaggedExpenses derived value', () => {
                 t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
             });
 
-            const result = compute([buildReports(draftReport, submittedReport), buildTransactions(draftTransaction, submittedTransaction), violations, createSession()], emptyContext);
+            const result = compute([buildReports(draftReport, submittedReport), buildTransactions(draftTransaction, submittedTransaction), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toHaveLength(1);
             expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
@@ -192,7 +193,7 @@ describe('flaggedExpenses derived value', () => {
                 t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
             });
 
-            const result = compute([buildReports(expenseReport, chatReport), buildTransactions(t1, t2), violations, createSession()], emptyContext);
+            const result = compute([buildReports(expenseReport, chatReport), buildTransactions(t1, t2), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toHaveLength(1);
             expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
@@ -205,7 +206,7 @@ describe('flaggedExpenses derived value', () => {
                 t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.REPORT_VIOLATIONS.FIELD_REQUIRED} as TransactionViolations[number]],
             });
 
-            const result = compute([buildReports(report), buildTransactions(transaction), violations, createSession()], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toEqual([]);
         });
@@ -217,11 +218,11 @@ describe('flaggedExpenses derived value', () => {
                 t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
             });
 
-            const flaggedResult = compute([buildReports(report), buildTransactions(transaction), flaggedViolations, createSession()], emptyContext);
+            const flaggedResult = compute([buildReports(report), buildTransactions(transaction), flaggedViolations, {}, createSession()], emptyContext);
             expect(flaggedResult.flaggedExpenses).toHaveLength(1);
 
             const clearedViolations = buildViolations({t1: []});
-            const clearedResult = compute([buildReports(report), buildTransactions(transaction), clearedViolations, createSession()], emptyContext);
+            const clearedResult = compute([buildReports(report), buildTransactions(transaction), clearedViolations, {}, createSession()], emptyContext);
             expect(clearedResult.flaggedExpenses).toEqual([]);
         });
 
@@ -229,7 +230,7 @@ describe('flaggedExpenses derived value', () => {
             const report = createExpenseReport('r1');
             const transaction = createTransaction('t1', 'r1');
 
-            const clearedResult = compute([buildReports(report), buildTransactions(transaction), buildViolations({t1: []}), createSession()], emptyContext);
+            const clearedResult = compute([buildReports(report), buildTransactions(transaction), buildViolations({t1: []}), {}, createSession()], emptyContext);
             expect(clearedResult.flaggedExpenses).toEqual([]);
 
             const reFlaggedResult = compute(
@@ -239,6 +240,7 @@ describe('flaggedExpenses derived value', () => {
                     buildViolations({
                         t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
                     }),
+                    {},
                     createSession(),
                 ],
                 emptyContext,
@@ -257,7 +259,7 @@ describe('flaggedExpenses derived value', () => {
                 t2: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
             });
 
-            const result = compute([buildReports(ownReport, foreignReport), buildTransactions(ownTransaction, foreignTransaction), violations, createSession()], emptyContext);
+            const result = compute([buildReports(ownReport, foreignReport), buildTransactions(ownTransaction, foreignTransaction), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toHaveLength(1);
             expect(result.flaggedExpenses.at(0)).toEqual({transactionID: 't1', reportID: 'r1'});
@@ -270,7 +272,27 @@ describe('flaggedExpenses derived value', () => {
                 t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
             });
 
-            const result = compute([buildReports(report), buildTransactions(transaction), violations, {} as Session], emptyContext);
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, {}, {} as Session], emptyContext);
+
+            expect(result.flaggedExpenses).toEqual([]);
+        });
+
+        it('excludes transactions when the current user dismissed the only reviewable violation', () => {
+            const report = createExpenseReport('r1');
+            const transaction = createTransaction('t1', 'r1', {
+                comment: {
+                    dismissedViolations: {
+                        [CONST.VIOLATIONS.MISSING_CATEGORY]: {
+                            [CURRENT_USER_EMAIL]: '2024-01-01',
+                        },
+                    },
+                },
+            });
+            const violations = buildViolations({
+                t1: [{type: CONST.VIOLATION_TYPES.VIOLATION, name: CONST.VIOLATIONS.MISSING_CATEGORY}],
+            });
+
+            const result = compute([buildReports(report), buildTransactions(transaction), violations, {}, createSession()], emptyContext);
 
             expect(result.flaggedExpenses).toEqual([]);
         });

From f52de533dc2e7fe946335d3ffc55df41ee3ea0de Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Mon, 8 Jun 2026 15:41:32 +0200
Subject: [PATCH 10/12] Fix cross-report carousel using wrong parent report

---
 .../MoneyRequestReportTransactionsNavigation.tsx      | 11 ++++++-----
 1 file changed, 6 insertions(+), 5 deletions(-)

diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
index b4c41f0352ed..5c0d06ffb66c 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx
@@ -93,7 +93,8 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
         };
     }, [nextTransactionID, parentReportActions, prevTransactionID, transactionIDsList]);
 
-    const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${currentTransaction?.reportID}`);
+    const [prevParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${prevTransaction?.reportID}`);
+    const [nextParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nextTransaction?.reportID}`);
     const [prevThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${prevParentReportAction?.childReportID}`);
     const [nextThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${nextParentReportAction?.childReportID}`);
 
@@ -132,7 +133,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
         }
         // We know that the next thread report exists, it just wasn't fetched to Onyx yet, so we set it optimistically.
         if (!nextThreadReport && nextThreadReportID) {
-            setOptimisticTransactionThread(nextThreadReportID, parentReport?.reportID, nextParentReportAction?.reportActionID, parentReport?.policyID);
+            setOptimisticTransactionThread(nextThreadReportID, nextParentReport?.reportID, nextParentReportAction?.reportActionID, nextParentReport?.policyID);
         }
         // The transaction thread doesn't exist yet, so we should create it
         if (!nextThreadReportID) {
@@ -141,7 +142,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
                 currentUserLogin: currentUserEmail ?? '',
                 currentUserAccountID,
                 betas,
-                iouReport: parentReport,
+                iouReport: nextParentReport,
                 iouReportAction: nextParentReportAction,
                 transaction: nextTransaction,
             });
@@ -168,7 +169,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
         }
         // We know that the previous thread report exists, it just wasn't fetched to Onyx yet, so we set it optimistically.
         if (!prevThreadReport && prevThreadReportID) {
-            setOptimisticTransactionThread(prevThreadReportID, parentReport?.reportID, prevParentReportAction?.reportActionID, parentReport?.policyID);
+            setOptimisticTransactionThread(prevThreadReportID, prevParentReport?.reportID, prevParentReportAction?.reportActionID, prevParentReport?.policyID);
         }
         // The transaction thread doesn't exist yet, so we should create it
         if (!prevThreadReportID) {
@@ -177,7 +178,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
                 currentUserLogin: currentUserEmail ?? '',
                 currentUserAccountID,
                 betas,
-                iouReport: parentReport,
+                iouReport: prevParentReport,
                 iouReportAction: prevParentReportAction,
                 transaction: prevTransaction,
             });

From 9403f022d5900a7342f734ba3236a6abef52841e Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Mon, 8 Jun 2026 17:38:57 +0200
Subject: [PATCH 11/12] Remove unused getIOUActionForTransactionID import

---
 .../MoneyRequestReportTransactionList.tsx                       | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
index 071119fb763d..8141d499b144 100644
--- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -44,7 +44,7 @@ import {resolveTransactionCardFields} from '@libs/CardUtils';
 import {hasNonReimbursableTransactions, isBillableEnabledOnPolicy} from '@libs/MoneyRequestReportUtils';
 import {navigationRef} from '@libs/Navigation/Navigation';
 import {isPolicyTaxEnabled} from '@libs/PolicyUtils';
-import {getIOUActionForTransactionID, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
 import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils';
 import {
     canAddTransaction,

From 9a150ec66f504bb647511520c0c04d41cb22f5c0 Mon Sep 17 00:00:00 2001
From: Adam Grzybowski <adam.grzybowski@swmansion.com>
Date: Tue, 9 Jun 2026 14:02:20 +0200
Subject: [PATCH 12/12] Recognize optimistic report focused in search RHP
 transaction carousel

Match the SEARCH_REPORT RHP route by name + reportID param so the "Review X
expenses" carousel swaps to the preexisting thread instead of clearing the
report and bouncing to the parent. Add a regression test for this case.
---
 ...replaceOptimisticReportWithActualReport.ts | 13 +++-
 ...aceOptimisticReportWithActualReportTest.ts | 65 +++++++++++++++++++
 2 files changed, 77 insertions(+), 1 deletion(-)

diff --git a/src/libs/actions/replaceOptimisticReportWithActualReport.ts b/src/libs/actions/replaceOptimisticReportWithActualReport.ts
index 150f65a6827d..0bb103ce149f 100644
--- a/src/libs/actions/replaceOptimisticReportWithActualReport.ts
+++ b/src/libs/actions/replaceOptimisticReportWithActualReport.ts
@@ -140,7 +140,18 @@ function replaceOptimisticReportWithActualReport(report: Report, draftReportComm
         const backTo = (currentRouteInfo?.params as {backTo?: Route})?.backTo;
         const screenName = currentRouteInfo?.name;
 
-        const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`);
+        // Besides the central-pane report route (/r/:reportID), the optimistic report can also be focused
+        // in the RHP transaction-thread carousel (search/view/:reportID) used by the "Review X expenses"
+        // flow. Matched via route name + reportID param (not a path substring) since the carousel's nested
+        // backTo can contain other report IDs. Without this, the report is cleared out from under the user
+        // and ReportNavigateAwayHandler treats it as removed, dismissing the RHP and navigating to the parent.
+        const isOptimisticReportFocusedInSearchRHP =
+            screenName === SCREENS.RIGHT_MODAL.SEARCH_REPORT &&
+            !!currentRouteInfo?.params &&
+            typeof currentRouteInfo.params === 'object' &&
+            'reportID' in currentRouteInfo.params &&
+            currentRouteInfo.params.reportID === reportID;
+        const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`) || isOptimisticReportFocusedInSearchRHP;
 
         // Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730.
         // When user is editing a money request report (/e/:reportID route) and has
diff --git a/tests/actions/ReplaceOptimisticReportWithActualReportTest.ts b/tests/actions/ReplaceOptimisticReportWithActualReportTest.ts
index f879c4dcb8cb..6bf71df0c476 100644
--- a/tests/actions/ReplaceOptimisticReportWithActualReportTest.ts
+++ b/tests/actions/ReplaceOptimisticReportWithActualReportTest.ts
@@ -917,6 +917,71 @@ describe('replaceOptimisticReportWithActualReport', () => {
         subscription.remove();
     });
 
+    it('should swap to preexisting thread when focused on the search RHP transaction-thread carousel', async () => {
+        // Given an optimistic transaction thread under a multi-transaction IOU report opened in the
+        // "Review X expenses" carousel, i.e. the focused route is the SEARCH_REPORT RHP (search/view/:reportID)
+        const iouReportID = '9999';
+        const optimisticReportID = '1234';
+        const preexistingReportID = '5555';
+
+        mockIsReady.mockReturnValue(true);
+        // The carousel route is search/view/<id>, NOT /r/<id>
+        mockGetActiveRoute.mockReturnValue(`/search/view/${optimisticReportID}`);
+        mockGetCurrentRoute.mockReturnValue({name: SCREENS.RIGHT_MODAL.SEARCH_REPORT, params: {reportID: optimisticReportID}});
+
+        const reportActionID1 = '1';
+        const reportActionID2 = '2';
+        await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, {
+            reportID: iouReportID,
+            type: CONST.REPORT.TYPE.IOU,
+            transactionCount: 2,
+        });
+        await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`, {
+            [reportActionID1]: {
+                reportActionID: reportActionID1,
+                actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+                originalMessage: {type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, IOUTransactionID: 'trans1'},
+            },
+            [reportActionID2]: {
+                reportActionID: reportActionID2,
+                actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+                originalMessage: {type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, IOUTransactionID: 'trans2'},
+            },
+        });
+
+        const optimisticReport = {
+            reportID: optimisticReportID,
+            type: CONST.REPORT.TYPE.CHAT,
+            parentReportID: iouReportID,
+            parentReportActionID: reportActionID1,
+            preexistingReportID,
+        };
+
+        await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`, optimisticReport);
+        await waitForBatchedUpdates();
+
+        let capturedEventData: SwitchReportEventData | undefined;
+        const subscription = DeviceEventEmitter.addListener(`switchToPreExistingReport_${optimisticReportID}`, (data: SwitchReportEventData) => {
+            capturedEventData = data;
+        });
+
+        // When replaceOptimisticReportWithActualReport is called and the callback is executed
+        replaceOptimisticReportWithActualReport(optimisticReport, undefined);
+        await waitForBatchedUpdates();
+
+        // Then the switch event is emitted (the focused optimistic report is recognized on the RHP route)
+        expect(capturedEventData).toBeDefined();
+
+        capturedEventData?.callback();
+        await waitForBatchedUpdates();
+
+        // And the carousel is swapped to the preexisting thread via setParams (staying in the RHP),
+        // instead of being cleared out and bounced to the parent report
+        expect(mockSetParams).toHaveBeenCalledWith({reportID: preexistingReportID.toString()});
+
+        subscription.remove();
+    });
+
     it('should update backTo route when optimistic report is in background on expense report screen', async () => {
         // Given an optimistic report in the background while user is on expense report screen with backTo pointing to it
         const reportID = '1';
