Skip to content

[Due for payment 2026-11-05] [$175] Decouple file download modal UI from report and Expensify Card business logic #104440

Open

Description

@roryabraham

Context

Follow-up to #103708, which was merged with this cleanup considered non-blocking because ExpensifyCardStatementPDFDownloadModalWrapper followed the existing ReportPDFDownloadModalWrapper pattern. Related report migration: #102987.

Problem

The global modal layer currently contains domain-specific wrappers for reports and Expensify Card statements. Both duplicate visibility/hide-animation lifecycle handling and render feature-specific components, even though they ultimately share PDFDownloadModal.

src/components/Modal should contain business-logic-agnostic UI building blocks, with no knowledge of reports or Expensify Card statements. This is an architectural cleanup, not another migration to the global modal stack.

Proposed cleanup

  • Provide a shared, generic FileDownloadModal in the modal layer, reusing the existing PDF download UI where appropriate.
  • Keep presentation, generic download states, visibility, and hide-animation lifecycle in the shared UI. Pass status, display text, and callbacks rather than domain identifiers or parameters.
  • Move/keep domain-specific behavior behind useExpensifyCardStatementPDFDownloadModal and useReportPDFDownloadModal: generation requests, Onyx subscriptions, filename resolution, download actions, error handling, and domain-specific cancellation.
  • Remove the duplicated domain-specific global wrappers. The modal layer should not import report/card-statement components or access their domain-specific Onyx keys.

Acceptance criteria

  • Both hooks consume the same business-logic-agnostic modal UI; src/components/Modal has no report/card-statement business logic.
  • Open modals remain reactive as generation state and filenames arrive, without remounting or reopening a dismissed modal.
  • Preserve automatic/manual downloads, loading/success/error states, existing close-on-download behavior, offline behavior, and report stall logging.
  • Preserve report cancellation semantics, including submit-via-PDF, and clear Search selection only after the modal finishes hiding.
  • Preserve stale-response protection and existing web/native presentation and dismissal behavior.
  • Update tests to cover the shared UI lifecycle and both domain-specific hook flows. No intended user-visible behavior change.
Issue OwnerCurrent Issue Owner: @daledah
Upwork Automation - Do Not Edit

Activity

  1. added theissue type on Oct 20, 2026
  2. daledah commented on Oct 21, 2026

    @daledah
    Contributor

    @MelvinBot please implement this issue in one PR, following these steps:

    1. Move src/components/PDFDownloadModal.tsx into the modal layer as src/components/Modal/FileDownloadModal.tsx. It only takes presentation props (status, message, onDownload, onClose, onModalHide, button flags). No Onyx, no report or card-statement imports.

    2. Add a single src/components/Modal/Global/FileDownloadModalWrapper.tsx that owns isVisible and calls closeModal once from onModalHide. Delete ReportPDFDownloadModalWrapper.tsx and ExpensifyCardStatementPDFDownloadModalWrapper.tsx.

    3. Move the logic of ReportPDFDownloadModal.tsx into useReportPDFDownloadModal: active reportID in state, Onyx subscriptions, useStallLogger, downloadReportPDF, and the cancel rule (onCancel only while the filename is still null). Show the wrapper with a fixed id and re-call showModal with the same id while open so props update in place; track open state so a late update never reopens a dismissed modal.

    4. Same for ExpensifyCardStatementPDFDownloadModal.tsx into useExpensifyCardStatementPDFDownloadModal: statementParams in state, EXPENSIFY_CARD_STATEMENT subscription, downloadExpensifyCardStatementPDF, keep the fixed id and closeModalByID. Leave the requestID guard and "clear selection after the promise" logic in useSearchBulkActions untouched.

    5. Switch MoneyReportHeaderModals.openPDFDownload to useReportPDFDownloadModal, then delete ReportPDFDownloadModal.tsx and ExpensifyCardStatementPDFDownloadModal.tsx. Nothing under src/components/Modal/** should import report or card-statement code or Onyx keys.

    6. Tests: add FileDownloadModalWrapperTest, useReportPDFDownloadModalTest and useExpensifyCardStatementPDFDownloadModalTest covering the hide lifecycle, in-place updates, cancel semantics and no-reopen after dismiss. Port and remove ReportPDFDownloadModalTest and ExpensifyCardStatementPDFDownloadModalWrapperTest, update the mocks in useSearchBulkActionsDownloadPDFTest.

  3. MelvinBot commented on Oct 21, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: Decouple the file download modal from report and Expensify Card logic
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    All 6 steps are implemented in one PR:

    Checks: typecheck, lint, spell check, and the React Compiler check (Babel and OXC) passed. 298 tests across 20 related Jest suites passed, including the 3 new test files.

    Not verified in a browser: I tried a web UI check, but the test browser session was blank because its onboarding setup failed. The modal has not been exercised manually.

    Out of scope: the existing Modal/Global/HoldMenuModalWrapper.tsx still reads report data from Onyx. I left it unchanged.

    Your fork doesn't give me push access, so the branch is on upstream. The PR has a comment with steps to move it to your fork.


    view run · no recording available

  4. changed the title [-]Decouple file download modal UI from report and Expensify Card business logic[/-] [+][$175] Decouple file download modal UI from report and Expensify Card business logic[/+] on Oct 24, 2026
  5. melvin-bot commented on Oct 24, 2026

    @melvin-bot
  6. mitarachim commented on Oct 27, 2026

    @mitarachim

    Deploy Blocker #105206 was identified to be related to the PR #104534 that attempted to fix this issue.

  7. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    ⚠️ Looks like this issue was linked to a Deploy Blocker here

    If you are the assigned CME please investigate whether the linked PR caused a regression and leave a comment with the results.

    If a regression has occurred and you are the assigned CM follow the instructions here.

    If this regression could have been avoided please consider also proposing a recommendation to the PR checklist so that we can avoid it in the future.

  8. changed the title [-][$175] Decouple file download modal UI from report and Expensify Card business logic[/-] [+][Due for payment 2026-11-05] [$175] Decouple file download modal UI from report and Expensify Card business logic[/+] on Oct 29, 2026
  9. melvin-bot commented on Oct 29, 2026

    @melvin-bot

    @daledah

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.24-8 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-11-05. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  10. daledah commented on Oct 29, 2026

    @daledah
    Contributor

    Contributor+ Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to the comment on the PR: N/A, refactor

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion:

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    Regression Test Proposal

    Precondition:

    Test:

    1. Search > Reports, select one report, click "Download report". Verify the PDF modal opens with a spinner, then auto-downloads and shows "Download" when ready.
    2. Repeat step 1 and click "Cancel" while generating. Verify the modal closes and does not reappear.
    3. Open a submitted report, click More > "Download report". Verify the same flow as step 1.
    4. On a workspace where you submit to yourself, choose "Submit via PDF" on a draft report and cancel while generating. Verify the report goes back to Draft. Repeat, wait until ready, then close. Verify the report stays submitted.
    5. Search > Expenses, select an Expensify Card settlement, click "Export as PDF". Click "Download" when ready. Verify the file downloads and the modal closes.

    Do we agree 👍 or 👎

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

Metadata

Metadata

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions