Five modules gate an action on the presence of an Info.plist key, used as a stand-in for a config.enable* flag Zig could not read. Each says so, and each was right when it was written. ios_config.zig reads those flags exactly now, and ios_dispatch.offerToModules refuses a gated action before any module runs — so the proxies are a second, weaker gate sitting behind the real one.
Two of these — the ones that are only stale prose — are also worth fixing: nine module headers still tell the reader their gate "has no Zig mirror" and their actions are "served unconditionally", which has not been true since ios_config.zig landed.
Proxies that are now redundant
| module | key | what it stands in for | the module's own words |
|---|---|---|---|
bridge_mobile_contactpicker.zig (:635) | NSContactsUsageDescription | enableContacts | "not a precondition of the API … purely as evidence of the flag" |
bridge_mobile_calendar.zig (:908) | NSCalendarsUsageDescription | enableCalendar | for deleteCalendarEvent, "only evidence of the flag — which is why it is still checked" |
bridge_mobile_motion.zig (:392) | NSMotionUsageDescription | enableMotionSensors | "an exact proxy for it" |
bridge_mobile_imagepicker.zig (:570) | NSPhotoLibraryUsageDescription | enableCamera || enableVideoRecording | "the closest proxy" — explicitly not exact |
gateFor maps every one of these actions today: pickContact/getContacts/addContact → .contacts, the three calendar actions → .calendar, startMotionUpdates → .motion_sensors, startListening → .speech_recognition, pickImage/openCamera → .camera.
Why this is not only tidying
The proxy and the flag can disagree, and only one direction is caught:
- flag off, key present — the real gate refuses first. No divergence.
- flag on, key absent — the real gate passes and the module refuses anyway. A false refusal, of an action the spec would serve.
For the exact proxies, packages/ios/src/index.ts writes the key iff the flag, so a generated app keeps them in step. A hand-edited Info.plist, or any app not built through the SDK, does not. imagepicker's is not exact even in generated apps: it stands in for a disjunction of two flags with one key.
Not to be touched
The plist checks that are crash guards must stay, and are a different thing entirely — iOS terminates the process when authorization is requested without the key, so these run before the API call and are not gates at all:
bridge_mobile_audiorec.zig—NSMicrophoneUsageDescriptionbridge_mobile_bluetooth.zig—NSBluetoothAlwaysUsageDescriptionbridge_mobile_speech.zig— both its keys.NSMicrophoneUsageDescriptionis not an exact proxy anyway (enableSpeechRecognition || enableAudioRecording), andNSSpeechRecognitionUsageDescriptionis also a precondition:requestAuthorization:without it does not fail, iOS terminates the process. It reads exactly like the four proxies above and is not one — an earlier revision of this issue listed it as redundant, which it is not.bridge_mobile_imagepicker.zig—NSCameraUsageDescriptionfor a.camerasourcebridge_mobile_location.zig— the two location keysbridge_mobile_calendar.zig— the key on the paths where it is an API precondition, which the module already separates (:902)
Any fix has to keep that distinction, which is why this is an issue rather than a sweep.
The stale headers
Twelve modules claimed a gate has no Zig mirror, no Zig-reachable channel, or that actions are served unconditionally: contactpicker, bgtasks, calendar, filepicker, motion, notifications, notifcancel, securestore, system, location, imagepicker, speech. All were false; the gate table covers every action they name. bridge_mobile_watch.zig makes the same claim and is still true — nothing in gateFor maps sendToWatch, updateWatchContext or isWatchReachable.
The first nine were found by grepping the phrase the first one used; the last three said the same thing in different words and turned up only on a second sweep for the idea rather than the wording. Fixed in #132, which states it once at the enforcement point and leaves each module a pointer.
bridge_mobile_imagepicker.zig was the sharpest of them: it asserted in the present tense that a default-config app hangs the page forever on openCamera and pickImage, which stopped being reachable the moment gateFor mapped those actions.
Related
The same rot in a manifest rather than a header was #134: haptic refused because config.enableHaptics was "not visible to Zig" while gateFor mapped that exact flag. That one is now caught by a build check, because a refusal naming a flag the gate table maps is a contradiction a test can see. These are prose, so nothing catches them.