Repository navigation
Replace ProcessMoneyReportHoldMenu with useHoldMenuModal in SelectionToolbar and report preview - #104532
Replace ProcessMoneyReportHoldMenu with useHoldMenuModal in SelectionToolbar and report preview#104532MelvinBot wants to merge 4 commits into
Conversation
… the global modal Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
|
I created this PR from an upstream branch since I don't have push access to your fork. To take ownership of this branch and be able to push updates, run: Then you can close this PR and open a new one from your fork. |
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.
|
|
🤖 Android test steps verification results: ❌ fail None of the six test cases ran, so this result says nothing about the PR's code. The setup never produced a payable report that still had a held expense, and then the app froze on an unrelated "Create expense" action. A Metro restart did not fix the freeze. The PR's Tests section is still a placeholder, so I used the acceptance criteria from [Batch 3] Replace ProcessMoneyReportHoldMenu with useHoldMenuModal in SelectionToolbar and report preview (2 modals). To get a real result, add steps to the Tests section that produce a payable report with a held expense. For example, an IOU between two accounts where the payer holds one expense. Then ask me again. |
|
⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai. |
|
@MelvinBot |
|
@MelvinBot |
…MoneyReportHoldMenu Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com> # Conflicts: # src/components/MoneyRequestReportView/SelectionToolbar/index.tsx
|
@ZhenjaHorbach 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] |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
I merged Conflict detailsThe only conflict was in Checks I ran after the merge:
|
|
|
||
| const formattedAmount = getTotalAmountForIOUReportPreviewButton(iouReport, policy, reportPreviewAction, transactions, convertToDisplayString); | ||
|
|
||
| // Only the pay flow opens this menu; approve surfaces its partial/full choice in the approve dropdown instead. |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This new openHoldMenu helper duplicates logic that now lives in three places: here, in useSelectionModeReportActions.ts, and in MoneyReportHeaderModals.tsx. All three compute hasOnlyHeldExpenses / getNonHeldAndFullAmount, assemble the same showHoldMenu({... nonHeldAmount, fullAmount, hasNonHeldExpenses, transactionCount ...}) payload, and then defer the open by one frame on iOS with the identical if (getPlatform() === CONST.PLATFORM.IOS) { requestAnimationFrame(...) } block. This is a DRY violation that will drift as the hold-menu payload evolves.
Extract the shared payload assembly and the iOS deferral into a single reusable helper (for example a small function or an addition to useHoldMenuModal) and call it from all three sites. At minimum, factor the verbatim iOS deferral block into a shared helper so the requestAnimationFrame timing logic exists in one place:
// e.g. src/libs/runAfterTouchFrame.ts
function openAfterTouchFrame(open: () => void) {
if (getPlatform() === CONST.PLATFORM.IOS) {
requestAnimationFrame(open);
return;
}
open();
}Reviewed at: c8158a6 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Fixed in 3265ee6. I added src/libs/HoldMenuUtils.ts with two helpers:
getHoldMenuAmountsbuildsnonHeldAmount,fullAmount,hasNonHeldExpenses, andtransactionCount.openAfterTouchFrameholds the iOS one-frame deferral. It returns a promise, soMoneyReportHeaderModalskeeps itsPromise<void>contract.
All three callers now use them: PayActionButton, useSelectionModeReportActions, and MoneyReportHeaderModals. Each caller passes only what differs: the IDs, the payment fields, and onConfirm. tests/unit/HoldMenuUtilsTest.ts covers both helpers.
| const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(report, shouldShowPayButton, transactions, convertToDisplayString); | ||
| const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(transactions); | ||
|
|
||
| const openHoldMenu = (requestType: ActionHandledType, paymentType?: PaymentMethodType, methodID?: number, onConfirm?: () => void) => { |
There was a problem hiding this comment.
❌ CONSISTENCY-3 (docs)
This openHoldMenu helper duplicates the one added to PayActionButton.tsx (and the equivalent already in MoneyReportHeaderModals.tsx): the same showHoldMenu({... nonHeldAmount, fullAmount, hasNonHeldExpenses, transactionCount ...}) payload assembly and the same if (getPlatform() === CONST.PLATFORM.IOS) { requestAnimationFrame(() => open()); return; } iOS deferral. Keeping three near-identical copies means any change to the hold-menu contract or the deferral timing must be applied in multiple files.
Consolidate the shared payload building and the iOS deferral into a single reusable helper (or fold it into useHoldMenuModal.showHoldMenu) and have each caller pass only the data that actually differs (e.g. the selection-mode onConfirm that clears the selection).
Reviewed at: c8158a6 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Fixed in 3265ee6. This hook now calls the shared getHoldMenuAmounts and openAfterTouchFrame helpers from src/libs/HoldMenuUtils.ts. PayActionButton and MoneyReportHeaderModals call them too. The hook passes only its own data: the VBBA-only methodID and the onConfirm that clears the selection.
| @@ -74,14 +85,45 @@ function PayActionButton() { | |||
|
|
|||
| const formattedAmount = getTotalAmountForIOUReportPreviewButton(iouReport, policy, reportPreviewAction, transactions, convertToDisplayString); | |||
|
|
|||
There was a problem hiding this comment.
❌ CONSISTENCY-15 (docs)
This comment joins two independent sentences with a semicolon, which the plain-comment-style rule calls out. Split it into two sentences:
// Only the pay flow opens this menu. Approve surfaces its partial/full choice in the approve dropdown instead.Reviewed at: c8158a6 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
|
@MelvinBot |
Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
|
@roryabraham 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] |
|
I fixed the three review comments and pushed 3265ee6. I also replied in each thread.
Checks
|
Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
|
Fixed the failing The other failing checks are |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-10-23.at.13.19.55.movAndroid: mWeb ChromeScreen.Recording.2026-10-23.at.13.18.21.moviOS: HybridAppScreen.Recording.2026-10-23.at.13.15.57.moviOS: mWeb SafariScreen.Recording.2026-10-23.at.13.06.36.movMacOS: Chrome / SafariScreen.Recording.2026-10-23.at.13.02.19.mov |
|
LGTM! |
|
|
||
| /** Opens a modal. On iOS, it waits one frame first so the current touch animation finishes before the modal opens. */ | ||
| function openAfterTouchFrame<T>(open: () => T | PromiseLike<T>): Promise<T> { | ||
| if (getPlatform() === CONST.PLATFORM.IOS) { |
There was a problem hiding this comment.
This doesn't confirm to our cross-platform code philosophy. Let's just drop the platform check. Waiting one frame on all platforms shouldn't be a problem I think.
There was a problem hiding this comment.
It also doesn't seem like it needs to return a promise? Maybe we can just use requestAnimationFrame directly at the callsites?











Explanation of Change
ProcessMoneyReportHoldMenuwas an older, pay-only copy of the globalHoldMenuModalWrapper. This PR moves its last two callers touseHoldMenuModal().showHoldMenu, as the report header already does, and deletes the old component.useSelectionModeReportActions): bothonHoldMenuOpenhandlers now callshowHoldMenu. They passrequestType,paymentType,methodID(VBBA only), the amounts, andonConfirm.onConfirmnow runs the lifecycle callback that was dropped before (_onConfirm, for example marking the report as paid elsewhere), the same way the header dropdown does, then clears the selection. The local hold-menu state,handleHoldMenuClose, andhandleHoldMenuConfirmare removed, along with the hold-menu block inSelectionToolbar.PayActionButton.confirmPayment): callsshowHoldMenudirectly. It computes the amounts withgetNonHeldAndFullAmount, passes the report IDs (the wrapper reads the reports from Onyx), and passesonConfirm: startAnimation.ReportPreviewHoldMenu, the provider'sholdMenustate,onHoldMenuOpen/onHoldMenuClose, andReportPreviewHoldMenuContextare removed.ActionHandledTypeimports now point at@hooks/useHoldMenuSubmit.MoneyReportHeaderModals.Expected differences:
ExpenseReportListItemalready work this way.transactionCountnow excludes expenses pending deletion. The hook only receives those transactions, and the count is only used in the "all expenses on hold" prompt.Tests: deleted
ReportPreviewHoldMenuTest.tsx. Removed the provider-level hold-menu test fromMoneyRequestReportPreview.test.tsx; that wiring no longer exists. UpdatedPayActionButtonTest.tsxanduseSelectionModeReportActions.test.tsto assert theshowHoldMenucalls, including VBBAmethodIDforwarding and the forwarded lifecycleonConfirm.AI checks run locally:
npm run typecheckpassed. ESLint on all changed files passed.react-compiler-compliance-check check <changed files>passed.oxfmtwas applied.jest --findRelatedTestson the changed source files passed: 23 suites, 495 tests.spell-changedcould not run becausegit merge-basefailed in the shallow clone. An automated web run of the Tests steps passed the report preview and selection mode pay flows (screenshots under MacOS: Chrome / Safari). Other platforms are still untested.Fixed Issues
$ #102977
PROPOSAL:
Tests
Setup (one workspace admin account is enough)
Report preview
Selection mode
Dismiss
Offline tests
QA Steps
Same as Tests.
On iOS native, also verify the hold menu opens right after you tap Pay in both the report preview and selection mode, with no stuck or frozen screen.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)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
Tested on web (Chrome) by MelvinBot. Report preview: Pay → Mark as paid opens the hold menu; Pay only pays $10 and moves the held expense to a new report.
Selection mode: Pay in the selection toolbar → Mark as paid opens the hold menu; Pay $30.00 pays the full report and clears the selection.