AuthURLPresenter.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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(iOS)
  15. import Foundation
  16. import UIKit
  17. import WebKit
  18. import SafariServices
  19. // TODO: Remove objc's and publics
  20. /** @class AuthURLPresenter
  21. @brief A Class responsible for presenting URL via SFSafariViewController or WKWebView.
  22. */
  23. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  24. @objc(FIRAuthURLPresenter) public class AuthURLPresenter: NSObject,
  25. SFSafariViewControllerDelegate, AuthWebViewControllerDelegate {
  26. /** @fn
  27. @brief Presents an URL to interact with user.
  28. @param url The URL to present.
  29. @param uiDelegate The UI delegate to present view controller.
  30. @param completion A block to be called either synchronously if the presentation fails to start,
  31. or asynchronously in future on an unspecified thread once the presentation finishes.
  32. */
  33. @objc(presentURL:UIDelegate:callbackMatcher:completion:) public
  34. func present(_ url: URL,
  35. uiDelegate: AuthUIDelegate?,
  36. callbackMatcher: @escaping (URL?) -> Bool,
  37. completion: @escaping (URL?, Error?) -> Void) {
  38. if isPresenting {
  39. // Unable to start a new presentation on top of another.
  40. // Invoke the new completion closure and leave the old one as-is
  41. // to be invoked when the presentation finishes.
  42. DispatchQueue.main.async {
  43. completion(nil, AuthErrorUtils.webContextCancelledError(message: nil))
  44. }
  45. return
  46. }
  47. isPresenting = true
  48. self.callbackMatcher = callbackMatcher
  49. self.completion = completion
  50. DispatchQueue.main.async {
  51. self.uiDelegate = uiDelegate ?? AuthDefaultUIDelegate.defaultUIDelegate()
  52. #if targetEnvironment(macCatalyst)
  53. self.webViewController = AuthWebViewController(url: url, delegate: self)
  54. if let webViewController = self.webViewController {
  55. let navController = UINavigationController(rootViewController: webViewController)
  56. if let fakeUIDelegate = self.fakeUIDelegate {
  57. fakeUIDelegate.present(navController, animated: true)
  58. } else {
  59. self.uiDelegate?.present(navController, animated: true)
  60. }
  61. }
  62. #else
  63. self.safariViewController = SFSafariViewController(url: url)
  64. self.safariViewController?.delegate = self
  65. if let safariViewController = self.safariViewController {
  66. if let fakeUIDelegate = self.fakeUIDelegate {
  67. fakeUIDelegate.present(safariViewController, animated: true)
  68. } else {
  69. self.uiDelegate?.present(safariViewController, animated: true)
  70. }
  71. }
  72. #endif
  73. }
  74. }
  75. /** @fn canHandleURL:
  76. @brief Determines if a URL was produced by the currently presented URL.
  77. @param url The URL to handle.
  78. @return Whether the URL could be handled or not.
  79. */
  80. @objc(canHandleURL:) public func canHandle(url: URL) -> Bool {
  81. if isPresenting,
  82. let callbackMatcher = callbackMatcher,
  83. callbackMatcher(url) {
  84. finishPresentation(withURL: url, error: nil)
  85. return true
  86. }
  87. return false
  88. }
  89. // MARK: AuthWebViewControllerDelegate
  90. func webViewControllerDidCancel(_ controller: AuthWebViewController) {
  91. kAuthGlobalWorkQueue.async {
  92. if self.webViewController == controller {
  93. self.finishPresentation(withURL: nil,
  94. error: AuthErrorUtils.webContextCancelledError(message: nil))
  95. }
  96. }
  97. }
  98. func webViewController(_ controller: AuthWebViewController, canHandle url: URL) -> Bool {
  99. var result = false
  100. kAuthGlobalWorkQueue.sync {
  101. if self.webViewController == controller {
  102. result = self.canHandle(url: url)
  103. }
  104. }
  105. return result
  106. }
  107. func webViewController(_ controller: AuthWebViewController,
  108. didFailWithError error: Error) {
  109. kAuthGlobalWorkQueue.async {
  110. if self.webViewController == controller {
  111. self.finishPresentation(withURL: nil, error: error)
  112. }
  113. }
  114. }
  115. /** @var_isPresenting
  116. @brief Whether or not some web-based content is being presented.
  117. Accesses to this property are serialized on the global Auth work queue
  118. and thus this variable should not be read or written outside of the work queue.
  119. */
  120. private var isPresenting: Bool = false
  121. /** @var callbackMatcher
  122. @brief The callback URL matcher for the current presentation, if one is active.
  123. */
  124. private var callbackMatcher: ((URL) -> Bool)?
  125. /** @var safariViewController
  126. @brief The SFSafariViewController used for the current presentation, if any.
  127. */
  128. private var safariViewController: SFSafariViewController?
  129. /** @var webViewController
  130. @brief The FIRAuthWebViewController used for the current presentation, if any.
  131. */
  132. private var webViewController: AuthWebViewController?
  133. /** @var uiDelegate
  134. @brief The UIDelegate used to present the SFSafariViewController.
  135. */
  136. var uiDelegate: AuthUIDelegate?
  137. /** @var completion
  138. @brief The completion handler for the current presentation, if one is active.
  139. Accesses to this variable are serialized on the global Auth work queue
  140. and thus this variable should not be read or written outside of the work queue.
  141. @remarks This variable is also used as a flag to indicate a presentation is active.
  142. */
  143. var completion: ((URL?, Error?) -> Void)?
  144. /** @var fakeUIDelegate
  145. @brief Test-only option to validate the calls to the uiDelegate.
  146. */
  147. var fakeUIDelegate: AuthUIDelegate?
  148. // MARK: Private methods
  149. private func finishPresentation(withURL url: URL?, error: Error?) {
  150. callbackMatcher = nil
  151. let uiDelegate = self.uiDelegate
  152. self.uiDelegate = nil
  153. let completion = self.completion
  154. self.completion = nil
  155. let safariViewController = self.safariViewController
  156. self.safariViewController = nil
  157. let webViewController = self.webViewController
  158. self.webViewController = nil
  159. if safariViewController != nil || webViewController != nil {
  160. uiDelegate?.dismiss(animated: true) {
  161. kAuthGlobalWorkQueue.async {
  162. self.isPresenting = false
  163. if let completion {
  164. completion(url, error)
  165. }
  166. }
  167. }
  168. }
  169. isPresenting = false
  170. if let completion {
  171. completion(url, error)
  172. }
  173. }
  174. }
  175. #endif