fix: explain notification permission state instead of failing silently

requestAuthorization only shows the system prompt once ever; when
permission is already denied it silently returns false and the checkbox
just snapped off with no explanation. toggleNotify now checks the
authorization status first: already-authorized enables directly,
not-determined triggers the system prompt (errors surfaced in an
alert), and denied shows an alert with an Open System Settings button
pointing at the Notifications pane.

Also adds a UNUserNotificationCenterDelegate so banners are shown even
while BrewBar is the active app — otherwise macOS suppresses them,
which reads as "notifications don't work" right after enabling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
maxsoch 2026-07-07 06:00:45 +02:00
parent 25e4476317
commit fe469d9157
2 changed files with 62 additions and 3 deletions

View file

@ -47,6 +47,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_: Notification) {
Settings.registerDefaults()
UNUserNotificationCenter.current().delegate = self
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.imagePosition = .imageLeft // icon stays visible next to the count
@ -437,3 +438,14 @@ class AppDelegate: NSObject, NSApplicationDelegate {
logWindow?.update(text: logBuffer)
}
}
extension AppDelegate: UNUserNotificationCenterDelegate {
/// Without this, macOS suppresses banners while BrewBar is the active
/// app (e.g. right after toggling the setting with the window open).
func userNotificationCenter(_: UNUserNotificationCenter,
willPresent _: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void)
{
completionHandler([.banner])
}
}

View file

@ -246,15 +246,62 @@ class SettingsWindowController: NSWindowController {
return
}
UNUserNotificationCenter.current().requestAuthorization(options: [.alert]) { granted, _ in
// requestAuthorization only shows the system prompt the first time
// ever; if permission was denied before, it silently returns false.
// Check the status first so the user learns what actually happened.
let center = UNUserNotificationCenter.current()
center.getNotificationSettings { notificationSettings in
// callback arrives on a background queue; UI needs main
DispatchQueue.main.async {
switch notificationSettings.authorizationStatus {
case .authorized, .provisional:
Settings.notifyOnNewUpdates = true
case .notDetermined:
center.requestAuthorization(options: [.alert, .sound]) { granted, error in
DispatchQueue.main.async {
Settings.notifyOnNewUpdates = granted
sender.state = granted ? .on : .off
if let error {
self.showNotificationProblem(error.localizedDescription)
}
}
}
case .denied:
sender.state = .off
self.showNotificationsDenied()
@unknown default:
sender.state = .off
}
}
}
}
func showNotificationsDenied() {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Notifications are disabled for BrewBar"
alert.informativeText = "macOS has notifications turned off for BrewBar. Enable them in System Settings > Notifications > BrewBar, then flip this switch again."
alert.addButton(withTitle: "Open System Settings")
alert.addButton(withTitle: "Cancel")
if alert.runModal() == .alertFirstButtonReturn,
let url = URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension")
{
NSWorkspace.shared.open(url)
}
}
func showNotificationProblem(_ message: String) {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = "Could not enable notifications"
alert.informativeText = message
alert.runModal()
}
@objc func toggleShowCount(_ sender: NSButton) {
Settings.showCountInMenuBar = sender.state == .on
appDelegate?.updateMenuBarCount() // reflect immediately, no refetch needed