From f9d17025447b868f642061cbbab1e516658f09c4 Mon Sep 17 00:00:00 2001 From: Marcin Swornowski 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 ? ( + + ) : ( + <> + 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(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 ( - - {page === -1 ? ( - + + updatePage(newPage)} + onClose={onClose} + ref={pagerRef} + reportID={report.reportID} /> - ) : ( - <> - cycleThroughAttachments(-1)} - onForward={() => cycleThroughAttachments(1)} - autoHideArrow={autoHideArrows} - cancelAutoHideArrow={cancelAutoHideArrows} - /> - - updatePage(newPage)} - onClose={onClose} - ref={pagerRef} - reportID={report.reportID} - /> - - )} - + + ); } 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 ? ( - - ) : ( - <> - cycleThroughAttachments(-1)} - onForward={() => cycleThroughAttachments(1)} - autoHideArrow={autoHideArrows} - cancelAutoHideArrow={cancelAutoHideArrows} - /> - - - - - - - - - )} + + + + + + + + + ); } From 21096e9654b63cf733f5996375db5e3dcbbeba60 Mon Sep 17 00:00:00 2001 From: Marcin Swornowski 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 require('../../../../pages/iou/request/step/IOURequestStepDestination').default, [SCREENS.MONEY_REQUEST.STEP_TIME_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepTime').default, [SCREENS.MONEY_REQUEST.STEP_SUBRATE_EDIT]: () => require('../../../../pages/iou/request/step/IOURequestStepSubrate').default, + [SCREENS.MONEY_REQUEST.RECEIPT_VIEW_MODAL]: () => require('../../../../pages/iou/request/step/IOURequestStepScan/ReceiptViewModal').default, }); const TravelModalStackNavigator = createModalStackNavigator({ 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['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 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 ( - - + + - - + + ); } 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(selectedIndex); + const {shouldShowArrows, setShouldShowArrows, autoHideArrows, cancelAutoHideArrows} = useCarouselArrows(); + const pagerRef = useRef(null); + + // const onCycleThroughAttachments = (deltaSlide: number) => { + // const newIndex = (currentIndex + deltaSlide + sources.length) % sources.length; + // setCurrentIndex(newIndex); + // }; + + const [page, setPage] = useState(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 ( + { + setIsOpen(false); + Navigation.goBack(); + }} + > + + + + + updatePage(newPage)} + onClose={() => {}} + ref={pagerRef} + /> + + + + ); +} + +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({ }} /> )} +