ReviewOS

craft-native/craft

feat(native): a native screen can pick a photo but has no way to upload one

#341
Closed glennmichael123 opened this 8 days ago ·
8 days ago

The gap

A native screen can neither choose a file nor send one. The picker exists; the path from it to a request does not.

The picker is already there. craft-native/dist/mobile.js exposes camera.takePicture, camera.pickImage, camera.pickMultiple and camera.isAvailable, returning { base64, uri, width, height, mimeType }. Two things keep a native screen from reaching it:

  1. The wrapper is DOM-gated. Every method checks typeof window !== "undefined" && craft?.camera, and when that fails it builds document.createElement("input") and a FileReader. A native screen running in JavaScriptCore has neither window nor document, so both the bridge path and the fallback are dead.
  2. The native capability protocol does not carry it. The capabilities a native screen is handed are fetch, secureStorage, haptics and lifecycle. There is no camera, photo-library or document capability in that set.

And fetch cannot carry the bytes anyway. The native network capability takes a string body — there is no Blob, File or FormData in JavaScriptCore — so even with a base64 string in hand there is no multipart request to put it in.

What it blocks

Four wildloop views, all of which post multipart/form-data today:

ViewWhat it uploadsEndpoint
settingsprofile photo, accept="image/jpeg,image/png,image/webp"POST /api/users/{id}/photo
trail/[id]review photos, accept="image/*" multiplePOST /api/trails/{id}/photos, field photo
activity/[id]activity photos, accept="image/*" multiplePOST /api/activities/{id}/photos
recorda recorded file to import, accept=".gpx,.tcx,.fit"import endpoint

resources/assets/scripts/game-api.ts:599 is the shape: new FormData(), body.append('photo', file), and the Content-Type deliberately deleted so the browser writes its own boundary. None of that exists natively.

record also needs a document picker rather than an image one — .gpx, .tcx, .fit are not photos, and the camera API has no accept list.

What would be enough

Two directions, and they trade off differently:

  1. Expose the picker as a capability, and let fetch take multipart parts by file URI — { parts: [{ name: 'photo', uri, filename, mimeType }] }, with the host streaming the file and never passing the bytes through JS. More general, and it also covers a future "attach a GPX".
  2. One upload capability that does the whole job — hand it a picked uri, a URL, headers and a field name, and let the host build the multipart request and return the response. Less to design on the JS side, and it avoids base64-ing a 12-megapixel photo through JavaScriptCore to no purpose.

(2) looks cheaper for exactly the case we have, (1) is the better primitive. I have not tried either, and the distinction may not matter if the picker returns a path the host can stream.

Either way the picker needs an accept list, or a sibling documentPicker, for the import case.

Scope

Not asking for background uploads, resumable uploads, or progress reporting. A photo chosen, sent, and a reply read would cover all four screens.

Filed from the wildloop screen inventory for stacksjs/stx#1992, alongside craft-native/craft#340 (no Map primitive). Unlike the map, this one is partly built already — the device side is done and the gap is the seam between it and a native screen.

8 days ago

Resolved on main. Native screens can pick image/file handles and submit owned-file multipart forms without base64-ing the payload through JavaScriptCore. iOS coverage is in the native files builder suite; Android coverage is in scripts/native-renderer-contract.test.ts (including the owned-file multipart path).

Sign in to comment on this issue.