Skip to content

White Blank Screen #628

@udoigwe

Description

@udoigwe

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);
`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions