diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx index 806eeed02ee8..c78958b56256 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx @@ -31,6 +31,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePaymentOptions from '@hooks/usePaymentOptions'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; @@ -142,6 +143,7 @@ function MoneyReportHeaderSecondaryActionsContent({reportID, primaryAction, isRe const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); const isAnyTransactionOnHold = hasHeldExpensesReportUtils(allTransactions); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const confirmPayment = ({paymentType: type, payAsBusiness, methodID, paymentMethod}: PaymentActionParams) => { if (!type || !chatReport) { @@ -186,6 +188,7 @@ function MoneyReportHeaderSecondaryActionsContent({reportID, primaryAction, isRe betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email ?? '', lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { startAnimation(); @@ -209,6 +212,7 @@ function MoneyReportHeaderSecondaryActionsContent({reportID, primaryAction, isRe onPaid: () => { startAnimation(); }, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx index 8e650a9dfd7e..eb90a28f34bd 100644 --- a/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx +++ b/src/components/MoneyReportHeaderActions/MoneyReportHeaderSelectionDropdown.tsx @@ -31,6 +31,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePaymentOptions from '@hooks/usePaymentOptions'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; @@ -128,6 +129,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn const activePolicy = usePolicy(activePolicyID); const chatReportPolicy = usePolicy(chatReport?.policyID); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); const {transactionThreadReportID, reportActions} = useTransactionThreadReport(reportID); @@ -286,6 +288,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email ?? '', lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { payMoneyRequest({ @@ -308,6 +311,7 @@ function MoneyReportHeaderSelectionDropdown({reportID, primaryAction, isReportIn onPaid: () => { startAnimation(); }, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx index bb7bc78ba6a2..c231d8e40c4a 100644 --- a/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx +++ b/src/components/MoneyReportHeaderPrimaryAction/PayPrimaryAction.tsx @@ -13,6 +13,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePolicy from '@hooks/usePolicy'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; @@ -64,6 +65,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const {convertToDisplayString} = useCurrencyListActions(); const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport); @@ -146,6 +148,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email ?? '', lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { startAnimation(); @@ -167,6 +170,7 @@ function PayPrimaryAction({reportID, chatReportID}: PayPrimaryActionProps) { ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, onPaid: startAnimation, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx index 2fd9e05b0ccf..6abf481dc080 100644 --- a/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx +++ b/src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx @@ -12,6 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; @@ -99,6 +100,7 @@ function PayActionButton({ const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const canAllowSettlement = hasUpdatedTotal(iouReport, policy); const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations, currentUserAccountID, currentUserEmail); @@ -188,6 +190,7 @@ function PayActionButton({ betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(currentUserEmail, lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); } else { payMoneyRequest({ @@ -207,6 +210,7 @@ function PayActionButton({ amountOwed, ownerBillingGracePeriodEnd, onPaid: startAnimation, + chatReportActions: getChatReportActions(false), }); } } diff --git a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx index 986c132ab11f..fbaf118baf6a 100644 --- a/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx +++ b/src/components/Search/SearchList/ListItem/ActionCell/PayActionCell.tsx @@ -15,7 +15,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {payInvoice, payMoneyRequest} from '@libs/actions/IOU/PayMoneyRequest'; import {canIOUBePaid} from '@libs/actions/IOU/ReportWorkflow'; import {getSearchPayOnyxData} from '@libs/actions/Search'; -import {isInvoiceReport} from '@libs/ReportUtils'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import {isIndividualInvoiceRoom, isInvoiceReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -42,6 +43,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, const policy = usePolicy(policyID); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); + const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined; const invoiceReceiverPolicy = usePolicy(invoiceReceiverPolicyID); @@ -93,6 +95,13 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, invoiceReceiverPolicyID ?? chatReport?.policyID, ); + // getPayMoneyRequestParams resolves the chat report from `chatReport` but swaps to `existingB2BInvoiceReport` + // when paying an individual invoice room as a business. `payAsBusiness` is only known at click time, so pick + // the right report's actions here in the function scope. + const shouldUseB2BInvoiceReport = !!payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(chatReport); + const chatReportActions = + allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(shouldUseB2BInvoiceReport ? existingB2BInvoiceReport?.reportID : chatReport?.reportID)}`]; + payInvoice({ paymentMethodType: type, chatReport, @@ -111,6 +120,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, isSelfTourViewed, defaultWorkspaceName, additionalOnyxData, + chatReportActions, }); return; } @@ -133,6 +143,7 @@ function PayActionCell({isLoading, policyID, reportID, hash, amount, extraSmall, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, additionalOnyxData, + chatReportActions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID)}`], }); }; diff --git a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx index d6fa7b78e970..51015ce700e9 100644 --- a/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx +++ b/src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx @@ -132,6 +132,9 @@ function ExpenseReportListItemInner({ }, [searchData, snapshotReport?.chatReportID, reportItem.parentReportID]); const chatReport = parentChatReport ?? snapshotChatReport; + const [chatReportActions] = originalUseOnyx( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`, + ); const snapshotPolicy = useMemo(() => { return (searchData?.[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] ?? {}) as Policy; @@ -277,6 +280,7 @@ function ExpenseReportListItemInner({ chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData, + chatReportActions, }); }, [ currentSearchHash, @@ -312,6 +316,7 @@ function ExpenseReportListItemInner({ activePolicy, chatReportPolicy, nextStep, + chatReportActions, ]); const handleSelectionButtonPress = useCallback(() => { diff --git a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx index 2f6007a84805..07bf6ce329b5 100644 --- a/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx +++ b/src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx @@ -263,6 +263,9 @@ function ReportListItemHeaderInner({ const [parentPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(snapshotReport?.policyID ?? reportItem.policyID)}`); const [parentChatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? reportItem.parentReportID)}`); const chatReport = parentChatReport ?? snapshotChatReport; + const [chatReportActions] = useOnyx( + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(chatReport?.reportID ?? snapshotReport?.chatReportID ?? snapshotReport.parentReportID)}`, + ); const {currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy, amountOwed} = useReportPaymentContext({ reportID: reportItem.reportID, chatReportPolicyID: chatReport?.policyID, @@ -308,6 +311,7 @@ function ReportListItemHeaderInner({ chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData: snapshot?.data, + chatReportActions, }); }; return !isLargeScreenWidth ? ( diff --git a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx index 3f91b98da310..b62aa3b9e85c 100644 --- a/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx +++ b/src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx @@ -127,6 +127,7 @@ function TransactionListItemInner({ ]); const currentUserDetails = useCurrentUserPersonalDetails(); const [parentChatReport] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID)}`); + const [chatReportActions] = originalUseOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(snapshotReport?.chatReportID ?? snapshotReport?.parentReportID)}`); const {amountOwed, currentUserAccountID, currentUserLogin, introSelected, betas, isSelfTourViewed, activePolicy, nextStep, chatReportPolicy} = useReportPaymentContext({ reportID: transactionItem.reportID, chatReportPolicyID: parentChatReport?.policyID, @@ -214,6 +215,7 @@ function TransactionListItemInner({ chatReportPolicy, iouReportCurrentNextStepDeprecated: nextStep, searchData: currentSearchResults?.data, + chatReportActions, }); }; diff --git a/src/hooks/useHoldMenuSubmit.ts b/src/hooks/useHoldMenuSubmit.ts index f5082038cfdd..679dbf19fcb2 100644 --- a/src/hooks/useHoldMenuSubmit.ts +++ b/src/hooks/useHoldMenuSubmit.ts @@ -12,6 +12,7 @@ import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; import useOnyx from './useOnyx'; +import usePayChatReportActions from './usePayChatReportActions'; import usePermissions from './usePermissions'; import usePolicy from './usePolicy'; @@ -35,6 +36,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment const activePolicy = usePolicy(activePolicyID); const policy = usePolicy(moneyRequestReport?.policyID); const chatReportPolicy = usePolicy(chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, undefined); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [betas] = useOnyx(ONYXKEYS.BETAS); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); @@ -97,6 +99,7 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment ownerBillingGracePeriodEnd, methodID, onPaid: animationCallback, + chatReportActions: getChatReportActions(false), }); } onClose(); diff --git a/src/hooks/useLifecycleActions.tsx b/src/hooks/useLifecycleActions.tsx index 9fdcbfbb3339..9954eca9f8c6 100644 --- a/src/hooks/useLifecycleActions.tsx +++ b/src/hooks/useLifecycleActions.tsx @@ -68,6 +68,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`); + const [chatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(moneyRequestReport?.chatReportID)}`); const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -318,7 +319,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, CONST.IOU.REPORT_ACTION_TYPE.PAY, () => { startAnimation(); - markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? ''); + markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? '', chatReportActions); }, CONST.IOU.PAYMENT_TYPE.ELSEWHERE, ); @@ -326,7 +327,7 @@ function useLifecycleActions({reportID, startApprovedAnimation, startAnimation, } startAnimation(); - markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? ''); + markReportPaymentReceived(chatReport, moneyRequestReport, nextStep, accountID, email ?? '', chatReportActions); }, }, [CONST.REPORT.SECONDARY_ACTIONS.UNAPPROVE]: { diff --git a/src/hooks/usePayChatReportActions.ts b/src/hooks/usePayChatReportActions.ts new file mode 100644 index 000000000000..2b5aa7053d04 --- /dev/null +++ b/src/hooks/usePayChatReportActions.ts @@ -0,0 +1,26 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import {isIndividualInvoiceRoom} from '@libs/ReportUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportActions} from '@src/types/onyx'; +import useOnyx from './useOnyx'; + +/** + * Returns a resolver for the chat report's actions that getPayMoneyRequestParams will use internally. + * + * getPayMoneyRequestParams resolves the chat report from `initialChatReport` but swaps to + * `existingB2BInvoiceReport` when paying an individual invoice room as a business. `payAsBusiness` + * is chosen at click time, so the hook subscribes to both candidates and exposes a callback that + * picks the right slice once the user has chosen. + */ +function usePayChatReportActions(initialChatReport: OnyxEntry, existingB2BInvoiceReport: OnyxEntry): (payAsBusiness: boolean | undefined) => OnyxEntry { + const [initialChatReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(initialChatReport?.reportID)}`); + const [b2bInvoiceReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(existingB2BInvoiceReport?.reportID)}`); + + return (payAsBusiness: boolean | undefined) => { + const shouldUseB2BInvoiceReport = !!payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(initialChatReport); + return shouldUseB2BInvoiceReport ? b2bInvoiceReportActions : initialChatReportActions; + }; +} + +export default usePayChatReportActions; diff --git a/src/hooks/useSearchBulkActions.ts b/src/hooks/useSearchBulkActions.ts index 3f2544cdb41d..fb7ca51f0c6c 100644 --- a/src/hooks/useSearchBulkActions.ts +++ b/src/hooks/useSearchBulkActions.ts @@ -56,6 +56,7 @@ import { isCurrentUserSubmitter, isDM, isExpenseReport as isExpenseReportUtil, + isIndividualInvoiceRoom, isInvoiceReport, isIOUReport as isIOUReportUtil, isSelfDM, @@ -1091,6 +1092,9 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { invoiceReceiverPolicyID ?? chatReport.policyID, ); + const shouldUseB2BInvoiceReport = !!paymentItem.payAsBusiness && !!existingB2BInvoiceReport && isIndividualInvoiceRoom(chatReport); + const payChatReportID = shouldUseB2BInvoiceReport ? existingB2BInvoiceReport.reportID : chatReport.reportID; + payInvoice({ paymentMethodType: paymentItem.paymentType as PaymentMethodType, chatReport, @@ -1110,6 +1114,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { defaultWorkspaceName, additionalOnyxData, shouldPlaySuccessSound: false, + chatReportActions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${payChatReportID}`], }); paidReportCount += 1; continue; @@ -1136,6 +1141,7 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { paymentItem.paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? (paymentItem.bankAccountID ?? workspaceMethodID ?? reportPolicy?.achAccount?.bankAccountID) : undefined, additionalOnyxData, shouldPlaySuccessSound: false, + chatReportActions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`], }); paidReportCount += 1; } @@ -1159,12 +1165,12 @@ function useSearchBulkActions({queryJSON}: UseSearchBulkActionsParams) { policies, showDelegateNoAccessModal, allReports, + allReportActions, allNextSteps, personalPolicyID, lastPaymentMethods, allTransactions, policyIDsWithVBBA, - allReportActions, formatPhoneNumber, clearSelectedTransactions, accountID, diff --git a/src/hooks/useSelectionModeReportActions.ts b/src/hooks/useSelectionModeReportActions.ts index f36ef6a38d00..98cad9ea958e 100644 --- a/src/hooks/useSelectionModeReportActions.ts +++ b/src/hooks/useSelectionModeReportActions.ts @@ -60,6 +60,7 @@ import useLocalize from './useLocalize'; import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; import useParticipantsInvoiceReport from './useParticipantsInvoiceReport'; +import usePayChatReportActions from './usePayChatReportActions'; import usePaymentOptions from './usePaymentOptions'; import usePermissions from './usePermissions'; import usePolicy from './usePolicy'; @@ -132,6 +133,7 @@ function useSelectionModeReportActions({ const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector}); const existingB2BInvoiceReport = useParticipantsInvoiceReport(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport?.policyID); + const getChatReportActions = usePayChatReportActions(chatReport, existingB2BInvoiceReport); const activeAdminPolicies = useActiveAdminPolicies(); const lastWorkspaceNumber = useLastWorkspaceNumber(); const {convertToDisplayString} = useCurrencyListActions(); @@ -408,6 +410,7 @@ function useSelectionModeReportActions({ betas, isSelfTourViewed, defaultWorkspaceName: generateDefaultWorkspaceName(email, lastWorkspaceNumber, translate), + chatReportActions: getChatReportActions(payAsBusiness), }); clearSelectedTransactions(true); turnOffMobileSelectionMode(); @@ -429,6 +432,7 @@ function useSelectionModeReportActions({ amountOwed, ownerBillingGracePeriodEnd, methodID: type === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, + chatReportActions: getChatReportActions(false), }); if (currentSearchQueryJSON && !isOffline) { search({ diff --git a/src/libs/actions/IOU/PayMoneyRequest.ts b/src/libs/actions/IOU/PayMoneyRequest.ts index 10a1e2a2806a..9b2f8b299f89 100644 --- a/src/libs/actions/IOU/PayMoneyRequest.ts +++ b/src/libs/actions/IOU/PayMoneyRequest.ts @@ -60,6 +60,7 @@ type PayInvoiceArgs = { betas: OnyxEntry; isSelfTourViewed: boolean | undefined; defaultWorkspaceName: string; + chatReportActions: OnyxEntry; additionalOnyxData?: AdditionalPayOnyxData; shouldPlaySuccessSound?: boolean; }; @@ -110,6 +111,7 @@ type PayMoneyRequestFunctionParams = { shouldPlaySuccessSound?: boolean; // TODO: delegateAccountID will be made required in PR 12 when all callers pass the value (https://github.com/Expensify/App/issues/66425) delegateAccountID?: number | undefined; + chatReportActions: OnyxEntry; }; function mergeAdditionalPayOnyxData< @@ -153,6 +155,7 @@ function getPayMoneyRequestParams({ defaultWorkspaceName, currentUserLocalCurrency, delegateAccountID, + chatReportActions, }: { initialChatReport: OnyxTypes.Report; iouReport: OnyxEntry; @@ -176,6 +179,7 @@ function getPayMoneyRequestParams({ currentUserLocalCurrency: string | undefined; // TODO: delegateAccountID will be made required in PR 12 when all callers pass the value (https://github.com/Expensify/App/issues/66425) delegateAccountID?: number | undefined; + chatReportActions: OnyxEntry; }): PayMoneyRequestData { // TODO: https://github.com/Expensify/App/issues/66512 // eslint-disable-next-line @typescript-eslint/no-deprecated @@ -271,7 +275,7 @@ function getPayMoneyRequestParams({ // In some instances, the report preview action might not be available to the payer (only whispered to the requestor) // hence we need to make the updates to the action safely. let optimisticReportPreviewAction = null; - const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport?.reportID); + const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport?.reportID, chatReportActions); if (reportPreviewAction) { optimisticReportPreviewAction = updateReportPreview(iouReport, reportPreviewAction, true); } @@ -799,6 +803,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { additionalOnyxData, shouldPlaySuccessSound = true, delegateAccountID, + chatReportActions, } = params; const policyForBillingRestriction = chatReportPolicy ?? (policy?.id === chatReport.policyID ? policy : undefined); if ( @@ -832,6 +837,7 @@ function payMoneyRequest(params: PayMoneyRequestFunctionParams) { isSelfTourViewed, bankAccountID: paymentType === CONST.IOU.PAYMENT_TYPE.VBBA ? methodID : undefined, delegateAccountID, + chatReportActions, }); // For now, we need to call the PayMoneyRequestWithWallet API since PayMoneyRequest was not updated to work with @@ -853,6 +859,7 @@ function markReportPaymentReceived( iouReportCurrentNextStepDeprecated: OnyxEntry, currentUserAccountID: number, currentUserEmail: string, + chatReportActions: OnyxEntry, ) { if (!chatReport || !iouReport) { return; @@ -883,7 +890,7 @@ function markReportPaymentReceived( const receivedPaymentMessage = getElsewherePaymentReportActionMessage(translateLocal, optimisticIOUReportAction.originalMessage); optimisticIOUReportAction.message = [{html: receivedPaymentMessage, text: receivedPaymentMessage, isEdited: false, type: CONST.REPORT.MESSAGE.TYPE.COMMENT}]; - const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID); + const reportPreviewAction = getReportPreviewAction(chatReport.reportID, iouReport.reportID, chatReportActions); const optimisticReportPreviewAction = reportPreviewAction ? updateReportPreview(iouReport, reportPreviewAction, true) : null; const optimisticNextStepDeprecated = // buildOptimisticNextStep is used in parallel @@ -1043,6 +1050,7 @@ function payInvoice({ betas, isSelfTourViewed, defaultWorkspaceName, + chatReportActions, additionalOnyxData, shouldPlaySuccessSound = true, }: PayInvoiceArgs) { @@ -1079,6 +1087,7 @@ function payInvoice({ betas, isSelfTourViewed, defaultWorkspaceName, + chatReportActions, }); const paymentSelected = paymentMethodType === CONST.IOU.PAYMENT_TYPE.VBBA ? CONST.IOU.PAYMENT_SELECTED.BBA : CONST.IOU.PAYMENT_SELECTED.PBA; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index b773454c7c5a..c0360155486a 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -77,6 +77,7 @@ import type { Policy, Report, ReportAction, + ReportActions, ReportNextStepDeprecated, Transaction, } from '@src/types/onyx'; @@ -213,6 +214,7 @@ type HandleActionButtonPressParams = { chatReportPolicy?: OnyxEntry; iouReportCurrentNextStepDeprecated?: OnyxEntry; searchData?: SearchResultDataType; + chatReportActions: OnyxEntry; }; function handleActionButtonPress({ @@ -246,6 +248,7 @@ function handleActionButtonPress({ chatReportPolicy, iouReportCurrentNextStepDeprecated, searchData, + chatReportActions, }: HandleActionButtonPressParams) { // The transactionIDList is needed to handle actions taken on `status:""` where transactions on single expense reports can be approved/paid. // We need the transactionID to display the loading indicator for that list item's action. @@ -295,6 +298,7 @@ function handleActionButtonPress({ amountOwed, policy, searchData, + chatReportActions, }); return; case CONST.SEARCH.ACTION_TYPES.APPROVE: @@ -472,6 +476,7 @@ type GetPayActionCallbackParams = { amountOwed: OnyxEntry; policy: OnyxEntry; searchData?: SearchResultDataType; + chatReportActions: OnyxEntry; }; function getPayActionCallback({ @@ -497,6 +502,7 @@ function getPayActionCallback({ amountOwed, policy, searchData, + chatReportActions, }: GetPayActionCallbackParams) { const lastPolicyPaymentMethod = getLastPolicyPaymentMethod(item.policyID, personalPolicyID, lastPaymentMethod, getReportType(item.reportID)); @@ -543,6 +549,7 @@ function getPayActionCallback({ ownerBillingGracePeriodEnd, methodID: lastPolicyPaymentMethod === CONST.IOU.PAYMENT_TYPE.VBBA ? snapshotPolicy?.achAccount?.bankAccountID : undefined, additionalOnyxData: getSearchPayOnyxData(hash, item.reportID, currentSearchKey), + chatReportActions, }); } diff --git a/tests/actions/IOUTest/PayMoneyRequestTest.ts b/tests/actions/IOUTest/PayMoneyRequestTest.ts index 71e7b6f84ae4..da076ec3f755 100644 --- a/tests/actions/IOUTest/PayMoneyRequestTest.ts +++ b/tests/actions/IOUTest/PayMoneyRequestTest.ts @@ -253,6 +253,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -465,6 +466,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -631,6 +633,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -685,6 +688,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -802,6 +806,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(partialPayChatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -899,6 +904,7 @@ describe('actions/IOU/PayMoneyRequest', () => { isSelfTourViewed: false, userBillingGracePeriodEnds: undefined, amountOwed: 0, + chatReportActions: undefined, }); await waitForBatchedUpdates(); const newExpenseReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${newExpenseReportID}`); @@ -936,6 +942,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyTrueTour, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -986,6 +993,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFalseTour, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1069,12 +1077,13 @@ describe('actions/IOU/PayMoneyRequest', () => { reportID: outstandingReport.reportID, }; - await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { + const mockChatReportActions = { [reportPreview1.reportActionID]: reportPreview1, [reportPreview2.reportActionID]: reportPreview2, - }); + }; + await Onyx.merge(ONYXKEYS.SESSION, {accountID: currentUserAccountID, email: currentUserEmail}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, mockChatReportActions); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reimbursedReport.reportID}`, reimbursedReport); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${outstandingReport.reportID}`, outstandingReport); @@ -1082,7 +1091,7 @@ describe('actions/IOU/PayMoneyRequest', () => { mockFetch?.pause?.(); - markReportPaymentReceived(chatReport, reimbursedReport, undefined, currentUserAccountID, currentUserEmail); + markReportPaymentReceived(chatReport, reimbursedReport, undefined, currentUserAccountID, currentUserEmail, mockChatReportActions); await waitForBatchedUpdates(); const updatedChatReport = await new Promise>((resolve) => { @@ -1151,6 +1160,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: pastDate, policy, chatReportPolicy: policy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1217,6 +1227,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: pastDate, policy: expensePolicy, chatReportPolicy: workspacePolicy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1273,6 +1284,7 @@ describe('actions/IOU/PayMoneyRequest', () => { ownerBillingGracePeriodEnd: futureGraceEnd, chatReportPolicy: workspacePolicy, policy: workspacePolicy, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1309,6 +1321,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyAmountZero, + chatReportActions: undefined, }); await waitForBatchedUpdates(); @@ -1433,6 +1446,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); return waitForBatchedUpdates(); }) @@ -1805,6 +1819,7 @@ describe('actions/IOU/PayMoneyRequest', () => { userBillingGracePeriodEnds: undefined, amountOwed: 0, chatReportPolicy: chatReportPolicyFromChat(chatReport), + chatReportActions: undefined, }); } await waitForBatchedUpdates(); diff --git a/tests/unit/Search/handleActionButtonPressTest.ts b/tests/unit/Search/handleActionButtonPressTest.ts index 1d2b33a60838..fdb019213665 100644 --- a/tests/unit/Search/handleActionButtonPressTest.ts +++ b/tests/unit/Search/handleActionButtonPressTest.ts @@ -341,6 +341,7 @@ describe('handleActionButtonPress', () => { userBillingGracePeriodEnds: undefined, onHoldMenuOpen: jest.fn(), policy: snapshotPolicy as Policy, + chatReportActions: undefined, currentUserAccountID: 1206, }); expect(goToItem).not.toHaveBeenCalled(); @@ -361,6 +362,7 @@ describe('handleActionButtonPress', () => { amountOwed: undefined, onHoldMenuOpen, policy: snapshotPolicy as Policy, + chatReportActions: undefined, currentUserAccountID: 1206, }); @@ -381,6 +383,7 @@ describe('handleActionButtonPress', () => { amountOwed: undefined, userBillingGracePeriodEnds: undefined, policy: snapshotPolicy as Policy, + chatReportActions: undefined, currentUserAccountID: 1206, }); expect(goToItem).toHaveBeenCalledTimes(0); diff --git a/tests/unit/hooks/usePayChatReportActions.test.ts b/tests/unit/hooks/usePayChatReportActions.test.ts new file mode 100644 index 000000000000..41136ee1876c --- /dev/null +++ b/tests/unit/hooks/usePayChatReportActions.test.ts @@ -0,0 +1,129 @@ +import {renderHook, waitFor} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import usePayChatReportActions from '@hooks/usePayChatReportActions'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; +import createRandomReportAction from '../../utils/collections/reportActions'; +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const INITIAL_CHAT_REPORT_ID = '1'; +const B2B_INVOICE_REPORT_ID = '2'; + +function buildReport(reportID: string, overrides: Partial = {}): Report { + return { + reportID, + chatType: undefined, + ...overrides, + } as Report; +} + +function buildIndividualInvoiceRoom(reportID: string): Report { + return buildReport(reportID, { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + invoiceReceiver: { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, + accountID: 99, + }, + }); +} + +const initialChatReportActions = {a1: {...createRandomReportAction(1), reportActionID: 'a1'}}; +const b2bInvoiceReportActions = {b1: {...createRandomReportAction(2), reportActionID: 'b1'}}; + +async function seedReportActions() { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${INITIAL_CHAT_REPORT_ID}`, initialChatReportActions); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${B2B_INVOICE_REPORT_ID}`, b2bInvoiceReportActions); +} + +describe('usePayChatReportActions', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('returns initial chat report actions when payAsBusiness is false', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(false)).toEqual(initialChatReportActions); + }); + }); + + it('returns initial chat report actions when payAsBusiness is undefined', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(undefined)).toEqual(initialChatReportActions); + }); + }); + + it('returns b2b invoice report actions when paying as business from an individual invoice room', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(true)).toEqual(b2bInvoiceReportActions); + }); + }); + + it('falls back to initial chat report actions when payAsBusiness is true but no b2b invoice report exists', async () => { + await seedReportActions(); + const initialChatReport = buildIndividualInvoiceRoom(INITIAL_CHAT_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, undefined)); + + await waitFor(() => { + expect(result.current(true)).toEqual(initialChatReportActions); + }); + }); + + it('falls back to initial chat report actions when chat is not an individual invoice room', async () => { + await seedReportActions(); + const initialChatReport = buildReport(INITIAL_CHAT_REPORT_ID, {chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM}); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(true)).toEqual(initialChatReportActions); + }); + }); + + it('falls back to initial chat report actions for a business invoice room (not individual)', async () => { + await seedReportActions(); + const initialChatReport = buildReport(INITIAL_CHAT_REPORT_ID, { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + invoiceReceiver: { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: 'some-policy', + }, + }); + const existingB2BInvoiceReport = buildReport(B2B_INVOICE_REPORT_ID); + + const {result} = renderHook(() => usePayChatReportActions(initialChatReport, existingB2BInvoiceReport)); + + await waitFor(() => { + expect(result.current(true)).toEqual(initialChatReportActions); + }); + }); +});