AppDelegate.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import UIKit
  2. import SafariServices
  3. import UserNotifications
  4. import Firebase
  5. import FirebaseCore
  6. import FirebaseMessaging
  7. import CoreData
  8. class AppDelegate: UIResponder, UIApplicationDelegate, ObservableObject {
  9. private let tag = "AppDelegate"
  10. private let pollTopic = "~poll" // See ntfy server if ever changed
  11. // Implements navigation from notifications, see https://stackoverflow.com/a/70731861/1440785
  12. @Published var selectedBaseUrl: String? = nil
  13. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
  14. Log.d(tag, "Launching AppDelegate")
  15. FirebaseApp.configure()
  16. FirebaseConfiguration.shared.setLoggerLevel(.max)
  17. // Register app permissions for push notifications
  18. UNUserNotificationCenter.current().delegate = self
  19. Messaging.messaging().delegate = self
  20. UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
  21. guard success else {
  22. Log.e(self.tag, "Failed to register for local push notifications", error)
  23. return
  24. }
  25. Log.d(self.tag, "Successfully registered for local push notifications")
  26. }
  27. // Register too receive remote notifications
  28. application.registerForRemoteNotifications()
  29. return true
  30. }
  31. /// Executed when a background notification arrives on the "~poll" topic. This is used to trigger polling of local topics.
  32. /// See https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app
  33. func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
  34. Log.d(tag, "Background notification received", userInfo)
  35. // Exit out early if this message is not expected
  36. let topic = userInfo["topic"] as? String ?? ""
  37. if topic != pollTopic {
  38. completionHandler(.noData)
  39. return
  40. }
  41. // Poll and show new messages as notifications
  42. let store = Store.shared
  43. let subscriptionManager = SubscriptionManager(store: store)
  44. store.getSubscriptions()?.forEach { subscription in
  45. subscriptionManager.poll(subscription) { messages in
  46. messages.forEach { message in
  47. self.showNotification(subscription, message)
  48. }
  49. }
  50. }
  51. completionHandler(.newData)
  52. }
  53. func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  54. let token = deviceToken.map { data in String(format: "%02.2hhx", data) }.joined()
  55. Messaging.messaging().apnsToken = deviceToken
  56. Log.d(tag, "Registered for remote notifications. Passing APNs token to Firebase: \(token)")
  57. }
  58. func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
  59. Log.e(tag, "Failed to register for remote notifications", error)
  60. }
  61. /// Create a local notification manually (as opposed to a remote notification being generated by Firebase). We need to make the
  62. /// local notification look exactly like the remote one (same userInfo), so that when we tap it, the userNotificationCenter(didReceive) function
  63. /// has the same information available.
  64. private func showNotification(_ subscription: Subscription, _ message: Message) {
  65. let content = UNMutableNotificationContent()
  66. content.modify(message: message, baseUrl: subscription.baseUrl ?? "?")
  67. let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */)
  68. UNUserNotificationCenter.current().add(request) { (error) in
  69. if let error = error {
  70. Log.e(self.tag, "Unable to create notification", error)
  71. }
  72. }
  73. }
  74. }
  75. extension AppDelegate: UNUserNotificationCenterDelegate {
  76. /// Executed when the app is in the foreground. Nothing has to be done here, except call the completionHandler.
  77. func userNotificationCenter(
  78. _ center: UNUserNotificationCenter,
  79. willPresent notification: UNNotification,
  80. withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  81. ) {
  82. let userInfo = notification.request.content.userInfo
  83. Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo)
  84. completionHandler([[.banner, .sound]])
  85. }
  86. /// Executed when the user clicks on the notification.
  87. func userNotificationCenter(
  88. _ center: UNUserNotificationCenter,
  89. didReceive response: UNNotificationResponse,
  90. withCompletionHandler completionHandler: @escaping () -> Void
  91. ) {
  92. let userInfo = response.notification.request.content.userInfo
  93. Log.d(tag, "Notification received via userNotificationCenter(didReceive)", userInfo)
  94. guard let message = Message.from(userInfo: userInfo) else {
  95. Log.w(tag, "Cannot convert userInfo to message", userInfo)
  96. completionHandler()
  97. return
  98. }
  99. let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
  100. let action = message.actions?.first { $0.id == response.actionIdentifier }
  101. // Show current topic
  102. if message.topic != "" {
  103. selectedBaseUrl = topicUrl(baseUrl: baseUrl, topic: message.topic)
  104. }
  105. // Execute user action or click action (if any)
  106. if let action = action {
  107. ActionExecutor.execute(action)
  108. } else if let click = message.click, click != "", let url = URL(string: click) {
  109. UIApplication.shared.open(url, options: [:], completionHandler: nil)
  110. }
  111. completionHandler()
  112. }
  113. }
  114. extension AppDelegate: MessagingDelegate {
  115. func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
  116. Log.d(tag, "Firebase token received: \(String(describing: fcmToken))")
  117. // Subscribe to ~poll topic
  118. Messaging.messaging().subscribe(toTopic: pollTopic)
  119. // Re-subscribe to Firebase for all topics
  120. let store = Store.shared
  121. store.getSubscriptions()?.forEach{ subscription in
  122. if let baseUrl = subscription.baseUrl, let topic = subscription.topic {
  123. Log.d(tag, "Re-subscribing to topic \(baseUrl)/\(topic)")
  124. if baseUrl == Config.appBaseUrl {
  125. Messaging.messaging().subscribe(toTopic: topic)
  126. } else {
  127. Messaging.messaging().subscribe(toTopic: topicHash(baseUrl: baseUrl, topic: topic))
  128. }
  129. }
  130. }
  131. }
  132. }