From 28e2210060a1dc5d31d2ae72826f56a2ad5bc582 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 07:50:41 +0300 Subject: [PATCH 01/15] fix: use PressableWithoutFocus on enlarge button to prevent blue focus ring after ESC --- .../ReportActionItem/MoneyRequestReceiptView.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 299da8b18bafd..6548a9b2d06db 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -9,6 +9,7 @@ import Icon from '@components/Icon'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import ReceiptAudit, {ReceiptAuditMessages} from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; import useActiveRoute from '@hooks/useActiveRoute'; @@ -497,13 +498,13 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, )} - Navigation.navigate( ROUTES.TRANSACTION_RECEIPT.getRoute(report?.reportID, (updatedTransaction ?? transaction)?.transactionID, readonly || !canEditReceipt), ) } - style={styles.receiptActionButton} + style={[styles.receiptActionButton, styles.noOutline]} accessibilityLabel={translate('accessibilityHints.viewAttachment')} role={CONST.ROLE.BUTTON} sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ENLARGE_BUTTON} @@ -516,7 +517,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, fill={theme.icon} /> - + )} From 48438e9508172996edee7a375cc5e71d651d9b25 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 08:04:54 +0300 Subject: [PATCH 02/15] fix: validate attachment files and restrict file types on receipt add button --- .../MoneyRequestReceiptView.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 6548a9b2d06db..ec96d3def1da7 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -18,6 +18,7 @@ import useCardFeedErrors from '@hooks/useCardFeedErrors'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; +import useFilesValidation from '@hooks/useFilesValidation'; import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAction'; import useHover from '@hooks/useHover'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -160,6 +161,22 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const canEditReceipt = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy); + const onAttachmentFilesValidated = (files: FileObject[]) => { + if (!report?.reportID) { + return; + } + addAttachmentWithComment({ + report, + notifyReportID: moneyRequestReport?.reportID ?? report.reportID, + ancestors, + attachments: files, + currentUserAccountID, + timezone: currentUserTimezone, + }); + }; + + const {validateFiles, PDFValidationComponent, ErrorModal: AttachmentErrorModal} = useFilesValidation(onAttachmentFilesValidated); + const iouType = useMemo(() => { if (isTrackExpense) { return CONST.IOU.TYPE.TRACK; @@ -462,23 +479,13 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, /> {canShowReceiptActions && ( - + {({openPicker}) => ( { openPicker({ onPicked: (files) => { - if (!report?.reportID) { - return; - } - addAttachmentWithComment({ - report, - notifyReportID: moneyRequestReport?.reportID ?? report.reportID, - ancestors, - attachments: files, - currentUserAccountID, - timezone: currentUserTimezone, - }); + validateFiles(files); }, }); }} @@ -529,6 +536,8 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, )} {!shouldShowReceiptEmptyState && !hasReceipt && } {!!shouldShowAuditMessage && !hasReceipt && receiptAuditMessagesRow} + {AttachmentErrorModal} + {PDFValidationComponent} ); } From 54d5e4319c6a1ba3f3a879fc6fcdb4ae6ca694f7 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 08:11:39 +0300 Subject: [PATCH 03/15] fix: notify transaction thread report for scroll-to-bottom after adding attachment --- src/components/ReportActionItem/MoneyRequestReceiptView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index ec96d3def1da7..60a47541a9cca 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -167,7 +167,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, } addAttachmentWithComment({ report, - notifyReportID: moneyRequestReport?.reportID ?? report.reportID, + notifyReportID: report.reportID, ancestors, attachments: files, currentUserAccountID, From 98e7b98f5cba501e165c9f5b766b15129feb54e5 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 6 Mar 2026 08:35:20 +0300 Subject: [PATCH 04/15] fix: show receipt action buttons when hovering during image load --- .../MoneyRequestReceiptView.tsx | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 60a47541a9cca..4c29aff77c4e0 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -1,5 +1,5 @@ import mapValues from 'lodash/mapValues'; -import React, {useMemo, useState} from 'react'; +import React, {useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -146,9 +146,17 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const theme = useTheme(); const ancestors = useAncestors(report); const {hovered, bind: hoverBind} = useHover(); + const isMouseInsideRef = useRef(false); const isTouchScreen = canUseTouchScreen(); const lazyIcons = useMemoizedLazyExpensifyIcons(['Expand', 'ReceiptPlus']); + useEffect(() => { + if (isLoading || !isMouseInsideRef.current) { + return; + } + hoverBind.onMouseEnter(); + }, [isLoading, hoverBind]); + // Flags for allowing or disallowing editing an expense // Used for non-restricted fields such as: description, category, tag, billable, etc... const isReportArchived = useReportIsArchived(report?.reportID); @@ -457,8 +465,16 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, showBorderlessLoading && styles.flex1, fillSpace && !shouldShowReceiptEmptyState && isMapDistanceRequest && styles.flex1, ]} - onMouseEnter={() => !isLoading && hoverBind.onMouseEnter()} - onMouseLeave={hoverBind.onMouseLeave} + onMouseEnter={() => { + isMouseInsideRef.current = true; + if (!isLoading) { + hoverBind.onMouseEnter(); + } + }} + onMouseLeave={() => { + isMouseInsideRef.current = false; + hoverBind.onMouseLeave(); + }} > Date: Sat, 7 Mar 2026 15:50:22 +0300 Subject: [PATCH 05/15] fix: use hasHoverSupport instead of canUseTouchScreen for receipt action buttons --- .../ReportActionItem/MoneyRequestReceiptView.tsx | 6 +++--- src/hooks/useHover.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 4c29aff77c4e0..1a684fd6d8c11 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -31,7 +31,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import {getBrokenConnectionUrlToFixPersonalCard} from '@libs/CardUtils'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import {hasHoverSupport} from '@libs/DeviceCapabilities'; import {getMicroSecondOnyxErrorWithTranslationKey, isReceiptError} from '@libs/ErrorUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; @@ -147,7 +147,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const ancestors = useAncestors(report); const {hovered, bind: hoverBind} = useHover(); const isMouseInsideRef = useRef(false); - const isTouchScreen = canUseTouchScreen(); + const deviceHasHoverSupport = hasHoverSupport(); const lazyIcons = useMemoizedLazyExpensifyIcons(['Expand', 'ReceiptPlus']); useEffect(() => { @@ -494,7 +494,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, onLoadFailure={() => setIsLoading(false)} /> {canShowReceiptActions && ( - + {({openPicker}) => ( { const [hovered, setHovered] = useState(false); - const canUseTouchScreen = canUseTouchScreenUtil(); + const deviceHasHoverSupport = hasHoverSupport(); return { hovered, bind: { - onMouseEnter: () => !canUseTouchScreen && setHovered(true), - onMouseLeave: () => !canUseTouchScreen && setHovered(false), + onMouseEnter: () => deviceHasHoverSupport && setHovered(true), + onMouseLeave: () => deviceHasHoverSupport && setHovered(false), }, }; }; From 465391b9965fb3a30f5d040c2e40bc2a9864272d Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 9 Mar 2026 21:53:56 +0300 Subject: [PATCH 06/15] feat: add hover states to receipt action buttons --- .../MoneyRequestReceiptView.tsx | 30 +++++++++---------- src/styles/index.ts | 2 ++ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index f780db9fa3f88..0d3e05a854f14 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -507,18 +507,17 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, }); }} style={styles.receiptActionButton} + hoverStyle={styles.buttonDefaultHovered} accessibilityLabel={translate('reportActionCompose.addAttachment')} role={CONST.ROLE.BUTTON} sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ADD_ATTACHMENT_BUTTON} > - - - + )} @@ -529,18 +528,17 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, ) } style={[styles.receiptActionButton, styles.noOutline]} + hoverStyle={styles.buttonDefaultHovered} accessibilityLabel={translate('accessibilityHints.viewAttachment')} role={CONST.ROLE.BUTTON} sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ENLARGE_BUTTON} > - - - + )} diff --git a/src/styles/index.ts b/src/styles/index.ts index f839bd0fa2511..dc27a5c6b9a77 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3635,6 +3635,8 @@ const staticStyles = (theme: ThemeColors) => height: 40, alignItems: 'center', justifyContent: 'center', + backgroundColor: theme.buttonDefaultBG, + borderRadius: 20, }, bgGreenSuccess: { From 74630944686569060e4231681c0f358c3b8a5bc8 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Fri, 13 Mar 2026 02:36:20 +0300 Subject: [PATCH 07/15] feat: add tooltips to receipt action buttons --- .../MoneyRequestReceiptView.tsx | 85 ++++++++++--------- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 11 files changed, 55 insertions(+), 40 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 0d3e05a854f14..a7953390d13ea 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -12,6 +12,7 @@ import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeed import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import ReceiptAudit, {ReceiptAuditMessages} from '@components/ReceiptAudit'; import ReceiptEmptyState from '@components/ReceiptEmptyState'; +import Tooltip from '@components/Tooltip'; import useActiveRoute from '@hooks/useActiveRoute'; import useAncestors from '@hooks/useAncestors'; import useCardFeedErrors from '@hooks/useCardFeedErrors'; @@ -498,48 +499,52 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, {({openPicker}) => ( - { - openPicker({ - onPicked: (files) => { - validateFiles(files); - }, - }); - }} - style={styles.receiptActionButton} - hoverStyle={styles.buttonDefaultHovered} - accessibilityLabel={translate('reportActionCompose.addAttachment')} - role={CONST.ROLE.BUTTON} - sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ADD_ATTACHMENT_BUTTON} - > - - + + { + openPicker({ + onPicked: (files) => { + validateFiles(files); + }, + }); + }} + style={styles.receiptActionButton} + hoverStyle={styles.buttonDefaultHovered} + accessibilityLabel={translate('receipt.addAdditionalReceipt')} + role={CONST.ROLE.BUTTON} + sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ADD_ATTACHMENT_BUTTON} + > + + + )} - - Navigation.navigate( - ROUTES.TRANSACTION_RECEIPT.getRoute(report?.reportID, (updatedTransaction ?? transaction)?.transactionID, readonly || !canEditReceipt), - ) - } - style={[styles.receiptActionButton, styles.noOutline]} - hoverStyle={styles.buttonDefaultHovered} - accessibilityLabel={translate('accessibilityHints.viewAttachment')} - role={CONST.ROLE.BUTTON} - sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ENLARGE_BUTTON} - > - - + + + Navigation.navigate( + ROUTES.TRANSACTION_RECEIPT.getRoute(report?.reportID, (updatedTransaction ?? transaction)?.transactionID, readonly || !canEditReceipt), + ) + } + style={[styles.receiptActionButton, styles.noOutline]} + hoverStyle={styles.buttonDefaultHovered} + accessibilityLabel={translate('accessibilityHints.viewAttachment')} + role={CONST.ROLE.BUTTON} + sentryLabel={CONST.SENTRY_LABEL.RECEIPT.ENLARGE_BUTTON} + > + + + )} diff --git a/src/languages/de.ts b/src/languages/de.ts index ac5090cf337af..f85baf1bbf768 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1178,6 +1178,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Beleg löschen', deleteConfirmation: 'Sind Sie sicher, dass Sie diesen Beleg löschen möchten?', addReceipt: 'Beleg hinzufügen', + addAdditionalReceipt: 'Zusätzlichen Beleg hinzufügen', scanFailed: 'Der Beleg konnte nicht gescannt werden, da Händler, Datum oder Betrag fehlen.', crop: 'Zuschneiden', addAReceipt: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 2f7e97db1d289..73d948c3cdc5b 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1212,6 +1212,7 @@ const translations = { deleteReceipt: 'Delete receipt', deleteConfirmation: 'Are you sure you want to delete this receipt?', addReceipt: 'Add receipt', + addAdditionalReceipt: 'Add additional receipt', scanFailed: "The receipt couldn't be scanned, as it's missing a merchant, date, or amount.", crop: 'Crop', addAReceipt: { diff --git a/src/languages/es.ts b/src/languages/es.ts index be67749dfcd57..e192483f1ef2e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1050,6 +1050,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Eliminar recibo', deleteConfirmation: '¿Estás seguro de que quieres borrar este recibo?', addReceipt: 'Añadir recibo', + addAdditionalReceipt: 'Añadir recibo adicional', scanFailed: 'El recibo no pudo ser escaneado, ya que falta el comerciante, la fecha o el monto.', crop: 'Recortar', addAReceipt: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3a34c0c1947c1..5c0cd0115057e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1182,6 +1182,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Supprimer le reçu', deleteConfirmation: 'Voulez-vous vraiment supprimer ce reçu ?', addReceipt: 'Ajouter un reçu', + addAdditionalReceipt: 'Ajouter un reçu supplémentaire', scanFailed: 'Le reçu n’a pas pu être scanné, car il lui manque un commerçant, une date ou un montant.', crop: 'Recadrer', addAReceipt: { diff --git a/src/languages/it.ts b/src/languages/it.ts index e65e7db28213a..dc9519c9fb879 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1176,6 +1176,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Elimina ricevuta', deleteConfirmation: 'Sei sicuro di voler eliminare questa ricevuta?', addReceipt: 'Aggiungi ricevuta', + addAdditionalReceipt: 'Aggiungi ricevuta aggiuntiva', scanFailed: 'La ricevuta non può essere acquisita perché manca il nome dell’esercente, la data o l’importo.', crop: 'Ritaglia', addAReceipt: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 9ab90d2d8dd3d..903ffde44d988 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1168,6 +1168,7 @@ const translations: TranslationDeepObject = { deleteReceipt: '領収書を削除', deleteConfirmation: 'この領収書を削除してもよろしいですか?', addReceipt: '領収書を追加', + addAdditionalReceipt: 'レシートを追加', scanFailed: 'このレシートは、店舗名、日付、または金額が不足しているためスキャンできませんでした。', crop: 'トリミング', addAReceipt: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 1ade218c60dfc..d3fb00c4131e3 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1175,6 +1175,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Bon verwijderen', deleteConfirmation: 'Weet je zeker dat je deze bon wilt verwijderen?', addReceipt: 'Bon toevoegen', + addAdditionalReceipt: 'Voeg extra bon toe', scanFailed: 'De bon is niet gescand, omdat er een handelaar, datum of bedrag ontbreekt.', crop: 'Bijsnijden', addAReceipt: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index dea6dcd3a6ea8..280d46f0f2a6d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1175,6 +1175,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Usuń paragon', deleteConfirmation: 'Czy na pewno chcesz usunąć ten paragon?', addReceipt: 'Dodaj paragon', + addAdditionalReceipt: 'Dodaj dodatkowy paragon', scanFailed: 'Nie można było zeskanować paragonu, ponieważ brakuje na nim sprzedawcy, daty lub kwoty.', crop: 'Przytnij', addAReceipt: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index be11c98aa0865..a412d957d4ba2 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1174,6 +1174,7 @@ const translations: TranslationDeepObject = { deleteReceipt: 'Excluir recibo', deleteConfirmation: 'Tem certeza de que deseja excluir este recibo?', addReceipt: 'Adicionar recibo', + addAdditionalReceipt: 'Adicionar recibo adicional', scanFailed: 'O recibo não pôde ser digitalizado porque está faltando o comerciante, a data ou o valor.', crop: 'Cortar', addAReceipt: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index f87e2bac47587..43c1ba47b4e34 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1151,6 +1151,7 @@ const translations: TranslationDeepObject = { deleteReceipt: '删除收据', deleteConfirmation: '确定要删除这张收据吗?', addReceipt: '添加收据', + addAdditionalReceipt: '添加额外收据', scanFailed: '无法扫描此收据,因为缺少商家、日期或金额。', crop: '裁剪', addAReceipt: { From 8a81fcaabd8b408e978dbe645aea8802d39e522c Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 16 Mar 2026 08:28:15 +0300 Subject: [PATCH 08/15] fix: reset stale hover states after file picker cancel and improve first-upload hover detection --- .../MoneyRequestReceiptView.tsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index a7953390d13ea..d4bb5bf0ddf12 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -148,15 +148,21 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const theme = useTheme(); const ancestors = useAncestors(report); const {hovered, bind: hoverBind} = useHover(); - const isMouseInsideRef = useRef(false); + const receiptContainerRef = useRef(null); + const addButtonRef = useRef(null); + const skipContainerMouseLeaveRef = useRef(false); const deviceHasHoverSupport = hasHoverSupport(); const lazyIcons = useMemoizedLazyExpensifyIcons(['Expand', 'ReceiptPlus']); + // Browsers don't fire mouseenter when an element mounts under the cursor useEffect(() => { - if (isLoading || !isMouseInsideRef.current) { + if (isLoading) { return; } - hoverBind.onMouseEnter(); + const receiptElement = receiptContainerRef.current as unknown as HTMLElement | null; + if (receiptElement?.matches?.(':hover')) { + hoverBind.onMouseEnter(); + } }, [isLoading, hoverBind]); // Flags for allowing or disallowing editing an expense @@ -365,7 +371,6 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const isMapDistanceRequest = !!transaction && isDistanceRequest && !isManualDistanceRequest(transaction); const canShowReceiptActions = hasReceipt && !isLoading && isEditable && !isMapDistanceRequest && !mergeTransactionID; - const receiptAuditMessagesRow = ( @@ -461,20 +466,19 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, > {hasReceipt && ( { - isMouseInsideRef.current = true; - if (!isLoading) { - hoverBind.onMouseEnter(); - } - }} + onMouseEnter={() => !isLoading && hoverBind.onMouseEnter()} onMouseLeave={() => { - isMouseInsideRef.current = false; + if (skipContainerMouseLeaveRef.current) { + skipContainerMouseLeaveRef.current = false; + return; + } hoverBind.onMouseLeave(); }} > @@ -501,11 +505,19 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, {({openPicker}) => ( { openPicker({ onPicked: (files) => { validateFiles(files); }, + onCanceled: () => { + // Reset stale hover states after native file dialog dismiss + const buttonEl = addButtonRef.current as unknown as HTMLElement; + buttonEl?.dispatchEvent(new PointerEvent('pointerleave')); + skipContainerMouseLeaveRef.current = true; + buttonEl?.dispatchEvent(new MouseEvent('mouseout', {bubbles: true, relatedTarget: document.body})); + }, }); }} style={styles.receiptActionButton} @@ -531,7 +543,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, ROUTES.TRANSACTION_RECEIPT.getRoute(report?.reportID, (updatedTransaction ?? transaction)?.transactionID, readonly || !canEditReceipt), ) } - style={[styles.receiptActionButton, styles.noOutline]} + style={styles.receiptActionButton} hoverStyle={styles.buttonDefaultHovered} accessibilityLabel={translate('accessibilityHints.viewAttachment')} role={CONST.ROLE.BUTTON} From 0b77c550af1af36e9d950affb2b1c7ba1a8e66e2 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 16 Mar 2026 09:43:52 +0300 Subject: [PATCH 09/15] fix: notify both transaction thread and money request report for scroll after attachment --- .../ReportActionItem/MoneyRequestReceiptView.tsx | 3 ++- src/libs/actions/Report/index.ts | 16 +++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index d4bb5bf0ddf12..0fe4b128ae4bd 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -181,9 +181,10 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, if (!report?.reportID) { return; } + const notifyReportID = moneyRequestReport?.reportID ? [report.reportID, moneyRequestReport.reportID] : report.reportID; addAttachmentWithComment({ report, - notifyReportID: report.reportID, + notifyReportID, ancestors, attachments: files, currentUserAccountID, diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index f4e03d26c5a28..333ea66551447 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -341,7 +341,7 @@ type AddCommentParams = { type AddActionsParams = { report: OnyxEntry; - notifyReportID: string; + notifyReportID: string | string[]; ancestors: Ancestor[]; timezoneParam: Timezone; currentUserAccountID: number; @@ -354,7 +354,7 @@ type AddActionsParams = { type AddAttachmentWithCommentParams = { report: OnyxEntry; - notifyReportID: string; + notifyReportID: string | string[]; ancestors: Ancestor[]; attachments: FileObject | FileObject[]; currentUserAccountID: number; @@ -630,12 +630,14 @@ function subscribeToNewActionEvent(reportID: string, callback: SubscriberCallbac } /** Notify the ReportActionsView that a new comment has arrived */ -function notifyNewAction(reportID: string | undefined, reportAction: ReportAction | undefined, isFromCurrentUser: boolean) { - const actionSubscriber = newActionSubscribers.find((subscriber) => subscriber.reportID === reportID); - if (!actionSubscriber) { - return; +function notifyNewAction(reportID: string | string[] | undefined, reportAction: ReportAction | undefined, isFromCurrentUser: boolean) { + const ids = Array.isArray(reportID) ? reportID : [reportID]; + for (const id of ids) { + const actionSubscriber = newActionSubscribers.find((subscriber) => subscriber.reportID === id); + if (actionSubscriber) { + actionSubscriber.callback(isFromCurrentUser, reportAction); + } } - actionSubscriber.callback(isFromCurrentUser, reportAction); } /** From 777b8638ad77195ad16a5e2775c2005b598c1cbe Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 16 Mar 2026 09:56:41 +0300 Subject: [PATCH 10/15] test: update receipt button accessibility label in tests --- tests/ui/components/MoneyRequestReceiptViewTest.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ui/components/MoneyRequestReceiptViewTest.tsx b/tests/ui/components/MoneyRequestReceiptViewTest.tsx index 6a14647d407e4..9ea3f4f33734d 100644 --- a/tests/ui/components/MoneyRequestReceiptViewTest.tsx +++ b/tests/ui/components/MoneyRequestReceiptViewTest.tsx @@ -247,7 +247,7 @@ describe('MoneyRequestReceiptView', () => { await waitForBatchedUpdatesWithAct(); expect(screen.queryByLabelText(translateLocal('accessibilityHints.viewAttachment'))).toBeNull(); - expect(screen.queryByLabelText(translateLocal('reportActionCompose.addAttachment'))).toBeNull(); + expect(screen.queryByLabelText(translateLocal('receipt.addAdditionalReceipt'))).toBeNull(); }); it('shows action buttons when transaction has a receipt', async () => { @@ -264,7 +264,7 @@ describe('MoneyRequestReceiptView', () => { await waitForBatchedUpdatesWithAct(); expect(screen.getByLabelText(translateLocal('accessibilityHints.viewAttachment'))).toBeTruthy(); - expect(screen.getByLabelText(translateLocal('reportActionCompose.addAttachment'))).toBeTruthy(); + expect(screen.getByLabelText(translateLocal('receipt.addAdditionalReceipt'))).toBeTruthy(); }); it('shows action buttons when receipt is scanning', async () => { @@ -281,7 +281,7 @@ describe('MoneyRequestReceiptView', () => { await waitForBatchedUpdatesWithAct(); expect(screen.getByLabelText(translateLocal('accessibilityHints.viewAttachment'))).toBeTruthy(); - expect(screen.getByLabelText(translateLocal('reportActionCompose.addAttachment'))).toBeTruthy(); + expect(screen.getByLabelText(translateLocal('receipt.addAdditionalReceipt'))).toBeTruthy(); }); it('does not show action buttons in readonly mode', async () => { @@ -301,7 +301,7 @@ describe('MoneyRequestReceiptView', () => { await waitForBatchedUpdatesWithAct(); expect(screen.queryByLabelText(translateLocal('accessibilityHints.viewAttachment'))).toBeNull(); - expect(screen.queryByLabelText(translateLocal('reportActionCompose.addAttachment'))).toBeNull(); + expect(screen.queryByLabelText(translateLocal('receipt.addAdditionalReceipt'))).toBeNull(); }); }); }); From 031b56c7e59b96f7db778839c5b067c1da3d0e75 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 16 Mar 2026 11:37:39 +0300 Subject: [PATCH 11/15] fix: keep receipt action buttons visible when offline --- .../MoneyRequestReceiptView.tsx | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 0fe4b128ae4bd..7f63f959e4756 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -24,6 +24,7 @@ import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAct import useHover from '@hooks/useHover'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useOriginalReportID from '@hooks/useOriginalReportID'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -148,6 +149,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const theme = useTheme(); const ancestors = useAncestors(report); const {hovered, bind: hoverBind} = useHover(); + const {isOffline} = useNetwork(); const receiptContainerRef = useRef(null); const addButtonRef = useRef(null); const skipContainerMouseLeaveRef = useRef(false); @@ -372,6 +374,8 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const isMapDistanceRequest = !!transaction && isDistanceRequest && !isManualDistanceRequest(transaction); const canShowReceiptActions = hasReceipt && !isLoading && isEditable && !isMapDistanceRequest && !mergeTransactionID; + const receiptPendingAction = isDistanceRequest ? getPendingFieldAction('waypoints') : getPendingFieldAction('receipt'); + const isReceiptOfflinePending = isOffline && !!receiptPendingAction; const receiptAuditMessagesRow = ( @@ -430,7 +434,8 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, )} {(hasReceipt || !isEmptyObject(errors)) && ( { @@ -483,23 +488,25 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, hoverBind.onMouseLeave(); }} > - setIsLoading(false)} - onLoadFailure={() => setIsLoading(false)} - /> + + setIsLoading(false)} + onLoadFailure={() => setIsLoading(false)} + /> + {canShowReceiptActions && ( From 017a0d6fd5916ccc4223a5d7f696d2a9109372aa Mon Sep 17 00:00:00 2001 From: TaduJR Date: Mon, 16 Mar 2026 23:39:39 +0300 Subject: [PATCH 12/15] fix: reset stale hover states after file picker cancel and improve first-upload hover detection --- src/components/ReportActionItem/MoneyRequestReceiptView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 9150241329c19..5465dea373783 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -513,6 +513,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, onPress={() => { openPicker({ onPicked: (files) => { + skipContainerMouseLeaveRef.current = resetButtonHoverState(addButtonRef); validateFiles(files); }, onCanceled: () => { From 870f9e1fb96836f5ba590b276a6b3ffdd34ffeec Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 17 Mar 2026 09:41:30 +0300 Subject: [PATCH 13/15] fix: add early return for undefined reportID in notifyNewAction --- src/libs/actions/Report/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index f02c8897b258b..cf36b9f5f712b 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -644,6 +644,9 @@ function subscribeToNewActionEvent(reportID: string, callback: SubscriberCallbac /** Notify the ReportActionsView that a new comment has arrived */ function notifyNewAction(reportID: string | string[] | undefined, reportAction: ReportAction | undefined, isFromCurrentUser: boolean) { + if (!reportID) { + return; + } const ids = Array.isArray(reportID) ? reportID : [reportID]; for (const id of ids) { const actionSubscriber = newActionSubscribers.find((subscriber) => subscriber.reportID === id); From c20fc25b034a96a2747b215187f1556338a7d912 Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 17 Mar 2026 10:50:46 +0300 Subject: [PATCH 14/15] refactor: extract receipt hover utilities into platform-specific files --- .../MoneyRequestReceiptView.tsx | 31 +++++++++---------- .../receiptHoverUtils/index.native.ts | 8 +++++ .../receiptHoverUtils/index.ts | 16 ++++++++++ .../resetButtonHoverState/index.native.ts | 4 --- .../resetButtonHoverState/index.ts | 10 ------ 5 files changed, 39 insertions(+), 30 deletions(-) create mode 100644 src/components/ReportActionItem/receiptHoverUtils/index.native.ts create mode 100644 src/components/ReportActionItem/receiptHoverUtils/index.ts delete mode 100644 src/components/ReportActionItem/resetButtonHoverState/index.native.ts delete mode 100644 src/components/ReportActionItem/resetButtonHoverState/index.ts diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 5465dea373783..aeeae47ccd504 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -70,8 +70,8 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {TransactionPendingFieldsKey} from '@src/types/onyx/Transaction'; import type {FileObject} from '@src/types/utils/Attachment'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {isElementHovered, resetButtonHoverState} from './receiptHoverUtils'; import ReportActionItemImage from './ReportActionItemImage'; -import resetButtonHoverState from './resetButtonHoverState'; type MoneyRequestReceiptViewProps = { /** The report currently being looked at */ @@ -153,7 +153,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, const {isOffline} = useNetwork(); const receiptContainerRef = useRef(null); const addButtonRef = useRef(null); - const skipContainerMouseLeaveRef = useRef(false); + const [isPickerOpen, setIsPickerOpen] = useState(false); const deviceHasHoverSupport = hasHoverSupport(); const lazyIcons = useMemoizedLazyExpensifyIcons(['Expand', 'ReceiptPlus']); @@ -162,8 +162,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, if (isLoading) { return; } - const receiptElement = receiptContainerRef.current as unknown as HTMLElement | null; - if (receiptElement?.matches?.(':hover')) { + if (isElementHovered(receiptContainerRef)) { hoverBind.onMouseEnter(); } }, [isLoading, hoverBind]); @@ -476,13 +475,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, ref={receiptContainerRef} style={[styles.getMoneyRequestViewImage(showBorderlessLoading), receiptStyle, showBorderlessLoading && styles.flex1]} onMouseEnter={() => !isLoading && hoverBind.onMouseEnter()} - onMouseLeave={() => { - if (skipContainerMouseLeaveRef.current) { - skipContainerMouseLeaveRef.current = false; - return; - } - hoverBind.onMouseLeave(); - }} + onMouseLeave={hoverBind.onMouseLeave} > {canShowReceiptActions && ( - + {({openPicker}) => ( { + setIsPickerOpen(true); + resetButtonHoverState(addButtonRef); + const onPickerClosed = () => { + setIsPickerOpen(false); + if (isElementHovered(receiptContainerRef)) { + hoverBind.onMouseEnter(); + } + }; openPicker({ onPicked: (files) => { - skipContainerMouseLeaveRef.current = resetButtonHoverState(addButtonRef); + onPickerClosed(); validateFiles(files); }, - onCanceled: () => { - skipContainerMouseLeaveRef.current = resetButtonHoverState(addButtonRef); - }, + onCanceled: onPickerClosed, }); }} style={styles.receiptActionButton} diff --git a/src/components/ReportActionItem/receiptHoverUtils/index.native.ts b/src/components/ReportActionItem/receiptHoverUtils/index.native.ts new file mode 100644 index 0000000000000..a7564507ba3b2 --- /dev/null +++ b/src/components/ReportActionItem/receiptHoverUtils/index.native.ts @@ -0,0 +1,8 @@ +// No-op on native — hover states don't exist on mobile +function resetButtonHoverState() {} + +function isElementHovered(): boolean { + return false; +} + +export {resetButtonHoverState, isElementHovered}; diff --git a/src/components/ReportActionItem/receiptHoverUtils/index.ts b/src/components/ReportActionItem/receiptHoverUtils/index.ts new file mode 100644 index 0000000000000..6afbeb418a33a --- /dev/null +++ b/src/components/ReportActionItem/receiptHoverUtils/index.ts @@ -0,0 +1,16 @@ +import type {RefObject} from 'react'; +import type {View} from 'react-native'; + +/** Reset stale button hover/tooltip when file picker opens (browsers don't fire mouseleave). */ +function resetButtonHoverState(addButtonRef: RefObject) { + const buttonEl = addButtonRef.current as unknown as HTMLElement; + buttonEl?.dispatchEvent(new PointerEvent('pointerleave')); + buttonEl?.dispatchEvent(new MouseEvent('mouseout', {bubbles: true, relatedTarget: document.body})); +} + +/** Check if cursor is over the element (web only). */ +function isElementHovered(ref: RefObject): boolean { + return !!(ref.current as unknown as HTMLElement)?.matches?.(':hover'); +} + +export {resetButtonHoverState, isElementHovered}; diff --git a/src/components/ReportActionItem/resetButtonHoverState/index.native.ts b/src/components/ReportActionItem/resetButtonHoverState/index.native.ts deleted file mode 100644 index b5516364f58cf..0000000000000 --- a/src/components/ReportActionItem/resetButtonHoverState/index.native.ts +++ /dev/null @@ -1,4 +0,0 @@ -// No-op on native — hover states don't exist on mobile -export default function resetButtonHoverState(): boolean { - return false; -} diff --git a/src/components/ReportActionItem/resetButtonHoverState/index.ts b/src/components/ReportActionItem/resetButtonHoverState/index.ts deleted file mode 100644 index f40afcea6bd3a..0000000000000 --- a/src/components/ReportActionItem/resetButtonHoverState/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type {RefObject} from 'react'; -import type {View} from 'react-native'; - -/** Reset stale button hover/tooltip after file dialog dismiss (browsers don't fire mouseleave). */ -export default function resetButtonHoverState(addButtonRef: RefObject): boolean { - const buttonEl = addButtonRef.current as unknown as HTMLElement; - buttonEl?.dispatchEvent(new PointerEvent('pointerleave')); - buttonEl?.dispatchEvent(new MouseEvent('mouseout', {bubbles: true, relatedTarget: document.body})); - return true; -} From 03f024186cee0590b445c0193d4d511930c7d35b Mon Sep 17 00:00:00 2001 From: TaduJR Date: Tue, 17 Mar 2026 11:04:56 +0300 Subject: [PATCH 15/15] fix: center loading indicator in receipt image container --- src/components/ReportActionItem/MoneyRequestReceiptView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index aeeae47ccd504..93917404400dc 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -477,7 +477,7 @@ function MoneyRequestReceiptView({report, readonly = false, updatedTransaction, onMouseEnter={() => !isLoading && hoverBind.onMouseEnter()} onMouseLeave={hoverBind.onMouseLeave} > - +