Skip to content

Add a Rejected suggested search that appears once the user has a rejected expense - #104438

Draft
MelvinBot wants to merge 2 commits into
mainfrom
claude-rejectedSuggestedSearch
Draft

MelvinBot wants to merge 2 commits into
mainfrom
claude-rejectedSuggestedSearch

Conversation

@MelvinBot

Copy link
Copy Markdown
Contributor

Explanation of Change

Users lose track of expenses after they are rejected. This PR adds a Rejected suggested search to the Expense reports section of the Search left-hand nav, so users can find those expenses.

  • What it opens: type:expense declined:yes, using the Rejected filter added in [Payment due @QichenZhu] Add Rejected search filter for expenses and expense reports. It uses the same ThumbsDown icon as the Rejected filter chip.
  • When it appears: a new useHasRejectedExpense hook returns true when any transaction has the rejected-expense violation (hasTransactionBeenRejected). It also returns true when any report was rejected as a whole (hasReportBeenRejectedToSubmitter), because whole-report rejections don't add per-transaction violations. Both checks use boolean Onyx selectors, so the nav only re-renders when the answer changes.
  • Why it stays: the first time the hook returns true, useSearchTypeMenuSections sets a new nvp_hasSeenRejectedSuggestedSearch NVP. This follows the nvp_hasSeenForYouTodo pattern. The item then stays visible even after every rejection is resolved, so it doesn't appear and disappear.
  • Visibility wiring: getSuggestedSearchesVisibility / createTypeMenuSections take a new shouldShowRejectedSuggestion flag. The item has its own empty state and translations in all locales.

Not included in this PR:

  • Held suggested search. Its held:yes filter hasn't merged yet. It's still a draft in Add Held search filter for expenses. Held can follow the same pattern once that PR lands.

Open questions for reviewers:

  • The new nvp_hasSeenRejectedSuggestedSearch NVP is written through SetNameValuePair. An internal engineer should confirm the backend accepts this NVP name.
  • The issue says expenses in a fully rejected report should appear under Rejected. That depends on backend matching for declined:yes, which I couldn't verify from the App repo.

Fixed Issues

$ #103339
PROPOSAL: #103339 (comment)

Tests

// TODO: The human co-author must fill out the tests you ran before marking this PR as "ready for review"
// Please describe what tests you performed that validates your changed worked.

  • Verify that no errors appear in the JS console

Offline tests

QA Steps

// TODO: The human co-author must fill out the QA tests you ran before marking this PR as "ready for review".
// Please describe what QA needs to do to validate your changes and what areas do they need to test for regressions.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified any copy / text that was added to the app is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@MelvinBot
MelvinBot requested a review from a team October 20, 2026 04:05
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Oct 20, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch because I don't have push access to your fork.

To take ownership of this branch and push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-rejectedSuggestedSearch
git checkout -b claude-rejectedSuggestedSearch upstream/claude-rejectedSuggestedSearch
git push -u origin claude-rejectedSuggestedSearch

Then you can close this PR and open a new one from your fork.

@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 7cabe5d0..79af5ece 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -10168,10 +10168,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
                 title: 'Keine zu bezahlenden Ausgaben',
                 subtitle: 'Glückwunsch! Du hast die Ziellinie überquert.',
             },
