79 lines
3.6 KiB
Swift
79 lines
3.6 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import UserNotifications
|
|
|
|
/// Captures this phone's APNS device token so it can ride along in the sync `Hello` (SYNC §6) and,
|
|
/// once the relay ships, let the host wake the phone for an approval (docs/CLOUD_INFRA.md §3).
|
|
///
|
|
/// Best-effort: the token may be nil until APNS responds, in which case it's simply included on a
|
|
/// later (re)connect — the phone still works fully on LAN without it.
|
|
@MainActor
|
|
final class PushRegistrar {
|
|
static let shared = PushRegistrar()
|
|
private init() {}
|
|
|
|
/// Hex-encoded APNS device token, or nil until registration completes.
|
|
private(set) var tokenHex: String?
|
|
|
|
/// Surface the notifications prompt and, if granted, register for remote notifications. Safe to
|
|
/// call on every launch; the system de-dupes registration.
|
|
func requestAuthorizationAndRegister() {
|
|
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in
|
|
guard granted else { return }
|
|
Task { @MainActor in UIApplication.shared.registerForRemoteNotifications() }
|
|
}
|
|
}
|
|
|
|
func didRegister(deviceToken: Data) {
|
|
tokenHex = deviceToken.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
}
|
|
|
|
/// SwiftUI needs a UIKit app delegate to receive the remote-registration callbacks; wired via
|
|
/// `@UIApplicationDelegateAdaptor` in `NucleicRemoteApp`.
|
|
final class PushAppDelegate: NSObject, UIApplicationDelegate {
|
|
func application(
|
|
_ application: UIApplication,
|
|
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
|
|
) {
|
|
Task { @MainActor in PushRegistrar.shared.didRegister(deviceToken: deviceToken) }
|
|
}
|
|
|
|
func application(
|
|
_ application: UIApplication,
|
|
didFailToRegisterForRemoteNotificationsWithError error: Error
|
|
) {
|
|
// Best-effort: no token → the phone still works on LAN, just no remote wake.
|
|
}
|
|
|
|
/// Silent pushes (content-available). Two kinds route here:
|
|
/// - `clear`: recall the "waiting for your approval" tickle once the approval is resolved on
|
|
/// another device — wake briefly to withdraw the stale notification.
|
|
/// - `adopt`: a Mac push-started a Live Activity while the app was closed; come online long
|
|
/// enough to harvest and register that activity's update token so live updates + a clean end
|
|
/// resume without the user opening the app (UX_IOS §5.3). This one holds the completion handler
|
|
/// until the token is registered (or the background window closes), so the process isn't
|
|
/// suspended mid-connect.
|
|
/// Other pushes (e.g. the approval wake tickle, which is an alert) don't route here.
|
|
func application(
|
|
_ application: UIApplication,
|
|
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
|
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
|
|
) {
|
|
if userInfo["adopt"] != nil {
|
|
// The store manages its own background-task assertion for this; report .newData once it
|
|
// has brought the connection up and registered the token (or given up).
|
|
Task { @MainActor in
|
|
RemoteStore.shared.handleLiveActivityAdoptWake { completionHandler(.newData) }
|
|
}
|
|
return
|
|
}
|
|
if userInfo["clear"] != nil {
|
|
// Fire-and-forget on the main actor (NotificationRouter is @MainActor); the removal is a
|
|
// quick UNUserNotificationCenter call, so we can report .noData right away.
|
|
Task { @MainActor in NotificationRouter.shared.withdrawApprovalAttention() }
|
|
}
|
|
completionHandler(.noData)
|
|
}
|
|
}
|