Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import {useFocusEffect} from '@react-navigation/core';
import React, {useRef, useState} from 'react';
import {Alert, AppState, StyleSheet, View} from 'react-native';
import React, {useRef} from 'react';
import {Alert, StyleSheet, View} from 'react-native';
import type {LayoutRectangle} from 'react-native';
import ReactNativeBlobUtil from 'react-native-blob-util';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import {GestureDetector} from 'react-native-gesture-handler';
import {RESULTS} from 'react-native-permissions';
import Animated, {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 Animated, {useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
import type {PhotoFile} from 'react-native-vision-camera';
import ActivityIndicator from '@components/ActivityIndicator';
import AttachmentPicker from '@components/AttachmentPicker';
import Button from '@components/Button';
import {useFullScreenLoaderActions, useFullScreenLoaderState} from '@components/FullScreenLoaderContext';
import Icon from '@components/Icon';
import ImageSVG from '@components/ImageSVG';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
Expand All @@ -20,49 +17,29 @@
import useFilesValidation from '@hooks/useFilesValidation';
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {showCameraPermissionsAlert} from '@libs/fileDownload/FileUtils';
import getPhotoSource from '@libs/fileDownload/getPhotoSource';
import getPlatform from '@libs/getPlatform';
import type Platform from '@libs/getPlatform/types';
import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath';
import {shouldUseTransactionDraft} from '@libs/IOUUtils';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import CameraPermission from '@pages/iou/request/step/IOURequestStepScan/CameraPermission';
import NavigationAwareCamera from '@pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/Camera';
import {cropImageToAspectRatio} from '@pages/iou/request/step/IOURequestStepScan/cropImageToAspectRatio';
import type {ImageObject} from '@pages/iou/request/step/IOURequestStepScan/cropImageToAspectRatio';
import useNativeCamera from '@pages/iou/request/step/IOURequestStepScan/hooks/useNativeCamera';
import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper';
import withFullTransactionOrNotFound from '@pages/iou/request/step/withFullTransactionOrNotFound';
import type {WithFullTransactionOrNotFoundProps} from '@pages/iou/request/step/withFullTransactionOrNotFound';
import variables from '@styles/variables';
import {setMoneyRequestOdometerImage} from '@userActions/IOU';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type {FileObject} from '@src/types/utils/Attachment';
import {getEmptyObject} from '@src/types/utils/EmptyObject';

type IOURequestStepOdometerImageProps = WithFullTransactionOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.ODOMETER_IMAGE>;

function focusCamera(cameraRef: React.RefObject<Camera | null>, point: Point) {
if (!cameraRef.current) {
return;
}

cameraRef.current.focus(point).catch((error: Record<string, unknown>) => {
if (error.message === '[unknown/unknown] Cancelled by another startFocusAndMetering()') {
return;
}
Log.warn('Error focusing camera', error);
});
}

function IOURequestStepOdometerImage({
route: {
params: {action, iouType, transactionID, reportID, backToReport, imageType, isEditingConfirmation},
Expand All @@ -74,25 +51,28 @@
const lazyIcons = useMemoizedLazyExpensifyIcons(['Bolt', 'Gallery', 'boltSlash', 'OdometerStart', 'OdometerEnd']);
const lazyIllustrationsOnly = useMemoizedLazyIllustrations(['Hand', 'Shutter']);

const {isLoaderVisible} = useFullScreenLoaderState();
const {setIsLoaderVisible} = useFullScreenLoaderActions();
const [isAttachmentPickerActive, setIsAttachmentPickerActive] = useState(false);

const device = useCameraDevice('back', {
physicalDevices: ['wide-angle-camera', 'ultra-wide-angle-camera'],
});
const platform = getPlatform(true);
const [mutedPlatforms = getEmptyObject<Partial<Record<Platform, true>>>()] = useOnyx(ONYXKEYS.NVP_MUTED_PLATFORMS);
const isPlatformMuted = mutedPlatforms[platform];

const [cameraPermissionStatus, setCameraPermissionStatus] = useState<string | null>(null);
const hasFlash = !!device?.hasFlash;
const [flash, setFlash] = useState(false);
const [didCapturePhoto, setDidCapturePhoto] = useState(false);
const camera = useRef<Camera>(null);
const viewfinderLayout = useRef<LayoutRectangle>(null);
const isTransactionDraft = shouldUseTransactionDraft(action ?? CONST.IOU.ACTION.CREATE, iouType ?? CONST.IOU.TYPE.REQUEST);

const {
camera,
device,
cameraPermissionStatus,
flash,
setFlash,
hasFlash,
didCapturePhoto,
setDidCapturePhoto,
isAttachmentPickerActive,
setIsAttachmentPickerActive,
isPlatformMuted,
askForPermissions,
tapGesture,
cameraFocusIndicatorAnimatedStyle,
cameraLoadingReasonAttributes,
setIsLoaderVisible,
} = useNativeCamera({context: 'IOURequestStepOdometerImage'});

const title = imageType === 'start' ? translate('distance.odometer.startTitle') : translate('distance.odometer.endTitle');
const snapPhotoText = imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START ? translate('distance.odometer.snapPhotoStart') : translate('distance.odometer.snapPhotoEnd');
const icon = imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START ? lazyIcons.OdometerStart : lazyIcons.OdometerEnd;
Expand All @@ -106,77 +86,11 @@
Navigation.goBack(goBackRoute);
};

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: string) => {
setCameraPermissionStatus(status);

if (status === RESULTS.BLOCKED) {
showCameraPermissionsAlert(translate);
}
})
.catch(() => {
setCameraPermissionStatus(RESULTS.UNAVAILABLE);
});
};

const blinkOpacity = useSharedValue(0);
const blinkStyle = useAnimatedStyle(() => ({
opacity: blinkOpacity.get(),
}));

const focusIndicatorOpacity = useSharedValue(0);
const focusIndicatorScale = useSharedValue(1);
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 tapGesture = Gesture.Tap()
.enabled(device?.supportsFocus ?? false)
.runOnJS(true)
.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);

