Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
cc2fa63
Create TransactionPreview component & story for it
JakubKorytko Feb 27, 2025
4820723
Add two variants to storybook & correct date
JakubKorytko Feb 28, 2025
68ab7cb
Create TransactionPreview structure in a new dir
JakubKorytko Mar 5, 2025
596b40c
Remove redundant props from TransactionPreview
JakubKorytko Mar 4, 2025
b68506d
Improve TransactionPreview storybook
JakubKorytko Mar 4, 2025
e59e825
Add every visual version of TransactionPreview
JakubKorytko Mar 4, 2025
801d44c
Add dynamic assets and Figma values to story
JakubKorytko Mar 4, 2025
9c8fcba
Generate RBRmessage in TransactionPreviewContent
JakubKorytko Mar 5, 2025
ca764ed
Correct component preview header text & story
JakubKorytko Mar 5, 2025
6c7a2a3
Use existing styles for Avatars
JakubKorytko Mar 5, 2025
398280b
Add IOU indicator to TransactionPreview
JakubKorytko Mar 7, 2025
5230e5c
Change the structure of TransactionPreview
JakubKorytko Mar 7, 2025
177c905
Remove redundant hooks
JakubKorytko Mar 7, 2025
96bc336
Correct styling
JakubKorytko Mar 7, 2025
8abb843
Apply fixes mentioned in Design Doc
JakubKorytko Mar 7, 2025
92a6498
Remove named imports
JakubKorytko Mar 10, 2025
6f434c4
Add TransactionPreviewUtils tests
JakubKorytko Mar 10, 2025
ee0cff0
Redesign the structure and story
JakubKorytko Mar 12, 2025
b505da1
Apply sumo-slonik & Kicu review comments
JakubKorytko Mar 12, 2025
4719c05
Minor visual fixes
JakubKorytko Mar 12, 2025
c9761e3
Fix TransactionPreview styles & add hold translate path
JakubKorytko Mar 13, 2025
0d08b2a
Fix Transaction test & IOU header font size
JakubKorytko Mar 13, 2025
830ed3c
Add NoMerchant variant to Storybook
JakubKorytko Mar 13, 2025
bd3063f
Fix UI according to Figma
JakubKorytko Mar 13, 2025
96eba91
Add fixes according to mountiny comments
JakubKorytko Mar 17, 2025
a71f8c1
Add more tests for TransactionPreviewUtils
JakubKorytko Mar 17, 2025
24dc63e
Cover shouldShowSplitShare & showCashOrCard in tests
JakubKorytko Mar 17, 2025
1784f8c
Merge branch 'main' into transaction-preview
JakubKorytko Mar 17, 2025
697bb11
Fix TransactionPreviewUtils tests after merge
JakubKorytko Mar 17, 2025
b8ef110
Merge branch 'main' into transaction-preview
JakubKorytko Mar 17, 2025
51430f9
Fix Jest workflow
JakubKorytko Mar 17, 2025
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: 1 addition & 0 deletions .storybook/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const webpackConfig = ({config}: {config: Configuration}) => {
'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'),
...custom.resolve.alias,
};

Expand Down
4 changes: 4 additions & 0 deletions src/components/ReportActionItem/MoneyRequestAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ function MoneyRequestAction({
}
return <RenderHTML html={`<deleted-action ${CONST.REVERSED_TRANSACTION_ATTRIBUTE}="${isReversedTransaction}">${translate(message)}</deleted-action>`} />;
}

// NOTE: this part of code is needed here if we want to replace MoneyRequestPreview with TransactionPreview
// const renderCondition = lodashIsEmpty(iouReport) && !(isSplitBillAction || isTrackExpenseAction);
// return renderCondition ? null : (
Comment on lines +120 to +122

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.

Since we have the tableReportView beta, could we just use that now and render the new component if user is on that beta?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Maybe to keep the PR cleaner, let's not modify the app code in this PR so that it only adds to storybook. We can use them in the code later, when other PR's (like the carousel) are ready

return (
<MoneyRequestPreview
iouReportID={requestReportID}
Expand Down

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions src/components/ReportActionItem/TransactionPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {useRoute} from '@react-navigation/native';
import React, {useCallback, useMemo} from 'react';
import type {GestureResponderEvent} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import useTransactionViolations from '@hooks/useTransactionViolations';
import {getOriginalMessage, isMoneyRequestAction as isMoneyRequestActionReportActionsUtils} from '@libs/ReportActionsUtils';
import {getReviewNavigationRoute} from '@libs/TransactionPreviewUtils';
import {removeSettledAndApprovedTransactions} from '@libs/TransactionUtils';
import Navigation from '@navigation/Navigation';
import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types';
import type {TransactionDuplicateNavigatorParamList} from '@navigation/types';
import {clearWalletTermsError} from '@userActions/PaymentMethods';
import {clearIOUError} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import TransactionPreviewContent from './TransactionPreviewContent';
import type {TransactionPreviewProps} from './types';

function TransactionPreview(props: TransactionPreviewProps) {
const {action, chatReportID, reportID, contextMenuAnchor, checkIfContextMenuActive = () => {}, shouldDisplayContextMenu, iouReportID} = props;

const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`);
const route = useRoute<PlatformStackRouteProp<TransactionDuplicateNavigatorParamList, typeof SCREENS.TRANSACTION_DUPLICATE.REVIEW>>();
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`);
const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action);
const transactionID = isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : null;
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
const violations = useTransactionViolations(transaction?.transactionID);
const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);

