Skip to content

fix: disable report preview Submit button when strict workflow rules block submission - #97946

Merged
lakchote merged 6 commits into
Expensify:mainfrom
kamronbek04:fix/92367-preview-submit-strict-rules
Sep 7, 2026
Merged

lakchote merged 6 commits into
Expensify:mainfrom
kamronbek04:fix/92367-preview-submit-strict-rules

Conversation

@kamronbek04

@kamronbek04 kamronbek04 commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

There are two Submit buttons for an expense report: the report header button (SubmitPrimaryAction) and the report preview card button in chat (SubmitActionButton). Only the header was wired to the strict-policy-rules gate, so on a domain with Strictly Enforce Workflow Rules a violating report showed a disabled header button but an enabled preview button. Pressing the preview button fired SubmitReport, the backend rejected it, and the user only saw the generic "Unexpected error. Please try again later."

This PR mirrors the header's existing two-layer gate inside SubmitActionButtonContent:

  • compute shouldBlockSubmit from the same helpers the header uses — shouldBlockSubmitDueToStrictPolicyRules (with areStrictPolicyRulesEnabled from useStrictPolicyRules) OR shouldBlockSubmitDueToPreventSelfApproval — with identical argument order and variable naming, so a git grep shouldBlockSubmit parity audit passes across SubmitPrimaryAction.tsx, useLifecycleActions.tsx, and this file;
  • pass isDisabled={shouldBlockSubmit} to AnimatedSubmitButton (which already forwards it to Button);
  • early-return at the top of handleSubmit, above the isSubmitPolicy popover branch — the submit-to popover runs the submission itself, so a lower guard would leave that path unblocked. This matches the guard position in today's SubmitPrimaryAction.handleSubmit.

Violations come from the preview's own context (useReportPreviewTransactionViolations) — the same source the component's existing hasViolations/hasAnyPendingRTERViolation calls use.

No copy changes (src/languages/en.ts untouched); the fix prevents the doomed call rather than rewording its error.