-            emptyRejectedResults: {
-                title: 'Keine abgelehnten Ausgaben',
-                subtitle: 'Nichts zu korrigieren. Gute Arbeit!',
-            },
+            emptyRejectedResults: {title: 'Keine abgelehnten Ausgaben', subtitle: 'Nichts zu beheben. Gute Arbeit!'},
             emptyExportResults: {
                 title: 'Keine Spesen zum Exportieren',
                 subtitle: 'Zeit, es ruhig angehen zu lassen – gute Arbeit.',
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 0091b941..2a80863e 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -10374,10 +10374,7 @@ ${reportName}`,
                 title: 'Δεν υπάρχουν έξοδα προς πληρωμή',
                 subtitle: 'Συγχαρητήρια! Περάσατε τη γραμμή τερματισμού.',
             },
-            emptyRejectedResults: {
-                title: 'Δεν υπάρχουν απορριφθέντα έξοδα',
-                subtitle: 'Τίποτα για διόρθωση. Μπράβο!',
-            },
+            emptyRejectedResults: {title: 'Καμία απορριφθείσα δαπάνη', subtitle: 'Δεν υπάρχει τίποτα προς διόρθωση. Μπράβο!'},
             emptyExportResults: {
                 title: 'Δεν υπάρχουν έξοδα για εξαγωγή',
                 subtitle: 'Ώρα να χαλαρώσετε, μπράβο σας.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 886e5eba..ee55ab59 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -10005,10 +10005,7 @@ ${reportName}`,
                 title: 'No hay gastos para pagar',
                 subtitle: '¡Felicidades! Has cruzado la línea de meta.',
             },
-            emptyRejectedResults: {
-                title: 'No hay gastos rechazados',
-                subtitle: 'Nada que corregir. ¡Buen trabajo!',
-            },
+            emptyRejectedResults: {title: 'No hay gastos rechazados', subtitle: 'Nada que corregir. ¡Buen trabajo!'},
             emptyExportResults: {
                 title: 'No hay gastos para exportar',
                 subtitle: 'Es hora de relajarse, buen trabajo.',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index b09157f4..50df5b33 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -10198,10 +10198,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
                 title: 'Aucune dépense à payer',
                 subtitle: 'Félicitations ! Vous avez franchi la ligne d’arrivée.',
             },
-            emptyRejectedResults: {
-                title: 'Aucune dépense rejetée',
-                subtitle: 'Rien à corriger. Beau travail !',
-            },
+            emptyRejectedResults: {title: 'Aucune dépense rejetée', subtitle: 'Rien à corriger. Beau travail !'},
             emptyExportResults: {
                 title: 'Aucune dépense à exporter',
                 subtitle: 'Il est temps de se détendre, beau travail.',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 4b1aa31f..cbf8dee2 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -10119,10 +10119,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
                 title: 'Nessuna spesa da pagare',
                 subtitle: 'Congratulazioni! Hai tagliato il traguardo.',
             },
