import UIKit import UserNotifications /// The only way to receive an APNs device token and a notification tap. /// SwiftUI has no equivalent, so the app keeps one delegate whose whole /// job is handing both to the push types. /// /// The references are weak: `gitbayApp` holds all three in `@State` for /// the life of the app, so this delegate borrows them rather than /// keeping them alive. final class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { weak var registrar: PushRegistrar? weak var router: PushRouter? weak var session: SessionStore? /// A tap that arrived before `gitbayApp` wired this delegate up. /// /// This is the common case, not an edge one: tapping a notification /// while the app is not running launches it, and iOS can deliver /// the tap before SwiftUI's root `.task` has run. Held as the raw /// `userInfo` rather than a resolved `PushTarget`, because resolving /// one needs `session.accounts` — precisely what is missing at that /// moment. /// /// One slot, newest wins. A queue would open several screens in /// sequence, which is not what a tap asks for. private var pendingUserInfo: [AnyHashable: Any]? func application( _ application: UIApplication, didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { UNUserNotificationCenter.current().delegate = self return true } func application( _ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data ) { Task { await registrar?.deviceTokenArrived(deviceToken) } } func application( _ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error ) { // Nothing to do and nothing to say: push is a side channel, and // the next launch tries again. } /// A notification arriving while the app is open still shows — you /// may be reading a different repository when one lands. func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification ) async -> UNNotificationPresentationOptions { [.banner, .sound, .badge] } func userNotificationCenter( _ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse ) async { deliver(response.notification.request.content.userInfo) } /// Drains a tap that arrived before the wiring did. Called from /// `gitbayApp`'s root task, once `session` and `router` are set. func deliverPendingNotification() { guard let userInfo = pendingUserInfo else { return } // Cleared whatever the resolve decides: a payload for an // account the user has since signed out of resolves to no // target, and must not sit here waiting to fire later. pendingUserInfo = nil deliver(userInfo) } /// The one resolve-and-deliver path, so a buffered tap and a live /// one cannot diverge. Parks the payload if the wiring is not up /// yet; `deliverPendingNotification` comes back for it. private func deliver(_ userInfo: [AnyHashable: Any]) { guard let session, let router else { pendingUserInfo = userInfo return } guard let payload = PushPayload(userInfo: userInfo), let target = PushTarget(payload: payload, accounts: session.accounts) else { return } router.pending = target } }