focusCamera(camera, point);
});

useFocusEffect(() => {
setDidCapturePhoto(false);
const refreshCameraPermissionStatus = () => {
CameraPermission?.getCameraPermissionStatus?.()
.then(setCameraPermissionStatus)
.catch(() => setCameraPermissionStatus(RESULTS.UNAVAILABLE));
};

refreshCameraPermissionStatus();

// Refresh permission status when app gains focus
const subscription = AppState.addEventListener('change', (appState) => {
if (appState !== 'active') {
return;
}

refreshCameraPermissionStatus();
});

return () => {
subscription.remove();

if (isLoaderVisible) {
setIsLoaderVisible(false);
}
};
});

const handleImageSelected = (files: FileObject[]) => {
if (files.length === 0) {
return;
Expand Down Expand Up @@ -268,12 +182,6 @@
});
};

const cameraLoadingReasonAttributes: SkeletonSpanReasonAttributes = {
context: 'IOURequestStepOdometerImage',
cameraPermissionGranted: cameraPermissionStatus === RESULTS.GRANTED,
deviceAvailable: device != null,
};

// Wait for camera permission status to render
if (cameraPermissionStatus == null) {
return null;
Expand Down Expand Up @@ -363,7 +271,7 @@
</View>
<Animated.View
pointerEvents="none"
style={[StyleSheet.absoluteFillObject, styles.backgroundWhite, blinkStyle, styles.zIndex10]}

Check failure on line 274 in src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

`absoluteFillObject` is deprecated. Use `StyleSheet.absoluteFill`

Check failure on line 274 in src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

`absoluteFillObject` is deprecated. Use `StyleSheet.absoluteFill`
/>
<Animated.View style={[styles.cameraFocusIndicator, cameraFocusIndicatorAnimatedStyle]} />
</View>
Expand Down Expand Up @@ -431,7 +339,7 @@

IOURequestStepOdometerImage.displayName = 'IOURequestStepOdometerImage';

// eslint-disable-next-line rulesdir/no-negated-variables
// eslint-disable-next-line rulesdir/no-negated-variables -- withFullTransactionOrNotFound HOC requires this pattern
const IOURequestStepOdometerImageWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepOdometerImage);

export default IOURequestStepOdometerImageWithFullTransactionOrNotFound;
Loading
Loading