gitbay/Push/AppDelegate.swift

v1.8.0
gitbay-ios/gitbay/Push/AppDelegate.swift history · blame · raw

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}