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
1 change: 0 additions & 1 deletion .storybook/webpackMockPaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ export default {
'react-native$': 'react-native-web',
'@react-native-community/netinfo': path.resolve(__dirname, '../__mocks__/@react-native-community/netinfo.ts'),
'@react-navigation/native': path.resolve(__dirname, '../__mocks__/@react-navigation/native'),
'@libs/TransactionPreviewUtils': path.resolve(__dirname, '../src/libs/__mocks__/TransactionPreviewUtils.ts'),
};
/* eslint-enable @typescript-eslint/naming-convention */
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function MoneyRequestReportPreview({
[StyleUtils, currentWidth, currentWrapperWidth, shouldUseNarrowLayout, transactions.length],
);

const shouldShowIOUData = useMemo(() => {
const shouldShowPayerAndReceiver = useMemo(() => {
if (!isIOUReport(iouReport) && action.childType !== CONST.REPORT.TYPE.IOU) {
return false;
}
Expand Down Expand Up @@ -99,7 +99,7 @@ function MoneyRequestReportPreview({
transactionID={item.transactionID}
reportPreviewAction={action}
onPreviewPressed={openReportFromPreview}
shouldShowIOUData={shouldShowIOUData}
shouldShowPayerAndReceiver={shouldShowPayerAndReceiver}
/>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import {getCleanedTagName} from '@libs/PolicyUtils';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
import type {TransactionDetails} from '@libs/ReportUtils';
import {canEditMoneyRequest, getTransactionDetails, getWorkspaceIcon, isIOUReport, isPolicyExpenseChat, isReportApproved, isSettled} from '@libs/ReportUtils';
import {canEditMoneyRequest, getTransactionDetails, getWorkspaceIcon, isPolicyExpenseChat, isReportApproved, isSettled} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import type {TranslationPathOrText} from '@libs/TransactionPreviewUtils';
import {createTransactionPreviewConditionals, getIOUData, getTransactionPreviewTextAndTranslationPaths} from '@libs/TransactionPreviewUtils';
import {createTransactionPreviewConditionals, getIOUPayerAndReceiver, getTransactionPreviewTextAndTranslationPaths} from '@libs/TransactionPreviewUtils';
import {isScanning} from '@libs/TransactionUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import variables from '@styles/variables';
Expand All @@ -41,9 +41,10 @@ function TransactionPreviewContent({
isHovered,
chatReport,
personalDetails,
iouReport,
report,
transaction,
violations,
transactionRawAmount,
offlineWithFeedbackOnClose,
containerStyles,
transactionPreviewWidth,
Expand All @@ -53,32 +54,33 @@ function TransactionPreviewContent({
walletTermsErrors,
reportPreviewAction,
shouldHideOnDelete = true,
shouldShowIOUData,
shouldShowPayerAndReceiver,
navigateToReviewFields,
isReviewDuplicateTransactionPage = false,
}: TransactionPreviewContentProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();

const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${iouReport?.policyID}`, {canBeMissing: true});
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, {canBeMissing: true});
const transactionDetails = useMemo<Partial<TransactionDetails>>(() => getTransactionDetails(transaction, undefined, policy) ?? {}, [transaction, policy]);
const managerID = iouReport?.managerID ?? reportPreviewAction?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const ownerAccountID = iouReport?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const {amount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails;

const managerID = report?.managerID ?? reportPreviewAction?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const ownerAccountID = report?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID;
const isReportAPolicyExpenseChat = isPolicyExpenseChat(chatReport);
const {amount: requestAmount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails;
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(iouReport?.reportID)}`, {canBeMissing: true});
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.reportID)}`, {canBeMissing: true});

const transactionPreviewCommonArguments = useMemo(
() => ({
iouReport,
iouReport: report,
transaction,
action,
isBillSplit,
violations,
transactionDetails,
}),
[action, iouReport, isBillSplit, transaction, transactionDetails, violations],
[action, report, isBillSplit, transaction, transactionDetails, violations],
);

const conditionals = useMemo(
Expand Down Expand Up @@ -118,19 +120,15 @@ function TransactionPreviewContent({
const displayAmountText = getTranslatedText(previewText.displayAmountText);
const displayDeleteAmountText = getTranslatedText(previewText.displayDeleteAmountText);

const iouData = shouldShowIOUData
? getIOUData(managerID, ownerAccountID, isIOUReport(iouReport) || reportPreviewAction?.childType === CONST.REPORT.TYPE.IOU, personalDetails, requestAmount ?? 0)
: undefined;
const {from, to} = iouData ?? {from: null, to: null};
const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const shouldShowCategoryOrTag = shouldShowCategory || shouldShowTag;
const shouldShowMerchantOrDescription = shouldShowDescription || shouldShowMerchant;
const shouldShowIOUHeader = !!from && !!to;

const description = truncate(StringUtils.lineBreaksToSpaces(Parser.htmlToText(requestComment ?? '')), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
const isApproved = isReportApproved({report: iouReport});
const isIOUSettled = isSettled(iouReport?.reportID);
const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial;
const isApproved = isReportApproved({report});
const isIOUSettled = isSettled(report?.reportID);
const isSettlementOrApprovalPartial = !!report?.pendingFields?.partial;
const isTransactionScanning = isScanning(transaction);
const displayAmount = isDeleted ? displayDeleteAmountText : displayAmountText;
const receiptImages = [{...getThumbnailAndImageURIs(transaction), transaction}];
Expand All @@ -142,23 +140,34 @@ function TransactionPreviewContent({
sortedParticipantAvatars.push(getWorkspaceIcon(chatReport));
}

// Compute the from/to data only for IOU reports
const {from, to} = useMemo(() => {
if (!shouldShowPayerAndReceiver) {
return {
from: undefined,
to: undefined,
};
}

// For IOU or Split, we want the unprocessed amount because it is important whether the amount was positive or negative
const payerAndReceiver = getIOUPayerAndReceiver(managerID, ownerAccountID, personalDetails, transactionRawAmount);

return {
from: payerAndReceiver.from,
to: payerAndReceiver.to,
};
}, [managerID, ownerAccountID, personalDetails, shouldShowPayerAndReceiver, transactionRawAmount]);

const shouldShowIOUHeader = !!from && !!to;

// If available, retrieve the split share from the splits object of the transaction, if not, display an even share.
const splitShare = useMemo(
() =>
shouldShowSplitShare
? (transaction?.comment?.splits?.find((split) => split.accountID === sessionAccountID)?.amount ??
calculateAmount(isReportAPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount ?? 0, requestCurrency ?? '', action?.actorAccountID === sessionAccountID))
calculateAmount(isReportAPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, amount ?? 0, requestCurrency ?? '', action?.actorAccountID === sessionAccountID))
: 0,
[
shouldShowSplitShare,
isReportAPolicyExpenseChat,
action?.actorAccountID,
participantAccountIDs.length,
transaction?.comment?.splits,
requestAmount,
requestCurrency,
sessionAccountID,
],
[shouldShowSplitShare, isReportAPolicyExpenseChat, action?.actorAccountID, participantAccountIDs.length, transaction?.comment?.splits, amount, requestCurrency, sessionAccountID],
);

const shouldWrapDisplayAmount = !(isBillSplit || shouldShowMerchantOrDescription || isTransactionScanning);
Expand Down
18 changes: 12 additions & 6 deletions src/components/ReportActionItem/TransactionPreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ function TransactionPreview(props: TransactionPreviewProps) {
contextAction,
} = props;

const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, {canBeMissing: true});
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, {canBeMissing: true});
const route = useRoute<PlatformStackRouteProp<TransactionDuplicateNavigatorParamList, typeof SCREENS.TRANSACTION_DUPLICATE.REVIEW>>();
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`, {canBeMissing: true});
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`, {canBeMissing: true});
const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action);
const transactionID = transactionIDFromProps ?? (isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : null);
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {canBeMissing: true});
Expand Down Expand Up @@ -75,8 +75,8 @@ function TransactionPreview(props: TransactionPreviewProps) {
}, [chatReportID]);

const navigateToReviewFields = useCallback(() => {
Navigation.navigate(getReviewNavigationRoute(route, report, transaction, duplicates));
}, [duplicates, report, route, transaction]);
Navigation.navigate(getReviewNavigationRoute(route, transactionThreadReport, transaction, duplicates));
}, [duplicates, transactionThreadReport, route, transaction]);

let transactionPreview = transaction;

Expand All @@ -86,6 +86,10 @@ function TransactionPreview(props: TransactionPreviewProps) {
transactionPreview = originalTransaction;
}

// See description of `transactionRawAmount` prop for more context
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const transactionRawAmount = (transaction?.modifiedAmount || transaction?.amount) ?? 0;

const iouAction = isBillSplit && originalTransaction ? (getIOUActionForReportID(chatReportID, originalTransaction.transactionID) ?? action) : action;

const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction);
Expand All @@ -112,7 +116,8 @@ function TransactionPreview(props: TransactionPreviewProps) {
chatReport={chatReport}
personalDetails={personalDetails}
transaction={transactionPreview}
iouReport={iouReport}
transactionRawAmount={transactionRawAmount}
report={report}
violations={violations}
offlineWithFeedbackOnClose={offlineWithFeedbackOnClose}
navigateToReviewFields={navigateToReviewFields}
Expand All @@ -135,7 +140,8 @@ function TransactionPreview(props: TransactionPreviewProps) {
chatReport={chatReport}
personalDetails={personalDetails}
transaction={originalTransaction}
iouReport={iouReport}
transactionRawAmount={transactionRawAmount}
report={report}
violations={violations}
offlineWithFeedbackOnClose={offlineWithFeedbackOnClose}
navigateToReviewFields={navigateToReviewFields}
Expand Down
14 changes: 9 additions & 5 deletions src/components/ReportActionItem/TransactionPreview/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type TransactionPreviewStyleType = {
};

type TransactionPreviewProps = {
/** The active IOUReport, used for Onyx subscription */
/** The active reportID linked to the transaction */
iouReportID: string | undefined;

/** The associated chatReport */
Expand Down Expand Up @@ -65,7 +65,7 @@ type TransactionPreviewProps = {
reportPreviewAction?: ReportAction;

/** Whether to show payer/receiver data in the preview */
shouldShowIOUData?: boolean;
shouldShowPayerAndReceiver?: boolean;

/** In case we want to override context menu action */
contextAction?: OnyxEntry<ReportAction>;
Expand Down Expand Up @@ -93,15 +93,19 @@ type TransactionPreviewContentProps = {
/** Records any errors related to wallet terms. */
walletTermsErrors: Errors | undefined;

/** Represents the IOU report entry from Onyx */
iouReport: OnyxEntry<Report>;
/** Represents the report linked to the transaction */
report: OnyxEntry<Report>;

@Kicu Kicu Jun 16, 2025

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.

Why I renamed this prop?

The name iouReport suggests that the report that gets passed is of type IOU. That is not true for the previews and was a source of confusion for me.
Both MoneyRequestReportPreview, and it's children TransactionPreview's can be rendered for both IOU and EXPENSE report.

So in reality this variable can be either expenseReport or iouReport. To make it simpler I just used the name report.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like this change.


/** Flag to determine if a transaction involves a bill split among multiple parties. */
isBillSplit: boolean;

/** Holds the transaction data entry from Onyx */
transaction: OnyxEntry<Transaction>;

/** The original amount value on the transaction. This is used to deduce who is the sender and who is the receiver of the money request

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I do not like the word original in this context as technically we have the modified amount, that is the changed/edited amount and amount is the original initially set amount so I think this could be slightly confusing to some

Suggested change
/** The original amount value on the transaction. This is used to deduce who is the sender and who is the receiver of the money request
/** The amount of the transaction saved in the database. This is used to deduce who is the sender and who is the receiver of the money request

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.

Sounds good. We a few different versions of the amount so the nomenclature is tricky.
For split, if I split 10usd with you 5/5, then there exists both a transaction for (+|-)500 and the transaction for full 1000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FYI corrected here: 389f7f9

* In case of Splits the property `transaction` is actually an original transaction (for the whole split) and it does not have the data required to deduce who is the sender */
transactionRawAmount: number;

/** Represents the action entry from Onyx */
action: OnyxEntry<ReportAction>;

Expand Down Expand Up @@ -130,7 +134,7 @@ type TransactionPreviewContentProps = {
reportPreviewAction?: ReportAction;

/** Whether to show payer/receiver data in the preview */
shouldShowIOUData?: boolean;
shouldShowPayerAndReceiver?: boolean;

/** Is this component used during duplicate review flow */
isReviewDuplicateTransactionPage?: boolean;
Expand Down
25 changes: 17 additions & 8 deletions src/libs/TransactionPreviewUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ const emptyPersonalDetails: OnyxTypes.PersonalDetails = {
login: undefined,
};

function getIOUData(managerID: number, ownerAccountID: number, isIOUReport: boolean, personalDetails: OnyxTypes.PersonalDetailsList | undefined, amount: number) {
/**
* Returns the data for displaying payer and receiver (`from` and `to`) values for given ids and amount.
* In IOU transactions we can deduce who is the payer and receiver based on sign (positive/negative) of the amount.
*/
function getIOUPayerAndReceiver(managerID: number, ownerAccountID: number, personalDetails: OnyxTypes.PersonalDetailsList | undefined, amount: number) {
let fromID = ownerAccountID;
let toID = managerID;

Expand All @@ -51,12 +55,10 @@ function getIOUData(managerID: number, ownerAccountID: number, isIOUReport: bool
toID = ownerAccountID;
}

return fromID && toID && isIOUReport
? {
from: personalDetails ? personalDetails[fromID] : emptyPersonalDetails,
to: personalDetails ? personalDetails[toID] : emptyPersonalDetails,
}
: undefined;
return {
from: personalDetails ? personalDetails[fromID] : emptyPersonalDetails,
to: personalDetails ? personalDetails[toID] : emptyPersonalDetails,
};
}

const getReviewNavigationRoute = (
Expand Down Expand Up @@ -348,5 +350,12 @@ function createTransactionPreviewConditionals({
};
}

export {getReviewNavigationRoute, getIOUData, getTransactionPreviewTextAndTranslationPaths, createTransactionPreviewConditionals, getViolationTranslatePath, getUniqueActionErrors};
export {
getReviewNavigationRoute,
getIOUPayerAndReceiver,
getTransactionPreviewTextAndTranslationPaths,
createTransactionPreviewConditionals,
getViolationTranslatePath,
getUniqueActionErrors,
};
export type {TranslationPathOrText};
40 changes: 0 additions & 40 deletions src/libs/__mocks__/TransactionPreviewUtils.ts

This file was deleted.

3 changes: 2 additions & 1 deletion src/stories/MoneyRequestReportPreview.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ const mockRenderItem: ListRenderItem<Transaction> = ({item}) => (
isHovered={false}
chatReport={chatReportR14932}
personalDetails={personalDetails}
iouReport={iouReportR14932}
report={iouReportR14932}
transaction={item}
transactionRawAmount={item.amount}
violations={item.errors ? violationsR14932 : []}
offlineWithFeedbackOnClose={() => undefined}
navigateToReviewFields={() => undefined}
Expand Down
6 changes: 3 additions & 3 deletions src/stories/TransactionPreviewContent.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const story: Meta<typeof TransactionPreviewContent> = {
isHovered: false,
chatReport: chatReportR14932,
personalDetails,
iouReport: iouReportR14932,
report: iouReportR14932,
transaction: transactionR14932,
violations: [],
offlineWithFeedbackOnClose(): void {},
Expand All @@ -119,7 +119,7 @@ const story: Meta<typeof TransactionPreviewContent> = {
},
argTypes: {
...disabledProperties,
iouReport: generateArgTypes(iouReportMap),
report: generateArgTypes(iouReportMap),
transaction: generateArgTypes(transactionsMap),
violations: generateArgTypes(violationsMap),
action: generateArgTypes(actionMap),
Expand Down Expand Up @@ -180,7 +180,7 @@ KeepButtonSplitRBRCategoriesAndTag.args = {

KeepButtonIOURbrCategoriesAndTag.args = {
...KeepButtonRBRCategoriesAndTag.args,
iouReport: iouReportWithModifiedType(CONST.REPORT.TYPE.IOU),
report: iouReportWithModifiedType(CONST.REPORT.TYPE.IOU),
};

DeletedKeepButtonSplitRBRCategoriesAndTag.args = {
Expand Down
Loading