Kaynağa Gözat

[auth-swift] AuthURLPresenter implementation and PhoneAuth request (#11092)

Paul Beusterien 3 yıl önce
ebeveyn
işleme
89bbe74ce0

+ 0 - 4
FirebaseAuth/Sources/Auth/FIRAuth.m

@@ -33,10 +33,6 @@
 #import "FirebaseAuth/Sources/SystemService/FIRAuthStoredUserManager.h"
 #import "FirebaseAuth/Sources/Utilities/FIRAuthExceptionUtils.h"
 
-#if TARGET_OS_IOS
-#import "FirebaseAuth/Sources/Utilities/FIRAuthURLPresenter.h"
-#endif
-
 NS_ASSUME_NONNULL_BEGIN
 
 #pragma mark-- Logger Service String.

+ 180 - 0
FirebaseAuth/Sources/Swift/Utilities/AuthURLPresenter.swift

@@ -0,0 +1,180 @@
+// Copyright 2023 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#if os(iOS)
+
+  import Foundation
+  import UIKit
+  import WebKit
+  import SafariServices
+
+  // TODO: Remove objc's and publics
+
+  /** @class AuthURLPresenter
+      @brief A Class responsible for presenting URL via SFSafariViewController or WKWebView.
+   */
+  @objc(FIRAuthURLPresenter) public class AuthURLPresenter: NSObject,
+    SFSafariViewControllerDelegate,
+    FIRAuthWebViewControllerDelegate {
+    /** @fn
+        @brief Presents an URL to interact with user.
+        @param URL The URL to present.
+        @param UIDelegate The UI delegate to present view controller.
+        @param completion A block to be called either synchronously if the presentation fails to start,
+            or asynchronously in future on an unspecified thread once the presentation finishes.
+     */
+    @objc(presentURL:UIDelegate:callbackMatcher:completion:) public
+    func present(_ url: URL,
+                 uiDelegate: AuthUIDelegate?,
+                 callbackMatcher: @escaping (URL?) -> Bool,
+                 completion: @escaping (URL?, Error?) -> Void) {
+      if isPresenting {
+        // Unable to start a new presentation on top of another.
+        // Invoke the new completion closure and leave the old one as-is
+        // to be invoked when the presentation finishes.
+        DispatchQueue.main.async {
+          completion(nil, AuthErrorUtils.webContextCancelledError(message: nil))
+        }
+        return
+      }
+      isPresenting = true
+      self.callbackMatcher = callbackMatcher
+      self.completion = completion
+      DispatchQueue.main.async {
+        // TODO: Next line after AuthDefaultUIDelegate
+        self.uiDelegate = uiDelegate // ?? FIRAuthDefaultUIDelegate.defaultUIDelegate()
+        #if targetEnvironment(macCatalyst)
+          self.webViewController = AuthWebViewController(url: url, delegate: self)
+          if let webViewController = self.webViewController {
+            let navController = UINavigationController(rootViewController: webViewController)
+            self.uiDelegate?.present(navController, animated: true)
+          }
+        #else
+          self.safariViewController = SFSafariViewController(url: url)
+          self.safariViewController?.delegate = self
+          if let safariViewController = self.safariViewController {
+            self.uiDelegate?.present(safariViewController, animated: true)
+          }
+        #endif
+      }
+    }
+
+    /** @fn canHandleURL:
+        @brief Determines if a URL was produced by the currently presented URL.
+        @param URL The URL to handle.
+        @return Whether the URL could be handled or not.
+     */
+    @objc(canHandleURL:) public func canHandle(url: URL) -> Bool {
+      if isPresenting,
+         let callbackMatcher = callbackMatcher,
+         callbackMatcher(url) {
+        return true
+      }
+      return false
+    }
+
+    // MARK: AuthWebViewControllerDelegate
+
+    public func webViewControllerDidCancel(_ controller: AuthWebViewController) {
+      kAuthGlobalWorkQueue.async {
+        if self.webViewController == controller {
+          self.finishPresentation(withURL: nil,
+                                  error: AuthErrorUtils.webContextCancelledError(message: nil))
+        }
+      }
+    }
+
+    public func webViewController(_ controller: AuthWebViewController, canHandle url: URL) -> Bool {
+      var result = false
+      kAuthGlobalWorkQueue.sync {
+        if self.webViewController == controller {
+          result = self.canHandle(url: url)
+        }
+      }
+      return result
+    }
+
+    public func webViewController(_ controller: AuthWebViewController,
+                                  didFailWithError error: Error) {
+      kAuthGlobalWorkQueue.async {
+        if self.webViewController == controller {
+          self.finishPresentation(withURL: nil, error: error)
+        }
+      }
+    }
+
+    /** @var _isPresenting
+        @brief Whether or not some web-based content is being presented.
+            Accesses to this property are serialized on the global Auth work queue
+            and thus this variable should not be read or written outside of the work queue.
+     */
+    private var isPresenting: Bool = false
+
+    /** @var _callbackMatcher
+        @brief The callback URL matcher for the current presentation, if one is active.
+     */
+    private var callbackMatcher: ((URL) -> Bool)?
+
+    /** @var _safariViewController
+        @brief The SFSafariViewController used for the current presentation, if any.
+     */
+    private var safariViewController: SFSafariViewController?
+
+    /** @var _webViewController
+        @brief The FIRAuthWebViewController used for the current presentation, if any.
+     */
+    private var webViewController: AuthWebViewController?
+
+    /** @var _UIDelegate
+        @brief The UIDelegate used to present the SFSafariViewController.
+     */
+    private var uiDelegate: AuthUIDelegate?
+
+    /** @var _completion
+        @brief The completion handler for the current presentation, if one is active.
+            Accesses to this variable are serialized on the global Auth work queue
+            and thus this variable should not be read or written outside of the work queue.
+        @remarks This variable is also used as a flag to indicate a presentation is active.
+     */
+    private var completion: ((URL?, Error?) -> Void)?
+
+    // MARK: Private methods
+
+    private func finishPresentation(withURL url: URL?, error: Error?) {
+      callbackMatcher = nil
+      let uiDelegate = self.uiDelegate
+      self.uiDelegate = nil
+      let completion = self.completion
+      self.completion = nil
+      let safariViewController = self.safariViewController
+      self.safariViewController = nil
+      let webViewController = self.webViewController
+      self.webViewController = nil
+      if safariViewController != nil || webViewController != nil {
+        uiDelegate?.dismiss(animated: true) {
+          kAuthGlobalWorkQueue.async {
+            self.isPresenting = false
+            if let completion {
+              completion(url, error)
+            }
+          }
+        }
+      }
+      isPresenting = false
+      if let completion {
+        completion(url, error)
+      }
+    }
+  }
+#endif

+ 5 - 15
FirebaseAuth/Sources/Swift/Utilities/AuthWebView.swift

@@ -73,21 +73,11 @@
     }
 
     private func createSpinner() -> UIActivityIndicatorView {
-      var spinnerStyle: UIActivityIndicatorView.Style = .gray
-      #if targetEnvironment(macCatalyst)
-        if #available(iOS 13.0, *) {
-          spinnerStyle = .medium
-        } else {
-          // iOS 13 deprecation
-          //            #pragma clang diagnostic push
-          //            #pragma clang diagnostic ignored "-Wdeprecated-declarations"
-          spinnerStyle = .gray
-          //            #pragma clang diagnostic pop
-        }
-      #endif
-      let spinner = UIActivityIndicatorView(style: spinnerStyle)
-      return spinner
+      if #available(iOS 13.0, macCatalyst 13.0, *) {
+        return UIActivityIndicatorView(style: .medium)
+      } else {
+        return UIActivityIndicatorView(style: .gray)
+      }
     }
   }
