From 6f4489cac298c27797bb6f337882794b154a7585 Mon Sep 17 00:00:00 2001 From: lonewolfyx Date: Thu, 16 Jul 2026 09:41:32 +0800 Subject: [PATCH] fix: guard app update notifications when running outside an app bundle When the binary is run outside a `.app` bundle (e.g. `swift run` / CLI / tests), `UNUserNotificationCenter.current()` is not a valid host context, and instantiating it plus setting its delegate can throw/crash before the app is even usable. Treat the notification center as optional and no-op the service when not running from an app bundle. - AppUpdateNotificationService: type `notificationCenter` as optional (`UNUserNotificationCenter?`); assign it only when `isRunningFromAppBundle` is true, otherwise `nil`, and switch the delegate assignment to optional chaining - `removeNotificationIfInstalled` / `notifyUpdateReady`: early-return when `notificationCenter` is nil so non-bundle runs skip notification work - `requestAuthorizationAndNotify` / `deliverUpdateReadyNotification`: on a nil center, finalize the in-flight attempt as failed instead of dereferencing - Add `isRunningFromAppBundle` helper checking `Bundle.main.bundleURL.pathExtension == "app"` Co-Authored-By: Claude --- .../AppUpdateNotificationService.swift | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Sources/AgentBar/Services/AppUpdateNotificationService.swift b/Sources/AgentBar/Services/AppUpdateNotificationService.swift index d50ec7b..e42c3e1 100644 --- a/Sources/AgentBar/Services/AppUpdateNotificationService.swift +++ b/Sources/AgentBar/Services/AppUpdateNotificationService.swift @@ -8,21 +8,26 @@ final class AppUpdateNotificationService: NSObject, UNUserNotificationCenterDele private static let notificationThreadIdentifier = "com.lonewolfyx.AgentBar.app-update" private static let lastNotifiedBuildVersionKey = "AppUpdateLastNotifiedBuildVersion" - private let notificationCenter: UNUserNotificationCenter + private let notificationCenter: UNUserNotificationCenter? private let userDefaults: UserDefaults private let stateLock = NSLock() private var inFlightBuildVersion: String? override init() { - let notificationCenter = UNUserNotificationCenter.current() + let notificationCenter = Self.isRunningFromAppBundle + ? UNUserNotificationCenter.current() + : nil self.notificationCenter = notificationCenter userDefaults = .standard super.init() - notificationCenter.delegate = self + notificationCenter?.delegate = self } func removeNotificationIfInstalled(buildVersion: String) { - guard lastNotifiedBuildVersion() == buildVersion else { + guard + let notificationCenter, + lastNotifiedBuildVersion() == buildVersion + else { return } @@ -31,7 +36,10 @@ final class AppUpdateNotificationService: NSObject, UNUserNotificationCenterDele } func notifyUpdateReady(displayVersion: String, buildVersion: String) { - guard beginNotificationAttempt(buildVersion: buildVersion) else { + guard + let notificationCenter, + beginNotificationAttempt(buildVersion: buildVersion) + else { return } @@ -60,6 +68,11 @@ final class AppUpdateNotificationService: NSObject, UNUserNotificationCenterDele } private func requestAuthorizationAndNotify(displayVersion: String, buildVersion: String) { + guard let notificationCenter else { + finishNotificationAttempt(buildVersion: buildVersion, succeeded: false) + return + } + notificationCenter.requestAuthorization(options: [.alert, .sound]) { [weak self] granted, _ in guard let self else { return @@ -78,6 +91,11 @@ final class AppUpdateNotificationService: NSObject, UNUserNotificationCenterDele } private func deliverUpdateReadyNotification(displayVersion: String, buildVersion: String) { + guard let notificationCenter else { + finishNotificationAttempt(buildVersion: buildVersion, succeeded: false) + return + } + let content = UNMutableNotificationContent() content.title = I18n.current.appUpdateReadyTitle content.body = I18n.current.appUpdateReadyMessage(version: displayVersion) @@ -138,6 +156,10 @@ final class AppUpdateNotificationService: NSObject, UNUserNotificationCenterDele return userDefaults.string(forKey: Self.lastNotifiedBuildVersionKey) } + private static var isRunningFromAppBundle: Bool { + Bundle.main.bundleURL.pathExtension.caseInsensitiveCompare("app") == .orderedSame + } + func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification,