From f9d17025447b868f642061cbbab1e516658f09c4 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Tue, 27 May 2025 13:30:59 +0200
Subject: [PATCH 01/35] refactor: AttachmentCarousel, extract common UI

---
 .../AttachmentCarouselView.tsx                | 61 ++++++++++++
 .../AttachmentCarousel/index.native.tsx       | 61 +++++-------
 .../Attachments/AttachmentCarousel/index.tsx  | 93 ++++++++-----------
 3 files changed, 120 insertions(+), 95 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
new file mode 100644
index 000000000000..40e9b3fb9011
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
@@ -0,0 +1,61 @@
+import React from 'react';
+import type {ReactNode} from 'react';
+import CarouselButtons from '@components/Attachments/AttachmentCarousel/CarouselButtons';
+import type {Attachment} from '@components/Attachments/types';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useLocalize from '@hooks/useLocalize';
+import variables from '@styles/variables';
+
+type AttachmentCarouselViewProps = {
+    /** Where the arrows should be visible */
+    shouldShowArrows: boolean;
+
+    /** The current page index */
+    page: number;
+
+    /** The attachments from the carousel */
+    attachments: Attachment[];
+
+    /** Callback for auto hiding carousel button arrows */
+    autoHideArrows: () => void;
+
+    /** Callback for cancelling auto hiding of carousel button arrows */
+    cancelAutoHideArrow: () => void;
+
+    /** Callback to cycle through attachments */
+    cycleThroughAttachments: (deltaSlide: number) => void;
+
+    /** children components */
+    children: ReactNode;
+};
+
+function AttachmentCarouselView({children, page, cycleThroughAttachments, attachments, shouldShowArrows, autoHideArrows, cancelAutoHideArrow}: AttachmentCarouselViewProps) {
+    const {translate} = useLocalize();
+
+    return page === -1 ? (
+        <BlockingView
+            icon={Illustrations.ToddBehindCloud}
+            iconWidth={variables.modalTopIconWidth}
+            iconHeight={variables.modalTopIconHeight}
+            title={translate('notFound.notHere')}
+        />
+    ) : (
+        <>
+            <CarouselButtons
+                shouldShowArrows={shouldShowArrows}
+                page={page}
+                attachments={attachments}
+                onBack={() => cycleThroughAttachments(-1)}
+                onForward={() => cycleThroughAttachments(1)}
+                autoHideArrow={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrow}
+            />
+            {children}
+        </>
+    );
+}
+
+AttachmentCarouselView.displayName = 'AttachmentCarouselView';
+
+export default AttachmentCarouselView;
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
index a625b4215d52..bd8f89e5953a 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -2,16 +2,12 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
 import {Keyboard, View} from 'react-native';
 import {useOnyx} from 'react-native-onyx';
 import type {Attachment, AttachmentSource} from '@components/Attachments/types';
-import BlockingView from '@components/BlockingViews/BlockingView';
 import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import * as Illustrations from '@components/Icon/Illustrations';
-import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
-import variables from '@styles/variables';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import CarouselButtons from './CarouselButtons';
+import AttachmentCarouselView from './AttachmentCarouselView/AttachmentCarouselView';
 import extractAttachments from './extractAttachments';
 import type {AttachmentCarouselPagerHandle} from './Pager';
 import AttachmentCarouselPager from './Pager';
@@ -20,7 +16,6 @@ import useCarouselArrows from './useCarouselArrows';
 
 function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownloadButtonVisibility, onClose, type, accountID, onAttachmentError}: AttachmentCarouselProps) {
     const styles = useThemeStyles();
-    const {translate} = useLocalize();
     const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
     const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {canEvict: false, canBeMissing: true});
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, canBeMissing: true});
@@ -121,40 +116,28 @@ function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownlo
     }
 
     return (
-        <View style={containerStyles}>
-            {page === -1 ? (
-                <BlockingView
-                    icon={Illustrations.ToddBehindCloud}
-                    iconWidth={variables.modalTopIconWidth}
-                    iconHeight={variables.modalTopIconHeight}
-                    title={translate('notFound.notHere')}
+        <AttachmentCarouselView
+            cycleThroughAttachments={cycleThroughAttachments}
+            page={page}
+            attachments={attachments}
+            shouldShowArrows={shouldShowArrows}
+            autoHideArrows={autoHideArrows}
+            cancelAutoHideArrow={cancelAutoHideArrows}
+        >
+            <View style={containerStyles}>
+                <AttachmentCarouselPager
+                    items={attachments}
+                    initialPage={page}
+                    onAttachmentError={onAttachmentError}
+                    activeAttachmentID={activeAttachmentID}
+                    setShouldShowArrows={setShouldShowArrows}
+                    onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
+                    onClose={onClose}
+                    ref={pagerRef}
+                    reportID={report.reportID}
                 />
-            ) : (
-                <>
-                    <CarouselButtons
-                        shouldShowArrows={shouldShowArrows}
-                        page={page}
-                        attachments={attachments}
-                        onBack={() => cycleThroughAttachments(-1)}
-                        onForward={() => cycleThroughAttachments(1)}
-                        autoHideArrow={autoHideArrows}
-                        cancelAutoHideArrow={cancelAutoHideArrows}
-                    />
-
-                    <AttachmentCarouselPager
-                        items={attachments}
-                        initialPage={page}
-                        onAttachmentError={onAttachmentError}
-                        activeAttachmentID={activeAttachmentID}
-                        setShouldShowArrows={setShouldShowArrows}
-                        onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                        onClose={onClose}
-                        ref={pagerRef}
-                        reportID={report.reportID}
-                    />
-                </>
-            )}
-        </View>
+            </View>
+        </AttachmentCarouselView>
     );
 }
 
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index 3c883530df4f..58957098bcf3 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -7,22 +7,17 @@ import type {ComposedGesture, GestureType} from 'react-native-gesture-handler';
 import {Gesture, GestureDetector} from 'react-native-gesture-handler';
 import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated';
 import type {Attachment, AttachmentSource} from '@components/Attachments/types';
-import BlockingView from '@components/BlockingViews/BlockingView';
-import {ToddBehindCloud} from '@components/Icon/Illustrations';
 import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
-import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import useWindowDimensions from '@hooks/useWindowDimensions';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
 import Navigation from '@libs/Navigation/Navigation';
-import variables from '@styles/variables';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
+import AttachmentCarouselView from './AttachmentCarouselView/AttachmentCarouselView';
 import CarouselActions from './CarouselActions';
-import CarouselButtons from './CarouselButtons';
 import CarouselItem from './CarouselItem';
 import extractAttachments from './extractAttachments';
 import AttachmentCarouselPagerContext from './Pager/AttachmentCarouselPagerContext';
@@ -52,8 +47,6 @@ function DeviceAwareGestureDetector({canUseTouchScreen, gesture, children}: Devi
 }
 
 function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose, attachmentLink, onAttachmentError}: AttachmentCarouselProps) {
-    const theme = useTheme();
-    const {translate} = useLocalize();
     const {windowWidth} = useWindowDimensions();
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const styles = useThemeStyles();
@@ -294,54 +287,42 @@ function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownlo
             onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
             onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
         >
-            {page === -1 ? (
-                <BlockingView
-                    icon={ToddBehindCloud}
-                    iconColor={theme.offline}
-                    iconWidth={variables.modalTopIconWidth}
-                    iconHeight={variables.modalTopIconHeight}
-                    title={translate('notFound.notHere')}
-                />
-            ) : (
-                <>
-                    <CarouselButtons
-                        shouldShowArrows={shouldShowArrows}
-                        page={page}
-                        attachments={attachments}
-                        onBack={() => cycleThroughAttachments(-1)}
-                        onForward={() => cycleThroughAttachments(1)}
-                        autoHideArrow={autoHideArrows}
-                        cancelAutoHideArrow={cancelAutoHideArrows}
-                    />
-                    <AttachmentCarouselPagerContext.Provider value={context}>
-                        <DeviceAwareGestureDetector
-                            canUseTouchScreen={canUseTouchScreen}
-                            gesture={pan}
-                        >
-                            <Animated.FlatList
-                                keyboardShouldPersistTaps="handled"
-                                horizontal
-                                showsHorizontalScrollIndicator={false}
-                                // scrolling is controlled by the pan gesture
-                                scrollEnabled={false}
-                                ref={scrollRef}
-                                initialScrollIndex={page}
-                                initialNumToRender={3}
-                                windowSize={5}
-                                maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
-                                data={attachments}
-                                renderItem={renderItem}
-                                getItemLayout={getItemLayout}
-                                keyExtractor={extractItemKey}
-                                viewabilityConfig={viewabilityConfig}
-                                onViewableItemsChanged={updatePage}
-                            />
-                        </DeviceAwareGestureDetector>
-                    </AttachmentCarouselPagerContext.Provider>
-
-                    <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
-                </>
-            )}
+            <AttachmentCarouselView
+                cycleThroughAttachments={cycleThroughAttachments}
+                page={page}
+                attachments={attachments}
+                shouldShowArrows={shouldShowArrows}
+                autoHideArrows={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrows}
+            >
+                <AttachmentCarouselPagerContext.Provider value={context}>
+                    <DeviceAwareGestureDetector
+                        canUseTouchScreen={canUseTouchScreen}
+                        gesture={pan}
+                    >
+                        <Animated.FlatList
+                            keyboardShouldPersistTaps="handled"
+                            horizontal
+                            showsHorizontalScrollIndicator={false}
+                            // scrolling is controlled by the pan gesture
+                            scrollEnabled={false}
+                            ref={scrollRef}
+                            initialScrollIndex={page}
+                            initialNumToRender={3}
+                            windowSize={5}
+                            maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
+                            data={attachments}
+                            renderItem={renderItem}
+                            getItemLayout={getItemLayout}
+                            keyExtractor={extractItemKey}
+                            viewabilityConfig={viewabilityConfig}
+                            onViewableItemsChanged={updatePage}
+                        />
+                    </DeviceAwareGestureDetector>
+                </AttachmentCarouselPagerContext.Provider>
+
+                <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
+            </AttachmentCarouselView>
         </View>
     );
 }

From 21096e9654b63cf733f5996375db5e3dcbbeba60 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 29 May 2025 16:02:21 +0200
Subject: [PATCH 02/35] add: basic navigation setup for receipt view modal

---
 src/ROUTES.ts                                                | 5 +++++
 src/SCREENS.ts                                               | 1 +
 .../Navigation/AppNavigator/ModalStackNavigators/index.tsx   | 1 +
 src/libs/Navigation/linkingConfig/config.ts                  | 1 +
 src/libs/Navigation/types.ts                                 | 5 +++++
 5 files changed, 13 insertions(+)

diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 8dcb10af3ce5..bbba3e399001 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -906,6 +906,11 @@ const ROUTES = {
             `create/${iouType as string}/start/${transactionID}/${reportID}/per-diem/${backToReport ?? ''}` as const,
     },
 
