From a5e97d0ceba366fce0b22a7ce014a84bd028f3dc Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Wed, 27 Nov 2024 20:46:46 +1300 Subject: [PATCH 1/6] Fix camera not working on the second time opening it --- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index bc8622072226..5cea87eb4475 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -167,7 +167,7 @@ function IOURequestStepScan({ setCameraPermissionState('denied'); }); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); + }, [videoConstraints]); useEffect(() => { if (!Browser.isMobile() || !isTabActive) { From d9922bfec235bf8ed1a399007780fb50f3c8adaa Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Wed, 27 Nov 2024 21:01:13 +1300 Subject: [PATCH 2/6] Remove unnecessary eslint disabling comment --- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 5cea87eb4475..c2c81c19b992 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -166,7 +166,6 @@ function IOURequestStepScan({ setVideoConstraints(defaultConstraints); setCameraPermissionState('denied'); }); - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [videoConstraints]); useEffect(() => { From 4cdb3954346cb10394cf11049849c88c09e580b2 Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:29:14 +1300 Subject: [PATCH 3/6] Fix random camera init failures on some iOS versions --- .../NavigationAwareCamera/WebCamera.tsx | 9 ++++-- .../NavigationAwareCamera/types.ts | 10 ++----- .../request/step/IOURequestStepScan/index.tsx | 30 ++++++++++++++++--- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx index db8c1656b3f8..f79997231a32 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx @@ -4,17 +4,22 @@ import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import type {Camera} from 'react-native-vision-camera'; import Webcam from 'react-webcam'; +import useThemeStyles from '@hooks/useThemeStyles'; 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. -function WebCamera({torchOn, onTorchAvailability, cameraTabIndex, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) { +function WebCamera({aspectRatio, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) { const shouldShowCamera = useIsFocused(); if (!shouldShowCamera) { return null; } + + const styles = useThemeStyles(); + const webcamContainerStyles = {...styles.justifyContentCenter, ...styles.w100, aspectRatio}; + return ( - + void; - - /** Callback function when media stream becomes available - user granted camera permissions and camera starts to work */ - cameraTabIndex: number; + /** Sets the CSS aspect ratio of the webcam container. */ + aspectRatio?: string; }; type NavigationAwareCameraNativeProps = Omit & { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index c2c81c19b992..93e1a881e4dc 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -27,6 +27,7 @@ import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; @@ -91,8 +92,11 @@ function IOURequestStepScan({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`); const [isLoadingReceipt, setIsLoadingReceipt] = useState(false); + const [cameraViewAspectRatio, setCameraViewAspectRatio] = useState(); + const [previousWindowHeight, setPreviousWindowHeight] = useState(0); + const [previousWindowWidth, setPreviousWindowWidth] = useState(0); + const {windowHeight, windowWidth} = useWindowDimensions(); const [videoConstraints, setVideoConstraints] = useState(); - const tabIndex = 1; const isTabActive = useIsFocused(); const isEditing = action === CONST.IOU.ACTION.EDIT; @@ -117,7 +121,7 @@ function IOURequestStepScan({ * The last deviceId is of regular len camera. */ const requestCameraPermission = useCallback(() => { - if (!isEmptyObject(videoConstraints) || !Browser.isMobile()) { + if (!Browser.isMobile()) { return; } @@ -134,6 +138,12 @@ function IOURequestStepScan({ const setting = track.getSettings(); if (setting.zoom === 1) { deviceId = setting.deviceId; + // Set a fixed aspect ratio for the camera view to prevent resizing during initialization, which can cause failures on some iOS versions. + if (setting.height && setting.width) { + setCameraViewAspectRatio(`${setting.height}/${setting.width}`); + setPreviousWindowHeight(windowHeight); + setPreviousWindowWidth(windowWidth); + } break; } } @@ -166,10 +176,11 @@ function IOURequestStepScan({ setVideoConstraints(defaultConstraints); setCameraPermissionState('denied'); }); - }, [videoConstraints]); + }, [windowHeight, windowWidth]); useEffect(() => { if (!Browser.isMobile() || !isTabActive) { + setVideoConstraints(undefined); return; } navigator.permissions @@ -192,6 +203,17 @@ function IOURequestStepScan({ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isTabActive]); + /** + * Invalidate camera view aspect ratio on orientation change. + */ + useEffect(() => { + if (!Browser.isMobile() || previousWindowHeight === windowHeight || previousWindowWidth === windowWidth) { + return; + } + + setCameraViewAspectRatio(undefined); + }, [previousWindowHeight, previousWindowWidth, windowHeight, windowWidth]); + const hideRecieptModal = () => { setIsAttachmentInvalid(false); }; @@ -654,9 +676,9 @@ function IOURequestStepScan({ style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" + aspectRatio={cameraViewAspectRatio} videoConstraints={videoConstraints} forceScreenshotSourceSize - cameraTabIndex={tabIndex} audio={false} disablePictureInPicture={false} imageSmoothing={false} From 1196254dd361d477e93b2dd1857335bf4d8e8d59 Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:40:20 +1300 Subject: [PATCH 4/6] Fix lint errors --- .../step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx index f79997231a32..92608816e5ba 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx @@ -10,12 +10,12 @@ 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. function WebCamera({aspectRatio, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) { const shouldShowCamera = useIsFocused(); + const styles = useThemeStyles(); if (!shouldShowCamera) { return null; } - const styles = useThemeStyles(); const webcamContainerStyles = {...styles.justifyContentCenter, ...styles.w100, aspectRatio}; return ( From 5023cfe04907b5d64f096be08aeb21d9a86d8792 Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Sat, 30 Nov 2024 14:45:56 +1300 Subject: [PATCH 5/6] Hide webcam during init to prevent random failures on iOS --- .../NavigationAwareCamera/WebCamera.tsx | 11 ++++---- .../NavigationAwareCamera/types.ts | 5 +--- .../request/step/IOURequestStepScan/index.tsx | 25 +------------------ 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx index 92608816e5ba..fd20bb7aae5d 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/WebCamera.tsx @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import React from 'react'; +import React, {useState} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import type {Camera} from 'react-native-vision-camera'; @@ -8,7 +8,8 @@ import useThemeStyles from '@hooks/useThemeStyles'; 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. -function WebCamera({aspectRatio, ...props}: NavigationAwareCameraProps, ref: ForwardedRef) { +function WebCamera(props: NavigationAwareCameraProps, ref: ForwardedRef) { + const [isInitialized, setIsInitialized] = useState(false); const shouldShowCamera = useIsFocused(); const styles = useThemeStyles(); @@ -16,13 +17,13 @@ function WebCamera({aspectRatio, ...props}: NavigationAwareCameraProps, ref: For return null; } - const webcamContainerStyles = {...styles.justifyContentCenter, ...styles.w100, aspectRatio}; - return ( - + // Hide the camera during initialization to prevent random failures on some iOS versions. + setIsInitialized(true)} ref={ref as unknown as ForwardedRef} /> diff --git a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts index 11e3a0ec0c65..0869ecf34199 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts +++ b/src/pages/iou/request/step/IOURequestStepScan/NavigationAwareCamera/types.ts @@ -1,10 +1,7 @@ import type {CameraProps} from 'react-native-vision-camera'; import type {WebcamProps} from 'react-webcam'; -type NavigationAwareCameraProps = WebcamProps & { - /** Sets the CSS aspect ratio of the webcam container. */ - aspectRatio?: string; -}; +type NavigationAwareCameraProps = WebcamProps; type NavigationAwareCameraNativeProps = Omit & { cameraTabIndex: number; diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index ea7ca278ead2..23934805db05 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -28,7 +28,6 @@ import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import getCurrentPosition from '@libs/getCurrentPosition'; @@ -93,10 +92,6 @@ function IOURequestStepScan({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`); const [isLoadingReceipt, setIsLoadingReceipt] = useState(false); - const [cameraViewAspectRatio, setCameraViewAspectRatio] = useState(); - const [previousWindowHeight, setPreviousWindowHeight] = useState(0); - const [previousWindowWidth, setPreviousWindowWidth] = useState(0); - const {windowHeight, windowWidth} = useWindowDimensions(); const [videoConstraints, setVideoConstraints] = useState(); const isTabActive = useIsFocused(); @@ -139,12 +134,6 @@ function IOURequestStepScan({ const setting = track.getSettings(); if (setting.zoom === 1) { deviceId = setting.deviceId; - // Set a fixed aspect ratio for the camera view to prevent resizing during initialization, which can cause failures on some iOS versions. - if (setting.height && setting.width) { - setCameraViewAspectRatio(`${setting.height}/${setting.width}`); - setPreviousWindowHeight(windowHeight); - setPreviousWindowWidth(windowWidth); - } break; } } @@ -177,7 +166,7 @@ function IOURequestStepScan({ setVideoConstraints(defaultConstraints); setCameraPermissionState('denied'); }); - }, [windowHeight, windowWidth]); + }, []); useEffect(() => { if (!Browser.isMobile() || !isTabActive) { @@ -204,17 +193,6 @@ function IOURequestStepScan({ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isTabActive]); - /** - * Invalidate camera view aspect ratio on orientation change. - */ - useEffect(() => { - if (!Browser.isMobile() || previousWindowHeight === windowHeight || previousWindowWidth === windowWidth) { - return; - } - - setCameraViewAspectRatio(undefined); - }, [previousWindowHeight, previousWindowWidth, windowHeight, windowWidth]); - const hideRecieptModal = () => { setIsAttachmentInvalid(false); }; @@ -684,7 +662,6 @@ function IOURequestStepScan({ style={{...styles.videoContainer, display: cameraPermissionState !== 'granted' ? 'none' : 'block'}} ref={cameraRef} screenshotFormat="image/png" - aspectRatio={cameraViewAspectRatio} videoConstraints={videoConstraints} forceScreenshotSourceSize audio={false} From 9e68794e15bf69703053b8adb621d8a1ef7a74cf Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Sat, 30 Nov 2024 16:21:12 +1300 Subject: [PATCH 6/6] Unify camera selection behavior across browsers on iOS --- src/pages/iou/request/step/IOURequestStepScan/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index 23934805db05..8708d1d9ce54 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -128,7 +128,7 @@ function IOURequestStepScan({ setCameraPermissionState('granted'); stream.getTracks().forEach((track) => track.stop()); // Only Safari 17+ supports zoom constraint - if (Browser.isMobileSafari() && stream.getTracks().length > 0) { + if (Browser.isMobileWebKit() && stream.getTracks().length > 0) { let deviceId; for (const track of stream.getTracks()) { const setting = track.getSettings();