Skip to content

Commit 3ce43b1

Browse files
authored
Merge pull request Expensify#82139 from callstack-internal/update-uploading-receipt-screen
2 parents f8191c6 + 9af2e63 commit 3ce43b1

20 files changed

Lines changed: 174 additions & 55 deletions

File tree

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

src/languages/de.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1446,6 +1446,8 @@ const translations: TranslationDeepObject<typeof en> = {
14461446
receiptFailureMessage:
14471447
'<rbr>Beim Hochladen deiner Quittung ist ein Fehler aufgetreten. Bitte <a href="download">speichere die Quittung</a> und <a href="retry">versuche es später erneut</a>.</rbr>',
14481448
receiptFailureMessageShort: 'Beim Hochladen Ihres Belegs ist ein Fehler aufgetreten.',
1449+
receiptUploadFailedMessage: 'Beleg-Upload fehlgeschlagen. Speichere den Beleg oder lösche die Ausgabe und verliere sie.',
1450+
saveReceipt: 'Beleg speichern',
14491451
genericDeleteFailureMessage: 'Unerwarteter Fehler beim Löschen dieses Belegs. Bitte versuche es später erneut.',
14501452
genericEditFailureMessage: 'Unerwarteter Fehler beim Bearbeiten dieser Ausgabe. Bitte versuche es später noch einmal.',
14511453
genericSmartscanFailureMessage: 'Der Transaktion fehlen Felder',

src/languages/en.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1500,6 +1500,8 @@ const translations = {
15001500
receiptDeleteFailureError: 'Unexpected error deleting this receipt. Please try again later.',
15011501
receiptFailureMessage: '<rbr>There was an error uploading your receipt. Please <a href="download">save the receipt</a> and <a href="retry">try again</a> later.</rbr>',
15021502
receiptFailureMessageShort: 'There was an error uploading your receipt.',
1503+
receiptUploadFailedMessage: 'Receipt upload failed. Save the receipt, or delete the expense and lose it.',
1504+
saveReceipt: 'Save receipt',
15031505
genericDeleteFailureMessage: 'Unexpected error deleting this expense. Please try again later.',
15041506
genericEditFailureMessage: 'Unexpected error editing this expense. Please try again later.',
15051507
genericSmartscanFailureMessage: 'Transaction is missing fields',

src/languages/es.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,8 @@ const translations: TranslationDeepObject<typeof en> = {
14111411
receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Por favor, vuelve a intentarlo más tarde.',
14121412
receiptFailureMessage: '<rbr>Hubo un error al cargar tu recibo. Por favor, <a href="download">guarda el recibo</a> e <a href="retry">inténtalo de nuevo</a> más tarde.</rbr>',
14131413
receiptFailureMessageShort: 'Hubo un error al cargar tu recibo.',
1414+
receiptUploadFailedMessage: 'Error al subir el recibo. Guarda el recibo, o elimina el gasto y piérdelo.',
1415+
saveReceipt: 'Guardar recibo',
14141416
genericDeleteFailureMessage: 'Error inesperado al eliminar este gasto. Por favor, inténtalo más tarde.',
14151417
genericEditFailureMessage: 'Error inesperado al editar este gasto. Por favor, inténtalo más tarde.',
14161418
genericSmartscanFailureMessage: 'La transacción tiene campos vacíos',

0 commit comments

Comments
 (0)