+    MONEY_REQUEST_RECEIPT_VIEW_MODAL: {
+        route: 'receipt-view-modal',
+        getRoute: () => `receipt-view-modal`,
+    },
+
     MONEY_REQUEST_STATE_SELECTOR: {
         route: 'submit/state',
 
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index de58e3edcf3e..ffd79165510c 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -246,6 +246,7 @@ const SCREENS = {
         WAYPOINT: 'Money_Request_Waypoint',
         EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
         RECEIPT: 'Money_Request_Receipt',
+        RECEIPT_VIEW_MODAL: 'Money_Request_Receipt_View_Modal',
         STATE_SELECTOR: 'Money_Request_State_Selector',
         STEP_ATTENDEES: 'Money_Request_Attendee',
         STEP_ACCOUNTANT: 'Money_Request_Accountant',
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index f5d1b6ee8c2c..4fc4acb5d891 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -124,6 +124,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator<MoneyRequestNa
     [SCREENS.MONEY_REQUEST.STEP_DESTINATION_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepDestination').default,
     [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepTime').default,
     [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepSubrate').default,
+    [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepScan/ReceiptViewModal').default,
 });
 
 const TravelModalStackNavigator = createModalStackNavigator<TravelNavigatorParamList>({
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index a767df53efa1..35195d99ebcd 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1321,6 +1321,7 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
                         [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route,
                         [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route,
                         [SCREENS.MONEY_REQUEST.STEP_SCAN]: ROUTES.MONEY_REQUEST_STEP_SCAN.route,
+                        [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.route,
                         [SCREENS.MONEY_REQUEST.STEP_TAG]: ROUTES.MONEY_REQUEST_STEP_TAG.route,
                         [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: ROUTES.MONEY_REQUEST_STEP_WAYPOINT.route,
                         [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 87423aa671ba..90c846e3a1af 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1320,6 +1320,11 @@ type MoneyRequestNavigatorParamList = {
         backTo: Routes;
         backToReport?: string;
     };
+    [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: {
+        transactionID?: string;
+        reportID?: string;
+        backTo?: Routes;
+    };
     [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
         action: IOUAction;
         iouType: IOUType;

From 7b60797defbe02e844db833e12ecd635f07a96f1 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 29 May 2025 17:47:08 +0200
Subject: [PATCH 03/35] add: draft ReceiptViewModal component and setup

---
 .../AttachmentCarousel/index.native.tsx       |  22 +--
 src/components/Attachments/types.ts           |   6 +-
 .../ReceiptViewModal/index.tsx                | 144 ++++++++++++++++++
 .../step/IOURequestStepScan/index.native.tsx  |   4 +
 4 files changed, 162 insertions(+), 14 deletions(-)
 create mode 100644 src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx

diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
index bd8f89e5953a..1d2957239a3b 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -116,15 +116,15 @@ function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownlo
     }
 
     return (
-        <AttachmentCarouselView
-            cycleThroughAttachments={cycleThroughAttachments}
-            page={page}
-            attachments={attachments}
-            shouldShowArrows={shouldShowArrows}
-            autoHideArrows={autoHideArrows}
-            cancelAutoHideArrow={cancelAutoHideArrows}
-        >
-            <View style={containerStyles}>
+        <View style={containerStyles}>
+            <AttachmentCarouselView
+                cycleThroughAttachments={cycleThroughAttachments}
+                page={page}
+                attachments={attachments}
+                shouldShowArrows={shouldShowArrows}
+                autoHideArrows={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrows}
+            >
                 <AttachmentCarouselPager
                     items={attachments}
                     initialPage={page}
@@ -136,8 +136,8 @@ function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownlo
                     ref={pagerRef}
                     reportID={report.reportID}
                 />
-            </View>
-        </AttachmentCarouselView>
+            </AttachmentCarouselView>
+        </View>
     );
 }
 
diff --git a/src/components/Attachments/types.ts b/src/components/Attachments/types.ts
index f394799dfce4..cc431813ef72 100644
--- a/src/components/Attachments/types.ts
+++ b/src/components/Attachments/types.ts
@@ -4,6 +4,9 @@ import type IconAsset from '@src/types/utils/IconAsset';
 type AttachmentSource = string | IconAsset | number;
 
 type Attachment = {
+    /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
+    source: AttachmentSource;
+
     /** Report action ID of the attachment */
     reportActionID?: string;
 
@@ -13,9 +16,6 @@ type Attachment = {
     /** Whether source url requires authentication */
     isAuthTokenRequired?: boolean;
 
-    /** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
-    source: AttachmentSource;
-
     /** URL to preview-sized attachment that is also used for the thumbnail */
     previewSource?: AttachmentSource;
 
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
new file mode 100644
index 000000000000..6ebea5c3901d
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -0,0 +1,144 @@
+import React, {useCallback, useRef, useState} from 'react';
+import {Keyboard, View} from 'react-native';
+import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
+import type {AttachmentCarouselPagerHandle} from '@components/Attachments/AttachmentCarousel/Pager';
+import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
+import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import Modal from '@components/Modal';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+
+type ReceiptViewModalProps = {
+    /** An array of receipt image source URLs to display in the modal. */
+    sources: Attachment[];
+    /** The index of the currently selected receipt in the sources array. */
+    selectedIndex: number;
+    /** Callback function invoked when a receipt is deleted. */
+    onDelete: () => void;
+};
+
+const mockAttachments: Attachment[] = [
+    {
+        source: 'https://picsum.photos/200/300',
+        attachmentID: '1',
+    },
+    {
+        source: 'https://picsum.photos/200/300',
+        attachmentID: '2',
+    },
+    {
+        source: 'https://picsum.photos/200/300',
+        attachmentID: '3',
+    },
+];
+
+function ReceiptViewModal({
+    sources = mockAttachments,
+    selectedIndex = 0,
+    onDelete = () => {},
+}: // route: {
+//     params: {action, iouType, reportID, transactionID: initialTransactionID, backTo, backToReport},
+// },
+ReceiptViewModalProps) {
+    // const [currentIndex, setCurrentIndex] = useState(selectedIndex);
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const [isOpen, setIsOpen] = useState(true);
+    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(selectedIndex);
+    const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+    const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
+
+    // const onCycleThroughAttachments = (deltaSlide: number) => {
+    //     const newIndex = (currentIndex + deltaSlide + sources.length) % sources.length;
+    //     setCurrentIndex(newIndex);
+    // };
+
+    const [page, setPage] = useState<number>(0);
+
+    const updatePage = useCallback(
+        (newPageIndex: number) => {
+            Keyboard.dismiss();
+            setShouldShowArrows(true);
+
+            const item = sources.at(newPageIndex);
+
+            setPage(newPageIndex);
+            if (newPageIndex >= 0 && item) {
+                setActiveAttachmentID(item.attachmentID ?? item.source);
+                // if (onNavigate) {
+                //     onNavigate(item);
+                // }
+                // onNavigate?.(item);
+            }
+        },
+        [setShouldShowArrows, sources],
+    );
+
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide: number) => {
+            if (page === undefined) {
+                return;
+            }
+            const nextPageIndex = page + deltaSlide;
+            updatePage(nextPageIndex);
+            pagerRef.current?.setPage(nextPageIndex);
+
+            autoHideArrows();
+        },
+        [autoHideArrows, page, updatePage],
+    );
+
+    const containerStyles = [styles.flex1, styles.attachmentCarouselContainer];
+
+    return (
+        <Modal
+            type="centered"
+            isVisible={isOpen}
+            onClose={() => {
+                setIsOpen(false);
+                Navigation.goBack();
+            }}
+        >
+            <HeaderWithBackButton
+                title={translate('common.receipt')}
+                onBackButtonPress={Navigation.goBack}
+                shouldShowThreeDotsButton
+                threeDotsMenuIcon={Expensicons.Trashcan}
+                onThreeDotsButtonPress={onDelete}
+            />
+
+            <View style={containerStyles}>
+                <AttachmentCarouselView
+                    cycleThroughAttachments={cycleThroughAttachments}
+                    page={page}
+                    attachments={sources}
+                    shouldShowArrows={shouldShowArrows}
+                    autoHideArrows={autoHideArrows}
+                    cancelAutoHideArrow={cancelAutoHideArrows}
+                >
+                    <AttachmentCarouselPager
+                        items={sources}
+                        initialPage={page}
+                        activeAttachmentID={activeAttachmentID}
+                        setShouldShowArrows={setShouldShowArrows}
+                        onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
+                        onClose={() => {}}
+                        ref={pagerRef}
+                    />
+                </AttachmentCarouselView>
+            </View>
+        </Modal>
+    );
+}
+
+ReceiptViewModal.displayName = 'ReceiptViewModal';
+
+export default ReceiptViewModal;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index 03e1e4c860af..0cc9ba898cdd 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -859,6 +859,10 @@ function IOURequestStepScan({
                         }}
                     />
                 )}
+                <Button
+                    onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute())}
+                    text="PRESS"
+                />
             </View>
         </StepScreenWrapper>
     );

From c246819812343fc63a2cf79d2ac898302d274e12 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Fri, 30 May 2025 17:25:32 +0200
Subject: [PATCH 04/35] change: ReceiptViewModal to depend on local state
 instead of navigation to operate

---
 src/ROUTES.ts                                 |  5 --
 src/SCREENS.ts                                |  1 -
 .../ModalStackNavigators/index.tsx            |  1 -
 src/libs/Navigation/linkingConfig/config.ts   |  1 -
 src/libs/Navigation/types.ts                  |  5 --
 .../ReceiptViewModal/index.tsx                | 82 +++++++------------
 .../step/IOURequestStepScan/index.native.tsx  | 33 +++++++-
 7 files changed, 63 insertions(+), 65 deletions(-)

diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index bbba3e399001..8dcb10af3ce5 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -906,11 +906,6 @@ const ROUTES = {
             `create/${iouType as string}/start/${transactionID}/${reportID}/per-diem/${backToReport ?? ''}` as const,
     },
 
-    MONEY_REQUEST_RECEIPT_VIEW_MODAL: {
-        route: 'receipt-view-modal',
-        getRoute: () => `receipt-view-modal`,
-    },
-
     MONEY_REQUEST_STATE_SELECTOR: {
         route: 'submit/state',
 
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index ffd79165510c..de58e3edcf3e 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -246,7 +246,6 @@ const SCREENS = {
         WAYPOINT: 'Money_Request_Waypoint',
         EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
         RECEIPT: 'Money_Request_Receipt',
-        RECEIPT_VIEW_MODAL: 'Money_Request_Receipt_View_Modal',
         STATE_SELECTOR: 'Money_Request_State_Selector',
         STEP_ATTENDEES: 'Money_Request_Attendee',
         STEP_ACCOUNTANT: 'Money_Request_Accountant',
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 4fc4acb5d891..f5d1b6ee8c2c 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -124,7 +124,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator<MoneyRequestNa
     [SCREENS.MONEY_REQUEST.STEP_DESTINATION_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepDestination').default,
     [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepTime').default,
     [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepSubrate').default,
-    [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepScan/ReceiptViewModal').default,
 });
 
 const TravelModalStackNavigator = createModalStackNavigator<TravelNavigatorParamList>({
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 35195d99ebcd..a767df53efa1 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1321,7 +1321,6 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
                         [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route,
                         [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route,
                         [SCREENS.MONEY_REQUEST.STEP_SCAN]: ROUTES.MONEY_REQUEST_STEP_SCAN.route,
-                        [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.route,
                         [SCREENS.MONEY_REQUEST.STEP_TAG]: ROUTES.MONEY_REQUEST_STEP_TAG.route,
                         [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: ROUTES.MONEY_REQUEST_STEP_WAYPOINT.route,
                         [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 90c846e3a1af..87423aa671ba 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1320,11 +1320,6 @@ type MoneyRequestNavigatorParamList = {
         backTo: Routes;
         backToReport?: string;
     };
-    [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: {
-        transactionID?: string;
-        reportID?: string;
-        backTo?: Routes;
-    };
     [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
         action: IOUAction;
         iouType: IOUType;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 6ebea5c3901d..6561c672f7ec 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -4,60 +4,49 @@ import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/A
 import type {AttachmentCarouselPagerHandle} from '@components/Attachments/AttachmentCarousel/Pager';
 import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
-import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import type {AttachmentSource} from '@components/Attachments/types';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Modal from '@components/Modal';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
+import type {Receipt} from '@src/types/onyx/Transaction';
+
+type ReceiptSource = Receipt & {
+    source: AttachmentSource;
+    receiptID: number;
+};
 
 type ReceiptViewModalProps = {
+    isOpen: boolean;
     /** An array of receipt image source URLs to display in the modal. */
-    sources: Attachment[];
+    sources: ReceiptSource[];
     /** The index of the currently selected receipt in the sources array. */
     selectedIndex: number;
-    /** Callback function invoked when a receipt is deleted. */
-    onDelete: () => void;
+    /** Callback to delete a receipt */
+    onDelete: (index: number) => void;
+    /** Callback to close modal */
+    onClose: () => void;
 };
 
-const mockAttachments: Attachment[] = [
-    {
-        source: 'https://picsum.photos/200/300',
-        attachmentID: '1',
-    },
-    {
-        source: 'https://picsum.photos/200/300',
-        attachmentID: '2',
-    },
-    {
-        source: 'https://picsum.photos/200/300',
-        attachmentID: '3',
-    },
-];
-
-function ReceiptViewModal({
-    sources = mockAttachments,
-    selectedIndex = 0,
-    onDelete = () => {},
-}: // route: {
-//     params: {action, iouType, reportID, transactionID: initialTransactionID, backTo, backToReport},
-// },
-ReceiptViewModalProps) {
-    // const [currentIndex, setCurrentIndex] = useState(selectedIndex);
+function ReceiptViewModal({sources, isOpen, selectedIndex, onDelete, onClose}: ReceiptViewModalProps) {
     const {translate} = useLocalize();
     const styles = useThemeStyles();
-    const [isOpen, setIsOpen] = useState(true);
-    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(selectedIndex);
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
     const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
 
-    // const onCycleThroughAttachments = (deltaSlide: number) => {
-    //     const newIndex = (currentIndex + deltaSlide + sources.length) % sources.length;
-    //     setCurrentIndex(newIndex);
-    // };
+    const [page, setPage] = useState<number>(selectedIndex);
+    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(sources.at(selectedIndex)?.receiptID ?? sources.at(selectedIndex)?.source ?? 0);
 
-    const [page, setPage] = useState<number>(0);
+    const handleDelete = useCallback(() => {
+        onDelete(page);
+
+        if (!sources.length) {
+            onClose();
+        } else if (page >= sources.length - 1) {
+            setPage(page - 1);
+        }
+    }, [onDelete, onClose, sources.length, page]);
 
     const updatePage = useCallback(
         (newPageIndex: number) => {
@@ -68,11 +57,7 @@ ReceiptViewModalProps) {
 
             setPage(newPageIndex);
             if (newPageIndex >= 0 && item) {
-                setActiveAttachmentID(item.attachmentID ?? item.source);
-                // if (onNavigate) {
-                //     onNavigate(item);
-                // }
-                // onNavigate?.(item);
+                setActiveAttachmentID(item.receiptID ?? item.source);
             }
         },
         [setShouldShowArrows, sources],
@@ -96,26 +81,21 @@ ReceiptViewModalProps) {
         [autoHideArrows, page, updatePage],
     );
 
-    const containerStyles = [styles.flex1, styles.attachmentCarouselContainer];
-
     return (
         <Modal
             type="centered"
             isVisible={isOpen}
-            onClose={() => {
-                setIsOpen(false);
-                Navigation.goBack();
-            }}
+            onClose={onClose}
         >
             <HeaderWithBackButton
                 title={translate('common.receipt')}
-                onBackButtonPress={Navigation.goBack}
+                onBackButtonPress={onClose}
                 shouldShowThreeDotsButton
                 threeDotsMenuIcon={Expensicons.Trashcan}
-                onThreeDotsButtonPress={onDelete}
+                onThreeDotsButtonPress={handleDelete}
             />
 
-            <View style={containerStyles}>
+            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
                 <AttachmentCarouselView
                     cycleThroughAttachments={cycleThroughAttachments}
                     page={page}
@@ -130,7 +110,7 @@ ReceiptViewModalProps) {
                         activeAttachmentID={activeAttachmentID}
                         setShouldShowArrows={setShouldShowArrows}
                         onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                        onClose={() => {}}
+                        onClose={onClose}
                         ref={pagerRef}
                     />
                 </AttachmentCarouselView>
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index 0cc9ba898cdd..4d02fb43d641 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -75,9 +75,26 @@ import type Transaction from '@src/types/onyx/Transaction';
 import type {Receipt} from '@src/types/onyx/Transaction';
 import CameraPermission from './CameraPermission';
 import NavigationAwareCamera from './NavigationAwareCamera/Camera';
+import ReceiptViewModal from './ReceiptViewModal';
 import type IOURequestStepScanProps from './types';
 import type {ReceiptFile} from './types';
 
+// TODO: remove this
+const mockAttachments = [
+    {
+        source: 'https://picsum.photos/200/300',
+        receiptID: 1,
+    },
+    {
+        source: 'https://picsum.photos/200/300',
+        receiptID: 2,
+    },
+    {
+        source: 'https://picsum.photos/200/300',
+        receiptID: 3,
+    },
+];
+
 function IOURequestStepScan({
     report,
     route: {
@@ -94,6 +111,13 @@ function IOURequestStepScan({
         physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
     });
 
+    const [isReceiptModalVisible, setIsReceiptModalVisible] = useState(false);
+    const [currentSelectedIndex, setCurrentSelectedIndex] = useState(0);
+
+    const handleDeleteReceipt = useCallback((index: number) => {
+        // Delete the receipt at the specified index
+    }, []);
+
     const [elementTop, setElementTop] = useState(0);
     const isEditing = action === CONST.IOU.ACTION.EDIT;
     const hasFlash = !!device?.hasFlash;
@@ -859,8 +883,15 @@ function IOURequestStepScan({
                         }}
                     />
                 )}
+                <ReceiptViewModal
+                    sources={mockAttachments}
+                    isOpen={isReceiptModalVisible}
+                    selectedIndex={currentSelectedIndex}
+                    onClose={() => setIsReceiptModalVisible(false)}
+                    onDelete={handleDeleteReceipt}
+                />
                 <Button
-                    onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute())}
+                    onPress={() => setIsReceiptModalVisible(true)}
                     text="PRESS"
                 />
             </View>

From bff82d4f88ad5ef4cf367a4a2df917942f4dc015 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Mon, 2 Jun 2025 09:56:26 +0200
Subject: [PATCH 05/35] chore: adjustments to slack replies, general clean up

---
 .../ReceiptPreviews/index.tsx                 |  8 ++++--
 .../ReceiptViewModal/index.tsx                | 15 ++++------
 .../step/IOURequestStepScan/index.native.tsx  | 28 ++++---------------
 3 files changed, 15 insertions(+), 36 deletions(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
index 6e9a97173c1e..3c36bbea28a8 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
@@ -27,11 +27,14 @@ type ReceiptPreviewsProps = {
     /** If the receipts preview should be shown */
     isMultiScanEnabled: boolean;
 
+    /** Callback to open the receipt view modal */
+    onOpenModal: () => void;
+
     /** Method to disable swipe between tabs */
     setTabSwipeDisabled?: (isDisabled: boolean) => void;
 };
 
-function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: ReceiptPreviewsProps) {
+function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled, onOpenModal}: ReceiptPreviewsProps) {
     const styles = useThemeStyles();
     const theme = useTheme();
     const {translate} = useLocalize();
@@ -91,8 +94,7 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: Rece
                 accessible
                 accessibilityLabel={translate('common.receipt')}
                 accessibilityRole={CONST.ROLE.BUTTON}
-                // TODO: open ReceiptViewModal when implemented https://github.com/Expensify/App/issues/61182
-                onPress={() => {}}
+                onPress={onOpenModal}
             >
                 <Image
                     source={{uri: item.source}}
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 6561c672f7ec..cc2575b6abdd 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -12,15 +12,15 @@ import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import type {Receipt} from '@src/types/onyx/Transaction';
 
-type ReceiptSource = Receipt & {
+type ReceiptWithSource = Receipt & {
     source: AttachmentSource;
-    receiptID: number;
 };
 
 type ReceiptViewModalProps = {
+    /** Whether the modal is open or not */
     isOpen: boolean;
     /** An array of receipt image source URLs to display in the modal. */
-    sources: ReceiptSource[];
+    sources: ReceiptWithSource[];
     /** The index of the currently selected receipt in the sources array. */
     selectedIndex: number;
     /** Callback to delete a receipt */
@@ -40,13 +40,8 @@ function ReceiptViewModal({sources, isOpen, selectedIndex, onDelete, onClose}: R
 
     const handleDelete = useCallback(() => {
         onDelete(page);
-
-        if (!sources.length) {
-            onClose();
-        } else if (page >= sources.length - 1) {
-            setPage(page - 1);
-        }
-    }, [onDelete, onClose, sources.length, page]);
+        onClose();
+    }, [onDelete, onClose, page]);
 
     const updatePage = useCallback(
         (newPageIndex: number) => {
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index a9ac80f28a5f..f1ea5d1fc794 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -79,25 +79,10 @@ import type {Receipt} from '@src/types/onyx/Transaction';
 import CameraPermission from './CameraPermission';
 import NavigationAwareCamera from './NavigationAwareCamera/Camera';
 import ReceiptPreviews from './ReceiptPreviews';
+import ReceiptViewModal from './ReceiptViewModal';
 import type IOURequestStepScanProps from './types';
 import type {ReceiptFile} from './types';
 
-// TODO: remove this
-const mockAttachments = [
-    {
-        source: 'https://picsum.photos/200/300',
-        receiptID: 1,
-    },
-    {
-        source: 'https://picsum.photos/200/300',
-        receiptID: 2,
-    },
-    {
-        source: 'https://picsum.photos/200/300',
-        receiptID: 3,
-    },
-];
-
 function IOURequestStepScan({
     report,
     route: {
@@ -129,7 +114,7 @@ function IOURequestStepScan({
     const [flash, setFlash] = useState(false);
     // TODO: remove when multi-scan functionality is removed from beta
     const {canUseMultiScan: canUseMultiScanBeta} = usePermissions();
-    const canUseMultiScan = canUseMultiScanBeta && !isEditing && iouType !== CONST.IOU.TYPE.SPLIT;
+    const canUseMultiScan = true;
     const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);
     const [receiptFiles, setReceiptFiles] = useState<ReceiptFile[]>([]);
     const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true});
@@ -931,9 +916,10 @@ function IOURequestStepScan({
                 </View>
 
                 <ReceiptPreviews
-                    isMultiScanEnabled={isMultiScanEnabled}
+                    isMultiScanEnabled
                     submit={submitReceipts}
                     setTabSwipeDisabled={setTabSwipeDisabled}
+                    onOpenModal={() => setIsReceiptModalVisible(true)}
                 />
 
                 {startLocationPermissionFlow && !!receiptFiles.length && (
@@ -948,16 +934,12 @@ function IOURequestStepScan({
                     />
                 )}
                 <ReceiptViewModal
-                    sources={mockAttachments}
+                    sources={receiptFiles}
                     isOpen={isReceiptModalVisible}
                     selectedIndex={currentSelectedIndex}
                     onClose={() => setIsReceiptModalVisible(false)}
                     onDelete={handleDeleteReceipt}
                 />
-                <Button
-                    onPress={() => setIsReceiptModalVisible(true)}
-                    text="PRESS"
-                />
             </View>
         </StepScreenWrapper>
     );

From d9084733e8cb8439acc29fbcba4adafc27bea53b Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Mon, 2 Jun 2025 21:03:43 +0200
Subject: [PATCH 06/35] change: reintroduced navigation with params due to
 mobile web requirements

---
 src/ROUTES.ts                                 |   5 +
 src/SCREENS.ts                                |   1 +
 .../ModalStackNavigators/index.tsx            |   1 +
 src/libs/Navigation/linkingConfig/config.ts   |   1 +
 src/libs/Navigation/types.ts                  |   4 +
 .../ReceiptPreviews/index.tsx                 |  13 +-
 .../ReceiptViewModal/index.tsx                | 124 ++++++------------
 .../step/IOURequestStepScan/index.native.tsx  |  21 +--
 8 files changed, 61 insertions(+), 109 deletions(-)

diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 1d6937c1ff7c..946d32ecb1b6 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -924,6 +924,11 @@ const ROUTES = {
             `create/${iouType as string}/start/${transactionID}/${reportID}/per-diem/${backToReport ?? ''}` as const,
     },
 
+    MONEY_REQUEST_RECEIPT_VIEW_MODAL: {
+        route: 'receipt-view-modal/:transactionID/:reportID',
+        getRoute: (transactionID: string, reportID: string) => `receipt-view-modal/${transactionID}/${reportID}` as const,
+    },
+
     MONEY_REQUEST_STATE_SELECTOR: {
         route: 'submit/state',
 
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 96344091d4a8..68267c9975ec 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -246,6 +246,7 @@ const SCREENS = {
         STEP_WAYPOINT: 'Money_Request_Step_Waypoint',
         STEP_TAX_AMOUNT: 'Money_Request_Step_Tax_Amount',
         STEP_TAX_RATE: 'Money_Request_Step_Tax_Rate',
+        RECEIPT_VIEW_MODAL: 'Money_Request_Receipt_View_Modal',
         STEP_SPLIT_PAYER: 'Money_Request_Step_Split_Payer',
         STEP_SEND_FROM: 'Money_Request_Step_Send_From',
         STEP_COMPANY_INFO: 'Money_Request_Step_Company_Info',
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index a7f1c14f5e14..a9aff2af9e11 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -124,6 +124,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator<MoneyRequestNa
     [SCREENS.MONEY_REQUEST.STEP_DESTINATION_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepDestination').default,
     [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepTime').default,
     [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepSubrate').default,
+    [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: () => require<ReactComponentModule>('../../../../pages/iou/request/step/IOURequestStepScan/ReceiptViewModal').default,
     [SCREENS.MONEY_REQUEST.SPLIT_EXPENSE]: () => require<ReactComponentModule>('../../../../pages/iou/SplitExpensePage').default,
     [SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT]: () => require<ReactComponentModule>('../../../../pages/iou/SplitExpenseEditPage').default,
 });
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index b27ff3d6ebd4..ed077371134a 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1321,6 +1321,7 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
                         [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route,
                         [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route,
                         [SCREENS.MONEY_REQUEST.STEP_SCAN]: ROUTES.MONEY_REQUEST_STEP_SCAN.route,
+                        [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.route,
                         [SCREENS.MONEY_REQUEST.STEP_TAG]: ROUTES.MONEY_REQUEST_STEP_TAG.route,
                         [SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: ROUTES.MONEY_REQUEST_STEP_WAYPOINT.route,
                         [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 1a830a3aa79c..f21c12cec87c 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1318,6 +1318,10 @@ type MoneyRequestNavigatorParamList = {
         backTo: Routes;
         backToReport?: string;
     };
+    [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: {
+        transactionID: string;
+        reportID: string;
+    };
     [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
         action: IOUAction;
         iouType: IOUType;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
index 3c36bbea28a8..a31f576029e2 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
@@ -13,8 +13,10 @@ import usePrevious from '@hooks/usePrevious';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import useWindowDimensions from '@hooks/useWindowDimensions';
+import Navigation from '@libs/Navigation/Navigation';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
 import type {Receipt} from '@src/types/onyx/Transaction';
 import SubmitButtonShadow from './SubmitButtonShadow';
 
@@ -27,14 +29,11 @@ type ReceiptPreviewsProps = {
     /** If the receipts preview should be shown */
     isMultiScanEnabled: boolean;
 
-    /** Callback to open the receipt view modal */
-    onOpenModal: () => void;
-
     /** Method to disable swipe between tabs */
     setTabSwipeDisabled?: (isDisabled: boolean) => void;
 };
 
-function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled, onOpenModal}: ReceiptPreviewsProps) {
+function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: ReceiptPreviewsProps) {
     const styles = useThemeStyles();
     const theme = useTheme();
     const {translate} = useLocalize();
@@ -49,8 +48,8 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled, onOpe
     const [optimisticTransactionsReceipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) =>
             Object.values(items ?? {})
-                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
-                .filter((receipt): receipt is ReceiptWithTransactionID => !!receipt),
+                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID, reportID: transaction.reportID} : undefined))
+                .filter((receipt): receipt is ReceiptWithTransactionID & {reportID: string} => !!receipt),
         canBeMissing: true,
     });
     const receipts = useMemo(() => {
@@ -94,7 +93,7 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled, onOpe
                 accessible
                 accessibilityLabel={translate('common.receipt')}
                 accessibilityRole={CONST.ROLE.BUTTON}
-                onPress={onOpenModal}
+                onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute(item.transactionID, item.reportID))}
             >
                 <Image
                     source={{uri: item.source}}
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index cc2575b6abdd..76dbee254dff 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -1,115 +1,71 @@
-import React, {useCallback, useRef, useState} from 'react';
-import {Keyboard, View} from 'react-native';
-import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
-import type {AttachmentCarouselPagerHandle} from '@components/Attachments/AttachmentCarousel/Pager';
-import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
-import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
-import type {AttachmentSource} from '@components/Attachments/types';
+import React, {useCallback} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import AttachmentCarousel from '@components/Attachments/AttachmentCarousel';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
+import Image from '@components/Image';
 import Modal from '@components/Modal';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import ONYXKEYS from '@src/ONYXKEYS';
 import type {Receipt} from '@src/types/onyx/Transaction';
 
-type ReceiptWithSource = Receipt & {
-    source: AttachmentSource;
-};
-
 type ReceiptViewModalProps = {
-    /** Whether the modal is open or not */
-    isOpen: boolean;
-    /** An array of receipt image source URLs to display in the modal. */
-    sources: ReceiptWithSource[];
-    /** The index of the currently selected receipt in the sources array. */
-    selectedIndex: number;
-    /** Callback to delete a receipt */
-    onDelete: (index: number) => void;
-    /** Callback to close modal */
-    onClose: () => void;
+    route: {
+        params: {
+            transactionID: string;
+            reportID: string;
+        };
+    };
 };
 
-function ReceiptViewModal({sources, isOpen, selectedIndex, onDelete, onClose}: ReceiptViewModalProps) {
+type ReceiptWithTransactionID = Receipt & {transactionID: string};
+
+function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const {translate} = useLocalize();
     const styles = useThemeStyles();
-    const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
-    const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
-
-    const [page, setPage] = useState<number>(selectedIndex);
-    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(sources.at(selectedIndex)?.receiptID ?? sources.at(selectedIndex)?.source ?? 0);
-
-    const handleDelete = useCallback(() => {
-        onDelete(page);
-        onClose();
-    }, [onDelete, onClose, page]);
 
-    const updatePage = useCallback(
-        (newPageIndex: number) => {
-            Keyboard.dismiss();
-            setShouldShowArrows(true);
+    const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
+        selector: (items) =>
+            Object.values(items ?? {})
+                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
+                .filter((receipt): receipt is ReceiptWithTransactionID => !!receipt),
+        canBeMissing: true,
+    });
 
-            const item = sources.at(newPageIndex);
+    const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route?.params?.reportID}`);
 
-            setPage(newPageIndex);
-            if (newPageIndex >= 0 && item) {
-                setActiveAttachmentID(item.receiptID ?? item.source);
-            }
-        },
-        [setShouldShowArrows, sources],
-    );
+    const handleDelete = useCallback(() => {
+        // onDelete();
+        Navigation.goBack();
+    }, []);
 
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    const cycleThroughAttachments = useCallback(
-        (deltaSlide: number) => {
-            if (page === undefined) {
-                return;
-            }
-            const nextPageIndex = page + deltaSlide;
-            updatePage(nextPageIndex);
-            pagerRef.current?.setPage(nextPageIndex);
-
-            autoHideArrows();
-        },
-        [autoHideArrows, page, updatePage],
-    );
+    const selectedReceipt = receipts?.find((receipt) => receipt.transactionID === route?.params?.transactionID);
 
     return (
         <Modal
             type="centered"
-            isVisible={isOpen}
-            onClose={onClose}
+            isVisible
+            onClose={Navigation.goBack}
         >
             <HeaderWithBackButton
                 title={translate('common.receipt')}
-                onBackButtonPress={onClose}
+                onBackButtonPress={Navigation.goBack}
                 shouldShowThreeDotsButton
                 threeDotsMenuIcon={Expensicons.Trashcan}
                 onThreeDotsButtonPress={handleDelete}
             />
 
-            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
-                <AttachmentCarouselView
-                    cycleThroughAttachments={cycleThroughAttachments}
-                    page={page}
-                    attachments={sources}
-                    shouldShowArrows={shouldShowArrows}
-                    autoHideArrows={autoHideArrows}
-                    cancelAutoHideArrow={cancelAutoHideArrows}
-                >
-                    <AttachmentCarouselPager
-                        items={sources}
-                        initialPage={page}
-                        activeAttachmentID={activeAttachmentID}
-                        setShouldShowArrows={setShouldShowArrows}
-                        onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                        onClose={onClose}
-                        ref={pagerRef}
-                    />
-                </AttachmentCarouselView>
-            </View>
+            <Image
+                style={[styles.flex1]}
+                source={{uri: selectedReceipt?.source ?? ''}}
+            />
+            {/* <AttachmentCarousel
+                onClose={Navigation.goBack}
+                report={report}
+                source={selectedReceipt?.source}
+            /> */}
         </Modal>
     );
 }
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index f1ea5d1fc794..4f42c6ba8478 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -79,7 +79,6 @@ import type {Receipt} from '@src/types/onyx/Transaction';
 import CameraPermission from './CameraPermission';
 import NavigationAwareCamera from './NavigationAwareCamera/Camera';
 import ReceiptPreviews from './ReceiptPreviews';
-import ReceiptViewModal from './ReceiptViewModal';
 import type IOURequestStepScanProps from './types';
 import type {ReceiptFile} from './types';
 
@@ -100,13 +99,6 @@ function IOURequestStepScan({
         physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
     });
 
-    const [isReceiptModalVisible, setIsReceiptModalVisible] = useState(false);
-    const [currentSelectedIndex, setCurrentSelectedIndex] = useState(0);
-
-    const handleDeleteReceipt = useCallback((index: number) => {
-        // Delete the receipt at the specified index
-    }, []);
-
     const [elementTop, setElementTop] = useState(0);
     const isEditing = action === CONST.IOU.ACTION.EDIT;
     const hasFlash = !!device?.hasFlash;
@@ -114,7 +106,8 @@ function IOURequestStepScan({
     const [flash, setFlash] = useState(false);
     // TODO: remove when multi-scan functionality is removed from beta
     const {canUseMultiScan: canUseMultiScanBeta} = usePermissions();
-    const canUseMultiScan = true;
+    const canUseMultiScan = canUseMultiScanBeta && !isEditing && iouType !== CONST.IOU.TYPE.SPLIT;
+
     const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);
     const [receiptFiles, setReceiptFiles] = useState<ReceiptFile[]>([]);
     const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true});
@@ -916,10 +909,9 @@ function IOURequestStepScan({
                 </View>
 
                 <ReceiptPreviews
-                    isMultiScanEnabled
+                    isMultiScanEnabled={isMultiScanEnabled}
                     submit={submitReceipts}
                     setTabSwipeDisabled={setTabSwipeDisabled}
-                    onOpenModal={() => setIsReceiptModalVisible(true)}
                 />
 
                 {startLocationPermissionFlow && !!receiptFiles.length && (
@@ -933,13 +925,6 @@ function IOURequestStepScan({
                         }}
                     />
                 )}
-                <ReceiptViewModal
-                    sources={receiptFiles}
-                    isOpen={isReceiptModalVisible}
-                    selectedIndex={currentSelectedIndex}
-                    onClose={() => setIsReceiptModalVisible(false)}
-                    onDelete={handleDeleteReceipt}
-                />
             </View>
         </StepScreenWrapper>
     );

From a99a584d17dcc9b31a58c4a41978be70fda30033 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Tue, 3 Jun 2025 15:04:27 +0200
Subject: [PATCH 07/35] add: switching between images in ReceiptViewModal with
 arrows, delete functionality

---
 src/ROUTES.ts                                 |  4 +-
 .../AttachmentCarouselView.tsx                | 69 +++++++++++------
 .../AttachmentCarousel/index.native.tsx       | 47 ++++++-----
 .../Attachments/AttachmentCarousel/index.tsx  | 77 +++++++++----------
 src/libs/Navigation/types.ts                  |  1 -
 .../ReceiptPreviews/index.tsx                 |  2 +-
 .../ReceiptViewModal/index.tsx                | 73 ++++++++++++------
 .../request/step/IOURequestStepScan/types.ts  |  3 +-
 8 files changed, 161 insertions(+), 115 deletions(-)

diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index afbb6f606460..d181e614cb7c 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -948,8 +948,8 @@ const ROUTES = {
     },
 
     MONEY_REQUEST_RECEIPT_VIEW_MODAL: {
-        route: 'receipt-view-modal/:transactionID/:reportID',
-        getRoute: (transactionID: string, reportID: string) => `receipt-view-modal/${transactionID}/${reportID}` as const,
+        route: 'receipt-view-modal/:transactionID',
+        getRoute: (transactionID: string) => `receipt-view-modal/${transactionID}` as const,
     },
 
     MONEY_REQUEST_STATE_SELECTOR: {
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
index 40e9b3fb9011..34ac19420d4f 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
@@ -1,10 +1,14 @@
 import React from 'react';
 import type {ReactNode} from 'react';
+import {View} from 'react-native';
 import CarouselButtons from '@components/Attachments/AttachmentCarousel/CarouselButtons';
 import type {Attachment} from '@components/Attachments/types';
 import BlockingView from '@components/BlockingViews/BlockingView';
 import * as Illustrations from '@components/Icon/Illustrations';
 import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
+import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
 import variables from '@styles/variables';
 
 type AttachmentCarouselViewProps = {
@@ -15,11 +19,13 @@ type AttachmentCarouselViewProps = {
     page: number;
 
     /** The attachments from the carousel */
-    attachments: Attachment[];
+    attachments: Attachment[] | ReceiptFile[];
 
     /** Callback for auto hiding carousel button arrows */
     autoHideArrows: () => void;
 
+    setShouldShowArrows: (shouldShow: boolean) => void;
+
     /** Callback for cancelling auto hiding of carousel button arrows */
     cancelAutoHideArrow: () => void;
 
@@ -30,29 +36,48 @@ type AttachmentCarouselViewProps = {
     children: ReactNode;
 };
 
-function AttachmentCarouselView({children, page, cycleThroughAttachments, attachments, shouldShowArrows, autoHideArrows, cancelAutoHideArrow}: AttachmentCarouselViewProps) {
+function AttachmentCarouselView({
+    children,
+    page,
+    attachments,
+    shouldShowArrows,
+    autoHideArrows,
+    cancelAutoHideArrow,
+    setShouldShowArrows,
+    cycleThroughAttachments,
+}: AttachmentCarouselViewProps) {
     const {translate} = useLocalize();
+    const canUseTouchScreen = canUseTouchScreenUtil();
+    const styles = useThemeStyles();
 
-    return page === -1 ? (
-        <BlockingView
-            icon={Illustrations.ToddBehindCloud}
-            iconWidth={variables.modalTopIconWidth}
-            iconHeight={variables.modalTopIconHeight}
-            title={translate('notFound.notHere')}
-        />
-    ) : (
-        <>
-            <CarouselButtons
-                shouldShowArrows={shouldShowArrows}
-                page={page}
-                attachments={attachments}
-                onBack={() => cycleThroughAttachments(-1)}
-                onForward={() => cycleThroughAttachments(1)}
-                autoHideArrow={autoHideArrows}
-                cancelAutoHideArrow={cancelAutoHideArrow}
-            />
-            {children}
-        </>
+    return (
+        <View
+            style={[styles.flex1, styles.attachmentCarouselContainer]}
+            onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
+            onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
+        >
+            {page === -1 ? (
+                <BlockingView
+                    icon={Illustrations.ToddBehindCloud}
+                    iconWidth={variables.modalTopIconWidth}
+                    iconHeight={variables.modalTopIconHeight}
+                    title={translate('notFound.notHere')}
+                />
+            ) : (
+                <>
+                    <CarouselButtons
+                        shouldShowArrows={shouldShowArrows}
+                        page={page}
+                        attachments={attachments}
+                        onBack={() => cycleThroughAttachments(-1)}
+                        onForward={() => cycleThroughAttachments(1)}
+                        autoHideArrow={autoHideArrows}
+                        cancelAutoHideArrow={cancelAutoHideArrow}
+                    />
+                    {children}
+                </>
+            )}
+        </View>
     );
 }
 
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
index 1d2957239a3b..3152ab3cc0fc 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -105,39 +105,36 @@ function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownlo
         [autoHideArrows, page, updatePage],
     );
 
-    const containerStyles = [styles.flex1, styles.attachmentCarouselContainer];
-
     if (page == null) {
         return (
-            <View style={containerStyles}>
+            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
                 <FullScreenLoadingIndicator />
             </View>
         );
     }
 
     return (
-        <View style={containerStyles}>
-            <AttachmentCarouselView
-                cycleThroughAttachments={cycleThroughAttachments}
-                page={page}
-                attachments={attachments}
-                shouldShowArrows={shouldShowArrows}
-                autoHideArrows={autoHideArrows}
-                cancelAutoHideArrow={cancelAutoHideArrows}
-            >
-                <AttachmentCarouselPager
-                    items={attachments}
-                    initialPage={page}
-                    onAttachmentError={onAttachmentError}
-                    activeAttachmentID={activeAttachmentID}
-                    setShouldShowArrows={setShouldShowArrows}
-                    onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                    onClose={onClose}
-                    ref={pagerRef}
-                    reportID={report.reportID}
-                />
-            </AttachmentCarouselView>
-        </View>
+        <AttachmentCarouselView
+            cycleThroughAttachments={cycleThroughAttachments}
+            page={page}
+            attachments={attachments}
+            shouldShowArrows={shouldShowArrows}
+            autoHideArrows={autoHideArrows}
+            cancelAutoHideArrow={cancelAutoHideArrows}
+            setShouldShowArrows={setShouldShowArrows}
+        >
+            <AttachmentCarouselPager
+                items={attachments}
+                initialPage={page}
+                onAttachmentError={onAttachmentError}
+                activeAttachmentID={activeAttachmentID}
+                setShouldShowArrows={setShouldShowArrows}
+                onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
+                onClose={onClose}
+                ref={pagerRef}
+                reportID={report.reportID}
+            />
+        </AttachmentCarouselView>
     );
 }
 
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index 58957098bcf3..779ad67193c1 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -282,48 +282,43 @@ function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownlo
     );
 
     return (
-        <View
-            style={[styles.flex1, styles.attachmentCarouselContainer]}
-            onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
-            onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
+        <AttachmentCarouselView
+            cycleThroughAttachments={cycleThroughAttachments}
+            page={page}
+            attachments={attachments}
+            shouldShowArrows={shouldShowArrows}
+            autoHideArrows={autoHideArrows}
+            cancelAutoHideArrow={cancelAutoHideArrows}
+            setShouldShowArrows={setShouldShowArrows}
         >
-            <AttachmentCarouselView
-                cycleThroughAttachments={cycleThroughAttachments}
-                page={page}
-                attachments={attachments}
-                shouldShowArrows={shouldShowArrows}
-                autoHideArrows={autoHideArrows}
-                cancelAutoHideArrow={cancelAutoHideArrows}
-            >
-                <AttachmentCarouselPagerContext.Provider value={context}>
-                    <DeviceAwareGestureDetector
-                        canUseTouchScreen={canUseTouchScreen}
-                        gesture={pan}
-                    >
-                        <Animated.FlatList
-                            keyboardShouldPersistTaps="handled"
-                            horizontal
-                            showsHorizontalScrollIndicator={false}
-                            // scrolling is controlled by the pan gesture
-                            scrollEnabled={false}
-                            ref={scrollRef}
-                            initialScrollIndex={page}
-                            initialNumToRender={3}
-                            windowSize={5}
-                            maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
-                            data={attachments}
-                            renderItem={renderItem}
-                            getItemLayout={getItemLayout}
-                            keyExtractor={extractItemKey}
-                            viewabilityConfig={viewabilityConfig}
-                            onViewableItemsChanged={updatePage}
-                        />
-                    </DeviceAwareGestureDetector>
-                </AttachmentCarouselPagerContext.Provider>
-
-                <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
-            </AttachmentCarouselView>
-        </View>
+            <AttachmentCarouselPagerContext.Provider value={context}>
+                <DeviceAwareGestureDetector
+                    canUseTouchScreen={canUseTouchScreen}
+                    gesture={pan}
+                >
+                    <Animated.FlatList
+                        keyboardShouldPersistTaps="handled"
+                        horizontal
+                        showsHorizontalScrollIndicator={false}
+                        // scrolling is controlled by the pan gesture
+                        scrollEnabled={false}
+                        ref={scrollRef}
+                        initialScrollIndex={page}
+                        initialNumToRender={3}
+                        windowSize={5}
+                        maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
+                        data={attachments}
+                        renderItem={renderItem}
+                        getItemLayout={getItemLayout}
+                        keyExtractor={extractItemKey}
+                        viewabilityConfig={viewabilityConfig}
+                        onViewableItemsChanged={updatePage}
+                    />
+                </DeviceAwareGestureDetector>
+            </AttachmentCarouselPagerContext.Provider>
+
+            <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
+        </AttachmentCarouselView>
     );
 }
 
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 89d93e18d624..96d67a2286b1 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1320,7 +1320,6 @@ type MoneyRequestNavigatorParamList = {
     };
     [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: {
         transactionID: string;
-        reportID: string;
     };
     [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
         action: IOUAction;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
index 5cdd84a2e6c2..1b183ae42fd6 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
@@ -93,7 +93,7 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: Rece
                 accessible
                 accessibilityLabel={translate('common.receipt')}
                 accessibilityRole={CONST.ROLE.BUTTON}
-                onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute(item.transactionID, item.reportID))}
+                onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute(item.transactionID))}
             >
                 <Image
                     source={{uri: item.source}}
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 76dbee254dff..bfe7fab08a20 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -1,6 +1,7 @@
-import React, {useCallback} from 'react';
+import React, {useCallback, useEffect} from 'react';
 import {useOnyx} from 'react-native-onyx';
-import AttachmentCarousel from '@components/Attachments/AttachmentCarousel';
+import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
+import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Image from '@components/Image';
@@ -8,40 +9,64 @@ import Modal from '@components/Modal';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
+import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
+import {removeTransactionReceipt} from '@userActions/TransactionEdit';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type {Receipt} from '@src/types/onyx/Transaction';
 
 type ReceiptViewModalProps = {
     route: {
         params: {
             transactionID: string;
-            reportID: string;
         };
     };
 };
 
-type ReceiptWithTransactionID = Receipt & {transactionID: string};
-
 function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const {translate} = useLocalize();
     const styles = useThemeStyles();
+    const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+
+    const [currentReceipt, setCurrentReceipt] = React.useState<ReceiptFile | null>();
+    const [page, setPage] = React.useState<number>(0);
 
     const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) =>
             Object.values(items ?? {})
                 .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
-                .filter((receipt): receipt is ReceiptWithTransactionID => !!receipt),
+                .filter((receipt): receipt is ReceiptFile => !!receipt),
         canBeMissing: true,
     });
 
-    const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route?.params?.reportID}`);
+    useEffect(() => {
+        if (!receipts || receipts.length === 0) {
+            return;
+        }
+
+        const activeReceipt = receipts.find((receipt) => receipt.transactionID === route?.params?.transactionID);
+        const currentReceiptIndex = receipts.findIndex((receipt) => receipt.transactionID === activeReceipt?.transactionID);
+
+        setCurrentReceipt(activeReceipt);
+        setPage(currentReceiptIndex);
+    }, [receipts, route?.params?.transactionID]);
+
+    const cycleThroughReceipts = useCallback(
+        (deltaSlide: number) => {
+            const nextIndex = page + deltaSlide;
+            const nextItem = receipts?.at(nextIndex);
+
+            setPage(nextIndex);
+            setCurrentReceipt(nextItem);
+        },
+        [page, receipts],
+    );
 
     const handleDelete = useCallback(() => {
-        // onDelete();
+        if (!currentReceipt) {
+            return;
+        }
         Navigation.goBack();
-    }, []);
-
-    const selectedReceipt = receipts?.find((receipt) => receipt.transactionID === route?.params?.transactionID);
+        removeTransactionReceipt(currentReceipt.transactionID);
+    }, [currentReceipt]);
 
     return (
         <Modal
@@ -56,16 +81,20 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                 threeDotsMenuIcon={Expensicons.Trashcan}
                 onThreeDotsButtonPress={handleDelete}
             />
-
-            <Image
-                style={[styles.flex1]}
-                source={{uri: selectedReceipt?.source ?? ''}}
-            />
-            {/* <AttachmentCarousel
-                onClose={Navigation.goBack}
-                report={report}
-                source={selectedReceipt?.source}
-            /> */}
+            <AttachmentCarouselView
+                attachments={receipts ?? []}
+                autoHideArrows={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrows}
+                cycleThroughAttachments={cycleThroughReceipts}
+                shouldShowArrows={shouldShowArrows}
+                page={page}
+                setShouldShowArrows={setShouldShowArrows}
+            >
+                <Image
+                    style={[styles.flex1]}
+                    source={{uri: currentReceipt?.source ?? ''}}
+                />
+            </AttachmentCarouselView>
         </Modal>
     );
 }
diff --git a/src/pages/iou/request/step/IOURequestStepScan/types.ts b/src/pages/iou/request/step/IOURequestStepScan/types.ts
index e25cb3bf2173..4f3ab3b21db6 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/types.ts
+++ b/src/pages/iou/request/step/IOURequestStepScan/types.ts
@@ -4,6 +4,7 @@ import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentU
 import type {WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound';
 import type SCREENS from '@src/SCREENS';
 import type * as OnyxTypes from '@src/types/onyx';
+import type {ReceiptSource} from '@src/types/onyx/Transaction';
 
 type IOURequestStepScanProps = WithCurrentUserPersonalDetailsProps &
     WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_SCAN | typeof SCREENS.MONEY_REQUEST.CREATE> & {
@@ -24,7 +25,7 @@ type IOURequestStepScanProps = WithCurrentUserPersonalDetailsProps &
     };
 
 type ReceiptFile = {
-    source: string;
+    source: ReceiptSource;
     file: FileObject;
     transactionID: string;
 };

From 8e921772a630c07baecc84de4f694dcb81f48d60 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Tue, 3 Jun 2025 16:33:53 +0200
Subject: [PATCH 08/35] chore: minor clean up

---
 .../AttachmentCarouselView/AttachmentCarouselView.tsx  |  3 +--
 .../step/IOURequestStepScan/ReceiptViewModal/index.tsx | 10 +++++-----
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
index 34ac19420d4f..5eb6ca4e9933 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
@@ -8,7 +8,6 @@ import * as Illustrations from '@components/Icon/Illustrations';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
-import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
 import variables from '@styles/variables';
 
 type AttachmentCarouselViewProps = {
@@ -19,7 +18,7 @@ type AttachmentCarouselViewProps = {
     page: number;
 
     /** The attachments from the carousel */
-    attachments: Attachment[] | ReceiptFile[];
+    attachments: Attachment[];
 
     /** Callback for auto hiding carousel button arrows */
     autoHideArrows: () => void;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index bfe7fab08a20..98976d598ad5 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect} from 'react';
+import React, {useCallback, useEffect, useState} from 'react';
 import {useOnyx} from 'react-native-onyx';
 import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
@@ -26,8 +26,8 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const styles = useThemeStyles();
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
 
-    const [currentReceipt, setCurrentReceipt] = React.useState<ReceiptFile | null>();
-    const [page, setPage] = React.useState<number>(0);
+    const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
+    const [page, setPage] = useState<number>(0);
 
     const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) =>
@@ -43,10 +43,10 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         }
 
         const activeReceipt = receipts.find((receipt) => receipt.transactionID === route?.params?.transactionID);
-        const currentReceiptIndex = receipts.findIndex((receipt) => receipt.transactionID === activeReceipt?.transactionID);
+        const activeReceiptIndex = receipts.findIndex((receipt) => receipt.transactionID === activeReceipt?.transactionID);
 
         setCurrentReceipt(activeReceipt);
-        setPage(currentReceiptIndex);
+        setPage(activeReceiptIndex);
     }, [receipts, route?.params?.transactionID]);
 
     const cycleThroughReceipts = useCallback(

From 54e5d52af4908d8dbf5bef0d49bf35b8d2e31392 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Tue, 3 Jun 2025 19:07:56 +0200
Subject: [PATCH 09/35] add: native initial implementation for ReceiptViewModal

---
 .../ReceiptViewModal/index.native.tsx         | 116 ++++++++++++++++++
 1 file changed, 116 insertions(+)
 create mode 100644 src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
new file mode 100644
index 000000000000..5d4edf03ec9f
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
@@ -0,0 +1,116 @@
+import React, {useCallback, useEffect, useRef, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import type PagerView from 'react-native-pager-view';
+import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
+import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
+import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import Modal from '@components/Modal';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
+import {removeTransactionReceipt} from '@userActions/TransactionEdit';
+import ONYXKEYS from '@src/ONYXKEYS';
+
+type ReceiptViewModalProps = {
+    route: {
+        params: {
+            transactionID: string;
+        };
+    };
+};
+
+function ReceiptViewModal({route}: ReceiptViewModalProps) {
+    const {translate} = useLocalize();
+    const styles = useThemeStyles();
+    const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+
+    const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
+    const [page, setPage] = useState<number>(0);
+    const pagerRef = useRef<PagerView>(null);
+
+    const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
+        selector: (items) =>
+            Object.values(items ?? {})
+                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
+                .filter((receipt): receipt is ReceiptFile => !!receipt),
+        canBeMissing: true,
+    });
+
+    useEffect(() => {
+        if (!receipts || receipts.length === 0) {
+            return;
+        }
+
+        const activeReceipt = receipts.find((receipt) => receipt.transactionID === route?.params?.transactionID);
+        const activeReceiptIndex = receipts.findIndex((receipt) => receipt.transactionID === activeReceipt?.transactionID);
+
+        setCurrentReceipt(activeReceipt);
+        setPage(activeReceiptIndex);
+    }, [receipts, route?.params?.transactionID]);
+
+    const cycleThroughReceipts = useCallback(
+        (deltaSlide: number) => {
+            const nextIndex = page + deltaSlide;
+            const nextItem = receipts?.at(nextIndex);
+
+            setPage(nextIndex);
+            setCurrentReceipt(nextItem);
+            pagerRef.current?.setPage(nextIndex);
+        },
+        [page, receipts],
+    );
+
+    const handleDelete = useCallback(() => {
+        if (!currentReceipt) {
+            return;
+        }
+        Navigation.goBack();
+        removeTransactionReceipt(currentReceipt.transactionID);
+    }, [currentReceipt]);
+
+    return (
+        <Modal
+            type="centered"
+            isVisible
+            onClose={Navigation.goBack}
+        >
+            <HeaderWithBackButton
+                title={translate('common.receipt')}
+                onBackButtonPress={Navigation.goBack}
+                shouldShowThreeDotsButton
+                threeDotsMenuIcon={Expensicons.Trashcan}
+                onThreeDotsButtonPress={handleDelete}
+            />
+            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
+                <AttachmentCarouselView
+                    cycleThroughAttachments={cycleThroughReceipts}
+                    page={page}
+                    attachments={receipts ?? []}
+                    shouldShowArrows={shouldShowArrows}
+                    autoHideArrows={autoHideArrows}
+                    cancelAutoHideArrow={cancelAutoHideArrows}
+                    setShouldShowArrows={setShouldShowArrows}
+                >
+                    <AttachmentCarouselPager
+                        items={receipts ?? []}
+                        initialPage={page}
+                        activeAttachmentID={currentReceipt?.transactionID ?? ''}
+                        setShouldShowArrows={setShouldShowArrows}
+                        onPageSelected={({nativeEvent: {position: newPage}}) => setPage(newPage)}
+                        onClose={Navigation.goBack}
+                        ref={pagerRef}
+                        reportID={currentReceipt?.transactionID ?? ''}
+                    />
+                </AttachmentCarouselView>
+            </View>
+        </Modal>
+    );
+}
+
+ReceiptViewModal.displayName = 'ReceiptViewModal';
+
+export default ReceiptViewModal;

From 6ddc7378080a8736d75e3a5e41a8e7ae186e3698 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 4 Jun 2025 13:13:48 +0200
Subject: [PATCH 10/35] remove: not needed receiptID assignment

---
 .../request/step/IOURequestStepScan/ReceiptPreviews/index.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
index a0706141af51..d5ce3f485c74 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
@@ -51,8 +51,8 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: Rece
     const [optimisticTransactionsReceipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) =>
             Object.values(items ?? {})
-                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID, reportID: transaction.reportID} : undefined))
-                .filter((receipt): receipt is ReceiptWithTransactionID & {reportID: string} => !!receipt),
+                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
+                .filter((receipt): receipt is ReceiptWithTransactionID => !!receipt),
         canBeMissing: true,
     });
     const receipts = useMemo(() => {

From 6897997cb7c034e6a914208d0f3c465166ad3ec4 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 4 Jun 2025 13:55:38 +0200
Subject: [PATCH 11/35] add: confirmation modal on deleting receipt

---
 .../ReceiptViewModal/index.native.tsx         | 93 ++++++++++++-------
 .../ReceiptViewModal/index.tsx                | 74 +++++++++------
 2 files changed, 104 insertions(+), 63 deletions(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
index 5d4edf03ec9f..f8c77aa0c7c3 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
@@ -5,6 +5,7 @@ import type PagerView from 'react-native-pager-view';
 import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
 import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
+import ConfirmModal from '@components/ConfirmModal';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Modal from '@components/Modal';
@@ -30,6 +31,8 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
 
     const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
     const [page, setPage] = useState<number>(0);
+    const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false);
+
     const pagerRef = useRef<PagerView>(null);
 
     const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
@@ -64,50 +67,68 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         [page, receipts],
     );
 
-    const handleDelete = useCallback(() => {
+    const handleDeleteReceipt = useCallback(() => {
         if (!currentReceipt) {
             return;
         }
-        Navigation.goBack();
+
         removeTransactionReceipt(currentReceipt.transactionID);
+        Navigation.goBack();
     }, [currentReceipt]);
 
+    const deleteReceiptAndCloseModal = useCallback(() => {
+        setIsDeleteReceiptConfirmModalVisible(false);
+        handleDeleteReceipt();
+    }, [handleDeleteReceipt]);
+
     return (
-        <Modal
-            type="centered"
-            isVisible
-            onClose={Navigation.goBack}
-        >
-            <HeaderWithBackButton
-                title={translate('common.receipt')}
-                onBackButtonPress={Navigation.goBack}
-                shouldShowThreeDotsButton
-                threeDotsMenuIcon={Expensicons.Trashcan}
-                onThreeDotsButtonPress={handleDelete}
-            />
-            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
-                <AttachmentCarouselView
-                    cycleThroughAttachments={cycleThroughReceipts}
-                    page={page}
-                    attachments={receipts ?? []}
-                    shouldShowArrows={shouldShowArrows}
-                    autoHideArrows={autoHideArrows}
-                    cancelAutoHideArrow={cancelAutoHideArrows}
-                    setShouldShowArrows={setShouldShowArrows}
-                >
-                    <AttachmentCarouselPager
-                        items={receipts ?? []}
-                        initialPage={page}
-                        activeAttachmentID={currentReceipt?.transactionID ?? ''}
+        <>
+            <Modal
+                type="centered"
+                isVisible
+                onClose={Navigation.goBack}
+            >
+                <HeaderWithBackButton
+                    title={translate('common.receipt')}
+                    onBackButtonPress={Navigation.goBack}
+                    shouldShowThreeDotsButton
+                    threeDotsMenuIcon={Expensicons.Trashcan}
+                    onThreeDotsButtonPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
+                />
+                <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
+                    <AttachmentCarouselView
+                        cycleThroughAttachments={cycleThroughReceipts}
+                        page={page}
+                        attachments={receipts ?? []}
+                        shouldShowArrows={shouldShowArrows}
+                        autoHideArrows={autoHideArrows}
+                        cancelAutoHideArrow={cancelAutoHideArrows}
                         setShouldShowArrows={setShouldShowArrows}
-                        onPageSelected={({nativeEvent: {position: newPage}}) => setPage(newPage)}
-                        onClose={Navigation.goBack}
-                        ref={pagerRef}
-                        reportID={currentReceipt?.transactionID ?? ''}
-                    />
-                </AttachmentCarouselView>
-            </View>
-        </Modal>
+                    >
+                        <AttachmentCarouselPager
+                            items={receipts ?? []}
+                            initialPage={page}
+                            activeAttachmentID={currentReceipt?.transactionID ?? ''}
+                            setShouldShowArrows={setShouldShowArrows}
+                            onPageSelected={({nativeEvent: {position: newPage}}) => setPage(newPage)}
+                            onClose={Navigation.goBack}
+                            ref={pagerRef}
+                            reportID={currentReceipt?.transactionID ?? ''}
+                        />
+                    </AttachmentCarouselView>
+                </View>
+            </Modal>
+            <ConfirmModal
+                title={translate('receipt.deleteReceipt')}
+                isVisible={isDeleteReceiptConfirmModalVisible}
+                onConfirm={deleteReceiptAndCloseModal}
+                onCancel={() => setIsDeleteReceiptConfirmModalVisible(false)}
+                prompt={translate('receipt.deleteConfirmation')}
+                confirmText={translate('common.delete')}
+                cancelText={translate('common.cancel')}
+                danger
+            />
+        </>
     );
 }
 
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 98976d598ad5..a504d44515cc 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react';
 import {useOnyx} from 'react-native-onyx';
 import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
+import ConfirmModal from '@components/ConfirmModal';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Image from '@components/Image';
@@ -28,6 +29,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
 
     const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
     const [page, setPage] = useState<number>(0);
+    const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false);
 
     const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) =>
@@ -60,42 +62,60 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         [page, receipts],
     );
 
-    const handleDelete = useCallback(() => {
+    const handleDeleteReceipt = useCallback(() => {
         if (!currentReceipt) {
             return;
         }
-        Navigation.goBack();
+
         removeTransactionReceipt(currentReceipt.transactionID);
+        Navigation.goBack();
     }, [currentReceipt]);
 
+    const deleteReceiptAndCloseModal = useCallback(() => {
+        setIsDeleteReceiptConfirmModalVisible(false);
+        handleDeleteReceipt();
+    }, [handleDeleteReceipt]);
+
     return (
-        <Modal
-            type="centered"
-            isVisible
-            onClose={Navigation.goBack}
-        >
-            <HeaderWithBackButton
-                title={translate('common.receipt')}
-                onBackButtonPress={Navigation.goBack}
-                shouldShowThreeDotsButton
-                threeDotsMenuIcon={Expensicons.Trashcan}
-                onThreeDotsButtonPress={handleDelete}
-            />
-            <AttachmentCarouselView
-                attachments={receipts ?? []}
-                autoHideArrows={autoHideArrows}
-                cancelAutoHideArrow={cancelAutoHideArrows}
-                cycleThroughAttachments={cycleThroughReceipts}
-                shouldShowArrows={shouldShowArrows}
-                page={page}
-                setShouldShowArrows={setShouldShowArrows}
+        <>
+            <Modal
+                type="centered"
+                isVisible
+                onClose={Navigation.goBack}
             >
-                <Image
-                    style={[styles.flex1]}
-                    source={{uri: currentReceipt?.source ?? ''}}
+                <HeaderWithBackButton
+                    title={translate('common.receipt')}
+                    onBackButtonPress={Navigation.goBack}
+                    shouldShowThreeDotsButton
+                    threeDotsMenuIcon={Expensicons.Trashcan}
+                    onThreeDotsButtonPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
                 />
-            </AttachmentCarouselView>
-        </Modal>
+                <AttachmentCarouselView
+                    attachments={receipts ?? []}
+                    autoHideArrows={autoHideArrows}
+                    cancelAutoHideArrow={cancelAutoHideArrows}
+                    cycleThroughAttachments={cycleThroughReceipts}
+                    shouldShowArrows={shouldShowArrows}
+                    page={page}
+                    setShouldShowArrows={setShouldShowArrows}
+                >
+                    <Image
+                        style={[styles.flex1]}
+                        source={{uri: currentReceipt?.source ?? ''}}
+                    />
+                </AttachmentCarouselView>
+            </Modal>
+            <ConfirmModal
+                title={translate('receipt.deleteReceipt')}
+                isVisible={isDeleteReceiptConfirmModalVisible}
+                onConfirm={deleteReceiptAndCloseModal}
+                onCancel={() => setIsDeleteReceiptConfirmModalVisible(false)}
+                prompt={translate('receipt.deleteConfirmation')}
+                confirmText={translate('common.delete')}
+                cancelText={translate('common.cancel')}
+                danger
+            />
+        </>
     );
 }
 

From e28343caa9016e8d7d27c02011edcfba666f8994 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 4 Jun 2025 16:15:24 +0200
Subject: [PATCH 12/35] chore: removed dependency on attachments from carousel
 buttons

---
 .../AttachmentCarouselView.tsx                      |  7 +++++--
 .../AttachmentCarousel/CarouselButtons.tsx          | 13 +++++--------
 2 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
index 5eb6ca4e9933..4de3a3410b61 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
@@ -49,6 +49,9 @@ function AttachmentCarouselView({
     const canUseTouchScreen = canUseTouchScreenUtil();
     const styles = useThemeStyles();
 
+    const isBackDisabled = page === 0;
+    const isForwardDisabled = page === attachments.length - 1;
+
     return (
         <View
             style={[styles.flex1, styles.attachmentCarouselContainer]}
@@ -66,8 +69,8 @@ function AttachmentCarouselView({
                 <>
                     <CarouselButtons
                         shouldShowArrows={shouldShowArrows}
-                        page={page}
-                        attachments={attachments}
+                        isBackDisabled={isBackDisabled}
+                        isForwardDisabled={isForwardDisabled}
                         onBack={() => cycleThroughAttachments(-1)}
                         onForward={() => cycleThroughAttachments(1)}
                         autoHideArrow={autoHideArrows}
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
index 04fd1163af44..ad96ae1968fd 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
@@ -1,6 +1,5 @@
 import React from 'react';
 import {View} from 'react-native';
-import type {Attachment} from '@components/Attachments/types';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Tooltip from '@components/Tooltip';
@@ -13,11 +12,11 @@ type CarouselButtonsProps = {
     /** Where the arrows should be visible */
     shouldShowArrows: boolean;
 
-    /** The current page index */
-    page: number;
+    /** Whether the back button is disabled */
+    isBackDisabled: boolean;
 
-    /** The attachments from the carousel */
-    attachments: Attachment[];
+    /** Whether the forward button is disabled */
+    isForwardDisabled: boolean;
 
     /** Callback to go one page back */
     onBack: () => void;
@@ -32,11 +31,9 @@ type CarouselButtonsProps = {
     cancelAutoHideArrow?: () => void;
 };
 
-function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
+function CarouselButtons({isBackDisabled, isForwardDisabled, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
-    const isBackDisabled = page === 0;
-    const isForwardDisabled = page === attachments.length - 1;
     const {translate} = useLocalize();
     const {shouldUseNarrowLayout} = useResponsiveLayout();
 

From b602ef58e99fd15e55a5f2793185eab41dac5c2f Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 4 Jun 2025 19:38:35 +0200
Subject: [PATCH 13/35] add: ReceiptViewModal independent of platform

---
 .../ReceiptViewModal/index.native.tsx         | 137 ------------------
 .../ReceiptViewModal/index.tsx                |  36 ++---
 2 files changed, 12 insertions(+), 161 deletions(-)
 delete mode 100644 src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
deleted file mode 100644
index f8c77aa0c7c3..000000000000
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.native.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {View} from 'react-native';
-import {useOnyx} from 'react-native-onyx';
-import type PagerView from 'react-native-pager-view';
-import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
-import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
-import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
-import ConfirmModal from '@components/ConfirmModal';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import * as Expensicons from '@components/Icon/Expensicons';
-import Modal from '@components/Modal';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
-import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
-import {removeTransactionReceipt} from '@userActions/TransactionEdit';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-type ReceiptViewModalProps = {
-    route: {
-        params: {
-            transactionID: string;
-        };
-    };
-};
-
-function ReceiptViewModal({route}: ReceiptViewModalProps) {
-    const {translate} = useLocalize();
-    const styles = useThemeStyles();
-    const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
-
-    const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
-    const [page, setPage] = useState<number>(0);
-    const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false);
-
-    const pagerRef = useRef<PagerView>(null);
-
-    const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
-        selector: (items) =>
-            Object.values(items ?? {})
-                .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
-                .filter((receipt): receipt is ReceiptFile => !!receipt),
-        canBeMissing: true,
-    });
-
-    useEffect(() => {
-        if (!receipts || receipts.length === 0) {
-            return;
-        }
-
-        const activeReceipt = receipts.find((receipt) => receipt.transactionID === route?.params?.transactionID);
-        const activeReceiptIndex = receipts.findIndex((receipt) => receipt.transactionID === activeReceipt?.transactionID);
-
-        setCurrentReceipt(activeReceipt);
-        setPage(activeReceiptIndex);
-    }, [receipts, route?.params?.transactionID]);
-
-    const cycleThroughReceipts = useCallback(
-        (deltaSlide: number) => {
-            const nextIndex = page + deltaSlide;
-            const nextItem = receipts?.at(nextIndex);
-
-            setPage(nextIndex);
-            setCurrentReceipt(nextItem);
-            pagerRef.current?.setPage(nextIndex);
-        },
-        [page, receipts],
-    );
-
-    const handleDeleteReceipt = useCallback(() => {
-        if (!currentReceipt) {
-            return;
-        }
-
-        removeTransactionReceipt(currentReceipt.transactionID);
-        Navigation.goBack();
-    }, [currentReceipt]);
-
-    const deleteReceiptAndCloseModal = useCallback(() => {
-        setIsDeleteReceiptConfirmModalVisible(false);
-        handleDeleteReceipt();
-    }, [handleDeleteReceipt]);
-
-    return (
-        <>
-            <Modal
-                type="centered"
-                isVisible
-                onClose={Navigation.goBack}
-            >
-                <HeaderWithBackButton
-                    title={translate('common.receipt')}
-                    onBackButtonPress={Navigation.goBack}
-                    shouldShowThreeDotsButton
-                    threeDotsMenuIcon={Expensicons.Trashcan}
-                    onThreeDotsButtonPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
-                />
-                <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
-                    <AttachmentCarouselView
-                        cycleThroughAttachments={cycleThroughReceipts}
-                        page={page}
-                        attachments={receipts ?? []}
-                        shouldShowArrows={shouldShowArrows}
-                        autoHideArrows={autoHideArrows}
-                        cancelAutoHideArrow={cancelAutoHideArrows}
-                        setShouldShowArrows={setShouldShowArrows}
-                    >
-                        <AttachmentCarouselPager
-                            items={receipts ?? []}
-                            initialPage={page}
-                            activeAttachmentID={currentReceipt?.transactionID ?? ''}
-                            setShouldShowArrows={setShouldShowArrows}
-                            onPageSelected={({nativeEvent: {position: newPage}}) => setPage(newPage)}
-                            onClose={Navigation.goBack}
-                            ref={pagerRef}
-                            reportID={currentReceipt?.transactionID ?? ''}
-                        />
-                    </AttachmentCarouselView>
-                </View>
-            </Modal>
-            <ConfirmModal
-                title={translate('receipt.deleteReceipt')}
-                isVisible={isDeleteReceiptConfirmModalVisible}
-                onConfirm={deleteReceiptAndCloseModal}
-                onCancel={() => setIsDeleteReceiptConfirmModalVisible(false)}
-                prompt={translate('receipt.deleteConfirmation')}
-                confirmText={translate('common.delete')}
-                cancelText={translate('common.cancel')}
-                danger
-            />
-        </>
-    );
-}
-
-ReceiptViewModal.displayName = 'ReceiptViewModal';
-
-export default ReceiptViewModal;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index a504d44515cc..3b27078c92d5 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -1,14 +1,12 @@
 import React, {useCallback, useEffect, useState} from 'react';
 import {useOnyx} from 'react-native-onyx';
-import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView';
+import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
 import ConfirmModal from '@components/ConfirmModal';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
-import Image from '@components/Image';
 import Modal from '@components/Modal';
 import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
 import {removeTransactionReceipt} from '@userActions/TransactionEdit';
@@ -24,7 +22,6 @@ type ReceiptViewModalProps = {
 
 function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const {translate} = useLocalize();
-    const styles = useThemeStyles();
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
 
     const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
@@ -51,17 +48,6 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         setPage(activeReceiptIndex);
     }, [receipts, route?.params?.transactionID]);
 
-    const cycleThroughReceipts = useCallback(
-        (deltaSlide: number) => {
-            const nextIndex = page + deltaSlide;
-            const nextItem = receipts?.at(nextIndex);
-
-            setPage(nextIndex);
-            setCurrentReceipt(nextItem);
-        },
-        [page, receipts],
-    );
-
     const handleDeleteReceipt = useCallback(() => {
         if (!currentReceipt) {
             return;
@@ -76,6 +62,9 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         handleDeleteReceipt();
     }, [handleDeleteReceipt]);
 
+    // TODO: Implement the logic to handle attachment errors
+    const handleAttachmentError = () => {};
+
     return (
         <>
             <Modal
@@ -92,18 +81,17 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                 />
                 <AttachmentCarouselView
                     attachments={receipts ?? []}
+                    source={currentReceipt?.source ?? ''}
+                    page={page}
+                    setPage={setPage}
+                    attachmentID={currentReceipt?.transactionID ?? ''}
+                    onClose={Navigation.goBack}
                     autoHideArrows={autoHideArrows}
                     cancelAutoHideArrow={cancelAutoHideArrows}
-                    cycleThroughAttachments={cycleThroughReceipts}
-                    shouldShowArrows={shouldShowArrows}
-                    page={page}
                     setShouldShowArrows={setShouldShowArrows}
-                >
-                    <Image
-                        style={[styles.flex1]}
-                        source={{uri: currentReceipt?.source ?? ''}}
-                    />
-                </AttachmentCarouselView>
+                    onAttachmentError={handleAttachmentError}
+                    shouldShowArrows={shouldShowArrows}
+                />
             </Modal>
             <ConfirmModal
                 title={translate('receipt.deleteReceipt')}

From f9dfcc06d5004e6434e05e7256bcf8562b63250e Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 4 Jun 2025 19:40:50 +0200
Subject: [PATCH 14/35] refactor: native versions of AttachmentCarousel and
 AttachmentCarouselView

---
 .../AttachmentCarouselView/index.native.tsx   | 150 ++++++++++++++++++
 .../AttachmentCarousel/index.native.tsx       |  71 ++-------
 .../Attachments/AttachmentCarousel/types.ts   |  12 +-
 .../Attachments/AttachmentView/index.tsx      |   4 +-
 4 files changed, 170 insertions(+), 67 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
new file mode 100644
index 000000000000..e6a8b0600528
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
@@ -0,0 +1,150 @@
+import React, {useCallback, useRef, useState} from 'react';
+import {Keyboard, View} from 'react-native';
+import CarouselButtons from '@components/Attachments/AttachmentCarousel/CarouselButtons';
+import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
+import type {AttachmentCarouselPagerHandle} from '@components/Attachments/AttachmentCarousel/Pager';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
+import variables from '@styles/variables';
+import type {Report} from '@src/types/onyx';
+
+type AttachmentCarouselViewProps = {
+    /** Where the arrows should be visible */
+    shouldShowArrows: boolean;
+
+    /** The current page index */
+    page: number;
+
+    /** The attachments from the carousel */
+    attachments: Attachment[];
+
+    attachmentID?: string;
+
+    source: AttachmentSource;
+
+    report?: Report;
+
+    /** Callback for auto hiding carousel button arrows */
+    autoHideArrows: () => void;
+
+    setShouldShowArrows: (show?: React.SetStateAction<boolean>) => void;
+
+    /** Callback for cancelling auto hiding of carousel button arrows */
+    cancelAutoHideArrow: () => void;
+    onAttachmentError?: (source: AttachmentSource, state?: boolean) => void;
+    onNavigate?: (item: Attachment) => void;
+    onClose: () => void;
+    setPage: (page: number) => void;
+};
+
+function AttachmentCarouselView({
+    page,
+    attachments,
+    shouldShowArrows,
+    source,
+    report,
+    autoHideArrows,
+    cancelAutoHideArrow,
+    setShouldShowArrows,
+    onAttachmentError,
+    onNavigate,
+    onClose,
+    setPage,
+    attachmentID,
+}: AttachmentCarouselViewProps) {
+    const {translate} = useLocalize();
+    const canUseTouchScreen = canUseTouchScreenUtil();
+    const styles = useThemeStyles();
+    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(attachmentID ?? source);
+
+    const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
+
+    const isBackDisabled = page === 0;
+    const isForwardDisabled = page === attachments.length - 1;
+
+    /** Updates the page state when the user navigates between attachments */
+    const updatePage = useCallback(
+        (newPageIndex: number) => {
+            Keyboard.dismiss();
+            setShouldShowArrows(true);
+
+            const item = attachments.at(newPageIndex);
+
+            setPage(newPageIndex);
+            if (newPageIndex >= 0 && item) {
+                setActiveAttachmentID(item.attachmentID ?? item.source);
+                if (onNavigate) {
+                    onNavigate(item);
+                }
+                onNavigate?.(item);
+            }
+        },
+        [setShouldShowArrows, attachments, setPage, onNavigate],
+    );
+
+    /**
+     * Increments or decrements the index to get another selected item
+     * @param {Number} deltaSlide
+     */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide: number) => {
+            if (page === undefined) {
+                return;
+            }
+            const nextPageIndex = page + deltaSlide;
+            updatePage(nextPageIndex);
+            pagerRef.current?.setPage(nextPageIndex);
+
+            autoHideArrows();
+        },
+        [autoHideArrows, page, updatePage],
+    );
+
+    return (
+        <View
+            style={[styles.flex1, styles.attachmentCarouselContainer]}
+            onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
+            onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
+        >
+            {page === -1 ? (
+                <BlockingView
+                    icon={Illustrations.ToddBehindCloud}
+                    iconWidth={variables.modalTopIconWidth}
+                    iconHeight={variables.modalTopIconHeight}
+                    title={translate('notFound.notHere')}
+                />
+            ) : (
+                <>
+                    <CarouselButtons
+                        shouldShowArrows={shouldShowArrows}
+                        isBackDisabled={isBackDisabled}
+                        isForwardDisabled={isForwardDisabled}
+                        onBack={() => cycleThroughAttachments(-1)}
+                        onForward={() => cycleThroughAttachments(1)}
+                        autoHideArrow={autoHideArrows}
+                        cancelAutoHideArrow={cancelAutoHideArrow}
+                    />
+                    <AttachmentCarouselPager
+                        items={attachments}
+                        initialPage={page}
+                        onAttachmentError={onAttachmentError}
+                        activeAttachmentID={activeAttachmentID}
+                        setShouldShowArrows={setShouldShowArrows}
+                        onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
+                        onClose={onClose}
+                        ref={pagerRef}
+                        reportID={report?.reportID}
+                    />
+                </>
+            )}
+        </View>
+    );
+}
+
+AttachmentCarouselView.displayName = 'AttachmentCarouselView';
+
+export default AttachmentCarouselView;
diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
index 3152ab3cc0fc..f43a427dbf70 100644
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx
@@ -1,28 +1,25 @@
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {Keyboard, View} from 'react-native';
+import React, {useCallback, useEffect, useState} from 'react';
+import {View} from 'react-native';
 import {useOnyx} from 'react-native-onyx';
-import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import type {Attachment} from '@components/Attachments/types';
 import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import AttachmentCarouselView from './AttachmentCarouselView/AttachmentCarouselView';
+import AttachmentCarouselView from './AttachmentCarouselView';
 import extractAttachments from './extractAttachments';
-import type {AttachmentCarouselPagerHandle} from './Pager';
-import AttachmentCarouselPager from './Pager';
 import type {AttachmentCarouselProps} from './types';
 import useCarouselArrows from './useCarouselArrows';
 
 function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownloadButtonVisibility, onClose, type, accountID, onAttachmentError}: AttachmentCarouselProps) {
     const styles = useThemeStyles();
-    const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
     const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {canEvict: false, canBeMissing: true});
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, canBeMissing: true});
     const [page, setPage] = useState<number>();
     const [attachments, setAttachments] = useState<Attachment[]>([]);
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
-    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(attachmentID ?? source);
+
     const compareImage = useCallback((attachment: Attachment) => (attachmentID ? attachment.attachmentID === attachmentID : attachment.source === source), [attachmentID, source]);
 
     useEffect(() => {
@@ -67,44 +64,6 @@ function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownlo
         // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
     }, [reportActions, compareImage, report]);
 
-    /** Updates the page state when the user navigates between attachments */
-    const updatePage = useCallback(
-        (newPageIndex: number) => {
-            Keyboard.dismiss();
-            setShouldShowArrows(true);
-
-            const item = attachments.at(newPageIndex);
-
-            setPage(newPageIndex);
-            if (newPageIndex >= 0 && item) {
-                setActiveAttachmentID(item.attachmentID ?? item.source);
-                if (onNavigate) {
-                    onNavigate(item);
-                }
-                onNavigate?.(item);
-            }
-        },
-        [setShouldShowArrows, attachments, onNavigate],
-    );
-
-    /**
-     * Increments or decrements the index to get another selected item
-     * @param {Number} deltaSlide
-     */
-    const cycleThroughAttachments = useCallback(
-        (deltaSlide: number) => {
-            if (page === undefined) {
-                return;
-            }
-            const nextPageIndex = page + deltaSlide;
-            updatePage(nextPageIndex);
-            pagerRef.current?.setPage(nextPageIndex);
-
-            autoHideArrows();
-        },
-        [autoHideArrows, page, updatePage],
-    );
-
     if (page == null) {
         return (
             <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
@@ -115,26 +74,18 @@ function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownlo
 
     return (
         <AttachmentCarouselView
-            cycleThroughAttachments={cycleThroughAttachments}
             page={page}
+            setPage={setPage}
             attachments={attachments}
             shouldShowArrows={shouldShowArrows}
             autoHideArrows={autoHideArrows}
             cancelAutoHideArrow={cancelAutoHideArrows}
             setShouldShowArrows={setShouldShowArrows}
-        >
-            <AttachmentCarouselPager
-                items={attachments}
-                initialPage={page}
-                onAttachmentError={onAttachmentError}
-                activeAttachmentID={activeAttachmentID}
-                setShouldShowArrows={setShouldShowArrows}
-                onPageSelected={({nativeEvent: {position: newPage}}) => updatePage(newPage)}
-                onClose={onClose}
-                ref={pagerRef}
-                reportID={report.reportID}
-            />
-        </AttachmentCarouselView>
+            onClose={onClose}
+            source={source}
+            onAttachmentError={onAttachmentError}
+            attachmentID={attachmentID}
+        />
     );
 }
 
diff --git a/src/components/Attachments/AttachmentCarousel/types.ts b/src/components/Attachments/AttachmentCarousel/types.ts
index f98410d2bdae..3fa641168c33 100644
--- a/src/components/Attachments/AttachmentCarousel/types.ts
+++ b/src/components/Attachments/AttachmentCarousel/types.ts
@@ -12,6 +12,12 @@ type AttachmentCarouselProps = {
     /** Source is used to determine the starting index in the array of attachments */
     source: AttachmentSource;
 
+    /** The report currently being looked at */
+    report: Report;
+
+    /** A callback that is called when swipe-down-to-close gesture happens */
+    onClose: () => void;
+
     /** The id of the current active attachment */
     attachmentID?: string;
 
@@ -21,18 +27,12 @@ type AttachmentCarouselProps = {
     /** Function to change the download button Visibility */
     setDownloadButtonVisibility?: (isButtonVisible: boolean) => void;
 
-    /** The report currently being looked at */
-    report: Report;
-
     /** The type of the attachment */
     type?: ValueOf<typeof CONST.ATTACHMENT_TYPE>;
 
     /** If the attachment originates from a note, the accountID will represent the author of that note. */
     accountID?: number;
 
-    /** A callback that is called when swipe-down-to-close gesture happens */
-    onClose: () => void;
-
     attachmentLink?: string;
 
     /** Callback for attachment errors */
diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index a047ea411f5f..539d1bd2da53 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -88,7 +88,9 @@ type AttachmentViewProps = Attachment & {
 };
 
 function checkIsFileImage(source: string | number | ImageURISource | ImageURISource[], fileName: string | undefined) {
-    const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
+    const isSourceImage = typeof source === 'number' || typeof source === 'string';
+    // const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
+
     const isFileNameImage = fileName && Str.isImage(fileName);
 
     return isSourceImage || isFileNameImage;

From 513d3b0490497edc2689847dfa8f9c0c106ee779 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 4 Jun 2025 19:47:28 +0200
Subject: [PATCH 15/35] refactor: web versions of AttachmentCarousel and
 AttachmentCarouselView

---
 .../AttachmentCarouselView.tsx                |  88 -----
 .../AttachmentCarouselView/index.tsx          | 326 ++++++++++++++++++
 .../Attachments/AttachmentCarousel/index.tsx  | 244 +------------
 3 files changed, 337 insertions(+), 321 deletions(-)
 delete mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
 create mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
deleted file mode 100644
index 4de3a3410b61..000000000000
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/AttachmentCarouselView.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React from 'react';
-import type {ReactNode} from 'react';
-import {View} from 'react-native';
-import CarouselButtons from '@components/Attachments/AttachmentCarousel/CarouselButtons';
-import type {Attachment} from '@components/Attachments/types';
-import BlockingView from '@components/BlockingViews/BlockingView';
-import * as Illustrations from '@components/Icon/Illustrations';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
-import variables from '@styles/variables';
-
-type AttachmentCarouselViewProps = {
-    /** Where the arrows should be visible */
-    shouldShowArrows: boolean;
-
-    /** The current page index */
-    page: number;
-
-    /** The attachments from the carousel */
-    attachments: Attachment[];
-
-    /** Callback for auto hiding carousel button arrows */
-    autoHideArrows: () => void;
-
-    setShouldShowArrows: (shouldShow: boolean) => void;
-
-    /** Callback for cancelling auto hiding of carousel button arrows */
-    cancelAutoHideArrow: () => void;
-
-    /** Callback to cycle through attachments */
-    cycleThroughAttachments: (deltaSlide: number) => void;
-
-    /** children components */
-    children: ReactNode;
-};
-
-function AttachmentCarouselView({
-    children,
-    page,
-    attachments,
-    shouldShowArrows,
-    autoHideArrows,
-    cancelAutoHideArrow,
-    setShouldShowArrows,
-    cycleThroughAttachments,
-}: AttachmentCarouselViewProps) {
-    const {translate} = useLocalize();
-    const canUseTouchScreen = canUseTouchScreenUtil();
-    const styles = useThemeStyles();
-
-    const isBackDisabled = page === 0;
-    const isForwardDisabled = page === attachments.length - 1;
-
-    return (
-        <View
-            style={[styles.flex1, styles.attachmentCarouselContainer]}
-            onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
-            onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
-        >
-            {page === -1 ? (
-                <BlockingView
-                    icon={Illustrations.ToddBehindCloud}
-                    iconWidth={variables.modalTopIconWidth}
-                    iconHeight={variables.modalTopIconHeight}
-                    title={translate('notFound.notHere')}
-                />
-            ) : (
-                <>
-                    <CarouselButtons
-                        shouldShowArrows={shouldShowArrows}
-                        isBackDisabled={isBackDisabled}
-                        isForwardDisabled={isForwardDisabled}
-                        onBack={() => cycleThroughAttachments(-1)}
-                        onForward={() => cycleThroughAttachments(1)}
-                        autoHideArrow={autoHideArrows}
-                        cancelAutoHideArrow={cancelAutoHideArrow}
-                    />
-                    {children}
-                </>
-            )}
-        </View>
-    );
-}
-
-AttachmentCarouselView.displayName = 'AttachmentCarouselView';
-
-export default AttachmentCarouselView;
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
new file mode 100644
index 000000000000..09764b4eee17
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
@@ -0,0 +1,326 @@
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {MutableRefObject} from 'react';
+import type {ListRenderItemInfo} from 'react-native';
+import {Keyboard, PixelRatio, View} from 'react-native';
+import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import type {ComposedGesture, GestureType} from 'react-native-gesture-handler';
+import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated';
+import CarouselActions from '@components/Attachments/AttachmentCarousel/CarouselActions';
+import CarouselButtons from '@components/Attachments/AttachmentCarousel/CarouselButtons';
+import CarouselItem from '@components/Attachments/AttachmentCarousel/CarouselItem';
+import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
+import type {UpdatePageProps} from '@components/Attachments/AttachmentCarousel/types';
+import useCarouselContextEvents from '@components/Attachments/AttachmentCarousel/useCarouselContextEvents';
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import BlockingView from '@components/BlockingViews/BlockingView';
+import * as Illustrations from '@components/Icon/Illustrations';
+import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+import type {Report} from '@src/types/onyx';
+
+const viewabilityConfig = {
+    // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
+    // more than 95% visible. When that happens we update the page index in the state.
+    itemVisiblePercentThreshold: 95,
+};
+
+const MIN_FLING_VELOCITY = 500;
+
+type DeviceAwareGestureDetectorProps = {
+    canUseTouchScreen: boolean;
+    gesture: ComposedGesture | GestureType;
+    children: React.ReactNode;
+};
+
+function DeviceAwareGestureDetector({canUseTouchScreen, gesture, children}: DeviceAwareGestureDetectorProps) {
+    // Don't render GestureDetector on non-touchable devices to prevent unexpected pointer event capture.
+    // This issue is left out on touchable devices since finger touch works fine.
+    // See: https://github.com/Expensify/App/issues/51246
+    return canUseTouchScreen ? <GestureDetector gesture={gesture}>{children}</GestureDetector> : children;
+}
+
+type AttachmentCarouselViewProps = {
+    /** Where the arrows should be visible */
+    shouldShowArrows: boolean;
+
+    /** The current page index */
+    page: number;
+
+    /** The attachments from the carousel */
+    attachments: Attachment[];
+
+    attachmentID?: string;
+
+    source: AttachmentSource;
+
+    report?: Report;
+
+    /** Callback for auto hiding carousel button arrows */
+    autoHideArrows: () => void;
+
+    setShouldShowArrows: (show?: React.SetStateAction<boolean>) => void;
+
+    /** Callback for cancelling auto hiding of carousel button arrows */
+    cancelAutoHideArrow: () => void;
+    onAttachmentError?: (source: AttachmentSource, state?: boolean) => void;
+    onNavigate?: (item: Attachment) => void;
+    onClose: () => void;
+    setPage: (page: number) => void;
+};
+
+function AttachmentCarouselView({
+    page,
+    attachments,
+    shouldShowArrows,
+    source,
+    report,
+    autoHideArrows,
+    cancelAutoHideArrow,
+    setShouldShowArrows,
+    onAttachmentError,
+    onNavigate,
+    onClose,
+    setPage,
+    attachmentID,
+}: AttachmentCarouselViewProps) {
+    const {translate} = useLocalize();
+    const canUseTouchScreen = canUseTouchScreenUtil();
+    const styles = useThemeStyles();
+    const {isFullScreenRef} = useFullScreenContext();
+
+    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource | null>(attachmentID ?? source);
+
+    const pagerRef = useRef<GestureType>(null);
+    const scrollRef = useAnimatedRef<Animated.FlatList<ListRenderItemInfo<Attachment>>>();
+
+    const isPagerScrolling = useSharedValue(false);
+    const {handleTap, handleScaleChange, isScrollEnabled} = useCarouselContextEvents(setShouldShowArrows);
+
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
+    const modalStyles = styles.centeredModalStyles(shouldUseNarrowLayout, true);
+    const {windowWidth} = useWindowDimensions();
+
+    const cellWidth = useMemo(
+        () => PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2),
+        [modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth],
+    );
+
+    const isBackDisabled = page === 0;
+    const isForwardDisabled = page === attachments.length - 1;
+
+    /** Updates the page state when the user navigates between attachments */
+    const updatePage = useCallback(
+        ({viewableItems}: UpdatePageProps) => {
+            if (isFullScreenRef.current) {
+                return;
+            }
+
+            Keyboard.dismiss();
+
+            // Since we can have only one item in view at a time, we can use the first item in the array
+            // to get the index of the current page
+            const entry = viewableItems.at(0);
+            if (!entry) {
+                setActiveAttachmentID(null);
+                return;
+            }
+
+            const item = entry.item as Attachment;
+            if (entry.index !== null) {
+                setPage(entry.index);
+                setActiveAttachmentID(item.attachmentID ?? item.source);
+            }
+
+            if (onNavigate) {
+                onNavigate(item);
+            }
+        },
+        [isFullScreenRef, onNavigate, setPage, setActiveAttachmentID],
+    );
+
+    /** Increments or decrements the index to get another selected item */
+    const cycleThroughAttachments = useCallback(
+        (deltaSlide: number) => {
+            if (isFullScreenRef.current) {
+                return;
+            }
+
+            const nextIndex = page + deltaSlide;
+            const nextItem = attachments.at(nextIndex);
+
+            if (!nextItem || nextIndex < 0 || !scrollRef.current) {
+                return;
+            }
+
+            scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
+        },
+        [attachments, canUseTouchScreen, isFullScreenRef, page, scrollRef],
+    );
+
+    const extractItemKey = useCallback(
+        (item: Attachment) =>
+            !!item.attachmentID || (typeof item.source !== 'string' && typeof item.source !== 'number')
+                ? `attachmentID-${item.attachmentID}`
+                : `source-${item.source}|${item.attachmentLink}`,
+        [],
+    );
+
+    /** Calculate items layout information to optimize scrolling performance */
+    const getItemLayout = useCallback(
+        (data: ArrayLike<Attachment> | null | undefined, index: number) => ({
+            length: cellWidth,
+            offset: cellWidth * index,
+            index,
+        }),
+        [cellWidth],
+    );
+
+    const context = useMemo(
+        () => ({
+            pagerItems: [{source, index: 0, isActive: true}],
+            activePage: 0,
+            pagerRef,
+            isPagerScrolling,
+            isScrollEnabled,
+            onTap: handleTap,
+            onScaleChanged: handleScaleChange,
+            onSwipeDown: onClose,
+            onAttachmentError,
+        }),
+        [onAttachmentError, source, isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange, onClose],
+    );
+
+    /** Defines how a single attachment should be rendered */
+    const renderItem = useCallback(
+        ({item}: ListRenderItemInfo<Attachment>) => (
+            <View style={[styles.h100, {width: cellWidth}]}>
+                <CarouselItem
+                    item={item}
+                    isFocused={activeAttachmentID === (item.attachmentID ?? item.source)}
+                    onPress={canUseTouchScreen ? handleTap : undefined}
+                    isModalHovered={shouldShowArrows}
+                    reportID={report?.reportID}
+                />
+            </View>
+        ),
+        [activeAttachmentID, canUseTouchScreen, cellWidth, handleTap, report?.reportID, shouldShowArrows, styles.h100],
+    );
+    /** Pan gesture handing swiping through attachments on touch screen devices */
+    const pan = useMemo(
+        () =>
+            Gesture.Pan()
+                .enabled(canUseTouchScreen)
+                .onUpdate(({translationX}) => {
+                    if (!isScrollEnabled.get()) {
+                        return;
+                    }
+
+                    if (translationX !== 0) {
+                        isPagerScrolling.set(true);
+                    }
+
+                    scrollTo(scrollRef, page * cellWidth - translationX, 0, false);
+                })
+                .onEnd(({translationX, velocityX}) => {
+                    if (!isScrollEnabled.get()) {
+                        return;
+                    }
+
+                    let newIndex;
+                    if (velocityX > MIN_FLING_VELOCITY) {
+                        // User flung to the right
+                        newIndex = Math.max(0, page - 1);
+                    } else if (velocityX < -MIN_FLING_VELOCITY) {
+                        // User flung to the left
+                        newIndex = Math.min(attachments.length - 1, page + 1);
+                    } else {
+                        // snap scroll position to the nearest cell (making sure it's within the bounds of the list)
+                        const delta = Math.round(-translationX / cellWidth);
+                        newIndex = Math.min(attachments.length - 1, Math.max(0, page + delta));
+                    }
+
+                    isPagerScrolling.set(false);
+                    scrollTo(scrollRef, newIndex * cellWidth, 0, true);
+                })
+                // eslint-disable-next-line react-compiler/react-compiler
+                .withRef(pagerRef as MutableRefObject<GestureType | undefined>),
+        [attachments.length, canUseTouchScreen, cellWidth, page, isScrollEnabled, scrollRef, isPagerScrolling],
+    );
+
+    // Scroll position is affected when window width is resized, so we readjust it on width changes
+    useEffect(() => {
+        if (attachments.length === 0 || scrollRef.current == null) {
+            return;
+        }
+
+        scrollRef.current.scrollToIndex({index: page, animated: false});
+        // The hook is not supposed to run on page change, so we keep the page out of the dependencies
+        // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+    }, [cellWidth]);
+
+    return (
+        <View
+            style={[styles.flex1, styles.attachmentCarouselContainer]}
+            onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
+            onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
+        >
+            {page === -1 ? (
+                <BlockingView
+                    icon={Illustrations.ToddBehindCloud}
+                    iconWidth={variables.modalTopIconWidth}
+                    iconHeight={variables.modalTopIconHeight}
+                    title={translate('notFound.notHere')}
+                />
+            ) : (
+                <>
+                    <CarouselButtons
+                        shouldShowArrows={shouldShowArrows}
+                        isBackDisabled={isBackDisabled}
+                        isForwardDisabled={isForwardDisabled}
+                        onBack={() => cycleThroughAttachments(-1)}
+                        onForward={() => cycleThroughAttachments(1)}
+                        autoHideArrow={autoHideArrows}
+                        cancelAutoHideArrow={cancelAutoHideArrow}
+                    />
+                    <AttachmentCarouselPagerContext.Provider value={context}>
+                        <DeviceAwareGestureDetector
+                            canUseTouchScreen={canUseTouchScreen}
+                            gesture={pan}
+                        >
+                            <Animated.FlatList
+                                keyboardShouldPersistTaps="handled"
+                                horizontal
+                                showsHorizontalScrollIndicator={false}
+                                // scrolling is controlled by the pan gesture
+                                scrollEnabled={false}
+                                ref={scrollRef}
+                                initialScrollIndex={page}
+                                initialNumToRender={3}
+                                windowSize={5}
+                                maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
+                                data={attachments}
+                                renderItem={renderItem}
+                                getItemLayout={getItemLayout}
+                                keyExtractor={extractItemKey}
+                                viewabilityConfig={viewabilityConfig}
+                                onViewableItemsChanged={updatePage}
+                            />
+                        </DeviceAwareGestureDetector>
+                    </AttachmentCarouselPagerContext.Provider>
+
+                    <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
+                </>
+            )}
+        </View>
+    );
+}
+
+AttachmentCarouselView.displayName = 'AttachmentCarouselView';
+
+export default AttachmentCarouselView;
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index 779ad67193c1..f99f2745166c 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -1,73 +1,24 @@
 import isEqual from 'lodash/isEqual';
-import type {MutableRefObject} from 'react';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {ListRenderItemInfo} from 'react-native';
-import {Keyboard, PixelRatio, View} from 'react-native';
-import type {ComposedGesture, GestureType} from 'react-native-gesture-handler';
-import {Gesture, GestureDetector} from 'react-native-gesture-handler';
-import Animated, {scrollTo, useAnimatedRef, useSharedValue} from 'react-native-reanimated';
-import type {Attachment, AttachmentSource} from '@components/Attachments/types';
-import {useFullScreenContext} from '@components/VideoPlayerContexts/FullScreenContext';
+import React, {useCallback, useEffect, useState} from 'react';
+import type {Attachment} from '@components/Attachments/types';
 import useOnyx from '@hooks/useOnyx';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
 import Navigation from '@libs/Navigation/Navigation';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import AttachmentCarouselView from './AttachmentCarouselView/AttachmentCarouselView';
-import CarouselActions from './CarouselActions';
-import CarouselItem from './CarouselItem';
+import AttachmentCarouselView from './AttachmentCarouselView';
 import extractAttachments from './extractAttachments';
-import AttachmentCarouselPagerContext from './Pager/AttachmentCarouselPagerContext';
-import type {AttachmentCarouselProps, UpdatePageProps} from './types';
+import type {AttachmentCarouselProps} from './types';
 import useCarouselArrows from './useCarouselArrows';
-import useCarouselContextEvents from './useCarouselContextEvents';
-
-const viewabilityConfig = {
-    // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
-    // more than 95% visible. When that happens we update the page index in the state.
-    itemVisiblePercentThreshold: 95,
-};
-
-const MIN_FLING_VELOCITY = 500;
-
-type DeviceAwareGestureDetectorProps = {
-    canUseTouchScreen: boolean;
-    gesture: ComposedGesture | GestureType;
-    children: React.ReactNode;
-};
-
-function DeviceAwareGestureDetector({canUseTouchScreen, gesture, children}: DeviceAwareGestureDetectorProps) {
-    // Don't render GestureDetector on non-touchable devices to prevent unexpected pointer event capture.
-    // This issue is left out on touchable devices since finger touch works fine.
-    // See: https://github.com/Expensify/App/issues/51246
-    return canUseTouchScreen ? <GestureDetector gesture={gesture}>{children}</GestureDetector> : children;
-}
 
 function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownloadButtonVisibility, type, accountID, onClose, attachmentLink, onAttachmentError}: AttachmentCarouselProps) {
-    const {windowWidth} = useWindowDimensions();
-    const {shouldUseNarrowLayout} = useResponsiveLayout();
-    const styles = useThemeStyles();
-    const {isFullScreenRef} = useFullScreenContext();
-    const scrollRef = useAnimatedRef<Animated.FlatList<ListRenderItemInfo<Attachment>>>();
-    const isPagerScrolling = useSharedValue(false);
-    const pagerRef = useRef<GestureType>(null);
     const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {canEvict: false, canBeMissing: true});
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, canBeMissing: true});
     const canUseTouchScreen = canUseTouchScreenUtil();
 
-    const modalStyles = styles.centeredModalStyles(shouldUseNarrowLayout, true);
-    const cellWidth = useMemo(
-        () => PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2),
-        [modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth],
-    );
     const [page, setPage] = useState(0);
     const [attachments, setAttachments] = useState<Attachment[]>([]);
-    const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource | null>(attachmentID ?? source);
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
-    const {handleTap, handleScaleChange, isScrollEnabled} = useCarouselContextEvents(setShouldShowArrows);
 
     useEffect(() => {
         if (!canUseTouchScreen) {
@@ -131,194 +82,21 @@ function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownlo
         }
     }, [reportActions, parentReportActions, compareImage, attachments, setDownloadButtonVisibility, onNavigate, accountID, type, report]);
 
-    // Scroll position is affected when window width is resized, so we readjust it on width changes
-    useEffect(() => {
-        if (attachments.length === 0 || scrollRef.current == null) {
-            return;
-        }
-
-        scrollRef.current.scrollToIndex({index: page, animated: false});
-        // The hook is not supposed to run on page change, so we keep the page out of the dependencies
-        // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
-    }, [cellWidth]);
-
-    /** Updates the page state when the user navigates between attachments */
-    const updatePage = useCallback(
-        ({viewableItems}: UpdatePageProps) => {
-            if (isFullScreenRef.current) {
-                return;
-            }
-
-            Keyboard.dismiss();
-
-            // Since we can have only one item in view at a time, we can use the first item in the array
-            // to get the index of the current page
-            const entry = viewableItems.at(0);
-            if (!entry) {
-                setActiveAttachmentID(null);
-                return;
-            }
-
-            const item = entry.item as Attachment;
-            if (entry.index !== null) {
-                setPage(entry.index);
-                setActiveAttachmentID(item.attachmentID ?? item.source);
-            }
-
-            if (onNavigate) {
-                onNavigate(item);
-            }
-        },
-        [isFullScreenRef, onNavigate],
-    );
-
-    /** Increments or decrements the index to get another selected item */
-    const cycleThroughAttachments = useCallback(
-        (deltaSlide: number) => {
-            if (isFullScreenRef.current) {
-                return;
-            }
-
-            const nextIndex = page + deltaSlide;
-            const nextItem = attachments.at(nextIndex);
-
-            if (!nextItem || nextIndex < 0 || !scrollRef.current) {
-                return;
-            }
-
-            scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen});
-        },
-        [attachments, canUseTouchScreen, isFullScreenRef, page, scrollRef],
-    );
-
-    const extractItemKey = useCallback(
-        (item: Attachment) =>
-            !!item.attachmentID || (typeof item.source !== 'string' && typeof item.source !== 'number')
-                ? `attachmentID-${item.attachmentID}`
-                : `source-${item.source}|${item.attachmentLink}`,
-        [],
-    );
-
-    /** Calculate items layout information to optimize scrolling performance */
-    const getItemLayout = useCallback(
-        (data: ArrayLike<Attachment> | null | undefined, index: number) => ({
-            length: cellWidth,
-            offset: cellWidth * index,
-            index,
-        }),
-        [cellWidth],
-    );
-
-    const context = useMemo(
-        () => ({
-            pagerItems: [{source, index: 0, isActive: true}],
-            activePage: 0,
-            pagerRef,
-            isPagerScrolling,
-            isScrollEnabled,
-            onTap: handleTap,
-            onScaleChanged: handleScaleChange,
-            onSwipeDown: onClose,
-            onAttachmentError,
-        }),
-        [onAttachmentError, source, isPagerScrolling, isScrollEnabled, handleTap, handleScaleChange, onClose],
-    );
-
-    /** Defines how a single attachment should be rendered */
-    const renderItem = useCallback(
-        ({item}: ListRenderItemInfo<Attachment>) => (
-            <View style={[styles.h100, {width: cellWidth}]}>
-                <CarouselItem
-                    item={item}
-                    isFocused={activeAttachmentID === (item.attachmentID ?? item.source)}
-                    onPress={canUseTouchScreen ? handleTap : undefined}
-                    isModalHovered={shouldShowArrows}
-                    reportID={report.reportID}
-                />
-            </View>
-        ),
-        [activeAttachmentID, canUseTouchScreen, cellWidth, handleTap, report.reportID, shouldShowArrows, styles.h100],
-    );
-    /** Pan gesture handing swiping through attachments on touch screen devices */
-    const pan = useMemo(
-        () =>
-            Gesture.Pan()
-                .enabled(canUseTouchScreen)
-                .onUpdate(({translationX}) => {
-                    if (!isScrollEnabled.get()) {
-                        return;
-                    }
-
-                    if (translationX !== 0) {
-                        isPagerScrolling.set(true);
-                    }
-
-                    scrollTo(scrollRef, page * cellWidth - translationX, 0, false);
-                })
-                .onEnd(({translationX, velocityX}) => {
-                    if (!isScrollEnabled.get()) {
-                        return;
-                    }
-
-                    let newIndex;
-                    if (velocityX > MIN_FLING_VELOCITY) {
-                        // User flung to the right
-                        newIndex = Math.max(0, page - 1);
-                    } else if (velocityX < -MIN_FLING_VELOCITY) {
-                        // User flung to the left
-                        newIndex = Math.min(attachments.length - 1, page + 1);
-                    } else {
-                        // snap scroll position to the nearest cell (making sure it's within the bounds of the list)
-                        const delta = Math.round(-translationX / cellWidth);
-                        newIndex = Math.min(attachments.length - 1, Math.max(0, page + delta));
-                    }
-
-                    isPagerScrolling.set(false);
-                    scrollTo(scrollRef, newIndex * cellWidth, 0, true);
-                })
-                // eslint-disable-next-line react-compiler/react-compiler
-                .withRef(pagerRef as MutableRefObject<GestureType | undefined>),
-        [attachments.length, canUseTouchScreen, cellWidth, page, isScrollEnabled, scrollRef, isPagerScrolling],
-    );
-
     return (
         <AttachmentCarouselView
-            cycleThroughAttachments={cycleThroughAttachments}
             page={page}
+            setPage={setPage}
             attachments={attachments}
             shouldShowArrows={shouldShowArrows}
             autoHideArrows={autoHideArrows}
             cancelAutoHideArrow={cancelAutoHideArrows}
             setShouldShowArrows={setShouldShowArrows}
-        >
-            <AttachmentCarouselPagerContext.Provider value={context}>
-                <DeviceAwareGestureDetector
-                    canUseTouchScreen={canUseTouchScreen}
-                    gesture={pan}
-                >
-                    <Animated.FlatList
-                        keyboardShouldPersistTaps="handled"
-                        horizontal
-                        showsHorizontalScrollIndicator={false}
-                        // scrolling is controlled by the pan gesture
-                        scrollEnabled={false}
-                        ref={scrollRef}
-                        initialScrollIndex={page}
-                        initialNumToRender={3}
-                        windowSize={5}
-                        maxToRenderPerBatch={CONST.MAX_TO_RENDER_PER_BATCH.CAROUSEL}
-                        data={attachments}
-                        renderItem={renderItem}
-                        getItemLayout={getItemLayout}
-                        keyExtractor={extractItemKey}
-                        viewabilityConfig={viewabilityConfig}
-                        onViewableItemsChanged={updatePage}
-                    />
-                </DeviceAwareGestureDetector>
-            </AttachmentCarouselPagerContext.Provider>
-
-            <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
-        </AttachmentCarouselView>
+            onClose={onClose}
+            onAttachmentError={onAttachmentError}
+            report={report}
+            attachmentID={attachmentID}
+            source={source}
+        />
     );
 }
 

From c5644ec1f546a33ffcbd367acaa868dafacdedaa Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 5 Jun 2025 13:02:40 +0200
Subject: [PATCH 16/35] chore: clean up, extracted common types

---
 .../AttachmentCarouselView/index.native.tsx   | 40 ++--------------
 .../AttachmentCarouselView/index.tsx          | 46 +++----------------
 .../AttachmentCarouselView/types.ts           | 33 +++++++++++++
 .../AttachmentCarousel/CarouselButtons.tsx    | 14 ++++--
 .../ReceiptViewModal/index.tsx                | 19 ++++----
 5 files changed, 63 insertions(+), 89 deletions(-)
 create mode 100644 src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
index e6a8b0600528..85dcc5927ec6 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
@@ -3,43 +3,14 @@ import {Keyboard, View} from 'react-native';
 import CarouselButtons from '@components/Attachments/AttachmentCarousel/CarouselButtons';
 import AttachmentCarouselPager from '@components/Attachments/AttachmentCarousel/Pager';
 import type {AttachmentCarouselPagerHandle} from '@components/Attachments/AttachmentCarousel/Pager';
-import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import type {AttachmentSource} from '@components/Attachments/types';
 import BlockingView from '@components/BlockingViews/BlockingView';
 import * as Illustrations from '@components/Icon/Illustrations';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
 import variables from '@styles/variables';
-import type {Report} from '@src/types/onyx';
-
-type AttachmentCarouselViewProps = {
-    /** Where the arrows should be visible */
-    shouldShowArrows: boolean;
-
-    /** The current page index */
-    page: number;
-
-    /** The attachments from the carousel */
-    attachments: Attachment[];
-
-    attachmentID?: string;
-
-    source: AttachmentSource;
-
-    report?: Report;
-
-    /** Callback for auto hiding carousel button arrows */
-    autoHideArrows: () => void;
-
-    setShouldShowArrows: (show?: React.SetStateAction<boolean>) => void;
-
-    /** Callback for cancelling auto hiding of carousel button arrows */
-    cancelAutoHideArrow: () => void;
-    onAttachmentError?: (source: AttachmentSource, state?: boolean) => void;
-    onNavigate?: (item: Attachment) => void;
-    onClose: () => void;
-    setPage: (page: number) => void;
-};
+import type AttachmentCarouselViewProps from './types';
 
 function AttachmentCarouselView({
     page,
@@ -63,9 +34,6 @@ function AttachmentCarouselView({
 
     const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
 
-    const isBackDisabled = page === 0;
-    const isForwardDisabled = page === attachments.length - 1;
-
     /** Updates the page state when the user navigates between attachments */
     const updatePage = useCallback(
         (newPageIndex: number) => {
@@ -120,9 +88,9 @@ function AttachmentCarouselView({
             ) : (
                 <>
                     <CarouselButtons
+                        page={page}
+                        attachments={attachments}
                         shouldShowArrows={shouldShowArrows}
-                        isBackDisabled={isBackDisabled}
-                        isForwardDisabled={isForwardDisabled}
                         onBack={() => cycleThroughAttachments(-1)}
                         onForward={() => cycleThroughAttachments(1)}
                         autoHideArrow={autoHideArrows}
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
index 09764b4eee17..28475535afa1 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
@@ -22,7 +22,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
 import variables from '@styles/variables';
 import CONST from '@src/CONST';
-import type {Report} from '@src/types/onyx';
+import type AttachmentCarouselViewProps from './types';
 
 const viewabilityConfig = {
     // To facilitate paging through the attachments, we want to consider an item "viewable" when it is
@@ -45,35 +45,6 @@ function DeviceAwareGestureDetector({canUseTouchScreen, gesture, children}: Devi
     return canUseTouchScreen ? <GestureDetector gesture={gesture}>{children}</GestureDetector> : children;
 }
 
-type AttachmentCarouselViewProps = {
-    /** Where the arrows should be visible */
-    shouldShowArrows: boolean;
-
-    /** The current page index */
-    page: number;
-
-    /** The attachments from the carousel */
-    attachments: Attachment[];
-
-    attachmentID?: string;
-
-    source: AttachmentSource;
-
-    report?: Report;
-
-    /** Callback for auto hiding carousel button arrows */
-    autoHideArrows: () => void;
-
-    setShouldShowArrows: (show?: React.SetStateAction<boolean>) => void;
-
-    /** Callback for cancelling auto hiding of carousel button arrows */
-    cancelAutoHideArrow: () => void;
-    onAttachmentError?: (source: AttachmentSource, state?: boolean) => void;
-    onNavigate?: (item: Attachment) => void;
-    onClose: () => void;
-    setPage: (page: number) => void;
-};
-
 function AttachmentCarouselView({
     page,
     attachments,
@@ -90,18 +61,17 @@ function AttachmentCarouselView({
     attachmentID,
 }: AttachmentCarouselViewProps) {
     const {translate} = useLocalize();
-    const canUseTouchScreen = canUseTouchScreenUtil();
     const styles = useThemeStyles();
+    const canUseTouchScreen = canUseTouchScreenUtil();
     const {isFullScreenRef} = useFullScreenContext();
+    const isPagerScrolling = useSharedValue(false);
+    const {handleTap, handleScaleChange, isScrollEnabled} = useCarouselContextEvents(setShouldShowArrows);
 
     const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource | null>(attachmentID ?? source);
 
     const pagerRef = useRef<GestureType>(null);
     const scrollRef = useAnimatedRef<Animated.FlatList<ListRenderItemInfo<Attachment>>>();
 
-    const isPagerScrolling = useSharedValue(false);
-    const {handleTap, handleScaleChange, isScrollEnabled} = useCarouselContextEvents(setShouldShowArrows);
-
     const {shouldUseNarrowLayout} = useResponsiveLayout();
     const modalStyles = styles.centeredModalStyles(shouldUseNarrowLayout, true);
     const {windowWidth} = useWindowDimensions();
@@ -111,9 +81,6 @@ function AttachmentCarouselView({
         [modalStyles.borderWidth, modalStyles.marginHorizontal, windowWidth],
     );
 
-    const isBackDisabled = page === 0;
-    const isForwardDisabled = page === attachments.length - 1;
-
     /** Updates the page state when the user navigates between attachments */
     const updatePage = useCallback(
         ({viewableItems}: UpdatePageProps) => {
@@ -280,9 +247,9 @@ function AttachmentCarouselView({
             ) : (
                 <>
                     <CarouselButtons
+                        page={page}
+                        attachments={attachments}
                         shouldShowArrows={shouldShowArrows}
-                        isBackDisabled={isBackDisabled}
-                        isForwardDisabled={isForwardDisabled}
                         onBack={() => cycleThroughAttachments(-1)}
                         onForward={() => cycleThroughAttachments(1)}
                         autoHideArrow={autoHideArrows}
@@ -313,7 +280,6 @@ function AttachmentCarouselView({
                             />
                         </DeviceAwareGestureDetector>
                     </AttachmentCarouselPagerContext.Provider>
-
                     <CarouselActions onCycleThroughAttachments={cycleThroughAttachments} />
                 </>
             )}
diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts
new file mode 100644
index 000000000000..29d9cea6b431
--- /dev/null
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts
@@ -0,0 +1,33 @@
+import type {Attachment, AttachmentSource} from '@components/Attachments/types';
+import type {Report} from '@src/types/onyx';
+
+type AttachmentCarouselViewProps = {
+    /** Where the arrows should be visible */
+    shouldShowArrows: boolean;
+    /** The current page index */
+    page: number;
+    /** The attachments from the carousel */
+    attachments: Attachment[];
+    /** The id of the current active attachment */
+    attachmentID?: string;
+    /** Source is used to determine the starting index in the array of attachments */
+    source: AttachmentSource;
+    /** Callback for auto hiding carousel button arrows */
+    autoHideArrows: () => void;
+    /** Sets the visibility of the arrows. */
+    setShouldShowArrows: (show?: React.SetStateAction<boolean>) => void;
+    /** Callback for cancelling auto hiding of carousel button arrows */
+    cancelAutoHideArrow: () => void;
+    /** A callback that is called when swipe-down-to-close gesture happens */
+    onClose: () => void;
+    /** Sets current page */
+    setPage: (page: number) => void;
+    /** The report currently being looked at */
+    report?: Report;
+    /** Callback for attachment errors */
+    onAttachmentError?: (source: AttachmentSource, state?: boolean) => void;
+    /** Callback to update the parent modal's state with a source and name from the attachments array */
+    onNavigate?: (attachment: Attachment) => void;
+};
+
+export default AttachmentCarouselViewProps;
diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
index ad96ae1968fd..3688b9ac5518 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
@@ -1,5 +1,6 @@
 import React from 'react';
 import {View} from 'react-native';
+import type {Attachment} from '@components/Attachments/types';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Tooltip from '@components/Tooltip';
@@ -12,11 +13,11 @@ type CarouselButtonsProps = {
     /** Where the arrows should be visible */
     shouldShowArrows: boolean;
 
-    /** Whether the back button is disabled */
-    isBackDisabled: boolean;
+    /** The current page index */
+    page: number;
 
-    /** Whether the forward button is disabled */
-    isForwardDisabled: boolean;
+    /** The attachments from the carousel */
+    attachments: Attachment[];
 
     /** Callback to go one page back */
     onBack: () => void;
@@ -31,12 +32,15 @@ type CarouselButtonsProps = {
     cancelAutoHideArrow?: () => void;
 };
 
-function CarouselButtons({isBackDisabled, isForwardDisabled, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
+function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
     const {translate} = useLocalize();
     const {shouldUseNarrowLayout} = useResponsiveLayout();
 
+    const isBackDisabled = page === 0;
+    const isForwardDisabled = page === attachments.length - 1;
+
     return shouldShowArrows ? (
         <>
             {!isBackDisabled && (
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 3b27078c92d5..ac6f63a7553e 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useState} from 'react';
 import {useOnyx} from 'react-native-onyx';
 import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
+import useAttachmentErrors from '@components/Attachments/AttachmentView/useAttachmentErrors';
 import ConfirmModal from '@components/ConfirmModal';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -11,6 +12,9 @@ import Navigation from '@libs/Navigation/Navigation';
 import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
 import {removeTransactionReceipt} from '@userActions/TransactionEdit';
 import ONYXKEYS from '@src/ONYXKEYS';
+import type {Receipt} from '@src/types/onyx/Transaction';
+
+type ReceiptWithTransactionIDAndSource = Receipt & ReceiptFile;
 
 type ReceiptViewModalProps = {
     route: {
@@ -22,17 +26,18 @@ type ReceiptViewModalProps = {
 
 function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const {translate} = useLocalize();
+    const {setAttachmentError, clearAttachmentErrors} = useAttachmentErrors();
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
 
-    const [currentReceipt, setCurrentReceipt] = useState<ReceiptFile | null>();
+    const [currentReceipt, setCurrentReceipt] = useState<ReceiptWithTransactionIDAndSource | null>();
     const [page, setPage] = useState<number>(0);
     const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false);
 
-    const [receipts] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
+    const [receipts = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) =>
             Object.values(items ?? {})
                 .map((transaction) => (transaction?.receipt ? {...transaction?.receipt, transactionID: transaction.transactionID} : undefined))
-                .filter((receipt): receipt is ReceiptFile => !!receipt),
+                .filter((receipt): receipt is ReceiptWithTransactionIDAndSource => !!receipt),
         canBeMissing: true,
     });
 
@@ -62,15 +67,13 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         handleDeleteReceipt();
     }, [handleDeleteReceipt]);
 
-    // TODO: Implement the logic to handle attachment errors
-    const handleAttachmentError = () => {};
-
     return (
         <>
             <Modal
                 type="centered"
                 isVisible
                 onClose={Navigation.goBack}
+                onModalHide={clearAttachmentErrors}
             >
                 <HeaderWithBackButton
                     title={translate('common.receipt')}
@@ -80,7 +83,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                     onThreeDotsButtonPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
                 />
                 <AttachmentCarouselView
-                    attachments={receipts ?? []}
+                    attachments={receipts}
                     source={currentReceipt?.source ?? ''}
                     page={page}
                     setPage={setPage}
@@ -89,7 +92,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                     autoHideArrows={autoHideArrows}
                     cancelAutoHideArrow={cancelAutoHideArrows}
                     setShouldShowArrows={setShouldShowArrows}
-                    onAttachmentError={handleAttachmentError}
+                    onAttachmentError={setAttachmentError}
                     shouldShowArrows={shouldShowArrows}
                 />
             </Modal>

From 92001ab6e145a29ff0611c0c33e78f8a6ddb0107 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 5 Jun 2025 13:19:39 +0200
Subject: [PATCH 17/35] chore: remove comment

---
 src/components/Attachments/AttachmentView/index.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 539d1bd2da53..5dd040dcd482 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -89,7 +89,6 @@ type AttachmentViewProps = Attachment & {
 
 function checkIsFileImage(source: string | number | ImageURISource | ImageURISource[], fileName: string | undefined) {
     const isSourceImage = typeof source === 'number' || typeof source === 'string';
-    // const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
 
     const isFileNameImage = fileName && Str.isImage(fileName);
 

From b1be0a4779a3c741bf758b99894e88abdca5412a Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 5 Jun 2025 13:37:17 +0200
Subject: [PATCH 18/35] fix: eslint CI error

---
 .../request/step/IOURequestStepScan/ReceiptViewModal/index.tsx  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index ac6f63a7553e..864c3bdd06ac 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -87,7 +87,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                     source={currentReceipt?.source ?? ''}
                     page={page}
                     setPage={setPage}
-                    attachmentID={currentReceipt?.transactionID ?? ''}
+                    attachmentID={currentReceipt?.transactionID}
                     onClose={Navigation.goBack}
                     autoHideArrows={autoHideArrows}
                     cancelAutoHideArrow={cancelAutoHideArrows}

From 58cd754b1d2880524b9f3eac4a0f140aead83e0e Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 5 Jun 2025 13:46:15 +0200
Subject: [PATCH 19/35] fix: prettier CI error

---
 src/pages/iou/request/step/IOURequestStepScan/index.native.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index f49aa82debe4..2f91f62fdaf7 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -128,7 +128,7 @@ function IOURequestStepScan({
         canBeMissing: true,
     });
     const transactions = useMemo(() => {
-        const allTransactions = initialTransactionID === CONST.IOU.OPTIMISTIC_TRANSACTION_ID ? optimisticTransactions ?? [] : [initialTransaction];
+        const allTransactions = initialTransactionID === CONST.IOU.OPTIMISTIC_TRANSACTION_ID ? (optimisticTransactions ?? []) : [initialTransaction];
         return allTransactions.filter((transaction): transaction is Transaction => !!transaction);
     }, [initialTransaction, initialTransactionID, optimisticTransactions]);
 

From 1e2622f2a6bf6fb63707becc3126d1c381843fbd Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 5 Jun 2025 17:07:43 +0200
Subject: [PATCH 20/35] chore: removed duplicate of AttachmentCarousel for
 native platform

---
 .../AttachmentCarousel/index.native.tsx       | 94 -------------------
 .../Attachments/AttachmentCarousel/index.tsx  | 14 ++-
 2 files changed, 13 insertions(+), 95 deletions(-)
 delete mode 100644 src/components/Attachments/AttachmentCarousel/index.native.tsx

diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx
deleted file mode 100644
index f43a427dbf70..000000000000
--- a/src/components/Attachments/AttachmentCarousel/index.native.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import React, {useCallback, useEffect, useState} from 'react';
-import {View} from 'react-native';
-import {useOnyx} from 'react-native-onyx';
-import type {Attachment} from '@components/Attachments/types';
-import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
-import useThemeStyles from '@hooks/useThemeStyles';
-import Navigation from '@libs/Navigation/Navigation';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import AttachmentCarouselView from './AttachmentCarouselView';
-import extractAttachments from './extractAttachments';
-import type {AttachmentCarouselProps} from './types';
-import useCarouselArrows from './useCarouselArrows';
-
-function AttachmentCarousel({report, source, attachmentID, onNavigate, setDownloadButtonVisibility, onClose, type, accountID, onAttachmentError}: AttachmentCarouselProps) {
-    const styles = useThemeStyles();
-    const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {canEvict: false, canBeMissing: true});
-    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, canBeMissing: true});
-    const [page, setPage] = useState<number>();
-    const [attachments, setAttachments] = useState<Attachment[]>([]);
-    const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
-
-    const compareImage = useCallback((attachment: Attachment) => (attachmentID ? attachment.attachmentID === attachmentID : attachment.source === source), [attachmentID, source]);
-
-    useEffect(() => {
-        const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined;
-        let newAttachments: Attachment[] = [];
-        if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) {
-            newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, report});
-        } else if (type === CONST.ATTACHMENT_TYPE.ONBOARDING) {
-            newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.ONBOARDING, {parentReportAction, reportActions: reportActions ?? undefined, report});
-        } else {
-            newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, report});
-        }
-
-        let newIndex = newAttachments.findIndex(compareImage);
-        const index = attachments.findIndex(compareImage);
-
-        // If newAttachments includes an attachment with the same index, update newIndex to that index.
-        // Previously, uploading an attachment offline would dismiss the modal when the image was previewed and the connection was restored.
-        // Now, instead of dismissing the modal, we replace it with the new attachment that has the same index.
-        if (newIndex === -1 && index !== -1 && newAttachments.at(index)) {
-            newIndex = index;
-        }
-
-        // If no matching attachment with the same index, dismiss the modal
-        if (newIndex === -1 && index !== -1 && attachments.at(index)) {
-            Navigation.dismissModal();
-        } else {
-            setPage(newIndex);
-            setAttachments(newAttachments);
-
-            // Update the download button visibility in the parent modal
-            if (setDownloadButtonVisibility) {
-                setDownloadButtonVisibility(newIndex !== -1);
-            }
-
-            const attachment = newAttachments.at(newIndex);
-            // Update the parent modal's state with the source and name from the mapped attachments
-            if (newIndex !== -1 && attachment !== undefined && onNavigate) {
-                onNavigate(attachment);
-            }
-        }
-        // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
-    }, [reportActions, compareImage, report]);
-
-    if (page == null) {
-        return (
-            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
-                <FullScreenLoadingIndicator />
-            </View>
-        );
-    }
-
-    return (
-        <AttachmentCarouselView
-            page={page}
-            setPage={setPage}
-            attachments={attachments}
-            shouldShowArrows={shouldShowArrows}
-            autoHideArrows={autoHideArrows}
-            cancelAutoHideArrow={cancelAutoHideArrows}
-            setShouldShowArrows={setShouldShowArrows}
-            onClose={onClose}
-            source={source}
-            onAttachmentError={onAttachmentError}
-            attachmentID={attachmentID}
-        />
-    );
-}
-
-AttachmentCarousel.displayName = 'AttachmentCarousel';
-
-export default AttachmentCarousel;
diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx
index f99f2745166c..7abfe734a481 100644
--- a/src/components/Attachments/AttachmentCarousel/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/index.tsx
@@ -1,7 +1,10 @@
 import isEqual from 'lodash/isEqual';
 import React, {useCallback, useEffect, useState} from 'react';
+import {View} from 'react-native';
 import type {Attachment} from '@components/Attachments/types';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
 import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
 import Navigation from '@libs/Navigation/Navigation';
 import CONST from '@src/CONST';
@@ -15,8 +18,9 @@ function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownlo
     const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, {canEvict: false, canBeMissing: true});
     const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, canBeMissing: true});
     const canUseTouchScreen = canUseTouchScreenUtil();
+    const styles = useThemeStyles();
 
-    const [page, setPage] = useState(0);
+    const [page, setPage] = useState<number>();
     const [attachments, setAttachments] = useState<Attachment[]>([]);
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
 
@@ -82,6 +86,14 @@ function AttachmentCarousel({report, attachmentID, source, onNavigate, setDownlo
         }
     }, [reportActions, parentReportActions, compareImage, attachments, setDownloadButtonVisibility, onNavigate, accountID, type, report]);
 
+    if (page == null) {
+        return (
+            <View style={[styles.flex1, styles.attachmentCarouselContainer]}>
+                <FullScreenLoadingIndicator />
+            </View>
+        );
+    }
+
     return (
         <AttachmentCarouselView
             page={page}

From 9be12a412d226533f15a9504fb86d71e3742a2bf Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 5 Jun 2025 17:48:28 +0200
Subject: [PATCH 21/35] fix: no found page after deleting a receipt

---
 .../step/IOURequestStepScan/ReceiptViewModal/index.tsx      | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 864c3bdd06ac..967f6a8940f5 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -1,4 +1,5 @@
 import React, {useCallback, useEffect, useState} from 'react';
+import {InteractionManager} from 'react-native';
 import {useOnyx} from 'react-native-onyx';
 import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
@@ -58,7 +59,10 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
             return;
         }
 
-        removeTransactionReceipt(currentReceipt.transactionID);
+        InteractionManager.runAfterInteractions(() => {
+            removeTransactionReceipt(currentReceipt.transactionID);
+        });
+
         Navigation.goBack();
     }, [currentReceipt]);
 

From 5a14e1fd1bc242f1b079e1b12eec2a9eecf0ad6b Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Fri, 6 Jun 2025 10:55:02 +0200
Subject: [PATCH 22/35] revert: CarouselButtons changes

---
 .../Attachments/AttachmentCarousel/CarouselButtons.tsx       | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
index 3688b9ac5518..04fd1163af44 100644
--- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
+++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx
@@ -35,11 +35,10 @@ type CarouselButtonsProps = {
 function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}: CarouselButtonsProps) {
     const theme = useTheme();
     const styles = useThemeStyles();
-    const {translate} = useLocalize();
-    const {shouldUseNarrowLayout} = useResponsiveLayout();
-
     const isBackDisabled = page === 0;
     const isForwardDisabled = page === attachments.length - 1;
+    const {translate} = useLocalize();
+    const {shouldUseNarrowLayout} = useResponsiveLayout();
 
     return shouldShowArrows ? (
         <>

From 84e22c081640f1c296a947b52f8d1140784ed3b7 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Mon, 9 Jun 2025 16:09:50 +0200
Subject: [PATCH 23/35] add: backTo param to handle correct navigation after
 reload

---
 src/ROUTES.ts                                        |  4 ++--
 src/libs/Navigation/types.ts                         |  1 +
 .../IOURequestStepScan/ReceiptPreviews/index.tsx     |  3 ++-
 .../IOURequestStepScan/ReceiptViewModal/index.tsx    | 12 +++++++++---
 4 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 8f00826604c2..da169ef277e1 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -925,8 +925,8 @@ const ROUTES = {
 
     MONEY_REQUEST_RECEIPT_VIEW_MODAL: {
         route: 'receipt-view-modal/:transactionID',
-        getRoute: (transactionID: string) => `receipt-view-modal/${transactionID}` as const,
-    },
+        getRoute: (transactionID: string, backTo: string) => getUrlWithBackToParam(`receipt-view-modal/${transactionID}`, backTo),
+    } as const,
 
     MONEY_REQUEST_STATE_SELECTOR: {
         route: 'submit/state',
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 43004b3d6c17..fd5b3997a83b 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1415,6 +1415,7 @@ type MoneyRequestNavigatorParamList = {
     };
     [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: {
         transactionID: string;
+        backTo: Routes;
     };
     [SCREENS.MONEY_REQUEST.STEP_CURRENCY]: {
         action: IOUAction;
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
index 2cafb5cb0c3a..74c8fcb8497c 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptPreviews/index.tsx
@@ -41,6 +41,7 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: Rece
     const theme = useTheme();
     const {translate} = useLocalize();
     const {windowWidth} = useWindowDimensions();
+    const backTo = Navigation.getActiveRoute();
     const isPreviewsVisible = useSharedValue(false);
     const previewsHeight = styles.receiptPlaceholder.height + styles.pv2.paddingVertical * 2;
     const previewItemWidth = styles.receiptPlaceholder.width + styles.receiptPlaceholder.marginRight;
@@ -96,7 +97,7 @@ function ReceiptPreviews({submit, setTabSwipeDisabled, isMultiScanEnabled}: Rece
                 accessible
                 accessibilityLabel={translate('common.receipt')}
                 accessibilityRole={CONST.ROLE.BUTTON}
-                onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute(item.transactionID))}
+                onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_RECEIPT_VIEW_MODAL.getRoute(item.transactionID, backTo))}
             >
                 <Image
                     source={{uri: item.source}}
diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 967f6a8940f5..25a355e8fb28 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -13,6 +13,7 @@ import Navigation from '@libs/Navigation/Navigation';
 import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
 import {removeTransactionReceipt} from '@userActions/TransactionEdit';
 import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
 import type {Receipt} from '@src/types/onyx/Transaction';
 
 type ReceiptWithTransactionIDAndSource = Receipt & ReceiptFile;
@@ -21,6 +22,7 @@ type ReceiptViewModalProps = {
     route: {
         params: {
             transactionID: string;
+            backTo: Route;
         };
     };
 };
@@ -71,17 +73,21 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         handleDeleteReceipt();
     }, [handleDeleteReceipt]);
 
+    const handleGoBack = useCallback(() => {
+        Navigation.goBack(route.params.backTo);
+    }, [route.params.backTo]);
+
     return (
         <>
             <Modal
                 type="centered"
                 isVisible
-                onClose={Navigation.goBack}
+                onClose={handleGoBack}
                 onModalHide={clearAttachmentErrors}
             >
                 <HeaderWithBackButton
                     title={translate('common.receipt')}
-                    onBackButtonPress={Navigation.goBack}
+                    onBackButtonPress={handleGoBack}
                     shouldShowThreeDotsButton
                     threeDotsMenuIcon={Expensicons.Trashcan}
                     onThreeDotsButtonPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
@@ -92,7 +98,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                     page={page}
                     setPage={setPage}
                     attachmentID={currentReceipt?.transactionID}
-                    onClose={Navigation.goBack}
+                    onClose={handleGoBack}
                     autoHideArrows={autoHideArrows}
                     cancelAutoHideArrow={cancelAutoHideArrows}
                     setShouldShowArrows={setShouldShowArrows}

From ac069d77a6b7c33738b26a9375d2b461ad7d6d13 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Mon, 9 Jun 2025 17:14:27 +0200
Subject: [PATCH 24/35] add: check for localSource in AttachmentView

---
 src/components/Attachments/AttachmentView/index.tsx | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 5dd040dcd482..3cbf2f060051 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -88,7 +88,9 @@ type AttachmentViewProps = Attachment & {
 };
 
 function checkIsFileImage(source: string | number | ImageURISource | ImageURISource[], fileName: string | undefined) {
-    const isSourceImage = typeof source === 'number' || typeof source === 'string';
+    const isLocalSource = typeof source === 'string' && /^file:|^blob:/.test(source);
+
+    const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source)) || isLocalSource;
 
     const isFileNameImage = fileName && Str.isImage(fileName);
 

From b395fdae6e276c4d84d9a842161303d70cd8195c Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Tue, 10 Jun 2025 15:38:10 +0200
Subject: [PATCH 25/35] chore: moved check for local source next to if block
 responsible for returning an image

---
 src/components/Attachments/AttachmentView/index.tsx | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx
index 3cbf2f060051..d5506d84c6bf 100644
--- a/src/components/Attachments/AttachmentView/index.tsx
+++ b/src/components/Attachments/AttachmentView/index.tsx
@@ -88,9 +88,7 @@ type AttachmentViewProps = Attachment & {
 };
 
 function checkIsFileImage(source: string | number | ImageURISource | ImageURISource[], fileName: string | undefined) {
-    const isLocalSource = typeof source === 'string' && /^file:|^blob:/.test(source);
-
-    const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source)) || isLocalSource;
+    const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
 
     const isFileNameImage = fileName && Str.isImage(fileName);
 
@@ -252,9 +250,14 @@ function AttachmentView({
     // For this check we use both source and file.name since temporary file source is a blob
     // both PDFs and images will appear as images when pasted into the text field.
     // We also check for numeric source since this is how static images (used for preview) are represented in RN.
+
+    // isLocalSource checks if the source is blob as that's the type of the temp image coming from mobile web
     const isFileImage = checkIsFileImage(source, file?.name);
+    const isLocalSourceImage = typeof source === 'string' && source.startsWith('blob:');
+
+    const isImage = isFileImage ?? isLocalSourceImage;
 
-    if (isFileImage) {
+    if (isImage) {
         if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) {
             return (
                 <View style={[styles.flexColumn, styles.alignItemsCenter, styles.justifyContentCenter]}>
@@ -301,7 +304,7 @@ function AttachmentView({
                         file={file}
                         isAuthTokenRequired={isAuthTokenRequired}
                         loadComplete={loadComplete}
-                        isImage={isFileImage}
+                        isImage={isImage}
                         onPress={onPress}
                         onError={() => {
                             if (isOffline) {

From 739020bddc87a04613f2077c218d44e7e7794f5f Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 11 Jun 2025 11:56:39 +0200
Subject: [PATCH 26/35] fix: in native modal always opening on first image

---
 .../request/step/IOURequestStepScan/ReceiptViewModal/index.tsx  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 25a355e8fb28..91aae4386dd6 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -33,7 +33,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
 
     const [currentReceipt, setCurrentReceipt] = useState<ReceiptWithTransactionIDAndSource | null>();
-    const [page, setPage] = useState<number>(0);
+    const [page, setPage] = useState<number>(-1);
     const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false);
 
     const [receipts = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {

From df21be3773fa2982a5b7d1ac331c413da6529de8 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 12 Jun 2025 09:16:47 +0200
Subject: [PATCH 27/35] fix: ts error

---
 .../AttachmentCarousel/AttachmentCarouselView/types.ts          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts
index 29d9cea6b431..ed7f98a1a858 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/types.ts
@@ -19,7 +19,7 @@ type AttachmentCarouselViewProps = {
     /** Callback for cancelling auto hiding of carousel button arrows */
     cancelAutoHideArrow: () => void;
     /** A callback that is called when swipe-down-to-close gesture happens */
-    onClose: () => void;
+    onClose?: () => void;
     /** Sets current page */
     setPage: (page: number) => void;
     /** The report currently being looked at */

From 1b20901e49123dd76e0d4b3ce5712efd0b8b9810 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Fri, 13 Jun 2025 10:46:11 +0200
Subject: [PATCH 28/35] chore: change canUseMultiScan to true for ad hoc build

---
 src/pages/iou/request/step/IOURequestStepScan/index.native.tsx | 2 +-
 src/pages/iou/request/step/IOURequestStepScan/index.tsx        | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index f4ed982554c7..4db0bd3daabd 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -105,7 +105,7 @@ function IOURequestStepScan({
     const [flash, setFlash] = useState(false);
     // TODO: use correct canUseMultiScan value when all multi-scan functionality is implemented
     // const canUseMultiScan = isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_SCAN) && !isEditing && iouType !== CONST.IOU.TYPE.SPLIT;
-    const canUseMultiScan = false;
+    const canUseMultiScan = true;
     const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false);
     const [receiptFiles, setReceiptFiles] = useState<ReceiptFile[]>([]);
     const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true});
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
index a8173c07375c..89e92edc2858 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -136,7 +136,7 @@ function IOURequestStepScan({
     const isEditing = action === CONST.IOU.ACTION.EDIT;
     // TODO: use correct canUseMultiScan value when all multi-scan functionality is implemented
     // const canUseMultiScan = isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_SCAN) && !isEditing && iouType !== CONST.IOU.TYPE.SPLIT;
-    const canUseMultiScan = false;
+    const canUseMultiScan = true;
 
     const [optimisticTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {
         selector: (items) => Object.values(items ?? {}),

From af477937806214fc7ba3c9b86cff300e8023cd68 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Mon, 16 Jun 2025 14:10:24 +0200
Subject: [PATCH 29/35] fix: disabled help button in header

---
 .../request/step/IOURequestStepScan/ReceiptViewModal/index.tsx   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 91aae4386dd6..aaeb8c7db740 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -87,6 +87,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
             >
                 <HeaderWithBackButton
                     title={translate('common.receipt')}
+                    shouldDisplayHelpButton={false}
                     onBackButtonPress={handleGoBack}
                     shouldShowThreeDotsButton
                     threeDotsMenuIcon={Expensicons.Trashcan}

From 5395e1beb3db7cf34a63f4c05e2870b65dae75aa Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 18 Jun 2025 00:03:51 +0200
Subject: [PATCH 30/35] fix: double modal showing up on delete receipt press

---
 .../ReceiptViewModal/index.tsx                | 30 ++++++++++++++-----
 .../request/step/IOURequestStepScan/types.ts  |  2 +-
 2 files changed, 23 insertions(+), 9 deletions(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index aaeb8c7db740..b8772712c5a0 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -4,14 +4,17 @@ import {useOnyx} from 'react-native-onyx';
 import AttachmentCarouselView from '@components/Attachments/AttachmentCarousel/AttachmentCarouselView';
 import useCarouselArrows from '@components/Attachments/AttachmentCarousel/useCarouselArrows';
 import useAttachmentErrors from '@components/Attachments/AttachmentView/useAttachmentErrors';
+import Button from '@components/Button';
 import ConfirmModal from '@components/ConfirmModal';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import * as Expensicons from '@components/Icon/Expensicons';
 import Modal from '@components/Modal';
 import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
 import type {ReceiptFile} from '@pages/iou/request/step/IOURequestStepScan/types';
 import {removeTransactionReceipt} from '@userActions/TransactionEdit';
+import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {Route} from '@src/ROUTES';
 import type {Receipt} from '@src/types/onyx/Transaction';
@@ -31,6 +34,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
     const {translate} = useLocalize();
     const {setAttachmentError, clearAttachmentErrors} = useAttachmentErrors();
     const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows();
+    const styles = useThemeStyles();
 
     const [currentReceipt, setCurrentReceipt] = useState<ReceiptWithTransactionIDAndSource | null>();
     const [page, setPage] = useState<number>(-1);
@@ -68,8 +72,12 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
         Navigation.goBack();
     }, [currentReceipt]);
 
-    const deleteReceiptAndCloseModal = useCallback(() => {
+    const handleCloseConfirmModal = () => {
         setIsDeleteReceiptConfirmModalVisible(false);
+    };
+
+    const deleteReceipt = useCallback(() => {
+        handleCloseConfirmModal();
         handleDeleteReceipt();
     }, [handleDeleteReceipt]);
 
@@ -80,7 +88,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
     return (
         <>
             <Modal
-                type="centered"
+                type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
                 isVisible
                 onClose={handleGoBack}
                 onModalHide={clearAttachmentErrors}
@@ -89,10 +97,15 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                     title={translate('common.receipt')}
                     shouldDisplayHelpButton={false}
                     onBackButtonPress={handleGoBack}
-                    shouldShowThreeDotsButton
-                    threeDotsMenuIcon={Expensicons.Trashcan}
-                    onThreeDotsButtonPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
-                />
+                    onCloseButtonPress={handleCloseConfirmModal}
+                >
+                    <Button
+                        shouldShowRightIcon
+                        iconRight={Expensicons.Trashcan}
+                        onPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
+                        innerStyles={styles.bgTransparent}
+                    />
+                </HeaderWithBackButton>
                 <AttachmentCarouselView
                     attachments={receipts}
                     source={currentReceipt?.source ?? ''}
@@ -110,11 +123,12 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
             <ConfirmModal
                 title={translate('receipt.deleteReceipt')}
                 isVisible={isDeleteReceiptConfirmModalVisible}
-                onConfirm={deleteReceiptAndCloseModal}
-                onCancel={() => setIsDeleteReceiptConfirmModalVisible(false)}
+                onConfirm={deleteReceipt}
+                onCancel={handleCloseConfirmModal}
                 prompt={translate('receipt.deleteConfirmation')}
                 confirmText={translate('common.delete')}
                 cancelText={translate('common.cancel')}
+                onBackdropPress={handleCloseConfirmModal}
                 danger
             />
         </>
diff --git a/src/pages/iou/request/step/IOURequestStepScan/types.ts b/src/pages/iou/request/step/IOURequestStepScan/types.ts
index d1ffa33ceaea..6ad8d973b87e 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/types.ts
+++ b/src/pages/iou/request/step/IOURequestStepScan/types.ts
@@ -26,7 +26,7 @@ type IOURequestStepScanProps = WithCurrentUserPersonalDetailsProps &
 
 type ReceiptFile = {
     source: ReceiptSource;
-    file: FileObject;
+    file?: FileObject;
     transactionID: string;
 };
 

From c3d400e33e39fc331129817aefee4bc891efe496 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Mon, 23 Jun 2025 13:39:36 +0200
Subject: [PATCH 31/35] fix: icon size

---
 .../request/step/IOURequestStepScan/ReceiptViewModal/index.tsx   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index b8772712c5a0..5491c721606f 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -104,6 +104,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                         iconRight={Expensicons.Trashcan}
                         onPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
                         innerStyles={styles.bgTransparent}
+                        large
                     />
                 </HeaderWithBackButton>
                 <AttachmentCarouselView

From 053056a1846b0f7a9a6f494ef97348416d93808b Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 25 Jun 2025 09:35:55 +0200
Subject: [PATCH 32/35] fix: incorrect spacing for photos on ios

---
 .../AttachmentCarouselView/index.native.tsx                  | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
index 85dcc5927ec6..bc75feb3a795 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
@@ -9,7 +9,9 @@ import * as Illustrations from '@components/Icon/Illustrations';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
+import getPlatform from '@libs/getPlatform';
 import variables from '@styles/variables';
+import CONST from '@src/CONST';
 import type AttachmentCarouselViewProps from './types';
 
 function AttachmentCarouselView({
@@ -31,6 +33,7 @@ function AttachmentCarouselView({
     const canUseTouchScreen = canUseTouchScreenUtil();
     const styles = useThemeStyles();
     const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(attachmentID ?? source);
+    const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
 
     const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
 
@@ -74,7 +77,7 @@ function AttachmentCarouselView({
 
     return (
         <View
-            style={[styles.flex1, styles.attachmentCarouselContainer]}
+            style={[styles.flex1, styles.attachmentCarouselContainer, isIOSNative && styles.mhn2]}
             onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
             onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
         >

From 38d07528e86c8cc97e7835ade0cb919858cb1422 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 25 Jun 2025 11:35:31 +0200
Subject: [PATCH 33/35] Revert "fix: incorrect spacing for photos on ios"

This reverts commit 053056a1846b0f7a9a6f494ef97348416d93808b.
---
 .../AttachmentCarouselView/index.native.tsx                  | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
index bc75feb3a795..85dcc5927ec6 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.native.tsx
@@ -9,9 +9,7 @@ import * as Illustrations from '@components/Icon/Illustrations';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {canUseTouchScreen as canUseTouchScreenUtil} from '@libs/DeviceCapabilities';
-import getPlatform from '@libs/getPlatform';
 import variables from '@styles/variables';
-import CONST from '@src/CONST';
 import type AttachmentCarouselViewProps from './types';
 
 function AttachmentCarouselView({
@@ -33,7 +31,6 @@ function AttachmentCarouselView({
     const canUseTouchScreen = canUseTouchScreenUtil();
     const styles = useThemeStyles();
     const [activeAttachmentID, setActiveAttachmentID] = useState<AttachmentSource>(attachmentID ?? source);
-    const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
 
     const pagerRef = useRef<AttachmentCarouselPagerHandle>(null);
 
@@ -77,7 +74,7 @@ function AttachmentCarouselView({
 
     return (
         <View
-            style={[styles.flex1, styles.attachmentCarouselContainer, isIOSNative && styles.mhn2]}
+            style={[styles.flex1, styles.attachmentCarouselContainer]}
             onMouseEnter={() => !canUseTouchScreen && setShouldShowArrows(true)}
             onMouseLeave={() => !canUseTouchScreen && setShouldShowArrows(false)}
         >

From fbcc34586e651c588c952a07f950f9dc823ff3d2 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Wed, 25 Jun 2025 18:39:43 +0200
Subject: [PATCH 34/35] fix: lint error

---
 .../AttachmentCarousel/AttachmentCarouselView/index.tsx       | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
index 28475535afa1..40d78b1104ea 100644
--- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
+++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselView/index.tsx
@@ -1,5 +1,5 @@
 import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import type {MutableRefObject} from 'react';
+import type {RefObject} from 'react';
 import type {ListRenderItemInfo} from 'react-native';
 import {Keyboard, PixelRatio, View} from 'react-native';
 import {Gesture, GestureDetector} from 'react-native-gesture-handler';
@@ -216,7 +216,7 @@ function AttachmentCarouselView({
                     scrollTo(scrollRef, newIndex * cellWidth, 0, true);
                 })
                 // eslint-disable-next-line react-compiler/react-compiler
-                .withRef(pagerRef as MutableRefObject<GestureType | undefined>),
+                .withRef(pagerRef as RefObject<GestureType | undefined>),
         [attachments.length, canUseTouchScreen, cellWidth, page, isScrollEnabled, scrollRef, isPagerScrolling],
     );
 

From 9d7ebfb23bd024f67b28eb44f426536a8b0473a0 Mon Sep 17 00:00:00 2001
From: Marcin Swornowski <swor71@gmail.com>
Date: Thu, 26 Jun 2025 16:07:21 +0200
Subject: [PATCH 35/35] fix: confirm delete modal not showing up

---
 .../ReceiptViewModal/index.tsx                | 66 +++++++++----------
 1 file changed, 32 insertions(+), 34 deletions(-)

diff --git a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
index 5491c721606f..37c6b276864d 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/ReceiptViewModal/index.tsx
@@ -86,41 +86,39 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
     }, [route.params.backTo]);
 
     return (
-        <>
-            <Modal
-                type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
-                isVisible
-                onClose={handleGoBack}
-                onModalHide={clearAttachmentErrors}
+        <Modal
+            type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
+            isVisible
+            onClose={handleGoBack}
+            onModalHide={clearAttachmentErrors}
+        >
+            <HeaderWithBackButton
+                title={translate('common.receipt')}
+                shouldDisplayHelpButton={false}
+                onBackButtonPress={handleGoBack}
+                onCloseButtonPress={handleCloseConfirmModal}
             >
-                <HeaderWithBackButton
-                    title={translate('common.receipt')}
-                    shouldDisplayHelpButton={false}
-                    onBackButtonPress={handleGoBack}
-                    onCloseButtonPress={handleCloseConfirmModal}
-                >
-                    <Button
-                        shouldShowRightIcon
-                        iconRight={Expensicons.Trashcan}
-                        onPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
-                        innerStyles={styles.bgTransparent}
-                        large
-                    />
-                </HeaderWithBackButton>
-                <AttachmentCarouselView
-                    attachments={receipts}
-                    source={currentReceipt?.source ?? ''}
-                    page={page}
-                    setPage={setPage}
-                    attachmentID={currentReceipt?.transactionID}
-                    onClose={handleGoBack}
-                    autoHideArrows={autoHideArrows}
-                    cancelAutoHideArrow={cancelAutoHideArrows}
-                    setShouldShowArrows={setShouldShowArrows}
-                    onAttachmentError={setAttachmentError}
-                    shouldShowArrows={shouldShowArrows}
+                <Button
+                    shouldShowRightIcon
+                    iconRight={Expensicons.Trashcan}
+                    onPress={() => setIsDeleteReceiptConfirmModalVisible(true)}
+                    innerStyles={styles.bgTransparent}
+                    large
                 />
-            </Modal>
+            </HeaderWithBackButton>
+            <AttachmentCarouselView
+                attachments={receipts}
+                source={currentReceipt?.source ?? ''}
+                page={page}
+                setPage={setPage}
+                attachmentID={currentReceipt?.transactionID}
+                onClose={handleGoBack}
+                autoHideArrows={autoHideArrows}
+                cancelAutoHideArrow={cancelAutoHideArrows}
+                setShouldShowArrows={setShouldShowArrows}
+                onAttachmentError={setAttachmentError}
+                shouldShowArrows={shouldShowArrows}
+            />
             <ConfirmModal
                 title={translate('receipt.deleteReceipt')}
                 isVisible={isDeleteReceiptConfirmModalVisible}
@@ -132,7 +130,7 @@ function ReceiptViewModal({route}: ReceiptViewModalProps) {
                 onBackdropPress={handleCloseConfirmModal}
                 danger
             />
-        </>
+        </Modal>
     );
 }
 
