gitbay/Push/AppDelegate.swift
93 lines · 3622 bytes
1import UIKit
2import UserNotifications
3
4/// The only way to receive an APNs device token and a notification tap.
5/// SwiftUI has no equivalent, so the app keeps one delegate whose whole
6/// job is handing both to the push types.
7///
8/// The references are weak: `gitbayApp` holds all three in `@State` for
9/// the life of the app, so this delegate borrows them rather than
10/// keeping them alive.
11final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
12
13 weak var registrar: PushRegistrar?
14 weak var router: PushRouter?
15 weak var session: SessionStore?
16
17 /// A tap that arrived before `gitbayApp` wired this delegate up.
18 ///
19 /// This is the common case, not an edge one: tapping a notification
20 /// while the app is not running launches it, and iOS can deliver
21 /// the tap before SwiftUI's root `.task` has run. Held as the raw
22 /// `userInfo` rather than a resolved `PushTarget`, because resolving
23 /// one needs `session.accounts` — precisely what is missing at that
24 /// moment.
25 ///
26 /// One slot, newest wins. A queue would open several screens in
27 /// sequence, which is not what a tap asks for.
28 private var pendingUserInfo: [AnyHashable: Any]?
29
30 func application(
31 _ application: UIApplication,
32 didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil
33 ) -> Bool {
34 UNUserNotificationCenter.current().delegate = self
35 return true
36 }
37
38 func application(
39 _ application: UIApplication,
40 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
41 ) {
42 Task { await registrar?.deviceTokenArrived(deviceToken) }
43 }
44
45 func application(
46 _ application: UIApplication,
47 didFailToRegisterForRemoteNotificationsWithError error: any Error
48 ) {
49 // Nothing to do and nothing to say: push is a side channel, and
50 // the next launch tries again.
51 }
52
53 /// A notification arriving while the app is open still shows — you
54 /// may be reading a different repository when one lands.
55 func userNotificationCenter(
56 _ center: UNUserNotificationCenter,
57 willPresent notification: UNNotification
58 ) async -> UNNotificationPresentationOptions {
59 [.banner, .sound, .badge]
60 }
61
62 func userNotificationCenter(
63 _ center: UNUserNotificationCenter,
64 didReceive response: UNNotificationResponse
65 ) async {
66 deliver(response.notification.request.content.userInfo)
67 }
68
69 /// Drains a tap that arrived before the wiring did. Called from
70 /// `gitbayApp`'s root task, once `session` and `router` are set.
71 func deliverPendingNotification() {
72 guard let userInfo = pendingUserInfo else { return }
73 // Cleared whatever the resolve decides: a payload for an
74 // account the user has since signed out of resolves to no
75 // target, and must not sit here waiting to fire later.
76 pendingUserInfo = nil
77 deliver(userInfo)
78 }
79
80 /// The one resolve-and-deliver path, so a buffered tap and a live
81 /// one cannot diverge. Parks the payload if the wiring is not up
82 /// yet; `deliverPendingNotification` comes back for it.
83 private func deliver(_ userInfo: [AnyHashable: Any]) {
84 guard let session, let router else {
85 pendingUserInfo = userInfo
86 return
87 }
88 guard let payload = PushPayload(userInfo: userInfo),
89 let target = PushTarget(payload: payload, accounts: session.accounts)
90 else { return }
91 router.pending = target
92 }
93}