-
Notifications
You must be signed in to change notification settings - Fork 4k
Decompose MoneyReportHeader modals into imperative hooks and context provider #87063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rlinoz
merged 22 commits into
Expensify:main
from
callstack-internal:organize-moneyreportheader-modals
Apr 7, 2026
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
697b683
Refactor useNonReimbursablePaymentModal to be fully imperative
TMisiukiewicz 5caacde
Refactor ProcessMoneyReportHoldMenu to use onConfirm callback pattern
TMisiukiewicz 0815e3b
Add MoneyReportHeaderModalsContext
TMisiukiewicz c4656c9
Add MoneyReportHeaderModals provider component
TMisiukiewicz 25ec7ca
Wire MoneyReportHeader to use MoneyReportHeaderModals provider
TMisiukiewicz add9996
Update MoneyReportHeaderPrimaryAction to consume modals context directly
TMisiukiewicz a7fd0d0
Migrate ProcessMoneyReportHoldMenu to imperative useHoldMenuModal hook
TMisiukiewicz 583665e
Fix lint and type errors
TMisiukiewicz 76ab787
Restore original non-reimbursable check in hold menu
TMisiukiewicz 9819121
Fix isSelectionModePaymentRef reset and getNonHeldAndFullAmount param
TMisiukiewicz e51c7ac
Merge remote-tracking branch 'origin/main' into organize-moneyreporth…
TMisiukiewicz 14120b2
Fix modals context by splitting provider and consumer into separate c…
TMisiukiewicz af57247
Inline IIFE to ternary in HoldMenuModalWrapper
TMisiukiewicz c48f125
Encapsulate educational modals state and data fetching
TMisiukiewicz 115a0b6
Merge remote-tracking branch 'origin/main' into organize-moneyreporth…
TMisiukiewicz 130af46
Move decision modals into MoneyReportHeaderModalsContext
TMisiukiewicz 67e95ff
remove unused param from PayPrimaryAction
TMisiukiewicz 1fd24f3
fix prettier
TMisiukiewicz 8e07137
Merge remote-tracking branch 'origin/main' into organize-moneyreporth…
TMisiukiewicz e8aa264
Address PR review comments: extract useHoldMenuSubmit, re-add iOS Int…
TMisiukiewicz a37dcd3
Fix Prettier: sort imports in useHoldMenuSubmit
TMisiukiewicz 6ce1795
Suppress no-deprecated for InteractionManager.runAfterInteractions
TMisiukiewicz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import React, {useState} from 'react'; | ||
| import DecisionModal from '@components/DecisionModal'; | ||
| import useHoldMenuSubmit from '@hooks/useHoldMenuSubmit'; | ||
| import type {ActionHandledType} from '@hooks/useHoldMenuSubmit'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useResponsiveLayout from '@hooks/useResponsiveLayout'; | ||
| import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; | ||
| import type {ModalProps} from './ModalContext'; | ||
|
|
||
| type HoldMenuModalWrapperProps = ModalProps & { | ||
| reportID: string | undefined; | ||
| chatReportID: string | undefined; | ||
| requestType: ActionHandledType; | ||
| paymentType?: PaymentMethodType; | ||
| methodID?: number; | ||
| nonHeldAmount?: string; | ||
| fullAmount: string; | ||
| hasNonHeldExpenses?: boolean; | ||
| transactionCount: number; | ||
| onConfirm?: (full: boolean) => void; | ||
| }; | ||
|
|
||
| function HoldMenuModalWrapper({ | ||
| closeModal, | ||
| reportID, | ||
| chatReportID, | ||
| requestType, | ||
| paymentType, | ||
| methodID, | ||
| nonHeldAmount = '0', | ||
| fullAmount, | ||
| hasNonHeldExpenses, | ||
| transactionCount, | ||
| onConfirm, | ||
| }: HoldMenuModalWrapperProps) { | ||
| const [isVisible, setIsVisible] = useState(true); | ||
| const {translate} = useLocalize(); | ||
| // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type | ||
| // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth | ||
| const {isSmallScreenWidth} = useResponsiveLayout(); | ||
|
|
||
| const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); | ||
| const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); | ||
|
|
||
| const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID); | ||
| const transactions = Object.values(reportTransactions); | ||
|
|
||
| const {onSubmit, isApprove} = useHoldMenuSubmit({ | ||
| moneyRequestReport, | ||
| chatReport, | ||
| requestType, | ||
| paymentType, | ||
| methodID, | ||
| onClose: () => setIsVisible(false), | ||
| onConfirm, | ||
| transactions, | ||
| }); | ||
|
|
||
| return ( | ||
| <DecisionModal | ||
| title={translate(isApprove ? 'iou.confirmApprove' : 'iou.confirmPay')} | ||
| onClose={() => setIsVisible(false)} | ||
| isVisible={isVisible} | ||
| prompt={ | ||
| hasNonHeldExpenses | ||
| ? translate(isApprove ? 'iou.confirmApprovalAmount' : 'iou.confirmPayAmount') | ||
| : translate(isApprove ? 'iou.confirmApprovalAllHoldAmount' : 'iou.confirmPayAllHoldAmount', {count: transactionCount}) | ||
| } | ||
| firstOptionText={hasNonHeldExpenses ? `${translate(isApprove ? 'iou.approveOnly' : 'iou.payOnly')} ${nonHeldAmount}` : undefined} | ||
| secondOptionText={`${translate(isApprove ? 'iou.approve' : 'iou.pay')} ${fullAmount}`} | ||
| onFirstOptionSubmit={() => onSubmit(false)} | ||
| onSecondOptionSubmit={() => onSubmit(true)} | ||
| isSmallScreenWidth={isSmallScreenWidth} | ||
| onModalHide={() => { | ||
| if (isVisible) { | ||
| return; | ||
| } | ||
| closeModal({action: 'CLOSE'}); | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default HoldMenuModalWrapper; | ||
| export type {ActionHandledType, HoldMenuModalWrapperProps}; | ||
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| import React, {useRef, useState} from 'react'; | ||
| import type {ReactNode} from 'react'; | ||
| import {InteractionManager} from 'react-native'; | ||
| import useDecisionModal from '@hooks/useDecisionModal'; | ||
| import useHoldMenuModal from '@hooks/useHoldMenuModal'; | ||
| import useLocalize from '@hooks/useLocalize'; | ||
| import useOnyx from '@hooks/useOnyx'; | ||
| import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; | ||
| import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; | ||
| import getPlatform from '@libs/getPlatform'; | ||
| import {getNonHeldAndFullAmount, hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils, hasOnlyNonReimbursableTransactions} from '@libs/ReportUtils'; | ||
| import {canIOUBePaid as canIOUBePaidAction} from '@userActions/IOU'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import MoneyReportHeaderEducationalModals from './MoneyReportHeaderEducationalModals'; | ||
| import type {MoneyReportHeaderEducationalModalsHandle, RejectModalAction} from './MoneyReportHeaderEducationalModals'; | ||
| import MoneyReportHeaderModalsContext from './MoneyReportHeaderModalsContext'; | ||
| import type {HoldMenuParams} from './MoneyReportHeaderModalsContext'; | ||
| import ReportPDFDownloadModal from './ReportPDFDownloadModal'; | ||
|
|
||
| type MoneyReportHeaderModalsProps = { | ||
| reportID: string | undefined; | ||
| children: ReactNode; | ||
| }; | ||
|
|
||
| function MoneyReportHeaderModals({reportID, children}: MoneyReportHeaderModalsProps) { | ||
| // PDF modal state | ||
| const [isPDFModalVisible, setIsPDFModalVisible] = useState(false); | ||
|
|
||
| // Educational modals ref | ||
| const educationalModalsRef = useRef<MoneyReportHeaderEducationalModalsHandle>(null); | ||
|
|
||
| // Fetch data from IDs | ||
| const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); | ||
| const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`); | ||
| const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`); | ||
| const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); | ||
|
|
||
| const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(moneyRequestReport?.reportID); | ||
| const transactions = Object.values(reportTransactions); | ||
|
|
||
| // Derive data for hold menu | ||
| const canIOUBePaid = canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList); | ||
| const onlyShowPayElsewhere = !canIOUBePaid && canIOUBePaidAction(moneyRequestReport, chatReport, policy, bankAccountList, undefined, true); | ||
| const reportHasOnlyNonReimbursableTransactions = hasOnlyNonReimbursableTransactions(moneyRequestReport?.reportID, transactions); | ||
| const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere || reportHasOnlyNonReimbursableTransactions; | ||
| const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); | ||
| const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID); | ||
| const transactionIDs = transactions.map((t) => t.transactionID); | ||
|
|
||
| // Imperative modals | ||
| const {showHoldMenu} = useHoldMenuModal(); | ||
| const {showDecisionModal} = useDecisionModal(); | ||
| const {translate} = useLocalize(); | ||
|
|
||
| const showOfflineModal = () => { | ||
| showDecisionModal({ | ||
| title: translate('common.youAppearToBeOffline'), | ||
| prompt: translate('common.offlinePrompt'), | ||
| secondOptionText: translate('common.buttonConfirm'), | ||
| }); | ||
| }; | ||
|
|
||
| const showDownloadErrorModal = () => { | ||
| showDecisionModal({ | ||
| title: translate('common.downloadFailedTitle'), | ||
| prompt: translate('common.downloadFailedDescription'), | ||
| secondOptionText: translate('common.buttonConfirm'), | ||
| }); | ||
| }; | ||
|
|
||
| const openHoldMenu = ({requestType, paymentType, methodID, onConfirm}: HoldMenuParams): Promise<void> => { | ||
| const open = () => | ||
| showHoldMenu({ | ||
| reportID: moneyRequestReport?.reportID, | ||
| chatReportID: chatReport?.reportID, | ||
| requestType, | ||
| paymentType, | ||
| methodID, | ||
| nonHeldAmount: !hasOnlyHeldExpenses && hasValidNonHeldAmount ? nonHeldAmount : undefined, | ||
| fullAmount, | ||
| hasNonHeldExpenses: !hasOnlyHeldExpenses, | ||
| transactionCount: transactionIDs.length, | ||
| onConfirm, | ||
| }); | ||
|
|
||
| // On iOS, delay opening the hold menu until active touch interactions finish to prevent visual glitches | ||
| if (getPlatform() === CONST.PLATFORM.IOS) { | ||
| return new Promise<void>((resolve) => { | ||
| // eslint-disable-next-line @typescript-eslint/no-deprecated -- InteractionManager is widely used across the codebase (120+ files) and kept alive via a dedicated RN patch | ||
| InteractionManager.runAfterInteractions(() => { | ||
| open().then(() => resolve()); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| return open().then(() => {}); | ||
| }; | ||
|
|
||
| const contextValue = { | ||
| openHoldMenu, | ||
| openPDFDownload: () => setIsPDFModalVisible(true), | ||
| openHoldEducational: () => educationalModalsRef.current?.openHoldEducational(), | ||
| openRejectModal: (action: RejectModalAction) => educationalModalsRef.current?.openRejectModal(action), | ||
| showOfflineModal, | ||
| showDownloadErrorModal, | ||
| }; | ||
|
|
||
| return ( | ||
| <MoneyReportHeaderModalsContext.Provider value={contextValue}> | ||
| {children} | ||
|
|
||
| <MoneyReportHeaderEducationalModals | ||
| ref={educationalModalsRef} | ||
| reportID={moneyRequestReport?.reportID} | ||
| /> | ||
|
|
||
| <ReportPDFDownloadModal | ||
| reportID={moneyRequestReport?.reportID} | ||
| isVisible={isPDFModalVisible} | ||
| onClose={() => setIsPDFModalVisible(false)} | ||
| /> | ||
| </MoneyReportHeaderModalsContext.Provider> | ||
| ); | ||
| } | ||
|
|
||
| export default MoneyReportHeaderModals; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import {createContext, useContext} from 'react'; | ||
| import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; | ||
| import type {ActionHandledType} from './Modal/Global/HoldMenuModalWrapper'; | ||
| import type {RejectModalAction} from './MoneyReportHeaderEducationalModals'; | ||
|
|
||
| type HoldMenuParams = { | ||
| requestType: ActionHandledType; | ||
| paymentType?: PaymentMethodType; | ||
| methodID?: number; | ||
| onConfirm?: (full: boolean) => void; | ||
| }; | ||
|
|
||
| type MoneyReportHeaderModalsContextValue = { | ||
| openHoldMenu: (params: HoldMenuParams) => Promise<void>; | ||
| openPDFDownload: () => void; | ||
| openHoldEducational: () => void; | ||
| openRejectModal: (action: RejectModalAction) => void; | ||
| showOfflineModal: () => void; | ||
| showDownloadErrorModal: () => void; | ||
| }; | ||
|
|
||
| const defaultValue: MoneyReportHeaderModalsContextValue = { | ||
| openHoldMenu: () => Promise.resolve(), | ||
| openPDFDownload: () => {}, | ||
| openHoldEducational: () => {}, | ||
| openRejectModal: () => {}, | ||
| showOfflineModal: () => {}, | ||
| showDownloadErrorModal: () => {}, | ||
| }; | ||
|
|
||
| const MoneyReportHeaderModalsContext = createContext<MoneyReportHeaderModalsContextValue>(defaultValue); | ||
|
|
||
| function useMoneyReportHeaderModals() { | ||
| return useContext(MoneyReportHeaderModalsContext); | ||
| } | ||
|
|
||
| export default MoneyReportHeaderModalsContext; | ||
| export {useMoneyReportHeaderModals}; | ||
| export type {HoldMenuParams}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.