Notes for the reviewer (scope decisions):

  • The accepted proposal also sketched a secondary fix (stop overwriting the backend's failure detail with iou.error.other in submitReport's failureData). That is deliberately not included: it depends on SubmitReport returning violation detail in response.onyxData, which needs backend confirmation first (asked in the issue).
  • The strict-rules gate receives dismissal-filtered violations, replicating useTransactionsAndViolationsForReport's filtering as a pure computation over data already in the preview context (no extra Onyx subscriptions per preview card) — so the preview and header gates receive semantically identical input, including for dismissals that need report/owner/policy context (this addresses the Codex review's P2). The component's pre-existing hasViolations/hasAnyPendingRTERViolation calls keep receiving the raw slice, as they did before this PR — changing them is out of scope.
  • Other strict-rules-blind Submit surfaces exist (Search row/bulk Submit, the submit-to popover's other entry points, LHN SUBMIT badge). Happy to file follow-up issues; this PR fixes the reported surface.
  • Root cause is a regression: PR Block report submission when strict policy rules are enabled with violations #72663 gated the preview via a REVIEW action swap, and commit 796e69d later removed that action wholesale along with its gate and test.

Fixed Issues

$ #92367
PROPOSAL: #92367 (comment)

Tests

Strict workflow rules (main path):

  1. Use an account on a private domain whose security group has Strictly Enforce Workflow Rules enabled (OldDot: Settings > Domains > [domain] > Groups > edit group).
  2. On a workspace with Workflows > Delay submissions > Manually, create an expense that carries a blocking policy violation (e.g. enable "Members must categorize all expenses" and create an uncategorized expense).
  3. Open the workspace chat and find the report preview card → verify the Submit button is disabled.
  4. Open the report itself → verify the header Submit button is also disabled (the two buttons agree).
  5. Fix the violation (categorize the expense) → verify both buttons re-enable and submitting from the preview works.
  6. Regression: on the same domain, a report without violations submits normally from the preview; on an account without strict rules, a violating report still submits from the preview (violations alone do not block).

Prevent self-approval (second operand):
7. On a workspace with Workflows > Add approvals where the approver is the submitter themselves, enable Prevent self-approvals (Workflows > Advanced) → verify the preview Submit button is disabled; disable the setting → it re-enables.

  • Verify that no errors appear in the JS console

Offline tests

  1. Set up the blocked state from Tests step 1–3, then go offline.
  2. Verify the preview Submit button stays disabled (domain security group and violations are cached in Onyx).
  3. Go back online → state unchanged.
  4. Note: a brand-new session that has never hydrated the security group fails open (button enabled) — the backend remains the enforcement point, matching the header's existing behavior.

QA Steps

Same as Tests (requires a domain with Strictly Enforce Workflow Rules on staging).

  • 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 followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and 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 followed the guidelines as stated in the Review Guidelines
  • 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 a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • 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.
  • 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.

Note on platform coverage: the change is platform-agnostic shared logic (a boolean gate + isDisabled on an existing shared component) with no platform-specific code or styling. I don't have access to a Mac or iOS hardware; native platforms can be verified via an adhoc test build or during C+ review — happy to follow up on any platform on request. The blocked state in the screenshots reproduces the domain security-group flag and server violation locally (the real "Strictly Enforce Workflow Rules" setting requires a private-domain admin), which drives the exact same Onyx-derived gate the fix reads.

Screenshots/Videos

Android: Native

No local HybridApp build environment available — requesting verification via adhoc test build / C+ review. The gate is shared JS with no native-specific code.

Android: mWeb Chrome

Real device (Xiaomi Redmi Note, Android Chrome). Blocked: violation shown, Submit disabled — while the unaffected report above keeps its active Approve button. Then violation cleared: Submit re-enables.

Android mWeb blocked
Android mWeb re-enabled

iOS: Native

No macOS/iOS hardware available — requesting verification via adhoc test build / C+ review. The gate is shared JS with no native-specific code.

iOS: mWeb Safari

No iOS hardware available; captures below are the identical responsive bundle at a mobile viewport (the same code path mobile Safari loads). Happy to re-verify on a real device via adhoc/C+ if needed.

mWeb responsive blocked - chat
mWeb responsive blocked - report

MacOS: Chrome / Safari

Desktop Chrome. Blocked preview (violation + disabled Submit), then the same report open — header and preview Submit both disabled (parity with SubmitPrimaryAction), then state cleared — both re-enabled (no over-blocking).

Web blocked preview
Web header and preview parity
Web re-enabled

…ubmission

Mirror the report header's SubmitPrimaryAction gate in the report
preview's SubmitActionButton: compute shouldBlockSubmit from
shouldBlockSubmitDueToStrictPolicyRules and
shouldBlockSubmitDueToPreventSelfApproval, disable the button, and
early-return in handleSubmit before the submit-to popover branch so no
doomed SubmitReport call reaches the backend.

Previously only the header button was gated, so the preview button on
the same report stayed enabled and clicking it surfaced the generic
"Unexpected error. Please try again later."
Issue: Expensify#92367

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Bb6ztR6SVJyk85SaHhL1a
@github-actions

github-actions Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@kamronbek04

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

exfy-clabot Bot added a commit to Expensify/CLA that referenced this pull request Aug 6, 2026
@kamronbek04
kamronbek04 marked this pull request as ready for review August 6, 2026 09:24
@kamronbek04
kamronbek04 requested review from a team as code owners August 6, 2026 09:24
Copilot AI lite review requested due to automatic review settings August 6, 2026 09:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@melvin-bot
melvin-bot Bot removed request for a team August 6, 2026 09:24
@melvin-bot

melvin-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

@mananjadhav Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7fe6a8b73

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +87 to +89
const isBlockSubmitDueToStrictPolicyRules = shouldBlockSubmitDueToStrictPolicyRules(
iouReport?.reportID,
transactionViolations,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter dismissed violations before disabling preview submit

When strict rules are enabled, this passes the preview context’s raw transactionViolations slice into shouldBlockSubmitDueToStrictPolicyRules, whereas the header uses the filtered violations returned by useTransactionsAndViolationsForReport. That matters for dismissed violations that require report/policy context to ignore (for example instant-submit RTER dismissals by another participant): the helper’s hasAnyViolations call receives no report or policy here, so it treats those dismissed violations as still blocking and leaves the preview Submit button disabled even though the report header can submit. Please feed the same filtered violations as the header or extend the helper so this path filters dismissals with the report, submitter, and policy context.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in cfbfe86 — the strict-rules gate now receives dismissal-filtered violations, replicating useTransactionsAndViolationsForReport's filtering as a pure computation over the preview context's own data (no additional Onyx subscriptions per preview card), so the preview and header gates receive semantically identical input. Covered by a new unit test asserting the gate gets the filtered collection with the full report/owner/policy context.

Replicate useTransactionsAndViolationsForReport's dismissal filtering as
a pure computation over the preview context's data, and feed the filtered
collection to shouldBlockSubmitDueToStrictPolicyRules. The header's gate
receives pre-filtered violations, so without this the two Submit buttons
disagree on violations whose dismissal is only detectable with
report/owner/policy context.

Addresses the Codex review P2 on the pull request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Bb6ztR6SVJyk85SaHhL1a
@mananjadhav

Copy link
Copy Markdown
Collaborator

Reviewing the PR.

@JmillsExpensify JmillsExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No product review required.

// The header's gate receives violations pre-filtered by useTransactionsAndViolationsForReport, which drops dismissals
// that are only detectable with report/owner/policy context (e.g. RTER violations dismissed under instant submit). The
// preview context exposes the raw slice, so apply the same filter here or the two Submit buttons disagree on dismissed
// violations. This stays a pure computation over data already in scope; calling the hook instead would duplicate its

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop is a copy of useTransactionsAndViolationsForReport. The comment explains why we can’t call the hook (extra Onyx per preview card). I think it's worth extracting a shared pure helper later so the two can’t drift

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the duplication is the one liability of this approach, and a shared pure helper (e.g. a filterDismissedTransactionViolations(transactions, violations, email, accountID, report, ownerLogin, policy) in TransactionUtils) would let both call sites use it without adding Onyx subscriptions. Happy to do it whichever way you prefer: extract it in this PR, or keep this diff focused and handle it as a follow-up since useTransactionsAndViolationsForReport has many consumers. Your call.

});