-
 #endif

+ 0 - 60
FirebaseAuth/Sources/Utilities/FIRAuthURLPresenter.h

@@ -1,60 +0,0 @@
-/*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#import <TargetConditionals.h>
-#if TARGET_OS_IOS && (!defined(TARGET_OS_XR) || !TARGET_OS_XR)
-
-#import <Foundation/Foundation.h>
-#import <SafariServices/SafariServices.h>
-
-NS_ASSUME_NONNULL_BEGIN
-
-@protocol FIRAuthUIDelegate;
-@protocol FIRAuthWebViewControllerDelegate;
-
-/** @class FIRAuthURLPresenter
-    @brief A Class responsible for presenting URL via SFSafariViewController or WKWebView.
- */
-@interface FIRAuthURLPresenter
-    : NSObject <SFSafariViewControllerDelegate, FIRAuthWebViewControllerDelegate>
-
-typedef BOOL (^FIRAuthURLCallbackMatcher)(NSURL *_Nullable callbackURL);
-typedef void (^FIRAuthURLPresentationCompletion)(NSURL *_Nullable callbackURL,
-                                                 NSError *_Nullable error);
-/** @fn presentURL:UIDelegate:callbackMatcher:completion:
-    @brief Presents an URL to interact with user.
-    @param URL The URL to present.
-    @param UIDelegate The UI delegate to present view controller.
-    @param completion A block to be called either synchronously if the presentation fails to start,
-        or asynchronously in future on an unspecified thread once the presentation finishes.
- */
-- (void)presentURL:(NSURL *)URL
-         UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
-    callbackMatcher:(FIRAuthURLCallbackMatcher)callbackMatcher
-         completion:(FIRAuthURLPresentationCompletion)completion;
-
-/** @fn canHandleURL:
-    @brief Determines if a URL was produced by the currently presented URL.
-    @param URL The URL to handle.
-    @return Whether the URL could be handled or not.
- */
-- (BOOL)canHandleURL:(NSURL *)URL;
-
-@end
-
-NS_ASSUME_NONNULL_END
-
-#endif  // TARGET_OS_IOS && (!defined(TARGET_OS_XR) || !TARGET_OS_XR)

