diff --git a/__mocks__/react-native-vision-camera.ts b/__mocks__/react-native-vision-camera.ts deleted file mode 100644 index 632a902e6bbd..000000000000 --- a/__mocks__/react-native-vision-camera.ts +++ /dev/null @@ -1,13 +0,0 @@ -const useCameraDevice = jest.fn(() => null); -const useCameraFormat = jest.fn(() => null); -const useCameraPermission = jest.fn(() => ({hasPermission: false, requestPermission: jest.fn(() => Promise.resolve(false))})); - -const Camera = Object.assign( - jest.fn(() => null), - { - getCameraPermissionStatus: jest.fn(() => 'not-determined'), - requestCameraPermission: jest.fn(() => Promise.resolve('granted')), - }, -); - -export {Camera, useCameraDevice, useCameraFormat, useCameraPermission}; diff --git a/assets/images/camera-flip.svg b/assets/images/camera-flip.svg deleted file mode 100644 index 2c3baf717701..000000000000 --- a/assets/images/camera-flip.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/AttachmentPicker/AttachmentCamera.tsx b/src/components/AttachmentPicker/AttachmentCamera.tsx deleted file mode 100644 index 4ecbf78bfdc3..000000000000 --- a/src/components/AttachmentPicker/AttachmentCamera.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {Modal, View} from 'react-native'; -import {GestureDetector} from 'react-native-gesture-handler'; -import {RESULTS} from 'react-native-permissions'; -import Animated from 'react-native-reanimated'; -import type {Camera, PhotoFile} from 'react-native-vision-camera'; -import {useCameraDevice, useCameraFormat, Camera as VisionCamera} from 'react-native-vision-camera'; -import ActivityIndicator from '@components/ActivityIndicator'; -import Button from '@components/Button'; -import Icon from '@components/Icon'; -import ImageSVG from '@components/ImageSVG'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import Text from '@components/Text'; -import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import {useTapToFocusGesture} from '@hooks/useNativeCamera'; -import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {showCameraPermissionsAlert} from '@libs/fileDownload/FileUtils'; -import getPhotoSource from '@libs/fileDownload/getPhotoSource'; -import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath'; -import Log from '@libs/Log'; -import CameraPermission from '@pages/iou/request/step/IOURequestStepScan/CameraPermission'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; - -type CapturedPhoto = { - uri: string; - fileName: string; - type: string; - width: number; - height: number; -}; - -type AttachmentCameraProps = { - /** Whether the camera modal is visible */ - isVisible: boolean; - - /** Callback when a photo is captured */ - onCapture: (photos: CapturedPhoto[]) => void; - - /** Callback when the camera is closed */ - onClose: () => void; -}; - -function AttachmentCamera({isVisible, onCapture, onClose}: AttachmentCameraProps) { - const theme = useTheme(); - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const insets = useSafeAreaInsets(); - const StyleUtils = useStyleUtils(); - const lazyIcons = useMemoizedLazyExpensifyIcons(['Bolt', 'boltSlash', 'CameraFlip', 'Close']); - const lazyIllustrations = useMemoizedLazyIllustrations(['Shutter', 'Hand']); - - const [cameraPosition, setCameraPosition] = useState<'back' | 'front'>('back'); - const [flash, setFlash] = useState(false); - const [cameraPermissionStatus, setCameraPermissionStatus] = useState(null); - const isCapturing = useRef(false); - const isActiveRef = useRef(false); - const cameraRef = useRef(null); - - const device = useCameraDevice(cameraPosition, { - physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'], - }); - - const format = useCameraFormat(device, [ - {photoAspectRatio: CONST.RECEIPT_CAMERA.PHOTO_ASPECT_RATIO}, - {photoResolution: {width: CONST.RECEIPT_CAMERA.PHOTO_WIDTH, height: CONST.RECEIPT_CAMERA.PHOTO_HEIGHT}}, - ]); - const hasFlash = !!device?.hasFlash; - // Format dimensions are in landscape orientation, so height/width gives portrait aspect ratio - const cameraAspectRatio = useMemo(() => (format ? format.photoHeight / format.photoWidth : undefined), [format]); - - const {tapGesture, cameraFocusIndicatorAnimatedStyle} = useTapToFocusGesture(cameraRef, device?.supportsFocus ?? false); - - // Permission management - const askForPermissions = useCallback(() => { - CameraPermission.requestCameraPermission?.() - .then((status: string) => { - setCameraPermissionStatus(status); - if (status === RESULTS.BLOCKED) { - showCameraPermissionsAlert(translate); - } - }) - .catch(() => { - setCameraPermissionStatus(RESULTS.UNAVAILABLE); - }); - }, [translate]); - - // Track visibility in a ref so async takePhoto callbacks can detect stale sessions - useEffect(() => { - isActiveRef.current = isVisible; - }, [isVisible]); - - // Refresh permissions when modal becomes visible and auto-request if denied - useEffect(() => { - if (!isVisible) { - return; - } - - let ignore = false; - CameraPermission?.getCameraPermissionStatus?.() - .then((status: string) => { - if (ignore) { - return; - } - setCameraPermissionStatus(status); - if (status === RESULTS.DENIED) { - askForPermissions(); - } - }) - .catch(() => { - if (ignore) { - return; - } - setCameraPermissionStatus(RESULTS.UNAVAILABLE); - }); - return () => { - ignore = true; - }; - }, [isVisible, askForPermissions]); - - const capturePhoto = useCallback(() => { - if (cameraPermissionStatus !== RESULTS.GRANTED) { - askForPermissions(); - return; - } - - if (!cameraRef.current || isCapturing.current) { - return; - } - - isCapturing.current = true; - - const path = getReceiptsUploadFolderPath(); - - cameraRef.current - .takePhoto({ - flash: flash && hasFlash ? 'on' : 'off', - path, - }) - .then((photo: PhotoFile) => { - // Discard capture if the camera was closed while takePhoto was in-flight - if (!isActiveRef.current) { - return; - } - const uri = getPhotoSource(photo.path); - const fileName = photo.path.substring(photo.path.lastIndexOf('/') + 1) || `photo_${Date.now()}.jpg`; - - onCapture([ - { - uri, - fileName, - type: 'image/jpeg', - width: photo.width, - height: photo.height, - }, - ]); - }) - .catch((error: Error) => { - Log.warn('Error capturing photo', {error: error.message}); - }) - .finally(() => { - isCapturing.current = false; - }); - }, [askForPermissions, cameraPermissionStatus, flash, hasFlash, onCapture]); - - const handleClose = useCallback(() => { - isCapturing.current = false; - setFlash(false); - setCameraPosition('back'); - onClose(); - }, [onClose]); - - return ( - - - {/* Close button */} - - - - - - - {/* Camera area */} - - {cameraPermissionStatus !== RESULTS.GRANTED && ( - - - {translate('receipt.takePhoto')} - {translate('receipt.cameraAccess')} -