Skip to content

Commit 4a6433a

Browse files
committed
feat: send push notifications on VPN failures
1 parent c19b39a commit 4a6433a

File tree

4 files changed

+66
-10
lines changed

4 files changed

+66
-10
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,16 @@ struct DesktopApp: App {
4242
@MainActor
4343
class AppDelegate: NSObject, NSApplicationDelegate {
4444
private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "app-delegate")
45-
private var menuBar: MenuBarController?
45+
var menuBar: MenuBarController?
4646
let vpn: CoderVPNService
4747
let state: AppState
4848
let fileSyncDaemon: MutagenDaemon
4949
let urlHandler: URLHandler
50-
let notifDelegate: NotifDelegate
5150
let helper: HelperService
5251
let autoUpdater: UpdaterService
5352

5453
override init() {
55-
notifDelegate = NotifDelegate()
54+
AppDelegate.registerNotificationCategories()
5655
vpn = CoderVPNService()
5756
helper = HelperService()
5857
autoUpdater = UpdaterService()
@@ -79,8 +78,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
7978
}
8079
self.fileSyncDaemon = fileSyncDaemon
8180
urlHandler = URLHandler(state: state, vpn: vpn)
81+
super.init()
8282
// `delegate` is weak
83-
UNUserNotificationCenter.current().delegate = notifDelegate
83+
UNUserNotificationCenter.current().delegate = self
8484
}
8585

8686
func applicationDidFinishLaunching(_: Notification) {
@@ -161,7 +161,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
161161
do { try urlHandler.handle(url) } catch let handleError {
162162
Task {
163163
do {
164-
try await sendNotification(title: "Failed to handle link", body: handleError.description)
164+
try await sendNotification(
165+
title: "Failed to handle link",
166+
body: handleError.description,
167+
category: .uriFailure
168+
)
165169
} catch let notifError {
166170
logger.error("Failed to send notification (\(handleError.description)): \(notifError)")
167171
}
Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
11
import UserNotifications
22

3-
class NotifDelegate: NSObject, UNUserNotificationCenterDelegate {
4-
override init() {
5-
super.init()
3+
extension AppDelegate: UNUserNotificationCenterDelegate {
4+
static func registerNotificationCategories() {
5+
let vpnFailure = UNNotificationCategory(
6+
identifier: NotificationCategory.vpnFailure.rawValue,
7+
actions: [],
8+
intentIdentifiers: [],
9+
options: []
10+
)
11+
12+
let uriFailure = UNNotificationCategory(
13+
identifier: NotificationCategory.uriFailure.rawValue,
14+
actions: [],
15+
intentIdentifiers: [],
16+
options: []
17+
)
18+
19+
UNUserNotificationCenter.current()
20+
.setNotificationCategories([vpnFailure, uriFailure])
621
}
722

823
// This function is required for notifications to appear as banners whilst the app is running.
@@ -13,9 +28,28 @@ class NotifDelegate: NSObject, UNUserNotificationCenterDelegate {
1328
) async -> UNNotificationPresentationOptions {
1429
[.banner]
1530
}
31+
32+
nonisolated func userNotificationCenter(
33+
_: UNUserNotificationCenter,
34+
didReceive response: UNNotificationResponse,
35+
withCompletionHandler completionHandler: @escaping () -> Void
36+
) {
37+
let category = response.notification.request.content.categoryIdentifier
38+
let action = response.actionIdentifier
39+
switch (category, action) {
40+
// Default action for VPN failure notification
41+
case (NotificationCategory.vpnFailure.rawValue, UNNotificationDefaultActionIdentifier):
42+
Task { @MainActor in
43+
self.menuBar?.menuBarExtra.toggleVisibility()
44+
}
45+
default:
46+
break
47+
}
48+
completionHandler()
49+
}
1650
}
1751

18-
func sendNotification(title: String, body: String) async throws {
52+
func sendNotification(title: String, body: String, category: NotificationCategory) async throws {
1953
let nc = UNUserNotificationCenter.current()
2054
let granted = try await nc.requestAuthorization(options: [.alert, .badge])
2155
guard granted else {
@@ -24,5 +58,11 @@ func sendNotification(title: String, body: String) async throws {
2458
let content = UNMutableNotificationContent()
2559
content.title = title
2660
content.body = body
61+
content.categoryIdentifier = category.rawValue
2762
try await nc.add(.init(identifier: UUID().uuidString, content: content, trigger: nil))
2863
}
64+
65+
enum NotificationCategory: String {
66+
case vpnFailure = "VPN_FAILURE"
67+
case uriFailure = "URI_FAILURE"
68+
}

Coder-Desktop/Coder-Desktop/VPN/VPNService.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ final class CoderVPNService: NSObject, VPNService {
6161
if tunnelState == .connecting {
6262
progress = .init(stage: .initial, downloadProgress: nil)
6363
}
64+
if case let .failed(tunnelError) = tunnelState, tunnelState != oldValue {
65+
Task {
66+
do {
67+
try await sendNotification(
68+
title: "Coder Connect has failed!",
69+
body: tunnelError.description, category: .vpnFailure
70+
)
71+
} catch let notifError {
72+
logger.error("Failed to send notification (\(tunnelError.description)): \(notifError)")
73+
}
74+
}
75+
}
6476
}
6577
}
6678

Coder-Desktop/project.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ packages:
9898
# - Set onAppear/disappear handlers.
9999
# The upstream repo has a purposefully limited API
100100
url: https://github.com/coder/fluid-menu-bar-extra
101-
revision: 8e1d8b8
101+
revision: afc9256
102102
KeychainAccess:
103103
url: https://github.com/kishikawakatsumi/KeychainAccess
104104
branch: e0c7eebc5a4465a3c4680764f26b7a61f567cdaf

0 commit comments

Comments
 (0)