Skip to content

[$175] Add a "Rejected" and a "Held" suggested search that appears once a user has rejected/held expenses #103339

Open

Description

@melvin-bot

Problem

Users can't find expenses after they are rejected or held. One customer gave an NPS score of 0 and said "expenses disappear" after being rejected. We are adding a Rejected and a Held filter to Search, but users still need to know it exists.

Solution

Add a "Rejected" and a "Held" suggested search to the Search left-hand nav.

  • Show it only after the user has at least one rejected or held expense.
  • After it first appears, keep showing it permanently, even when the user has no rejected or held expenses, since users get confused when items appear and disappear.

Note: If an entire report is rejected, the expenses in that report should also display in that filter. Any expenses that are marked as resolved should then disappear from the filter.

Reported in Slack.

Upwork Automation - Do Not Edit

Activity

  1. changed the title [-]Add a "Rejected" suggested search that appears once a user has a rejected expense[/-] [+]Add a "Rejected" and a "Held" suggested search that appears once a user has rejected/held expenses[/+] on Oct 19, 2026
  2. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 19, 2026
  3. melvin-bot commented on Oct 19, 2026

    @melvin-bot
    Author

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @Krishna2323 (External)

  4. changed the title [-]Add a "Rejected" and a "Held" suggested search that appears once a user has rejected/held expenses[/-] [+][$175] Add a "Rejected" and a "Held" suggested search that appears once a user has rejected/held expenses[/+] on Oct 19, 2026
  5. melvin-bot commented on Oct 19, 2026

    @melvin-bot
    Author
  6. flaviadefaria commented on Oct 19, 2026

    @flaviadefaria
    Contributor

    @Krishna2323 do you think Melvin can work on this or should I add the help wanted label?

  7. Krishna2323 commented on Oct 19, 2026

    @Krishna2323
    Contributor

    @flaviadefaria I think I can work with Melvin, but I'll confirm once I have a proposal from him.

  8. flaviadefaria commented on Oct 19, 2026

    @flaviadefaria
    Contributor

    Cool, want to already prompt him for the proposal then?

  9. Krishna2323 commented on Oct 20, 2026

    @Krishna2323
    Contributor

    @MelvinBot please post a proposal for this

  10. MelvinBot commented on Oct 20, 2026

    @MelvinBot
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    Users lose track of expenses after they are rejected or held. The Search left-hand nav has no "Rejected" or "Held" suggested search to help them find those expenses.

    What is the root cause of that problem?

    This is a new feature. CONST.SEARCH.SEARCH_KEYS has no rejected or held key. getSuggestedSearchesVisibility builds visibility only from policy and approval signals, so it can't show an item based on the user's own rejected or held expenses. There is also no stored flag that keeps a suggested search visible after it first appears.

    The filters these items will use:

    What changes do you think we should make in order to solve the problem?

    1. Add keys. Add REJECTED and HELD to CONST.SEARCH.SEARCH_KEYS.
    2. Define the searches. Add both entries to getSuggestedSearches, copying the "Unapproved cash" entry:
      • Rejected: type:expense declined:yes, with the ThumbsDown icon that the Rejected filter chip already uses.
      • Held: type:expense held:yes, with the Stopwatch icon.
      • Add both icons to SEARCH_TYPE_MENU_ICON_NAMES. Add search.tabs.rejected / search.tabs.held labels and empty-state copy in every locale.
    3. Detect the condition on the client. Add a useHasRejectedOrHeldExpenses hook, modeled on useHasReportAwaitingApproval. It returns two booleans:
      • hasRejected: any transaction where hasTransactionBeenRejected(violations) is true, or whose report matches hasReportBeenRejectedToSubmitter(report). The second check covers whole-report rejections.
      • hasHeld: any transaction where isOnHold(transaction) is true.
    4. Make it sticky. Add two boolean NVPs, nvp_hasSeenRejectedSearch and nvp_hasSeenHeldSearch, following the nvp_hasSeenForYouTodo pattern. In useSearchTypeMenuSections, a useEffect calls setNameValuePair(..., true) the first time each condition is true.
    5. Wire up visibility. Pass shouldShowRejectedSuggestion = hasRejected || hasSeenRejectedSearch (and the same for held) into createTypeMenuSections → getSuggestedSearchesVisibility. Push both items into the "Expense reports" section after Pay.
    6. Add tests. In SearchUIUtilsTest, cover hidden by default, shown when the condition is true, and shown when only the NVP is set.
    Code references
    Open questions before implementation
    • Held depends on a draft PR. The Held item needs held:yes from Add Held search filter for expenses. We can ship Rejected first and add Held when that PR merges, or put this on hold until then.
    • Whole-report rejections. The issue says expenses in a fully rejected report should appear under Rejected. The test steps for the Rejected filter PR suggest declined:yes may match only expenses rejected one at a time. If so, the backend matching needs an internal change. I could not verify this from the App repo.
    • New NVP names. An internal engineer should confirm that the backend accepts the two new NVPs through SetNameValuePair. One object NVP, such as nvp_seenSuggestedSearches: {rejected, held}, would also work.

    What alternative solutions did you explore? (Optional)

    • Use the search results count instead of local Onyx data. This is more accurate for old data that isn't loaded locally. But it needs an extra API call on every Search page load.
    • Skip the NVP and always show the items once any policy exists. This is simpler, but it goes against the "only after the first rejected or held expense" requirement.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/103339) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  11. Krishna2323 commented on Oct 20, 2026

    @Krishna2323
    Contributor

    @MelvinBot implement this

  12. MelvinBot commented on Oct 20, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: Add a Rejected suggested search that appears once the user has a rejected expense
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    What's in it:

    • New item: a Rejected suggested search (type:expense declined:yes) in the Expense reports section.
    • When it appears: once the user has a rejected expense or a rejected report.
    • Why it stays: a new NVP, nvp_hasSeenRejectedSuggestedSearch, keeps the item visible after it first appears.

    Not included:

    Please confirm:

    • New NVP: an internal engineer should confirm the backend accepts nvp_hasSeenRejectedSuggestedSearch.
    • Whole-report rejections: expenses in a fully rejected report only appear under Rejected if the backend matches them for declined:yes.
    Checks run
    • ✅ npm run typecheck
    • ✅ ESLint on changed files
    • ✅ Spelling check on changed files
    • ✅ React Compiler check on the two changed hooks
    • ✅ Jest: SearchUIUtilsTest, SearchRouterNavigationTest, plus 9 related Search, ExportOnyxState, and search-menu suites (1,344 tests)
    • ⚠️ Formatter (oxfmt) couldn't run in this environment. I checked line lengths against the 190-character print width by hand.
    • ❌ Web test couldn't run: the test session loaded a blank page during onboarding, before reaching the app. Manual testing is still needed.

    view run · no recording available

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

Metadata

Metadata

Labels

DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions