From 44e598042a9188e45c1a251275b3f7024526a2c6 Mon Sep 17 00:00:00 2001 From: Sherkhan Azimov Date: Tue, 21 Apr 2026 16:14:22 +0100 Subject: [PATCH] fix(notifications): guard UN calls when running unbundled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `UNUserNotificationCenter` dereferences `NSBundle.mainBundle` and throws `NSInternalInconsistencyException: bundleProxyForCurrentProcess is nil` when the executable is launched directly (e.g. `pnpm run tauri dev`, which runs `target/debug/meetily` rather than the bundled `.app`). Since commits #32/#33 migrated macOS notifications from the legacy `NSUserNotification` path to UN, every `tauri dev` run crashes at startup. Skip the UN calls when `current_exe()` has no `.app` ancestor. `request_authorization` returns `Err` (not `Ok(false)`) so `manager.rs` falls into its existing `Err` arm and does not persist `system_permission_granted = false` — the dev run and the bundled `.app` share `~/Library/Application Support/com.meetily.ai/`, so persisting `false` would silently suppress every real notification until the user re-granted consent. `show()` returns `Ok(())`; callers do not store state on success. Bundled `.app` behavior is unchanged. --- .../src-tauri/src/notifications/macos_un.rs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/frontend/src-tauri/src/notifications/macos_un.rs b/frontend/src-tauri/src/notifications/macos_un.rs index 332f576bcc..6b956f27cf 100644 --- a/frontend/src-tauri/src/notifications/macos_un.rs +++ b/frontend/src-tauri/src/notifications/macos_un.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Result}; use block2::RcBlock; -use log::{error as log_error, info as log_info}; +use log::{error as log_error, info as log_info, warn as log_warn}; use objc2::rc::Retained; use objc2::runtime::ProtocolObject; use objc2::{define_class, msg_send, AllocAnyThread}; @@ -25,6 +25,22 @@ use uuid::Uuid; use crate::notifications::types::{Notification, NotificationPriority}; +/// Returns true if the current process is running inside a macOS `.app` bundle. +/// +/// `UNUserNotificationCenter` dereferences `NSBundle.mainBundle` and throws +/// `NSInternalInconsistencyException: bundleProxyForCurrentProcess is nil` when the executable +/// was launched directly (e.g. `pnpm run tauri dev`, which runs the raw binary at +/// `target/debug/meetily` rather than the bundled `.app`). We short-circuit every UN entry +/// point in that case so dev runs don't crash at startup. +fn is_running_in_app_bundle() -> bool { + static IN_BUNDLE: OnceCell = OnceCell::new(); + *IN_BUNDLE.get_or_init(|| { + let Ok(path) = std::env::current_exe() else { return false }; + path.ancestors() + .any(|ancestor| ancestor.extension().and_then(|ext| ext.to_str()) == Some("app")) + }) +} + define_class!( // SAFETY: `NSObject` is a valid Objective-C superclass with no subclassing constraints // (no required inits, no `dealloc` hooks to preserve). We add no ivars (unit type) and @@ -95,6 +111,14 @@ fn install_delegate_if_needed() { /// Request notification authorization. Idempotent; if the user has already granted (or denied), /// macOS returns the stored decision without re-prompting. pub async fn request_authorization() -> Result { + // Report unavailability as `Err` (not `Ok(false)`) so the caller in `manager.rs` falls into + // its `Err` arm and does *not* persist `system_permission_granted = false`. A `tauri dev` + // run shares its config directory with the bundled `.app` — persisting `false` here would + // silently suppress every real notification until the user manually re-granted consent. + if !is_running_in_app_bundle() { + log_warn!("UN authorization skipped: not running inside a .app bundle (likely `tauri dev`)"); + return Err(anyhow!("UN unavailable: process is not running inside a .app bundle")); + } install_delegate_if_needed(); // Scope the ObjC handles (not Send) so they drop before we await. @@ -133,6 +157,10 @@ pub async fn request_authorization() -> Result { /// Present a notification via `UNUserNotificationCenter`. pub async fn show(notification: &Notification) -> Result<()> { + if !is_running_in_app_bundle() { + log_warn!("UN present skipped (no .app bundle): id={:?}", notification.id); + return Ok(()); + } install_delegate_if_needed(); let id_str = notification