ReviewOS

stacks/stx

native: craft.permissions.check answers "undetermined" for everything but notifications, and overrides any host implementation

#2115
Closed glennmichael123 opened this 8 days ago ·
8 days ago

craft.permissions.check and .request are typed, callable, never throw, and answer "undetermined" for everything but notifications.

In dist/native/runtime/craft-bridge.js (0.2.663):

craft.permissions = Object.assign({}, craft.permissions, {
  check: permission => permission === 'notifications' ? craft.push.getPermissionStatus() : Promise.resolve('undetermined'),
  request: permission => permission === 'notifications' ? craft.push.requestPermission() : Promise.resolve('undetermined'),
  openSettings: () => craft.push.openSettings().then(() => {}),
})

Three separate problems, in order of how much they cost:

1. It answers confidently and wrongly. A screen asking await craft.permissions.check('location') gets 'undetermined' whether location is granted, denied or never asked. Nothing throws and nothing warns, so the only way to find out is to put a build on a device and watch it behave as though a granted permission were not.

This is worse than a missing API. A missing one fails loudly at the call site; this one returns a plausible value, and 'undetermined' is the single answer most likely to send a screen down an "ask the user" path that then does nothing.

2. Object.assign means it overrides a real implementation rather than standing in for one. The stub is the last argument, so it wins over anything already on craft.permissions. If a host grows a Permissions module tomorrow, this line silently discards it — the shim outlives the gap it was filling.

3. It only exists under the push capability. The whole block sits inside if (capabilities.has('push')), so an app that does not declare push has no craft.permissions at all, and one that does gets the stub for location, camera and the rest. Neither is a property of permissions.

Why this is hard to find from the outside

iOS does not refuse startUpdatingLocation when location is denied. It accepts the call and then delivers nothing. So a recording screen that asked permissions.check('location'), believed 'undetermined', and started anyway got active: true back from the host and sat on a running clock with a distance stuck at zero — a screen that looks like it is working. Measured on a device: forty-three seconds of "moving", not one sample.

We have since had to write "do not trust this" into two screens' comments, and the real check is now "did any fix arrive", which is a behavioural proxy for a question the API claims to answer.

What would help, in preference order

  1. Implement it against a host Permissions module. craft-native has none either (no "Permissions" case in CraftNativeActions.swift as of 0.0.293), so this is two halves of one gap; happy to file the other half there if useful.
  2. Failing that, reject for permissions the bridge cannot answer, so a caller finds out at the call site rather than on a device.
  3. At minimum, stop the Object.assign from clobbering a host implementation, and move the block out from under the push capability.

A craft.capabilities()-style answer for "can this host answer permission questions at all" would also let a screen branch honestly instead of guessing.

Versions

stx 0.2.663, craft-native 0.0.293, iOS 26.5 simulator and device.

8 days ago

Fixed on main in 7e7edac (fix(native): preserve host permissions and reject unsupported checks).

Version status: unreleased after STX v0.2.674. Published v0.2.674 does not include this fix.

  • Existing host permission objects/methods are retained, including prototype methods, their receiver, original return values and errors. Only missing methods or STX's own previous defaults are filled/refreshed.
  • The fallback API is installed independently of the push capability. Unsupported check / request calls reject with NativeCapabilityError code PERMISSION_UNSUPPORTED, naming the operation/permission. They no longer return a guessed undetermined or issue an unimplemented native Permissions request.
  • Notification check/request/settings keep their existing Push bridge behavior when push is advertised. Real granted/denied/undetermined answers and native failures are preserved. Missing push rejects the notification/settings fallback too.
  • Reinstallation refreshes STX defaults for the new capabilities without overwriting a later host replacement.

Regression: permissions.test.ts. Tests cover push/no-push, class prototypes and receivers, complete frozen host APIs, partial host APIs, original host errors, notification responses/errors, reinstalls, and normal/minified native bundles. Injecting the original bridge makes seven direct regression checks fail; the fixed focused cohort passes 51 tests.

Verification: 993 native tests passed / 0 failed; release preflight passed, including build/typecheck/export validation/installed-package smoke/performance budgets and 319 targeted tests. Repository lint passed with existing warnings, formatting passed. CI for the pushed commit is separate and has just been triggered.

Closing under the requested STX-only fallback fix. No Craft code was changed. This does not implement a host Permissions module or claim location/camera/microphone permission support; those checks require a real host implementation and otherwise reject honestly.

Sign in to comment on this issue.