Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ function MoneyRequestReportPreviewContent({
<ReportPreviewActionButton
iouReportID={iouReportID}
chatReportID={chatReportID}
chatReport={chatReport}
isPaidAnimationRunning={isPaidAnimationRunning}
isApprovedAnimationRunning={isApprovedAnimationRunning}
isSubmittingAnimationRunning={isSubmittingAnimationRunning}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {delegateEmailSelector} from '@selectors/Account';
import {hasSeenTourSelector} from '@selectors/Onboarding';
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton';
Expand All @@ -18,6 +19,7 @@ import useReportTransactionsCollection from '@hooks/useReportTransactionsCollect
import {generateDefaultWorkspaceName} from '@libs/actions/Policy/Policy';
import {getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils';
import {
getReportOrDraftReport,
hasHeldExpensesFromTransactions as hasHeldExpensesReportUtils,
hasUpdatedTotal,
hasViolations as hasViolationsReportUtils,
Expand All @@ -28,12 +30,13 @@ import {approveMoneyRequest, canIOUBePaid as canIOUBePaidIOUActions} from '@user
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Transaction} from '@src/types/onyx';
import type {Report, Transaction} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';

type PayActionButtonProps = {
iouReportID: string | undefined;
chatReportID: string | undefined;
chatReport: OnyxEntry<Report>;
isPaidAnimationRunning: boolean;
isApprovedAnimationRunning: boolean;
stopAnimation: () => void;
Expand All @@ -49,6 +52,7 @@ type PayActionButtonProps = {
function PayActionButton({
iouReportID,
chatReportID,
chatReport,
isPaidAnimationRunning,
isApprovedAnimationRunning,
stopAnimation,
Expand All @@ -74,7 +78,6 @@ function PayActionButton({
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
const activePolicy = usePolicy(activePolicyID);
const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`);
const chatReportPolicy = usePolicy(chatReport?.policyID);
const [invoiceReceiverPolicy] = useOnyx(
Expand Down Expand Up @@ -169,11 +172,12 @@ function PayActionButton({
} else if (hasHeldExpensesReportUtils(transactions)) {
onHoldMenuOpen(CONST.IOU.REPORT_ACTION_TYPE.PAY, type, shouldShowPayButton);
} else if (chatReport && iouReport) {
const currentChatReport = getReportOrDraftReport(chatReportID) ?? chatReport;
if (isInvoiceReportUtils(iouReport)) {
startAnimation();
payInvoice({
paymentMethodType: type,
chatReport,
chatReport: currentChatReport,
invoiceReport: iouReport,
invoiceReportCurrentNextStepDeprecated: iouReportNextStep,
introSelected,
Expand All @@ -192,7 +196,7 @@ function PayActionButton({
} else {
payMoneyRequest({
paymentType: type,
chatReport,
chatReport: currentChatReport,
iouReport,
introSelected,
iouReportCurrentNextStepDeprecated: iouReportNextStep,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import ExportWithDropdownMenu from '@components/ReportActionItem/ExportWithDropdownMenu';
Expand All @@ -21,7 +22,7 @@ import {canIOUBePaid as canIOUBePaidIOUActions} from '@userActions/IOU/ReportWor
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft';
import type {Transaction} from '@src/types/onyx';
import type {Report, Transaction} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import ApproveActionButton from './ApproveActionButton';
import PayActionButton from './PayActionButton';
Expand All @@ -30,6 +31,7 @@ import SubmitActionButton from './SubmitActionButton';
type ReportPreviewActionButtonProps = {
iouReportID: string | undefined;
chatReportID: string | undefined;
chatReport: OnyxEntry<Report>;
isPaidAnimationRunning: boolean;
isApprovedAnimationRunning: boolean;
isSubmittingAnimationRunning: boolean;
Expand All @@ -47,6 +49,7 @@ type ReportPreviewActionButtonProps = {
function ReportPreviewActionButton({
iouReportID,
chatReportID,
chatReport,
isPaidAnimationRunning,
isApprovedAnimationRunning,
isSubmittingAnimationRunning,
Expand All @@ -67,7 +70,6 @@ function ReportPreviewActionButton({
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'ReceiptPlus', 'Plus']);

const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`);
const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined;
const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`);
Expand Down Expand Up @@ -169,6 +171,7 @@ function ReportPreviewActionButton({
<PayActionButton
iouReportID={iouReportID}
chatReportID={chatReportID}
chatReport={chatReport}
isPaidAnimationRunning={isPaidAnimationRunning}
isApprovedAnimationRunning={isApprovedAnimationRunning}
stopAnimation={stopAnimation}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ function MoneyRequestReportPreview({
iouReportID,
policyID,
chatReportID,
chatReport,
action,
isHovered = false,
isWhisper = false,
Expand All @@ -42,7 +43,6 @@ function MoneyRequestReportPreview({
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout();
const personalDetailsList = usePersonalDetails();
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const invoiceReceiverPolicyID = chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : undefined;
const [invoiceReceiverPolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(invoiceReceiverPolicyID)}`);
const invoiceReceiverPersonalDetail = chatReport?.invoiceReceiver && 'accountID' in chatReport.invoiceReceiver ? personalDetailsList?.[chatReport.invoiceReceiver.accountID] : null;
Expand Down Expand Up @@ -128,7 +128,7 @@ function MoneyRequestReportPreview({

const renderItem: ListRenderItem<Transaction> = ({item}) => (
<TransactionPreview
chatReportID={chatReportID}
chatReport={chatReport}
action={getIOUActionForReportID(item.reportID, item.transactionID)}
contextAction={action}
reportID={item.reportID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ type MoneyRequestReportPreviewProps = {
/** The associated chatReport */
chatReportID: string | undefined;

/** The chat report this preview belongs to */
chatReport: OnyxEntry<Report>;

/** The active IOUReport, used for Onyx subscription */
iouReportID: string | undefined;

Expand All @@ -53,7 +56,6 @@ type MoneyRequestReportPreviewProps = {
};

type MoneyRequestReportPreviewContentOnyxProps = {
chatReport: OnyxEntry<Report>;
invoiceReceiverPolicy: OnyxEntry<Policy>;
iouReport: OnyxEntry<Report>;
transactions: Transaction[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ import type {TransactionPreviewProps} from './types';
function TransactionPreview(props: TransactionPreviewProps) {
const {translate} = useLocalize();
const {convertToDisplayString} = useCurrencyListActions();
const {action, chatReportID, reportID, transactionID: transactionIDFromProps, onPreviewPressed, shouldHighlight, reportPreviewAction, contextAction} = props;
const {action, chatReport, reportID, transactionID: transactionIDFromProps, onPreviewPressed, shouldHighlight, reportPreviewAction, contextAction} = props;
const chatReportID = chatReport?.reportID;
const {anchor: contextMenuAnchorRef, shouldDisplayContextMenu, originalReportID} = useShowContextMenuState();
const {checkIfContextMenuActive} = useShowContextMenuActions();

const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const route = useRoute<PlatformStackRouteProp<TransactionDuplicateNavigatorParamList, typeof SCREENS.TRANSACTION_DUPLICATE.REVIEW>>();
const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action);
const transactionID = transactionIDFromProps ?? (isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : undefined);
Expand Down
6 changes: 3 additions & 3 deletions src/components/ReportActionItem/TransactionPreview/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ type TransactionPreviewProps = {
/** The active reportID linked to the transaction */
iouReportID: string | undefined;

/** The associated chatReport */
chatReportID: string | undefined;
/** The associated chat report */
chatReport: OnyxEntry<Report>;

/** The ID of the current report */
reportID: string | undefined;
Expand Down Expand Up @@ -101,7 +101,7 @@ type TransactionPreviewContentProps = {
violations: TransactionViolations;

/** Holds the chat report entry from Onyx */
chatReport?: Report;
chatReport: OnyxEntry<Report>;

/** Optional details about people involved in the transaction */
personalDetails?: PersonalDetailsList;
Expand Down
17 changes: 12 additions & 5 deletions src/hooks/useHoldMenuSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {delegateEmailSelector} from '@selectors/Account';
import {hasSeenTourSelector} from '@selectors/Onboarding';
import type {OnyxEntry} from 'react-native-onyx';
import {useDelegateNoAccessActions, useDelegateNoAccessState} from '@components/DelegateNoAccessModalProvider';
import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {getReportOrDraftReport, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {payMoneyRequest} from '@userActions/IOU/PayMoneyRequest';
import {approveMoneyRequest} from '@userActions/IOU/ReportWorkflow';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -59,9 +59,16 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment

const animationCallback = () => onConfirm?.(full);

// moneyRequestReport/chatReport can be lightweight versions of the report (the report list drops fields
// like the last message text/time so it doesn't re-render on every new message). The pay/approve actions
// restore the report on failure by merging it back in, so we grab the full reports here to make sure the
// chat's last message comes back correctly if the payment fails.
const currentMoneyRequestReport = getReportOrDraftReport(moneyRequestReport?.reportID) ?? moneyRequestReport;
const currentChatReport = getReportOrDraftReport(chatReport?.reportID) ?? chatReport;

if (isApprove) {
approveMoneyRequest({
expenseReport: moneyRequestReport,
expenseReport: currentMoneyRequestReport,
policy: activePolicy,
currentUserAccountIDParam: currentUserDetails.accountID,
currentUserEmailParam: currentUserDetails.email ?? '',
Expand All @@ -77,11 +84,11 @@ function useHoldMenuSubmit({moneyRequestReport, chatReport, requestType, payment
expenseReportPolicy: policy,
delegateEmail,
});
} else if (chatReport && paymentType) {
} else if (currentChatReport && paymentType) {
payMoneyRequest({
paymentType,
chatReport,
iouReport: moneyRequestReport,
chatReport: currentChatReport,
iouReport: currentMoneyRequestReport,
introSelected,
iouReportCurrentNextStepDeprecated: moneyRequestReportNextStep,
currentUserAccountID: currentUserDetails.accountID,
Expand Down
1 change: 1 addition & 0 deletions src/pages/inbox/report/ReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ function ReportActionItem({
<ActionContentRouter
action={action}
report={report}
chatReport={chatReport}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Should we update the ReportActionItemTest? It still passes chatReport={undefined}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chatReport is only read when rendering a REPORT_PREVIEW action, and these tests don't render that action, so it's never used here. undefined is valid and a real report wouldn't change anything

reportID={reportID}
originalReportID={originalReportID}
iouReport={iouReport}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type ActionContentRouterProps = {
/** Report for this action */
report: OnyxEntry<OnyxTypes.Report>;

/** The chat report associated with the report for this action */
chatReport: OnyxEntry<OnyxTypes.Report>;

/** ID of the original report from which the given reportAction is first created */
originalReportID?: string;

Expand Down Expand Up @@ -124,6 +127,7 @@ type ActionContentRouterProps = {
function ActionContentRouter({
action,
report,
chatReport,
originalReportID,
iouReport,
reportID,
Expand Down Expand Up @@ -167,7 +171,7 @@ function ActionContentRouter({
<ChatTransactionPreview
action={action}
reportID={reportID}
chatReportID={report?.type === CONST.REPORT.TYPE.CHAT ? reportID : report?.chatReportID}
chatReport={report?.type === CONST.REPORT.TYPE.CHAT ? report : chatReport}
iouReport={report?.type === CONST.REPORT.TYPE.CHAT ? iouReport : report}
shouldShowSplitPreview={shouldShowSplitPreview}
transactionID={shouldShowSplitPreview ? moneyRequestOriginalMessage?.IOUTransactionID : undefined}
Expand All @@ -192,6 +196,7 @@ function ActionContentRouter({
iouReportID={getIOUReportIDFromReportActionPreview(action)}
policyID={policyID}
chatReportID={reportID}
chatReport={report}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve full chat report for payment rollback

When the preview is rendered from the report list, report here is the reportStable projection from ReportActionsList, and that selector deliberately omits lastMessageText, lastMessageHtml, lastReadTime, etc. Passing that projection down into the pay button means payMoneyRequest/payInvoice now build optimistic and failure Onyx updates from a stripped report; if the payment API fails, the failure MERGE with this stripped chatReport cannot restore the previous last-message fields after the optimistic “paid” message was written, leaving the chat preview showing a failed payment as the latest message. Please keep a full report subscription for mutation/rollback paths, or include the omitted fields before calling the pay actions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the full report for hold-menu payments

When this preview is rendered from ReportActionsList, report is the reportStable projection from getStableReportSelector, which omits the last-message fields. The fresh evidence is the held-expense path: this same prop reaches ProcessMoneyReportHoldMenu, and useHoldMenuSubmit still calls payMoneyRequest with it, while the full-report fallback was only added in PayActionButton. If the user pays a report with held expenses and the API fails, PayMoneyRequest restores value: chatReport, so the MERGE cannot bring back the previous lastMessageText/lastMessageHtml, leaving the chat preview stuck on the optimistic paid message.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

action={action}
isHovered={hovered}
isWhisper={isWhisper}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ type ChatTransactionPreviewProps = {
/** The ID of the current report where the preview is rendered */
reportID: string | undefined;

/** The ID of the chat report that owns the transaction. Defaults to reportID for chats; expense reports pass their parent chat. */
chatReportID?: string;
/** The chat report that owns the transaction */
chatReport: OnyxEntry<OnyxTypes.Report>;

/** The IOU report linked to this transaction, used when creating a transaction thread on demand */
iouReport: OnyxEntry<OnyxTypes.Report>;
Expand All @@ -36,7 +36,7 @@ type ChatTransactionPreviewProps = {
transactionID: string | undefined;
};

function ChatTransactionPreview({action, reportID, chatReportID = reportID, iouReport, shouldShowSplitPreview, transactionID}: ChatTransactionPreviewProps) {
function ChatTransactionPreview({action, reportID, chatReport, iouReport, shouldShowSplitPreview, transactionID}: ChatTransactionPreviewProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();
Expand All @@ -50,7 +50,7 @@ function ChatTransactionPreview({action, reportID, chatReportID = reportID, iouR
<View style={[styles.mt1, styles.w100]}>
<TransactionPreview
iouReportID={getIOUReportIDFromReportActionPreview(action)}
chatReportID={chatReportID}
chatReport={chatReport}
reportID={reportID}
action={action}
isBillSplit={isSplitBillAction(action)}
Expand Down
9 changes: 5 additions & 4 deletions tests/ui/MoneyRequestReportPreview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ const renderPage = ({isWhisper = false, isHovered = false}: Partial<MoneyRequest
policyID={mockChatReport.policyID}
action={mockAction}
iouReportID={mockIOUReport.reportID}
chatReportID={mockChatReport.chatReportID}
chatReportID={mockChatReport.reportID}
chatReport={mockChatReport}
onPaymentOptionsShow={() => {}}
onPaymentOptionsHide={() => {}}
isHovered={isHovered}
Expand All @@ -130,7 +131,7 @@ const getTransactionDisplayAmountAndHeaderText = (transaction: Transaction) => {
const isTransactionMadeWithCard = isManagedCardTransaction(transaction);
const cashOrCard = isTransactionMadeWithCard ? TestHelper.translateLocal('iou.card') : TestHelper.translateLocal('iou.cash');
const transactionHeaderText = `${date} ${CONST.DOT_SEPARATOR} ${cashOrCard}`;
const transactionDisplayAmount = convertToDisplayString(transaction.amount, transaction.currency);
const transactionDisplayAmount = convertToDisplayString(-transaction.amount, transaction.currency);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Any reason we're making the amount negative?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test showed positive before only because it passed the wrong chatReportID, so the component got an undefined chatReport. Now it gets the real one, so the displayed amount is correctly negative.

return {transactionHeaderText, transactionDisplayAmount};
};

Expand Down Expand Up @@ -179,7 +180,7 @@ const setReportPreviewData = (
};

const setHasOnceLoadedReportActions = async (hasOnceLoadedReportActions: boolean) => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockChatReport.chatReportID}`, {
await Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${mockChatReport.reportID}`, {
hasOnceLoadedReportActions,
});
};
Expand Down Expand Up @@ -223,7 +224,7 @@ describe('MoneyRequestReportPreview', () => {
for (const transaction of arrayOfTransactions) {
const {transactionDisplayAmount, transactionHeaderText} = getTransactionDisplayAmountAndHeaderText(transaction);

expect(screen.getByText(transactionDisplayAmount)).toBeOnTheScreen();
expect(screen.getAllByText(transactionDisplayAmount).length).toBeGreaterThan(0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Is this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, with the correct chatReport the amount renders in more than one place

expect(screen.getAllByText(transactionHeaderText)).toHaveLength(arrayOfTransactions.length);
expect(screen.getAllByText(transaction.merchant)).toHaveLength(arrayOfTransactions.length);
}
Expand Down
Loading