Skip to content

Wait for FCM token before replying to push enable, add ORNotificationHandler - #40

Merged
MartinaeyNL merged 2 commits into
mainfrom
fix/push-token-late-upload
Oct 14, 2026
Merged

MartinaeyNL merged 2 commits into
mainfrom
fix/push-token-late-upload

Conversation

@Miggets7

@Miggets7 Miggets7 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21. Refs #39 (first-launch race only, see Behaviour notes). Root cause of openremote/ourgrid#227.

Problem

On a fresh iOS install the push provider answered PROVIDER_ENABLE with the FCM token cached in UserDefaults. Firebase had not delivered one yet, so the console registered without a token and push only worked after an app restart.

Changes

  • PushNotificationProvider.enable() now does what the Android console does: it registers for remote notifications right away and waits for the token before replying. The permission prompt runs in parallel, so a slow tap on the prompt no longer counts against the web console's 5 s provider timeout. The wait is capped at 4.5 s.
  • New ORNotificationHandler takes over the notification and Firebase token handling every app duplicated in its AppDelegate. Apps forward three callbacks to it (snippet in the README). It stores the token and wakes a waiting push provider.
  • The handler finds the console view controller through the connected scenes, so "open in app" from a notification now works in scene based apps such as OurGrid.
  • ORLib still has no Firebase dependency.
  • README lists the Firebase setup the app must do: FirebaseApp.configure() and the messaging delegate on every launch path, and forwarding apnsToken when swizzling is off.

Behaviour notes

  • A rotated token is stored and reaches the backend the next time the console enables push, which iOS does on every page load. Android behaves the same.
  • If Firebase needs more than 4.5 s, the reply goes out without a token and the next launch sends it. Closing that gap would need a change in console.ts.
  • When no token is cached (first launch), the enable reply can take up to 4.5 s. console.ts enables providers one after another, so the geofence enable and the registration POST wait as well. Android waits on getToken() with no cap, so a reply after 5 s is dropped by console.ts and no registration is sent. The 4.5 s cap avoids that on iOS.
  • If the token arrives before the user answers the permission prompt, the reply carries hasPermission: true. Android always reports true, and the backend does not read the field.
  • When the user denies the first prompt, the token is still registered. Enabling notifications later in iOS Settings then works without a new registration. The .denied branch still skips remote registration.
  • ORViewcontroller holds the enable callback weakly, so the wait doesn't keep it alive.

Testing

  • New suites PushNotificationProviderTest and ORNotificationHandlerTest. Full suite passes on the simulator (118 tests).
  • Tested end to end with OurGrid on a device against this branch.

Follow-up

A release is needed after merge. openremote/console-ios-app and openremote/ourgrid PRs bump to it.

#39 stays open: tokens that arrive after 4.5 s, and rotated tokens, only reach the backend on the next page load. Closing that needs console.ts to accept a token update from the provider.

return Int64(notificationIdString)
}

// TODO: geofence provider should also be adapted to store "per project"
@Miggets7 Miggets7 self-assigned this Oct 12, 2026
@Miggets7 Miggets7 added the Bug Error or issue in the system label Oct 12, 2026

@MartinaeyNL MartinaeyNL left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial AI assisted Review

Verdict: the approach is right — waiting in enable() is the only hook ORLib owns (console.ts
drops late replies and has no token-update channel), and ORNotificationHandler as a forwarding
helper matches #21.

