/** * CameraCapture — unified camera for browser + native Capacitor app * * On native (Android / iOS via Capacitor): * → Uses @capacitor/camera which calls the OS native camera directly. * No WebView permission popup, no getUserMedia, no dialog needed. * The user sees the full native camera UI, then returns here with the photo. * * On browser (desktop / mobile web): * → Falls back to getUserMedia stream inside this dialog component. */ import { useState, useRef, useCallback, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; import { Camera, RotateCcw, Check, Loader2, SwitchCamera, Upload, } from 'lucide-react'; // ─── Capacitor detection ──────────────────────────────────────────────────── // We dynamically import Capacitor to avoid breaking the web build. // On native this module exists; on web it throws/returns isNativePlatform=false. async function isNative(): Promise { try { const { Capacitor } = await import('@capacitor/core'); return Capacitor.isNativePlatform(); } catch { return false; } } async function captureNative(): Promise { const { Camera, CameraResultType, CameraSource } = await import('@capacitor/camera'); const photo = await Camera.getPhoto({ resultType: CameraResultType.DataUrl, source: CameraSource.Camera, quality: 90, correctOrientation: true, }); if (!photo.dataUrl) return null; const res = await fetch(photo.dataUrl); const blob = await res.blob(); return new File([blob], `skate-spot-${Date.now()}.jpg`, { type: 'image/jpeg' }); } // ─── Props ────────────────────────────────────────────────────────────────── interface CameraCaptureProps { open: boolean; onOpenChange: (open: boolean) => void; onCapture: (file: File) => void; isUploading?: boolean; } // ─── Component ────────────────────────────────────────────────────────────── export function CameraCapture({ open, onOpenChange, onCapture, isUploading, }: CameraCaptureProps) { const { toast } = useToast(); const videoRef = useRef(null); const canvasRef = useRef(null); const streamRef = useRef(null); const [hasPermission, setHasPermission] = useState(null); const [capturedImage, setCapturedImage] = useState(null); const [facingMode, setFacingMode] = useState<'user' | 'environment'>('environment'); const [isStarting, setIsStarting] = useState(false); const [isCapturingNative, setIsCapturingNative] = useState(false); // ── Native capture (Capacitor) ──────────────────────────────────────────── const handleNativeCapture = useCallback(async () => { setIsCapturingNative(true); try { const file = await captureNative(); if (file) { onCapture(file); onOpenChange(false); } } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); // User cancelled — not an error if (msg.toLowerCase().includes('cancel') || msg.toLowerCase().includes('user cancel')) { onOpenChange(false); return; } toast({ title: 'Camera error', description: 'Could not open the camera. Please check app permissions in Settings.', variant: 'destructive', }); } finally { setIsCapturingNative(false); } }, [onCapture, onOpenChange, toast]); // ── Browser camera ──────────────────────────────────────────────────────── const startCamera = useCallback(async (facing: 'user' | 'environment' = facingMode) => { setIsStarting(true); try { if (streamRef.current) { streamRef.current.getTracks().forEach(track => track.stop()); } const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: facing, width: { ideal: 1920 }, height: { ideal: 1080 } }, audio: false, }); streamRef.current = stream; if (videoRef.current) { videoRef.current.srcObject = stream; await videoRef.current.play(); } setHasPermission(true); setFacingMode(facing); } catch { setHasPermission(false); toast({ title: 'Camera access denied', description: 'Please allow camera access in your browser settings.', variant: 'destructive', }); } finally { setIsStarting(false); } }, [facingMode, toast]); const stopCamera = useCallback(() => { streamRef.current?.getTracks().forEach(track => track.stop()); streamRef.current = null; if (videoRef.current) videoRef.current.srcObject = null; }, []); // On native: immediately trigger the native camera when the dialog opens // On web: start the stream useEffect(() => { if (!open) { stopCamera(); setCapturedImage(null); setHasPermission(null); return; } let cancelled = false; isNative().then((native) => { if (cancelled) return; if (native) { handleNativeCapture(); } else { startCamera(); } }); return () => { cancelled = true; stopCamera(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); const switchCamera = () => { const next = facingMode === 'user' ? 'environment' : 'user'; setCapturedImage(null); startCamera(next); }; const capturePhoto = () => { if (!videoRef.current || !canvasRef.current) return; const video = videoRef.current; const canvas = canvasRef.current; const ctx = canvas.getContext('2d'); if (!ctx) return; canvas.width = video.videoWidth; canvas.height = video.videoHeight; ctx.drawImage(video, 0, 0, canvas.width, canvas.height); setCapturedImage(canvas.toDataURL('image/jpeg', 0.9)); }; const confirmPhoto = async () => { if (!capturedImage || !canvasRef.current) return; const res = await fetch(capturedImage); const blob = await res.blob(); const file = new File([blob], `skate-spot-${Date.now()}.jpg`, { type: 'image/jpeg' }); onCapture(file); }; const handleClose = () => { stopCamera(); setCapturedImage(null); onOpenChange(false); }; // ── Native in-progress screen ───────────────────────────────────────────── // While native camera is active this dialog acts as a thin loading screen. if (isCapturingNative) { return (

Opening camera…

Use your phone camera, then return here

); } // ── Browser camera dialog ───────────────────────────────────────────────── return ( Take a Photo
{/* Loading */} {isStarting && (

Starting camera...

)} {/* No permission */} {hasPermission === false && (

Camera Access Required

Allow camera access in your browser settings, or use the file picker below.

)} {/* Video feed */} {!capturedImage && (
); }