also looking at this
feat(ios): haptic, whose reason expired when ios_config landed
#134
18 files
+562
-163
Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.
| @@ -103,10 +103,13 @@ | ||
| 103 | 103 | //! posting); a field that is **present with the wrong type** is refused, never |
| 104 | 104 | //! coerced. |
| 105 | 105 | //! |
| 106 | //! **The `config.enableBackgroundTasks` gate has no Zig mirror.** It appears | |
| 107 | //! nowhere in `packages/zig/src`, so the three actions here are served | |
| 108 | //! unconditionally — the securestore and notifcancel modules document the same | |
| 109 | //! choice. In an app that left the gate at its default `false` this changes a | |
| 106 | //! **The `config.enableBackgroundTasks` gate is read, not skipped.** | |
| 107 | //! `ios_config.gateFor` maps all three actions to `.background_tasks`, so | |
| 108 | //! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this | |
| 109 | //! module is asked. This paragraph used to say the flag "appears nowhere in | |
| 110 | //! `packages/zig/src`" and that the actions were served unconditionally — true | |
| 111 | //! when written, false since `ios_config.zig`, and repeated in eight other | |
| 112 | //! module headers that were equally true and are equally false. In an app that left the gate at its default `false` this changes a | |
| 110 | 113 | //! hang into an answer: `schedule` now reaches the scheduler and reports |
| 111 | 114 | //! whatever it says (a rejection, until the plist gains |
| 112 | 115 | //! `BGTaskSchedulerPermittedIdentifiers`), and the cancels report the no-op |
| @@ -139,10 +139,17 @@ | ||
| 139 | 139 | //! `deleteCalendarEvent` — which requests nothing (3350) — the key is only |
| 140 | 140 | //! evidence of `config.enableCalendar`, and it is checked for that reason. |
| 141 | 141 | //! |
| 142 | //! **`config.enableCalendar` has no Zig mirror**, the same gap | |
| 143 | //! `bridge_mobile_contactpicker.zig` records for `enableContacts`. The plist key | |
| 144 | //! `packages/ios/src/index.ts:190` writes iff that flag is the stand-in, and the | |
| 145 | //! mapping is exact — nothing else writes or shares it. | |
| 142 | //! **`config.enableCalendar` is read directly now.** `ios_config.gateFor` maps | |
| 143 | //! all three actions to `.calendar`, so `ios_dispatch.offerToModules` answers | |
| 144 | //! `CAPABILITY_DISABLED` before this module is asked. This used to say the flag | |
| 145 | //! had no Zig mirror, which was true until `ios_config.zig` read | |
| 146 | //! `craft.config.json`. | |
| 147 | //! | |
| 148 | //! The plist key `packages/ios/src/index.ts:190` writes iff that flag was the | |
| 149 | //! stand-in, and on the one path where it is *only* evidence of the flag rather | |
| 150 | //! than a precondition of the API it is now redundant with the real gate. | |
| 151 | //! Tracked in issue #131; the paths where the key is a genuine precondition are | |
| 152 | //! separated below and stay. | |
| 146 | 153 | //! |
| 147 | 154 | //! **Behaviour changes on every disabled or malformed path, in Zig's favour.** |
| 148 | 155 | //! Swift's three cases have no `else` (717-731): an app with |
| @@ -83,12 +83,20 @@ | ||
| 83 | 83 | //! Until then, "the native call failed" is what a cancel says, and saying so |
| 84 | 84 | //! here is the point. |
| 85 | 85 | //! |
| 86 | //! **`config.enableContacts` has no Zig mirror.** `ios.zig`'s `AppConfig` has | |
| 87 | //! no such field — the same gap `bridge_mobile_system.zig` records for | |
| 88 | //! `enableShare`. The Info.plist is read instead, and for this flag the mapping | |
| 89 | //! is exact: `packages/ios/src/index.ts:189` writes `NSContactsUsageDescription` | |
| 90 | //! if and only if `config.enableContacts`, with none of the sharing that made | |
| 91 | //! the location keys ambiguous. | |
| 86 | //! **`config.enableContacts` is read directly now, and the plist proxy below | |
| 87 | //! is what is left over.** `ios_config.gateFor` maps this action to | |
| 88 | //! `.contacts`, so `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` | |
| 89 | //! before this module is asked. The paragraph here used to say the flag had no | |
| 90 | //! Zig mirror, which was true until `ios_config.zig` read | |
| 91 | //! `craft.config.json`. | |
| 92 | //! | |
| 93 | //! The `NSContactsUsageDescription` check further down was the stand-in for | |
| 94 | //! that flag — `packages/ios/src/index.ts:189` writes the key if and only if | |
| 95 | //! `config.enableContacts`, with none of the sharing that made the location | |
| 96 | //! keys ambiguous. It is now a second, weaker gate behind the real one, and it | |
| 97 | //! can only differ in the direction that refuses an action the config allows: | |
| 98 | //! an app whose Info.plist was not written by the SDK. Tracked in issue #131 | |
| 99 | //! along with three more of the same shape. | |
| 92 | 100 | //! |
| 93 | 101 | //! Two things about that gate have to be said plainly. First, the key is **not |
| 94 | 102 | //! a precondition of the API**: `CNContactPickerViewController` runs out of |
| @@ -74,11 +74,12 @@ | ||
| 74 | 74 | //! Info.plist key to read instead: `renderUsageDescriptions` |
| 75 | 75 | //! (`packages/ios/src/index.ts:183-199`) writes nothing from either flag. |
| 76 | 76 | //! |
| 77 | //! Both are therefore served unconditionally, and an app that set | |
| 78 | //! `enableFilePicker: false` or `enableFileDownload: false` now gets the | |
| 79 | //! behaviour anyway. Restoring the gate means adding the fields to | |
| 80 | //! `AppConfig`, not adding a guess here — the same trade | |
| 81 | //! `bridge_mobile_system.zig` records word for word for `share`. | |
| 77 | //! Both flags are read directly now: `ios_config.gateFor` maps `pickFile` to | |
| 78 | //! `.file_picker` and `saveFile` to `.file_download`, so | |
| 79 | //! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this | |
| 80 | //! module is asked. This used to say both were served unconditionally and that | |
| 81 | //! restoring the gate meant adding fields to `AppConfig` — true when written, | |
| 82 | //! and answered by `ios_config.zig` reading `craft.config.json` instead. | |
| 82 | 83 | //! |
| 83 | 84 | //! Falling through to the shim is not the safe option it was for |
| 84 | 85 | //! `takeScreenshot`, because the shim does not answer these correctly today: |
| @@ -78,23 +78,30 @@ | ||
| 78 | 78 | //! **false** (`CraftApp.swift:192`, `packages/ios/src/index.ts:120`). When it |
| 79 | 79 | //! is false the `case` falls off the end and replies *nothing at all*, while |
| 80 | 80 | //! `CraftSwiftShim.handleAction` still returns `true` (`:5247`) so Zig |
| 81 | //! believes the hand-off succeeded. The injected promise is the hand-built | |
| 82 | //! kind with no timeout. **In a default-config app, `openCamera` and | |
| 83 | //! `pickImage` hang the page forever today.** The "working Swift shim" is | |
| 84 | //! only working for an app that set `enableCamera: true`. | |
| 81 | //! believes the hand-off succeeded, and the injected promise is the hand-built | |
| 82 | //! kind with no timeout — so on the shim a default-config app hangs the page | |
| 83 | //! forever. `ios_config.gateFor` maps both actions to `.camera` now, so | |
| 84 | //! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` first and that | |
| 85 | //! hang is unreachable while Zig is linked. This paragraph asserted the hang | |
| 86 | //! in the present tense until then, and the next one said `enableCamera` had | |
| 87 | //! no Zig-reachable channel at all. | |
| 85 | 88 | //! |
| 86 | //! `enableCamera` has no Zig-reachable channel, so this module gates on the | |
| 87 | //! Info.plist keys `packages/ios/src/index.ts:185-186` writes: | |
| 89 | //! The Info.plist keys `packages/ios/src/index.ts:185-186` writes are still | |
| 90 | //! read, for two different reasons: | |
| 88 | 91 | //! |
| 89 | 92 | //! - **`NSCameraUsageDescription` is an extra gate on `openCamera` only, and |
| 90 | 93 | //! it is a crash guard rather than a config proxy.** Presenting a `.camera` picker in a |
| 91 | 94 | //! process without that key is a TCC termination, not an error anything |
| 92 | 95 | //! could be told about. It is written for |
| 93 | 96 | //! `enableCamera || enableVideoRecording || enableQRScanner || enableAR`. |
| 94 | //! - **`NSPhotoLibraryUsageDescription` stands in for `config.enableCamera`, | |
| 97 | //! - **`NSPhotoLibraryUsageDescription` stood in for `config.enableCamera`, | |
| 95 | 98 | //! and it is over-broad by `enableVideoRecording`** — it is written for |
| 96 | //! `enableCamera || enableVideoRecording`, which is the closest proxy | |
| 97 | //! available. It is emphatically **not** a photo-library permission gate: | |
| 99 | //! `enableCamera || enableVideoRecording`, which was the closest proxy | |
| 100 | //! available while the flag could not be read. It is now a second, weaker | |
| 101 | //! gate behind the real one, and the only one of these proxies inexact even | |
| 102 | //! in an SDK-generated app. Tracked in issue #131 with three more. | |
| 103 | //! | |
| 104 | //! It is emphatically **not** a photo-library permission gate: | |
| 98 | 105 | //! `UIImagePickerController` with `.photoLibrary` has run out-of-process |
| 99 | 106 | //! since iOS 11 and neither prompts nor requires the key. Refusing on it is |
| 100 | 107 | //! a statement about how the app was configured, never about what the |
| @@ -1236,16 +1236,20 @@ const key_always = "NSLocationAlwaysAndWhenInUseUsageDescription"; | ||
| 1236 | 1236 | /// Decide, from evidence in the running process, whether geolocation was |
| 1237 | 1237 | /// configured and which prompt to ask for. |
| 1238 | 1238 | /// |
| 1239 | /// Neither `config.enableGeolocation` nor `config.enableBackgroundLocation` has | |
| 1240 | /// a Zig mirror — they appear nowhere in `packages/zig/src` outside a comment — | |
| 1241 | /// which is the same gap `bridge_mobile_watch.zig` documents for | |
| 1242 | /// `enableWatchApp`. Rather than guess, this reads the Info.plist keys the | |
| 1243 | /// generator writes from exactly those flags. Apple requires the usage | |
| 1244 | /// description before authorization may be requested at all, so its absence is | |
| 1245 | /// both the honest signal that the app was never built for location and the | |
| 1246 | /// condition under which requesting would be illegitimate; the refusal happens | |
| 1239 | /// Both `config.enableGeolocation` and `config.enableBackgroundLocation` are | |
| 1240 | /// read directly now — `ios_config.gateFor` maps `getCurrentPosition`, | |
| 1241 | /// `watchPosition` and `startLocationRecording` to `.geolocation`, and | |
| 1242 | /// `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this | |
| 1243 | /// module is asked. This used to say neither had a Zig mirror and that they | |
| 1244 | /// appeared nowhere in `packages/zig/src`; `ios_config.zig`'s own header names | |
| 1245 | /// this file as one of the workarounds it replaced. | |
| 1246 | /// | |
| 1247 | /// The plist read below stays, and not as a config proxy. Apple requires the | |
| 1248 | /// usage description before authorization may be requested at all, so its | |
| 1249 | /// absence is the condition under which requesting would be illegitimate — a | |
| 1250 | /// precondition of the API rather than evidence of a flag. The refusal happens | |
| 1247 | 1251 | /// before any request is made, so what the system would otherwise do never |
| 1248 | /// arises. | |
| 1252 | /// arises, and which of the two keys is present still selects the prompt. | |
| 1249 | 1253 | /// |
| 1250 | 1254 | /// Two divergences, both real and neither hideable: |
| 1251 | 1255 | /// |
| @@ -79,15 +79,21 @@ | ||
| 79 | 79 | //! `else`**, so on the Swift path with the flag off `startMotionUpdates` |
| 80 | 80 | //! replies nothing at all while `CraftSwiftShim.handleAction` still returns |
| 81 | 81 | //! true: the page's promise never settles. The flag defaults to `false` |
| 82 | //! (`packages/ios/src/index.ts:139`) and has no mirror anywhere under | |
| 83 | //! `packages/zig/src`. | |
| 82 | //! (`packages/ios/src/index.ts:139`). It is read directly now: | |
| 83 | //! `ios_config.gateFor` maps `startMotionUpdates` to `.motion_sensors`, so | |
| 84 | //! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this | |
| 85 | //! module is asked. Until `ios_config.zig` read `craft.config.json` the flag | |
| 86 | //! had no mirror anywhere under `packages/zig/src`, and the paragraph below is | |
| 87 | //! what this module did about that. | |
| 84 | 88 | //! |
| 85 | 89 | //! `NSMotionUsageDescription` is an *exact* proxy for it — |
| 86 | 90 | //! `packages/ios/src/index.ts:192` writes that Info.plist key from |
| 87 | 91 | //! `config.enableMotionSensors` and from nothing else, with no `||` (unlike the |
| 88 | 92 | //! location keys, where `bridge_mobile_location.zig` had to document a residual |
| 89 | 93 | //! gap). So `requireMotionConfigured` reads the key and refuses when it is |
| 90 | //! absent. Two things that buys, and one it costs: | |
| 94 | //! absent — now a second, weaker gate behind the real one, which can only | |
| 95 | //! differ by refusing an action the config allows. Tracked in issue #131 with | |
| 96 | //! three more of the same shape. Two things it bought, and one it cost: | |
| 91 | 97 | //! |
| 92 | 98 | //! - a hang becomes a nameable refusal, which is strictly better than the |
| 93 | 99 | //! shim's silence; |
| @@ -249,8 +255,9 @@ pub const default_interval_ms: f64 = 100; | ||
| 249 | 255 | const reply_true = "true"; |
| 250 | 256 | |
| 251 | 257 | /// Emitted into Info.plist by `packages/ios/src/index.ts:192` iff |
| 252 | /// `config.enableMotionSensors` — the flag with no Zig mirror. See the module | |
| 253 | /// comment for what this proxy claims and what it does not. | |
| 258 | /// `config.enableMotionSensors`, which `ios_config.gateFor` now reads directly. | |
| 259 | /// See the module comment for what this proxy claims, what it does not, and why | |
| 260 | /// it is redundant with the real gate. | |
| 254 | 261 | const key_motion_usage = "NSMotionUsageDescription"; |
| 255 | 262 | |
| 256 | 263 | /// Which handler an action selects, split out from `handleMessage` so the |
| @@ -43,11 +43,11 @@ | ||
| 43 | 43 | //! with no `else`: a missing `id`, a non-string `id`, or the gate at its |
| 44 | 44 | //! default `false` replies nothing at all, and the untimed promise never |
| 45 | 45 | //! settles. Malformed input errors here instead (`MissingData` / |
| 46 | //! `InvalidParameter`). The `enableLocalNotifications` gate has no Zig mirror | |
| 47 | //! (it appears nowhere in `packages/zig/src`), so the actions are served | |
| 48 | //! unconditionally — the securestore module documents the same choice, and | |
| 49 | //! cancelling notifications grants a page strictly less than the wipe that | |
| 50 | //! precedent already serves ungated. | |
| 46 | //! `InvalidParameter`), and the gate itself is read rather than skipped: | |
| 47 | //! `ios_config.gateFor` maps all three actions to `.local_notifications`, so | |
| 48 | //! `ios_dispatch.offerToModules` answers `CAPABILITY_DISABLED` before this | |
| 49 | //! module is asked. A settled rejection rather than a hang is the divergence, | |
| 50 | //! and it is the whole of it. | |
| 51 | 51 | //! |
| 52 | 52 | //! **An `id` with an embedded NUL.** `\u0000` is a legal JSON escape and a |
| 53 | 53 | //! page can send it; the route to the UN center is `stringWithUTF8String:`, |
| @@ -40,11 +40,10 @@ | ||
| 40 | 40 | //! **Swift's silent hang.** The dispatcher arm is |
| 41 | 41 | //! `if config.enableLocalNotifications { … }` with no `else`, and the gate |
| 42 | 42 | //! defaults to `false` — so with notifications disabled the untimed promise |
| 43 | //! never settles. `enableLocalNotifications` has no mirror anywhere in | |
| 44 | //! `packages/zig/src`, so this action is served unconditionally, the same call | |
| 45 | //! `bridge_mobile_securestore.zig` and `bridge_mobile_notifcancel.zig` made. | |
| 46 | //! Listing an app's *own* pending notifications grants a page strictly less | |
| 47 | //! than the cancel-everything that precedent already serves ungated. | |
| 43 | //! never settles. Here the flag is read: `ios_config.gateFor` maps this action | |
| 44 | //! to `.local_notifications` and `ios_dispatch.offerToModules` answers | |
| 45 | //! `CAPABILITY_DISABLED` before this module is asked. A settled rejection | |
| 46 | //! rather than a hang is the divergence, and it is the whole of it. | |
| 48 | 47 | //! |
| 49 | 48 | //! **A truncated string reported as the whole one.** `-[NSString UTF8String]` |
| 50 | 49 | //! is NUL-terminated, so a stored title carrying an embedded U+0000 — which |
| @@ -40,18 +40,27 @@ | ||
| 40 | 40 | //! **Swift's silent hang.** The set/get/remove dispatcher arms are `if let`s |
| 41 | 41 | //! with no `else`: a missing `key`, a non-string `value`, or a disabled |
| 42 | 42 | //! `enableSecureStorage` config replies nothing at all, and the legacy promise |
| 43 | //! never settles. Malformed input errors here instead. The capability gate has | |
| 44 | //! no Zig mirror (`enableSecureStorage` appears nowhere in `packages/zig/src`), | |
| 45 | //! so the actions are served unconditionally. For set/get/remove that grants a | |
| 46 | //! page nothing new: the *ungated* sharedItems actions next door already | |
| 47 | //! read and write the same items (one namespace — see above). The two gated | |
| 48 | //! modules that came before chose differently — clipboard kept its gate | |
| 49 | //! plumbable (`craft_ios_set_clipboard_enabled`, rejecting `PermissionDenied` | |
| 50 | //! when off) and haptics declared its gated action `.unavailable` — and | |
| 51 | //! `secureClear` is where the difference is real: under Swift's default-false | |
| 52 | //! gate a page got CAPABILITY_DISABLED instead of a wipe, and here the wipe is | |
| 53 | //! always live. If that gate is wanted back, clipboard's hook is the pattern; | |
| 54 | //! what must not come back is Swift's silent hang on the other three. | |
| 43 | //! never settles. Malformed input errors here instead, and the gate is read | |
| 44 | //! rather than skipped: `ios_config.gateFor` maps all four actions to | |
| 45 | //! `.secure_storage`, so `ios_dispatch.offerToModules` answers | |
| 46 | //! `CAPABILITY_DISABLED` before this module is asked. A settled rejection | |
| 47 | //! rather than a hang is the divergence, and it is the whole of it. | |
| 48 | //! | |
| 49 | //! This paragraph argued the opposite case at length — that | |
| 50 | //! `enableSecureStorage` "appears nowhere in `packages/zig/src`", that the | |
| 51 | //! actions were therefore served unconditionally, and that `secureClear` was | |
| 52 | //! where it mattered because a page got a wipe where Swift's default-false | |
| 53 | //! gate would have refused. Every sentence of it was true when written and | |
| 54 | //! none of it survived `ios_config.zig`, which read the flags out of | |
| 55 | //! `craft.config.json` and closed the gap for thirty-odd actions at once. | |
| 56 | //! Worth knowing while reading any other module's account of a missing gate: | |
| 57 | //! the same paragraph was written in eight other files. | |
| 58 | //! | |
| 59 | //! What has not changed is that `secureClear` is the one action where the gate | |
| 60 | //! is load-bearing. The *ungated* sharedItems actions next door read and write | |
| 61 | //! the same items (one namespace — see above), so set/get/remove being gated | |
| 62 | //! grants a page nothing it could not reach anyway; a wipe has no such | |
| 63 | //! equivalent. | |
| 55 | 64 | //! |
| 56 | 65 | //! **`get`'s collapse of every failure into `null`.** Swift returns `nil` for |
| 57 | 66 | //! *any* non-success status, so a page cannot tell "nothing stored" from "the |
| @@ -56,13 +56,24 @@ | ||
| 56 | 56 | //! **The plist guard replaces the nil-recognizer guard.** Swift builds its |
| 57 | 57 | //! `SFSpeechRecognizer` only when `config.enableSpeechRecognition` is on |
| 58 | 58 | //! (`:439-440`), and `beginRecording`'s `guard let speechRecognizer` then |
| 59 | //! emits `"Speech recognizer unavailable"` when it is off. Zig cannot see the | |
| 60 | //! ivar, but `packages/ios/src/index.ts:183` writes | |
| 59 | //! emits `"Speech recognizer unavailable"` when it is off. Zig cannot see that | |
| 60 | //! ivar — still true — but `packages/ios/src/index.ts:183` writes | |
| 61 | 61 | //! `NSSpeechRecognitionUsageDescription` from that flag **and nothing else, |
| 62 | //! with no `||`** — the exact-proxy shape `bridge_mobile_motion.zig` already | |
| 62 | //! with no `||`** — the exact-proxy shape `bridge_mobile_motion.zig` also | |
| 63 | 63 | //! reads. So a missing key means the flag was off, and the message emitted is |
| 64 | 64 | //! the one Swift would have emitted for the same configuration. |
| 65 | 65 | //! |
| 66 | //! The flag itself is no longer out of reach: `ios_config.gateFor` maps | |
| 67 | //! `startListening` to `.speech_recognition`, so `ios_dispatch` answers | |
| 68 | //! `CAPABILITY_DISABLED` before this module is asked. That does **not** make | |
| 69 | //! the plist read redundant, and it is worth being precise about why, because | |
| 70 | //! the four other modules reading a key as a flag proxy *are* now redundant | |
| 71 | //! (#131) and this one looks identical at a glance. Here the key is also a | |
| 72 | //! precondition: `requestAuthorization:` without it does not fail, iOS | |
| 73 | //! terminates the process — so the check has to happen whatever the config | |
| 74 | //! says. The gate takes over the reason for refusing a disabled app; the key | |
| 75 | //! still owns not being killed, and the message. | |
| 76 | //! | |
| 66 | 77 | //! The check has to run *before* `requestAuthorization:`, where Swift's runs |
| 67 | 78 | //! after, because asking for speech authorization without the key does not |
| 68 | 79 | //! fail — iOS terminates the process. Order diverges; the page-visible |
| @@ -36,11 +36,13 @@ const is_darwin = builtin.target.os.tag.isDarwin(); | ||
| 36 | 36 | /// custom app schemes are its main use. Filtering here would silently break |
| 37 | 37 | /// them. |
| 38 | 38 | /// |
| 39 | /// **`share` is served unconditionally.** Swift gates it on | |
| 40 | /// `config.enableShare` and replies `CAPABILITY_DISABLED`; `ios.zig`'s | |
| 41 | /// `AppConfig` has no such field, so Zig cannot reproduce the gate. An app that | |
| 42 | /// set `enableShare: false` will now get a share sheet. Restoring the gate | |
| 43 | /// means adding the field to `AppConfig`, not adding a guess here. | |
| 39 | /// **`share` is gated, and not here.** Swift replies `CAPABILITY_DISABLED` | |
| 40 | /// when `config.enableShare` is off, and `ios_dispatch.offerToModules` sends | |
| 41 | /// the same code before this module is asked — `ios_config.gateFor("share")` | |
| 42 | /// maps it. This paragraph used to say the opposite, that `AppConfig` had no | |
| 43 | /// such field and an app setting `enableShare: false` would get a share sheet | |
| 44 | /// anyway; that was true until `ios_config.zig` read the flags out of | |
| 45 | /// `craft.config.json`, and false from then on with nothing to notice. | |
| 44 | 46 | pub const A = struct { |
| 45 | 47 | pub const open_url = "openURL"; |
| 46 | 48 | pub const share = "share"; |
| @@ -76,10 +76,13 @@ | ||
| 76 | 76 | //! `localizedDescription` are all logged before the enum is chosen. |
| 77 | 77 | //! |
| 78 | 78 | //! **The `enableWatchApp` config gate.** Swift only ever calls |
| 79 | //! `setupWatchConnectivity` under `if config.enableWatchApp`, and that flag has | |
| 80 | //! no Zig mirror — it appears nowhere in `packages/zig/src`. Both actions are | |
| 81 | //! therefore served unconditionally, exactly as `securestore` and `notifcancel` | |
| 82 | //! document for their own gates. This grants a page nothing it should not have: | |
| 79 | //! `setupWatchConnectivity` under `if config.enableWatchApp` — a setup-time | |
| 80 | //! gate rather than a per-action one, so no `case` arm here carries an | |
| 81 | //! `if config.` and `ios_config.gateFor` correctly maps none of these actions. | |
| 82 | //! Both are therefore served unconditionally, and this is the one module whose | |
| 83 | //! version of that sentence is still true: `securestore` and `notifcancel` said | |
| 84 | //! the same until `ios_config.zig` read the flags their arms *do* carry. | |
| 85 | //! Serving these ungated grants a page nothing it should not have: | |
| 83 | 86 | //! with the gate off Swift never activated a session, so `defaultSession`'s |
| 84 | 87 | //! `activationState` is `NotActivated` and both actions answer honestly — |
| 85 | 88 | //! `{"reachable":false}` and a refusal to update. |