Skip to content

feat: send push notifications on VPN failures #196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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) {
Expand Down Expand Up @@ -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)")
}
Expand Down
48 changes: 44 additions & 4 deletions Coder-Desktop/Coder-Desktop/Notifications.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import UserNotifications
Copy link
Preview

Copilot AI Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old NotifDelegate class was removed but I don’t see its full definition cleaned up. Please remove any leftover NotifDelegate declaration to avoid dead code.

Copilot uses AI. Check for mistakes.


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.
Expand All @@ -13,9 +28,28 @@ class NotifDelegate: NSObject, UNUserNotificationCenterDelegate {
) async -> UNNotificationPresentationOptions {
Copy link
Preview

Copilot AI Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure the userNotificationCenter(_:willPresent:withCompletionHandler:) method is inside the AppDelegate extension so banners still appear while the app is active.

Copilot uses AI. Check for mistakes.

[.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 {
Expand All @@ -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"
}
12 changes: 12 additions & 0 deletions Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion Coder-Desktop/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading