From 054b420d9f48d1125367b6e028fa09ce2b081a67 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 11 Sep 2026 13:06:02 +0700 Subject: [PATCH 1/2] Clarify approval options for Spend page --- .../ExpenseHeaderApprovalButton.tsx | 49 +++++++- .../Modal/Global/HoldMenuModalWrapper.tsx | 7 +- .../useConfirmApproval.ts | 10 +- src/components/ProcessMoneyReportHoldMenu.tsx | 7 +- .../ListItem/ActionCell/ApproveActionCell.tsx | 117 ++++++++++++++++++ .../SearchList/ListItem/ActionCell/index.tsx | 13 ++ .../ListItem/ExpenseReportListItem.tsx | 35 +----- src/libs/actions/Search.ts | 10 +- tests/unit/HoldMenuModalWrapperTest.tsx | 73 +++++++++++ .../Search/handleActionButtonPressTest.ts | 11 +- 10 files changed, 279 insertions(+), 53 deletions(-) create mode 100644 src/components/Search/SearchList/ListItem/ActionCell/ApproveActionCell.tsx create mode 100644 tests/unit/HoldMenuModalWrapperTest.tsx diff --git a/src/components/ExpenseHeaderApprovalButton.tsx b/src/components/ExpenseHeaderApprovalButton.tsx index e505997be890..50e56726a49e 100644 --- a/src/components/ExpenseHeaderApprovalButton.tsx +++ b/src/components/ExpenseHeaderApprovalButton.tsx @@ -11,7 +11,9 @@ import type {Report, Transaction} from '@src/types/onyx'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import type IconAsset from '@src/types/utils/IconAsset'; +import type {StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import React from 'react'; @@ -44,6 +46,30 @@ type ExpenseHeaderApprovalButtonProps = { /** Whether to disable the approve button */ isDisabled?: boolean; + + /** Whether the button should show a loading spinner */ + isLoading?: boolean; + + /** The size of the button */ + size?: ValueOf; + + /** Whether the dropdown button should use its compact inline form */ + shouldUseShortForm?: boolean; + + /** Whether the button is rendered inside another pressable, since nesting buttons isn't valid html */ + isNested?: boolean; + + /** Whether the button should stay visually normal even when disabled */ + stayNormalOnDisable?: boolean; + + /** Additional styles to add to the button */ + style?: StyleProp; + + /** Additional styles to add to the dropdown button's wrapper */ + wrapperStyle?: StyleProp; + + /** Label used to identify this button in Sentry */ + sentryLabel?: string; }; type ApprovalOption = { @@ -115,6 +141,14 @@ function ExpenseHeaderApprovalButton({ transactions, shouldShowPayButton, isDisabled, + isLoading, + size, + shouldUseShortForm, + isNested, + stayNormalOnDisable, + style, + wrapperStyle, + sentryLabel = CONST.SENTRY_LABEL.REPORT_PREVIEW.APPROVE_BUTTON, }: ExpenseHeaderApprovalButtonProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -153,7 +187,13 @@ function ExpenseHeaderApprovalButton({ // edge — the menu would otherwise be clamped to the window edge and cover the header. Flip instead. shouldSwitchPositionIfOverflow isDisabled={isDisabled} - sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.APPROVE_BUTTON} + isLoading={isLoading} + size={size} + shouldUseShortForm={shouldUseShortForm} + stayNormalOnDisable={stayNormalOnDisable} + style={style} + wrapperStyle={wrapperStyle} + sentryLabel={sentryLabel} /> ); } @@ -162,8 +202,13 @@ function ExpenseHeaderApprovalButton({ diff --git a/src/components/Modal/Global/HoldMenuModalWrapper.tsx b/src/components/Modal/Global/HoldMenuModalWrapper.tsx index 1bea9e5d2b72..e543646aa3a4 100644 --- a/src/components/Modal/Global/HoldMenuModalWrapper.tsx +++ b/src/components/Modal/Global/HoldMenuModalWrapper.tsx @@ -45,7 +45,7 @@ function HoldMenuModalWrapper({ requestType, paymentType, methodID, - nonHeldAmount = '0', + nonHeldAmount, fullAmount, hasNonHeldExpenses, transactionCount, @@ -88,7 +88,10 @@ function HoldMenuModalWrapper({ onClose={() => setIsVisible(false)} isVisible={isVisible} prompt={approvalPrompt} - firstOptionText={hasNonHeldExpenses ? `${translate(isApprove ? 'iou.approveOnly' : 'iou.payOnly')} ${nonHeldAmount}` : undefined} + // Callers pass `undefined` when the non-held amount isn't meaningfully different from the full amount, so + // gate on the amount itself rather than on `hasNonHeldExpenses` — otherwise a report whose unheld expenses + // net out to nothing offers a partial option for a zero amount. + firstOptionText={nonHeldAmount !== undefined ? `${translate(isApprove ? 'iou.approveOnly' : 'iou.payOnly')} ${nonHeldAmount}` : undefined} secondOptionText={`${translate(isApprove ? 'iou.approve' : 'iou.pay')} ${fullAmount}`} onFirstOptionSubmit={() => onSubmit(false)} onSecondOptionSubmit={() => onSubmit(true)} diff --git a/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts b/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts index 3752480fef51..f0a8d9a44565 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts +++ b/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts @@ -11,6 +11,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isSubmitPolicy} from '@libs/PolicyUtils'; import {hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; +import type {AdditionalPayOnyxData} from '@userActions/IOU/PayMoneyRequest'; import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow'; import CONST from '@src/CONST'; @@ -20,7 +21,13 @@ import {delegateEmailSelector} from '@selectors/Account'; import {isTrackIntentUserSelector} from '@selectors/Onboarding'; import {personalDetailsLoginSelector} from '@selectors/PersonalDetails'; -function useConfirmApproval(reportID: string | undefined, startApprovedAnimation: () => void) { +/** + * Shared approve handler for the report header, report preview and Search rows. + * + * `getAdditionalOnyxData` is resolved at approve time (not on every render) so Search rows can attach the + * optimistic data that removes the row from the current results. + */ +function useConfirmApproval(reportID: string | undefined, startApprovedAnimation: () => void, getAdditionalOnyxData?: () => AdditionalPayOnyxData) { const {accountID, email} = useCurrentUserPersonalDetails(); const {getCurrencyDecimals} = useCurrencyListActions(); const {isBetaEnabled} = usePermissions(); @@ -72,6 +79,7 @@ function useConfirmApproval(reportID: string | undefined, startApprovedAnimation delegateEmail, delegateAccountID, isTrackIntentUser, + additionalOnyxData: getAdditionalOnyxData?.(), }); }; diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 7ecb6b1d6a01..f2bf36602f7d 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -48,7 +48,7 @@ type ProcessMoneyReportHoldMenuProps = { }; function ProcessMoneyReportHoldMenu({ - nonHeldAmount = '0', + nonHeldAmount, fullAmount, onClose, isVisible, @@ -81,7 +81,10 @@ function ProcessMoneyReportHoldMenu({ onClose={onClose} isVisible={isVisible} prompt={promptText} - firstOptionText={hasNonHeldExpenses ? `${translate('iou.payOnly')} ${nonHeldAmount}` : undefined} + // Callers pass `undefined` when the non-held amount isn't meaningfully different from the full amount, so + // gate on the amount itself rather than on `hasNonHeldExpenses` — otherwise a report whose unheld expenses + // net out to nothing offers a partial option for a zero amount. + firstOptionText={nonHeldAmount !== undefined ? `${translate('iou.payOnly')} ${nonHeldAmount}` : undefined} secondOptionText={`${translate('iou.pay')} ${fullAmount}`} onFirstOptionSubmit={() => onSubmit(false)} onSecondOptionSubmit={() => onSubmit(true)} diff --git a/src/components/Search/SearchList/ListItem/ActionCell/ApproveActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/ApproveActionCell.tsx new file mode 100644 index 000000000000..25158b3f9324 --- /dev/null +++ b/src/components/Search/SearchList/ListItem/ActionCell/ApproveActionCell.tsx @@ -0,0 +1,117 @@ +import {useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider'; +import ExpenseHeaderApprovalButton from '@components/ExpenseHeaderApprovalButton'; +import useConfirmApproval from '@components/MoneyReportHeaderPrimaryAction/useConfirmApproval'; +import {useSearchQueryContext} from '@components/Search/SearchContext'; +import {SearchScopeProvider} from '@components/Search/SearchScopeProvider'; + +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import useReportWithTransactionsAndViolations from '@hooks/useReportWithTransactionsAndViolations'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {getSearchApproveOnyxData} from '@libs/actions/Search'; +import {hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils} from '@libs/ReportUtils'; + +import {canIOUBePaid as canIOUBePaidAction} from '@userActions/IOU/ReportWorkflow'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +import type {OnyxEntry} from 'react-native-onyx'; + +import React from 'react'; + +type ApproveActionCellProps = { + isLoading: boolean; + reportID: string; + hash?: number; + shouldDisablePointerEvents?: boolean; + chatReport: OnyxEntry; +}; + +/** + * Approve action for a Search row. Mirrors PayActionCell in owning the action end to end, so the row can render the + * same ExpenseHeaderApprovalButton the report header uses and surface the partial/full approval choice up front when + * the report has held expenses, rather than routing through the (pay-only) hold menu. + */ +function ApproveActionCell({isLoading, reportID, hash, shouldDisablePointerEvents, chatReport}: ApproveActionCellProps) { + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const currentUserDetails = useCurrentUserPersonalDetails(); + const {isDelegateAccessRestricted} = useDelegateNoAccessState(); + const {currentSearchKey} = useSearchQueryContext(); + + const [iouReport, transactions] = useReportWithTransactionsAndViolations(reportID); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const activePolicy = usePolicy(activePolicyID); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + + const invoiceReceiverPolicyID = iouReport?.invoiceReceiver && 'policyID' in iouReport.invoiceReceiver ? iouReport.invoiceReceiver.policyID : undefined; + const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID); + + const isAnyTransactionOnHold = hasHeldExpensesReportUtils(transactions); + + // Same derivation as ApprovePrimaryAction: the non-held amount only excludes non-reimbursables when a Pay button would show. + const canIOUBePaid = canIOUBePaidAction( + iouReport, + chatReport, + activePolicy, + bankAccountList, + currentUserDetails.login ?? '', + currentUserDetails.accountID, + // `undefined` (not the row's transactions) matches ApprovePrimaryAction, so Spend and the report header + // derive shouldShowPayButton — and therefore the displayed approval amounts — identically. + undefined, + false, + undefined, + invoiceReceiverPolicy, + ); + const onlyShowPayElsewhere = + !canIOUBePaid && + canIOUBePaidAction( + iouReport, + chatReport, + activePolicy, + bankAccountList, + currentUserDetails.login ?? '', + currentUserDetails.accountID, + undefined, + true, + undefined, + invoiceReceiverPolicy, + ); + + // Search rows have no approval animation, but they do need the optimistic data that drops the row from the results. + const {onApprove} = useConfirmApproval(reportID, () => {}, hash === undefined ? undefined : () => getSearchApproveOnyxData(hash, reportID, currentSearchKey)); + + return ( + + + + ); +} + +export default ApproveActionCell; diff --git a/src/components/Search/SearchList/ListItem/ActionCell/index.tsx b/src/components/Search/SearchList/ListItem/ActionCell/index.tsx index 3ecfdb1a25b9..58376e984b46 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/index.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/index.tsx @@ -16,6 +16,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import React from 'react'; import actionTranslationsMap from './actionTranslationsMap'; +import ApproveActionCell from './ApproveActionCell'; import PayActionCell from './PayActionCell'; type ActionCellProps = { @@ -76,6 +77,18 @@ function ActionCell({ ); } + if (action === CONST.SEARCH.ACTION_TYPES.APPROVE) { + return ( + + ); + } + if (action === CONST.SEARCH.ACTION_TYPES.PAY) { return ( ({ const {isDelegateAccessRestricted} = useDelegateNoAccessState(); const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); const {showConfirmModal} = useConfirmModal(); - const {showHoldMenu} = useHoldMenuModal(); const openReportSubmitToPopover = useOpenReportSubmitToPopover(); const {shouldDisableSearchSubmitPress, consumeIgnoreNextSearchSubmitPress} = useSearchSubmitPopoverGuard(); const {transactions: reportTransactions, violations: reportViolations} = useTransactionsAndViolationsForReport(reportItem.reportID); @@ -258,31 +256,6 @@ function ExpenseReportListItemInner({ isDelegateAccessRestricted, onDelegateAccessRestricted: showDelegateNoAccessModal, personalPolicyID, - onHoldMenuOpen: (holdItem, requestType, paymentType) => { - // Search rows render from a snapshot; the report may not exist in the main - // collection yet. Fall back to the snapshot so the modal can submit. - const moneyRequestReport = parentReport ?? snapshotReport; - const transactionsForHoldMenu = liveReportTransactions.length > 0 ? liveReportTransactions : holdItem.transactions; - const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount( - moneyRequestReport, - holdItem.canPay ?? false, - transactionsForHoldMenu, - convertToDisplayString, - ); - const hasNonHeldExpenses = transactionsForHoldMenu.some((t) => !isOnHold(t)); - showHoldMenu({ - reportID: holdItem.reportID, - chatReportID: holdItem.parentReportID, - moneyRequestReport, - chatReport, - requestType, - paymentType, - nonHeldAmount: hasNonHeldExpenses && hasValidNonHeldAmount ? nonHeldAmount : undefined, - fullAmount, - hasNonHeldExpenses, - transactionCount: transactionsForHoldMenu.length > 0 ? transactionsForHoldMenu.length : (holdItem.transactionCount ?? 0), - }); - }, ownerBillingGracePeriodEnd, amountOwed, openReportSubmitToPopover, @@ -320,15 +293,12 @@ function ExpenseReportListItemInner({ snapshotPolicy, submitterLogin, parentPolicy, - parentReport, lastPaymentMethod, userBillingGracePeriodEnds, personalPolicyID, currentSearchKey, isDelegateAccessRestricted, showDelegateNoAccessModal, - showHoldMenu, - liveReportTransactions, ownerBillingGracePeriodEnd, amountOwed, openReportSubmitToPopover, @@ -336,7 +306,6 @@ function ExpenseReportListItemInner({ consumeIgnoreNextSearchSubmitPress, showConfirmModal, translate, - convertToDisplayString, getCurrencyDecimals, currentUserAccountID, currentUserLogin, diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 6afa95400650..6197391dab04 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -2,7 +2,6 @@ import type {FormOnyxValues} from '@components/Form/types'; import type {ContinueActionParams, PaymentMethod, PaymentMethodType} from '@components/KYCWall/types'; import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleContextProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; -import type {HoldMenuCallback} from '@components/Search'; import type {TransactionListItemType, TransactionReportGroupListItemType} from '@components/Search/SearchList/ListItem/types'; import type {BankAccountMenuItem, BulkPaySelectionData, PaymentData, SearchQueryJSON, SelectedReports, SelectedTransactions} from '@components/Search/types'; @@ -249,7 +248,6 @@ type HandleActionButtonPressParams = { lastPaymentMethod: OnyxEntry; userBillingGracePeriodEnds: OnyxCollection; currentSearchKey?: SearchKey; - onHoldMenuOpen?: HoldMenuCallback; isDelegateAccessRestricted?: boolean; onDelegateAccessRestricted?: () => void; personalPolicyID: string | undefined; @@ -291,7 +289,6 @@ function handleActionButtonPress({ lastPaymentMethod, userBillingGracePeriodEnds, currentSearchKey, - onHoldMenuOpen, isDelegateAccessRestricted, onDelegateAccessRestricted, personalPolicyID, @@ -329,7 +326,8 @@ function handleActionButtonPress({ hasHeldExpense && item.action !== CONST.SEARCH.ACTION_TYPES.SUBMIT && item.action !== CONST.SEARCH.ACTION_TYPES.UNDELETE && - (item.action !== CONST.SEARCH.ACTION_TYPES.APPROVE || !onHoldMenuOpen) + // Approve is excluded: ApproveActionCell renders its own dropdown for held expenses and never routes here. + item.action !== CONST.SEARCH.ACTION_TYPES.APPROVE ) { goToItem(); return; @@ -383,10 +381,6 @@ function handleActionButtonPress({ Navigation.navigate(ROUTES.RESTRICTED_ACTION.getRoute(snapshotReport.policyID)); return; } - if (hasHeldExpense) { - onHoldMenuOpen?.(item as TransactionReportGroupListItemType, CONST.IOU.REPORT_ACTION_TYPE.APPROVE); - return; - } getApproveActionCallback({ hash, item, diff --git a/tests/unit/HoldMenuModalWrapperTest.tsx b/tests/unit/HoldMenuModalWrapperTest.tsx new file mode 100644 index 000000000000..995278d25bf2 --- /dev/null +++ b/tests/unit/HoldMenuModalWrapperTest.tsx @@ -0,0 +1,73 @@ +import {render} from '@testing-library/react-native'; + +import type {DecisionModalProps} from '@components/DecisionModal'; +import HoldMenuModalWrapper from '@components/Modal/Global/HoldMenuModalWrapper'; + +import CONST from '@src/CONST'; + +let mockDecisionModalProps: DecisionModalProps | undefined; + +jest.mock('@components/DecisionModal', () => { + return (props: DecisionModalProps) => { + mockDecisionModalProps = props; + return null; + }; +}); + +jest.mock('@hooks/useHoldMenuSubmit', () => ({ + __esModule: true, + default: () => ({onSubmit: jest.fn(), isApprove: true}), +})); + +jest.mock('@hooks/useOnyx', () => ({ + __esModule: true, + default: () => [undefined], +})); + +function renderWrapper(overrides: {nonHeldAmount?: string; hasNonHeldExpenses?: boolean}) { + return render( + , + ); +} + +describe('HoldMenuModalWrapper', () => { + beforeEach(() => { + mockDecisionModalProps = undefined; + }); + + it('offers the partial option when a valid non-held amount is supplied', () => { + renderWrapper({nonHeldAmount: '$40.00', hasNonHeldExpenses: true}); + + expect(mockDecisionModalProps?.firstOptionText).toContain('$40.00'); + }); + + it('omits the partial option when the non-held amount is not meaningful, even though non-held expenses exist', () => { + // Regression for https://github.com/Expensify/App/issues/100639: the report still has an unheld expense, but + // its amount nets out against the held one, so the caller sends `undefined` rather than an amount. The modal + // must not fall back to rendering a zero amount. + renderWrapper({nonHeldAmount: undefined, hasNonHeldExpenses: true}); + + expect(mockDecisionModalProps?.firstOptionText).toBeUndefined(); + }); + + it('omits the partial option when every expense is on hold', () => { + renderWrapper({nonHeldAmount: undefined, hasNonHeldExpenses: false}); + + expect(mockDecisionModalProps?.firstOptionText).toBeUndefined(); + }); + + it('still shows the full amount option in every case', () => { + renderWrapper({nonHeldAmount: undefined, hasNonHeldExpenses: true}); + + expect(mockDecisionModalProps?.secondOptionText).toContain('$100.00'); + }); +}); diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index 210ac00efb89..06be5c79f84f 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -353,7 +353,6 @@ describe('handleActionButtonPress', () => { ownerBillingGracePeriodEnd: undefined, amountOwed: undefined, userBillingGracePeriodEnds: undefined, - onHoldMenuOpen: jest.fn(), policy: snapshotPolicy, chatReportActions: undefined, currentUserAccountID: 1206, @@ -364,8 +363,10 @@ describe('handleActionButtonPress', () => { expect(goToItem).not.toHaveBeenCalled(); }); - test('Should open the hold menu when the report has one transaction on hold and action is approve', () => { - const onHoldMenuOpen = jest.fn(); + // The partial/full choice is now surfaced up front by ApproveActionCell's dropdown, so reaching this path means the + // full report was chosen. It must approve directly rather than opening the hold menu, which is pay-only. + test('Should approve the full report when the report has one transaction on hold and action is approve', () => { + const approveMoneyRequestMock = jest.spyOn(ReportWorkflow, 'approveMoneyRequest').mockImplementation(jest.fn()); handleActionButtonPress({ conciergeChat: undefined, getCurrencyDecimals: getCurrencyDecimalsLocal, @@ -380,7 +381,6 @@ describe('handleActionButtonPress', () => { userBillingGracePeriodEnds: undefined, ownerBillingGracePeriodEnd: undefined, amountOwed: undefined, - onHoldMenuOpen, policy: snapshotPolicy, chatReportActions: undefined, currentUserAccountID: 1206, @@ -389,7 +389,8 @@ describe('handleActionButtonPress', () => { allViolations: undefined, }); - expect(onHoldMenuOpen).toHaveBeenCalledWith(mockReportItemWithHold, CONST.IOU.REPORT_ACTION_TYPE.APPROVE); + expect(approveMoneyRequestMock).toHaveBeenCalledWith(expect.objectContaining({full: true})); + approveMoneyRequestMock.mockRestore(); }); test('Should not navigate to item when the hold is removed', () => { From 0bedf901eb423718df6e910e6f1c9b0b8ea606bf Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 11 Sep 2026 14:18:17 +0700 Subject: [PATCH 2/2] fix: typecheck --- .../MoneyReportHeaderPrimaryAction/useConfirmApproval.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts b/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts index 5b66351ee47c..0d623ea77260 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts +++ b/src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts @@ -11,8 +11,8 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isSubmitPolicy} from '@libs/PolicyUtils'; import {hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils'; -import type {AdditionalPayOnyxData} from '@userActions/IOU/PayMoneyRequest'; import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow'; +import type AdditionalPayOnyxData from '@userActions/IOU/types/AdditionalPayOnyxData'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS';