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>
116 lines
3.3 KiB
TypeScript
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;
|
|
}
|