diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index 4d78735..e2fe3ab 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -1,8 +1,10 @@ import FluidMenuBarExtra import NetworkExtension +import os import SDWebImageSVGCoder import SDWebImageSwiftUI import SwiftUI +import UserNotifications import VPNLib @main @@ -36,13 +38,16 @@ struct DesktopApp: App { @MainActor class AppDelegate: NSObject, NSApplicationDelegate { + private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app-delegate") private var menuBar: MenuBarController? let vpn: CoderVPNService let state: AppState let fileSyncDaemon: MutagenDaemon let urlHandler: URLHandler + let notifDelegate: NotifDelegate override init() { + notifDelegate = NotifDelegate() vpn = CoderVPNService() let state = AppState(onChange: vpn.configureTunnelProviderProtocol) vpn.onStart = { @@ -67,6 +72,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } self.fileSyncDaemon = fileSyncDaemon urlHandler = URLHandler(state: state, vpn: vpn) + // `delegate` is weak + UNUserNotificationCenter.current().delegate = notifDelegate } func applicationDidFinishLaunching(_: Notification) { @@ -141,9 +148,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { // We only accept one at time, for now return } - do { try urlHandler.handle(url) } catch { - // TODO: Push notification - print(error.description) + do { try urlHandler.handle(url) } catch let handleError { + Task { + do { + try await sendNotification(title: "Failed to handle link", body: handleError.description) + } 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 new file mode 100644 index 0000000..44a2afb --- /dev/null +++ b/Coder-Desktop/Coder-Desktop/Notifications.swift @@ -0,0 +1,28 @@ +import UserNotifications + +class NotifDelegate: NSObject, UNUserNotificationCenterDelegate { + override init() { + super.init() + } + + // This function is required for notifications to appear as banners whilst the app is running. + // We're effectively forwarding the notification back to the OS + nonisolated func userNotificationCenter( + _: UNUserNotificationCenter, + willPresent _: UNNotification + ) async -> UNNotificationPresentationOptions { + [.banner] + } +} + +func sendNotification(title: String, body: String) async throws { + let nc = UNUserNotificationCenter.current() + let granted = try await nc.requestAuthorization(options: [.alert, .badge]) + guard granted else { + return + } + let content = UNMutableNotificationContent() + content.title = title + content.body = body + try await nc.add(.init(identifier: UUID().uuidString, content: content, trigger: nil)) +} diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml index 9ec3ba4..f2c96fa 100644 --- a/Coder-Desktop/project.yml +++ b/Coder-Desktop/project.yml @@ -147,6 +147,7 @@ targets: com.apple.developer.system-extension.install: true com.apple.security.application-groups: - $(TeamIdentifierPrefix)com.coder.Coder-Desktop + aps-environment: development settings: base: ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon # Sets the app icon to "AppIcon".