-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
I have noticed that when i take a picture in a bright light most times, my screen turns white requesting me to restart the app. Here is my hook
`import {
CameraRecordingOptions,
CameraType,
CameraView,
useCameraPermissions,
} from "expo-camera";
import * as MediaLibrary from "expo-media-library";
import { useCallback, useEffect, useRef, useState } from "react";
export function useCamera() {
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
const [mediaPermission, requestMediaPermission] =
MediaLibrary.usePermissions();
const cameraRef = useRef<CameraView | null>(null);
const [isReady, setIsReady] = useState(false);
const [type, setType] = useState<CameraType>("front");
const [mode, setMode] = useState<"photo" | "video">("photo");
const [isRecording, setIsRecording] = useState(false);
// ✅ Now managed inside the hook
const [photoUri, setPhotoUri] = useState<string | null>(null);
const [videoUri, setVideoUri] = useState<string | null>(null);
// Request permissions at startup
useEffect(() => {
if (!cameraPermission?.granted) {
requestCameraPermission();
}
if (!mediaPermission?.granted) {
requestMediaPermission();
}
}, [
cameraPermission,
mediaPermission,
requestCameraPermission,
requestMediaPermission,
]);
const toggleCameraType = useCallback(() => {
setType((prev) => (prev === "back" ? "front" : "back"));
}, []);
const toggleMode = useCallback(() => {
setMode((prev) => (prev === "photo" ? "video" : "photo"));
}, []);
const takePicture = useCallback(
async (options?: { base64?: boolean }) => {
if (!cameraRef.current || !isReady || mode !== "photo") {
return null;
}
try {
const photo = await cameraRef.current.takePictureAsync({
quality: 0.5,
base64: options?.base64 ?? false,
skipProcessing: true,
});
setPhotoUri(photo.uri); // ✅ store inside hook
return photo;
} catch (err) {
console.error("Error taking picture:", err);
return null;
}
},
[isReady, mode]
);
const startRecording = useCallback(async () => {
if (!cameraRef.current || !isReady || mode !== "video" || isRecording) {
return null;
}
try {
setIsRecording(true);
const opts: CameraRecordingOptions = {
maxDuration: 60, // seconds
};
const video = await cameraRef.current.recordAsync(opts);
if (video && video.uri) {
setVideoUri(video.uri); // ✅ store inside hook
}
setIsRecording(false);
return video;
} catch (err) {
console.error("Error recording video:", err);
setIsRecording(false);
return null;
}
}, [isReady, mode, isRecording]);
const stopRecording = useCallback(() => {
if (cameraRef.current && isRecording) {
cameraRef.current.stopRecording();
setIsRecording(false);
}
}, [isRecording]);
// Reset captured media
const resetMedia = useCallback(() => {
setPhotoUri(null);
setVideoUri(null);
}, []);
return {
cameraRef,
hasPermission: cameraPermission?.granted,
requestCameraPermission,
hasMediaPermission: mediaPermission?.granted,
requestMediaPermission,
isReady,
setIsReady,
type,
toggleCameraType,
mode,
toggleMode,
takePicture,
startRecording,
stopRecording,
isRecording,
photoUri, // ✅ exposed state
videoUri, // ✅ exposed state
resetMedia,
};
}
`
Here is my implementation
`import CancelBlueCircle from "@/assets/svg/cancel-blue-circle.svg";
import CheckBlue3 from "@/assets/svg/check-blue-3.svg";
import { images } from "@/constants/images";
import { useLocationContext } from "@/context/LocationContext";
import { useNetwork } from "@/context/NetworkContext";
import { useStaffFaceClockin } from "@/hooks/queries/clockin";
import { useCamera } from "@/hooks/useCamera";
import { cn } from "@/utils/clsx";
import { getDeviceId } from "@/utils/getDeviceID";
import { getDeviceInfo } from "@/utils/getDeviceInfo";
import { CameraView } from "expo-camera";
import { LinearGradient } from "expo-linear-gradient";
import React, { useCallback } from "react";
import {
ActivityIndicator,
Alert,
Dimensions,
Image,
Platform,
StatusBar,
Text,
TouchableOpacity,
View,
} from "react-native";
import Modal from "react-native-modal";
import CustomButton2 from "../CustomButton2";
const { width, height } = Dimensions.get("window");
const FaceIDClockinModal = ({
isVisible,
setIsVisible,
}: {
isVisible: boolean;
setIsVisible: React.Dispatch<React.SetStateAction>;
}) => {
const { locationReady, location } = useLocationContext();
const { ip } = useNetwork();
const {
cameraRef,
hasPermission,
requestCameraPermission,
setIsReady,
type,
takePicture,
photoUri,
resetMedia,
} = useCamera();
const { clockinWithFace, clockingInWithFace } = useStaffFaceClockin({
onSuccess: (values) => {
Alert.alert("Success", values.message);
onCancelPress();
},
onError: (message) => {
Alert.alert("Attention", message);
},
});
const onCancelPress = () => {
resetMedia();
setIsVisible(false);
};
const handleClockin = useCallback(async () => {
if (!locationReady) {
Alert.alert(
"Location Unavailable",
"Your GPS may be turned off. Please check your settings and try again.",
[{ text: "OK" }]
);
return;
}
if (!photoUri) {
//no image available
Alert.alert("Attention", "No captured image found");
return false;
}
if (!(await getDeviceId())) {
//no device id found
Alert.alert("Attention", "Device ID not found");
return false;
}
if (!getDeviceInfo().deviceName) {
//no device name found
Alert.alert("Attention", "Device name not found");
return false;
}
//instantiate formData
const formData = new FormData();
//add all images to formData
const fileName = photoUri.split("/").pop();
const ext = fileName?.split(".").pop()?.toLowerCase();
const mimeMap: Record<string, string> = {
jpg: "image/jpeg",
jpeg: "image/jpeg",
png: "image/png",
heic: "image/heic",
gif: "image/gif",
webp: "image/webp",
};
const fileType = mimeMap[ext || ""] || "image/jpeg"; // default to jpeg
formData.append("image", {
uri: photoUri,
type: fileType,
name: fileName,
} as any); // TypeScript needs this cast
//add device ID to formData
formData.append(
"device_id",
(await getDeviceId()) as unknown as string
);
//add device name to formData
formData.append(
"device_name",
getDeviceInfo().deviceName as unknown as string
);
//add ip address to formData
formData.append("ip_address", ip as unknown as string);
//add longitude to formData
formData.append(
"long_x",
location?.coords.longitude as unknown as string
);
//add latitude to formData
formData.append(
"lat_y",
location?.coords.latitude as unknown as string
);
//add clockin mode to formData
formData.append("clock_mode", "Facial Recognition");
//clockin/clockout
clockinWithFace({ payload: formData });
}, [locationReady, photoUri, location, ip, clockinWithFace]);
const onActionBtnPress = useCallback(() => {
if (!hasPermission) {
requestCameraPermission();
} else if (!photoUri) {
takePicture({ base64: false });
} else {
handleClockin();
}
}, [
handleClockin,
hasPermission,
photoUri,
requestCameraPermission,
takePicture,
]);
return (
<View
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
>
{/* Modal */}
<Modal
isVisible={isVisible}
style={{ margin: 0 }}
onBackdropPress={() => setIsVisible(false)}
backdropOpacity={0.9}
backdropColor="#000"
useNativeDriver
statusBarTranslucent
avoidKeyboard={true}
propagateSwipe={true}
deviceHeight={
height +
(Platform.OS === "android"
? StatusBar.currentHeight ?? 0
: 0)
}
>
<View style={{ flex: 1, backgroundColor: "#0000001F" }}>
<View
style={{
width,
height,
paddingHorizontal: 24,
paddingVertical: 44,
paddingTop: 50,
justifyContent: "space-between",
}}
>
<TouchableOpacity
onPress={onCancelPress}
className="w-full justify-center items-center"
>
<CancelBlueCircle />
</TouchableOpacity>
<View className="flex-1 items-center justify-center gap-3">
{!hasPermission ? (
<View className="w-80 h-80 rounded-full border-2 border-dashed border-blue-500 bg-white">
<Image
source={images.AVATAR}
className="w-full h-full rounded-full"
resizeMode="cover"
/>
</View>
) : photoUri ? (
<View className="w-80 h-80 rounded-full overflow-hidden border-[6px] border-white">
<Image
source={{ uri: photoUri }}
className="w-full h-full"
resizeMode="cover"
/>
</View>
) : (
<View className="w-80 h-80 rounded-full overflow-hidden border-[6px] border-white border-dashed ">
<CameraView
ref={cameraRef}
style={{ flex: 1 }}
facing={type}
onCameraReady={() => setIsReady(true)}
flash="off"
mirror={false}
/>
</View>
)}
{hasPermission && (
<Text className="font-poppins font-medium text-xl text-center text-[#FFFFFFCC]">
{photoUri
? "Great! Face capture complete."
: "Keep your face inside the circle and look straight"}
</Text>
)}
{photoUri && (
<View className="flex-row items-center gap-3">
<CheckBlue3 />
<Text className="font-poppins font-light text-[10px] text-[#2B58DA] leading-none">
Capture Done
</Text>
</View>
)}
</View>
<CustomButton2
disabled={clockingInWithFace === true}
className="h-16 w-full rounded-xl p-0 overflow-hidden"
handlePress={onActionBtnPress}
>
<LinearGradient
colors={["#0303753D", "#0606DB70", "#03037529"]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
className={cn(
"flex-1 rounded-xl w-full h-full items-center justify-center",
clockingInWithFace
? "flex-row items-center gap-3"
: ""
)}
>
<Text className="font-poppins font-semibold text-base text-white">
{!hasPermission
? "Grant Permission"
: photoUri
? "Clock-In / Clock-Out"
: "Capture"}
</Text>
{clockingInWithFace && (
<ActivityIndicator color="#fff" size={16} />
)}
</LinearGradient>
</CustomButton2>
</View>
</View>
</Modal>
</View>
);
};
export default React.memo(FaceIDClockinModal);
`