-            emptyRejectedResults: {
-                title: 'Nessuna spesa rifiutata',
-                subtitle: 'Niente da correggere. Ottimo lavoro!',
-            },
+            emptyRejectedResults: {title: 'Nessuna spesa rifiutata', subtitle: 'Niente da correggere. Bel lavoro!'},
             emptyExportResults: {
                 title: 'Nessuna spesa da esportare',
                 subtitle: 'È il momento di rilassarsi, bel lavoro.',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index e7bfac7a..10a67aa5 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -9976,10 +9976,7 @@ ${reportName}`,
                 title: '支払う経費はありません',
                 subtitle: 'おめでとうございます!ゴールに到達しました。',
             },
-            emptyRejectedResults: {
-                title: '却下された経費はありません',
-                subtitle: '修正が必要なものはありません。お疲れさまでした!',
-            },
+            emptyRejectedResults: {title: '却下された経費はありません', subtitle: '修正する必要はありません。よくできました!'},
             emptyExportResults: {
                 title: 'エクスポートする経費はありません',
                 subtitle: 'ひと休みする時間です、お疲れさまでした。',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 7c2a8a01..b8c9ee2f 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -10086,10 +10086,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
                 title: 'Geen uitgaven om te betalen',
                 subtitle: 'Gefeliciteerd! Je hebt de finish gehaald.',
             },
-            emptyRejectedResults: {
-                title: 'Geen afgekeurde uitgaven',
-                subtitle: 'Niets om te corrigeren. Goed gedaan!',
-            },
+            emptyRejectedResults: {title: 'Geen afgewezen uitgaven', subtitle: 'Niets om te corrigeren. Goed gedaan!'},
             emptyExportResults: {
                 title: 'Geen uitgaven om te exporteren',
                 subtitle: 'Tijd om rustig aan te doen, goed gedaan.',
@@ -10341,7 +10338,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
             submit: 'Concepten',
             approve: 'Moet worden goedgekeurd',
             pay: 'Klaar om te betalen',
-            rejected: 'Afgekeurd',
+            rejected: 'Afgewezen',
             accounting: 'Boekhouding',
             export: 'Wachten op export',
             unapprovedCash: 'Kasverplichtingen',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 1e78be91..cee39008 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -10096,10 +10096,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
                 title: 'Brak wydatków do opłacenia',
                 subtitle: 'Gratulacje! Przekroczyłeś linię mety.',
             },
-            emptyRejectedResults: {
-                title: 'Brak odrzuconych wydatków',
-                subtitle: 'Nie ma nic do poprawienia. Dobra robota!',
-            },
+            emptyRejectedResults: {title: 'Brak odrzuconych wydatków', subtitle: 'Nie ma nic do poprawy. Dobra robota!'},
             emptyExportResults: {
                 title: 'Brak wydatków do wyeksportowania',
                 subtitle: 'Czas trochę odetchnąć, dobra robota.',
@@ -10357,7 +10354,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
             submit: 'Szkice',
             approve: 'Wymaga zatwierdzenia',
             pay: 'Gotowe do zapłaty',
-            rejected: 'Odrzucone',
+            rejected: 'Odrzucono',
             accounting: 'Księgowość',
             export: 'Oczekuje na eksport',
             unapprovedCash: 'Rozliczenia kasowe',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 775f3ee5..ed247663 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -10083,10 +10083,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
                 title: 'Nenhuma despesa para pagar',
                 subtitle: 'Parabéns! Você cruzou a linha de chegada.',
             },
-            emptyRejectedResults: {
-                title: 'Nenhuma despesa rejeitada',
-                subtitle: 'Nada para corrigir. Bom trabalho!',
-            },
+            emptyRejectedResults: {title: 'Nenhuma despesa rejeitada', subtitle: 'Nada para corrigir. Bom trabalho!'},
             emptyExportResults: {
                 title: 'Nenhuma despesa para exportar',
                 subtitle: 'Hora de relaxar, bom trabalho.',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index ea3662ec..e8f0f5d0 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -9709,10 +9709,7 @@ ${reportName}`,
                 title: '暂无可支付的报销',
                 subtitle: '恭喜!你已冲过终点线。',
             },
