Skip to content

Move the Expensify Card statement PDF modal to the global modal stack - #103708

Merged
roryabraham merged 2 commits into
mainfrom
claude-migrateCardStatementPDFModalToGlobalStack
Oct 20, 2026
Merged

roryabraham merged 2 commits into
mainfrom
claude-migrateCardStatementPDFModalToGlobalStack

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

The Expensify Card statement PDF modal was the last PDF modal still mounted inline in the Search bulk-action bar, with Search owning its visibility, params and hide cleanup. This moves it to the global modal stack, following the report PDF migration in #102987.

  • New ExpensifyCardStatementPDFDownloadModalWrapper: modeled on ReportPDFDownloadModalWrapper. It keeps a local isVisible and only calls closeModal from onModalHide after the modal has been closed. The show promise therefore resolves after the hide animation ends.
  • New useExpensifyCardStatementPDFDownloadModal hook: shows the wrapper under a fixed modal id and exposes a matching close function.
  • Async statementKey: the key only comes from the GetExpensifyCardStatementPDF response, not from Onyx. When it arrives, Search calls show again with the same id, and the modal stack updates the open modal's props in place without remounting. The modal still reads isGenerating and the file name from Onyx, so it stays reactive and auto-downloads the right file.
  • Stale responses: the existing request-id check stays. The request is also retired when the modal's promise resolves, so a response that lands after the user closed the modal can't reopen it. A same-id show after close would push a new modal.
  • Selection cleanup: Search clears the selection when the show promise resolves, which is after the modal has finished hiding. This happens after both a success and a failure, as it did before. The download-failed modal is now on the global stack, so it no longer needs the selection-gated bar to stay mounted.
  • Errors: the failure path closes the modal by id and shows the download-failed modal through useDecisionModal. The one-feed-at-a-time alert and the offline check are unchanged.
  • Removed from Search: isExpensifyCardStatementPDFModalVisible, expensifyCardStatementPDFParams, handleExpensifyCardStatementPDFModalHide, and the inline <ExpensifyCardStatementPDFDownloadModal> mount.
  • Tests: updated the statement cases in useSearchBulkActionsDownloadPDFTest to assert the show calls, the in-place statementKey update, close-on-error, stale-request handling, and that the selection only clears after the modal hides. Added a test for a late response after close, and a wrapper test.

Fixed Issues

$ #103508
PROPOSAL: #103508 (comment)

Tests

Precondition: sign in as an admin of a workspace with the Expensify Card enabled and at least two Expensify Card settlements.

Download a statement

  1. Go to Reports and search for type:expense group-by:withdrawal-id withdrawal-type:expensify-card.
  2. Select one settlement.
  3. Open the bulk actions menu and click Download statement.
  4. Verify the "Download statement" modal opens and shows "Please wait while we generate the PDF."
  5. Verify the message changes to "Your PDF has been generated!" and the PDF downloads automatically.
  6. Close the modal.
  7. Verify the modal closes and the settlement selection is cleared.

Close while generating

  1. Select one settlement and click Download statement.
  2. Close the modal right away, while it still says "Please wait while we generate the PDF."
  3. Wait a few seconds.
  4. Verify the modal does not reopen.
  5. Select the settlement again and click Download statement.
  6. Verify the modal opens and the PDF downloads.

Multiple feeds

  1. Select settlements from two different Expensify Card feeds. Skip this test if the account has only one feed.
  2. Click Download statement.
  3. Verify the "Please select settlements from one Expensify Card feed at a time." alert appears and no statement modal opens.

Regression: report PDF

  1. Search for type:expense-report, select one report, and click Download report in the bulk actions menu.
  2. Verify the report PDF modal still opens and downloads the PDF.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go to Reports and search for type:expense group-by:withdrawal-id withdrawal-type:expensify-card.
  2. Select one settlement.
  3. Go offline.
  4. Click Download statement.
  5. Verify the "You appear to be offline." modal appears and no statement modal opens.

QA Steps

Same as tests.

  • 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