// Get transaction violations for given transaction id from onyx, find duplicated transactions violations and get duplicates
const allDuplicates = useMemo(() => violations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], [violations]);
const duplicates = useMemo(() => removeSettledAndApprovedTransactions(allDuplicates), [allDuplicates]);
const sessionAccountID = session?.accountID;
const areThereDuplicates = allDuplicates.length > 0 && duplicates.length > 0 && allDuplicates.length === duplicates.length;

const showContextMenu = (event: GestureResponderEvent) => {
if (!shouldDisplayContextMenu) {
return;
}
showContextMenuForReport(event, contextMenuAnchor, reportID, action, checkIfContextMenuActive);
};

const offlineWithFeedbackOnClose = useCallback(() => {
clearWalletTermsError();
clearIOUError(chatReportID);
}, [chatReportID]);

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

return (
<TransactionPreviewContent
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
chatReport={chatReport}
personalDetails={personalDetails}
iouReport={iouReport}
transaction={transaction}
violations={violations}
showContextMenu={showContextMenu}
offlineWithFeedbackOnClose={offlineWithFeedbackOnClose}
navigateToReviewFields={navigateToReviewFields}
areThereDuplicates={areThereDuplicates}
sessionAccountID={sessionAccountID}
walletTermsErrors={walletTerms?.errors}
routeName={route.name}
/>
);
}

TransactionPreview.displayName = 'TransactionPreview';

export default TransactionPreview;
112 changes: 112 additions & 0 deletions src/components/ReportActionItem/TransactionPreview/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import type {PersonalDetailsList, Report, ReportAction, Transaction, TransactionViolations} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';

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

/** The associated chatReport */
chatReportID: string | undefined;

/** The ID of the current report */
reportID: string | undefined;

/** Callback for the preview pressed */
onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent) => void;

/** All the data of the action, used for showing context menu */
action: ReportAction;

/** Popover context menu anchor, used for showing context menu */
contextMenuAnchor?: ContextMenuAnchor;

/** Callback for updating context menu active state, used for showing context menu */
checkIfContextMenuActive?: () => void;

/** Extra styles to pass to View wrapper */
containerStyles?: StyleProp<ViewStyle>;

/** True if this IOU has a type of split */
isBillSplit: boolean;

/** Whether this IOU is a track expense */
isTrackExpense: boolean;

/** True if the IOU Preview card is hovered */
isHovered?: boolean;

/** Whether or not an IOU report contains expenses in a different currency
* that are either created or cancelled offline, and thus haven't been converted to the report's currency yet
*/
shouldShowPendingConversionMessage?: boolean;

/** Whether a message is a whisper */
isWhisper?: boolean;

/** Whether context menu should be shown on press */
shouldDisplayContextMenu?: boolean;
};

type TransactionPreviewContentProps = {
/** Function to display the context menu in response to an event. */
showContextMenu: (event: GestureResponderEvent) => void;

/** Handles the UI response and data clean-up when the transaction goes offline. */
offlineWithFeedbackOnClose: () => void;

/** Navigates the user to a separate view or component for reviewing or editing transaction fields. */
navigateToReviewFields: () => void;

/** General callback for handling presses on the preview component, can also handle keyboard events. */
onPreviewPressed: (event?: GestureResponderEvent | KeyboardEvent | undefined) => void;

/** Whether the transaction is whisper. */
isWhisper?: boolean;

/** Determines if the element is currently hovered over. */
isHovered?: boolean;

/** Optional custom styles to be applied to container components. */
containerStyles?: StyleProp<ViewStyle>;

/** Records any errors related to wallet terms. */
walletTermsErrors: Errors | undefined;

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

/** 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>;

/** Represents the action entry from Onyx */
action: ReportAction;

/** Contains data about potential transaction violations */
violations: TransactionViolations;

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

/** Optional details about people involved in the transaction */
personalDetails?: PersonalDetailsList;

/** Indicates whether the transaction consists of duplicates */
areThereDuplicates: boolean;

/** Session account ID */
sessionAccountID?: number;

/** Name of the route where the transaction preview is being displayed */
routeName: string;

/** Determine whether to hide the component's children if deletion is pending */
shouldHideOnDelete?: boolean;
};