+ 0 - 204
FirebaseAuth/Sources/Utilities/FIRAuthURLPresenter.m

@@ -1,204 +0,0 @@
-/*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#import <TargetConditionals.h>
-#if TARGET_OS_IOS && (!defined(TARGET_OS_XR) || !TARGET_OS_XR)
-
-#import <SafariServices/SafariServices.h>
-#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRAuthUIDelegate.h"
-
-#import "FirebaseAuth-Swift.h"
-#import "FirebaseAuth/Sources/Auth/FIRAuthGlobalWorkQueue.h"
-#import "FirebaseAuth/Sources/Utilities/FIRAuthDefaultUIDelegate.h"
-#import "FirebaseAuth/Sources/Utilities/FIRAuthURLPresenter.h"
-
-NS_ASSUME_NONNULL_BEGIN
-
-// Disable unguarded availability warnings because SFSafariViewController is been used throughout
-// the code, including as an iVar, which cannot be simply excluded by @available check.
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wunguarded-availability"
-
-@implementation FIRAuthURLPresenter {
-  /** @var _isPresenting
-      @brief Whether or not some web-based content is being presented.
-          Accesses to this property are serialized on the global Auth work queue
-          and thus this variable should not be read or written outside of the work queue.
-   */
-  BOOL _isPresenting;
-
-  /** @var _callbackMatcher
-      @brief The callback URL matcher for the current presentation, if one is active.
-   */
-  FIRAuthURLCallbackMatcher _Nullable _callbackMatcher;
-
-  /** @var _safariViewController
-      @brief The SFSafariViewController used for the current presentation, if any.
-   */
-  SFSafariViewController *_Nullable _safariViewController;
-
-  /** @var _webViewController
-      @brief The FIRAuthWebViewController used for the current presentation, if any.
-   */
-  FIRAuthWebViewController *_Nullable _webViewController;
-
-  /** @var _UIDelegate
-      @brief The UIDelegate used to present the SFSafariViewController.
-   */
-  id<FIRAuthUIDelegate> _UIDelegate;
-
-  /** @var _completion
-      @brief The completion handler for the current presentation, if one is active.
-          Accesses to this variable are serialized on the global Auth work queue
-          and thus this variable should not be read or written outside of the work queue.
-      @remarks This variable is also used as a flag to indicate a presentation is active.
-   */
-  FIRAuthURLPresentationCompletion _Nullable _completion;
-}
-
-- (void)presentURL:(NSURL *)URL
-         UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
-    callbackMatcher:(FIRAuthURLCallbackMatcher)callbackMatcher
-         completion:(FIRAuthURLPresentationCompletion)completion {
-  if (_isPresenting) {
-    // Unable to start a new presentation on top of another.
-    // Invoke the new completion closure and leave the old one as-is
-    // to be invoked when the presentation finishes.
-    dispatch_async(dispatch_get_main_queue(), ^() {
-      completion(nil, [FIRAuthErrorUtils webContextAlreadyPresentedErrorWithMessage:nil]);
-    });
-    return;
-  }
-  _isPresenting = YES;
-  _callbackMatcher = callbackMatcher;
-  _completion = [completion copy];
-  dispatch_async(dispatch_get_main_queue(), ^() {
-    self->_UIDelegate = UIDelegate ?: [FIRAuthDefaultUIDelegate defaultUIDelegate];
-#if TARGET_OS_MACCATALYST
-    self->_webViewController = [[FIRAuthWebViewController alloc] initWithURL:URL delegate:self];
-    UINavigationController *navController =
-        [[UINavigationController alloc] initWithRootViewController:self->_webViewController];
-    [self->_UIDelegate presentViewController:navController animated:YES completion:nil];
-#else
-    if ([SFSafariViewController class]) {
-      self->_safariViewController = [[SFSafariViewController alloc] initWithURL:URL];
-      self->_safariViewController.delegate = self;
-      [self->_UIDelegate presentViewController:self->_safariViewController
-                                      animated:YES
-                                    completion:nil];
-      return;
-    } else {
-      self->_webViewController = [[FIRAuthWebViewController alloc] initWithURL:URL delegate:self];
-      UINavigationController *navController =
-          [[UINavigationController alloc] initWithRootViewController:self->_webViewController];
-      [self->_UIDelegate presentViewController:navController animated:YES completion:nil];
-    }
-#endif
-  });
-}
-
-- (BOOL)canHandleURL:(NSURL *)URL {
-  if (_isPresenting && _callbackMatcher && _callbackMatcher(URL)) {
-    [self finishPresentationWithURL:URL error:nil];
-    return YES;
-  }
-  return NO;
-}
-
-#pragma mark - SFSafariViewControllerDelegate
-
-- (void)safariViewControllerDidFinish:(SFSafariViewController *)controller {
-  dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
-    if (controller == self->_safariViewController) {
-      self->_safariViewController = nil;
-      // TODO:Ensure that the SFSafariViewController is actually removed from the screen before
-      // invoking finishPresentationWithURL:error:
-      [self finishPresentationWithURL:nil
-                                error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]];
-    }
-  });
-}
-
-#pragma mark - FIRAuthwebViewControllerDelegate
-
-- (BOOL)webViewController:(FIRAuthWebViewController *)webViewController canHandleURL:(NSURL *)URL {
-  __block BOOL result = NO;
-  dispatch_sync(FIRAuthGlobalWorkQueue(), ^() {
-    if (webViewController == self->_webViewController) {
-      result = [self canHandleURL:URL];
-    }
-  });
-  return result;
-}
-
-- (void)webViewControllerDidCancel:(FIRAuthWebViewController *)webViewController {
-  dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
-    if (webViewController == self->_webViewController) {
-      [self finishPresentationWithURL:nil
-                                error:[FIRAuthErrorUtils webContextCancelledErrorWithMessage:nil]];
-    }
-  });
-}
-
-- (void)webViewController:(FIRAuthWebViewController *)webViewController
-         didFailWithError:(NSError *)error {
-  dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
-    if (webViewController == self->_webViewController) {
-      [self finishPresentationWithURL:nil error:error];
-    }
-  });
-}
-
-#pragma mark - Private methods
-
-/** @fn finishPresentationWithURL:error:
-    @brief Finishes the presentation for a given URL, if any.
-    @param URL The URL to finish presenting.
-    @param error The error with which to finish presenting, if any.
- */
-- (void)finishPresentationWithURL:(nullable NSURL *)URL error:(nullable NSError *)error {
-  _callbackMatcher = nil;
-  id<FIRAuthUIDelegate> UIDelegate = _UIDelegate;
-  _UIDelegate = nil;
-  FIRAuthURLPresentationCompletion completion = [_completion copy];
-  _completion = NULL;
-  void (^finishBlock)(void) = ^() {
-    self->_isPresenting = NO;
-    completion(URL, error);
-  };
-  SFSafariViewController *safariViewController = _safariViewController;
-  _safariViewController = nil;
-  FIRAuthWebViewController *webViewController = _webViewController;
-  _webViewController = nil;
-  if (safariViewController || webViewController) {
-    dispatch_async(dispatch_get_main_queue(), ^() {
-      [UIDelegate dismissViewControllerAnimated:YES
-                                     completion:^() {
-                                       dispatch_async(FIRAuthGlobalWorkQueue(), finishBlock);
-                                     }];
-    });
-  } else {
-    finishBlock();
-  }
-}
-
-#pragma clang diagnostic pop  // ignored "-Wunguarded-availability"
-
-@end
-
-NS_ASSUME_NONNULL_END
-
-#endif  // TARGET_OS_IOS && (!defined(TARGET_OS_XR) || !TARGET_OS_XR)

