Skip to content

IOU - "Tag no longer valid" error is not displayed until refresh the page #101584

Open

Description

@lanitochka17

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: 9.4.84-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://test-management.browserstack.com/projects/2219752/test-runs/74576165/folder/57609953/41236882/1772304455?sort=%2Bstatus_id
Email or phone of affected tester (no customers): gocemate+buzz19967@gmail.com
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: windows 10/chrome
App Component: Other

Action Performed:

Precondition:
As the admin of the policy enables categories/tags
Navigate to Rules > Require fields for all expenses
Enable category/tag toggle and Save
Add three tags (Tag 1, Tag 2, Tag 3)
Invite employee

Steps:

  1. Employee create expense using Tag 1
  2. Admin delete Tag 1

Expected Result:

"Tag no longer valid" error should be displayed in real time

Actual Result:

"Tag no longer valid" error is not displayed until refresh the page

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Bug7263090_1789724749812.Recording__1665.mp4

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @dukenv0307

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 18, 2026
  2. MelvinBot commented on Sep 18, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    Transaction violations are only recalculated in the one client that performs the tag deletion, and only for transactions that client already happens to hold in Onyx. Nothing recalculates them when a tag-list change arrives from the server, so the employee's session (and the admin's session when it hasn't loaded the expense) never gets a tagOutOfPolicy violation until the next full refetch — which is exactly what a page refresh does.

    deletePolicyTags marks the tag {pendingAction: DELETE, enabled: false} and then recomputes violations optimistically at src/libs/actions/Policy/Tag.ts:488. That math is correct — enabled: false is enough to trip src/libs/Violations/ViolationsUtils.ts:100 and push tagOutOfPolicy ("Tag no longer valid", src/languages/en.ts:10682). The problem is scope, not logic:

    The UI is not at fault — the expense detail subscribes live via src/hooks/useTransactionViolations.ts:21, so an Onyx write would render immediately. The data simply never arrives.

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

    Make the violation recompute react to incoming tag-list changes, not just locally-initiated ones. When policyTags_<policyID> changes for any reason (including a server push), re-run ViolationsUtils.getViolationsOnyxData for the locally-held transactions of that policy and write the result to transactionViolations_<id>. That fixes the employee's session client-side and also closes the acting-client blind spot at ReportUtils.ts:2337.

    Reuse the existing affectedTagNames fast path in pushTransactionViolationsOnyxData so only transactions holding a changed tag are touched — this recompute over large tag lists is what caused the freeze fixed in Fix Workspace Tags toggle freeze on large tag lists (scope violation recompute + pending switch feedback), so it must stay scoped.

    One caveat for the reviewer: the alternative fix is for the backend to include transactionViolations_<id> in the DeletePolicyTags response or push it over Pusher to each affected submitter. If you decide that is the right layer, this needs an internal engineer — that code is outside the App repo and I can't propose changes to it.

    What alternative solutions did you explore? (Optional)

    Alternatives and supporting evidence

    If the recording shows a Search page rather than the expense report, the cause is different. Inside SearchScopeProvider, useOnyx silently redirects transactionViolations_* reads to snapshot_<hash> (src/hooks/useOnyx.ts:74-99), so a live violation write does not re-render until the search re-runs. Several rows already work around this with useOnyxWithoutSnapshots; the same treatment would be the fix. ReportScreen and SearchMoneyRequestReportPage are not wrapped, so the expense-report surface is unaffected.

    Ruled out: the optimistic math itself. Traced against this exact precondition (single tag list, 3 tags, delete Tag 1, requiresTag on) every gate passes and tagOutOfPolicy is pushed. Nothing skips tags with pendingAction: DELETE, and an existing test covers the equivalent enabled: false shape at tests/unit/ReportUtilsTest.ts:13057-13108. tests/actions/PolicyTagTest.ts has zero assertions on TRANSACTION_VIOLATIONS, so the end-to-end delete → violation path is only covered indirectly — worth adding a test alongside the fix.

    Ruled out: stale UI or stale memoization on the report surfaces. The expense detail, transaction preview, and report transaction list all subscribe to TRANSACTION_VIOLATIONS reactively.

    Minor inconsistency spotted nearby (not this bug, 3 tags exist here): deleteWorkspaceCategories flips requiresCategory: false optimistically when the last category is removed (src/libs/actions/Policy/Category.ts:1402-1409); deletePolicyTags never flips requiresTag.

    What I could not verify: which account's screen the recording shows. That decides whether the primary gap is the admin's un-hydrated transaction or the employee's missing push — the fix above covers both. I also could not run a live repro: the test session handed to me failed during onboarding, so this analysis is code-evidence only.

    Confidence: medium — mechanism is confirmed in code with no production log or runtime confirmation.

    Related issues

    No duplicates found. A quick scan of open Expensify/App issues for this symptom turned up nothing covering tag-violation staleness.


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


    view run · no recording available

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 18, 2026
  4. melvin-bot commented on Sep 18, 2026

    @melvin-bot

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

  5. melvin-bot commented on Sep 18, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  6. iprasen commented on Sep 19, 2026

    @iprasen

    Proposal

    What is the root cause of that problem?

    1. When a workspace tag is deleted or disabled (e.g. via deletePolicyTags or setWorkspaceTagEnabled in src/libs/actions/Policy/Tag.ts), only ONYXKEYS.COLLECTION.POLICY_TAGS is updated optimistically and on the wire.
    2. Neither deletePolicyTags nor setWorkspaceTagEnabled re-evaluates or emits optimistic data for ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS for existing transactions in Onyx holding that tag.
    3. Furthermore, transaction components (like MoneyRequestView) consume transactionViolations stored in Onyx. When server updates to policyTags arrive, no reactive listener triggers ViolationsUtils.getViolationsOnyxData for existing transactions.
    4. Only when a page is hard-refreshed (triggering initial search/report fetches) or when a transaction field is edited does the client re-calculate violations, causing the "Tag no longer valid" error to remain hidden until refresh.

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

    1. In src/libs/actions/Policy/Tag.ts:
      • In deletePolicyTags and setWorkspaceTagEnabled, when tags are removed or disabled, look up loaded transactions for the given policyID and generate optimistic ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS updates using ViolationsUtils.getViolationsOnyxData.
    2. In the transaction viewing flow (MoneyRequestView.tsx / useViolations):
      • Ensure tag violations reactively account for changes in the active policyTags list so that even when tag changes arrive via server push, existing transactions immediately reflect the tagOutOfPolicy state.

    What alternative solutions did you explore? (Optional)

    Relying exclusively on the backend to push updated transactionViolations for all transactions was considered, but backend push does not retroactively broadcast violations for all historical transactions upon tag deletion. Handling optimistic and reactive re-evaluation in App ensures immediate, consistent feedback across all platforms.

  7. linhvovan29546 commented on Sep 20, 2026

    @linhvovan29546
    Contributor

    @MelvinBot The backend already includes transactionViolations_<id> in the DeletePolicyTags pushes it over Pusher to each affected submitter.

  8. MelvinBot commented on Sep 20, 2026

    @MelvinBot
    Contributor

    Taking that as correct — I can't read the backend repo, so I'm going on your word. It drops my "subscribe to policyTags_ and recompute violations" recommendation as the primary fix.

    If the submitter already receives a transactionViolations_<id> push, two App-side gaps remain that each produce "only after refresh". Which one applies depends on whose screen the recording shows.

    1. The viewer isn't a submitter (the admin who deleted the tag, or an approver watching the expense). No push targets them. Their only source of the violation is the optimistic recompute in the delete action, and that is scoped to transactions already hydrated in Onyx: usePolicyData builds reports/transactionsAndViolations from the local derived value (src/hooks/usePolicyData/index.ts:24), and pushTransactionViolationsOnyxData returns early when the policy has no locally-held transactions (src/libs/ReportUtils.ts:2328-2331). An admin deleting the tag from the Tags page without the employee's expense loaded writes nothing, so the error only shows after a refetch.

    2. The viewer is in search scope (Reports/Expenses tab, or any surface under SearchScopeProvider). The push does land in transactionViolations_<id>, but in search scope useOnyx redirects that read to snapshot_<hash> (src/hooks/useOnyx.ts:76-100; transactionViolations_ is in the redirect list at src/CONST/runtimeDefaults.ts:91). Only locally-initiated actions mirror their updates into the snapshot (src/libs/actions/IOU/SearchUpdate.ts:152); a Pusher update from another user's action never does, so the snapshot stays stale until the search re-runs — which a refresh does. Fix shape here is reading violations via useOnyxWithoutSnapshots on the affected rows.

    If the recording is instead the submitter on the Inbox expense view, neither applies — that surface isn't in search scope and MoneyRequestView subscribes to violations live — so the push isn't landing, and that's a delivery question outside the App repo.

    Which session does the video show — the admin deleting the tag or the employee — and on which screen (Inbox report vs Reports/Expenses)? That picks between 1 and 2 and I'll revise the proposal to match. I couldn't confirm it myself: the frames aren't extractable in my environment, and a two-account repro was out of reach for this run.


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

  9. linhvovan29546 commented on Sep 20, 2026

    @linhvovan29546
    Contributor

    If the submitter already receives a transactionViolations_ push, two App-side gaps remain that each produce "only after refresh". Which one applies depends on whose screen the recording shows.

    @MelvinBot The problem is that useTransactionViolations doesn’t re-render when it receives a Pusher event with policyTags_.

  10. MelvinBot commented on Sep 20, 2026

    @MelvinBot
    Contributor

    Agreed — that localizes it, and the hook confirms it. Revised proposal below.

    Proposal

    What is the root cause of that problem?

    useTransactionViolations has no policyTags_ dependency. It subscribes to the transaction, transactionViolations_, report_, policy_ and personal details (src/hooks/useTransactionViolations.ts:20-24) and memoizes on exactly those (src/hooks/useTransactionViolations.ts:44). A tag-list change arriving on its own therefore cannot add or clear tagOutOfPolicy on an open expense — the view keeps rendering the stored violations array until a refetch replaces it, which is what the refresh does.

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

    Subscribe to policyTags_<policyID> in the hook and re-derive the tag violations inside the same memo, right next to the existing syncCustomUnitRateOutOfDateRangeViolation call. That helper (src/libs/Violations/ViolationsUtils.ts:397) exists for this exact reason — a stored violation that can go stale against current policy data is re-derived at read time instead of written — so a sibling syncTagOutOfPolicyViolation(violations, transaction, policyTagList, policy) fits the file's existing shape. The rules to reuse are already there: single-level at src/libs/Violations/ViolationsUtils.ts:89, multi-level at src/libs/Violations/ViolationsUtils.ts:241. It should cover missingTag as well as tagOutOfPolicy, since deleting the last tag flips the list's "has enabled tags" state.

    Why this shape:

    Search surfaces, and tests

    This also covers the Reports/Expenses surfaces: policyTags_ is not in the snapshot redirect list (src/CONST/runtimeDefaults.ts:91 has policy_, which does not match policyTags_), so inside search scope the hook reads live tags even though transactionViolations_ still resolves from snapshot_<hash>.

    tests/actions/PolicyTagTest.ts has no assertions on TRANSACTION_VIOLATIONS today. Worth adding unit coverage for the new helper: tag deleted → tagOutOfPolicy appears; tag re-added or re-enabled → clears; missingTag behaviour when the last enabled tag goes away.

    Confidence: medium — code evidence only. I couldn't run a live repro this round; it needs two signed-in accounts.


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

  11. linhvovan29546 commented on Sep 20, 2026

    @linhvovan29546
    Contributor

    @MelvinBot implement this

  12. linhvovan29546 commented on Sep 20, 2026

    @linhvovan29546
    Contributor

    MelvinBot's proposal LGTM!

    🎀 👀 🎀 C+ reviewed

  13. 29 remaining items

  14. melvin-bot commented on Oct 29, 2026

    @melvin-bot

    This issue has not been updated in over 14 days. @AndrewGable, @linhvovan29546 eroding to Weekly issue.

  15. jeffrey-graham commented on Nov 7, 2026

    @jeffrey-graham
  16. melvin-bot commented on Nov 7, 2026

    @melvin-bot
  17. linhvovan29546 commented on Nov 10, 2026

    @linhvovan29546
    Contributor

    Not overdue. I'll ask the BC team to take a look.

  18. added
    Hot PickReady for an engineer to pick up and run with
    on Nov 10, 2026
  19. self-assigned this
    on Nov 12, 2026
  20. removed
    Hot PickReady for an engineer to pick up and run with
    on Nov 12, 2026
  21. dukenv0307 commented on Nov 13, 2026

    @dukenv0307
    Contributor

    @MelvinBot create an E/E issue

  22. MelvinBot commented on Nov 13, 2026

    @MelvinBot
    Contributor

    Created an internal E/E issue for the backend investigation, including reproduction steps, acceptance criteria, and a link back to this issue.


    view run

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions