diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 220033b81234..9650d323d676 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -257,7 +257,12 @@ const ROUTES = { }, REPORT_AVATAR: { route: 'r/:reportID/avatar', - getRoute: (reportID: string) => `r/${reportID}/avatar` as const, + getRoute: (reportID: string, policyID?: string) => { + if (policyID) { + return `r/${reportID}/avatar?policyID=${policyID}` as const; + } + return `r/${reportID}/avatar` as const; + }, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx index 38bf3912ae4b..2ccdd47c3205 100644 --- a/src/components/AvatarWithDisplayName.tsx +++ b/src/components/AvatarWithDisplayName.tsx @@ -1,7 +1,7 @@ import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -58,12 +58,16 @@ function AvatarWithDisplayName({ const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const title = ReportUtils.getReportName(report); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`); + const [invoiceReceiverPolicy] = useOnyx( + `${ONYXKEYS.COLLECTION.POLICY}${parentReport?.invoiceReceiver && 'policyID' in parentReport.invoiceReceiver ? parentReport.invoiceReceiver.policyID : -1}`, + ); + const title = ReportUtils.getReportName(report, undefined, undefined, undefined, invoiceReceiverPolicy); const subtitle = ReportUtils.getChatRoomSubtitle(report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(report) || ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report) || ReportUtils.isInvoiceReport(report); - const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy); + const icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails) as PersonalDetails[], false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(report); diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 16b85942fa22..b35b14016235 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -114,10 +114,20 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const renderItem = useCallback( ({item: reportID}: RenderItemProps): ReactElement => { const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const itemParentReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${itemFullReport?.parentReportID ?? '-1'}`]; const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`]; const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? '-1']; + let invoiceReceiverPolicyID = '-1'; + if (itemFullReport?.invoiceReceiver && 'policyID' in itemFullReport.invoiceReceiver) { + invoiceReceiverPolicyID = itemFullReport.invoiceReceiver.policyID; + } + if (itemParentReport?.invoiceReceiver && 'policyID' in itemParentReport.invoiceReceiver) { + invoiceReceiverPolicyID = itemParentReport.invoiceReceiver.policyID; + } + const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]; + const iouReportIDOfLastAction = OptionsListUtils.getIOUReportIDOfLastAction(itemFullReport); const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; @@ -146,6 +156,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio parentReportAction={itemParentReportAction} iouReportReportActions={itemIouReportReportActions} policy={itemPolicy} + invoiceReceiverPolicy={itemInvoiceReceiverPolicy} personalDetails={personalDetails ?? {}} transaction={itemTransaction} lastReportActionTransaction={lastReportActionTransaction} diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index fb590ce13a43..8179aed9de0c 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -21,6 +21,7 @@ function OptionRowLHNData({ personalDetails = {}, preferredLocale = CONST.LOCALES.DEFAULT, policy, + invoiceReceiverPolicy, receiptTransactions, parentReportAction, iouReportReportActions, @@ -49,6 +50,7 @@ function OptionRowLHNData({ parentReportAction, hasViolations: !!shouldDisplayViolations || shouldDisplayReportViolations, transactionViolations, + invoiceReceiverPolicy, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -72,6 +74,7 @@ function OptionRowLHNData({ transaction, transactionViolations, receiptTransactions, + invoiceReceiverPolicy, shouldDisplayReportViolations, ]); diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 124c1479c181..f914b001aba6 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -50,6 +50,9 @@ type OptionRowLHNDataProps = { /** The policy which the user has access to and which the report could be tied to */ policy?: OnyxEntry; + /** Invoice receiver policy */ + invoiceReceiverPolicy?: OnyxEntry; + /** The action from the parent report */ parentReportAction?: OnyxEntry; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 655cdcaf2443..812baee2d10a 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -135,7 +135,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const displayedAmount = isAnyTransactionOnHold && canAllowSettlement ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); - const confirmPayment = (type?: PaymentMethodType | undefined) => { + const confirmPayment = (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type || !chatReport) { return; } @@ -144,7 +144,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport); + IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 2c52fe83f989..6d326b2d9eb9 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -3,7 +3,7 @@ import React, {useMemo, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -139,6 +139,9 @@ function ReportPreview({ const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? ''); const [paymentType, setPaymentType] = useState(); + const [invoiceReceiverPolicy] = useOnyx( + `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : -1}`, + ); const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? 0; const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); @@ -147,6 +150,7 @@ function ReportPreview({ const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); + const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); const isApproved = ReportUtils.isReportApproved(iouReport, action); @@ -187,7 +191,7 @@ function ReportPreview({ [chatReport?.isOwnPolicyExpenseChat, policy?.harvesting?.enabled], ); - const confirmPayment = (type: PaymentMethodType | undefined) => { + const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { return; } @@ -197,7 +201,7 @@ function ReportPreview({ setIsHoldMenuVisible(true); } else if (chatReport && iouReport) { if (ReportUtils.isInvoiceReport(iouReport)) { - IOU.payInvoice(type, chatReport, iouReport); + IOU.payInvoice(type, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, iouReport); } @@ -268,7 +272,16 @@ function ReportPreview({ if (isScanning) { return translate('common.receipt'); } - let payerOrApproverName = isPolicyExpenseChat ? ReportUtils.getPolicyName(chatReport) : ReportUtils.getDisplayNameForParticipant(managerID, true); + + let payerOrApproverName; + if (isPolicyExpenseChat) { + payerOrApproverName = ReportUtils.getPolicyName(chatReport); + } else if (isInvoiceRoom) { + payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport, invoiceReceiverPolicy); + } else { + payerOrApproverName = ReportUtils.getDisplayNameForParticipant(managerID, true); + } + if (isApproved) { return translate('iou.managerApproved', {manager: payerOrApproverName}); } diff --git a/src/components/RoomHeaderAvatars.tsx b/src/components/RoomHeaderAvatars.tsx index a1c7ace5f8f9..8d8419efd698 100644 --- a/src/components/RoomHeaderAvatars.tsx +++ b/src/components/RoomHeaderAvatars.tsx @@ -18,8 +18,8 @@ type RoomHeaderAvatarsProps = { function RoomHeaderAvatars({icons, reportID}: RoomHeaderAvatarsProps) { const navigateToAvatarPage = (icon: Icon) => { - if (icon.type === CONST.ICON_TYPE_WORKSPACE) { - Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID)); + if (icon.type === CONST.ICON_TYPE_WORKSPACE && icon.id) { + Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(reportID, icon.id.toString())); return; } diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton.tsx index d1803f403469..5c984af94d16 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton.tsx @@ -5,11 +5,13 @@ import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as BankAccounts from '@userActions/BankAccounts'; import * as IOU from '@userActions/IOU'; +import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; @@ -41,7 +43,7 @@ type SettlementButtonOnyxProps = { type SettlementButtonProps = SettlementButtonOnyxProps & { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType) => void; + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; /** Callback when the payment options popover is shown */ onPaymentOptionsShow?: () => void; @@ -151,6 +153,9 @@ function SettlementButton({ }: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + + const primaryPolicy = useMemo(() => PolicyActions.getPrimaryPolicy(activePolicyID), [activePolicyID]); const session = useSession(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. @@ -207,20 +212,39 @@ function SettlementButton({ } if (isInvoiceReport) { - buttonOptions.push({ - text: translate('iou.settlePersonal', {formattedAmount}), - icon: Expensicons.User, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - backButtonText: translate('iou.individual'), - subMenuItems: [ - { - text: translate('iou.payElsewhere', {formattedAmount: ''}), - icon: Expensicons.Cash, - value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), - }, - ], - }); + if (ReportUtils.isIndividualInvoiceRoom(chatReport)) { + buttonOptions.push({ + text: translate('iou.settlePersonal', {formattedAmount}), + icon: Expensicons.User, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.individual'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE), + }, + ], + }); + } + + if (PolicyUtils.isPolicyAdmin(primaryPolicy) && PolicyUtils.isPaidGroupPolicy(primaryPolicy)) { + buttonOptions.push({ + text: translate('iou.settleBusiness', {formattedAmount}), + icon: Expensicons.Building, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + backButtonText: translate('iou.business'), + subMenuItems: [ + { + text: translate('iou.payElsewhere', {formattedAmount: ''}), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + onSelected: () => onPress(CONST.IOU.PAYMENT_TYPE.ELSEWHERE, true), + }, + ], + }); + } } if (shouldShowApproveButton) { @@ -234,7 +258,7 @@ function SettlementButton({ return buttonOptions; // We don't want to reorder the options when the preferred payment method changes while the button is still visible // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [currency, formattedAmount, iouReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); + }, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { @@ -267,7 +291,7 @@ function SettlementButton({ return ( onPress(paymentType)} enablePaymentsRoute={enablePaymentsRoute} addBankAccountRoute={addBankAccountRoute} addDebitCardRoute={addDebitCardRoute} diff --git a/src/languages/en.ts b/src/languages/en.ts index 1340ec5e7978..83629e8ccaba 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -732,6 +732,7 @@ export default { settledExpensify: 'Paid', settledElsewhere: 'Paid elsewhere', individual: 'Individual', + business: 'Business', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} with Expensify` : `Pay with Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pay ${formattedAmount} as an individual` : `Pay as an individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pay ${formattedAmount}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 6e2c45d0abc0..f5ce103ef098 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -728,6 +728,7 @@ export default { settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', individual: 'Individual', + business: 'Empresa', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), settlePersonal: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pago ${formattedAmount} como individuo` : `Pago individual`), settlePayment: ({formattedAmount}: SettleExpensifyCardParams) => `Pagar ${formattedAmount}`, diff --git a/src/libs/API/parameters/PayInvoiceParams.ts b/src/libs/API/parameters/PayInvoiceParams.ts index 4c6633749adb..a6b9746d87bc 100644 --- a/src/libs/API/parameters/PayInvoiceParams.ts +++ b/src/libs/API/parameters/PayInvoiceParams.ts @@ -4,6 +4,7 @@ type PayInvoiceParams = { reportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + payAsBusiness: boolean; }; export default PayInvoiceParams; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fa62f6b1107c..a43eab452463 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1269,6 +1269,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & }; [SCREENS.REPORT_AVATAR]: { reportID: string; + policyID?: string; }; [SCREENS.NOT_FOUND]: undefined; [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2308cd14c363..64360b0b7e36 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -990,11 +990,20 @@ function isTripRoom(report: OnyxEntry): boolean { return isChatReport(report) && getChatType(report) === CONST.REPORT.CHAT_TYPE.TRIP_ROOM; } +function isIndividualInvoiceRoom(report: OnyxEntry): boolean { + return isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; +} + function isCurrentUserInvoiceReceiver(report: OnyxEntry): boolean { if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { return currentUserAccountID === report.invoiceReceiver.accountID; } + if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS) { + const policy = PolicyUtils.getPolicy(report.invoiceReceiver.policyID); + return PolicyUtils.isPolicyAdmin(policy); + } + return false; } @@ -1159,10 +1168,8 @@ function findSelfDMReportID(): string | undefined { * In this case report and workspace members must be compared to determine whether the report belongs to the workspace. */ function doesReportBelongToWorkspace(report: OnyxEntry, policyMemberAccountIDs: number[], policyID?: string) { - return ( - isConciergeChatReport(report) || - (report?.policyID === CONST.POLICY.ID_FAKE || !report?.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : report?.policyID === policyID) - ); + const isPolicyRelatedReport = report?.policyID === policyID || !!(report?.invoiceReceiver && 'policyID' in report.invoiceReceiver && report.invoiceReceiver.policyID === policyID); + return isConciergeChatReport(report) || (report?.policyID === CONST.POLICY.ID_FAKE || !report?.policyID ? hasParticipantInArray(report, policyMemberAccountIDs) : isPolicyRelatedReport); } /** @@ -2071,6 +2078,7 @@ function getIcons( defaultName = '', defaultAccountID = -1, policy?: OnyxInputOrEntry, + invoiceReceiverPolicy?: OnyxInputOrEntry, ): Icon[] { if (isEmptyObject(report)) { const fallbackIcon: Icon = { @@ -2148,9 +2156,15 @@ function getIcons( if (report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL) { icons.push(...getIconsForParticipants([report?.invoiceReceiver.accountID], personalDetails)); } else { - const receiverPolicy = getPolicy(report?.invoiceReceiver?.policyID); + const receiverPolicyID = report?.invoiceReceiver?.policyID; + const receiverPolicy = invoiceReceiverPolicy ?? getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(report, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicyID, + }); } } } @@ -2222,10 +2236,16 @@ function getIcons( return icons; } - const receiverPolicy = getPolicy(invoiceRoomReport?.invoiceReceiver?.policyID); + const receiverPolicyID = invoiceRoomReport?.invoiceReceiver?.policyID; + const receiverPolicy = invoiceReceiverPolicy ?? getPolicy(receiverPolicyID); if (!isEmptyObject(receiverPolicy)) { - icons.push(getWorkspaceIcon(invoiceRoomReport, receiverPolicy)); + icons.push({ + source: receiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(receiverPolicy.name), + type: CONST.ICON_TYPE_WORKSPACE, + name: receiverPolicy.name, + id: receiverPolicyID, + }); } return icons; @@ -2671,7 +2691,7 @@ function getAvailableReportFields(report: Report, policyReportFields: PolicyRepo /** * Get the title for an IOU or expense chat which will be showing the payer and the amount */ -function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry): string { +function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry, invoiceReceiverPolicy?: OnyxEntry): string { const isReportSettled = isSettled(report?.reportID ?? '-1'); const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? '-1'); const titleReportField = getFormulaTypeReportField(reportFields ?? {}); @@ -2682,7 +2702,17 @@ function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry const moneyRequestTotal = getMoneyRequestSpendBreakdown(report).totalDisplaySpend; const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); - let payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; + + let payerOrApproverName; + if (isExpenseReport(report)) { + payerOrApproverName = getPolicyName(report, false, policy); + } else if (isInvoiceReport(report)) { + const chatReport = getReportOrDraftReport(report?.chatReportID); + payerOrApproverName = getInvoicePayerName(chatReport, invoiceReceiverPolicy); + } else { + payerOrApproverName = getDisplayNameForParticipant(report?.managerID) ?? ''; + } + const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, amount: formattedAmount, @@ -3385,7 +3415,7 @@ function getAdminRoomInvitedParticipants(parentReportAction: OnyxEntry): string { +function getInvoicePayerName(report: OnyxEntry, invoiceReceiverPolicy?: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; @@ -3393,7 +3423,7 @@ function getInvoicePayerName(report: OnyxEntry): string { return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiver.accountID]); } - return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]); + return getPolicyName(report, false, invoiceReceiverPolicy ?? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiver?.policyID}`]); } /** @@ -3469,13 +3499,13 @@ function getReportActionMessage(reportAction: OnyxEntry, reportID? /** * Get the title for an invoice room. */ -function getInvoicesChatName(report: OnyxEntry): string { +function getInvoicesChatName(report: OnyxEntry, receiverPolicy: OnyxEntry): string { const invoiceReceiver = report?.invoiceReceiver; const isIndividual = invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; const invoiceReceiverAccountID = isIndividual ? invoiceReceiver.accountID : -1; const invoiceReceiverPolicyID = isIndividual ? '' : invoiceReceiver?.policyID ?? '-1'; - const isCurrentUserReceiver = - (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyEmployee(invoiceReceiverPolicyID, allPolicies)); + const invoiceReceiverPolicy = receiverPolicy ?? getPolicy(invoiceReceiverPolicyID); + const isCurrentUserReceiver = (isIndividual && invoiceReceiverAccountID === currentUserAccountID) || (!isIndividual && PolicyUtils.isPolicyAdmin(invoiceReceiverPolicy)); if (isCurrentUserReceiver) { return getPolicyName(report); @@ -3485,8 +3515,7 @@ function getInvoicesChatName(report: OnyxEntry): string { return PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[invoiceReceiverAccountID]); } - // TODO: Check this flow in a scope of the Invoice V0.3 - return getPolicyName(report, false, allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]); + return getPolicyName(report, false, invoiceReceiverPolicy); } /** @@ -3497,6 +3526,7 @@ function getReportName( policy?: OnyxEntry, parentReportActionParam?: OnyxInputOrEntry, personalDetails?: Partial, + invoiceReceiverPolicy?: OnyxEntry, ): string { let formattedName: string | undefined; const parentReportAction = parentReportActionParam ?? ReportActionsUtils.getParentReportAction(report); @@ -3568,12 +3598,16 @@ function getReportName( formattedName = getPolicyExpenseChatName(report, policy); } - if (isMoneyRequestReport(report) || isInvoiceReport(report)) { + if (isMoneyRequestReport(report)) { formattedName = getMoneyRequestReportName(report, policy); } + if (isInvoiceReport(report)) { + formattedName = getMoneyRequestReportName(report, policy, invoiceReceiverPolicy); + } + if (isInvoiceRoom(report)) { - formattedName = getInvoicesChatName(report); + formattedName = getInvoicesChatName(report, invoiceReceiverPolicy); } if (isArchivedRoom(report, getReportNameValuePairs(report?.reportID))) { @@ -3584,10 +3618,6 @@ function getReportName( formattedName = getDisplayNameForParticipant(currentUserAccountID, undefined, undefined, true, personalDetails); } - if (isInvoiceRoom(report)) { - formattedName = getInvoicesChatName(report); - } - if (formattedName) { return formatReportLastMessageText(formattedName); } @@ -3662,14 +3692,14 @@ function getPendingChatMembers(accountIDs: number[], previousPendingChatMembers: /** * Gets the parent navigation subtitle for the report */ -function getParentNavigationSubtitle(report: OnyxEntry): ParentNavigationSummaryParams { +function getParentNavigationSubtitle(report: OnyxEntry, invoiceReceiverPolicy?: OnyxEntry): ParentNavigationSummaryParams { const parentReport = getParentReport(report); if (isEmptyObject(parentReport)) { return {}; } if (isInvoiceReport(report) || isInvoiceRoom(parentReport)) { - let reportName = `${getPolicyName(parentReport)} & ${getInvoicePayerName(parentReport)}`; + let reportName = `${getPolicyName(parentReport)} & ${getInvoicePayerName(parentReport, invoiceReceiverPolicy)}`; if (isArchivedRoom(parentReport, getReportNameValuePairs(parentReport?.reportID))) { reportName += ` (${Localize.translateLocal('common.archived')})`; @@ -7748,6 +7778,7 @@ export { getReport, getReportNameValuePairs, hasReportViolations, + isIndividualInvoiceRoom, }; export type { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index ceb10be6a634..62d88e1fe706 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -237,6 +237,7 @@ function getOptionData({ parentReportAction, hasViolations, transactionViolations, + invoiceReceiverPolicy, }: { report: OnyxEntry; reportActions: OnyxEntry; @@ -245,6 +246,7 @@ function getOptionData({ policy: OnyxEntry | undefined; parentReportAction: OnyxEntry | undefined; hasViolations: boolean; + invoiceReceiverPolicy?: OnyxEntry; transactionViolations?: OnyxCollection; }): ReportUtils.OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for @@ -454,13 +456,13 @@ function getOptionData({ result.phoneNumber = personalDetail?.phoneNumber; } - const reportName = ReportUtils.getReportName(report, policy); + const reportName = ReportUtils.getReportName(report, policy, undefined, undefined, invoiceReceiverPolicy); result.text = reportName; result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, personalDetail?.avatar, personalDetail?.login, personalDetail?.accountID, policy); + result.icons = ReportUtils.getIcons(report, personalDetails, personalDetail?.avatar, personalDetail?.login, personalDetail?.accountID, policy, invoiceReceiverPolicy); result.displayNamesWithTooltips = displayNamesWithTooltips; if (status) { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1a6d9d3c207f..9549bc23a9d8 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -269,6 +269,12 @@ Onyx.connect({ }, }); +let primaryPolicyID: OnyxEntry; +Onyx.connect({ + key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, + callback: (value) => (primaryPolicyID = value), +}); + /** * Get the report or draft report given a reportID */ @@ -6506,13 +6512,22 @@ function getReportFromHoldRequestsOnyxData( } function getPayMoneyRequestParams( - chatReport: OnyxTypes.Report, + initialChatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType, full: boolean, + payAsBusiness?: boolean, ): PayMoneyRequestData { const isInvoiceReport = ReportUtils.isInvoiceReport(iouReport); + let chatReport = initialChatReport; + + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', primaryPolicyID); + if (existingB2BInvoiceRoom) { + chatReport = existingB2BInvoiceRoom; + } + } let total = (iouReport.total ?? 0) - (iouReport.nonReimbursableTotal ?? 0); if (ReportUtils.hasHeldExpenses(iouReport.reportID) && !full && !!iouReport.unheldTotal) { @@ -6545,19 +6560,27 @@ function getPayMoneyRequestParams( optimisticNextStep = NextStepUtils.buildNextStep(iouReport, CONST.REPORT.STATUS_NUM.REIMBURSED); } + const optimisticChatReport = { + ...chatReport, + lastReadTime: DateUtils.getDBTime(), + lastVisibleActionCreated: optimisticIOUReportAction.created, + hasOutstandingChildRequest: false, + iouReportID: null, + lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), + lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), + }; + if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && primaryPolicyID) { + optimisticChatReport.invoiceReceiver = { + type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, + policyID: primaryPolicyID, + }; + } + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, - value: { - ...chatReport, - lastReadTime: DateUtils.getDBTime(), - lastVisibleActionCreated: optimisticIOUReportAction.created, - hasOutstandingChildRequest: false, - iouReportID: null, - lastMessageText: ReportActionsUtils.getReportActionText(optimisticIOUReportAction), - lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticIOUReportAction), - }, + value: optimisticChatReport, }, { onyxMethod: Onyx.METHOD.MERGE, @@ -7305,19 +7328,20 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R API.write(apiCommand, params, {optimisticData, successData, failureData}); } -function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report) { +function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxTypes.Report, payAsBusiness = false) { const recipient = {accountID: invoiceReport.ownerAccountID}; const { optimisticData, successData, failureData, params: {reportActionID}, - } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true); + } = getPayMoneyRequestParams(chatReport, invoiceReport, recipient, paymentMethodType, true, payAsBusiness); const params: PayInvoiceParams = { reportID: invoiceReport.reportID, reportActionID, paymentMethodType, + payAsBusiness, }; API.write(WRITE_COMMANDS.PAY_INVOICE, params, {optimisticData, successData, failureData}); diff --git a/src/pages/ReportAvatar.tsx b/src/pages/ReportAvatar.tsx index 5ec34e3b71dc..eeb6829e7fc0 100644 --- a/src/pages/ReportAvatar.tsx +++ b/src/pages/ReportAvatar.tsx @@ -20,8 +20,9 @@ type ReportAvatarOnyxProps = { type ReportAvatarProps = ReportAvatarOnyxProps & StackScreenProps; -function ReportAvatar({report = {} as Report, policies, isLoadingApp = true}: ReportAvatarProps) { - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`]; +function ReportAvatar({report = {} as Report, route, policies, isLoadingApp = true}: ReportAvatarProps) { + const policyID = route.params.policyID ?? '-1'; + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const policyName = ReportUtils.getPolicyName(report, false, policy); const avatarURL = ReportUtils.getWorkspaceAvatar(report); diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 626da27e22da..4282eb689d22 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -1,7 +1,7 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import Badge from '@components/Badge'; import Button from '@components/Button'; import CaretWrapper from '@components/CaretWrapper'; @@ -64,6 +64,8 @@ type HeaderViewProps = HeaderViewOnyxProps & { function HeaderView({report, personalDetails, parentReport, parentReportAction, policy, reportID, onNavigationMenuButtonClicked, shouldUseNarrowLayout = false}: HeaderViewProps) { const [isDeleteTaskConfirmModalVisible, setIsDeleteTaskConfirmModalVisible] = React.useState(false); + const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`); + const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -82,7 +84,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, const isTaskReport = ReportUtils.isTaskReport(report); const reportHeaderData = !isTaskReport && !isChatThread && report.parentReportID ? parentReport : report; // Use sorted display names for the title for group chats on native small screen widths - const title = ReportUtils.getReportName(reportHeaderData, undefined, parentReportAction, personalDetails); + const title = ReportUtils.getReportName(reportHeaderData, undefined, parentReportAction, personalDetails, invoiceReceiverPolicy); const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData); const reportDescription = ReportUtils.getReportDescriptionText(report); @@ -129,7 +131,7 @@ function HeaderView({report, personalDetails, parentReport, parentReportAction, const shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); const defaultSubscriptSize = ReportUtils.isExpenseRequest(report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; - const icons = ReportUtils.getIcons(reportHeaderData, personalDetails); + const icons = ReportUtils.getIcons(reportHeaderData, personalDetails, null, '', -1, undefined, invoiceReceiverPolicy); const brickRoadIndicator = ReportUtils.hasReportNameError(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; const shouldShowBorderBottom = !isTaskReport || !shouldUseNarrowLayout; const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(report); diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 764a8c0d3d1e..ec9f5ea9915f 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -1,7 +1,8 @@ +import lodashIsEqual from 'lodash/isEqual'; import React, {memo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx, withOnyx} from 'react-native-onyx'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -35,29 +36,30 @@ type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID: string | undefined; }; -function ReportActionItemCreated(props: ReportActionItemCreatedProps) { +function ReportActionItemCreated({report, personalDetails, policy, reportID}: ReportActionItemCreatedProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {shouldUseNarrowLayout, isLargeScreenWidth} = useResponsiveLayout(); + const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? report.invoiceReceiver.policyID : -1}`); - if (!ReportUtils.isChatReport(props.report)) { + if (!ReportUtils.isChatReport(report)) { return null; } - let icons = ReportUtils.getIcons(props.report, props.personalDetails); - const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(props.report); + let icons = ReportUtils.getIcons(report, personalDetails, null, '', -1, undefined, invoiceReceiverPolicy); + const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(report); - if (ReportUtils.isInvoiceRoom(props.report) && ReportUtils.isCurrentUserInvoiceReceiver(props.report)) { + if (ReportUtils.isInvoiceRoom(report) && ReportUtils.isCurrentUserInvoiceReceiver(report)) { icons = [...icons].reverse(); } return ( navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID, undefined, true)} + onClose={() => navigateToConciergeChatAndDeleteReport(report?.reportID ?? reportID, undefined, true)} > @@ -65,9 +67,9 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { accessibilityLabel={translate('accessibilityHints.chatWelcomeMessage')} style={[styles.p5]} > - + ReportUtils.navigateToDetailsPage(props.report)} + onPress={() => ReportUtils.navigateToDetailsPage(report)} style={[styles.mh5, styles.mb3, styles.alignSelfStart, shouldDisableDetailPage && styles.cursorDefault]} accessibilityLabel={translate('common.details')} role={CONST.ROLE.BUTTON} @@ -84,8 +86,8 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { @@ -122,6 +124,7 @@ export default withOnyx