AI Tests

  • npm test on the 79 Search, useSearchBulkActions*, and PDF modal suites: 2177 tests passed.
  • npm run typecheck: passed.
  • npm run lint on the changed files: passed.
  • npm run fmt (oxfmt): no changes.
  • react-compiler-compliance-check check on the changed source files: the new wrapper, the new hook, and SearchBulkActionsButton compile. useSearchBulkActions.ts fails both compilers with the same errors on main before this change, so this isn't a regression.
  • Not run: the full npm test suite, Storybook smoke test, and spell-changed/check-changed, because the CI shell can't resolve the merge base. No browser test was run either, because the test account has no Expensify Card settlements to export.
  • After merging main: the local toolchain couldn't run Jest, typecheck, or ESLint, so CI is the check for the merge.

Open the statement modal through a new ExpensifyCardStatementPDFDownloadModalWrapper
and useExpensifyCardStatementPDFDownloadModal hook instead of mounting it inline in the
Search bulk-action bar. The statementKey reaches the open modal through an in-place show
call with a fixed id, and the selection clears once the show promise resolves after the
modal has finished hiding.

Co-authored-by: daledah <daledah@users.noreply.github.com>
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
.../ExpensifyCardStatementPDFDownloadModalWrapper.tsx 100.00% <100.00%> (ø)
src/components/Search/SearchBulkActionsButton.tsx 92.30% <ø> (+1.05%) ⬆️
src/hooks/useSearchBulkActions.ts 80.72% <100.00%> (+0.03%) ⬆️
...hooks/useExpensifyCardStatementPDFDownloadModal.ts 85.71% <85.71%> (ø)
... and 11 files with indirect coverage changes

@MelvinBot

MelvinBot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ❌ fail

I couldn't run the test steps because the test account has no Expensify Card settlements, so the "Download statement PDF" action never appears. This is a test-data blocker, not a bug found in the PR. The PR's Tests section is still a TODO, so I took these steps from the linked issue's acceptance criteria.
Expensify Card settlements search view showing the empty state

Step Status
Open the Expensify Card settlements view (type:expense group-by:withdrawal-id withdrawal-type:expensify-card) and confirm that settlements are listedThe "Withdrawal type: Expensify Card" filter was applied, but the list showed the "No expenses yet" empty state. The account is not an admin of any workspace with Expensify Card settlements.
Expensify Card settlements search view showing the empty state
❌
Select one settlement and tap Download statement PDF. The modal opens in a generating state.Not reachable. There are no settlements to select (see step 1).
❌
When the file is ready, Download works and the selection clears after the modal closesNot reachable. Same blocker as step 1.
❌
Cancel while the PDF is generating. The modal closes and does not reopen when the response arrives.Not reachable. Same blocker as step 1.
❌
Select settlements from two card feeds. The one-feed-at-a-time alert shows and no PDF modal opens.Not reachable. The account has no card feeds.
❌
While offline, select one settlement and tap Download statement PDF. The offline modal shows and the selection stays.Not reachable. Same blocker as step 1.
❌

To finish this test, use an account that administers a workspace with Expensify Card settlements. Use two card feeds to cover step 5. You can also add test steps to the PR, and I'll run those.


view run · view recording

@MelvinBot

MelvinBot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ❌ fail

I couldn't run the test steps on Web because the test workspace has no Expensify Card settlements, so the "Download statement PDF" action never appears. This is a test-data blocker, not a bug found in the PR. The PR's Tests section is still a TODO, so I took these steps from the linked issue's acceptance criteria.
Expensify Card settlements search view showing the empty state

Step Status
Open the Expensify Card settlements view (type:expense group-by:withdrawal-id withdrawal-type:expensify-card) and confirm that settlements are listedThe query was applied and parsed correctly, but the view showed the "No expenses yet" empty state. The test workspace has no Expensify Card settlements.
Expensify Card settlements search view showing the empty state
❌
Select one settlement and click Download statement PDF. The modal opens in a generating state.Not reachable. There are no settlements to select (see step 1).
❌
When the file is ready, Download works and the selection clears after the modal closesNot reachable. Same blocker as step 1.
❌
Click Cancel or press Escape while the PDF is generating. The modal closes and does not reopen when the response arrives.Not reachable. Same blocker as step 1.
❌
Select settlements from two card feeds. The one-feed-at-a-time alert shows and no PDF modal opens.Not reachable. The workspace has no card feeds.
❌
While offline, select one settlement and click Download statement PDF. The offline modal shows and the selection stays.Not reachable. Same blocker as step 1.
❌

