ReviewOS

also looking at this

craft-native/craft

feat(ios): scheduleNotification — the deferral that expired on its own, ratchet 26 → 25

#110
Merged glennmichael123 wants to merge feat/ios-schedule-notification into main
6 files +922 -37
10 days ago

bridge_mobile_notifications.zig left this action to the Swift shim and wrote down exactly why. The blocker was never a missing selector:

Both Swift failure paths are rejections, not results … ios_async can only resolve — deliverJson is the whole asynchronous reply surface. Resolving the string "denied" for a denied prompt would be textbook fabricated success … The fix is an ios_async.deliverError(ticket, code, message) sharing deliverJson's slot, dispatch_async_f hop, and request-id restoration.

commitdate
that comment written0a97def2026-08-31
deliverErrorCode lands, sharing exactly those three thingsabfbd8f2026-09-01

The comment described a limitation that stopped existing the next day, and the shim has been keeping an action Zig could serve. Worth re-reading the other deferrals for the same reason before assuming they still hold — that note is now in the ratchet's own history.

The chain

Authorization, then add: the spec's two stages in the spec's order. Content and request are built inside the authorization completion, where the spec builds them, so the autoreleased request never crosses a pool boundary and nothing needs retaining — which is what the original note expected to be the hard part. What survives is the identifier, owned heap memory parked on the slot and answered in stage two, because that is the value a page needs to cancel with later. Both stages take-and-clear, so a double-fired completion replies to nobody rather than twice.

A crash not reproduced

The spec passes delay / 1000 to UNTimeIntervalNotificationTrigger unguarded, and an interval of zero or less raises an uncatchable Objective-C exception — so {delay: 0} SIGABRTs the app today. A non-positive delay here produces a nil trigger, which is how UserNotifications spells "deliver now". bridge_notification.zig documents the same trap and answers it the same way.

Constants cited, not guessed

  • UNAuthorizationOptionAlert|Sound|Badge = 7 — UNUserNotificationCenter.h:23-25, independently the same value bridge_mobile_permissions.zig already resolved.
  • Calendar mask = 252 — NSCalendar.h:61-66 defines each unit as the matching kCFCalendarUnit*; CFCalendar.h:68-73 gives year 1<<2 through second 1<<7.

A sibling module once shipped a guessed HealthKit bit that named a different option and crashed the app, so both are pinned by a test that names its source.

Deliberate divergences

A missing notification object rejects. The spec's case has no else, so that call never reaches a callback and the page's promise hangs forever. Refusals carry typed codes rather than the spec's free text, matching every other migrated module.

Also in here

getPendingNotifications abandoned its slot and left the caller hanging when its reply would not shape — the same "ios_async can only resolve" reasoning, recorded in the same file. It rejects now.

Verification

Cold zig build test: 150/150 steps, 2486/2498 passed, 12 skipped, 0 failed. zig fmt --check clean. build-ios-all -Doptimize=ReleaseSafe produces all three archives.

6 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.