scry/lib/hooks/useCamera.ts
Chris Kruining 83ab4df537
Migrate from .NET MAUI to Expo + Convex
Complete rewrite of Scry using TypeScript stack:

- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)

Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support

Removes all .NET/MAUI code (src/, test/, tools/).

💘 Generated with Crush

Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
2026-02-09 16:16:34 +01:00

116 lines
3.3 KiB
TypeScript

/**
* Hook for camera permissions.
* Supports both expo-camera (Expo Go) and react-native-vision-camera (production).
*/
import { useState, useEffect, useRef } from "react";
import { Platform, Linking, Alert } from "react-native";
import Constants from "expo-constants";
// Detect if running in Expo Go
const isExpoGo = Constants.appOwnership === "expo";
export interface CameraPermissionState {
hasPermission: boolean;
isLoading: boolean;
error: string | null;
}
/**
* Hook for camera permissions that works in both Expo Go and production builds.
*/
export function useCameraPermission(): CameraPermissionState & { requestPermission: () => Promise<void> } {
const [hasPermission, setHasPermission] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
checkPermission();
}, []);
const checkPermission = async () => {
try {
if (isExpoGo) {
// Use expo-camera
const { Camera } = await import("expo-camera");
const { status } = await Camera.getCameraPermissionsAsync();
setHasPermission(status === "granted");
} else {
// Use react-native-vision-camera
const { Camera } = await import("react-native-vision-camera");
const status = await Camera.getCameraPermissionStatus();
setHasPermission(status === "granted");
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to check camera permission");
} finally {
setIsLoading(false);
}
};
const requestPermission = async () => {
try {
setIsLoading(true);
setError(null);
if (isExpoGo) {
// Use expo-camera
const { Camera } = await import("expo-camera");
const { status, canAskAgain } = await Camera.requestCameraPermissionsAsync();
if (status === "granted") {
setHasPermission(true);
} else {
if (!canAskAgain) {
showSettingsAlert();
}
setError("Camera permission denied");
}
} else {
// Use react-native-vision-camera
const { Camera } = await import("react-native-vision-camera");
const status = await Camera.requestCameraPermission();
if (status === "granted") {
setHasPermission(true);
} else if (status === "denied") {
showSettingsAlert();
setError("Camera permission denied");
}
}
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to request camera permission");
} finally {
setIsLoading(false);
}
};
return { hasPermission, isLoading, error, requestPermission };
}
function showSettingsAlert() {
Alert.alert(
"Camera Permission Required",
"Scry needs camera access to scan cards. Please enable it in Settings.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Open Settings",
onPress: () => {
if (Platform.OS === "ios") {
Linking.openURL("app-settings:");
} else {
Linking.openSettings();
}
},
},
]
);
}
/**
* Returns true if using expo-camera (Expo Go), false for Vision Camera (production).
*/
export function useIsExpoGo(): boolean {
return isExpoGo;
}