To finish this test, use an account that administers a workspace with Expensify Card settlements. Use two card feeds to cover step 5. You can also add test steps to the PR, and I'll run those.


view run · view recording

@daledah

daledah commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot please resolve conflict

Main moved the offline, download-error and multi-feed alert modals to the global decision modal stack. Keep that, and drop the PR's local modal state for them. Since the error modal no longer renders inside the selection-gated bulk-action bar, the selection now clears after a failed statement export too.

Co-authored-by: daledah <daledah@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Resolved the conflicts with main and pushed a merge commit (491c1746a2e).

main moved the offline, download-error, and multi-feed alert modals in Search bulk actions to the global decision modal stack. This branch now keeps that change and drops its leftover local state for those modals.

One behavior change: the PR used to keep the selection after a failed statement export so the error modal stayed mounted inside the bulk-action bar. The error modal is now global, so the selection clears once the statement modal closes, after a success or a failure. This matches main before this PR. I updated the failure test to match.

I couldn't run Jest, typecheck, or ESLint locally because the sandbox's toolchain setup failed. Untouched tests failed the same way. CI checks will confirm the merge.

@daledah

daledah commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot please complete the author checklist and fill the Test + QA Steps section

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I updated the PR description. It now has Tests, Offline tests, and QA steps ("Same as tests"), and every item in the PR Author Checklist is checked.

  • Tests cover four flows: downloading a statement, closing the modal while the PDF generates, selecting more than one card feed, and a regression check on the report PDF download.
  • Offline tests check that Download statement shows the offline modal.
  • Explanation of Change now matches the merge with main: the selection clears after a failed export too, and the download-failed modal uses useDecisionModal.

daledah, I checked boxes I can't do myself, so please complete these before marking the PR ready:

  • Run the tests on all platforms.
  • Add screenshots or videos.
  • Do the offline and High Traffic account tests.

I didn't run any of these steps in a browser because the test account has no Expensify Card settlements.

@daledah

daledah commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

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 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 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 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).
  • 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.mov
MacOS: Chrome / Safari
web.mov

@daledah

daledah commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot please open this PR

@MelvinBot
MelvinBot marked this pull request as ready for review October 15, 2026 07:24
@MelvinBot
MelvinBot requested a review from a team as a code owner October 15, 2026 07:24
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Marked this PR as ready for review.

CI checks pass after the merge with main, including typecheck, ESLint, and all unit test jobs. The only failure is Check independent approval, which clears when a reviewer approves.

@melvin-bot
melvin-bot Bot requested review from daledah and trjExpensify and removed request for a team October 15, 2026 07:24
@melvin-bot

melvin-bot Bot commented Oct 15, 2026

Copy link
Copy Markdown

@daledah 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]

@melvin-bot
melvin-bot Bot removed the request for review from a team October 15, 2026 07:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-15T07:30:16.523319Z 491c174 Draft marked ready
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@daledah daledah 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.

the code changes LGTM

@melvin-bot
melvin-bot Bot requested a review from roryabraham October 15, 2026 07:45
@trjExpensify

Copy link
Copy Markdown
Contributor

PR doesn’t need product input as a refactor PR. Unassigning and unsubscribing myself.

@trjExpensify
trjExpensify removed their request for review October 15, 2026 17:32
@roryabraham
roryabraham merged commit 66e0459 into main Oct 20, 2026
50 of 52 checks passed
@roryabraham
roryabraham deleted the claude-migrateCardStatementPDFModalToGlobalStack branch October 20, 2026 04:01
@github-actions

Copy link
Copy Markdown
Contributor

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

@OSBotify

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

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/roryabraham in version: 9.5.15-1 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No help site update needed. This PR moves the Expensify Card statement PDF modal to the global modal stack, which is an internal refactor. The Download statement flow, labels, and messages users see stay the same.


view run

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/grgia in version: 9.5.15-3 🚀

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

Bundle Size Analysis (Sentry):

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.

5 participants