Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3293e33
feat: update error uploading receipt screen
OlimpiaZurek Feb 11, 2026
5f3324e
eslint fixes
OlimpiaZurek Feb 12, 2026
e05cbf9
fix conflicts
OlimpiaZurek Feb 12, 2026
24ea40f
undo submodule
OlimpiaZurek Feb 12, 2026
f87c01f
move retry params to hook
OlimpiaZurek Feb 12, 2026
2f4d2db
update translations
OlimpiaZurek Feb 13, 2026
68e6bc5
resolve conflicts
OlimpiaZurek Feb 26, 2026
54c3df2
Merge main into update-uploading-receipt-screen
OlimpiaZurek Mar 1, 2026
e92edee
remove submodule changes
OlimpiaZurek Mar 1, 2026
303ad69
ts fix
OlimpiaZurek Mar 1, 2026
255171d
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Mar 2, 2026
05b5f89
fix conflicts
OlimpiaZurek Mar 24, 2026
3c3efa7
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Apr 1, 2026
6e80ff8
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Apr 2, 2026
afdcb3a
ts fix
OlimpiaZurek Apr 9, 2026
c1e7769
revert submodule
OlimpiaZurek Apr 9, 2026
1a99dcf
run prettier
OlimpiaZurek Apr 9, 2026
11952f4
address review feedback
OlimpiaZurek Apr 10, 2026
573012b
change buttons width
OlimpiaZurek Apr 16, 2026
1666f79
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Apr 16, 2026
50949be
change button width
OlimpiaZurek Apr 16, 2026
c27b12d
Merge branch 'Expensify:main' into update-uploading-receipt-screen
adhorodyski Apr 18, 2026
7d3913a
fix: guard isReceiptError against null values
OlimpiaZurek Apr 20, 2026
757bb50
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Apr 28, 2026
3902c70
Update receipt upload error layout and actions
OlimpiaZurek Apr 29, 2026
fc75a9e
address comments and fix bugs
OlimpiaZurek Apr 30, 2026
45a50ca
Remove unused hook and fix tests
OlimpiaZurek Apr 30, 2026
22c49dc
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Apr 30, 2026
5d0a754
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek Apr 30, 2026
ce093b0
fix lint
OlimpiaZurek Apr 30, 2026
51e20a5
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek May 5, 2026
4412423
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek May 7, 2026
4e3f93c
address review comments
OlimpiaZurek May 7, 2026
3fa3155
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek May 8, 2026
eaacd26
fix conflicts
OlimpiaZurek May 13, 2026
64abf5b
Merge branch 'main' into update-uploading-receipt-screen
OlimpiaZurek May 13, 2026
9af2e63
fix conflict
OlimpiaZurek May 15, 2026
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
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9745,6 +9745,9 @@ const CONST = {
SETTINGS_EXIT_SURVEY: {
GO_TO_CLASSIC: 'SettingsExitSurvey-GoToExpensifyClassic',
},
MESSAGES_ROW: {
DISMISS: 'MessagesRow-Dismiss',
},
PROFILE_PAGE: {
AVATAR: 'ProfilePage-Avatar',
},
Expand Down
86 changes: 56 additions & 30 deletions src/components/DotIndicatorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {ReactElement} from 'react';
import React from 'react';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useConfirmModal from '@hooks/useConfirmModal';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand All @@ -13,12 +12,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import {isReceiptError, isTranslationKeyError} from '@libs/ErrorUtils';
import fileDownload from '@libs/fileDownload';
import handleRetryPress from '@libs/ReceiptUploadRetryHandler';
import CONST from '@src/CONST';
import type {TranslationKeyError} from '@src/types/onyx/OnyxCommon';
import type {ReceiptError} from '@src/types/onyx/Transaction';
import Button from './Button';
import Icon from './Icon';
import RenderHTML from './RenderHTML';
import Text from './Text';

type DotIndicatorMessageProps = {
Expand Down Expand Up @@ -50,8 +48,8 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles, dismissErr
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']);
const {shouldUseNarrowLayout} = useResponsiveLayout();
const {showConfirmModal} = useConfirmModal();
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {shouldUseNarrowLayout, isSmallScreenWidth, isInNarrowPaneModal} = useResponsiveLayout();

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

const isErrorMessage = type === 'error';
const receiptError = uniqueMessages.find(isReceiptError);
const handleLinkPress = (href: string) => {
if (!receiptError) {
return;
}

if (href.endsWith('retry')) {
handleRetryPress(receiptError, dismissError, () => {
showConfirmModal({
prompt: translate('common.genericErrorMessage'),
confirmText: translate('common.ok'),
shouldShowCancelButton: false,
});
});
} else if (href.endsWith('download')) {
fileDownload(translate, receiptError.source, receiptError.filename).finally(() => dismissError());
}
};

Comment thread
OlimpiaZurek marked this conversation as resolved.
const isTextSelectable = !canUseTouchScreen() || !shouldUseNarrowLayout;

const renderMessage = (message: string | ReceiptError | ReactElement, index: number) => {
if (isReceiptError(message)) {
return (
<View style={[styles.renderHTML, styles.flexRow]}>
<RenderHTML
html={translate('iou.error.receiptFailureMessage')}
onLinkPress={(_evt, href) => handleLinkPress(href)}
/>
</View>
);
return null;
}

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

if (receiptError) {
const isStackedLayout = !(isInNarrowPaneModal && !isSmallScreenWidth);
const messageRow = (
<View style={[styles.dotIndicatorMessage, isStackedLayout && styles.alignItemsStart, styles.flex1]}>
<View style={styles.offlineFeedbackErrorDot}>
<Icon
src={expensifyIcons.DotIndicator}
fill={isErrorMessage ? theme.danger : theme.success}
/>
</View>
<Text
style={[StyleUtils.getDotIndicatorTextStyles(isErrorMessage), textStyles, styles.flex1]}
accessibilityRole={isErrorMessage ? CONST.ROLE.ALERT : undefined}
accessibilityLiveRegion={isErrorMessage ? 'assertive' : undefined}
>
{translate('iou.error.receiptUploadFailedMessage')}
</Text>
</View>
);
const buttonsRow = (
<View style={[styles.flexRow, styles.gap3]}>
<Button
small
text={translate('iou.error.saveReceipt')}
onPress={() => {
fileDownload(translate, receiptError.source, receiptError.filename);
}}
/>
<Button
small
danger
text={translate('iou.deleteExpense', {count: 1})}
onPress={dismissError}
/>
</View>
);
if (!isStackedLayout) {
return (
<View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter, style]}>
{messageRow}
{buttonsRow}
</View>
);
}
return (
<View style={style}>
{messageRow}
<View style={styles.mt3}>{buttonsRow}</View>
</View>
);
}

return (
<View style={[styles.dotIndicatorMessage, style]}>
<View
Expand Down
5 changes: 4 additions & 1 deletion src/components/MessagesRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {isReceiptError} from '@libs/ErrorUtils';
import CONST from '@src/CONST';
import type {TranslationKeyError} from '@src/types/onyx/OnyxCommon';
import type {ReceiptError} from '@src/types/onyx/Transaction';
Expand Down Expand Up @@ -40,7 +41,8 @@ function MessagesRow({messages = {}, type, onDismiss, containerStyles, dismissEr
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Close']);

const showDismissButton = !!onDismiss;
const hasReceiptUploadError = Object.values(messages).some((message) => isReceiptError(message));
const showDismissButton = !!onDismiss && !hasReceiptUploadError;

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

Expand All @@ -63,6 +65,7 @@ function MessagesRow({messages = {}, type, onDismiss, containerStyles, dismissEr
onPress={onDismiss}
role={CONST.ROLE.BUTTON}
accessibilityLabel={dismissText}
sentryLabel={CONST.SENTRY_LABEL.MESSAGES_ROW.DISMISS}
>
<Icon
fill={theme.icon}
Expand Down
14 changes: 10 additions & 4 deletions src/components/ReceiptAudit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@ type ReceiptAuditProps = {

/** Whether to show audit result or not (e.g.`Verified`, `Issue Found`) */
shouldShowAuditResult: boolean;

/** Whether the receipt upload itself failed — forces the "Issue found" label/dot regardless of audit state */
hasReceiptUploadError?: boolean;
};

function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) {
function ReceiptAudit({notes, shouldShowAuditResult, hasReceiptUploadError = false}: ReceiptAuditProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'DotIndicator']);

const hasIssues = hasReceiptUploadError || notes.length > 0;
let auditText = '';
if (notes.length > 0 && shouldShowAuditResult) {
if (hasReceiptUploadError) {
auditText = translate('iou.receiptIssuesFound', {count: 1});
} else if (notes.length > 0 && shouldShowAuditResult) {
auditText = translate('iou.receiptIssuesFound', {count: notes.length});
} else if (!notes.length && shouldShowAuditResult) {
auditText = translate('common.verified');
Expand All @@ -39,8 +45,8 @@ function ReceiptAudit({notes, shouldShowAuditResult}: ReceiptAuditProps) {
<Icon
width={12}
height={12}
src={notes.length ? icons.DotIndicator : icons.Checkmark}
fill={notes.length ? theme.danger : theme.success}
src={hasIssues ? icons.DotIndicator : icons.Checkmark}
fill={hasIssues ? theme.danger : theme.success}
additionalStyles={styles.ml1}
/>
</>
Expand Down
56 changes: 42 additions & 14 deletions src/components/ReportActionItem/MoneyRequestReceiptView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useRoute} from '@react-navigation/native';
import {hasSeenTourSelector} from '@selectors/Onboarding';
import mapValues from 'lodash/mapValues';
import React, {useEffect, useMemo, useRef, useState} from 'react';
Expand Down Expand Up @@ -37,7 +38,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolations from '@hooks/useTransactionViolations';
import {getBrokenConnectionUrlToFixPersonalCard} from '@libs/CardUtils';
import {hasHoverSupport} from '@libs/DeviceCapabilities';
import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from '@libs/ErrorUtils';
import {getMicroSecondOnyxErrorObject, getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from '@libs/ErrorUtils';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
import {getOriginalMessage, isMoneyRequestAction, wasActionTakenByCurrentUser} from '@libs/ReportActionsUtils';
Expand All @@ -64,7 +65,7 @@ import variables from '@styles/variables';
import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors';
import {cleanUpMoneyRequest} from '@userActions/IOU/DeleteMoneyRequest';
import {replaceReceipt} from '@userActions/IOU/Receipt';
import {addAttachmentWithComment, navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
import {addAttachmentWithComment, navigateToConciergeChatAndDeleteReport, setDeleteTransactionNavigateBackUrl} from '@userActions/Report';
import {clearError, getLastModifiedExpense, revert} from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -125,6 +126,8 @@ function MoneyRequestReceiptView({
const {environmentURL} = useEnvironment();
const {shouldUseNarrowLayout, isInNarrowPaneModal} = useResponsiveLayout();
const {getReportRHPActiveRoute} = useActiveRoute();
const route = useRoute();
const routeBackTo = (route.params as {backTo?: string} | undefined)?.backTo;
const parentReportID = report?.parentReportID;
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReportID)}`);
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReport?.parentReportID)}`);
Expand Down Expand Up @@ -297,15 +300,6 @@ function MoneyRequestReceiptView({
const itemizedReceiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED);
const customRulesViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_RULES);

// Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`)
// `!!(receiptViolations.length || didReceiptScanSucceed)` is for not showing `Verified` when `receiptViolations` is empty and `didReceiptScanSucceed` is false.
const shouldShowAuditMessage =
!isTransactionScanning &&
(hasReceipt || !!receiptRequiredViolation || !!itemizedReceiptRequiredViolation || !!customRulesViolation) &&
!!(receiptViolations.length || didReceiptScanSucceed) &&
isPaidGroupPolicy(report);
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt);

const errorsWithoutReportCreation = useMemo(
() => ({
...(transaction?.errorFields?.route ?? transaction?.errorFields?.waypoints ?? transaction?.errors),
Expand All @@ -314,7 +308,38 @@ function MoneyRequestReceiptView({
[transaction?.errorFields?.route, transaction?.errorFields?.waypoints, transaction?.errors, parentReportAction?.errors],
);
const reportCreationError = useMemo(() => (getCreationReportErrors(report) ? getMicroSecondOnyxErrorWithTranslationKey('report.genericCreateReportFailureMessage') : {}), [report]);
const errors = useMemo(() => ({...errorsWithoutReportCreation, ...reportCreationError}), [errorsWithoutReportCreation, reportCreationError]);
const hasReceiptUploadError = Object.values(errorsWithoutReportCreation).some((error) => isReceiptError(error));

// Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`)
// `!!(receiptViolations.length || didReceiptScanSucceed)` is for not showing `Verified` when `receiptViolations` is empty and `didReceiptScanSucceed` is false.
const shouldShowAuditMessage =
!isTransactionScanning &&
(hasReceipt || !!receiptRequiredViolation || !!itemizedReceiptRequiredViolation || !!customRulesViolation) &&
!!(receiptViolations.length || didReceiptScanSucceed) &&
isPaidGroupPolicy(report);
const shouldShowReceiptAudit = !isInvoice && (shouldShowReceiptEmptyState || hasReceipt || hasReceiptUploadError);

const fallbackReceiptError = useMemo(() => {
if (hasReceiptUploadError || isEmptyObject(reportCreationError) || !hasReceipt || !transaction?.receipt) {
return {};
}

return getMicroSecondOnyxErrorObject({
error: CONST.IOU.RECEIPT_ERROR,
Comment thread
OlimpiaZurek marked this conversation as resolved.
source: transaction.receipt.source?.toString() ?? '',
filename: transaction.receipt.filename ?? '',
});
}, [hasReceiptUploadError, reportCreationError, hasReceipt, transaction]);

const errors = useMemo(() => {
if (hasReceiptUploadError) {
return errorsWithoutReportCreation;
}
if (!isEmptyObject(fallbackReceiptError)) {
return {...errorsWithoutReportCreation, ...fallbackReceiptError};
}
return {...errorsWithoutReportCreation, ...reportCreationError};
}, [hasReceiptUploadError, fallbackReceiptError, errorsWithoutReportCreation, reportCreationError]);
const showReceiptErrorWithEmptyState = shouldShowReceiptEmptyState && !hasReceipt && !isEmptyObject(errors);

const {showConfirmModal} = useConfirmModal();
Expand Down Expand Up @@ -372,6 +397,8 @@ function MoneyRequestReceiptView({
return;
}
if (parentReportAction) {
const backToRoute = routeBackTo ?? Navigation.getActiveRoute();
setDeleteTransactionNavigateBackUrl(backToRoute);
cleanUpMoneyRequest(
transaction?.transactionID ?? linkedTransactionID,
parentReportAction,
Expand Down Expand Up @@ -462,6 +489,7 @@ function MoneyRequestReceiptView({
<ReceiptAudit
notes={receiptViolations}
shouldShowAuditResult={!!shouldShowAuditMessage}
hasReceiptUploadError={hasReceiptUploadError}
/>
</OfflineWithFeedback>
)}
Expand Down Expand Up @@ -620,11 +648,11 @@ function MoneyRequestReceiptView({
)}
{/* 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.
Otherwise (when fillSpace is false), we use a fixed size, so there's no need to wait for the image to load. */}
{!!shouldShowAuditMessage && hasReceipt && (!isLoading || !fillSpace) && receiptAuditMessagesRow}
{!hasReceiptUploadError && !!shouldShowAuditMessage && hasReceipt && (!isLoading || !fillSpace) && receiptAuditMessagesRow}
</OfflineWithFeedback>
)}
{!shouldShowReceiptEmptyState && !hasReceipt && <View style={{marginVertical: 6}} />}
{!!shouldShowAuditMessage && !hasReceipt && receiptAuditMessagesRow}
{!hasReceiptUploadError && !!shouldShowAuditMessage && !hasReceipt && receiptAuditMessagesRow}
{AttachmentErrorModal}
{PDFValidationComponent}
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,8 @@ const translations: TranslationDeepObject<typeof en> = {
receiptFailureMessage:
'<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>',
receiptFailureMessageShort: 'Beim Hochladen Ihres Belegs ist ein Fehler aufgetreten.',
receiptUploadFailedMessage: 'Beleg-Upload fehlgeschlagen. Speichere den Beleg oder lösche die Ausgabe und verliere sie.',
saveReceipt: 'Beleg speichern',
genericDeleteFailureMessage: 'Unerwarteter Fehler beim Löschen dieses Belegs. Bitte versuche es später erneut.',
genericEditFailureMessage: 'Unerwarteter Fehler beim Bearbeiten dieser Ausgabe. Bitte versuche es später noch einmal.',
genericSmartscanFailureMessage: 'Der Transaktion fehlen Felder',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1500,6 +1500,8 @@ const translations = {
receiptDeleteFailureError: 'Unexpected error deleting this receipt. Please try again later.',
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>',
receiptFailureMessageShort: 'There was an error uploading your receipt.',
receiptUploadFailedMessage: 'Receipt upload failed. Save the receipt, or delete the expense and lose it.',
saveReceipt: 'Save receipt',
genericDeleteFailureMessage: 'Unexpected error deleting this expense. Please try again later.',
genericEditFailureMessage: 'Unexpected error editing this expense. Please try again later.',
genericSmartscanFailureMessage: 'Transaction is missing fields',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,8 @@ const translations: TranslationDeepObject<typeof en> = {
receiptDeleteFailureError: 'Error inesperado al borrar este recibo. Por favor, vuelve a intentarlo más tarde.',
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>',
receiptFailureMessageShort: 'Hubo un error al cargar tu recibo.',
receiptUploadFailedMessage: 'Error al subir el recibo. Guarda el recibo, o elimina el gasto y piérdelo.',
saveReceipt: 'Guardar recibo',
genericDeleteFailureMessage: 'Error inesperado al eliminar este gasto. Por favor, inténtalo más tarde.',
genericEditFailureMessage: 'Error inesperado al editar este gasto. Por favor, inténtalo más tarde.',
genericSmartscanFailureMessage: 'La transacción tiene campos vacíos',
Expand Down
Loading
Loading