From 835619b66ca87b0a799149dba3df24ecdfae602d Mon Sep 17 00:00:00 2001 From: Eric Han Date: Tue, 13 Feb 2024 16:21:58 +0800 Subject: [PATCH 01/10] feat: add PDFThumbnail to preview PDF receipt --- ...oraryForRefactorRequestConfirmationList.js | 2 + src/components/PDFThumbnail/index.native.tsx | 30 ++++++++++++ src/components/PDFThumbnail/index.tsx | 46 +++++++++++++++++++ src/components/PDFThumbnail/types.ts | 14 ++++++ .../ReportActionItemImage.tsx | 11 ++++- .../ReportActionItemImages.tsx | 3 +- src/libs/ReceiptUtils.ts | 7 ++- 7 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/components/PDFThumbnail/index.native.tsx create mode 100644 src/components/PDFThumbnail/index.tsx create mode 100644 src/components/PDFThumbnail/types.ts diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index ec4f0f9cf5f8..c1228a22bc17 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,5 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import {format} from 'date-fns'; +import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; @@ -37,6 +38,7 @@ import Image from './Image'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import optionPropTypes from './optionPropTypes'; import OptionsSelector from './OptionsSelector'; +import PDFThumbnail from './PDFThumbnail'; import ReceiptEmptyState from './ReceiptEmptyState'; import SettlementButton from './SettlementButton'; import Switch from './Switch'; diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx new file mode 100644 index 000000000000..a7f22ebaf870 --- /dev/null +++ b/src/components/PDFThumbnail/index.native.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import {View} from 'react-native'; +import Pdf from 'react-native-pdf'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import useThemeStyles from '@hooks/useThemeStyles'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import type PDFThumbnailProps from './types'; + +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false}: PDFThumbnailProps) { + const styles = useThemeStyles(); + const sizeStyles = [styles.w100, styles.h100]; + + return ( + + + } + source={{uri: isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL.toString()}} + singlePage + style={sizeStyles} + /> + + + ); +} + +PDFThumbnail.displayName = 'PDFThumbnail'; +export default React.memo(PDFThumbnail); diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx new file mode 100644 index 000000000000..403a2124dc12 --- /dev/null +++ b/src/components/PDFThumbnail/index.tsx @@ -0,0 +1,46 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import {Document, pdfjs, Thumbnail} from 'react-pdf'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import useThemeStyles from '@hooks/useThemeStyles'; +import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import type PDFThumbnailProps from './types'; + +if (!pdfjs.GlobalWorkerOptions.workerSrc) { + pdfjs.GlobalWorkerOptions.workerSrc = new URL( + 'pdfjs-dist/legacy/build/pdf.worker.min.js', + // @ts-expect-error - It is a recommended step for import worker - https://github.com/wojtekmaj/react-pdf/blob/main/packages/react-pdf/README.md#import-worker-recommended + import.meta.url, + ).toString(); +} + +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false}: PDFThumbnailProps) { + const styles = useThemeStyles(); + + const thumbnail = useMemo( + () => ( + } + file={isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL.toString()} + options={{ + cMapUrl: 'cmaps/', + cMapPacked: true, + }} + externalLinkTarget="_blank" + onPassword={() => {}} + > + + + ), + [isAuthTokenRequired, previewSourceURL], + ); + + return ( + + {thumbnail} + + ); +} + +PDFThumbnail.displayName = 'PDFThumbnail'; +export default React.memo(PDFThumbnail); diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts new file mode 100644 index 000000000000..8f25fb8b0d4e --- /dev/null +++ b/src/components/PDFThumbnail/types.ts @@ -0,0 +1,14 @@ +import type {StyleProp, ViewStyle} from 'react-native'; + +type PDFThumbnailProps = { + /** Source URL for the preview PDF */ + previewSourceURL: string; + + /** Any additional styles to apply */ + style?: StyleProp; + + /** Whether the image requires an authToken */ + isAuthTokenRequired?: boolean; +}; + +export default PDFThumbnailProps; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index ae8087439138..c5ee337e04d9 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -7,6 +7,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import AttachmentModal from '@components/AttachmentModal'; import EReceiptThumbnail from '@components/EReceiptThumbnail'; import Image from '@components/Image'; +import PDFThumbnail from '@components/PDFThumbnail'; import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus'; import {ShowContextMenuContext} from '@components/ShowContextMenuContext'; import ThumbnailImage from '@components/ThumbnailImage'; @@ -78,7 +79,7 @@ function ReportActionItemImage({ /> ); - } else if (thumbnail && !isLocalFile && !Str.isPDF(imageSource as string)) { + } else if (thumbnail && !isLocalFile) { receiptImageComponent = ( ); + } else if (isLocalFile && filename && Str.isPDF(filename) && typeof imageSource === 'string') { + receiptImageComponent = ( + + ); } else { receiptImageComponent = ( - {shownImages.map(({thumbnail, image, transaction, isLocalFile}, index) => { + {shownImages.map(({thumbnail, image, transaction, isLocalFile, filename}, index) => { const isLastImage = index === numberOfShownImages - 1; // Show a border to separate multiple images. Shown to the right for each except the last. @@ -82,6 +82,7 @@ function ReportActionItemImages({images, size, total, isHovered = false}: Report thumbnail={thumbnail} image={image} isLocalFile={isLocalFile} + filename={filename} transaction={transaction} iconSize={iconSize} /> diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 36479136c6ad..75d14be1a907 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -45,13 +45,14 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); const hasEReceipt = transaction?.hasEReceipt; + const isReceiptPDF = Str.isPDF(filename); if (hasEReceipt) { return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction, filename}; } // For local files, we won't have a thumbnail yet - if (isReceiptImage && typeof path === 'string' && (path.startsWith('blob:') || path.startsWith('file:'))) { + if ((isReceiptImage || isReceiptPDF) && typeof path === 'string' && (path.startsWith('blob:') || path.startsWith('file:'))) { return {thumbnail: null, image: path, isLocalFile: true, filename}; } @@ -59,6 +60,10 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa return {thumbnail: `${path}.1024.jpg`, image: path, filename}; } + if (isReceiptPDF && typeof path === 'string') { + return {thumbnail: `${path.substring(0, path.length - 4)}.jpg.1024.jpg`, image: path, filename}; + } + const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; let image = ReceiptGeneric; if (fileExtension === CONST.IOU.FILE_TYPES.HTML) { From cb360d333996495f179915f7d26cc5ebc473bfeb Mon Sep 17 00:00:00 2001 From: Eric Han Date: Wed, 14 Feb 2024 23:22:39 +0800 Subject: [PATCH 02/10] popup alert and return back when uploading a protected PDF --- ...oraryForRefactorRequestConfirmationList.js | 88 +++++++++++++------ src/components/PDFThumbnail/index.native.tsx | 3 +- src/components/PDFThumbnail/index.tsx | 6 +- src/components/PDFThumbnail/types.ts | 3 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + 6 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index c1228a22bc17..1d0b071e45f8 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -4,7 +4,7 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; @@ -32,6 +32,7 @@ import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import categoryPropTypes from './categoryPropTypes'; import ConfirmedRoute from './ConfirmedRoute'; +import ConfirmModal from './ConfirmModal'; import FormHelpMessage from './FormHelpMessage'; import * as Expensicons from './Icon/Expensicons'; import Image from './Image'; @@ -308,6 +309,15 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const [merchantError, setMerchantError] = useState(false); + const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + + const hideRecieptModal = () => { + setIsAttachmentInvalid(false); + InteractionManager.runAfterInteractions(() => { + Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, transaction.transactionID, reportID)); + }); + }; + const shouldDisplayFieldError = useMemo(() => { if (!isEditingSplitBill) { return false; @@ -851,7 +861,35 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ (supplementaryField) => supplementaryField.item, ); - const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; + const { + image: receiptImage, + thumbnail: receiptThumbnail, + isLocalFile, + } = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; + + const receiptThumbnailContent = useMemo( + () => + isLocalFile && Str.isPDF(receiptFilename) ? ( + setIsAttachmentInvalid(true)} + /> + ) : ( + + ), + [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile], + ); + return ( )} - {receiptImage || receiptThumbnail ? ( - - ) : ( - // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") - PolicyUtils.isPaidGroupPolicy(policy) && - !isDistanceRequest && - iouType === CONST.IOU.TYPE.REQUEST && ( - - Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), - ) - } - /> - ) - )} + {receiptImage || receiptThumbnail + ? receiptThumbnailContent + : // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") + PolicyUtils.isPaidGroupPolicy(policy) && + !isDistanceRequest && + iouType === CONST.IOU.TYPE.REQUEST && ( + + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ) + } + /> + )} {primaryFields} {!shouldShowAllFields && ( @@ -916,6 +945,15 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} {shouldShowAllFields && <>{supplementaryFields}} + ); } diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx index a7f22ebaf870..7b94c017a3c9 100644 --- a/src/components/PDFThumbnail/index.native.tsx +++ b/src/components/PDFThumbnail/index.native.tsx @@ -6,7 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import type PDFThumbnailProps from './types'; -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, onPassword = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); const sizeStyles = [styles.w100, styles.h100]; @@ -20,6 +20,7 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false}: PD source={{uri: isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL.toString()}} singlePage style={sizeStyles} + onError={onPassword} /> diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index 403a2124dc12..c5b02ac10d47 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -14,7 +14,7 @@ if (!pdfjs.GlobalWorkerOptions.workerSrc) { ).toString(); } -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, onPassword = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); const thumbnail = useMemo( @@ -27,12 +27,12 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false}: PD cMapPacked: true, }} externalLinkTarget="_blank" - onPassword={() => {}} + onPassword={onPassword} > ), - [isAuthTokenRequired, previewSourceURL], + [isAuthTokenRequired, previewSourceURL, onPassword], ); return ( diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index 8f25fb8b0d4e..db3785e5a115 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -9,6 +9,9 @@ type PDFThumbnailProps = { /** Whether the image requires an authToken */ isAuthTokenRequired?: boolean; + + /** Callback to call if PDF is password protected */ + onPassword?: () => void; }; export default PDFThumbnailProps; diff --git a/src/languages/en.ts b/src/languages/en.ts index c4698974c3b1..5eb045799212 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -564,6 +564,7 @@ export default { deleteReceipt: 'Delete receipt', deleteConfirmation: 'Are you sure you want to delete this receipt?', addReceipt: 'Add receipt', + protectedPDFNotSupportedError: 'Password protected PDF receipt is not supported.', }, iou: { amount: 'Amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index 74d7b634fc81..cb21690521b8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -557,6 +557,7 @@ export default { deleteReceipt: 'Eliminar recibo', deleteConfirmation: '¿Estás seguro de que quieres borrar este recibo?', addReceipt: 'Añadir recibo', + protectedPDFNotSupportedError: 'Spanish message TBD', }, iou: { amount: 'Importe', From 8d514bc40b45ce009002a2b009dd4d39e3290ede Mon Sep 17 00:00:00 2001 From: Eric Han Date: Fri, 16 Feb 2024 23:38:45 +0800 Subject: [PATCH 03/10] skip loading protected PDF and address PR comments --- ...oraryForRefactorRequestConfirmationList.js | 6 ++--- src/components/PDFThumbnail/index.native.tsx | 27 ++++++++++++------- src/components/PDFThumbnail/index.tsx | 17 ++++++++---- src/components/PDFThumbnail/types.ts | 4 +-- .../ReportActionItemImage.tsx | 1 - 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 1d0b071e45f8..cb35bb0803fa 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -312,7 +312,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); const hideRecieptModal = () => { - setIsAttachmentInvalid(false); InteractionManager.runAfterInteractions(() => { Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, transaction.transactionID, reportID)); }); @@ -873,9 +872,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ setIsAttachmentInvalid(true)} + skipLoadingProtectedPDF={isAttachmentInvalid ? true : () => setIsAttachmentInvalid(true)} /> ) : ( ), - [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile], + [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile, isAttachmentInvalid], ); return ( diff --git a/src/components/PDFThumbnail/index.native.tsx b/src/components/PDFThumbnail/index.native.tsx index 7b94c017a3c9..deb80d77a947 100644 --- a/src/components/PDFThumbnail/index.native.tsx +++ b/src/components/PDFThumbnail/index.native.tsx @@ -6,22 +6,29 @@ import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import type PDFThumbnailProps from './types'; -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, onPassword = () => {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, skipLoadingProtectedPDF = false}: PDFThumbnailProps) { const styles = useThemeStyles(); const sizeStyles = [styles.w100, styles.h100]; return ( - } - source={{uri: isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL.toString()}} - singlePage - style={sizeStyles} - onError={onPassword} - /> + {!(typeof skipLoadingProtectedPDF === 'boolean' && skipLoadingProtectedPDF) && ( + } + source={{uri: isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL}} + singlePage + style={sizeStyles} + onError={(error) => { + const errorObj = error as {message: string}; + if (errorObj.message.match(/password/i) && typeof skipLoadingProtectedPDF === 'function') { + skipLoadingProtectedPDF(); + } + }} + /> + )} ); diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index c5b02ac10d47..333fbd7ee1bc 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -14,30 +14,37 @@ if (!pdfjs.GlobalWorkerOptions.workerSrc) { ).toString(); } -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, onPassword = () => {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, skipLoadingProtectedPDF = false}: PDFThumbnailProps) { const styles = useThemeStyles(); const thumbnail = useMemo( () => ( } - file={isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL.toString()} + file={isAuthTokenRequired ? addEncryptedAuthTokenToURL(previewSourceURL) : previewSourceURL} options={{ cMapUrl: 'cmaps/', cMapPacked: true, }} externalLinkTarget="_blank" - onPassword={onPassword} + onPassword={() => { + if (typeof skipLoadingProtectedPDF !== 'function') { + return; + } + skipLoadingProtectedPDF(); + }} > ), - [isAuthTokenRequired, previewSourceURL, onPassword], + [isAuthTokenRequired, previewSourceURL, skipLoadingProtectedPDF], ); return ( - {thumbnail} + + {!(typeof skipLoadingProtectedPDF === 'boolean' && skipLoadingProtectedPDF) && thumbnail} + ); } diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index db3785e5a115..935326fbdd02 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -10,8 +10,8 @@ type PDFThumbnailProps = { /** Whether the image requires an authToken */ isAuthTokenRequired?: boolean; - /** Callback to call if PDF is password protected */ - onPassword?: () => void; + /** Whether need to skip loading password protected PDF */ + skipLoadingProtectedPDF?: (() => void) | boolean; }; export default PDFThumbnailProps; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index c5ee337e04d9..57a56e4067f0 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -93,7 +93,6 @@ function ReportActionItemImage({ ); } else { From f8d33e3dd968b6cb939449100068f2dba8044f2f Mon Sep 17 00:00:00 2001 From: Eric Han Date: Tue, 20 Feb 2024 22:26:53 +0800 Subject: [PATCH 04/10] use two props to control loading protected PDF --- ...eyTemporaryForRefactorRequestConfirmationList.js | 3 ++- src/components/PDFThumbnail/index.native.tsx | 8 ++++---- src/components/PDFThumbnail/index.tsx | 13 ++++--------- src/components/PDFThumbnail/types.ts | 5 ++++- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index ee7683ead69a..553a387fef93 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -864,7 +864,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ previewSourceURL={receiptImage} style={styles.moneyRequestImage} // We don't support scaning password protected PDF receipt - skipLoadingProtectedPDF={isAttachmentInvalid ? true : () => setIsAttachmentInvalid(true)} + shouldLoadPDFThumbnail={!isAttachmentInvalid} + onPasswordCallback={() => setIsAttachmentInvalid(true)} /> ) : ( {}}: PDFThumbnailProps) { const styles = useThemeStyles(); const sizeStyles = [styles.w100, styles.h100]; return ( - {!(typeof skipLoadingProtectedPDF === 'boolean' && skipLoadingProtectedPDF) && ( + {shouldLoadPDFThumbnail && ( { const errorObj = error as {message: string}; - if (errorObj.message.match(/password/i) && typeof skipLoadingProtectedPDF === 'function') { - skipLoadingProtectedPDF(); + if (errorObj.message.match(/password/i)) { + onPasswordCallback(); } }} /> diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index 333fbd7ee1bc..f9c80ee6362e 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -14,7 +14,7 @@ if (!pdfjs.GlobalWorkerOptions.workerSrc) { ).toString(); } -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, skipLoadingProtectedPDF = false}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, shouldLoadPDFThumbnail = true, onPasswordCallback = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); const thumbnail = useMemo( @@ -28,23 +28,18 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ski }} externalLinkTarget="_blank" onPassword={() => { - if (typeof skipLoadingProtectedPDF !== 'function') { - return; - } - skipLoadingProtectedPDF(); + onPasswordCallback(); }} > ), - [isAuthTokenRequired, previewSourceURL, skipLoadingProtectedPDF], + [isAuthTokenRequired, previewSourceURL, onPasswordCallback], ); return ( - - {!(typeof skipLoadingProtectedPDF === 'boolean' && skipLoadingProtectedPDF) && thumbnail} - + {shouldLoadPDFThumbnail && thumbnail} ); } diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index 935326fbdd02..36dc69da89f1 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -11,7 +11,10 @@ type PDFThumbnailProps = { isAuthTokenRequired?: boolean; /** Whether need to skip loading password protected PDF */ - skipLoadingProtectedPDF?: (() => void) | boolean; + shouldLoadPDFThumbnail?: boolean; + + /** Callback to call if PDF is password protected */ + onPasswordCallback?: () => void; }; export default PDFThumbnailProps; From 2b15512d9dd3092767591f8d40646620cd0409fc Mon Sep 17 00:00:00 2001 From: Eric Han Date: Wed, 21 Feb 2024 16:06:27 +0800 Subject: [PATCH 05/10] fix props naming --- ...MoneyTemporaryForRefactorRequestConfirmationList.js | 10 ++++------ src/components/PDFThumbnail/index.native.tsx | 10 +++++----- src/components/PDFThumbnail/index.tsx | 8 ++++---- src/components/PDFThumbnail/types.ts | 6 +++--- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 553a387fef93..078ba84fa139 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -4,7 +4,7 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; @@ -304,9 +304,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); const hideRecieptModal = () => { - InteractionManager.runAfterInteractions(() => { - Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, transaction.transactionID, reportID)); - }); + Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, transaction.transactionID, reportID)); }; const shouldDisplayFieldError = useMemo(() => { @@ -864,8 +862,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ previewSourceURL={receiptImage} style={styles.moneyRequestImage} // We don't support scaning password protected PDF receipt - shouldLoadPDFThumbnail={!isAttachmentInvalid} - onPasswordCallback={() => setIsAttachmentInvalid(true)} + enabled={!isAttachmentInvalid} + onPassword={() => setIsAttachmentInvalid(true)} /> ) : ( {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); const sizeStyles = [styles.w100, styles.h100]; return ( - {shouldLoadPDFThumbnail && ( + {enabled && ( { - const errorObj = error as {message: string}; - if (errorObj.message.match(/password/i)) { - onPasswordCallback(); + if (!('message' in error && typeof error.message === 'string' && error.message.match(/password/i))) { + return; } + onPassword(); }} /> )} diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index f9c80ee6362e..3cf0cbda8a84 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -14,7 +14,7 @@ if (!pdfjs.GlobalWorkerOptions.workerSrc) { ).toString(); } -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, shouldLoadPDFThumbnail = true, onPasswordCallback = () => {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); const thumbnail = useMemo( @@ -28,18 +28,18 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, sho }} externalLinkTarget="_blank" onPassword={() => { - onPasswordCallback(); + onPassword(); }} > ), - [isAuthTokenRequired, previewSourceURL, onPasswordCallback], + [isAuthTokenRequired, previewSourceURL, onPassword], ); return ( - {shouldLoadPDFThumbnail && thumbnail} + {enabled && thumbnail} ); } diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index 36dc69da89f1..8e9be8e96efb 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -10,11 +10,11 @@ type PDFThumbnailProps = { /** Whether the image requires an authToken */ isAuthTokenRequired?: boolean; - /** Whether need to skip loading password protected PDF */ - shouldLoadPDFThumbnail?: boolean; + /** Whether the PDF thumbnail can be loaded */ + enabled?: boolean; /** Callback to call if PDF is password protected */ - onPasswordCallback?: () => void; + onPassword?: () => void; }; export default PDFThumbnailProps; From 878be890e6f8c06eb2e272d5c490a681434c355a Mon Sep 17 00:00:00 2001 From: Eric Han Date: Wed, 28 Feb 2024 11:14:25 +0800 Subject: [PATCH 06/10] update alert messages of confirm modal --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- src/languages/en.ts | 6 +++--- src/languages/es.ts | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index f6613f4a2f6a..8ccf33c07ef9 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -938,7 +938,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ )} {shouldShowAllFields && supplementaryFields} Date: Thu, 29 Feb 2024 22:50:35 +0800 Subject: [PATCH 07/10] fix pointer style for not clickable thumbnail and address PR comments --- ...oraryForRefactorRequestConfirmationList.js | 9 +++--- src/components/PDFThumbnail/index.css | 4 +++ src/components/PDFThumbnail/index.tsx | 29 +++++++++++-------- src/components/PDFThumbnail/types.ts | 5 +++- src/languages/en.ts | 4 +-- src/languages/es.ts | 4 +-- 6 files changed, 34 insertions(+), 21 deletions(-) create mode 100644 src/components/PDFThumbnail/index.css diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 8ccf33c07ef9..3c059372a4a3 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -303,7 +303,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - const hideRecieptModal = () => { + const navigateBack = () => { Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(iouType, transaction.transactionID, reportID)); }; @@ -868,6 +868,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // We don't support scaning password protected PDF receipt enabled={!isAttachmentInvalid} onPassword={() => setIsAttachmentInvalid(true)} + isClickable={false} /> ) : ( diff --git a/src/components/PDFThumbnail/index.css b/src/components/PDFThumbnail/index.css new file mode 100644 index 000000000000..91228b2e4929 --- /dev/null +++ b/src/components/PDFThumbnail/index.css @@ -0,0 +1,4 @@ +/* These style overrides are necessary so that the PDF thumbnail shows default pointer when it's not clickable */ +.react-pdf__Thumbnail--notClickable { + cursor: default; +} diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index 3cf0cbda8a84..59397118c274 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -1,22 +1,24 @@ -import React, {useMemo} from 'react'; +// @ts-expect-error - We use the same method as PDFView to import the worker +import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker'; +import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {Document, pdfjs, Thumbnail} from 'react-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; +import './index.css'; import type PDFThumbnailProps from './types'; -if (!pdfjs.GlobalWorkerOptions.workerSrc) { - pdfjs.GlobalWorkerOptions.workerSrc = new URL( - 'pdfjs-dist/legacy/build/pdf.worker.min.js', - // @ts-expect-error - It is a recommended step for import worker - https://github.com/wojtekmaj/react-pdf/blob/main/packages/react-pdf/README.md#import-worker-recommended - import.meta.url, - ).toString(); -} - -function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}, isClickable = true}: PDFThumbnailProps) { const styles = useThemeStyles(); + useEffect(() => { + const workerURL = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'})); + if (pdfjs.GlobalWorkerOptions.workerSrc !== workerURL) { + pdfjs.GlobalWorkerOptions.workerSrc = workerURL; + } + }, []); + const thumbnail = useMemo( () => ( - + ), - [isAuthTokenRequired, previewSourceURL, onPassword], + [isAuthTokenRequired, previewSourceURL, onPassword, isClickable], ); return ( diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index 8e9be8e96efb..8c46c145887d 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -7,7 +7,7 @@ type PDFThumbnailProps = { /** Any additional styles to apply */ style?: StyleProp; - /** Whether the image requires an authToken */ + /** Whether the PDF thumbnail requires an authToken */ isAuthTokenRequired?: boolean; /** Whether the PDF thumbnail can be loaded */ @@ -15,6 +15,9 @@ type PDFThumbnailProps = { /** Callback to call if PDF is password protected */ onPassword?: () => void; + + /** Whether the PDF thumbnail is clickable */ + isClickable?: boolean; }; export default PDFThumbnailProps; diff --git a/src/languages/en.ts b/src/languages/en.ts index c287418553d4..abb866b409a7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -338,8 +338,9 @@ export default { attachmentTooSmall: 'Attachment too small', sizeNotMet: 'Attachment size must be greater than 240 bytes.', wrongFileType: 'Invalid file type', - notAllowedExtension: 'This file type is not allowed.', + notAllowedExtension: 'This file type is not allowed', folderNotAllowedMessage: 'Uploading a folder is not allowed. Try a different file.', + protectedPDFNotSupported: 'Password-protected PDF is not supported', }, avatarCropModal: { title: 'Edit photo', @@ -572,7 +573,6 @@ export default { deleteReceipt: 'Delete receipt', deleteConfirmation: 'Are you sure you want to delete this receipt?', addReceipt: 'Add receipt', - protectedPDFNotSupportedError: 'Password-protected PDF is not supported.', }, iou: { amount: 'Amount', diff --git a/src/languages/es.ts b/src/languages/es.ts index a6672c65a8b3..138f96f596a3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -328,8 +328,9 @@ export default { attachmentTooSmall: 'Archivo adjunto demasiado pequeño', sizeNotMet: 'El archivo adjunto debe ser más grande que 240 bytes.', wrongFileType: 'Tipo de archivo inválido', - notAllowedExtension: 'Este tipo de archivo no es compatible.', + notAllowedExtension: 'Este tipo de archivo no es compatible', folderNotAllowedMessage: 'Subir una carpeta no está permitido. Prueba con otro archivo.', + protectedPDFNotSupported: 'Los PDFs con password no son compatibles', }, avatarCropModal: { title: 'Editar foto', @@ -565,7 +566,6 @@ export default { deleteReceipt: 'Eliminar recibo', deleteConfirmation: '¿Estás seguro de que quieres borrar este recibo?', addReceipt: 'Añadir recibo', - protectedPDFNotSupportedError: 'Los PDFs con password no son compatibles.', }, iou: { amount: 'Importe', From 43b70c192920fd252693bd88c21ccad442e6d8e5 Mon Sep 17 00:00:00 2001 From: Eric Han Date: Fri, 1 Mar 2024 08:23:07 +0800 Subject: [PATCH 08/10] use pointerEvents='none' to remove default pointer style added by browser --- ...neyTemporaryForRefactorRequestConfirmationList.js | 1 - src/components/PDFThumbnail/index.css | 4 ---- src/components/PDFThumbnail/index.tsx | 12 +++++------- src/components/PDFThumbnail/types.ts | 3 --- 4 files changed, 5 insertions(+), 15 deletions(-) delete mode 100644 src/components/PDFThumbnail/index.css diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 3c059372a4a3..65572499c2d1 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -868,7 +868,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // We don't support scaning password protected PDF receipt enabled={!isAttachmentInvalid} onPassword={() => setIsAttachmentInvalid(true)} - isClickable={false} /> ) : ( {}, isClickable = true}: PDFThumbnailProps) { +function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); useEffect(() => { @@ -33,13 +32,12 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena onPassword(); }} > - + + + ), - [isAuthTokenRequired, previewSourceURL, onPassword, isClickable], + [isAuthTokenRequired, previewSourceURL, onPassword], ); return ( diff --git a/src/components/PDFThumbnail/types.ts b/src/components/PDFThumbnail/types.ts index 8c46c145887d..11253e462aca 100644 --- a/src/components/PDFThumbnail/types.ts +++ b/src/components/PDFThumbnail/types.ts @@ -15,9 +15,6 @@ type PDFThumbnailProps = { /** Callback to call if PDF is password protected */ onPassword?: () => void; - - /** Whether the PDF thumbnail is clickable */ - isClickable?: boolean; }; export default PDFThumbnailProps; From b18c364a049a520d586b363c390354e92713b861 Mon Sep 17 00:00:00 2001 From: Eric Han Date: Mon, 4 Mar 2024 21:35:16 +0800 Subject: [PATCH 09/10] improve pdf worker import and ts-expect-error comments --- src/components/PDFThumbnail/index.tsx | 15 ++++++--------- .../ReportActionItem/ReportActionItemImage.tsx | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/components/PDFThumbnail/index.tsx b/src/components/PDFThumbnail/index.tsx index af12aca29025..e69e4dd5075b 100644 --- a/src/components/PDFThumbnail/index.tsx +++ b/src/components/PDFThumbnail/index.tsx @@ -1,6 +1,6 @@ -// @ts-expect-error - We use the same method as PDFView to import the worker +// @ts-expect-error - This line imports a module from 'pdfjs-dist' package which lacks TypeScript typings. import pdfWorkerSource from 'pdfjs-dist/legacy/build/pdf.worker'; -import React, {useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {Document, pdfjs, Thumbnail} from 'react-pdf'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -8,16 +8,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import type PDFThumbnailProps from './types'; +if (!pdfjs.GlobalWorkerOptions.workerSrc) { + pdfjs.GlobalWorkerOptions.workerSrc = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'})); +} + function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, enabled = true, onPassword = () => {}}: PDFThumbnailProps) { const styles = useThemeStyles(); - useEffect(() => { - const workerURL = URL.createObjectURL(new Blob([pdfWorkerSource], {type: 'text/javascript'})); - if (pdfjs.GlobalWorkerOptions.workerSrc !== workerURL) { - pdfjs.GlobalWorkerOptions.workerSrc = workerURL; - } - }, []); - const thumbnail = useMemo( () => ( ); - } else if (thumbnail && !isLocalFile && !Str.isPDF(attachmentModalSource as string)) { + } else if (thumbnail && !isLocalFile) { receiptImageComponent = ( ); - } else if (isLocalFile && filename && Str.isPDF(filename) && typeof imageSource === 'string') { + } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { receiptImageComponent = ( ); From c999e1098381e5793e966e6b3e2fe309175cae1c Mon Sep 17 00:00:00 2001 From: Eric Han <117511920+eh2077@users.noreply.github.com> Date: Tue, 5 Mar 2024 08:18:31 +0800 Subject: [PATCH 10/10] Update src/languages/es.ts Co-authored-by: Carlos Martins --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index faf68d2785e1..8cb0bcebde40 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -330,7 +330,7 @@ export default { wrongFileType: 'Tipo de archivo inválido', notAllowedExtension: 'Este tipo de archivo no es compatible', folderNotAllowedMessage: 'Subir una carpeta no está permitido. Prueba con otro archivo.', - protectedPDFNotSupported: 'Los PDFs con password no son compatibles', + protectedPDFNotSupported: 'Los PDFs con contraseña no son compatibles', }, avatarCropModal: { title: 'Editar foto',