Skip to content

Commit

Permalink
Merge pull request #33969 from infinitered/32571-money-request-preview
Browse files Browse the repository at this point in the history
  • Loading branch information
cead22 authored Feb 17, 2024
2 parents 6dab905 + e3699d3 commit d28f86f
Show file tree
Hide file tree
Showing 14 changed files with 203 additions and 144 deletions.
14 changes: 4 additions & 10 deletions src/components/DotIndicatorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {View} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {isReceiptError} from '@libs/ErrorUtils';
import fileDownload from '@libs/fileDownload';
import type {MaybePhraseKey} from '@libs/Localize';
import * as Localize from '@libs/Localize';
import CONST from '@src/CONST';
import type {ReceiptError} from '@src/types/onyx/Transaction';
Expand Down Expand Up @@ -34,14 +36,6 @@ type DotIndicatorMessageProps = {
textStyles?: StyleProp<TextStyle>;
};

/** Check if the error includes a receipt. */
function isReceiptError(message: Localize.MaybePhraseKey | ReceiptError): message is ReceiptError {
if (typeof message === 'string' || Array.isArray(message)) {
return false;
}
return (message?.error ?? '') === CONST.IOU.RECEIPT_ERROR;
}

function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndicatorMessageProps) {
const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -52,12 +46,12 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica
}

// Fetch the keys, sort them, and map through each key to get the corresponding message
const sortedMessages = Object.keys(messages)
const sortedMessages: Array<MaybePhraseKey | ReceiptError> = Object.keys(messages)
.sort()
.map((key) => messages[key]);

// Removing duplicates using Set and transforming the result into an array
const uniqueMessages = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message)));
const uniqueMessages: Array<ReceiptError | string> = [...new Set(sortedMessages)].map((message) => (isReceiptError(message) ? message : Localize.translateIfPhraseKey(message)));

const isErrorMessage = type === 'error';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@ import {truncate} from 'lodash';
import lodashSortBy from 'lodash/sortBy';
import React from 'react';
import {View} from 'react-native';
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {GestureResponderEvent} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView';
import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithoutFeedback';
import RenderHTML from '@components/RenderHTML';
import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -24,86 +23,22 @@ import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import * as IOUUtils from '@libs/IOUUtils';
import * as Localize from '@libs/Localize';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import * as PaymentMethods from '@userActions/PaymentMethods';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import * as Localize from '@src/libs/Localize';
import ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxTypes from '@src/types/onyx';
import type {IOUMessage} from '@src/types/onyx/OriginalMessage';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import ReportActionItemImages from './ReportActionItemImages';

type MoneyRequestPreviewOnyxProps = {
/** All of the personal details for everyone */
personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>;

/** Chat report associated with iouReport */
chatReport: OnyxEntry<OnyxTypes.Report>;

/** IOU report data object */
iouReport: OnyxEntry<OnyxTypes.Report>;

/** Session info for the currently logged in user. */
session: OnyxEntry<OnyxTypes.Session>;

/** The transaction attached to the action.message.iouTransactionID */
transaction: OnyxEntry<OnyxTypes.Transaction>;

/** Information about the user accepting the terms for payments */
walletTerms: OnyxEntry<OnyxTypes.WalletTerms>;
};

