ReviewOS

craft-native/craft

The native-screen bridge has no badge API, so an all-native app cannot set its icon badge

#350
Closed glennmichael123 opened this 9 days ago ·
9 days ago

CraftApp.swift gives a page a badge setter — setBadgeCount, reached as craft.badge — which calls UNUserNotificationCenter.setBadgeCount and is careful not to request a badge-only authorization:

private func setBadgeCount(_ count: Int, callbackId: String?) {
    // Changing a badge must not ask for a badge-only authorization …
    UNUserNotificationCenter.current().setBadgeCount(count) { … }
}

CraftNativeActions.swift — the bridge a native screen uses — has no equivalent. Its Notifications module answers only cancel, cancelAll, pending and schedule, and is gated behind enableLocalNotifications besides. There is no badge module in the case ("…", _) list at all:

Auth, Background, Biometrics, Brightness, Browser, Clipboard, Database, DeepLinks,
Device, Dialog, Files, Haptics, Health, HeartRate, KeepAwake, Lifecycle, Linking,
Location, Music, Network, Notifications, Push, SecureStorage, Share, Speech,
StatusBar, Storage

Why this now matters more than it did. With renderer: "web" an app could always reach the page's bridge for this. With renderer: "native" there is no page, so the capability is unreachable from anywhere in the app.

What it costs. An app cannot show or clear its own unread badge. Today the only way a badge appears is inside an APNs payload, which means:

  • the badge cannot be set at launch from a count the app already knows
  • it cannot be cleared when the person reads those notifications somewhere else, or in the app itself — it stays until a later push happens to carry a lower number
  • an app with notifications turned off, or before push is configured, can never badge at all

What would be enough. A Badge module on the native bridge — set(count) and clear() — mapping to the same setBadgeCount the page path already uses, with the same care about not requesting a badge-only authorization. A badge capability flag alongside it would let an app declare it, as push and health already do.

More generally this looks like a parity gap rather than a design decision: the page bridge and the native bridge should offer the same capabilities, or the difference should be documented. Found in stacksjs/hq.training while trying to show an unread count on the app icon.

9 days ago

Resolved on main by d76653a (feat(ios): expose app badges to native screens). CraftNativeActions now exposes Badge.set and Badge.clear, reports the badge capability, and reuses the existing permission-free UNUserNotificationCenter.setBadgeCount path. Builder coverage is in packages/ios/src/index.test.ts.

9 days ago

Measured, and it is worse than "no API": the config keys for it exist on the Stacks side and reach nothing.

@stacksjs/mobile's CraftMobileCapabilities declares both badge?: boolean and appBadge?: boolean. Setting either (or both) in config/mobile.ts and running ./buddy build:ios produces a craft.config.json with no badge key at all — so nothing is passed to Craft, and Craft has no badge module to receive it either (CraftNativeActions.swift handles Auth, Background, Biometric, Brightness, Browser, Clipboard, Database, DeepLink, Device, Dialog, Files, Haptics, Health, HeartRate, KeepAwake, Lifecycle, Linking, Location, Music, Network, Notification, Push, SecureStorage, Share, Speech, StatusBar, Storage — and that is the list).

So an app author who wants an icon badge finds a capability flag with the right name, switches it on, and gets silence. Whatever the fix is, the dead keys should either start working or stop being offered.

Same shape as craft-native/craft#344, where enableWatchApp exists in Craft and no Stacks key reaches it.

9 days ago

Reopening: 0.0.173 added the Swift half, and a native screen still cannot reach it. Measured against 0.0.173 with a freshly generated bundle.

What now exists. CraftNativeActions.swift handles the module, and unusually it is not gated — badge is in the base capability list, so every app advertises it:

var values = ["device", "clipboard", "haptics", "storage", "lifecycle",
              "fetch", "dialog", "speech", "audio", "eventStream", "linking", "badge"]
…
case ("Badge", "set"):   // validates a non-negative whole number
case ("Badge", "clear"):

What is missing. The native-screen JS bridge never builds a craft.badge. It gates its objects on 33 capabilities —

appleSignIn audio background biometric brightness browser camera clipboard
database deepLinks device dialog eventStream fetch files geolocation haptics
health healthAvailability heartRate keepAwake lifecycle linking media music
notifications push scheduledWorkouts secureStorage share speech statusBar
storage

— and badge is not among them. "Badge","set" appears nowhere in the generated native-screens.js, and there is no generic escape hatch on the craft object to call a module the bridge does not know, so a screen cannot get at it even deliberately.

