Skip to content

Commit b08c709

Browse files
committed
Merge branch 'main' into narrow-invoice-and-report-selectors
2 parents a77fe4c + f4cd524 commit b08c709

24 files changed

Lines changed: 178 additions & 58 deletions

File tree

config/eslint/eslint.seatbelt.tsv

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@
110110
"../../src/components/MapView/MapView.web.tsx" "react-hooks/set-state-in-effect" 1
111111
"../../src/components/MapView/MapViewImpl.web.tsx" "react-hooks/set-state-in-effect" 1
112112
"../../src/components/MapView/ToggleDistanceUnitButton/index.android.tsx" "no-restricted-syntax" 1
113-
"../../src/components/MessagesRow.tsx" "no-restricted-syntax" 1
114113
"../../src/components/Modal/BaseModal.tsx" "react-hooks/set-state-in-effect" 1
115114
"../../src/components/Modal/Global/ConfirmModalWrapper.tsx" "@typescript-eslint/no-deprecated/ConfirmModal" 1
116115
"../../src/components/Modal/ReanimatedModal/Container/index.web.tsx" "react-hooks/refs" 1

docs/articles/new-expensify/reports-and-expenses/Expense-and-Report-Actions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Use this guide to understand:
146146
| **Reject** | On Outstanding reports | Assigned approver | Returns the entire report to the submitter or a previous approver with a required reason. The report moves to Draft (if rejected to submitter) or stays Outstanding (if rejected to a previous approver) |
147147
| **Approve** | For outstanding reports | Admin | Skips current approver |
148148
| **View details** | Any report | All roles | Opens details view with options to share, pin, view members |
149+
| **Received payment** | On approved, closed, or reimbursed expense reports where no bank payment has been initiated | Submitter | Confirms that payment was received outside of Expensify and marks the report as paid |
149150
| **Cancel payment** | After payment is initiated, but before the payment has been processed, or always for a manual payment | Payer | Cancels pending payment |
150151

151152
---

docs/articles/new-expensify/reports-and-expenses/Understanding-Report-Statuses-and-Actions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The grey **More** button is always visible in the report header. Tap it to acces
3333

3434
- Hold / Unhold
3535
- Unapprove
36+
- **Received payment**
3637
- Duplicate report
3738
- Download as CSV
3839
- Print

