AppDelegate.swift 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  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. // FIXME: Check that notification is not already there (in DB and via notification center!)
  49. self.showNotification(subscription, message)
  50. }
  51. }
  52. }
  53. completionHandler(.newData)
  54. }
  55. func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  56. let token = deviceToken.map { data in String(format: "%02.2hhx", data) }.joined()
  57. Messaging.messaging().apnsToken = deviceToken
  58. Log.d(tag, "Registered for remote notifications. Passing APNs token to Firebase: \(token)")
  59. }
  60. func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
  61. Log.e(tag, "Failed to register for remote notifications", error)
  62. }
  63. /// Create a local notification manually (as opposed to a remote notification being generated by Firebase). We need to make the
  64. /// local notification look exactly like the remote one (same userInfo), so that when we tap it, the userNotificationCenter(didReceive) function
  65. /// has the same information available.
  66. private func showNotification(_ subscription: Subscription, _ message: Message) {
  67. let content = UNMutableNotificationContent()
  68. content.modify(message: message, baseUrl: subscription.baseUrl ?? "?")
  69. let request = UNNotificationRequest(identifier: message.id, content: content, trigger: nil /* now */)
  70. UNUserNotificationCenter.current().add(request) { (error) in
  71. if let error = error {
  72. Log.e(self.tag, "Unable to create notification", error)
  73. }
  74. }
  75. }
  76. }
  77. extension AppDelegate: UNUserNotificationCenterDelegate {
  78. /// Executed when the app is in the foreground. Nothing has to be done here, except call the completionHandler.
  79. func userNotificationCenter(
  80. _ center: UNUserNotificationCenter,
  81. willPresent notification: UNNotification,
  82. withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  83. ) {
  84. let userInfo = notification.request.content.userInfo
  85. Log.d(tag, "Notification received via userNotificationCenter(willPresent)", userInfo)
  86. completionHandler([[.banner, .sound]])
  87. }
  88. /// Executed when the user clicks on the notification.
  89. func userNotificationCenter(
  90. _ center: UNUserNotificationCenter,
  91. didReceive response: UNNotificationResponse,
  92. withCompletionHandler completionHandler: @escaping () -> Void
  93. ) {
  94. let userInfo = response.notification.request.content.userInfo
  95. Log.d(tag, "Notification received via userNotificationCenter(didReceive)", userInfo)
  96. guard let message = Message.from(userInfo: userInfo) else {
  97. Log.w(tag, "Cannot convert userInfo to message", userInfo)
  98. completionHandler()
  99. return
  100. }
  101. let baseUrl = userInfo["base_url"] as? String ?? Config.appBaseUrl
  102. let action = message.actions?.first { $0.id == response.actionIdentifier }
  103. // Show current topic
  104. if message.topic != "" {
  105. selectedBaseUrl = topicUrl(baseUrl: baseUrl, topic: message.topic)
  106. }
  107. // Execute user action or click action (if any)
  108. if let action = action {
  109. ActionExecutor.execute(action)
  110. } else if let click = message.click, click != "", let url = URL(string: click) {
  111. UIApplication.shared.open(url, options: [:], completionHandler: nil)
  112. }
  113. completionHandler()
  114. }
  115. }
  116. extension AppDelegate: MessagingDelegate {
  117. func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
  118. Log.d(tag, "Firebase token received: \(String(describing: fcmToken))")
  119. // We don't actually need the FCM token, since we're just using topics.
  120. // We still print it so we can see if things were successful.
  121. }
  122. }