diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 0ed5e3b210875..1411f8ca59024 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1854,7 +1854,7 @@ const CONST = { ATTACHMENT_THUMBNAIL_WIDTH_ATTRIBUTE: 'data-expensify-width', ATTACHMENT_THUMBNAIL_HEIGHT_ATTRIBUTE: 'data-expensify-height', ATTACHMENT_DURATION_ATTRIBUTE: 'data-expensify-duration', - + ATTACHMENT_IMAGE_DEFAULT_NAME: 'shared_image.png', ATTACHMENT_PICKER_TYPE: { FILE: 'file', IMAGE: 'image', @@ -1893,6 +1893,7 @@ const CONST = { MSWORD: 'application/msword', ZIP: 'application/zip', RFC822: 'message/rfc822', + HEIC: 'image/heic', }, SHARE_FILE_MIMETYPE: { @@ -1903,6 +1904,7 @@ const CONST = { WEBP: 'image/webp', TIF: 'image/tif', TIFF: 'image/tiff', + HEIC: 'image/heic', IMG: 'image/*', PDF: 'application/pdf', MSWORD: 'application/msword', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ecbf1645e8407..f3884a90700c0 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -508,6 +508,8 @@ const ONYXKEYS = { /** Temporary file to be shared from outside the app */ SHARE_TEMP_FILE: 'shareTempFile', + VALIDATED_FILE_OBJECT: 'shareFileObject', + /** Corpay fields to be used in the bank account creation setup */ CORPAY_FIELDS: 'corpayFields', @@ -1220,6 +1222,7 @@ type OnyxValuesMapping = { [ONYXKEYS.CONCIERGE_REPORT_ID]: string; [ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS]: Participant; [ONYXKEYS.SHARE_TEMP_FILE]: OnyxTypes.ShareTempFile; + [ONYXKEYS.VALIDATED_FILE_OBJECT]: OnyxTypes.FileObject | undefined; [ONYXKEYS.CORPAY_FIELDS]: OnyxTypes.CorpayFields; [ONYXKEYS.PRESERVED_USER_SESSION]: OnyxTypes.Session; [ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining; diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 5085f0e2384e2..e611e4bfebf93 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -3,10 +3,10 @@ import findLast from 'lodash/findLast'; import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Transaction} from '@src/types/onyx'; +import type {ShareTempFile, Transaction} from '@src/types/onyx'; import type {ReceiptError, ReceiptSource} from '@src/types/onyx/Transaction'; -import * as FileUtils from './fileDownload/FileUtils'; -import * as TransactionUtils from './TransactionUtils'; +import {isLocalFile as isLocalFileUtils, splitExtensionFromFileName} from './fileDownload/FileUtils'; +import {hasReceipt, hasReceiptSource, isFetchingWaypointsFromServer} from './TransactionUtils'; type ThumbnailAndImageURI = { image?: string; @@ -27,10 +27,10 @@ type ThumbnailAndImageURI = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPath: ReceiptSource | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - if (!TransactionUtils.hasReceipt(transaction) && !receiptPath && !receiptFileName) { + if (!hasReceipt(transaction) && !receiptPath && !receiptFileName) { return {isEmptyReceipt: true}; } - if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { + if (isFetchingWaypointsFromServer(transaction)) { return {isThumbnail: true, isLocalFile: true}; } // If there're errors, we need to display them in preview. We can store many files in errors, but we just need to get the last one @@ -40,7 +40,7 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa // filename of uploaded image or last part of remote URI const filename = errors?.filename ?? transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); - const hasEReceipt = !TransactionUtils.hasReceiptSource(transaction) && transaction?.hasEReceipt; + const hasEReceipt = !hasReceiptSource(transaction) && transaction?.hasEReceipt; const isReceiptPDF = Str.isPDF(filename); if (hasEReceipt) { @@ -60,11 +60,15 @@ function getThumbnailAndImageURIs(transaction: OnyxEntry, receiptPa return {thumbnail: `${path.substring(0, path.length - 4)}.jpg.1024.jpg`, image: path, filename}; } - const isLocalFile = FileUtils.isLocalFile(path); - const {fileExtension} = FileUtils.splitExtensionFromFileName(filename); + const isLocalFile = isLocalFileUtils(path); + const {fileExtension} = splitExtensionFromFileName(filename); return {isThumbnail: true, fileExtension: Object.values(CONST.IOU.FILE_TYPES).find((type) => type === fileExtension), image: path, isLocalFile, filename}; } +const shouldValidateFile = (file: ShareTempFile | undefined) => { + return file?.mimeType === CONST.SHARE_FILE_MIMETYPE.HEIC || file?.mimeType === CONST.SHARE_FILE_MIMETYPE.IMG; +}; + // eslint-disable-next-line import/prefer-default-export -export {getThumbnailAndImageURIs}; +export {getThumbnailAndImageURIs, shouldValidateFile}; export type {ThumbnailAndImageURI}; diff --git a/src/libs/actions/Share.ts b/src/libs/actions/Share.ts index 8c9f3ebefe996..5e4b22541ab23 100644 --- a/src/libs/actions/Share.ts +++ b/src/libs/actions/Share.ts @@ -1,9 +1,10 @@ import Onyx from 'react-native-onyx'; +import type {FileObject} from '@components/AttachmentModal'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ShareTempFile} from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; -/** +/** Function for clearing old saved data before at the start of share-extension flow */ function clearShareData() { @@ -13,7 +14,7 @@ function clearShareData() { }); } -/** +/** Function storing natively shared file's properties for processing across share-extension screens function addTempShareFile(file: ShareTempFile) { @@ -23,7 +24,16 @@ function addTempShareFile(file: ShareTempFile) { Onyx.merge(ONYXKEYS.SHARE_TEMP_FILE, file); } -/** +/** + * Stores a previously validated file object in Onyx for further use. + * + * @param file Array of validated file objects to be saved + */ +function addValidatedShareFile(file: FileObject[]) { + Onyx.set(ONYXKEYS.VALIDATED_FILE_OBJECT, file.at(0)); +} + +/** Function storing selected user's details for the duration of share-extension flow, if account doesn't exist * @param user selected user's details @@ -31,6 +41,7 @@ Function storing selected user's details for the duration of share-extension flo function saveUnknownUserDetails(user: Participant) { Onyx.merge(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS, user); } + /** * Function to clear the unknown user details */ @@ -38,4 +49,4 @@ function clearUnknownUserDetails() { Onyx.merge(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS, null); } -export {addTempShareFile, saveUnknownUserDetails, clearShareData, clearUnknownUserDetails}; +export {addTempShareFile, saveUnknownUserDetails, clearShareData, addValidatedShareFile, clearUnknownUserDetails}; diff --git a/src/pages/Share/ShareDetailsPage.tsx b/src/pages/Share/ShareDetailsPage.tsx index 9781b4ea3ac7e..4fe355e399631 100644 --- a/src/pages/Share/ShareDetailsPage.tsx +++ b/src/pages/Share/ShareDetailsPage.tsx @@ -22,6 +22,7 @@ import {getFileName, readFileAsync} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {ShareNavigatorParamList} from '@libs/Navigation/types'; import {getReportDisplayOption} from '@libs/OptionsListUtils'; +import {shouldValidateFile} from '@libs/ReceiptUtils'; import {getReportOrDraftReport, isDraftReport} from '@libs/ReportUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import variables from '@styles/variables'; @@ -47,8 +48,11 @@ function ShareDetailsPage({ const {translate} = useLocalize(); const [unknownUserDetails] = useOnyx(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS, {canBeMissing: true}); const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE, {canBeMissing: true}); + const [validatedFile] = useOnyx(ONYXKEYS.VALIDATED_FILE_OBJECT, {canBeMissing: true}); + const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); - const isTextShared = currentAttachment?.mimeType === 'txt'; + const isTextShared = currentAttachment?.mimeType === CONST.SHARE_FILE_MIMETYPE.TXT; + const shouldUsePreValidatedFile = shouldValidateFile(currentAttachment); const [message, setMessage] = useState(isTextShared ? (currentAttachment?.content ?? '') : ''); const [errorTitle, setErrorTitle] = useState(undefined); const [errorMessage, setErrorMessage] = useState(undefined); @@ -56,6 +60,10 @@ function ShareDetailsPage({ const report: OnyxEntry = getReportOrDraftReport(reportOrAccountID); const displayReport = useMemo(() => getReportDisplayOption(report, unknownUserDetails, reportAttributesDerived), [report, unknownUserDetails, reportAttributesDerived]); + const fileSource = shouldUsePreValidatedFile ? (validatedFile?.uri ?? '') : (currentAttachment?.content ?? ''); + const validateFileName = shouldUsePreValidatedFile ? getFileName(validatedFile?.uri ?? CONST.ATTACHMENT_IMAGE_DEFAULT_NAME) : getFileName(currentAttachment?.content ?? ''); + const fileType = shouldUsePreValidatedFile ? (validatedFile?.type ?? CONST.SHARE_FILE_MIMETYPE.JPEG) : (currentAttachment?.mimeType ?? ''); + useEffect(() => { if (!currentAttachment?.content || errorTitle) { return; @@ -89,10 +97,8 @@ function ShareDetailsPage({ const currentUserID = getCurrentUserAccountID(); const shouldShowAttachment = !isTextShared; - const fileName = currentAttachment?.content.split('/').pop(); - const handleShare = () => { - if (!currentAttachment) { + if (!currentAttachment || (shouldUsePreValidatedFile && !validatedFile)) { return; } @@ -104,8 +110,8 @@ function ShareDetailsPage({ } readFileAsync( - currentAttachment.content, - getFileName(currentAttachment.content), + fileSource, + validateFileName, (file) => { if (isDraft) { openReport( @@ -126,7 +132,7 @@ function ShareDetailsPage({ Navigation.navigate(routeToNavigate, {forceReplace: true}); }, () => {}, - currentAttachment.mimeType, + fileType, ); }; @@ -196,14 +202,14 @@ function ShareDetailsPage({ {({show}) => ( { diff --git a/src/pages/Share/ShareRootPage.tsx b/src/pages/Share/ShareRootPage.tsx index 962b47df7daab..1d95e22300cac 100644 --- a/src/pages/Share/ShareRootPage.tsx +++ b/src/pages/Share/ShareRootPage.tsx @@ -5,16 +5,20 @@ import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import TabNavigatorSkeleton from '@components/Skeletons/TabNavigatorSkeleton'; import TabSelector from '@components/TabSelector/TabSelector'; +import useFilesValidation from '@hooks/useFilesValidation'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {addTempShareFile, clearShareData} from '@libs/actions/Share'; +import {addTempShareFile, addValidatedShareFile, clearShareData} from '@libs/actions/Share'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import {splitExtensionFromFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; +import {shouldValidateFile} from '@libs/ReceiptUtils'; import ShareActionHandler from '@libs/ShareActionHandlerModule'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ShareTempFile} from '@src/types/onyx'; import getFileSize from './getFileSize'; @@ -33,6 +37,25 @@ function showErrorAlert(title: string, message: string) { } function ShareRootPage() { + const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE, {canBeMissing: true}); + + const {validateFiles} = useFilesValidation(addValidatedShareFile); + const isTextShared = currentAttachment?.mimeType === 'txt'; + + const validateFileIfNecessary = (file: ShareTempFile) => { + if (!file || isTextShared || !shouldValidateFile(file)) { + return; + } + + validateFiles([ + { + name: file.id, + uri: file.content, + type: file.mimeType, + }, + ]); + }; + const appState = useRef(AppState.currentState); const [isFileReady, setIsFileReady] = useState(false); @@ -95,6 +118,7 @@ function ShareRootPage() { } else { setIsFileScannable(false); } + validateFileIfNecessary(tempFile); setIsFileReady(true); } diff --git a/src/pages/Share/SubmitDetailsPage.tsx b/src/pages/Share/SubmitDetailsPage.tsx index 7ad3b90db91a8..e8af065cd5b2f 100644 --- a/src/pages/Share/SubmitDetailsPage.tsx +++ b/src/pages/Share/SubmitDetailsPage.tsx @@ -22,6 +22,7 @@ import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction' import Navigation from '@libs/Navigation/Navigation'; import type {ShareNavigatorParamList} from '@libs/Navigation/types'; import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils'; +import {shouldValidateFile} from '@libs/ReceiptUtils'; import {getReportOrDraftReport, isSelfDM} from '@libs/ReportUtils'; import {getDefaultTaxCode} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; @@ -40,7 +41,6 @@ function SubmitDetailsPage({ }: ShareDetailsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE, {canBeMissing: true}); const [unknownUserDetails] = useOnyx(ONYXKEYS.SHARE_UNKNOWN_USER_DETAILS, {canBeMissing: true}); const [personalDetails] = useOnyx(`${ONYXKEYS.PERSONAL_DETAILS_LIST}`, {canBeMissing: true}); const report: OnyxEntry = getReportOrDraftReport(reportOrAccountID); @@ -53,6 +53,10 @@ function SubmitDetailsPage({ const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports}); const [currentDate] = useOnyx(ONYXKEYS.CURRENT_DATE, {canBeMissing: true}); + const [validFilesToUpload] = useOnyx(ONYXKEYS.VALIDATED_FILE_OBJECT, {canBeMissing: true}); + const [currentAttachment] = useOnyx(ONYXKEYS.SHARE_TEMP_FILE, {canBeMissing: true}); + const shouldUsePreValidatedFile = shouldValidateFile(currentAttachment); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const [startLocationPermissionFlow, setStartLocationPermissionFlow] = useState(false); @@ -62,6 +66,10 @@ function SubmitDetailsPage({ const {isBetaEnabled} = usePermissions(); const shouldGenerateTransactionThreadReport = !isBetaEnabled(CONST.BETAS.NO_OPTIMISTIC_TRANSACTION_THREADS) || !account?.shouldBlockTransactionThreadReportCreation; + const fileUri = shouldUsePreValidatedFile ? (validFilesToUpload?.uri ?? '') : (currentAttachment?.content ?? ''); + const fileName = shouldUsePreValidatedFile ? getFileName(validFilesToUpload?.uri ?? CONST.ATTACHMENT_IMAGE_DEFAULT_NAME) : getFileName(currentAttachment?.content ?? ''); + const fileType = shouldUsePreValidatedFile ? (validFilesToUpload?.type ?? CONST.RECEIPT_ALLOWED_FILE_TYPES.JPEG) : (currentAttachment?.mimeType ?? ''); + useEffect(() => { if (!errorTitle || !errorMessage) { return; @@ -193,13 +201,16 @@ function SubmitDetailsPage({ setStartLocationPermissionFlow(true); return; } + if (!currentAttachment) { + return; + } readFileAsync( - currentAttachment?.content ?? '', - getFileName(currentAttachment?.content ?? 'shared_image.png'), + fileUri, + fileName, (file) => onSuccess(file, shouldStartLocationPermissionFlow), () => {}, - currentAttachment?.mimeType ?? 'image/jpeg', + fileType, ); }; @@ -230,8 +241,8 @@ function SubmitDetailsPage({ iouComment={trimmedComment} iouCategory={transaction?.category} onConfirm={() => onConfirm(true)} - receiptPath={currentAttachment?.content} - receiptFilename={getFileName(currentAttachment?.content ?? '')} + receiptPath={fileUri} + receiptFilename={getFileName(fileName)} reportID={reportOrAccountID} shouldShowSmartScanFields={false} isDistanceRequest={false} diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index d169a77304623..ed46a44d265b1 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,3 +1,4 @@ +import type {FileObject} from '@components/AttachmentModal'; import type {OnboardingPurpose} from '@libs/actions/Welcome/OnboardingFlow'; import type Account from './Account'; import type AccountData from './AccountData'; @@ -125,6 +126,7 @@ import type WalletTerms from './WalletTerms'; import type WalletTransfer from './WalletTransfer'; export type { + FileObject, TryNewDot, Account, AccountData,