/** * Quick Snap capture helper — platform-aware, camera-first. * * NATIVE (Capacitor APK): uses Capacitor's official `registerPlugin('Camera')` * from `@capacitor/core` (which is bundled and always installed). This returns * a plugin PROXY that dispatches straight to the natively-registered Camera * implementation over the Capacitor bridge — it does NOT require the JS * `@capacitor/camera` package to have loaded and run its side-effect * registration first. That distinction is the whole fix: * * Before, this helper read `window.Capacitor.Plugins.Camera`, which is only * populated once something imports '@capacitor/camera'. Because we * deliberately never import that package (to avoid the old white-screen * crash), the plugin was never registered, `Plugins.Camera` was undefined, * `isNativeCapture()` returned false, and Quick Snap fell through to the web * file input → the GALLERY opened instead of the camera. * * `registerPlugin` sidesteps all of that: `@capacitor/core` guarantees the * proxy, and the native side provides the real implementation. No bare * `@capacitor/camera` specifier ends up in the bundle. * * WEB: there is no reliable way to force the camera from JS, so the caller * falls back to a file input with capture="environment". This module only * handles the native path; isNativeCapture() tells the caller which route to * take. */ import { Capacitor, registerPlugin } from '@capacitor/core'; interface CapacitorCameraPlugin { getPhoto(options: { resultType: string; source: string; quality: number; correctOrientation: boolean; saveToGallery?: boolean; allowEditing?: boolean; }): Promise<{ dataUrl?: string }>; } // Lazily create the plugin proxy once. `registerPlugin` is a pure factory — it // just builds a proxy object and never touches native APIs at call time, so // this is safe on web too (the proxy simply has no native impl there). let cameraPlugin: CapacitorCameraPlugin | null = null; function getCameraPlugin(): CapacitorCameraPlugin { if (!cameraPlugin) { cameraPlugin = registerPlugin('Camera'); } return cameraPlugin; } /** * True when we're running inside a native Capacitor shell, where the Camera * plugin proxy can reach a real native implementation. On web this is false and * the caller uses the file-input fallback. */ export function isNativeCapture(): boolean { try { return Capacitor.isNativePlatform(); } catch { return false; } } /** * Open the native camera and return the captured photo as a File. * Returns null if the user cancels. Throws if the plugin errors for any other * reason (caller should fall back to the web file input). */ export async function captureFromNativeCamera(): Promise { const Camera = getCameraPlugin(); let photo: { dataUrl?: string }; try { photo = await Camera.getPhoto({ resultType: 'dataUrl', // CameraResultType.DataUrl source: 'CAMERA', // CameraSource.Camera — forces the camera, not the gallery quality: 90, correctOrientation: true, saveToGallery: false, allowEditing: false, }); } catch (err) { // Capacitor throws with a "cancel"/"cancelled" message when the user backs // out — treat that as a benign null rather than an error. const msg = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase(); if (msg.includes('cancel')) return null; throw err; } 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' }); }