Root cause: quickCapture.ts relied on window.Capacitor.Plugins.Camera,
which is only populated once something imports '@capacitor/camera'. Since
we deliberately never import that package (to avoid the old white-screen
crash), the plugin was never registered, the native check failed, and Quick
Snap fell through to the web file input — opening the gallery.
Fix: use Capacitor's official registerPlugin('Camera') from the bundled
@capacitor/core. This returns a proxy that dispatches to the native Camera
implementation over the bridge without needing the JS @capacitor/camera
package to self-register, and without putting a bare '@capacitor/camera'
specifier in the bundle. Applied the same pattern to CameraCapture.tsx.
Also: on a permission error, show a clear "enable camera access" toast
instead of silently opening the gallery.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
95 lines
3.6 KiB
TypeScript
95 lines
3.6 KiB
TypeScript
/**
|
|
* 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<CapacitorCameraPlugin>('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<File | null> {
|
|
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' });
|
|
}
|