AuthURLPresenter.swift 6.9 KB

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