On iOS, an action whose capability is switched off in craft.config.json rejects with CAPABILITY_DISABLED. Swift checks config.enable* in each dispatcher case, and Zig checks the same flags through ios_config.gateFor, which ios_conformance_test.zig keeps in step with Swift.
On Android, the flags decide:
- which permissions go into
AndroidManifest.xml(packages/android/src/index.ts,permissions.add(...)around line 312); - what
window.craft.capabilitiesreports (CraftBridge.kt.template, theblock around line 316); - and, at runtime, only push (
becomes a literal rejection) and Health Connect (a stub class).
Nothing else checks them. Outside the capabilities object literal, no placeholder is used anywhere in CraftBridge.kt.template, and no bridge_android_*.zig module or android_dispatch.zig reads a flag. So with enableShare: false, enableHaptics: false or enableSecureStorage: false, the page is told craft.capabilities.share === false (and so on), and then the call works anyway.
For flags backed by a permission, such as camera, geolocation and speech, the call does not work anyway. It fails at the permission check with an Android-specific message, instead of the code that says the app was built without the capability.
Why it matters
- The two platforms disagree about the same config. A page written against iOS that relies on
CAPABILITY_DISABLEDnever sees it on Android. - It undercuts the design the hand-off relies on: the page should not be able to tell which side answered. On iOS the gate lives on both sides and is tested to match; on Android it lives on neither.
capabilitiesis a promise the bridge does not keep. A page that feature-detects with it and a page that just calls get different behaviour.
Not a one-liner
Gating every action would reject calls that work today in apps that never set the flags, since every enable* flag defaults to false in packages/android/src/index.ts. That is a behaviour change, and it needs a decision about defaults. iOS defaults the same flags to false and gates anyway, so the iOS behaviour is the obvious target, but the migration for existing Android apps should be deliberate.
A reasonable shape mirrors iOS: a small android_config.zig read by the dispatcher, the same checks in Kotlin, and an android_conformance_test.zig section that fails when the two disagree.
Found while fixing #203. share there was deliberately left ungated on Android so that fix did not also make this decision.