AuthNotificationManager.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #if !os(macOS) && !os(watchOS)
  15. import Foundation
  16. /** @class FIRAuthAppCredential
  17. @brief A class represents a credential that proves the identity of the app.
  18. */
  19. @objc(FIRAuthNotificationManager) public class AuthNotificationManager: NSObject {
  20. /** @var kNotificationKey
  21. @brief The key to locate payload data in the remote notification.
  22. */
  23. private let kNotificationDataKey = "com.google.firebase.auth"
  24. /** @var kNotificationReceiptKey
  25. @brief The key for the receipt in the remote notification payload data.
  26. */
  27. private let kNotificationReceiptKey = "receipt"
  28. /** @var kNotificationSecretKey
  29. @brief The key for the secret in the remote notification payload data.
  30. */
  31. private let kNotificationSecretKey = "secret"
  32. /** @var kNotificationProberKey
  33. @brief The key for marking the prober in the remote notification payload data.
  34. */
  35. private let kNotificationProberKey = "warning"
  36. /** @var kProbingTimeout
  37. @brief Timeout for probing whether the app delegate forwards the remote notification to us.
  38. */
  39. private let kProbingTimeout = 1.0
  40. /** @var _application
  41. @brief The application.
  42. */
  43. private let application: Application
  44. /** @var _appCredentialManager
  45. @brief The object to handle app credentials delivered via notification.
  46. */
  47. private let appCredentialManager: AuthAppCredentialManager
  48. /** @var _hasCheckedNotificationForwarding
  49. @brief Whether notification forwarding has been checked or not.
  50. */
  51. private var hasCheckedNotificationForwarding: Bool = false
  52. /** @var _isNotificationBeingForwarded
  53. @brief Whether or not notification is being forwarded
  54. */
  55. private var isNotificationBeingForwarded: Bool = false
  56. /** @property timeout
  57. @brief The timeout for checking for notification forwarding.
  58. @remarks Only tests should access this property.
  59. */
  60. @objc public let timeout: TimeInterval
  61. /** @property immediateCallbackForTestFaking
  62. @brief Disable callback waiting for tests.
  63. @remarks Only tests should access this property.
  64. */
  65. var immediateCallbackForTestFaking = false
  66. /** @var _pendingCallbacks
  67. @brief All pending callbacks while a check is being performed.
  68. */
  69. private var pendingCallbacks: [(Bool) -> Void]?
  70. /** @fn initWithApplication:appCredentialManager:
  71. @brief Initializes the instance.
  72. @param application The application.
  73. @param appCredentialManager The object to handle app credentials delivered via notification.
  74. @return The initialized instance.
  75. */
  76. @objc public init(withApplication application: Application,
  77. appCredentialManager: AuthAppCredentialManager) {
  78. self.application = application
  79. self.appCredentialManager = appCredentialManager
  80. timeout = kProbingTimeout
  81. }
  82. /** @fn checkNotificationForwardingWithCallback:
  83. @brief Checks whether or not remote notifications are being forwarded to this class.
  84. @param callback The block to be called either immediately or in future once a result
  85. is available.
  86. */
  87. @objc public func checkNotificationForwarding(withCallback callback: @escaping (Bool) -> Void) {
  88. if pendingCallbacks != nil {
  89. pendingCallbacks?.append(callback)
  90. return
  91. }
  92. if immediateCallbackForTestFaking {
  93. callback(true)
  94. return
  95. }
  96. if hasCheckedNotificationForwarding {
  97. callback(isNotificationBeingForwarded)
  98. return
  99. }
  100. hasCheckedNotificationForwarding = true
  101. pendingCallbacks = [callback]
  102. DispatchQueue.main.async {
  103. let proberNotification = [self.kNotificationDataKey: [self.kNotificationProberKey:
  104. "This fake notification should be forwarded to Firebase Auth."]]
  105. if let delegate = self.application.delegate {
  106. delegate.application!(
  107. UIApplication.shared,
  108. didReceiveRemoteNotification: proberNotification
  109. ) { _ in
  110. }
  111. } else {
  112. AuthLog.logWarning(
  113. code: "I-AUT000015",
  114. message: "The UIApplicationDelegate must handle " +
  115. "remote notification for phone number authentication to work."
  116. )
  117. }
  118. kAuthGlobalWorkQueue.asyncAfter(deadline: .now() + .seconds(Int(self.timeout))) {
  119. self.callback()
  120. }
  121. }
  122. }
  123. /** @fn canHandleNotification:
  124. @brief Attempts to handle the remote notification.
  125. @param notification The notification in question.
  126. @return Whether or the notification has been handled.
  127. */
  128. @objc(canHandleNotification:) public func canHandle(notification: [AnyHashable: Any]) -> Bool {
  129. var stringDictionary: [String: Any]?
  130. let data = notification[kNotificationDataKey]
  131. if let jsonString = data as? String {
  132. // Deserialize in case the data is a JSON string.
  133. guard let jsonData = jsonString.data(using: .utf8),
  134. let dictionary = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
  135. else {
  136. return false
  137. }
  138. stringDictionary = dictionary
  139. }
  140. guard let dictionary = stringDictionary ?? data as? [String: Any] else {
  141. return false
  142. }
  143. if dictionary[kNotificationProberKey] != nil {
  144. if pendingCallbacks == nil {
  145. // The prober notification probably comes from another instance, so pass it along.
  146. return false
  147. }
  148. isNotificationBeingForwarded = true
  149. callback()
  150. return true
  151. }
  152. guard let receipt = dictionary[kNotificationReceiptKey] as? String,
  153. let secret = dictionary[kNotificationSecretKey] as? String else {
  154. return false
  155. }
  156. return appCredentialManager.canFinishVerification(withReceipt: receipt, secret: secret)
  157. }
  158. // MARK: Internal methods
  159. private func callback() {
  160. guard let pendingCallbacks else {
  161. return
  162. }
  163. self.pendingCallbacks = nil
  164. for callback in pendingCallbacks {
  165. callback(isNotificationBeingForwarded)
  166. }
  167. }
  168. }
  169. // Protocol for UIApplication to enable unit testing
  170. @objc public protocol ApplicationDelegate {
  171. @objc optional func application(_ application: Application,
  172. didReceiveRemoteNotification userInfo: [AnyHashable: Any],
  173. fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult)
  174. -> Void)
  175. }
  176. @objc public protocol Application {
  177. var delegate: UIApplicationDelegate? { get set }
  178. }
  179. extension UIApplication: Application {}
  180. #endif