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:
- The wrapper is DOM-gated. Every method checks
typeof window !== "undefined" && craft?.camera, and when that fails it buildsdocument.createElement("input")and aFileReader. A native screen running in JavaScriptCore has neitherwindownordocument, so both the bridge path and the fallback are dead. - The native capability protocol does not carry it. The capabilities a native screen is handed are
fetch,secureStorage,hapticsandlifecycle. 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:
| View | What it uploads | Endpoint |
|---|---|---|
settings | profile photo, accept="image/jpeg,image/png,image/webp" | POST /api/users/{id}/photo |
trail/[id] | review photos, accept="image/*" multiple | POST /api/trails/{id}/photos, field photo |
activity/[id] | activity photos, accept="image/*" multiple | POST /api/activities/{id}/photos |
record | a 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:
- Expose the picker as a capability, and let
fetchtake 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". - 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.