Before merge

  1. hasPermission: granted ?? true (PushNotificationProvider.swift:122) is an optimistic
    guess when the token beats the user's tap. Registering for remote notifications before
    requestAuthorization makes this more likely. Wait for both signals within the budget (or
    document the trade-off in "Behaviour notes").
  2. README prerequisites are incomplete — the fix only works if the app also forwards the APNs
    token (Messaging.messaging().apnsToken = deviceToken) and configures Firebase on every launch
    path (console-ios-app#46 does both). Same gap in the ORNotificationHandler doc comment.
  3. Fixes #39 is partial — first-launch race is fixed (≤4.5 s); late (>4.5 s) and rotated
    tokens still only reach the backend on the next page load. Keep #39 open or file a follow-up for
    the console.ts change.

Behaviour changes / side effects

  1. PROVIDER_ENABLE now takes up to 4.5 s when no cached token (every first enable per launch,
    and any launch where the cache is empty, e.g. changed app group). console.ts awaits providers
    sequentially, so this also delays the geofence enable, the registration POST and the app's
    enableComplete callback.
  2. Remote registration now happens even when permission is denied → backend can hold tokens for
    devices that can't show notifications (delivery/ack stats get holes). Upside: enabling later in
    iOS Settings works without re-registering.
  3. No early exit when registration fails — nothing observes
    didFailToRegisterForRemoteNotifications; a device without APNs/Firebase (incl. simulator)
    always burns the full 4.5 s then replies token: null. Consider forwarding that failure to the
    handler.
  4. Pending callback retains ORViewcontroller up to 4.5 s (ORViewController wires
    self.sendData strongly) — was ~instant before; [weak self] would fix.
  5. console.ts "Message already pending" collision window grew from ms to 4.5 s — manual
    enableProvider("push") during auto-enable now throws easily.
  6. API break for subclasses: init() became convenience init() and the designated init is
    internal — external subclasses of PushNotificationProvider are no longer writable. Release
    notes should mention it.

Smaller items

  1. The 4.5 s ↔ console.ts 5000 ms coupling is an undocumented cross-repo constant — comment both
    ends, consider making it tunable.
  2. didReceiveRegistrationToken(nil) doesn't clear the stored token (deleteToken case) — stale
    token is re-sent.
  3. activeORViewController() only walks presentedViewController (no child containment → nav/tab
    hosts silently no-op) and is internal; apps still using window?.topController (e.g. geofence
    deep links) can't reuse it. Make it public, consider a containment walk.
  4. enable() itself is untested (UNUserNotificationCenter not injectable) — the new
    permission/hasPermission branches are only covered via waitForToken. Small injection seam
    would fix.

@Miggets7
Miggets7 force-pushed the fix/push-token-late-upload branch 2 times, most recently from 2f6f09c to fc543c5 Compare October 13, 2026 12:01
…ionHandler

On a fresh install the push provider replied to PROVIDER_ENABLE with the
cached FCM token, which Firebase had not delivered yet. The console
registration then had no token and push notifications only worked after
an app restart (#39).

Like the Android console, the provider now registers for remote
notifications straight away and waits for the token before replying.
The permission prompt is shown in parallel, so its duration does not
count against the web console's 5 second provider timeout. The wait is
capped at 4.5 seconds. A token that arrives later, or a rotated token,
is sent the next time the console enables push, which happens on every
page load.

ORNotificationHandler moves the notification and FCM token handling
that every console app duplicated into ORLib (#21). Apps forward their
UNUserNotificationCenterDelegate and MessagingDelegate callbacks to it;
it stores the token and notifies a waiting push provider. It finds the
console view controller through the connected scenes, which also makes
"open in app" work for scene based apps.
@Miggets7
Miggets7 force-pushed the fix/push-token-late-upload branch from fc543c5 to de6084d Compare October 13, 2026 12:12
@Miggets7
Miggets7 requested a review from MartinaeyNL October 13, 2026 12:16
@MartinaeyNL

Copy link
Copy Markdown
Member

Did a thorough review to understand what was going on here, because I'd like to prevent any unwanted bugs.
It was helpful getting the code explained to me, especially since I am not a Swift expert haha
Happy with the changes overall.

@MartinaeyNL
MartinaeyNL merged commit daff0b8 into main Oct 14, 2026
2 checks passed
@MartinaeyNL
MartinaeyNL deleted the fix/push-token-late-upload branch October 14, 2026 14:54
@Miggets7 Miggets7 mentioned this pull request Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Error or issue in the system

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move UNUserNotificationCenterDelegate and MessagingDelegate boilerplate into ORLib

3 participants