Skip to content

Commit

Permalink
Merge pull request #44778 from Krishna2323/krishna2323/issue/43645
Browse files Browse the repository at this point in the history
fix: Scan - No error pop-up and scan expense with corrupted PDF can be submitted via QAB
  • Loading branch information
grgia authored Jul 22, 2024
2 parents c0512d4 + 8d553b0 commit 637796d
Show file tree
Hide file tree
Showing 7 changed files with 90 additions and 49 deletions.
30 changes: 1 addition & 29 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -547,16 +543,6 @@ function MoneyRequestConfirmationListFooter({
<PDFThumbnail
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
previewSourceURL={resolvedReceiptImage as string}
// We don't support scanning password protected PDF receipt
enabled={!isAttachmentInvalid}
onPassword={() => {
setIsAttachmentInvalid(true);
setInvalidAttachmentPrompt(translate('attachmentPicker.protectedPDFNotSupported'));
}}
onLoadError={() => {
setInvalidAttachmentPrompt(translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'));
setIsAttachmentInvalid(true);
}}
/>
</PressableWithoutFocus>
) : (
Expand Down Expand Up @@ -591,7 +577,6 @@ function MoneyRequestConfirmationListFooter({
translate,
shouldDisplayReceipt,
resolvedReceiptImage,
isAttachmentInvalid,
isThumbnail,
resolvedThumbnail,
receiptThumbnail,
Expand All @@ -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 && (
Expand Down Expand Up @@ -655,15 +636,6 @@ function MoneyRequestConfirmationListFooter({
/>
)}
<View style={[styles.mb5]}>{shouldShowAllFields && supplementaryFields}</View>
<ConfirmModal
title={translate('attachmentPicker.attachmentError')}
onConfirm={navigateBack}
onCancel={navigateBack}
isVisible={isAttachmentInvalid}
prompt={invalidAttachmentPrompt}
confirmText={translate('common.close')}
shouldShowCancelButton={false}
/>
</>
);
}
Expand Down
14 changes: 10 additions & 4 deletions src/components/PDFThumbnail/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 && <PDFThumbnailError />}
Expand Down
10 changes: 8 additions & 2 deletions src/components/PDFThumbnail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -30,6 +30,12 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena
onLoad={() => {
setFailedToLoad(false);
}}
onLoadSuccess={() => {
if (!onLoadSuccess) {
return;
}
onLoadSuccess();
}}
onLoadError={() => {
if (onLoadError) {
onLoadError();
Expand All @@ -43,7 +49,7 @@ function PDFThumbnail({previewSourceURL, style, isAuthTokenRequired = false, ena
</View>
</Document>
),
[isAuthTokenRequired, previewSourceURL, onPassword, onLoadError],
[isAuthTokenRequired, previewSourceURL, onPassword, onLoadError, onLoadSuccess],
);

return (
Expand Down
3 changes: 3 additions & 0 deletions src/components/PDFThumbnail/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
32 changes: 31 additions & 1 deletion src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -66,6 +68,8 @@ function IOURequestStepScan({
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<string | null>(null);
const [didCapturePhoto, setDidCapturePhoto] = useState(false);

const [pdfFile, setPdfFile] = useState<null | FileObject>(null);

const defaultTaxCode = TransactionUtils.getDefaultTaxCode(policy, transaction);
const transactionTaxCode = (transaction?.taxCode ? transaction?.taxCode : defaultTaxCode) ?? '';
const transactionTaxAmount = transaction?.taxAmount ?? 0;
Expand Down Expand Up @@ -383,11 +387,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
Expand Down Expand Up @@ -458,6 +468,26 @@ function IOURequestStepScan({
shouldShowWrapper={!!backTo}
testID={IOURequestStepScan.displayName}
>
{pdfFile && (
<PDFThumbnail
style={styles.invisiblePDF}
previewSourceURL={pdfFile?.uri ?? ''}
onLoadSuccess={() => {
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 && (
<View style={[styles.cameraView, styles.permissionView, styles.userSelectNone]}>
<ImageSVG
Expand Down
43 changes: 30 additions & 13 deletions src/pages/iou/request/step/IOURequestStepScan/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {Str} from 'expensify-common';
import React, {useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState} from 'react';
import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native';
import {useOnyx, withOnyx} from 'react-native-onyx';
Expand All @@ -14,6 +15,7 @@ import CopyTextToClipboard from '@components/CopyTextToClipboard';
import {DragAndDropContext} from '@components/DragAndDrop/Provider';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PDFThumbnail from '@components/PDFThumbnail';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
Expand All @@ -25,7 +27,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Browser from '@libs/Browser';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import getCurrentPosition from '@libs/getCurrentPosition';
import isPdfFilePasswordProtected from '@libs/isPdfFilePasswordProtected';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import * as OptionsListUtils from '@libs/OptionsListUtils';
Expand Down Expand Up @@ -63,7 +64,7 @@ function IOURequestStepScan({
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState<TranslationPaths>();
const [attachmentInvalidReason, setAttachmentValidReason] = useState<TranslationPaths>();

const [pdfFile, setPdfFile] = useState<null | FileObject>(null);
const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0);
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
Expand Down Expand Up @@ -190,6 +191,7 @@ function IOURequestStepScan({
setIsAttachmentInvalid(isInvalid);
setAttachmentInvalidReasonTitle(title);
setAttachmentValidReason(reason);
setPdfFile(null);
};

function validateReceipt(file: FileObject) {
Expand All @@ -214,16 +216,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(() => {
Expand Down Expand Up @@ -427,11 +419,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
Expand Down Expand Up @@ -521,9 +519,27 @@ function IOURequestStepScan({
[],
);

const PDFThumbnailView = pdfFile ? (
<PDFThumbnail
style={styles.invisiblePDF}
previewSourceURL={pdfFile.uri ?? ''}
onLoadSuccess={() => {
setPdfFile(null);
setReceiptAndNavigate(pdfFile, true);
}}
onPassword={() => {
setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.protectedPDFNotSupported');
}}
onLoadError={() => {
setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment');
}}
/>
) : null;

const mobileCameraView = () => (
<>
<View style={[styles.cameraView]}>
{PDFThumbnailView}
{((cameraPermissionState === 'prompt' && !isQueriedPermissionState) || (cameraPermissionState === 'granted' && isEmptyObject(videoConstraints))) && (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
Expand Down Expand Up @@ -622,6 +638,7 @@ function IOURequestStepScan({

const desktopUploadView = () => (
<>
{PDFThumbnailView}
<View onLayout={({nativeEvent}) => setReceiptImageTopPosition(PixelRatio.roundToNearestPixel((nativeEvent.layout as DOMRect).top))}>
<ReceiptUpload
width={CONST.RECEIPT.ICON_SIZE}
Expand Down
7 changes: 7 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,13 @@ const styles = (theme: ThemeColors) =>
justifyContent: 'center',
},

invisiblePDF: {
position: 'absolute',
opacity: 0,
width: 1,
height: 1,
},

headerAnonymousFooter: {
color: theme.heading,
fontFamily: FontUtils.fontFamily.platform.EXP_NEW_KANSAS_MEDIUM,
Expand Down

0 comments on commit 637796d

Please sign in to comment.