export type {TransactionPreviewProps, TransactionPreviewContentProps};
29 changes: 8 additions & 21 deletions src/components/SelectionList/Search/ExpenseItemHeaderNarrow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {isCorrectSearchUserName} from '@libs/SearchUIUtils';
import variables from '@styles/variables';
import type {SearchPersonalDetails, SearchTransactionAction} from '@src/types/onyx/SearchResults';
import ActionCell from './ActionCell';
import UserInfoCell from './UserInfoCell';
import UserInfoCellsWithArrow from './UserInfoCellsWithArrow';

type ExpenseItemHeaderNarrowProps = {
text?: string;
Expand Down Expand Up @@ -77,26 +77,13 @@ function ExpenseItemHeaderNarrow({
</View>
</PressableWithFeedback>
)}
<View style={[styles.mw50]}>
<UserInfoCell
participant={participantFrom}
displayName={participantFromDisplayName}
/>
</View>
{!!shouldDisplayArrowIcon && (
<Icon
src={Expensicons.ArrowRightLong}
width={variables.iconSizeXXSmall}
height={variables.iconSizeXXSmall}
fill={theme.icon}
/>
)}
<View style={[styles.flex1, styles.mw50]}>
<UserInfoCell
participant={participantTo}
displayName={participantToDisplayName}
/>
</View>
<UserInfoCellsWithArrow
shouldDisplayArrowIcon={!!shouldDisplayArrowIcon}
participantFrom={participantFrom}
participantFromDisplayName={participantFromDisplayName}
participantTo={participantTo}
participantToDisplayName={participantToDisplayName}
/>
</View>
<View style={[StyleUtils.getWidthStyle(variables.w80)]}>
<ActionCell
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,13 +416,15 @@ function TransactionListItemRow({
</View>
<View style={[StyleUtils.getSearchTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FROM)]}>
<UserInfoCell
participant={item.from}
accountID={item.from.accountID}
avatar={item.from.avatar}
displayName={item.formattedFrom}
/>
</View>
<View style={[StyleUtils.getSearchTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.FROM)]}>
<UserInfoCell
participant={item.to}
accountID={item.to.accountID}

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.

item.to can be undefined when sending an invoice to new user. It's better if we keep the previous optional chaining here
More details: #61310 (comment)

avatar={item.to.avatar}
displayName={item.formattedTo}
/>
</View>
Expand Down
23 changes: 14 additions & 9 deletions src/components/SelectionList/Search/UserInfoCell.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
import React from 'react';
import type {TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import Avatar from '@components/Avatar';
import Text from '@components/Text';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {isCorrectSearchUserName} from '@libs/SearchUIUtils';
import type {AvatarSource} from '@libs/UserUtils';
import type {AvatarSizeName} from '@styles/utils';
import CONST from '@src/CONST';
import type {SearchPersonalDetails} from '@src/types/onyx/SearchResults';

type UserInfoCellProps = {
participant: SearchPersonalDetails;
accountID: number;
avatar: AvatarSource | undefined;
displayName: string;
avatarSize?: AvatarSizeName;
textStyle?: TextStyle;
avatarStyle?: ViewStyle;
};

function UserInfoCell({participant, displayName}: UserInfoCellProps) {
function UserInfoCell({avatar, accountID, displayName, avatarSize, textStyle, avatarStyle}: UserInfoCellProps) {
const styles = useThemeStyles();
const {isLargeScreenWidth} = useResponsiveLayout();
const avatarURL = participant?.avatar;

if (!isCorrectSearchUserName(displayName)) {
return null;
Expand All @@ -26,16 +31,16 @@ function UserInfoCell({participant, displayName}: UserInfoCellProps) {
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<Avatar
imageStyles={[styles.alignSelfCenter]}
size={CONST.AVATAR_SIZE.MID_SUBSCRIPT}
source={avatarURL}
size={avatarSize ?? CONST.AVATAR_SIZE.MID_SUBSCRIPT}
source={avatar}
name={displayName}
type={CONST.ICON_TYPE_AVATAR}
avatarID={participant?.accountID}
containerStyles={[styles.pr2]}
avatarID={accountID}
containerStyles={[styles.pr2, avatarStyle]}
/>
<Text
numberOfLines={1}
style={[isLargeScreenWidth ? styles.themeTextColor : styles.textMicroBold, styles.flexShrink1]}
style={[isLargeScreenWidth ? styles.themeTextColor : styles.textMicroBold, styles.flexShrink1, textStyle]}
>
{displayName}
</Text>
Expand Down
Loading