Restrict report state transitions to archived policies only - #98948
Conversation
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.
|
…ict-more-menu-archived-policies # Conflicts: # tests/unit/ReportPrimaryActionUtilsTest.ts # tests/unit/ReportSecondaryActionUtilsTest.ts
…ict-more-menu-archived-policies # Conflicts: # src/libs/ReportPreviewActionUtils.ts # src/libs/actions/IOU/ReportWorkflow.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f32667f64
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a7700cc17
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Expense reports cannot be paid when their policy is archived or pending delete. Reports archived for other reasons | ||
| // (e.g. the submitter was unshared from the policy) can still be paid. IOU reports have no policy | ||
| // archived state, so an archived chat blocks payment instead. | ||
| const isBlockedByArchivedState = isExpenseReport(iouReport) ? isArchivedOrPendingDeletePolicy(policy) : isChatReportArchived; |
There was a problem hiding this comment.
This logic is now duplicated in three places. Could we extract a single shared helper so the rule lives in one place?
There was a problem hiding this comment.
Extracted isPayBlockedByArchivedState in ReportUtils and used it in all three places. Thanks!
dylanexpensify
left a comment
There was a problem hiding this comment.
@c3024 have we applied the Reject facet of this yet? Based on this comment.
|
@dylanexpensify, that is implemented now. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9ea3059179
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // the manager, so don't block Submit when the default submit-to route is the owner. | ||
| if ( | ||
| canSubmitReport(report, ownerLogin, policy, allReportTransactions, allViolations, isIOUReportArchived || isChatReportArchived, currentUserLogin, currentUserAccountID) && | ||
| canSubmitReport(report, ownerLogin, policy, allReportTransactions, allViolations, isArchivedOrPendingDeletePolicy(policy), currentUserLogin, currentUserAccountID) && |
There was a problem hiding this comment.
Read live policies before exposing Search transitions
When a workspace is deleted while offline and the user returns to a cached Search, this still exposes Submit because policy comes from the nested Search snapshot (getPolicyFromKey(data, report)), whereas deleteWorkspace() only sets pendingAction: DELETE on the live policy_<id> Onyx key; the Search screen does not subscribe to the live policy collection, so this helper never observes the optimistic deletion. The same stale policy also reaches the newly changed Approve, Pay, and Reject predicates. The fresh evidence beyond the earlier review thread is that these are separate Onyx keys and cached Search data is only reconciled with live transactions/reports, not policies, so evaluate these gates using the live policy collection or update the snapshot optimistically.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Subscribing to the policy key is not necessary to observe optimistic deletion. Policy data is also reconciled through updateSnapshots inside Onyx.update in OnyxUtils.
Reviewer Checklist
Screenshots/VideosMacOS: Chrome / Safariweb.movweb-2.mov |
|
@c3024 could you please merge main again? |
…ict-more-menu-archived-policies
rlinoz
left a comment
There was a problem hiding this comment.
Looks good, one minor request.
|
🚧 rlinoz has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/rlinoz in version: 9.4.64-0 🚀
|
|
Yes — help site changes are required. Draft PR: #99803 Two published articles described the old behavior:
How I confirmed the retract regressionBefore this PR,
UI labels verified in the live app (not inferred)Confirmed on dev NewDot web: Remove from workspace, Delete workspace, Retract, Unapprove, Cancel payment, Change workspace, Approve, Pay. Member profile panel showing 'Remove from workspace' button
Workspace list three-dot menu showing 'Delete workspace'
Report More menu (Outstanding) showing 'Retract'
Report More menu (Approved) showing 'Unapprove'
Report More menu (Paid) showing 'Cancel payment'
Report More menu showing 'Change workspace'
Reopen could not be surfaced in any state I could reach without a real bank payment or an accounting export integration, so its label is taken from your PR description rather than from the live UI. Worth a second look when you review. The draft PR is labelled @c3024, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |






Explanation of Change
Report state transitions (Submit, Approve, Pay, Unapprove, Cancel payment, Retract, Reopen) are now hidden only when the report's policy is archived, keyed on
isArchivedPolicy. Reports archived for other reasons, such as the submitter being removed from the workspace, keep these actions so their outstanding reports can still be approved and paid. IOU and invoice reports keep the existing archived report checks because they have no policy archived state. The same keying is applied tocanIOUBePaid,canApproveIOUand the report preview actions, which drive the preview button and the settlement button payment options.Fixed Issues
$ #98868
PROPOSAL:
Tests
Removed member flow:
Archived policy flow:
Onyx.merge('policy_<policyID>', {archivedDate: '2026-01-01'})with the policyID created in step 1.Screen.Recording.2026-08-20.at.2.28.58.PM.mov
Screen.Recording.2026-08-19.at.8.32.06.PM.mov
Offline tests
Same as Tests.
QA Steps
Same as above.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-08-19.at.8.35.22.PM.mov
Screen.Recording.2026-08-19.at.8.32.06.PM.mov