diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4a9850e1f625..3976d12a9537 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -454,6 +454,14 @@ type MoneyRequestNavigatorParamList = { pageIndex?: string; backTo?: string; }; + [SCREENS.MONEY_REQUEST.STEP_SCAN]: { + action: ValueOf; + iouType: ValueOf; + transactionID: string; + reportID: string; + pageIndex: number; + backTo: Routes; + }; }; type NewTaskNavigatorParamList = { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c2d462bbc4a8..40a7d5a52f24 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -5277,9 +5277,9 @@ function replaceReceipt(transactionID: string, file: File, source: string) { * @param transactionID of the transaction to set the participants of * @param report attached to the transaction */ -function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxTypes.Report) { +function setMoneyRequestParticipantsFromReport(transactionID: string, report: OnyxEntry) { // If the report is iou or expense report, we should get the chat report to set participant for request money - const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report.chatReportID) : report; + const chatReport = ReportUtils.isMoneyRequestReport(report) ? ReportUtils.getReport(report?.chatReportID) : report; const currentUserAccountID = currentUserPersonalDetails.accountID; const shouldAddAsReport = !isEmptyObject(chatReport) && ReportUtils.isSelfDM(chatReport); const participants: Participant[] = diff --git a/src/libs/fileDownload/types.ts b/src/libs/fileDownload/types.ts index f8c92351d36c..b59a656d0ac2 100644 --- a/src/libs/fileDownload/types.ts +++ b/src/libs/fileDownload/types.ts @@ -8,7 +8,7 @@ type GetImageResolution = (url: File | Asset) => Promise; type ExtensionAndFileName = {fileName: string; fileExtension: string}; type SplitExtensionFromFileName = (fileName: string) => ExtensionAndFileName; -type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure: (error?: unknown) => void, fileType?: string) => Promise; +type ReadFileAsync = (path: string, fileName: string, onSuccess: (file: File) => void, onFailure?: (error?: unknown) => void, fileType?: string) => Promise; type AttachmentDetails = { previewSourceURL: null | string; diff --git a/src/pages/iou/ReceiptDropUI.js b/src/pages/iou/ReceiptDropUI.tsx similarity index 78% rename from src/pages/iou/ReceiptDropUI.js rename to src/pages/iou/ReceiptDropUI.tsx index 0f7226668a80..f1f25f80bc57 100644 --- a/src/pages/iou/ReceiptDropUI.js +++ b/src/pages/iou/ReceiptDropUI.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; @@ -9,19 +8,15 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -const propTypes = { - /** Callback to execute when a file is dropped. */ - onDrop: PropTypes.func.isRequired, +type ReceiptDropUIProps = { + /** Function to execute when an item is dropped in the drop zone. */ + onDrop: (event: DragEvent) => void; /** Pixels the receipt image should be shifted down to match the non-drag view UI */ - receiptImageTopPosition: PropTypes.number, + receiptImageTopPosition?: number; }; -const defaultProps = { - receiptImageTopPosition: 0, -}; - -function ReceiptDropUI({onDrop, receiptImageTopPosition}) { +function ReceiptDropUI({onDrop, receiptImageTopPosition = 0}: ReceiptDropUIProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); return ( @@ -43,7 +38,5 @@ function ReceiptDropUI({onDrop, receiptImageTopPosition}) { } ReceiptDropUI.displayName = 'ReceiptDropUI'; -ReceiptDropUI.propTypes = propTypes; -ReceiptDropUI.defaultProps = defaultProps; export default ReceiptDropUI; diff --git a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.android.js b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.android.tsx similarity index 63% rename from src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.android.js rename to src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.android.tsx index 3eb9ef4eea5a..9ab14d714fca 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.android.js +++ b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.android.tsx @@ -1,4 +1,5 @@ import {check, PERMISSIONS, request} from 'react-native-permissions'; +import type CameraPermissionModule from './types'; function requestCameraPermission() { return request(PERMISSIONS.ANDROID.CAMERA); @@ -9,4 +10,9 @@ function getCameraPermissionStatus() { return check(PERMISSIONS.ANDROID.CAMERA); } -export {requestCameraPermission, getCameraPermissionStatus}; +const CameraPermission: CameraPermissionModule = { + requestCameraPermission, + getCameraPermissionStatus, +}; + +export default CameraPermission; diff --git a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.ios.js b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.ios.tsx similarity index 54% rename from src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.ios.js rename to src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.ios.tsx index 3c24bfa10d6f..cc73d94515cb 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.ios.js +++ b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.ios.tsx @@ -1,4 +1,5 @@ import {check, PERMISSIONS, request} from 'react-native-permissions'; +import type CameraPermissionModule from './types'; function requestCameraPermission() { return request(PERMISSIONS.IOS.CAMERA); @@ -8,4 +9,9 @@ function getCameraPermissionStatus() { return check(PERMISSIONS.IOS.CAMERA); } -export {requestCameraPermission, getCameraPermissionStatus}; +const CameraPermission: CameraPermissionModule = { + requestCameraPermission, + getCameraPermissionStatus, +}; + +export default CameraPermission; diff --git a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.js b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.js deleted file mode 100644 index 4357b592d7ef..000000000000 --- a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.js +++ /dev/null @@ -1,5 +0,0 @@ -function requestCameraPermission() {} - -function getCameraPermissionStatus() {} - -export {requestCameraPermission, getCameraPermissionStatus}; diff --git a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.tsx new file mode 100644 index 000000000000..238aa672a998 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/index.tsx @@ -0,0 +1,8 @@ +import type CameraPermissionModule from './types'; + +const CameraPermission: CameraPermissionModule = { + requestCameraPermission: undefined, + getCameraPermissionStatus: undefined, +}; + +export default CameraPermission; diff --git a/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/types.ts b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/types.ts new file mode 100644 index 000000000000..b5a1f3fba9c7 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepScan/CameraPermission/types.ts @@ -0,0 +1,6 @@ +type CameraPermissionModule = { + requestCameraPermission: (() => Promise) | undefined; + getCameraPermissionStatus: (() => Promise) | undefined; +}; + +export default CameraPermissionModule; diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx similarity index 65% rename from src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js rename to src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx index 64fa291b2003..beeb8938e917 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.native.tsx @@ -1,15 +1,11 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {ForwardedRef} from 'react'; import {Camera} from 'react-native-vision-camera'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; - -const propTypes = { - /* The index of the tab that contains this camera */ - cameraTabIndex: PropTypes.number.isRequired, -}; +import type {NavigationAwareCameraNativeProps} from './types'; // Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => { +function NavigationAwareCamera({cameraTabIndex, ...props}: NavigationAwareCameraNativeProps, ref: ForwardedRef) { const isCameraActive = useTabNavigatorFocus({tabIndex: cameraTabIndex}); return ( @@ -21,9 +17,8 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) isActive={isCameraActive} /> ); -}); +} -NavigationAwareCamera.propTypes = propTypes; NavigationAwareCamera.displayName = 'NavigationAwareCamera'; -export default NavigationAwareCamera; +export default React.forwardRef(NavigationAwareCamera); diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.tsx similarity index 60% rename from src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js rename to src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.tsx index 37223915f4a2..2e5f1b2014b3 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/index.tsx @@ -1,16 +1,13 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {ForwardedRef} from 'react'; import {View} from 'react-native'; +import type {Camera} from 'react-native-vision-camera'; import Webcam from 'react-webcam'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; - -const propTypes = { - /** The index of the tab that contains this camera */ - cameraTabIndex: PropTypes.number.isRequired, -}; +import type {NavigationAwareCameraProps} from './types'; // Wraps a camera that will only be active when the tab is focused or as soon as it starts to become focused. -const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) => { +function NavigationAwareCamera({torchOn, onTorchAvailability, cameraTabIndex, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) { const shouldShowCamera = useTabNavigatorFocus({ tabIndex: cameraTabIndex, }); @@ -21,17 +18,14 @@ const NavigationAwareCamera = React.forwardRef(({cameraTabIndex, ...props}, ref) return ( } /> ); -}); +} -NavigationAwareCamera.propTypes = propTypes; NavigationAwareCamera.displayName = 'NavigationAwareCamera'; -export default NavigationAwareCamera; +export default React.forwardRef(NavigationAwareCamera); diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts new file mode 100644 index 000000000000..0e6845792122 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts @@ -0,0 +1,19 @@ +import type {CameraProps} from 'react-native-vision-camera'; +import type {WebcamProps} from 'react-webcam'; + +type NavigationAwareCameraProps = WebcamProps & { + /** Flag to turn on/off the torch/flashlight - if available */ + torchOn?: boolean; + + /** The index of the tab that contains this camera */ + onTorchAvailability?: (torchAvailable: boolean) => void; + + /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */ + cameraTabIndex: number; +}; + +type NavigationAwareCameraNativeProps = CameraProps & { + cameraTabIndex: number; +}; + +export type {NavigationAwareCameraProps, NavigationAwareCameraNativeProps}; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.js b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx similarity index 81% rename from src/pages/iou/request/step/IOURequestStepScan/index.native.js rename to src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index 83ca90e7330b..e084a3db7422 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -1,15 +1,16 @@ import {useFocusEffect} from '@react-navigation/core'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useRef, useState} from 'react'; import {ActivityIndicator, Alert, AppState, InteractionManager, View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import {RESULTS} from 'react-native-permissions'; import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withSpring, withTiming} from 'react-native-reanimated'; +import type {Camera, PhotoFile, Point} from 'react-native-vision-camera'; import {useCameraDevice} from 'react-native-vision-camera'; import Hand from '@assets/images/hand.svg'; import Shutter from '@assets/images/shutter.svg'; +import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Button from '@components/Button'; import Icon from '@components/Icon'; @@ -17,49 +18,31 @@ import * as Expensicons from '@components/Icon/Expensicons'; import ImageSVG from '@components/ImageSVG'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; -import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import IOURequestStepRoutePropTypes from '@pages/iou/request/step/IOURequestStepRoutePropTypes'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import * as CameraPermission from './CameraPermission'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import CameraPermission from './CameraPermission'; import NavigationAwareCamera from './NavigationAwareCamera'; +import type IOURequestStepOnyxProps from './types'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** The report that the transaction belongs to */ - report: reportPropTypes, - - /** Information about the logged in user's account */ - user: PropTypes.shape({ - /** Whether user muted all sounds in the application */ - isMutedAllSounds: PropTypes.bool, - }), - - /** The transaction (or draft transaction) being changed */ - transaction: transactionPropTypes, -}; - -const defaultProps = { - report: {}, - user: {}, - transaction: {}, -}; +type IOURequestStepScanProps = IOURequestStepOnyxProps & + WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; + }; function IOURequestStepScan({ report, @@ -67,8 +50,8 @@ function IOURequestStepScan({ route: { params: {action, iouType, reportID, transactionID, backTo}, }, - transaction: {isFromGlobalCreate}, -}) { + transaction, +}: IOURequestStepScanProps) { const theme = useTheme(); const styles = useThemeStyles(); const device = useCameraDevice('back', { @@ -76,9 +59,9 @@ function IOURequestStepScan({ }); const hasFlash = device != null && device.hasFlash; - const camera = useRef(null); + const camera = useRef(null); const [flash, setFlash] = useState(false); - const [cameraPermissionStatus, setCameraPermissionStatus] = useState(undefined); + const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); const [didCapturePhoto, setDidCapturePhoto] = useState(false); const {translate} = useLocalize(); @@ -86,8 +69,8 @@ function IOURequestStepScan({ const askForPermissions = () => { // There's no way we can check for the BLOCKED status without requesting the permission first // https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670 - CameraPermission.requestCameraPermission() - .then((status) => { + CameraPermission.requestCameraPermission?.() + .then((status: string) => { setCameraPermissionStatus(status); if (status === RESULTS.BLOCKED) { @@ -108,7 +91,7 @@ function IOURequestStepScan({ transform: [{translateX: focusIndicatorPosition.value.x}, {translateY: focusIndicatorPosition.value.y}, {scale: focusIndicatorScale.value}], })); - const focusCamera = (point) => { + const focusCamera = (point: Point) => { if (!camera.current) { return; } @@ -122,8 +105,8 @@ function IOURequestStepScan({ }; const tapGesture = Gesture.Tap() - .enabled(device && device.supportsFocus) - .onStart((ev) => { + .enabled(device?.supportsFocus ?? false) + .onStart((ev: {x: number; y: number}) => { const point = {x: ev.x, y: ev.y}; focusIndicatorOpacity.value = withSequence(withTiming(0.8, {duration: 250}), withDelay(1000, withTiming(0, {duration: 250}))); @@ -138,7 +121,7 @@ function IOURequestStepScan({ useCallback(() => { setDidCapturePhoto(false); const refreshCameraPermissionStatus = () => { - CameraPermission.getCameraPermissionStatus() + CameraPermission?.getCameraPermissionStatus?.() .then(setCameraPermissionStatus) .catch(() => setCameraPermissionStatus(RESULTS.UNAVAILABLE)); }; @@ -163,19 +146,21 @@ function IOURequestStepScan({ }, []), ); - const validateReceipt = (file) => { - const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', '')); - if (!CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase())) { + const validateReceipt = (file: FileObject) => { + const {fileExtension} = FileUtils.splitExtensionFromFileName(file?.name ?? ''); + if ( + !CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase() as (typeof CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS)[number]) + ) { Alert.alert(translate('attachmentPicker.wrongFileType'), translate('attachmentPicker.notAllowedExtension')); return false; } - if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + if ((file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { Alert.alert(translate('attachmentPicker.attachmentTooLarge'), translate('attachmentPicker.sizeExceeded')); return false; } - if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + if ((file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { Alert.alert(translate('attachmentPicker.attachmentTooSmall'), translate('attachmentPicker.sizeNotMet')); return false; } @@ -193,7 +178,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { + if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } @@ -201,22 +186,22 @@ function IOURequestStepScan({ // If the transaction was created from the + menu from the composer inside of a chat, the participants can automatically // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. IOU.setMoneyRequestParticipantsFromReport(transactionID, report); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); - }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); + }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]); const updateScanAndNavigate = useCallback( - (file, source) => { + (file: FileObject, source: string) => { Navigation.dismissModal(); - IOU.replaceReceipt(transactionID, file, source); + IOU.replaceReceipt(transactionID, file as File, source); }, [transactionID], ); /** * Sets the Receipt objects and navigates the user to the next page - * @param {Object} file */ - const setReceiptAndNavigate = (file) => { + const setReceiptAndNavigate = (file: FileObject) => { if (!validateReceipt(file)) { return; } @@ -224,10 +209,10 @@ function IOURequestStepScan({ // 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 - IOU.setMoneyRequestReceipt(transactionID, file.uri, file.name, action !== CONST.IOU.ACTION.EDIT, file.type); + IOU.setMoneyRequestReceipt(transactionID, file?.uri ?? '', file.name ?? '', action !== CONST.IOU.ACTION.EDIT, file.type); if (action === CONST.IOU.ACTION.EDIT) { - updateScanAndNavigate(file, file.uri); + updateScanAndNavigate(file, file?.uri ?? ''); return; } @@ -246,19 +231,18 @@ function IOURequestStepScan({ if (!camera.current) { showCameraAlert(); - return; } if (didCapturePhoto) { return; } - return camera.current - .takePhoto({ + camera?.current + ?.takePhoto({ flash: flash && hasFlash ? 'on' : 'off', - enableShutterSound: !user.isMutedAllSounds, + enableShutterSound: !user?.isMutedAllSounds, }) - .then((photo) => { + .then((photo: PhotoFile) => { // Store the receipt on the transaction object in Onyx const source = `file://${photo.path}`; IOU.setMoneyRequestReceipt(transactionID, source, photo.path, action !== CONST.IOU.ACTION.EDIT); @@ -273,12 +257,12 @@ function IOURequestStepScan({ setDidCapturePhoto(true); navigateToConfirmationStep(); }) - .catch((error) => { + .catch((error: string) => { setDidCapturePhoto(false); showCameraAlert(); Log.warn('Error taking photo', error); }); - }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user.isMutedAllSounds, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]); + }, [cameraPermissionStatus, didCapturePhoto, flash, hasFlash, user?.isMutedAllSounds, translate, transactionID, action, navigateToConfirmationStep, updateScanAndNavigate]); // Wait for camera permission status to render if (cameraPermissionStatus == null) { @@ -331,6 +315,7 @@ function IOURequestStepScan({ )} - + {({openPicker}) => ( ({ + user: { + key: ONYXKEYS.USER, + }, +})(IOURequestStepScan); + +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScanOnyxProps); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepScanWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepScanWithWritableReportOrNotFound); + +export default IOURequestStepScanWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.js b/src/pages/iou/request/step/IOURequestStepScan/index.tsx similarity index 76% rename from src/pages/iou/request/step/IOURequestStepScan/index.js rename to src/pages/iou/request/step/IOURequestStepScan/index.tsx index 056f68385dc4..b9c4f866d493 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.js +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -1,10 +1,11 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import {ActivityIndicator, PanResponder, PixelRatio, View} from 'react-native'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type Webcam from 'react-webcam'; import Hand from '@assets/images/hand.svg'; import ReceiptUpload from '@assets/images/receipt-upload.svg'; import Shutter from '@assets/images/shutter.svg'; +import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; @@ -14,74 +15,65 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; -import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useTabNavigatorFocus from '@hooks/useTabNavigatorFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import compose from '@libs/compose'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import ReceiptDropUI from '@pages/iou/ReceiptDropUI'; -import IOURequestStepRoutePropTypes from '@pages/iou/request/step/IOURequestStepRoutePropTypes'; import StepScreenDragAndDropWrapper from '@pages/iou/request/step/StepScreenDragAndDropWrapper'; import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound'; +import type {WithWritableReportOrNotFoundProps} from '@pages/iou/request/step/withWritableReportOrNotFound'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; -import reportPropTypes from '@pages/reportPropTypes'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import NavigationAwareCamera from './NavigationAwareCamera'; +import type IOURequestStepOnyxProps from './types'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, - - /* Onyx Props */ - /** The report that the transaction belongs to */ - report: reportPropTypes, - - /** The transaction (or draft transaction) being changed */ - transaction: transactionPropTypes, -}; - -const defaultProps = { - report: {}, - transaction: {}, -}; +type IOURequestStepScanProps = IOURequestStepOnyxProps & + WithWritableReportOrNotFoundProps & { + /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ + transaction: OnyxEntry; + }; function IOURequestStepScan({ report, route: { params: {action, iouType, reportID, transactionID, backTo}, }, - transaction: {isFromGlobalCreate}, -}) { + transaction, +}: IOURequestStepScanProps) { const theme = useTheme(); const styles = useThemeStyles(); // Grouping related states const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); - const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(''); - const [attachmentInvalidReason, setAttachmentValidReason] = useState(''); + const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(); + const [attachmentInvalidReason, setAttachmentValidReason] = useState(); const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); const {isDraggingOver} = useContext(DragAndDropContext); - const [cameraPermissionState, setCameraPermissionState] = useState('prompt'); + const [cameraPermissionState, setCameraPermissionState] = useState('prompt'); const [isFlashLightOn, toggleFlashlight] = useReducer((state) => !state, false); const [isTorchAvailable, setIsTorchAvailable] = useState(false); - const cameraRef = useRef(null); - const trackRef = useRef(null); + const cameraRef = useRef(null); + const trackRef = useRef(null); const [isQueriedPermissionState, setIsQueriedPermissionState] = useState(false); - const getScreenshotTimeoutRef = useRef(null); + const getScreenshotTimeoutRef = useRef(null); - const [videoConstraints, setVideoConstraints] = useState(null); + const [videoConstraints, setVideoConstraints] = useState(); const tabIndex = 1; const isTabActive = useTabNavigatorFocus({tabIndex}); @@ -90,23 +82,28 @@ function IOURequestStepScan({ * The last deviceId is of regular len camera. */ const requestCameraPermission = useCallback(() => { - if (!_.isEmpty(videoConstraints) || !Browser.isMobile()) { + if (!isEmptyObject(videoConstraints) || !Browser.isMobile()) { return; } const defaultConstraints = {facingMode: {exact: 'environment'}}; navigator.mediaDevices + // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 .getUserMedia({video: {facingMode: {exact: 'environment'}, zoom: {ideal: 1}}}) .then((stream) => { setCameraPermissionState('granted'); - _.forEach(stream.getTracks(), (track) => track.stop()); + stream.getTracks().forEach((track) => track.stop()); // Only Safari 17+ supports zoom constraint if (Browser.isMobileSafari() && stream.getTracks().length > 0) { - const deviceId = _.chain(stream.getTracks()) - .map((track) => track.getSettings()) - .find((setting) => setting.zoom === 1) - .get('deviceId') - .value(); + let deviceId; + for (const track of stream.getTracks()) { + const setting = track.getSettings(); + // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 + if (setting.zoom === 1) { + deviceId = setting.deviceId; + break; + } + } if (deviceId) { setVideoConstraints({deviceId}); return; @@ -117,12 +114,14 @@ function IOURequestStepScan({ return; } navigator.mediaDevices.enumerateDevices().then((devices) => { - const lastBackDeviceId = _.chain(devices) - .filter((item) => item.kind === 'videoinput') - .last() - .get('deviceId', '') - .value(); - + let lastBackDeviceId = ''; + for (let i = devices.length - 1; i >= 0; i--) { + const device = devices[i]; + if (device.kind === 'videoinput') { + lastBackDeviceId = device.deviceId; + break; + } + } if (!lastBackDeviceId) { setVideoConstraints(defaultConstraints); return; @@ -142,7 +141,10 @@ function IOURequestStepScan({ return; } navigator.permissions - .query({name: 'camera'}) + .query({ + // @ts-expect-error camera does exist in PermissionName + name: 'camera', + }) .then((permissionState) => { setCameraPermissionState(permissionState.state); if (permissionState.state === 'granted') { @@ -165,29 +167,28 @@ function IOURequestStepScan({ /** * Sets the upload receipt error modal content when an invalid receipt is uploaded - * @param {*} isInvalid - * @param {*} title - * @param {*} reason */ - const setUploadReceiptError = (isInvalid, title, reason) => { + const setUploadReceiptError = (isInvalid: boolean, title: TranslationPaths, reason: TranslationPaths) => { setIsAttachmentInvalid(isInvalid); setAttachmentInvalidReasonTitle(title); setAttachmentValidReason(reason); }; - function validateReceipt(file) { - const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', '')); - if (!CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase())) { + function validateReceipt(file: FileObject) { + const {fileExtension} = FileUtils.splitExtensionFromFileName(file?.name ?? ''); + if ( + !CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes(fileExtension.toLowerCase() as (typeof CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS)[number]) + ) { setUploadReceiptError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension'); return false; } - if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + if ((file?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { setUploadReceiptError(true, 'attachmentPicker.attachmentTooLarge', 'attachmentPicker.sizeExceeded'); return false; } - if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + if ((file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { setUploadReceiptError(true, 'attachmentPicker.attachmentTooSmall', 'attachmentPicker.sizeNotMet'); return false; } @@ -206,7 +207,7 @@ function IOURequestStepScan({ } // If the transaction was created from the global create, the person needs to select participants, so take them there. - if (isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { + if (transaction?.isFromGlobalCreate && iouType !== CONST.IOU.TYPE.TRACK_EXPENSE) { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(iouType, transactionID, reportID)); return; } @@ -215,11 +216,11 @@ function IOURequestStepScan({ // be added to the transaction (taken from the chat report participants) and then the person is taken to the confirmation step. IOU.setMoneyRequestParticipantsFromReport(transactionID, report); Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID)); - }, [iouType, report, reportID, transactionID, isFromGlobalCreate, backTo]); + }, [iouType, report, reportID, transactionID, transaction?.isFromGlobalCreate, backTo]); const updateScanAndNavigate = useCallback( - (file, source) => { - IOU.replaceReceipt(transactionID, file, source); + (file: FileObject, source: string) => { + IOU.replaceReceipt(transactionID, file as File, source); Navigation.dismissModal(); }, [transactionID], @@ -227,16 +228,16 @@ function IOURequestStepScan({ /** * Sets the Receipt objects and navigates the user to the next page - * @param {Object} file */ - const setReceiptAndNavigate = (file) => { + const setReceiptAndNavigate = (file: FileObject) => { if (!validateReceipt(file)) { return; } // Store the receipt on the transaction object in Onyx - const source = URL.createObjectURL(file); - IOU.setMoneyRequestReceipt(transactionID, source, file.name, action !== CONST.IOU.ACTION.EDIT); + const source = URL.createObjectURL(file as Blob); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + IOU.setMoneyRequestReceipt(transactionID, source, file.name || '', action !== CONST.IOU.ACTION.EDIT); if (action === CONST.IOU.ACTION.EDIT) { updateScanAndNavigate(file, source); @@ -246,15 +247,16 @@ function IOURequestStepScan({ navigateToConfirmationStep(); }; - const setupCameraPermissionsAndCapabilities = (stream) => { + const setupCameraPermissionsAndCapabilities = (stream: MediaStream) => { setCameraPermissionState('granted'); const [track] = stream.getVideoTracks(); const capabilities = track.getCapabilities(); - if (capabilities.torch) { + + if ('torch' in capabilities && capabilities.torch) { trackRef.current = track; } - setIsTorchAvailable(!!capabilities.torch); + setIsTorchAvailable('torch' in capabilities && !!capabilities.torch); }; const getScreenshot = useCallback(() => { @@ -266,7 +268,7 @@ function IOURequestStepScan({ const imageBase64 = cameraRef.current.getScreenshot(); const filename = `receipt_${Date.now()}.png`; - const file = FileUtils.base64ToFile(imageBase64, filename); + const file = FileUtils.base64ToFile(imageBase64 ?? '', filename); const source = URL.createObjectURL(file); IOU.setMoneyRequestReceipt(transactionID, source, file.name, action !== CONST.IOU.ACTION.EDIT); @@ -283,6 +285,7 @@ function IOURequestStepScan({ return; } trackRef.current.applyConstraints({ + // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 advanced: [{torch: false}], }); }, []); @@ -291,6 +294,7 @@ function IOURequestStepScan({ if (trackRef.current && isFlashLightOn) { trackRef.current .applyConstraints({ + // @ts-expect-error there is a type mismatch in typescipt types for MediaStreamTrack microsoft/TypeScript#39010 advanced: [{torch: true}], }) .then(() => { @@ -324,7 +328,7 @@ function IOURequestStepScan({ const mobileCameraView = () => ( <> - {((cameraPermissionState === 'prompt' && !isQueriedPermissionState) || (cameraPermissionState === 'granted' && _.isEmpty(videoConstraints))) && ( + {((cameraPermissionState === 'prompt' && !isQueriedPermissionState) || (cameraPermissionState === 'granted' && isEmptyObject(videoConstraints))) && ( {translate('receipt.takePhoto')} {translate('receipt.cameraAccess')} @@ -351,7 +355,7 @@ function IOURequestStepScan({ /> )} - {cameraPermissionState === 'granted' && !_.isEmpty(videoConstraints) && ( + {cameraPermissionState === 'granted' && !isEmptyObject(videoConstraints) && ( setCameraPermissionState('denied')} @@ -361,6 +365,11 @@ function IOURequestStepScan({ videoConstraints={videoConstraints} forceScreenshotSourceSize cameraTabIndex={tabIndex} + audio={false} + disablePictureInPicture={false} + imageSmoothing={false} + mirrored={false} + screenshotQuality={0} /> )} @@ -417,7 +426,7 @@ function IOURequestStepScan({ const desktopUploadView = () => ( <> - setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.top))}> + setReceiptImageTopPosition(PixelRatio.roundToNearestPixel(nativeEvent.layout.y))}> { - const file = lodashGet(e, ['dataTransfer', 'files', 0]); - setReceiptAndNavigate(file); + const file = e?.dataTransfer?.files[0]; + if (file) { + setReceiptAndNavigate(file); + } }} receiptImageTopPosition={receiptImageTopPosition} /> @@ -489,8 +500,11 @@ function IOURequestStepScan({ ); } -IOURequestStepScan.defaultProps = defaultProps; -IOURequestStepScan.propTypes = propTypes; IOURequestStepScan.displayName = 'IOURequestStepScan'; -export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepScan); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepScanWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepScan); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepScanWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepScanWithWritableReportOrNotFound); + +export default IOURequestStepScanWithFullTransactionOrNotFound; diff --git a/src/pages/iou/request/step/IOURequestStepScan/types.ts b/src/pages/iou/request/step/IOURequestStepScan/types.ts new file mode 100644 index 000000000000..adf3e5c81748 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepScan/types.ts @@ -0,0 +1,8 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type * as OnyxTypes from '@src/types/onyx'; + +type IOURequestStepOnyxProps = { + user: OnyxEntry; +}; + +export default IOURequestStepOnyxProps; diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 3d741725032b..e3aa1ed2431d 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -26,7 +26,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_TAG | typeof SCREENS.MONEY_REQUEST.STEP_CONFIRMATION | typeof SCREENS.MONEY_REQUEST.STEP_CATEGORY - | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE; + | typeof SCREENS.MONEY_REQUEST.STEP_TAX_RATE + | typeof SCREENS.MONEY_REQUEST.STEP_SCAN; type Route = RouteProp; diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 5e32e21f7eed..4cad980eb680 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -26,7 +26,8 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_TAG | typeof SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS | typeof SCREENS.MONEY_REQUEST.STEP_MERCHANT - | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT; + | typeof SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT + | typeof SCREENS.MONEY_REQUEST.STEP_SCAN; type Route = RouteProp;