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')}
-
-
- )}
- {cameraPermissionStatus === RESULTS.GRANTED && device == null && (
-
-
-
- )}
- {cameraPermissionStatus === RESULTS.GRANTED && device != null && (
-
-
-
-
-
-
-
-
- )}
-
-
- {/* Bottom controls */}
-
- {/* Flash toggle */}
- setFlash((prevFlash) => !prevFlash)}
- sentryLabel="AttachmentCamera-Flash"
- >
-
-
-
- {/* Shutter button */}
-
-
-
-
- {/* Camera flip */}
- setCameraPosition((prev) => (prev === 'back' ? 'front' : 'back'))}
- sentryLabel="AttachmentCamera-FlipCamera"
- >
-
-
-
-
-
- );
-}
-
-export default AttachmentCamera;
-export type {CapturedPhoto};
diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx
index a1a5f8faecf3..1b4a74e55441 100644
--- a/src/components/AttachmentPicker/index.native.tsx
+++ b/src/components/AttachmentPicker/index.native.tsx
@@ -24,8 +24,7 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {FileObject, ImagePickerResponse as FileResponse} from '@src/types/utils/Attachment';
import type IconAsset from '@src/types/utils/IconAsset';
-import AttachmentCamera from './AttachmentCamera';
-import type {CapturedPhoto} from './AttachmentCamera';
+import launchCamera from './launchCamera/launchCamera';
import type AttachmentPickerProps from './types';
const EXTENSION_TO_NATIVE_TYPE: Record = {
@@ -54,23 +53,14 @@ type LocalCopy = {
type: string | null;
};
-type Item =
- | {
- /** The icon associated with the item. */
- icon: IconAsset;
- /** The key in the translations file to use for the title */
- textTranslationKey: TranslationPaths;
- /** Function to call when the user clicks the item */
- pickAttachment: () => Promise;
- }
- | {
- /** The icon associated with the item. */
- icon: IconAsset;
- /** The key in the translations file to use for the title */
- textTranslationKey: TranslationPaths;
- /** Direct action that doesn't go through the promise-based selectItem flow */
- onPress: () => void;
- };
+type Item = {
+ /** The icon associated with the item. */
+ icon: IconAsset;
+ /** The key in the translations file to use for the title */
+ textTranslationKey: TranslationPaths;
+ /** Function to call when the user clicks the item */
+ pickAttachment: () => Promise;
+};
/**
* Ensures asset has proper fileName and type properties
@@ -158,7 +148,6 @@ function AttachmentPicker({
const icons = useMemoizedLazyExpensifyIcons(['Camera', 'Gallery', 'Paperclip']);
const styles = useThemeStyles();
const [isVisible, setIsVisible] = useState(false);
- const [showAttachmentCamera, setShowAttachmentCamera] = useState(false);
const StyleUtils = useStyleUtils();
const theme = useTheme();
@@ -181,20 +170,10 @@ function AttachmentPicker({
[translate],
);
- /**
- * Launch the in-app VisionCamera instead of the external system camera.
- * Opens the camera modal directly — bypasses the promise-based selectItem flow.
- * handleCameraCapture / handleCameraClose handle completion.
- */
- const launchInAppCamera = useCallback(() => {
- onOpenPicker?.();
- setShowAttachmentCamera(true);
- }, [onOpenPicker]);
-
/**
* Common image picker handling
*
- * @param {function} imagePickerFunc - RNImagePicker.launchImageLibrary
+ * @param {function} imagePickerFunc - RNImagePicker.launchCamera or RNImagePicker.launchImageLibrary
*/
const showImagePicker = useCallback(
(imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise =>
@@ -360,12 +339,12 @@ function AttachmentPicker({
data.unshift({
icon: icons.Camera,
textTranslationKey: 'attachmentPicker.takePhoto',
- onPress: launchInAppCamera,
+ pickAttachment: () => showImagePicker(launchCamera),
});
}
return data;
- }, [icons.Camera, icons.Paperclip, icons.Gallery, showDocumentPicker, shouldHideGalleryOption, shouldHideCameraOption, launchInAppCamera, showImagePicker]);
+ }, [icons.Camera, icons.Paperclip, icons.Gallery, showDocumentPicker, shouldHideGalleryOption, shouldHideCameraOption, showImagePicker]);
const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItemData.length - 1, isActive: isVisible});
@@ -488,31 +467,6 @@ function AttachmentPicker({
[handleImageProcessingError, shouldValidateImage, showGeneralAlert, showImageCorruptionAlert],
);
- const handleCameraCapture = useCallback(
- (photos: CapturedPhoto[]) => {
- setShowAttachmentCamera(false);
- const assets: Asset[] = photos.map((photo) => ({
- uri: photo.uri,
- fileName: photo.fileName,
- type: photo.type,
- width: photo.width,
- height: photo.height,
- }));
- Promise.resolve(pickAttachment(assets)).finally(() => {
- onClosed.current();
- delete onModalHide.current;
- });
- },
- [pickAttachment],
- );
-
- const handleCameraClose = useCallback(() => {
- setShowAttachmentCamera(false);
- onCanceled.current();
- onClosed.current();
- delete onModalHide.current;
- }, []);
-
/**
* Opens the attachment modal, or directly launches the document picker when shouldSkipAttachmentTypeModal is true.
*/
@@ -548,19 +502,6 @@ function AttachmentPicker({
*/
const selectItem = useCallback(
(item: Item) => {
- /* Items with onPress (e.g. in-app camera) handle their own flow
- * and don't go through the promise-based pickAttachment chain.
- * We still defer via onModalHide so the popover fully dismisses
- * before the camera Modal presents — on iOS, presenting a new
- * modal while another is closing can cause the new one to fail. */
- if ('onPress' in item) {
- onModalHide.current = () => {
- item.onPress();
- };
- close();
- return;
- }
-
onOpenPicker?.();
/* setTimeout delays execution to the frame after the modal closes
* without this on iOS closing the modal closes the gallery/camera as well */
@@ -637,11 +578,6 @@ function AttachmentPicker({
))}
-
{renderChildren()}
>
);
diff --git a/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts b/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts
new file mode 100644
index 000000000000..0f551d07019b
--- /dev/null
+++ b/src/components/AttachmentPicker/launchCamera/launchCamera.android.ts
@@ -0,0 +1,32 @@
+import {PermissionsAndroid} from 'react-native';
+import {launchCamera as launchCameraImagePicker} from 'react-native-image-picker';
+import type {LaunchCamera} from './types';
+import {ErrorLaunchCamera} from './types';
+
+/**
+ * Launching the camera for Android involves checking for permissions
+ * And only then starting the camera
+ * If the user deny permission the callback will be called with an error response
+ * in the same format as the error returned by react-native-image-picker
+ */
+const launchCamera: LaunchCamera = (options, callback) => {
+ // Checks current camera permissions and prompts the user in case they aren't granted
+ PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.CAMERA)
+ .then((permission) => {
+ if (permission !== PermissionsAndroid.RESULTS.GRANTED) {
+ throw new ErrorLaunchCamera('User did not grant permissions', 'permission');
+ }
+
+ launchCameraImagePicker(options, callback);
+ })
+ .catch((error: ErrorLaunchCamera) => {
+ /* Intercept the permission error as well as any other errors and call the callback
+ * follow the same pattern expected for image picker results */
+ callback({
+ errorMessage: error.message,
+ errorCode: error.errorCode || 'others',
+ });
+ });
+};
+
+export default launchCamera;
diff --git a/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts b/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts
new file mode 100644
index 000000000000..c4983285b6c6
--- /dev/null
+++ b/src/components/AttachmentPicker/launchCamera/launchCamera.ios.ts
@@ -0,0 +1,32 @@
+import {launchCamera as launchCameraImagePicker} from 'react-native-image-picker';
+import {PERMISSIONS, request, RESULTS} from 'react-native-permissions';
+import type {LaunchCamera} from './types';
+import {ErrorLaunchCamera} from './types';
+
+/**
+ * Launching the camera for iOS involves checking for permissions
+ * And only then starting the camera
+ * If the user deny permission the callback will be called with an error response
+ * in the same format as the error returned by react-native-image-picker
+ */
+const launchCamera: LaunchCamera = (options, callback) => {
+ // Checks current camera permissions and prompts the user in case they aren't granted
+ request(PERMISSIONS.IOS.CAMERA)
+ .then((permission) => {
+ if (permission !== RESULTS.GRANTED) {
+ throw new ErrorLaunchCamera('User did not grant permissions', 'permission');
+ }
+
+ launchCameraImagePicker(options, callback);
+ })
+ .catch((error: ErrorLaunchCamera) => {
+ /* Intercept the permission error as well as any other errors and call the callback
+ * follow the same pattern expected for image picker results */
+ callback({
+ errorMessage: error.message,
+ errorCode: error.errorCode || 'others',
+ });
+ });
+};
+
+export default launchCamera;
diff --git a/src/components/AttachmentPicker/launchCamera/launchCamera.ts b/src/components/AttachmentPicker/launchCamera/launchCamera.ts
new file mode 100644
index 000000000000..dc1f921086de
--- /dev/null
+++ b/src/components/AttachmentPicker/launchCamera/launchCamera.ts
@@ -0,0 +1,3 @@
+import {launchCamera} from 'react-native-image-picker';
+
+export default launchCamera;
diff --git a/src/components/AttachmentPicker/launchCamera/types.ts b/src/components/AttachmentPicker/launchCamera/types.ts
new file mode 100644
index 000000000000..fee9268c2f98
--- /dev/null
+++ b/src/components/AttachmentPicker/launchCamera/types.ts
@@ -0,0 +1,109 @@
+/**
+ * A callback function used to handle the response from the image picker.
+ *
+ * @param response - The response object containing information about the picked images or any errors encountered.
+ */
+type Callback = (response: ImagePickerResponse) => void;
+
+type OptionsCommon = {
+ /** Specifies the type of media to be captured. */
+ mediaType: MediaType;
+ /** Specifies the maximum width of the media to be captured. */
+ maxWidth?: number;
+ /** Specifies the maximum height of the media to be captured. */
+ maxHeight?: number;
+ /** Specifies the quality of the photo to be captured. */
+ quality?: PhotoQuality;
+ /** Specifies the video quality for video capture. */
+ videoQuality?: AndroidVideoOptions | IOSVideoOptions;
+ /** Specifies whether to include the media in base64 format. */
+ includeBase64?: boolean;
+ /** Specifies whether to include extra information about the captured media. */
+ includeExtra?: boolean;
+ /** Specifies the presentation style for the media picker. */
+ presentationStyle?: 'currentContext' | 'fullScreen' | 'pageSheet' | 'formSheet' | 'popover' | 'overFullScreen' | 'overCurrentContext';
+};
+
+type CameraOptions = OptionsCommon & {
+ /** Specifies the maximum duration limit. */
+ durationLimit?: number;
+ /** Specifies whether to save captured media. */
+ saveToPhotos?: boolean;
+ /** Specifies the type of camera to be used. */
+ cameraType?: CameraType;
+};
+
+type Asset = {
+ /** Base64 representation of the asset. */
+ base64?: string;
+ /** URI pointing to the asset. */
+ uri?: string;
+ /** Width of the asset. */
+ width?: number;
+ /** Height of the asset. */
+ height?: number;
+ /** Size of the asset file in bytes. */
+ fileSize?: number;
+ /** Type of the asset. */
+ type?: string;
+ /** Name of the asset file. */
+ fileName?: string;
+ /** Duration of the asset. */
+ duration?: number;
+ /** Bitrate of the asset. */
+ bitrate?: number;
+ /** Timestamp of when the asset was created or modified. */
+ timestamp?: string;
+ /** ID of the asset. */
+ id?: string;
+};
+
+type ImagePickerResponse = {
+ /** Indicates whether the image picker operation was canceled. */
+ didCancel?: boolean;
+ /** The error code, if an error occurred during the image picking process. */
+ errorCode?: ErrorCode;
+ /** A descriptive error message, if an error occurred during the image picking process. */
+ errorMessage?: string;
+ /** An array of assets representing the picked images. */
+ assets?: Asset[];
+};
+
+/** Represents the quality options. */
+type PhotoQuality = 0 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 1;
+
+/** Represents the type of camera to be used. */
+type CameraType = 'back' | 'front';
+
+/** Represents the type of media to be captured. */
+type MediaType = 'photo' | 'video' | 'mixed';
+
+/** Represents the quality options for video capture on Android devices. */
+type AndroidVideoOptions = 'low' | 'high';
+
+/** Represents the quality options for video capture on iOS devices. */
+type IOSVideoOptions = 'low' | 'medium' | 'high';
+
+/** Represents various error codes that may occur during camera operations. */
+type ErrorCode = 'camera_unavailable' | 'permission' | 'others';
+
+class ErrorLaunchCamera extends Error {
+ /** The error code associated with the error. */
+ errorCode: ErrorCode;
+
+ constructor(message: string, errorCode: ErrorCode) {
+ super(message);
+ this.errorCode = errorCode;
+ }
+}
+
+/**
+ * A function used to launch the camera with specified options and handle the callback.
+ *
+ * @param options - The options for the camera, specifying various settings.
+ * @param callback - The callback function to handle the response from the camera operation.
+ */
+type LaunchCamera = (options: CameraOptions, callback: Callback) => void;
+
+export {ErrorLaunchCamera};
+export type {LaunchCamera};
diff --git a/src/components/Icon/chunks/expensify-icons.chunk.ts b/src/components/Icon/chunks/expensify-icons.chunk.ts
index 441af58e797c..4b929e95dfec 100644
--- a/src/components/Icon/chunks/expensify-icons.chunk.ts
+++ b/src/components/Icon/chunks/expensify-icons.chunk.ts
@@ -37,7 +37,6 @@ import Building from '@assets/images/building.svg';
import Buildings from '@assets/images/buildings.svg';
import CalendarSolid from '@assets/images/calendar-solid.svg';
import Calendar from '@assets/images/calendar.svg';
-import CameraFlip from '@assets/images/camera-flip.svg';
import Camera from '@assets/images/camera.svg';
import CarCircleSlash from '@assets/images/car-circle-slash.svg';
import CarPlus from '@assets/images/car-plus.svg';
@@ -295,7 +294,6 @@ const Expensicons = {
Buildings,
Calendar,
Camera,
- CameraFlip,
Car,
CarPlus,
Cash,
diff --git a/src/hooks/useNativeCamera.ts b/src/hooks/useNativeCamera.ts
index 4ca1913b8c10..f09d5a01fde0 100644
--- a/src/hooks/useNativeCamera.ts
+++ b/src/hooks/useNativeCamera.ts
@@ -1,5 +1,4 @@
import {useFocusEffect} from '@react-navigation/core';
-import type React from 'react';
import {useCallback, useRef, useState} from 'react';
import {AppState} from 'react-native';
import {Gesture} from 'react-native-gesture-handler';
@@ -67,7 +66,41 @@ function useNativeCamera({context, onFocusStart, onFocusCleanup}: UseNativeCamer
});
}, [translate]);
- const {tapGesture, cameraFocusIndicatorAnimatedStyle} = useTapToFocusGesture(camera, device?.supportsFocus ?? false);
+ // Focus indicator animations
+ const focusIndicatorOpacity = useSharedValue(0);
+ const focusIndicatorScale = useSharedValue(2);
+ const focusIndicatorPosition = useSharedValue({x: 0, y: 0});
+
+ const cameraFocusIndicatorAnimatedStyle = useAnimatedStyle(() => ({
+ opacity: focusIndicatorOpacity.get(),
+ transform: [{translateX: focusIndicatorPosition.get().x}, {translateY: focusIndicatorPosition.get().y}, {scale: focusIndicatorScale.get()}],
+ }));
+
+ const focusCamera = useCallback((point: Point) => {
+ if (!camera.current) {
+ return;
+ }
+
+ camera.current.focus(point).catch((error: Record) => {
+ if (error.message === '[unknown/unknown] Cancelled by another startFocusAndMetering()') {
+ return;
+ }
+ Log.warn('Error focusing camera', error);
+ });
+ }, []);
+
+ const tapGesture = Gesture.Tap()
+ .enabled(device?.supportsFocus ?? false)
+ .onStart((ev: {x: number; y: number}) => {
+ const point = {x: ev.x, y: ev.y};
+
+ focusIndicatorOpacity.set(withSequence(withTiming(0.8, {duration: 250}), withDelay(1000, withTiming(0, {duration: 250}))));
+ focusIndicatorScale.set(2);
+ focusIndicatorScale.set(withSpring(1, {damping: 10, stiffness: 200}));
+ focusIndicatorPosition.set(point);
+
+ scheduleOnRN(focusCamera, point);
+ });
// Refresh camera permission on screen focus and app state changes
useFocusEffect(
@@ -127,53 +160,4 @@ function useNativeCamera({context, onFocusStart, onFocusCleanup}: UseNativeCamer
};
}
-/**
- * Encapsulates tap-to-focus gesture handling for VisionCamera.
- * This hook reads camera refs inside gesture worklet callbacks, which is incompatible
- * with React Compiler's "no ref access during render" rule. Keeping it here (an already-
- * grandfathered file) avoids forcing callers to fail the compliance check.
- */
-function useTapToFocusGesture(cameraRef: React.RefObject, supportsFocus: boolean) {
- const focusIndicatorOpacity = useSharedValue(0);
- const focusIndicatorScale = useSharedValue(2);
- const focusIndicatorPosition = useSharedValue({x: 0, y: 0});
-
- const cameraFocusIndicatorAnimatedStyle = useAnimatedStyle(() => ({
- opacity: focusIndicatorOpacity.get(),
- transform: [{translateX: focusIndicatorPosition.get().x}, {translateY: focusIndicatorPosition.get().y}, {scale: focusIndicatorScale.get()}],
- }));
-
- const focusCamera = useCallback(
- (point: Point) => {
- if (!cameraRef.current) {
- return;
- }
-
- cameraRef.current.focus(point).catch((error: Record) => {
- if (error.message === '[unknown/unknown] Cancelled by another startFocusAndMetering()') {
- return;
- }
- Log.warn('Error focusing camera', error);
- });
- },
- [cameraRef],
- );
-
- const tapGesture = Gesture.Tap()
- .enabled(supportsFocus)
- .onStart((ev: {x: number; y: number}) => {
- const point = {x: ev.x, y: ev.y};
-
- focusIndicatorOpacity.set(withSequence(withTiming(0.8, {duration: 250}), withDelay(1000, withTiming(0, {duration: 250}))));
- focusIndicatorScale.set(2);
- focusIndicatorScale.set(withSpring(1, {damping: 10, stiffness: 200}));
- focusIndicatorPosition.set(point);
-
- scheduleOnRN(focusCamera, point);
- });
-
- return {tapGesture, cameraFocusIndicatorAnimatedStyle};
-}
-
export default useNativeCamera;
-export {useTapToFocusGesture};
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 9a5aeefa6c1e..6086836ef955 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1138,7 +1138,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Lass es los',
dropMessage: 'Datei hierher ziehen',
flash: 'Blitz',
- flipCamera: 'Kamera wechseln',
multiScan: 'Mehrfachscan',
shutter: 'Verschluss',
gallery: 'Galerie',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 396aab2150ef..d0ea18c22157 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1193,7 +1193,6 @@ const translations = {
dropTitle: 'Let it go',
dropMessage: 'Drop your file here',
flash: 'flash',
- flipCamera: 'Flip camera',
multiScan: 'multi-scan',
shutter: 'shutter',
gallery: 'gallery',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 8d08b1e6958a..df13b4beed60 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1101,7 +1101,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Suéltalo',
dropMessage: 'Suelta tu archivo aquí',
flash: 'flash',
- flipCamera: 'Cambiar cámara',
multiScan: 'escaneo múltiple',
shutter: 'obturador',
gallery: 'galería',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 7fcf64db0a6f..b98594c39617 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1142,7 +1142,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Laisse tomber',
dropMessage: 'Déposez votre fichier ici',
flash: 'flash',
- flipCamera: 'Retourner la caméra',
multiScan: 'numérisation multiple',
shutter: 'obturateur',
gallery: 'galerie',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 90986f69e7a9..0b66570b8b53 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -1138,7 +1138,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Lascia perdere',
dropMessage: 'Rilascia qui il tuo file',
flash: 'flash',
- flipCamera: 'Cambia fotocamera',
multiScan: 'scansione multipla',
shutter: 'otturatore',
gallery: 'galleria',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index a7eeb6a8e00e..1683c08da18c 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -1121,7 +1121,6 @@ const translations: TranslationDeepObject = {
dropTitle: '手放して',
dropMessage: 'ここにファイルをドロップしてください',
flash: 'フラッシュ',
- flipCamera: 'カメラ切替',
multiScan: 'マルチスキャン',
shutter: 'シャッター',
gallery: 'ギャラリー',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index f7d9cf07de65..c6282c9c1c67 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1136,7 +1136,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Laat het los',
dropMessage: 'Zet je bestand hier neer',
flash: 'flits',
- flipCamera: 'Camera wisselen',
multiScan: 'meerscannen',
shutter: 'sluiter',
gallery: 'galerij',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 438182be01ce..5851baebfa38 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1136,7 +1136,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Odpuść to',
dropMessage: 'Upuść tutaj plik',
flash: 'błysk',
- flipCamera: 'Przełącz kamerę',
multiScan: 'wielokrotne skanowanie',
shutter: 'migawka',
gallery: 'galeria',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index b5016fec1c10..7363495a4bf1 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -1136,7 +1136,6 @@ const translations: TranslationDeepObject = {
dropTitle: 'Deixe pra lá',
dropMessage: 'Solte seu arquivo aqui',
flash: 'flash',
- flipCamera: 'Alternar câmera',
multiScan: 'escaneamento múltiplo',
shutter: 'obturador',
gallery: 'galeria',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 340df704c40a..bbdbba71b06d 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1100,7 +1100,6 @@ const translations: TranslationDeepObject = {
dropTitle: '随它去',
dropMessage: '将文件拖放到此处',
flash: '闪光',
- flipCamera: '切换相机',
multiScan: '多重扫描',
shutter: '快门',
gallery: '图库',