diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index de12c6e..73589d3 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -42,17 +42,16 @@ struct DesktopApp: App { @MainActor class AppDelegate: NSObject, NSApplicationDelegate { private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app-delegate") - private var menuBar: MenuBarController? + var menuBar: MenuBarController? let vpn: CoderVPNService let state: AppState let fileSyncDaemon: MutagenDaemon let urlHandler: URLHandler - let notifDelegate: NotifDelegate let helper: HelperService let autoUpdater: UpdaterService override init() { - notifDelegate = NotifDelegate() + AppDelegate.registerNotificationCategories() vpn = CoderVPNService() helper = HelperService() autoUpdater = UpdaterService() @@ -79,8 +78,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { } self.fileSyncDaemon = fileSyncDaemon urlHandler = URLHandler(state: state, vpn: vpn) + super.init() // `delegate` is weak - UNUserNotificationCenter.current().delegate = notifDelegate + UNUserNotificationCenter.current().delegate = self } func applicationDidFinishLaunching(_: Notification) { @@ -161,7 +161,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { do { try urlHandler.handle(url) } catch let handleError { Task { do { - try await sendNotification(title: "Failed to handle link", body: handleError.description) + try await sendNotification( + title: "Failed to handle link", + body: handleError.description, + category: .uriFailure + ) } catch let notifError { logger.error("Failed to send notification (\(handleError.description)): \(notifError)") } diff --git a/Coder-Desktop/Coder-Desktop/Notifications.swift b/Coder-Desktop/Coder-Desktop/Notifications.swift index 44a2afb..e3ffcf5 100644 --- a/Coder-Desktop/Coder-Desktop/Notifications.swift +++ b/Coder-Desktop/Coder-Desktop/Notifications.swift @@ -1,8 +1,23 @@ import UserNotifications -class NotifDelegate: NSObject, UNUserNotificationCenterDelegate { - override init() { - super.init() +extension AppDelegate: UNUserNotificationCenterDelegate { + static func registerNotificationCategories() { + let vpnFailure = UNNotificationCategory( + identifier: NotificationCategory.vpnFailure.rawValue, + actions: [], + intentIdentifiers: [], + options: [] + ) + + let uriFailure = UNNotificationCategory( + identifier: NotificationCategory.uriFailure.rawValue, + actions: [], + intentIdentifiers: [], + options: [] + ) + + UNUserNotificationCenter.current() + .setNotificationCategories([vpnFailure, uriFailure]) } // This function is required for notifications to appear as banners whilst the app is running. @@ -13,9 +28,28 @@ class NotifDelegate: NSObject, UNUserNotificationCenterDelegate { ) async -> UNNotificationPresentationOptions { [.banner] } + + nonisolated func userNotificationCenter( + _: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + let category = response.notification.request.content.categoryIdentifier + let action = response.actionIdentifier + switch (category, action) { + // Default action for VPN failure notification + case (NotificationCategory.vpnFailure.rawValue, UNNotificationDefaultActionIdentifier): + Task { @MainActor in + self.menuBar?.menuBarExtra.toggleVisibility() + } + default: + break + } + completionHandler() + } } -func sendNotification(title: String, body: String) async throws { +func sendNotification(title: String, body: String, category: NotificationCategory) async throws { let nc = UNUserNotificationCenter.current() let granted = try await nc.requestAuthorization(options: [.alert, .badge]) guard granted else { @@ -24,5 +58,11 @@ func sendNotification(title: String, body: String) async throws { let content = UNMutableNotificationContent() content.title = title content.body = body + content.categoryIdentifier = category.rawValue try await nc.add(.init(identifier: UUID().uuidString, content: content, trigger: nil)) } + +enum NotificationCategory: String { + case vpnFailure = "VPN_FAILURE" + case uriFailure = "URI_FAILURE" +} diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index 224174a..5f461ff 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -61,6 +61,18 @@ final class CoderVPNService: NSObject, VPNService { if tunnelState == .connecting { progress = .init(stage: .initial, downloadProgress: nil) } + if case let .failed(tunnelError) = tunnelState, tunnelState != oldValue { + Task { + do { + try await sendNotification( + title: "Coder Connect has failed!", + body: tunnelError.description, category: .vpnFailure + ) + } catch let notifError { + logger.error("Failed to send notification (\(tunnelError.description)): \(notifError)") + } + } + } } } diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml index 166a157..dc5bc7b 100644 --- a/Coder-Desktop/project.yml +++ b/Coder-Desktop/project.yml @@ -98,7 +98,7 @@ packages: # - Set onAppear/disappear handlers. # The upstream repo has a purposefully limited API url: https://github.com/coder/fluid-menu-bar-extra - revision: 8e1d8b8 + revision: afc9256 KeychainAccess: url: https://github.com/kishikawakatsumi/KeychainAccess branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf