craft.d.ts declares window.craft as CraftBridge, an interface with 78 direct methods. Thirty-three of them do not exist on the object iOS injects, and seventeen do not exist on either platform's.
They type-check. They throw TypeError at runtime.
// compiles clean; works on Android; TypeError on iOS
const tag = await window.craft.scanNFC()Absent from iOS, present on Android (16)
scanNFC · scanQRCode · takeScreenshot · startAudioRecording · stopAudioRecording · startVideoRecording · pickFile · downloadFile · saveFile · startMotionUpdates · stopMotionUpdates · signInWithGoogle · toggleFlashlight · onAppStateChange · offAppStateChange · offNetworkChange
These are the sharpest, because the same source compiles and runs on one platform and not the other, with nothing in between to warn you.
Absent from both (17)
getContacts · addContact · getCalendarEvents · createCalendarEvent · deleteCalendarEvent · scheduleNotification · cancelNotification · cancelAllNotifications · getPendingNotifications · restorePurchases · signInWithApple · startBluetoothScan · stopBluetoothScan · requestHealthAuthorization · getHealthData · requestFitnessAuthorization · getFitnessData
For several of these the capability exists under a namespace — craft.contacts.…, craft.notifications.… — so what is wrong is the declaration, not the bridge. That still means the published types describe a shape neither runtime has.
What this is not
The actions work. Both bridges expose a generic escape hatch — craft._invoke(action, payload) on iOS (CraftApp.swift:2543) and craft.invoke('namespace.action', params) in craft-bridge.js:297 — and the dispatcher arms behind these names are implemented, several of them now in Zig. So this is a missing named convenience method and a type declaration that promises one, not a missing capability.
I checked for that hatch before writing this issue, because "unreachable" was my first reading and it was wrong.
Why nothing caught it
ios_conformance_test.zig checks one direction only, and says so:
every action the page can call is one the spec handles — The
ota*check. Five methods in the injected JavaScript posted actions the switch never handled…
The reverse — a handled action with no method, or a declared method with no implementation on either side — has never been checked. bridge_mobile_motion.zig found one instance by hand and wrote it down:
Worth knowing while reading any of that:
window.craft.startMotionUpdatesdoes not exist in the injected iOS JavaScript. … So the README example and the test page's two buttons areTypeErrors on the legacy path … That is a second pre-existing bug
One module noticed, recorded it in its own header, and nothing else could see it. Same shape as craftShortcut in bridge_mobile_shortcuts.zig (#127) and the twelve gate claims fixed in #132. It is thirty-three, not one.
The check that would hold it
Three sets, all scannable from source, and the same shape as the event-channel checks in #128:
- method names on
CraftBridgeincraft.d.ts name: functionkeys on iOS's injectedwindow.craft- the same on Android's
Every declared method should exist on both, with a recorded-and-checked table for the deliberate exceptions — platform-only capabilities like craft.siri.* and craft.watch.* are legitimate and should be rows with reasons rather than silent absences.
Fixing the 33 is a separate question from checking them, and probably a mix of adding thin wrappers over _invoke and correcting declarations that were never true.