And the Stacks wrapper points at the wrong module. @stacksjs/mobile offers notifications.setBadge(count), which forwards to craft.notifications.setBadge(…). Craft's module is Badge, not Notifications, and craft.notifications is built only when enableLocalNotifications is on and carries just schedule/cancel/cancelAll/pending. So the one JS entry point that exists calls a method that is not there, on an object that usually is not either.

The web view is fine — CraftApp.swift routes setBadgeCount for it (lines 2149/2154 → 6315). So the capability works for an app with a web view and is unreachable for one with renderer: native, which is the case this issue was opened for.

So the remaining work is the JS side: badge in the bridge's capability list and a craft.badge object over Badge.set/Badge.clear, plus pointing @stacksjs/mobile's setBadge at it. hq.training wants this for the unread message and notification count (stacksjs/hq.training#271 decides which count it should be).

9 days ago

The Craft host side is shipped in v0.0.174: iOS advertises the permission-free badge capability and handles the native-screen Badge.set / Badge.clear actions.

This issue remains open because the shared STX runtime still reports badge: false and does not yet install the craft.badge API. That follow-up belongs in @stacksjs/stx; it should be implemented there and then validated against this host contract.

9 days ago

Still open on 0.0.173, and I can now show exactly where it breaks rather than inferring it. I also have to withdraw the "33 capabilities" line from my original report — that was wrong, and the real shape is more specific.

The Swift half is there and ungated. CraftNativeActions handles both cases with no config check:

case ("Badge", "set"):   … setBadgeCount(value.intValue) …
case ("Badge", "clear"): … setBadgeCount(0) …

And badge is advertised as a base capability — CraftNativeActions.capabilities(config:) line 43, in the unconditional list beside device, clipboard, haptics:

var values = ["device", "clipboard", "haptics", "storage", "lifecycle", "fetch",
              "dialog", "speech", "audio", "eventStream", "linking", "badge"]

Identical in 0.0.175.

But the bridge object a native screen receives has no badge key. Probed from a native screen on an iPhone 17 Pro, craft-native 0.0.173:

keys=app,appearance,audio,background,biometrics,browser,cancelAllNotifications,
cancelNotification,capabilities,capabilityProtocolVersion,clipboard,db,deepLinks,
device,dialog,files,getAppState,getBrightness,getPendingNotifications,haptic,
haptics,health,heartRate,lifecycle,linking,location,media,music,navigation,
notifications,onAppStateChange,openURL,permissions,platform,push,registerPush,
route,scheduleNotification,scheduledWorkouts,secureStorage,setBrightness,
setKeepAwake,share,signInWithApple,snapshots,speech,statusBar,storage

craft.badge = undefined            craft.badge.set = undefined
craft.notifications = object       craft.notifications.setBadge = undefined

46 keys, no badge. So the capability is advertised and the Swift is reachable in principle, but nothing on the JS side ever dispatches {module: "Badge", method: "set"}.

The other door is shut too. @stacksjs/mobile offers notifications.setBadge(count), which sends Notifications.setBadge — and craft's notifications(...) handler has only cancel, cancelAll, pending, schedule. There is no setBadge case, and ("Notifications", _) is additionally gated on enableLocalNotifications.

So on an all-native app there is no path to the icon badge at all: the capability says yes, the bridge has no module, and the SDK's only badge call names a module that cannot serve it.

For context on why this matters here: hq.training's header already carries unread counts (badge: on its rightButtons), so the in-app counts are right — it is only the springboard icon that cannot be set.

Happy to test a build. I cannot take 0.0.174+ yet, as it does not compile (#354).

9 days ago

Status check on current main: the Craft iOS host half remains shipped (d76653af and current CraftNativeActions.swift), including the unconditional badge capability and Badge.set / Badge.clear. The remaining missing piece is still outside this repository: the native STX runtime must install craft.badge and dispatch the module. I am not closing this from Craft while that shared-runtime follow-up is open.

9 days ago

The remaining shared-runtime seam is now fixed in stacksjs/stx main as dbe0a0f92:

  • capabilities.badge now reflects the host-advertised badge capability instead of always being false.
  • craft.badge.set(count) and craft.badge.clear() dispatch the existing Badge.set / Badge.clear protocol actions, only when the host advertises the capability.
  • Added protocol coverage for the capability-gated API and request payloads; 19 native capability tests pass and STX typecheck passes.

Craft’s iOS host implementation was already on main; this closes the shared STX bridge half. Android remains correctly unavailable because it does not advertise badge.

9 days ago

Follow-up: Craft main now pins the shared Mobile E2E compiler to STX 1a89af00b1db3c75948d826b4fab4234f236731f in ec37e35f. The badge bridge fix is now exercised by the maintained native compiler path; the new Craft CI/Mobile E2E runs are pending.

9 days ago

Follow-up on current main: the shared bridge fix is now in STX 4172cdfc7dd1d52a680223e5a28dc1c161c2ce67 (building on dbe0a0f92), and Craft main pins that compiler in 6d33ee82.

The local native capability and Craft contract suites are green. I am leaving #350 open until the queued CI and Mobile E2E runs for 6d33ee82 complete, because those are the authoritative cross-repository validation for the badge protocol.

9 days ago

Follow-up on the current main pin: STX browser-safe Link fallback is shipped in d940e23, and Craft mobile E2E now pins that exact compiler in 2af349a. The local STX native suite is 676 passed / 0 failed and typecheck is green. Exact-head Craft CI, Mobile E2E, Benchmark, and Binary Size workflows are queued or running; I am leaving #350 open until those authoritative workflows complete.

9 days ago

Follow-up on the layout-measurement pin: STX 27d647e485 now reports browser-peer onLayout frames through a guarded ResizeObserver, and Craft main pins it in 838290de. The full STX native suite is 677 passed / 0 failed (4,003 assertions); Craft contract/release checks remain 330 passed / 0 failed. Exact-head Craft CI, Mobile E2E, Benchmark, and SBOM/Binary Size workflows are running for 838290de; leaving this open until they complete.

9 days ago

The exact pin advanced once more: STX parent-observation fix is 4e95d61, Craft main is 9a330b9. Full STX native verification is 677 passed / 0 failed (4,004 assertions); Craft contract/release checks are 330 passed / 0 failed. Exact-head workflows are still running, so #350 stays open.

9 days ago

Final pin for this renderer slice: STX safe-area support is 584c9f6, Craft main is 1bb2be1. Full STX native verification is 678 passed / 0 failed (4,009 assertions); Craft contract/release checks are 330 passed / 0 failed. Exact-head workflows are running; #350 remains open until all authoritative gates complete.

9 days ago

Final shared-runtime pin: STX safe-area shorthand support is 19797f2, Craft main is da92950. Full STX native verification is 678 passed / 0 failed (4,009 assertions); Craft contract/release checks are 330 passed / 0 failed. Exact-head Craft workflows are still running; #350 remains open.

9 days ago

The exact shared compiler pin advanced again for the native component parity slice.

  • STX 4a517f6071 adds typed VideoProps.currentTime and compiler prop preservation.
  • Craft a3704a44 pins that compiler; Craft host support is in e39dbd57 for iOS/Android controlled seeks.
  • Local STX render-screen test: 37 passed / 0 failed; Craft contract + release suites: 330 passed / 0 failed.

The exact-head CI and Mobile E2E runs for a3704a44 are now the authoritative gates; #350 remains open until they finish.

9 days ago

The shared compiler pin advanced again for the video lifecycle parity slice. Craft 7720e79c pins STX fb61834822, which adds typed native Video onLoadStart/onLoadEnd events. Local STX native suite is now 679 passed / 0 failed (4,010 assertions); Craft contract + release suites are 330 passed / 0 failed. Exact-head workflows for 7720e79c are running; #350 remains open until the authoritative CI and Mobile E2E gates finish.

9 days ago

The shared native parity pin advanced again for the video contract slice:

  • STX c4ad8455a4033a34c189f1472fdbf6afc90553d8 types both autoplay and the host-supported autoPlay alias.
  • Craft main d28d175e pins that exact compiler across mobile E2E, release validation, and the Native STX guide.
  • STX native boundary: 58 passing / 0 failing, 279 assertions.
  • Craft focused contract/release suite: 330 passing / 0 failing, 3,741 assertions.

The exact-head Craft CI, Mobile E2E, Benchmark, SBOM, and Binary Size runs are queued or running; #350 remains open until those authoritative gates complete.

9 days ago

The current shared-runtime pin now includes the badge bridge and is on Craft main:

  • STX 6cb964cd3f054b0bd5ebf0f06ef4598ba9db0a02 retains capabilities.badge plus typed craft.badge.set/clear.
  • Craft 6ab4cbe6 pins that exact compiler revision across Mobile E2E, release validation, and the Native STX guide.
  • Full STX native verification is 704 passed / 0 failed (4,102 assertions); Craft contract/release verification is 330 passed / 0 failed.

The latest exact-head Craft CI, Mobile E2E, Benchmark, SBOM, and Binary Size runs for 6ab4cbe6 are queued/running; I am leaving the issue open until those authoritative gates finish.

9 days ago

Resolved on main. Native-screen Badge.set/clear and the permission-free setBadgeCount path shipped in d76653a, with generator and native-render tests in f684cc3/41a252b.\n\nThe bridge advertises the badge capability and does not request notification authorization.

Sign in to comment on this issue.