type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & {
/** The active IOUReport, used for Onyx subscription */
// The iouReportID is used inside withOnyx HOC
// eslint-disable-next-line react/no-unused-prop-types
iouReportID: string;

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

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

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

/** All the data of the action, used for showing context menu */
action: OnyxTypes.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 is this IOU is a split instead of a 1:1 request */
isBillSplit: boolean;

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

/** Whether or not an IOU report contains money requests 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;
};
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {MoneyRequestPreviewProps} from './types';

function MoneyRequestPreview({
function MoneyRequestPreviewContent({
iouReport,
isBillSplit,
session,
Expand All @@ -121,6 +56,7 @@ function MoneyRequestPreview({
shouldShowPendingConversionMessage = false,
isHovered = false,
isWhisper = false,
transactionViolations,
}: MoneyRequestPreviewProps) {
const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -129,10 +65,6 @@ function MoneyRequestPreview({
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
const parser = new ExpensiMark();

if (isEmptyObject(iouReport) && !isBillSplit) {
return null;
}

const sessionAccountID = session?.accountID;
const managerID = iouReport?.managerID ?? -1;
const ownerAccountID = iouReport?.ownerAccountID ?? -1;
Expand All @@ -153,7 +85,9 @@ function MoneyRequestPreview({
const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH});
const hasReceipt = TransactionUtils.hasReceipt(transaction);
const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction);
const hasViolations = TransactionUtils.hasViolation(transaction, transactionViolations);
const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction);
const shouldShowRBR = hasViolations || hasFieldErrors;
const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction);
const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction);
const isCardTransaction = TransactionUtils.isCardTransaction(transaction);
Expand Down Expand Up @@ -213,7 +147,14 @@ function MoneyRequestPreview({
}

let message = translate('iou.cash');
if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) {
if (hasViolations && transaction) {
const violations = TransactionUtils.getTransactionViolations(transaction, transactionViolations);
if (violations?.[0]) {
const violationMessage = ViolationsUtils.getViolationTranslation(violations[0], translate);
const isTooLong = violations.filter((v) => v.type === 'violation').length > 1 || violationMessage.length > 15;
message += ` • ${isTooLong ? translate('violations.reviewRequired') : violationMessage}`;
}
} else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport)) {
message += ` • ${translate('iou.approved')}`;
} else if (iouReport?.isWaitingOnBankAccount) {
message += ` • ${translate('iou.pending')}`;
Expand Down Expand Up @@ -280,7 +221,7 @@ function MoneyRequestPreview({
<Text style={[styles.textLabelSupporting, styles.flex1, styles.lh20, styles.mb1]}>
{getPreviewHeaderText() + (isSettled && !iouReport?.isCancelledIOU ? ` • ${getSettledMessage()}` : '')}
</Text>
{!isSettled && hasFieldErrors && (
{!isSettled && shouldShowRBR && (
<Icon
src={Expensicons.DotIndicator}
fill={theme.danger}
Expand Down Expand Up @@ -367,28 +308,6 @@ function MoneyRequestPreview({
);
}

MoneyRequestPreview.displayName = 'MoneyRequestPreview';
MoneyRequestPreviewContent.displayName = 'MoneyRequestPreview';

export default withOnyx<MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps>({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
chatReport: {
key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
},
iouReport: {
key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`,
},
session: {
key: ONYXKEYS.SESSION,
},
transaction: {
key: ({action}) => {
const originalMessage = action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? action.originalMessage : undefined;
return `${ONYXKEYS.COLLECTION.TRANSACTION}${originalMessage?.IOUTransactionID ?? 0}`;
},
},
walletTerms: {
key: ONYXKEYS.WALLET_TERMS,
},
})(MoneyRequestPreview);
export default MoneyRequestPreviewContent;
44 changes: 44 additions & 0 deletions src/components/ReportActionItem/MoneyRequestPreview/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import lodashIsEmpty from 'lodash/isEmpty';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import MoneyRequestPreviewContent from './MoneyRequestPreviewContent';
import type {MoneyRequestPreviewOnyxProps, MoneyRequestPreviewProps} from './types';

function MoneyRequestPreview(props: MoneyRequestPreviewProps) {
// We should not render the component if there is no iouReport and it's not a split.
// Moved outside of the component scope to allow for easier use of hooks in the main component.
// eslint-disable-next-line react/jsx-props-no-spreading
return lodashIsEmpty(props.iouReport) && !props.isBillSplit ? null : <MoneyRequestPreviewContent {...props} />;
}

MoneyRequestPreview.displayName = 'MoneyRequestPreview';

export default withOnyx<MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps>({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
chatReport: {
key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
},
iouReport: {
key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`,
},
session: {
key: ONYXKEYS.SESSION,
},
transaction: {
key: ({action}) => {
const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
const transactionID = isMoneyRequestAction ? action?.originalMessage?.IOUTransactionID : 0;
return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
walletTerms: {
key: ONYXKEYS.WALLET_TERMS,
},
transactionViolations: {
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
},
})(MoneyRequestPreview);
71 changes: 71 additions & 0 deletions src/components/ReportActionItem/MoneyRequestPreview/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import type * as OnyxTypes from '@src/types/onyx';

type MoneyRequestPreviewOnyxProps = {
/** All of the personal details for everyone */
personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>;

/** Chat report associated with iouReport */
chatReport: OnyxEntry<OnyxTypes.Report>;

/** IOU report data object */
iouReport: OnyxEntry<OnyxTypes.Report>;

/** Session info for the currently logged in user. */
session: OnyxEntry<OnyxTypes.Session>;

/** The transaction attached to the action.message.iouTransactionID */
transaction: OnyxEntry<OnyxTypes.Transaction>;

/** The transaction violations attached to the action.message.iouTransactionID */
transactionViolations: OnyxCollection<OnyxTypes.TransactionViolation[]>;

/** Information about the user accepting the terms for payments */
walletTerms: OnyxEntry<OnyxTypes.WalletTerms>;
};

type MoneyRequestPreviewProps = MoneyRequestPreviewOnyxProps & {
/** The active IOUReport, used for Onyx subscription */
// The iouReportID is used inside withOnyx HOC
// eslint-disable-next-line react/no-unused-prop-types
iouReportID: string;

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

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

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

/** All the data of the action, used for showing context menu */
action: OnyxTypes.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 is this IOU is a split instead of a 1:1 request */
isBillSplit: boolean;

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

/** Whether or not an IOU report contains money requests 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;
};

export type {MoneyRequestPreviewProps, MoneyRequestPreviewOnyxProps};
3 changes: 1 addition & 2 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function MoneyRequestView({
// Flags for showing categories and tags
// transactionCategory can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(Object.values(policyCategories ?? {})));
const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {}));
// transactionTag can be an empty string
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledTags(policyTagLists));
Expand Down Expand Up @@ -248,7 +248,6 @@ function MoneyRequestView({
if (!transaction?.transactionID) {
return;
}

Transaction.clearError(transaction.transactionID);
}}
>
Expand Down
6 changes: 3 additions & 3 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2305,7 +2305,7 @@ export default {
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`,
missingCategory: 'Missing category',
missingComment: 'Description required for selected category',
missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`,
missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`,
modifiedAmount: 'Amount greater than scanned receipt',
modifiedDate: 'Date differs from scanned receipt',
nonExpensiworksExpense: 'Non-Expensiworks expense',
Expand All @@ -2315,8 +2315,8 @@ export default {
overLimitAttendee: ({formattedLimit}: ViolationsOverLimitParams) => `Amount over ${formattedLimit}/person limit`,
perDayLimit: ({formattedLimit}: ViolationsPerDayLimitParams) => `Amount over daily ${formattedLimit}/person category limit`,
receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.',
receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams = {}) =>
`Receipt required${formattedLimit ? ` over ${formattedLimit}${category ? ' category limit' : ''}` : ''}`,
receiptRequired: (params: ViolationsReceiptRequiredParams) => `Receipt required${params ? ` over ${params.formattedLimit}${params.category ? ' category limit' : ''}` : ''}`,
reviewRequired: 'Review required',
rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
if (brokenBankConnection) {
return isAdmin
Expand Down
Loading

0 comments on commit d28f86f

Please sign in to comment.