src/CONST/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9745,6 +9745,9 @@ const CONST = {
97459745
SETTINGS_EXIT_SURVEY: {
97469746
GO_TO_CLASSIC: 'SettingsExitSurvey-GoToExpensifyClassic',
97479747
},
9748+
MESSAGES_ROW: {
9749+
DISMISS: 'MessagesRow-Dismiss',
9750+
},
97489751
PROFILE_PAGE: {
97499752
AVATAR: 'ProfilePage-Avatar',
97509753
},

src/components/DotIndicatorMessage.tsx

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type {ReactElement} from 'react';
33
import React from 'react';
44
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
55
import {View} from 'react-native';
6-
import useConfirmModal from '@hooks/useConfirmModal';
76
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
87
import useLocalize from '@hooks/useLocalize';
98
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -13,12 +12,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
1312
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
1413
import {isReceiptError, isTranslationKeyError} from '@libs/ErrorUtils';
1514
import fileDownload from '@libs/fileDownload';
16-
import handleRetryPress from '@libs/ReceiptUploadRetryHandler';
1715
import CONST from '@src/CONST';
1816
import type {TranslationKeyError} from '@src/types/onyx/OnyxCommon';
1917
import type {ReceiptError} from '@src/types/onyx/Transaction';
18+
import Button from './Button';
2019
import Icon from './Icon';
21-
import RenderHTML from './RenderHTML';
2220
import Text from './Text';
2321

2422
type DotIndicatorMessageProps = {
@@ -50,8 +48,8 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles, dismissErr
5048
const StyleUtils = useStyleUtils();
5149
const {translate} = useLocalize();
5250
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
53-
const {shouldUseNarrowLayout} = useResponsiveLayout();
54-
const {showConfirmModal} = useConfirmModal();
51+
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
52+
const {shouldUseNarrowLayout, isSmallScreenWidth, isInNarrowPaneModal} = useResponsiveLayout();
5553

5654
if (Object.keys(messages).length === 0) {
5755
return null;
@@ -67,36 +65,12 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles, dismissErr
6765

6866
const isErrorMessage = type === 'error';
6967
const receiptError = uniqueMessages.find(isReceiptError);
70-
const handleLinkPress = (href: string) => {
71-
if (!receiptError) {
72-
return;
73-
}
74-
75-
if (href.endsWith('retry')) {
76-
handleRetryPress(receiptError, dismissError, () => {
77-
showConfirmModal({
78-
prompt: translate('common.genericErrorMessage'),
79-
confirmText: translate('common.ok'),
80-
shouldShowCancelButton: false,
81-
});
82-
});
83-
} else if (href.endsWith('download')) {
84-
fileDownload(translate, receiptError.source, receiptError.filename).finally(() => dismissError());
85-
}
86-
};
8768

8869
const isTextSelectable = !canUseTouchScreen() || !shouldUseNarrowLayout;
8970

9071
const renderMessage = (message: string | ReceiptError | ReactElement, index: number) => {
9172
if (isReceiptError(message)) {
92-
return (
93-
<View style={[styles.renderHTML, styles.flexRow]}>
94-
<RenderHTML
95-
html={translate('iou.error.receiptFailureMessage')}
96-
onLinkPress={(_evt, href) => handleLinkPress(href)}
97-
/>
98-
</View>
99-
);
73+
return null;
10074
}
10175

10276
const displayMessage = isTranslationKeyError(message) ? translate(message.translationKey) : message;
@@ -114,6 +88,58 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles, dismissErr
11488
);
11589
};
11690

91+
if (receiptError) {
92+
const isStackedLayout = !(isInNarrowPaneModal && !isSmallScreenWidth);
93+
const messageRow = (
94+
<View style={[styles.dotIndicatorMessage, isStackedLayout && styles.alignItemsStart, styles.flex1]}>
95+
<View style={styles.offlineFeedbackErrorDot}>
96+
<Icon
97+
src={expensifyIcons.DotIndicator}
98+
fill={isErrorMessage ? theme.danger : theme.success}
99+
/>
100+
</View>
101+
<Text
102+
style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage), textStyles, styles.flex1]}
103+
accessibilityRole={isErrorMessage ? CONST.ROLE.ALERT : undefined}
104+
accessibilityLiveRegion={isErrorMessage ? 'assertive' : undefined}
105+
>
106+
{translate('iou.error.receiptUploadFailedMessage')}
107+
</Text>
108+
</View>
109+
);
110+
const buttonsRow = (
111+
<View style={[styles.flexRow, styles.gap3]}>
112+
<Button
113+
small
114+
text={translate('iou.error.saveReceipt')}
115+
onPress={() => {
116+
fileDownload(translate, receiptError.source, receiptError.filename);
117+
}}
118+
/>
119+
<Button
120+
small
121+
danger
122+
text={translate('iou.deleteExpense', {count: 1})}
123+
onPress={dismissError}
124+
/>
125+
</View>
126+
);
127+
if (!isStackedLayout) {
128+
return (
129+
<View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter, style]}>
130+
{messageRow}
131+
{buttonsRow}
132+
</View>
133+
);
134+
}
135+
return (
136+
<View style={style}>
137+
{messageRow}
138+
<View style={styles.mt3}>{buttonsRow}</View>
139+
</View>
140+
);
141+
}
142+
117143
return (
118144
<View style={[styles.dotIndicatorMessage, style]}>
119145
<View

src/components/LHNOptionsList/OptionRowLHN/OptionRow/Subtitle.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function Subtitle({optionItem, viewMode, isOptionFocused}: SubtitleProps) {
3434
const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT;
3535
const textStyle = isOptionFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
3636
const alternateTextStyle = isInFocusMode
37-
? [textStyle, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2]
38-
: [textStyle, styles.optionAlternateText, styles.textLabelSupporting];
37+
? [textStyle, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.pre, styles.ml2]
38+
: [textStyle, styles.optionAlternateText, styles.textLabelSupporting, styles.pre];
3939
const alternateTextFSClass = FS.getChatFSClass(optionItem);
4040

4141
const containsCustomEmojiWithText = containsCustomEmojiUtils(alternateText) && !containsOnlyCustomEmoji(alternateText);

src/components/MessagesRow.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
55
import useLocalize from '@hooks/useLocalize';
66
import useTheme from '@hooks/useTheme';
77
import useThemeStyles from '@hooks/useThemeStyles';
8+
import {isReceiptError} from '@libs/ErrorUtils';
89
import CONST from '@src/CONST';
910
import type {TranslationKeyError} from '@src/types/onyx/OnyxCommon';
1011
import type {ReceiptError} from '@src/types/onyx/Transaction';
@@ -40,7 +41,8 @@ function MessagesRow({messages = {}, type, onDismiss, containerStyles, dismissEr
4041
const {translate} = useLocalize();
4142
const icons = useMemoizedLazyExpensifyIcons(['Close']);
4243

43-
const showDismissButton = !!onDismiss;
44+
const hasReceiptUploadError = Object.values(messages).some((message) => isReceiptError(message));
45+
const showDismissButton = !!onDismiss && !hasReceiptUploadError;
4446

4547
const dismissText = translate('common.dismiss');
4648

@@ -63,6 +65,7 @@ function MessagesRow({messages = {}, type, onDismiss, containerStyles, dismissEr
6365
onPress={onDismiss}
6466
role={CONST.ROLE.BUTTON}
6567
accessibilityLabel={dismissText}
68+
sentryLabel={CONST.SENTRY_LABEL.MESSAGES_ROW.DISMISS}
6669
>
6770
<Icon
6871
fill={theme.icon}

src/components/ReceiptAudit.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,22 @@ type ReceiptAuditProps = {
1414

1515
/** Whether to show audit result or not (e.g.`Verified`, `Issue Found`) */
1616
shouldShowAuditResult: boolean;
17+
18+
/** Whether the receipt upload itself failed — forces the "Issue found" label/dot regardless of audit state */
19+
hasReceiptUploadError?: boolean;
1720
};
1821

19-
function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) {
22+
function ReceiptAudit({notes, shouldShowAuditResult, hasReceiptUploadError = false}: ReceiptAuditProps) {
2023
const styles = useThemeStyles();
2124
const theme = useTheme();
2225
const {translate} = useLocalize();
2326
const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'DotIndicator']);
2427

28+
const hasIssues = hasReceiptUploadError || notes.length > 0;
2529
let auditText = '';
26-
if (notes.length > 0 && shouldShowAuditResult) {
30+
if (hasReceiptUploadError) {
31+
auditText = translate('iou.receiptIssuesFound', {count: 1});
32+
} else if (notes.length > 0 && shouldShowAuditResult) {
2733
auditText = translate('iou.receiptIssuesFound', {count: notes.length});
2834
} else if (!notes.length && shouldShowAuditResult) {
2935
auditText = translate('common.verified');
@@ -39,8 +45,8 @@ function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) {
3945
<Icon
4046
width={12}
4147
height={12}
42-
src={notes.length ? icons.DotIndicator : icons.Checkmark}
43-
fill={notes.length ? theme.danger : theme.success}
48+
src={hasIssues ? icons.DotIndicator : icons.Checkmark}
49+
fill={hasIssues ? theme.danger : theme.success}
4450
additionalStyles={styles.ml1}
4551
/>
4652
</>

src/components/ReportActionItem/MoneyRequestReceiptView.tsx

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {useRoute} from '@react-navigation/native';
12
import {hasSeenTourSelector} from '@selectors/Onboarding';
23
import mapValues from 'lodash/mapValues';
34
import React, {useEffect, useMemo, useRef, useState} from 'react';
@@ -37,7 +38,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
3738
import useTransactionViolations from '@hooks/useTransactionViolations';
3839
import {getBrokenConnectionUrlToFixPersonalCard} from '@libs/CardUtils';
3940
import {hasHoverSupport} from '@libs/DeviceCapabilities';
40-
import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from '@libs/ErrorUtils';
41+
import {getMicroSecondOnyxErrorObject, getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from '@libs/ErrorUtils';
4142
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
4243
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
4344
import {getOriginalMessage, isMoneyRequestAction, wasActionTakenByCurrentUser} from '@libs/ReportActionsUtils';
@@ -64,7 +65,7 @@ import variables from '@styles/variables';
6465
import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors';
6566
import {cleanUpMoneyRequest} from '@userActions/IOU/DeleteMoneyRequest';
6667
import {replaceReceipt} from '@userActions/IOU/Receipt';
67-
import {addAttachmentWithComment, navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
68+
import {addAttachmentWithComment, navigateToConciergeChatAndDeleteReport, setDeleteTransactionNavigateBackUrl} from '@userActions/Report';
6869
import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction';
6970
import CONST from '@src/CONST';
7071
import ONYXKEYS from '@src/ONYXKEYS';
@@ -125,6 +126,8 @@ function MoneyRequestReceiptView({
125126
const {environmentURL} = useEnvironment();
126127
const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();
127128
const {getReportRHPActiveRoute} = useActiveRoute();
129+
const route = useRoute();
130+
const routeBackTo = (route.params as {backTo?: string} | undefined)?.backTo;
128131
const parentReportID = report?.parentReportID;
129132
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReportID)}`);
130133
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReport?.parentReportID)}`);
@@ -297,15 +300,6 @@ function MoneyRequestReceiptView({
297300
const itemizedReceiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED);
298301
const customRulesViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_RULES);
299302

300-
// Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`)
301-
// `!!(receiptViolations.length || didReceiptScanSucceed)` is for not showing `Verified` when `receiptViolations` is empty and `didReceiptScanSucceed` is false.
302-
const shouldShowAuditMessage =
303-
!isTransactionScanning &&
304-
(hasReceipt || !!receiptRequiredViolation || !!itemizedReceiptRequiredViolation || !!customRulesViolation) &&
305-
!!(receiptViolations.length || didReceiptScanSucceed) &&
306-
isPaidGroupPolicy(report);
307-
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt);
308-
309303
const errorsWithoutReportCreation = useMemo(
310304
() => ({
311305
...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
@@ -314,7 +308,38 @@ function MoneyRequestReceiptView({
314308
[transaction?.errorFields?.route, transaction?.errorFields?.waypoints, transaction?.errors, parentReportAction?.errors],
315309
);
316310
const reportCreationError = useMemo(() => (getCreationReportErrors(report) ? getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage') : {}), [report]);
317-
const errors = useMemo(() => ({...errorsWithoutReportCreation, ...reportCreationError}), [errorsWithoutReportCreation, reportCreationError]);
311+
const hasReceiptUploadError = Object.values(errorsWithoutReportCreation).some((error) => isReceiptError(error));
312+
313+
// Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`)
314+
// `!!(receiptViolations.length || didReceiptScanSucceed)` is for not showing `Verified` when `receiptViolations` is empty and `didReceiptScanSucceed` is false.
315+
const shouldShowAuditMessage =
316+
!isTransactionScanning &&
317+
(hasReceipt || !!receiptRequiredViolation || !!itemizedReceiptRequiredViolation || !!customRulesViolation) &&
318+
!!(receiptViolations.length || didReceiptScanSucceed) &&
319+
isPaidGroupPolicy(report);
320+
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt || hasReceiptUploadError);
321+
322+
const fallbackReceiptError = useMemo(() => {
323+
if (hasReceiptUploadError || isEmptyObject(reportCreationError) || !hasReceipt || !transaction?.receipt) {
324+
return {};
325+
}
326+
327+
return getMicroSecondOnyxErrorObject({
328+
error: CONST.IOU.RECEIPT_ERROR,
329+
source: transaction.receipt.source?.toString() ?? '',
330+
filename: transaction.receipt.filename ?? '',
331+
});
332+
}, [hasReceiptUploadError, reportCreationError, hasReceipt, transaction]);
333+
334+
const errors = useMemo(() => {
335+
if (hasReceiptUploadError) {
336+
return errorsWithoutReportCreation;
337+
}
338+
if (!isEmptyObject(fallbackReceiptError)) {
339+
return {...errorsWithoutReportCreation, ...fallbackReceiptError};
340+
}
341+
return {...errorsWithoutReportCreation, ...reportCreationError};
342+
}, [hasReceiptUploadError, fallbackReceiptError, errorsWithoutReportCreation, reportCreationError]);
318343
const showReceiptErrorWithEmptyState = shouldShowReceiptEmptyState && !hasReceipt && !isEmptyObject(errors);
319344

320345
const {showConfirmModal} = useConfirmModal();
@@ -372,6 +397,8 @@ function MoneyRequestReceiptView({
372397
return;
373398
}
374399
if (parentReportAction) {
400+
const backToRoute = routeBackTo ?? Navigation.getActiveRoute();
401+
setDeleteTransactionNavigateBackUrl(backToRoute);
375402
cleanUpMoneyRequest(
376403
transaction?.transactionID ?? linkedTransactionID,
377404
parentReportAction,
@@ -462,6 +489,7 @@ function MoneyRequestReceiptView({
462489
<ReceiptAudit
463490
notes={receiptViolations}
464491
shouldShowAuditResult={!!shouldShowAuditMessage}
492+
hasReceiptUploadError={hasReceiptUploadError}
465493
/>
466494
</OfflineWithFeedback>
467495
)}
@@ -620,11 +648,11 @@ function MoneyRequestReceiptView({
620648
)}
621649
{/* For WideRHP (fillSpace is true), we need to wait for the image to load to get the correct size, then display the violation message to avoid the jumping issue.
622650
Otherwise (when fillSpace is false), we use a fixed size, so there's no need to wait for the image to load. */}
623-
{!!shouldShowAuditMessage && hasReceipt && (!isLoading || !fillSpace) && receiptAuditMessagesRow}
651+
{!hasReceiptUploadError && !!shouldShowAuditMessage && hasReceipt && (!isLoading || !fillSpace) && receiptAuditMessagesRow}
624652
</OfflineWithFeedback>
625653
)}
626654
{!shouldShowReceiptEmptyState && !hasReceipt && <View style={{marginVertical: 6}} />}
627-
{!!shouldShowAuditMessage && !hasReceipt && receiptAuditMessagesRow}
655+
{!hasReceiptUploadError && !!shouldShowAuditMessage && !hasReceipt && receiptAuditMessagesRow}
628656
{AttachmentErrorModal}
629657
{PDFValidationComponent}
630658
</View>

0 commit comments

Comments
 (0)