+ 5 - 1
FirebaseAuth/Tests/Unit/FIRAuthURLPresenterTests.m

@@ -14,6 +14,9 @@
  * limitations under the License.
  */
 
+#ifdef TODO_SWIFT
+// TODO: Swiftify after AuthDefaultUIDelegate.swift implementation
+
 #import <TargetConditionals.h>
 #if TARGET_OS_IOS && (!defined(TARGET_OS_XR) || !TARGET_OS_XR)
 
@@ -144,4 +147,5 @@ static NSTimeInterval kExpectationTimeout = 2;
 
 @end
 
-#endif  // TARGET_OS_IOS && (!defined(TARGET_OS_XR) || !TARGET_OS_XR)
+#endif
+#endif

+ 9 - 0
FirebaseAuth/Tests/Unit/Fakes/FakeBackendRPCIssuer.swift

@@ -62,6 +62,11 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer {
    */
   var group: DispatchGroup?
 
+  /** @var verifyRequester
+      @brief Optional function to run tests on the request.
+   */
+  var verifyRequester: ((AuthRPCRequest) -> Void)?
+
   var fakeGetAccountProviderJSON: [[String: AnyHashable]]?
   var fakeSecureTokenServiceJSON: [String: AnyHashable]?
   var secureTokenNetworkError: Bool = false
@@ -75,6 +80,10 @@ class FakeBackendRPCIssuer: NSObject, AuthBackendRPCIssuer {
     self.request = request
     requestURL = request.requestURL()
 
+    if let verifyRequester {
+      verifyRequester(request)
+    }
+
     if let _ = request as? GetAccountInfoRequest,
        let json = fakeGetAccountProviderJSON {
       guard let _ = try? respond(withJSON: ["users": json]) else {

+ 12 - 0
FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift

@@ -139,6 +139,18 @@
       let expectation = self.expectation(description: #function)
       let group = createGroup()
 
+      if !testMode {
+        let requestExpectation = self.expectation(description: "verifyRequester")
+        rpcIssuer?.verifyRequester = { request in
+          let myRequest = request as? SendVerificationCodeRequest
+          XCTAssertNotNil(myRequest)
+          XCTAssertEqual(myRequest?.phoneNumber, self.kTestPhoneNumber)
+          XCTAssertEqual(myRequest?.appCredential?.receipt, self.kTestReceipt)
+          XCTAssertEqual(myRequest?.appCredential?.secret, self.kTestSecret)
+          requestExpectation.fulfill()
+        }
+      }
+
       provider.verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
         XCTAssertTrue(Thread.isMainThread)
         if valid {