-            emptyRejectedResults: {
-                title: '暂无被拒绝的报销',
-                subtitle: '没有需要修正的内容。干得好!',
-            },
+            emptyRejectedResults: {title: '没有被拒绝的报销', subtitle: '没有需要修复的地方。干得好!'},
             emptyExportResults: {
                 title: '没有可导出的报销费用',
                 subtitle: '该放松一下了,干得不错。',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/CONST/index.ts 95.90% <ø> (ø)
src/ONYXKEYS.ts 100.00% <ø> (ø)
src/hooks/useHasRejectedExpense.ts 100.00% <100.00%> (ø)
src/hooks/useSearchTypeMenuSections.ts 97.77% <100.00%> (+0.48%) ⬆️
src/libs/ExportOnyxState/masking.ts 80.95% <ø> (ø)
src/libs/SearchSuggestionUtils.ts 97.45% <100.00%> (+0.13%) ⬆️
src/libs/SearchUIUtils.ts 73.61% <100.00%> (+0.03%) ⬆️
src/libs/actions/Search.ts 67.26% <0.00%> (-0.19%) ⬇️
... and 10 files with indirect coverage changes

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the failing Oxfmt check. Oxfmt's import sorting needs a blank line between the import type group and the external value import in src/hooks/useHasRejectedExpense.ts. I added that blank line in d0aedce. Other hooks, such as useTodoSearchResults.ts, already use this layout.

Check independent approval is also failing. It needs a reviewer approval and does not need a code change.


view run

@MelvinBot

MelvinBot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ⚠️ partial

The Rejected suggested search appears with its thumbs-down icon once a rejection exists and opens the type:expense declined:yes filter, but it disappeared after the rejected expense was deleted, which contradicts the PR's claim that it stays visible permanently. The PR's Tests section is still a TODO, so these steps come from the PR description and the linked issue.
Rejected chip absent after deleting the rejected expense

Step Status
Before any rejection, the Spend (Reports) tab shows no Rejected suggested searchThe suggested-search row showed only Needs approval, Drafts, Reports, and Expenses. Scrolling the row found no Rejected item.
Spend tab before any rejection, no Rejected chip
✅
Create a workspace, then create, submit, and reject a manual expense as the approverCreated a $25.00 "Test Merchant" expense, submitted it, then used More > Reject with a reason. The banner read: "This report was rejected. Waiting on you to fix the issues and manually resubmit."
Report shows rejected banner
✅
After the rejection, a Rejected suggested search appears with a thumbs-down iconThe row showed Ready to pay, Needs approval, Drafts, Reports, and Rejected. Rejected had a thumbs-down icon.
Rejected chip with thumbs-down icon
✅
Tapping Rejected opens the type:expense declined:yes searchThe search applied a "Rejected: Yes" filter, which matches typing type:expense declined:yes. It showed the "No rejected expenses" empty state, not the rejected expense. The report had already returned to Draft after the whole-report rejection, so it no longer matched the filter.
Rejected filter applied, No rejected expenses empty state
✅
After the rejection is resolved, Rejected stays visibleAfter the rejected report was deleted, Rejected disappeared from the row. It was still gone after leaving the Spend tab and coming back. This may be linked to the PR's open question about whether the backend accepts the new nvp_hasSeenRejectedSuggestedSearch NVP.
Rejected chip absent after deleting the rejected expense
❌
No crash or red error screen during the flowThe app stayed in the foreground with no crashes or error screens from sign-in through deletion.
✅

view run · view recording

@MelvinBot

MelvinBot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ✅ pass

The Rejected suggested search appears only after a rejection, stays visible after the rejection is resolved, and opens type:expense declined:yes, but that search never listed the rejected expense because Reject immediately reopens the report as a Draft. The PR's Tests section is still a TODO, so these steps come from the PR description and the linked issue.
Rejected nav item still present after the rejected expense was deleted

Step Status
Before any rejection, the Expense reports section has no Rejected itemThe section listed only Expenses, Reports, Drafts, Needs approval, and Ready to pay.
Expense reports nav with no Rejected item before any rejection
✅
Create, submit, and reject a manual expense as the approverCreated a $25.00 "Melvin Test Merchant" expense, submitted the report, then used More > Reject with a reason. The banner read: "This report was rejected. Waiting on you to fix the issues and manually resubmit."
Report showing rejected state
✅
A Rejected item appears in the Expense reports section with a thumbs-down iconA new Rejected item appeared in Expense reports with a thumbs-down icon.
Rejected item with thumbs-down icon in Expense reports nav
✅
Clicking Rejected opens the type:expense declined:yes searchThe search opened with a "Rejected: Yes" filter. It showed the "No rejected expenses" empty state, not the expense just rejected. Rejecting the report returned it to Draft, so it no longer matched the filter.
Rejected search showing No rejected expenses empty state
✅
Rejected stays visible after the rejection is resolvedAfter the rejected report was deleted, Rejected was still visible after leaving the Spend page and coming back. The test did not reload the browser page, because a reload can lock the test session out of sign-in. On Android, the same step failed: Rejected disappeared after the report was deleted.
Rejected nav item still present after the rejected expense was deleted
✅
No new JS console errors related to Search or the Rejected itemNo error banners, red toasts, or failure dialogs appeared during the flow. The test tool can't read the browser console on this setup, so this check relies on what the screen showed.
✅

view run · view recording

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants