Skip to content

fix(notifications): switch to UNUserNotificationCenter so banners pop#23

Merged
mrdulasolutions merged 1 commit into
mainfrom
claude/un-notifications
May 14, 2026
Merged

fix(notifications): switch to UNUserNotificationCenter so banners pop#23
mrdulasolutions merged 1 commit into
mainfrom
claude/un-notifications

Conversation

@mrdulasolutions
Copy link
Copy Markdown
Owner

What

Swap the macOS notification path off tauri-plugin-notification and onto our own Rust commands that talk to UNUserNotificationCenter directly.

Why

tauri-plugin-notificationnotify-rustmac-notification-sys still call NSUserNotificationCenter, which Apple deprecated in macOS 10.14. On Sequoia (and progressively on earlier modern macOS) the deprecated API delivers notifications to Notification Center but no longer pops a banner — Apple's slow-roll deprecation behavior. Every native Mac app a user can name (Mail, Slack, Things, Linear, Spark…) uses UNUserNotificationCenter; ours was the odd one out.

The user-visible symptom: notifications fire, show up in Notification Center on the top right, but never pop as a banner — even with System Settings → Notifications → AOS Mail set to Alert Style: Temporary, "Allow notifications" on, Desktop ✓.

No upstream plugin migration is needed for us to fix this — we drive UN ourselves.

How

src-tauri/Cargo.toml         + objc2 / objc2-foundation /
                                 objc2-user-notifications / block2
                             - tauri-plugin-notification

src-tauri/src/
  native_notifications.rs    new: thin wrapper over UN — request auth,
                                  query state, send. Uses block2
                                  RcBlock for completion handlers,
                                  mpsc::channel to make the async
                                  callbacks block the (Tauri-worker)
                                  calling thread.
  lib.rs                     + three #[tauri::command]s:
                                 notify_request_permission
                                 notify_permission_state
                                 notify_send
                               - tauri_plugin_notification::init()

src-tauri/capabilities/      - notification:default (plugin gone)

src/renderer/services/
  notifications.ts           rewritten with the same public surface
                             (initNotifications / notifyNewEmails /
                              testNotification), now calls our
                             commands via @tauri-apps/api/core's
                             invoke() instead of the plugin.

components/SetupWizard.tsx,  consume UN's state words ("authorized" /
components/SettingsPanel.tsx  "provisional" / "denied" /
                              "not_determined" / "ephemeral") instead
                              of the plugin's "granted"/"denied"/"default".

package.json                 - @tauri-apps/plugin-notification

The Rust side bridges UN's Objective-C completion handlers via block2::RcBlock and a std::sync::mpsc::channel so each #[tauri::command] looks like a normal synchronous call from JS (Tauri runs sync commands on a worker thread, so the blocking rx.recv() doesn't stall anything).

Deferred to a follow-up

  • UNUserNotificationCenterDelegate — needs a custom Objective-C class registered at startup so we can implement willPresentNotification: (foreground banners) and didReceiveNotificationResponse: (route clicks to a specific thread). Without it, background banners pop — which is the user's actual complaint here — but foreground notifications go silently to Notification Center, and clicks just bring the app forward instead of jumping to the email.
  • Rich content (subtitle, threadIdentifier, attachments, sound)
  • Cross-platform support — AOS Mail is macOS-only today, so the new commands no-op on other platforms.

Verification plan

I can't fully test banners from the dev machine (UN refuses to deliver from unsigned dev builds). Verification happens on the v0.1.10 release:

  1. CI green on this PR
  2. Merge, tag v0.1.10
  3. User installs v0.1.10, walks through Settings → Notifications → "Test"
  4. A banner pops on screen instead of the notification going silently to NC

If banners still don't pop after this lands, the next thing to check is whether UN is granting permission at all (we should see notify_permission_state return "authorized"); failure modes from here are narrow and easy to chase.

Test plan

  • CI's Lint & Format / Type Check / Security Audit all pass
  • After release: banner pops on Settings → Test Notification
  • After release: banner pops on a real incoming email when the app is backgrounded
  • After release: clicking the banner brings AOS Mail forward (default macOS behavior; no delegate yet)

🤖 Generated with Claude Code

tauri-plugin-notification → notify-rust → mac-notification-sys all call
NSUserNotificationCenter, deprecated since macOS 10.14. On Sequoia (and
progressively on earlier modern macOS) the deprecated API still delivers
notifications to Notification Center but no longer pops a banner —
Apple's slow-roll deprecation. Other native apps (Mail, Slack, Things,
Linear) all use UNUserNotificationCenter; ours was the odd one out.

This swaps the macOS notification path to UNUserNotificationCenter via
the objc2-user-notifications crate. No upstream plugin migration is
needed; we drive UN ourselves.

  src-tauri/Cargo.toml         + objc2 / objc2-foundation /
                                 objc2-user-notifications / block2
                               - tauri-plugin-notification
  src-tauri/src/
    native_notifications.rs    new: thin wrapper over UN — request auth,
                                    query state, send. Uses block2
                                    RcBlock for completion handlers,
                                    mpsc::channel to make the async
                                    callbacks block the (Tauri-worker)
                                    calling thread.
    lib.rs                     + three #[tauri::command]s:
                                   notify_request_permission
                                   notify_permission_state
                                   notify_send
                                 - tauri_plugin_notification::init()
  src-tauri/capabilities/      - notification:default (plugin gone)

  src/renderer/services/
    notifications.ts           rewritten: same public surface
                               (initNotifications / notifyNewEmails /
                                testNotification), now calls our
                               commands via @tauri-apps/api/core's
                               invoke() instead of the plugin.

  components/SetupWizard.tsx,  use the new state words ("authorized" /
  components/SettingsPanel.tsx "provisional" / "denied" / "not_
                               determined" / "ephemeral") that UN
                               returns instead of the plugin's
                               "granted"/"denied"/"default" subset.

  package.json                 - @tauri-apps/plugin-notification

Deferred (follow-up):
  - UNUserNotificationCenterDelegate for willPresent (so foreground
    notifications pop a banner instead of going silently to NC) and
    didReceiveResponse (so clicks route to a specific thread). The
    delegate needs a custom Objective-C class registered at startup —
    larger change than the v1 fix. Without it, background banners pop
    (the user's actual complaint), foreground notifications go to NC
    silently, clicks just bring the app forward.
  - Rich content (subtitle, attachments, threadIdentifier, sound).
  - Cross-platform path. AOS Mail is macOS-only today; the new
    commands no-op on other platforms.

  Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@mrdulasolutions mrdulasolutions merged commit 3970342 into main May 14, 2026
3 checks passed
@mrdulasolutions
Copy link
Copy Markdown
Owner Author

Roll back checkpoint 1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant