From cc2fa637aaa3415aa02e179c9289d1ea79b670a5 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Thu, 27 Feb 2025 15:24:25 +0100 Subject: [PATCH 01/29] Create TransactionPreview component & story for it --- .../TransactionPreview.tsx | 395 ++++++++++++++++++ .../TransactionPreviewUI.tsx | 290 +++++++++++++ src/stories/TransactionPreview.stories.tsx | 144 +++++++ src/types/TransactionPreviewUI.type.ts | 50 +++ 4 files changed, 879 insertions(+) create mode 100644 src/components/ReportActionItem/MoneyRequestPreview/TransactionPreview.tsx create mode 100644 src/components/ReportActionItem/MoneyRequestPreview/TransactionPreviewUI.tsx create mode 100644 src/stories/TransactionPreview.stories.tsx create mode 100644 src/types/TransactionPreviewUI.type.ts diff --git a/src/components/ReportActionItem/MoneyRequestPreview/TransactionPreview.tsx b/src/components/ReportActionItem/MoneyRequestPreview/TransactionPreview.tsx new file mode 100644 index 000000000000..f642bcf3ba88 --- /dev/null +++ b/src/components/ReportActionItem/MoneyRequestPreview/TransactionPreview.tsx @@ -0,0 +1,395 @@ +import {useRoute} from '@react-navigation/native'; +import lodashSortBy from 'lodash/sortBy'; +import truncate from 'lodash/truncate'; +import React, { useCallback, useMemo } from "react"; +import type {GestureResponderEvent} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import {showContextMenuForReport} from '@components/ShowContextMenuContext'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useTransactionViolations from '@hooks/useTransactionViolations'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; +import {calculateAmount} from '@libs/IOUUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; +import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils'; +import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; +import {getOriginalMessage, getReportAction, isMessageDeleted, isMoneyRequestAction as isMoneyRequestActionReportActionsUtils} from '@libs/ReportActionsUtils'; +import { + canEditMoneyRequest, + getTransactionDetails, + getWorkspaceIcon, + isPaidGroupPolicy, + isPaidGroupPolicyExpenseReport, + isPolicyExpenseChat as isPolicyExpenseChatReportUtils, + isReportApproved, + isSettled as isSettledReportUtils, +} from '@libs/ReportUtils'; +import type {TransactionDetails} from '@libs/ReportUtils'; +import StringUtils from '@libs/StringUtils'; +import { + compareDuplicateTransactionFields, getFormattedCreated, + hasMissingSmartscanFields, + hasNoticeTypeViolation as hasNoticeTypeViolationTransactionUtils, + hasPendingRTERViolation, + hasReceipt as hasReceiptTransactionUtils, + hasViolation as hasViolationTransactionUtils, + hasWarningTypeViolation as hasWarningTypeViolationTransactionUtils, + isAmountMissing as isAmountMissingTransactionUtils, + isCardTransaction as isCardTransactionTransactionUtils, isCreatedMissing, + isDistanceRequest as isDistanceRequestTransactionUtils, + isFetchingWaypointsFromServer as isFetchingWaypointsFromServerTransactionUtils, + isMerchantMissing as isMerchantMissingTransactionUtils, + isOnHold as isOnHoldTransactionUtils, + isPending, + isPerDiemRequest as isPerDiemRequestTransactionUtils, + isReceiptBeingScanned, + removeSettledAndApprovedTransactions, + shouldShowBrokenConnectionViolation +} from "@libs/TransactionUtils"; +import ViolationsUtils from '@libs/Violations/ViolationsUtils'; +import {clearWalletTermsError} from '@userActions/PaymentMethods'; +import {clearIOUError} from '@userActions/Report'; +import {abandonReviewDuplicateTransactions, setReviewDuplicatesKey} from '@userActions/Transaction'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; +import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import { Hourglass } from "@components/Icon/Expensicons"; +import TransactionPreviewUI from "./TransactionPreviewUI"; +import type {MoneyRequestPreviewProps, PendingMessageProps} from './types'; + +function TransactionPreview({ + isBillSplit, + action, + contextMenuAnchor, + chatReportID, + reportID, + onPreviewPressed, + containerStyles, + checkIfContextMenuActive = () => {}, + shouldShowPendingConversionMessage = false, + isHovered = false, + isWhisper = false, + shouldDisplayContextMenu = true, + iouReportID, + }: MoneyRequestPreviewProps) { + const {translate} = useLocalize(); + const route = useRoute>(); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); + const [session] = useOnyx(ONYXKEYS.SESSION); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); + + const policy = usePolicy(iouReport?.policyID); + const isMoneyRequestAction = isMoneyRequestActionReportActionsUtils(action); + const transactionID = isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : undefined; + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); + const violations = useTransactionViolations(transaction?.transactionID); + + const sessionAccountID = session?.accountID; + const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID; + const ownerAccountID = iouReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; + const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(chatReport); + + const participantAccountIDs = isMoneyRequestActionReportActionsUtils(action) && isBillSplit ? getOriginalMessage(action)?.participantAccountIDs ?? [] : [managerID, ownerAccountID]; + const participantAvatars = getAvatarsForAccountIDs(participantAccountIDs, personalDetails ?? {}); + const sortedParticipantAvatars = lodashSortBy(participantAvatars, (avatar) => avatar.id); + if (isPolicyExpenseChat && isBillSplit) { + sortedParticipantAvatars.push(getWorkspaceIcon(chatReport)); + } + + // Pay button should only be visible to the manager of the report. + const isCurrentUserManager = managerID === sessionAccountID; + + const { + amount: requestAmount, + currency: requestCurrency, + comment: requestComment, + merchant, + tag, + category, + } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]); + + const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); + const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); + const hasReceipt = hasReceiptTransactionUtils(transaction); + const isScanning = hasReceipt && isReceiptBeingScanned(transaction); + const isOnHold = isOnHoldTransactionUtils(transaction); + const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; + const isPartialHold = isSettlementOrApprovalPartial && isOnHold; + const hasViolations = hasViolationTransactionUtils(transaction, violations, true); + const hasNoticeTypeViolations = hasNoticeTypeViolationTransactionUtils(transaction?.transactionID, violations, true) && isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = hasWarningTypeViolationTransactionUtils(transaction?.transactionID, violations, true); + + const hasFieldErrors = hasMissingSmartscanFields(transaction); + const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); + const isPerDiemRequest = isPerDiemRequestTransactionUtils(transaction); + const isFetchingWaypointsFromServer = isFetchingWaypointsFromServerTransactionUtils(transaction); + const isCardTransaction = isCardTransactionTransactionUtils(transaction); + const isSettled = isSettledReportUtils(iouReport?.reportID); + const isApproved = isReportApproved({report: iouReport}); + const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const isReviewDuplicateTransactionPage = route.name === SCREENS.TRANSACTION_DUPLICATE.REVIEW; + + const isFullySettled = isSettled && !isSettlementOrApprovalPartial; + const isFullyApproved = isApproved && !isSettlementOrApprovalPartial; + + // 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]); + + // Remove settled transactions from duplicates + const duplicates = useMemo(() => removeSettledAndApprovedTransactions(allDuplicates), [allDuplicates]); + + // When there are no settled transactions in duplicates, show the "Keep this one" button + const shouldShowKeepButton = !!(allDuplicates.length && duplicates.length && allDuplicates.length === duplicates.length); + + const shouldShowTag = !!tag && isPolicyExpenseChat; + const shouldShowCategory = !!category && isPolicyExpenseChat; + const shouldShowRBR = hasNoticeTypeViolations || hasWarningTypeViolations || hasViolations || hasFieldErrors || (!isFullySettled && !isFullyApproved && isOnHold); + const showCashOrCard = isCardTransaction ? translate('iou.card') : translate('iou.cash'); + // We don't use isOnHold because it's true for duplicated transaction too and we only want to show hold message if the transaction is truly on hold + const shouldShowHoldMessage = !(isSettled && !isSettlementOrApprovalPartial) && !!transaction?.comment?.hold; + + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`); + const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + const reviewingTransactionID = isMoneyRequestActionReportActionsUtils(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; + + /* + Show the merchant for IOUs and expenses only if: + - the merchant is not empty, is custom, or is not related to scanning smartscan; + - the expense is not a distance expense with a pending route and amount = 0 - in this case, + the merchant says: "Route pending...", which is already shown in the amount field; + */ + const shouldShowMerchant = + !!requestMerchant && + requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && + requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && + !(isFetchingWaypointsFromServer && !requestAmount); + const shouldShowDescription = !!description && !shouldShowMerchant && !isScanning; + + let merchantOrDescription = requestMerchant; + if (!shouldShowMerchant) { + merchantOrDescription = description || ''; + } + + const receiptImages = [{...getThumbnailAndImageURIs(transaction), transaction}]; + + const getSettledMessage = (): string => { + if (isCardTransaction) { + return translate('common.done'); + } + return translate('iou.settledExpensify'); + }; + + const showContextMenu = (event: GestureResponderEvent) => { + if (!shouldDisplayContextMenu) { + return; + } + showContextMenuForReport(event, contextMenuAnchor, reportID, action, checkIfContextMenuActive); + }; + + const getPreviewHeaderText = (): string => { + let message = ''; + + // todo: change this + if (!isCreatedMissing(transaction)) { + const transactionDate = getFormattedCreated(transaction); + message += new Date(transactionDate).toLocaleString('en-US', { month: 'short', day: 'numeric' }); + message += ` ${CONST.DOT_SEPARATOR} `; + } + + message += showCashOrCard; + + if (isDistanceRequest) { + message += translate('common.distance'); + } else if (isPerDiemRequest) { + message += translate('common.perDiem'); + } else if (isScanning) { + message += translate('common.receipt'); + } else if (isBillSplit) { + message += translate('iou.split'); + } + + if (isPending(transaction)) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.pending')}`; + } + + if (hasPendingRTERViolation(violations)) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.pendingMatch')}`; + } + + if (isSettled && !iouReport?.isCancelledIOU && !isPartialHold) { + message += ` ${CONST.DOT_SEPARATOR} ${getSettledMessage()}`; + return message; + } + + if (shouldShowRBR && transaction) { + if (shouldShowHoldMessage) { + return `${message} ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; + } + const firstViolation = violations?.at(0); + if (firstViolation) { + const canEdit = isMoneyRequestAction && canEditMoneyRequest(action, transaction); + const violationMessage = ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit); + const violationsCount = violations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; + const isTooLong = violationsCount > 1 || violationMessage.length > 15; + const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors; + + return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; + } + if (hasFieldErrors) { + const isMerchantMissing = isMerchantMissingTransactionUtils(transaction); + const isAmountMissing = isAmountMissingTransactionUtils(transaction); + if (isAmountMissing && isMerchantMissing) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.reviewRequired')}`; + } else if (isAmountMissing) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.missingAmount')}`; + } else if (isMerchantMissing) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.missingMerchant')}`; + } + return message; + } + } else if (hasNoticeTypeViolations && transaction && !isReportApproved({report: iouReport}) && !isSettledReportUtils(iouReport?.reportID)) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.reviewRequired')}`; + } else if (isPaidGroupPolicyExpenseReport(iouReport) && isReportApproved({report: iouReport}) && !isSettledReportUtils(iouReport?.reportID) && !isPartialHold) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`; + } else if (iouReport?.isCancelledIOU) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`; + } else if (shouldShowHoldMessage) { + message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; + } + return message; + }; + + const getPendingMessageProps: () => PendingMessageProps = () => { + if (shouldShowBrokenConnectionViolation(transaction, iouReport, policy, violations)) { + return {shouldShow: true, messageIcon: Hourglass, messageDescription: translate('violations.brokenConnection530Error')}; + } + return {shouldShow: false}; + }; + + const pendingMessageProps = getPendingMessageProps(); + + const getDisplayAmountText = (): string => { + if (isScanning) { + return translate('iou.receiptStatusTitle'); + } + + if (isFetchingWaypointsFromServer && !requestAmount) { + return translate('iou.fieldPending'); + } + + return convertToDisplayString(requestAmount, requestCurrency); + }; + + const getDisplayDeleteAmountText = (): string => { + const iouOriginalMessage: OnyxEntry = isMoneyRequestActionReportActionsUtils(action) ? getOriginalMessage(action) ?? undefined : undefined; + return convertToDisplayString(iouOriginalMessage?.amount, iouOriginalMessage?.currency); + }; + + const displayAmount = isDeleted ? getDisplayDeleteAmountText() : getDisplayAmountText(); + + const shouldShowSplitShare = isBillSplit && !!requestAmount && requestAmount > 0; + + // 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(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency ?? '', action.actorAccountID === sessionAccountID) + : 0, + [shouldShowSplitShare, isPolicyExpenseChat, action.actorAccountID, participantAccountIDs.length, transaction?.comment?.splits, requestAmount, requestCurrency, sessionAccountID], + ); + + const navigateToReviewFields = () => { + const backTo = route.params.backTo; + + // Clear the draft before selecting a different expense to prevent merging fields from the previous expense + // (e.g., category, tag, tax) that may be not enabled/available in the new expense's policy. + abandonReviewDuplicateTransactions(); + const comparisonResult = compareDuplicateTransactionFields(reviewingTransactionID, transaction?.reportID, transaction?.transactionID ?? reviewingTransactionID); + setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID, reportID: transaction?.reportID}); + + if ('merchant' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else if ('category' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_CATEGORY_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else if ('tag' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAG_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else if ('description' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_DESCRIPTION_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else if ('taxCode' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_TAX_CODE_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else if ('billable' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_BILLABLE_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else if ('reimbursable' in comparisonResult.change) { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_REIMBURSABLE_PAGE.getRoute(route.params?.threadReportID, backTo)); + } else { + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_CONFIRMATION_PAGE.getRoute(route.params?.threadReportID, backTo)); + } + }; + + const shouldDisableOnPress = isBillSplit && isEmptyObject(transaction); + const shouldShowSkeleton = isEmptyObject(transaction) && !isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + const previewHeaderText = getPreviewHeaderText(); + const offlineWithFeedbackOnClose = useCallback(() => { + clearWalletTermsError(); + clearIOUError(chatReportID); + }, [chatReportID]) + + return ( + + ) + +} + +TransactionPreview.displayName = 'TransactionPreview'; + +export default TransactionPreview; diff --git a/src/components/ReportActionItem/MoneyRequestPreview/TransactionPreviewUI.tsx b/src/components/ReportActionItem/MoneyRequestPreview/TransactionPreviewUI.tsx new file mode 100644 index 000000000000..8b178d86a5c9 --- /dev/null +++ b/src/components/ReportActionItem/MoneyRequestPreview/TransactionPreviewUI.tsx @@ -0,0 +1,290 @@ +import React, { useMemo } from "react"; +import { View } from "react-native"; +import Button from '@components/Button'; +import Icon from '@components/Icon'; +import { Checkmark, DotIndicator, Folder, Tag } from '@components/Icon/Expensicons'; +import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; +import MultipleAvatars from '@components/MultipleAvatars'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages'; +import Text from '@components/Text'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import ControlSelection from '@libs/ControlSelection'; +import { convertToDisplayString } from '@libs/CurrencyUtils'; +import { canUseTouchScreen } from '@libs/DeviceCapabilities'; +import { getCleanedTagName } from '@libs/PolicyUtils'; +import variables from '@styles/variables'; +import type TransactionPreviewUIProps from '@src/types/TransactionPreviewUI.type'; + +function TransactionPreviewUI({ + isDeleted, + isScanning, + isWhisper, + isHovered, + isSettled, + isPartialHold, + isBillSplit, + isApproved, + isCurrentUserManager, + isSettlementOrApprovalPartial, + isReviewDuplicateTransactionPage, + shouldShowSkeleton, + shouldShowRBR, + shouldDisableOnPress, + shouldShowKeepButton, + shouldShowDescription, + shouldShowMerchant, + shouldShowCategory, + shouldShowTag, + shouldShowPendingConversionMessage, + displayAmount, + category, + showCashOrCard, + tag, + requestCurrency, + merchantOrDescription, + previewHeaderText, + requestAmount, + splitShare, + receiptImages, + sortedParticipantAvatars, + pendingMessageProps, + containerStyles, + walletTermsErrors, + pendingAction, + showContextMenu, + offlineWithFeedbackOnClose, + translate, + navigateToReviewFields, + onPreviewPressed +}: TransactionPreviewUIProps) { + const theme = useTheme(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {windowWidth} = useWindowDimensions(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const themeStyles = useMemo(() => [{ + backgroundColor: theme.cardBG + }], [theme.cardBG]); + + const shouldShowCategoryOrTag = shouldShowCategory || shouldShowTag; + + const childContainer = ( + + offlineWithFeedbackOnClose} + errorRowStyles={[styles.mbn1]} + needsOffscreenAlphaCompositing + pendingAction={pendingAction} + shouldDisableStrikeThrough={isDeleted} + shouldDisableOpacity={isDeleted} + > + + {!isDeleted && ( + + )} + {shouldShowSkeleton ? ( + + ) : ( + + + + + {previewHeaderText} + {!isSettled && shouldShowRBR && ( + + )} + + + + + {(shouldShowMerchant || shouldShowDescription) && ( + + {merchantOrDescription} + + )} + + {displayAmount} + + {isSettled && !isPartialHold && !isBillSplit && ( + + + + )} + + {isBillSplit && ( + + + + )} + + + + {!isCurrentUserManager && shouldShowPendingConversionMessage && ( + {translate('iou.pendingConversionMessage')} + )} + + {!!splitShare && ( + + {translate('iou.yourSplit', {amount: convertToDisplayString(splitShare, requestCurrency)})} + + )} + + {pendingMessageProps.shouldShow && ( + + + {pendingMessageProps.messageDescription} + + )} + + {shouldShowCategoryOrTag && ( + + {shouldShowCategory && ( + + + + {category} + + + )} + {shouldShowTag && !!tag && ( + + + + {getCleanedTagName(tag)} + + + )} + + )} + + + + )} + + + + ); + + if (!onPreviewPressed) { + return childContainer; + } + + return ( + canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={showContextMenu} + shouldUseHapticsOnLongPress + accessibilityLabel={isBillSplit ? translate('iou.split') : showCashOrCard} + accessibilityHint={convertToDisplayString(requestAmount, requestCurrency)} + style={[ + styles.moneyRequestPreviewBox, + containerStyles, + themeStyles, + shouldDisableOnPress && styles.cursorDefault, + (isSettled || isApproved) && isSettlementOrApprovalPartial && styles.offlineFeedback.pending + ]} + > + {childContainer} + {isReviewDuplicateTransactionPage && !isSettled && !isApproved && shouldShowKeepButton && ( +