it('disables the button and blocks submission when the workspace prevents self-approval', () => {
// Given a workspace that prevents self-approval on a report whose next approver is the submitter

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be we can add expect(mockedShouldBlockSubmitDueToPreventSelfApproval).toHaveBeenCalledWith(iouReport, /* policy */ …), wdyt?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in e3b882f — added expect(mockedShouldBlockSubmitDueToPreventSelfApproval).toHaveBeenCalledWith(iouReport, undefined). The second argument is undefined because the policy is never hydrated in this fully-mocked environment; left a comment in the test noting that so it doesn't read like a typo.

mockedShouldBlockSubmitDueToStrictPolicyRules.mockReturnValue(true);

// When the button renders and its press handler runs anyway, because a disabled button swallows taps but the
// handler stays reachable through the keyboard shortcut and any in-flight press

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early return is good to test, but the comment about keyboard shortcuts might not be correct. This button doesn’t use pressOnEnter. Something like “call onPress directly to confirm it no-ops when blocked” would be clearer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — you're right, this button doesn't use pressOnEnter. Reworded in e3b882f: the comment now says onPress is called directly to confirm the handler no-ops on its own (the disabled prop is the first layer of defense, the early return in the handler is the second).

@mananjadhav mananjadhav left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comments @kamronbek04. Testing it out.

@mananjadhav

Copy link
Copy Markdown
Collaborator

@kamronbek04 One thing I want to highlight is that we need builds for all the platforms. I am requesting adhoc builds for now. I hope you're able to run the tests on those builds.

@mananjadhav

Copy link
Copy Markdown
Collaborator

@lakchote Can you help with the adhoc build here?

Assert shouldBlockSubmitDueToPreventSelfApproval receives the report and
policy the component reads, and reword an inaccurate test comment: this
button has no keyboard shortcut, the direct onPress call exercises the
handler's early return as the second layer behind the disabled prop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwjvYBYjgntCP6ZeKgEfga
@kamronbek04

Copy link
Copy Markdown
Contributor Author

Thanks @mananjadhav — both test comments addressed in e3b882f (9/9 passing, lint/format/typecheck clean), and replied inline on the helper extraction. On the adhoc builds: once they're posted I'll run the tests on the Android build on a physical device (same device as the mWeb Chrome screenshots) and add native screenshots to the PR body. I don't have iOS/macOS hardware, so I'd appreciate verification on the iOS build during your review.

@mananjadhav

Copy link
Copy Markdown
Collaborator

Reviewing the changes again.

@mananjadhav

mananjadhav commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and 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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp android-submit-role
Android: mWeb Chrome mweb-chrome-submit-role
iOS: HybridApp ios-submit-role
iOS: mWeb Safari mweb-safari-submit-role
MacOS: Chrome / Safari web-submit-role

@mananjadhav

Copy link
Copy Markdown
Collaborator

@kamronbek04 Please resolve conflicts.

@mananjadhav mananjadhav left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be good once conflicts are resolved.

@melvin-bot
melvin-bot Bot requested a review from lakchote August 20, 2026 23:02
@kamronbek04

Copy link
Copy Markdown
Contributor Author

@mananjadhav conflicts resolved — main merged in (bec4488c83a), GitHub now reports the PR as mergeable.

Both conflicts were additive collisions rather than genuine disagreements:

SubmitActionButton.tsx

  • Import block: took the union — hasOnlyHeldExpenses / showHeldExpensesBlockModal from main alongside shouldBlockSubmitDueToStrictPolicyRules / shouldBlockSubmitDueToPreventSelfApproval / getTransactionViolations from this PR.
  • main relocated shouldShowMarkAsDoneCopy above handleSubmit (the block modals now take it as a third argument), which landed on the exact lines where this PR adds the strict-rules gate. Kept both, with shouldShowMarkAsDoneCopy immediately above handleSubmit as main has it.
  • if (shouldBlockSubmit) return; is still the first statement in handleSubmit — above the pending-card modal, above main's new held-expenses modal, and above the submit-to popover — so none of those paths route around the block.

SubmitActionButtonTest.tsx

  • Import block and the jest.mock('@libs/ReportUtils') override list: union of both sides.
  • One worth flagging: git auto-merged main's new 'shows the held expenses block modal instead of submitting' test with no conflict marker, but it calls mockOnPressHolder.current?.() — cfbfe86 renamed that holder to mockSubmitButtonPropsHolder with an {onPress, isDisabled} shape. Left as-is it's a dangling identifier that fails the whole suite at compile time, so I updated it to mockSubmitButtonPropsHolder.current?.onPress?.(), matching every other call site in the file.
  • Also dropped the count from two now-stale comments on the @libs/TransactionUtils mock ("only these three functions" / "only the three exports") — main took it to five.

Nothing else in either file was hand-edited; the rest (useCurrencyListActions, betas, delegateAccountID, variant, the isMarkAsDone → shouldShowMarkAsDoneCopy rename) came through main's auto-merge untouched.

Verified locally on the merged head: ESLint clean, oxfmt clean, React Compiler compiles and memoizes on both Babel and OXC with no divergence, and typecheck reports no errors in either file.

The Check independent approval job is red purely because the new merge commit needs a fresh approval — ready for your re-review whenever you get a chance, which should also let you tick the last box on your checklist.

@lakchote

Copy link
Copy Markdown
Contributor

@kamronbek04

Copy link
Copy Markdown
Contributor Author

@lakchote Thanks for the ping — that finding isn't coming from this PR's diff.

This PR changes exactly two files, SubmitActionButton.tsx and SubmitActionButtonTest.tsx. It doesn't touch package.json or anything under .github/.

It was branch staleness. failureNotifier.ts has imported @octokit/plugin-rest-endpoint-methods since fdd18a6383f (February), and that line is identical on this branch and on main. What changed is that main has since landed baec14ff202 ("Fix knip regressions introduced by the Jest->Bun test migration"), which declares "@octokit/plugin-rest-endpoint-methods": "^17.0.0" in package.json. That commit was not an ancestor of the main I merged on Aug 21, so on this branch the import was undeclared ([unlisted]) while on main it is declared. The compare step diffs the two reports, so the drift surfaced as "added by PR".

Fixed by merging current main — c41fce173d7, no conflicts. package.json now declares the package, so the delta should be back to 0.

One thing worth flagging: the new Knip run is sitting at action_required and needs approval before it will execute.

On the merged head I verified that the PR's two files came through byte-identical, that the helpers they call are unchanged in signature (shouldBlockSubmitDueToStrictPolicyRules, shouldBlockSubmitDueToPreventSelfApproval, hasViolations, getTransactionViolations, hasAnyPendingRTERViolation), that tests/ui is still owned by Jest (the Bun runner only covers ./server and ./tests/tooling), and that every mocked module path in the test still resolves. I could not run the full suite locally on this machine, so CI is the gate there.

Check independent approval is also red, but that is just the new merge commit needing a fresh approval from @mananjadhav.

@lakchote

Copy link
Copy Markdown
Contributor

@kamronbek04 perf tests are failing

@kamronbek04

kamronbek04 commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor Author

@lakchote Thank you for flagging this — I took a close look at the run, and I believe the failure isn't caused by this PR's changes. It appears to be the same baseline-skew issue we saw with the Knip check. Please allow me to walk through the evidence.

The clearest signal comes from comparing the green Aug 21 run with the failing one, per scenario (baseline → branch):

Scenario Aug 21 (green) Aug 26 (failing)
Composer text input 3 → 3 1 → 3
press send 2 → 2 1 → 2
press create 4 → 4 3 → 4
SearchRouter cached options 2.10 → 2.10 1.10 → 2.10
SearchRouter text changes 5 → 5 4 → 5

The branch's render counts are identical in both runs, so the branch itself didn't regress. What changed is the baseline: main improved between the two runs, and because the comparison used mismatched snapshots, that improvement surfaced as an apparent regression on the PR side.

I was able to trace the cause: react-native-onyx 3.0.98 → 3.0.100 (#99301). Version 3.0.100 coalesces sync-event floods, which means fewer store notifications and 1–2 fewer renders in every reassure suite. The timeline, taken from the run's own logs:

  • Aug 25 05:50 UTC — I pushed c41fce; the workflow triggered, froze its merge snapshot at main = 1d0d7de38b4 (Onyx 3.0.98), and parked in action_required (fork PR).
  • Aug 25 08:16 — [Onyx bump] Bump react-native-onyx from 3.0.98 to 3.0.100 #99301 landed Onyx 3.0.100 on main.
  • Aug 26 07:49 — the run was approved and executed about 26 hours later: branch-perf-tests checked out the frozen Aug 25 snapshot, while baseline-perf-tests did a live git fetch origin main and got Aug 26 main (5778821, 406 commits ahead, Onyx 3.0.100). With COUNT_DEVIATION: 0, 1 → 3 fails immediately.

For completeness: none of the failing suites render SubmitActionButton (there is no reference to it anywhere under tests/perf-test/), and the PR's two files came through the merge byte-identical.

I've addressed it by merging current main — 9772f4ac19c — so the branch now carries Onyx 3.0.100 as well and both sides of the comparison are aligned. The new reassure run will need workflow approval again, same as last time — I'd appreciate it whenever someone has a moment.

One small observation that may be useful to the workflow maintainers: reassurePerformanceTests.yml measures the trigger-time merge snapshot against run-time main, so any fork PR whose run waits in action_required while a perf improvement lands on main can show that improvement inverted as a regression. I'd be glad to open an issue for it if that would be helpful.

Thanks again for the ping, and sorry for the extra review round-trips this is causing.

@lakchote lakchote left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kamronbek04

Copy link
Copy Markdown
Contributor Author

@lakchote Gentle bump — all checks are green on 9772f4ac19c and both approvals are in, so this should be ready to merge whenever you have a moment. Thank you!

@mananjadhav

Copy link
Copy Markdown
Collaborator

Quick bump @lakchote

@lakchote
lakchote merged commit 4600811 into Expensify:main Sep 7, 2026
33 of 34 checks passed
@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

🚧 lakchote has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify OSBotify mentioned this pull request Sep 8, 2026
69 tasks done
@OSBotify

OSBotify commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/lakchote in version: 9.4.74-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

Yes — help site changes are required, and I opened a draft PR for them: #100635

Why

This PR changes what a member sees: the Submit button on the report preview card in chat is now grayed out when a domain group enforces workspace rules on a report with violations, or when Prevent self-approvals blocks the submission. Previously the button looked active and the submission failed with a generic "Unexpected error. Please try again later." The help site documented neither the old error nor the new grayed-out state.

What changed in the docs PR

UI label verification and what I deliberately left alone

Verified against the live app on web:

  • The action button on the report preview card in chat and the button in the report header both read Submit (accessible label checked on both).
  • Prevent self-approvals matches exactly, under Advanced on the workspace Workflows page. Note the page sections are Submissions and Approvals — not "Delay submissions" / "Add approvals" as written in this PR's test steps — so I did not reuse that wording in the docs.
  • I could not verify the Rules label for requiring categories: Rules is gated behind a Control plan upgrade and upgrading is a billing action. The docs I wrote don't depend on that label.
  • Strictly enforce expense workspace rules is an Expensify Classic domain setting; I used the wording already in the help article rather than re-verifying it in the live UI.

Not changed:

  • Understanding-Report-Statuses-and-Actions.md — its "Where is the Submit or Approve button?" answer is about a missing button, and duplicating the disabled-button explanation there would break the one-workflow-per-article rule.
  • Workspace-Rules.md — Prevent self-approvals is already listed there; the submission-blocking consequence belongs in the submit article, which is where members search.

Both files follow HELP_AUTHORING_GUIDELINES.md, HELPSITE_NAMING_CONVENTIONS.md, and TEMPLATE.md: no new # headings, bold UI labels with no quotation marks, and relative cross-links placed outside the bulleted content.

One thing to flag: GitHub wouldn't let me set you as assignee on the docs PR because assignees need write access on Expensify/App. The HelpDot label is applied and PullerBear is requested as reviewer.

@kamronbek04, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

@jponikarchuk

Copy link
Copy Markdown

The Deploy Blocker issue #100692 is repro on all PRs. Could you please double check if it's not introduced by #97946

@kamronbek04

Copy link
Copy Markdown
Contributor Author

@jponikarchuk Double-checked — #97946 can be ruled out. It only changes the report preview's Submit button (SubmitActionButton.tsx + its test), and in the bug's state (report with zero transactions) that component never mounts — the preview takes the Add-expense/empty path before the Submit action is considered. Your own observation confirms the broader point: the issue disappearing when "Use Staging Server" is off means the staging bundle renders transactions correctly against the prod API, so the cause is on the staging backend (the App-side card assignment flow is unchanged between 9.4.73-3 and 9.4.74-0). Looks like the Plaid Direct feed import on the staging API is the place to look.

@OSBotify

OSBotify commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.4.74-2 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants