AppDelegate.swift 6.4 KB

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