diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 48190fb3c759..c9165a945025 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -1,7 +1,7 @@ import {format} from 'date-fns'; import {Str} from 'expensify-common'; import lodashIsEqual from 'lodash/isEqual'; -import React, {memo, useMemo, useReducer, useState} from 'react'; +import React, {memo, useMemo, useReducer} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -29,7 +29,6 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Unit} from '@src/types/onyx/Policy'; import ConfirmedRoute from './ConfirmedRoute'; -import ConfirmModal from './ConfirmModal'; import MenuItem from './MenuItem'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import PDFThumbnail from './PDFThumbnail'; @@ -224,9 +223,6 @@ function MoneyRequestConfirmationListFooter({ // A flag and a toggler for showing the rest of the form fields const [shouldExpandFields, toggleShouldExpandFields] = useReducer((state) => !state, false); - const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - const [invalidAttachmentPrompt, setInvalidAttachmentPrompt] = useState(translate('attachmentPicker.protectedPDFNotSupported')); - // A flag for showing the tags field // TODO: remove the !isTypeInvoice from this condition after BE supports tags for invoices: https://github.com/Expensify/App/issues/41281 const shouldShowTags = useMemo(() => isPolicyExpenseChat && OptionsListUtils.hasEnabledTags(policyTagLists) && !isTypeInvoice, [isPolicyExpenseChat, isTypeInvoice, policyTagLists]); @@ -547,16 +543,6 @@ function MoneyRequestConfirmationListFooter({ { - setIsAttachmentInvalid(true); - setInvalidAttachmentPrompt(translate('attachmentPicker.protectedPDFNotSupported')); - }} - onLoadError={() => { - setInvalidAttachmentPrompt(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); - setIsAttachmentInvalid(true); - }} /> ) : ( @@ -591,7 +577,6 @@ function MoneyRequestConfirmationListFooter({ translate, shouldDisplayReceipt, resolvedReceiptImage, - isAttachmentInvalid, isThumbnail, resolvedThumbnail, receiptThumbnail, @@ -602,10 +587,6 @@ function MoneyRequestConfirmationListFooter({ ], ); - const navigateBack = () => { - Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); - }; - return ( <> {isTypeInvoice && ( @@ -655,15 +636,6 @@ function MoneyRequestConfirmationListFooter({ /> )} {shouldShowAllFields && supplementaryFields} - ); } diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx index 27d41ede3263..66a42f6f0830 100644 --- a/src/components/PDFThumbnail/index.native.tsx +++ b/src/components/PDFThumbnail/index.native.tsx @@ -7,7 +7,7 @@ import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import PDFThumbnailError from './PDFThumbnailError'; import type PDFThumbnailProps from './types'; -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError, onLoadSuccess}: PDFThumbnailProps) { const styles = useThemeStyles(); const sizeStyles = [styles.w100, styles.h100]; const [failedToLoad, setFailedToLoad] = useState(false); @@ -24,15 +24,21 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena singlePage style={sizeStyles} onError={(error) => { - if (onLoadError) { - onLoadError(); - } if ('message' in error && typeof error.message === 'string' && error.message.match(/password/i) && onPassword) { onPassword(); return; } + if (onLoadError) { + onLoadError(); + } setFailedToLoad(true); }} + onLoadComplete={() => { + if (!onLoadSuccess) { + return; + } + onLoadSuccess(); + }} /> )} {failedToLoad && } diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index 8e79c027cf03..f064229f0739 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -12,7 +12,7 @@ if (!pdfjs.GlobalWorkerOptions.workerSrc) { pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'})); } -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword, onLoadError, onLoadSuccess}: PDFThumbnailProps) { const styles = useThemeStyles(); const [failedToLoad, setFailedToLoad] = useState(false); @@ -30,6 +30,12 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena onLoad={() => { setFailedToLoad(false); }} + onLoadSuccess={() => { + if (!onLoadSuccess) { + return; + } + onLoadSuccess(); + }} onLoadError={() => { if (onLoadError) { onLoadError(); @@ -43,7 +49,7 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena ), - [isAuthTokenRequired, previewSourceURL, onPassword, onLoadError], + [isAuthTokenRequired, previewSourceURL, onPassword, onLoadError, onLoadSuccess], ); return ( diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index 349669ecc33e..3d79e7c026d2 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -18,6 +18,9 @@ type PDFThumbnailProps = { /** Callback to call if PDF can't be loaded(corrupted) */ onLoadError?: () => void; + + /** Callback to call if PDF is loaded */ + onLoadSuccess?: () => void; }; export default PDFThumbnailProps; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 8d888d695b01..434f8a87b853 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -1,4 +1,5 @@ import {useFocusEffect} from '@react-navigation/core'; +import {Str} from 'expensify-common'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Alert, AppState, InteractionManager, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; @@ -16,6 +17,7 @@ import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import ImageSVG from '@components/ImageSVG'; +import PDFThumbnail from '@components/PDFThumbnail'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; @@ -65,6 +67,8 @@ function IOURequestStepScan({ const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); const [didCapturePhoto, setDidCapturePhoto] = useState(false); + const [pdfFile, setPdfFile] = useState(null); + const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction); const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? ''; const transactionTaxAmount = transaction?.taxAmount ?? 0; @@ -380,11 +384,17 @@ function IOURequestStepScan({ /** * Sets the Receipt objects and navigates the user to the next page */ - const setReceiptAndNavigate = (file: FileObject) => { + const setReceiptAndNavigate = (file: FileObject, isPdfValidated?: boolean) => { if (!validateReceipt(file)) { return; } + // If we have a pdf file and if it is not validated then set the pdf file for validation and return + if (Str.isPDF(file.name ?? '') && !isPdfValidated) { + setPdfFile(file); + return; + } + // Store the receipt on the transaction object in Onyx // On Android devices, fetching blob for a file with name containing spaces fails to retrieve the type of file. // So, let us also save the file type in receipt for later use during blob fetch @@ -455,6 +465,26 @@ function IOURequestStepScan({ shouldShowWrapper={!!backTo} testID={IOURequestStepScan.displayName} > + {pdfFile && ( + { + setPdfFile(null); + if (pdfFile) { + setReceiptAndNavigate(pdfFile, true); + } + }} + onPassword={() => { + setPdfFile(null); + Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.protectedPDFNotSupported')); + }} + onLoadError={() => { + setPdfFile(null); + Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedAttachment')); + }} + /> + )} {cameraPermissionStatus !== RESULTS.GRANTED && ( (); const [attachmentInvalidReason, setAttachmentValidReason] = useState(); - + const [pdfFile, setPdfFile] = useState(null); const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); @@ -188,6 +189,7 @@ function IOURequestStepScan({ setIsAttachmentInvalid(isInvalid); setAttachmentInvalidReasonTitle(title); setAttachmentValidReason(reason); + setPdfFile(null); }; function validateReceipt(file: FileObject) { @@ -212,16 +214,6 @@ function IOURequestStepScan({ setUploadReceiptError(true, 'attachmentPicker.attachmentTooSmall', 'attachmentPicker.sizeNotMet'); return false; } - - if (fileExtension === 'pdf') { - return isPdfFilePasswordProtected(file).then((isProtected: boolean) => { - if (isProtected) { - setUploadReceiptError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.protectedPDFNotSupported'); - return false; - } - return true; - }); - } return true; }) .catch(() => { @@ -425,11 +417,17 @@ function IOURequestStepScan({ /** * Sets the Receipt objects and navigates the user to the next page */ - const setReceiptAndNavigate = (file: FileObject) => { + const setReceiptAndNavigate = (file: FileObject, isPdfValidated?: boolean) => { validateReceipt(file).then((isFileValid) => { if (!isFileValid) { return; } + + // If we have a pdf file and if it is not validated then set the pdf file for validation and return + if (Str.isPDF(file.name ?? '') && !isPdfValidated) { + setPdfFile(file); + return; + } // Store the receipt on the transaction object in Onyx const source = URL.createObjectURL(file as Blob); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -519,9 +517,27 @@ function IOURequestStepScan({ [], ); + const PDFThumbnailView = pdfFile ? ( + { + setPdfFile(null); + setReceiptAndNavigate(pdfFile, true); + }} + onPassword={() => { + setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.protectedPDFNotSupported'); + }} + onLoadError={() => { + setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment'); + }} + /> + ) : null; + const mobileCameraView = () => ( <> + {PDFThumbnailView} {((cameraPermissionState === 'prompt' && !isQueriedPermissionState) || (cameraPermissionState === 'granted' && isEmptyObject(videoConstraints))) && ( ( <> + {PDFThumbnailView} setReceiptImageTopPosition(PixelRatio.roundToNearestPixel((nativeEvent.layout as DOMRect).top))}> justifyContent: 'center', }, + invisiblePDF: { + position: 'absolute', + opacity: 0, + width: 1, + height: 1, + }, + headerAnonymousFooter: { color: theme.heading, fontFamily: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM,