diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 67f6ad2c0904..12056f46bcd2 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9070,6 +9070,13 @@ const CONST = { OPTION_CARD_PICKER: { OPTION_ITEM: 'OptionCardPicker-OptionItem', }, + ATTACHMENT_CAMERA: { + CLOSE: 'AttachmentCamera-Close', + FLASH: 'AttachmentCamera-Flash', + SHUTTER: 'AttachmentCamera-Shutter', + FLIP_CAMERA: 'AttachmentCamera-FlipCamera', + PERMISSION_PROMPT_BUTTON: 'AttachmentCamera-PermissionPromptButton', + }, ATTACHMENT_CAROUSEL: { PREVIOUS_BUTTON: 'AttachmentCarousel-PreviousButton', NEXT_BUTTON: 'AttachmentCarousel-NextButton', diff --git a/src/components/AttachmentPicker/AttachmentCamera.tsx b/src/components/AttachmentPicker/AttachmentCamera.tsx index b73512da5177..757baa712c2a 100644 --- a/src/components/AttachmentPicker/AttachmentCamera.tsx +++ b/src/components/AttachmentPicker/AttachmentCamera.tsx @@ -7,6 +7,7 @@ import Icon from '@components/Icon'; import ImageSVG from '@components/ImageSVG'; import Modal from '@components/Modal'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useIsPlatformMuted from '@hooks/useIsPlatformMuted'; @@ -91,7 +92,12 @@ function AttachmentCamera({isVisible, onCapture, onClose, onModalHide}: Attachme const format = useCameraFormat(device, [ {photoAspectRatio: CONST.RECEIPT_CAMERA.PHOTO_ASPECT_RATIO}, - {photoResolution: {width: CONST.RECEIPT_CAMERA.PHOTO_WIDTH, height: CONST.RECEIPT_CAMERA.PHOTO_HEIGHT}}, + { + photoResolution: { + width: CONST.RECEIPT_CAMERA.PHOTO_WIDTH, + height: CONST.RECEIPT_CAMERA.PHOTO_HEIGHT, + }, + }, getVideoResolutionFormatFilter(windowWidth, windowHeight), ]); const hasFlash = !!device?.hasFlash; @@ -182,6 +188,9 @@ function AttachmentCamera({isVisible, onCapture, onClose, onModalHide}: Attachme ]); }) .catch((error: Error) => { + if (!isActiveRef.current) { + return; + } Alert.alert(translate('receipt.cameraErrorTitle'), translate('receipt.cameraErrorMessage')); logCameraCaptureFailed(error); }) @@ -191,11 +200,15 @@ function AttachmentCamera({isVisible, onCapture, onClose, onModalHide}: Attachme }; const handleCameraError = (error: CameraRuntimeError) => { + if (!isActiveRef.current) { + return; + } Alert.alert(translate('receipt.cameraErrorTitle'), translate('receipt.cameraErrorMessage')); logCameraRuntimeError({code: error.code, message: error.message}); }; const handleClose = () => { + isActiveRef.current = false; isCapturing.current = false; setFlash(false); setCameraPosition('back'); @@ -217,7 +230,7 @@ function AttachmentCamera({isVisible, onCapture, onClose, onModalHide}: Attachme role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.close')} onPress={handleClose} - sentryLabel="AttachmentCamera-Close" + sentryLabel={CONST.SENTRY_LABEL.ATTACHMENT_CAMERA.CLOSE} > - - {cameraPermissionStatus !== RESULTS.GRANTED && ( - + + + {cameraPermissionStatus !== RESULTS.GRANTED && ( + + + + {translate('receipt.takePhoto')} + {translate('receipt.cameraAccess')} + + + + )} + {cameraPermissionStatus === RESULTS.GRANTED && device == null && ( + + + + )} + {cameraPermissionStatus === RESULTS.GRANTED && device != null && ( + + + + + + + + + )} + + + + setFlash((prevFlash) => !prevFlash)} + sentryLabel={CONST.SENTRY_LABEL.ATTACHMENT_CAMERA.FLASH} + > + + + + - {translate('receipt.takePhoto')} - {translate('receipt.cameraAccess')} - - - )} - {cameraPermissionStatus === RESULTS.GRANTED && device == null && ( - - + + setCameraPosition((prev) => (prev === 'back' ? 'front' : 'back'))} + sentryLabel={CONST.SENTRY_LABEL.ATTACHMENT_CAMERA.FLIP_CAMERA} + > + - - )} - {cameraPermissionStatus === RESULTS.GRANTED && device != null && ( - - - - - - - - - )} - - - - setFlash((prevFlash) => !prevFlash)} - sentryLabel="AttachmentCamera-Flash" - > - - - - - - - - setCameraPosition((prev) => (prev === 'back' ? 'front' : 'back'))} - sentryLabel="AttachmentCamera-FlipCamera" - > - - + + diff --git a/tests/ui/components/AttachmentCameraTest.tsx b/tests/ui/components/AttachmentCameraTest.tsx index fb6dcb5937e8..a952e47d01cd 100644 --- a/tests/ui/components/AttachmentCameraTest.tsx +++ b/tests/ui/components/AttachmentCameraTest.tsx @@ -4,6 +4,8 @@ import AttachmentCamera from '@components/AttachmentPicker/AttachmentCamera'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import isInLandscapeMode from '@libs/isInLandscapeMode'; + import type {CameraDevice} from 'react-native-vision-camera'; import React from 'react'; @@ -17,6 +19,9 @@ import waitForBatchedUpdatesWithAct from '../../utils/waitForBatchedUpdatesWithA const mockTakePhoto = jest.fn(() => Promise.resolve({path: '/tmp/photos/shot.jpg', width: 3024, height: 4032})); let mockPermissionStatus = 'granted'; +jest.mock('@libs/isInLandscapeMode'); +jest.mock('@expensify/react-native-hybrid-app', () => ({__esModule: true, default: {isHybridApp: jest.fn(() => false)}})); + jest.mock('@pages/iou/request/step/IOURequestStepScan/CameraPermission', () => ({ getCameraPermissionStatus: jest.fn(() => Promise.resolve(mockPermissionStatus)), requestCameraPermission: jest.fn(() => Promise.resolve(mockPermissionStatus)), @@ -50,6 +55,7 @@ const FRONT_DEVICE = createMock({id: 'front', position: 'front', h const mockedUseCameraDevice = jest.mocked(useCameraDevice); const mockedUseCameraDevices = jest.mocked(useCameraDevices); +const mockedIsInLandscapeMode = jest.mocked(isInLandscapeMode); function renderCamera(props: Partial> = {}) { const onCapture = jest.fn(); @@ -84,6 +90,7 @@ describe('AttachmentCamera', () => { mockTakePhoto.mockResolvedValue({path: '/tmp/photos/shot.jpg', width: 3024, height: 4032}); mockedUseCameraDevice.mockReturnValue(BACK_DEVICE); mockedUseCameraDevices.mockReturnValue([BACK_DEVICE, FRONT_DEVICE]); + mockedIsInLandscapeMode.mockReturnValue(false); await act(async () => { await Onyx.clear(); }); @@ -181,4 +188,15 @@ describe('AttachmentCamera', () => { expect(mockTakePhoto).not.toHaveBeenCalled(); expect(onCapture).not.toHaveBeenCalled(); }); + + it('adapts layout for landscape orientation', async () => { + mockedIsInLandscapeMode.mockReturnValue(true); + renderCamera(); + await waitForBatchedUpdatesWithAct(); + + const shutter = screen.getByLabelText(translateLocal('receipt.shutter')); + expect(shutter).toBeOnTheScreen(); + const controlsContainer = shutter.parent; + expect(controlsContainer?.props.style).toEqual(expect.not.arrayContaining([expect.objectContaining({flexDirection: 'row'})])); + }); });