Procházet zdrojové kódy

Add a timed loader class (#331)

mdmathias před 2 roky
rodič
revize
18b5fbe51a

+ 0 - 1
GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h

@@ -22,7 +22,6 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-NS_CLASS_AVAILABLE_IOS(14.0)
 /// A `UIViewController` presented onscreen to indicate to the user that GSI is performing blocking
 /// work.
 @interface GIDActivityIndicatorViewController : UIViewController

+ 8 - 2
GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.m

@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#import "GIDActivityIndicatorViewController.h"
+#import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
@@ -25,7 +25,13 @@
 - (void)viewDidLoad {
   [super viewDidLoad];
 
-  _activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge];
+  UIActivityIndicatorViewStyle style;
+  if (@available(iOS 13.0, *)) {
+    style = UIActivityIndicatorViewStyleLarge;
+  } else {
+    style = UIActivityIndicatorViewStyleGray;
+  }
+  _activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style];
   self.activityIndicator.translatesAutoresizingMaskIntoConstraints = NO;
   [self.activityIndicator startAnimating];
   [self.view addSubview:self.activityIndicator];

+ 28 - 29
GoogleSignIn/Sources/GIDSignIn.m

@@ -33,6 +33,7 @@
 #import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
 #import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#import "GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h"
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
@@ -179,6 +180,10 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   BOOL _restarting;
   // Keychain manager for GTMAppAuth
   GTMKeychainStore *_keychainStore;
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  // The class used to manage presenting the loading screen for fetching app check tokens.
+  GIDTimedLoader *_timedLoader;
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 }
 
 #pragma mark - Public methods
@@ -632,37 +637,31 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
       [self additionalParametersFromOptions:options];
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   if (@available(iOS 14.0, *)) {
-    if (_appCheck) {
+    // Only use `_appCheck` (created via singleton `+[GIDSignIn sharedInstance]` call) if
+    // `-[GIDAppCheck prepareForAppCheckWithCompletion:]` has been called
+    if ([_appCheck isPrepared]) {
       shouldCallCompletion = NO;
-      GIDActivityIndicatorViewController *activityVC =
-          [[GIDActivityIndicatorViewController alloc] init];
-      [options.presentingViewController presentViewController:activityVC
-                                                     animated:true
-                                                   completion:^{
-        // Ensure that the activity indicator shows for at least 1/2 second to prevent "flashing"
-        // TODO: Re-implement per: https://github.com/google/GoogleSignIn-iOS/issues/329
-        dispatch_time_t halfSecond = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_MSEC / 2);
-        dispatch_after(halfSecond, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
-          [self->_appCheck getLimitedUseTokenWithCompletion:
-              ^(id<GACAppCheckTokenProtocol> _Nullable token, NSError * _Nullable error) {
-            if (token) {
-              additionalParameters[kClientAssertionTypeParameter] =
-                  kClientAssertionTypeParameterValue;
-              additionalParameters[kClientAssertionParameter] = token.token;
-              OIDAuthorizationRequest *request =
-                  [self authorizationRequestWithOptions:options
-                                   additionalParameters:additionalParameters];
-              [activityVC.activityIndicator stopAnimating];
-              [activityVC dismissViewControllerAnimated:YES completion:nil];
-              completion(request, nil);
-              return;
-            }
-            [activityVC.activityIndicator stopAnimating];
-            [activityVC dismissViewControllerAnimated:YES completion:nil];
-            completion(nil, error);
-            return;
+      UIViewController *presentingVC = options.presentingViewController;
+      if (!_timedLoader) {
+        _timedLoader = [[GIDTimedLoader alloc] initWithPresentingViewController:presentingVC];
+      }
+      [_timedLoader startTiming];
+      [self->_appCheck getLimitedUseTokenWithCompletion:
+          ^(id<GACAppCheckTokenProtocol> _Nullable token, NSError * _Nullable error) {
+        OIDAuthorizationRequest *request = nil;
+        if (token) {
+          additionalParameters[kClientAssertionTypeParameter] = kClientAssertionTypeParameterValue;
+          additionalParameters[kClientAssertionParameter] = token.token;
+          request = [self authorizationRequestWithOptions:options
+                                     additionalParameters:additionalParameters];
+        }
+        if (self->_timedLoader.animationStatus == GIDTimedLoaderAnimationStatusAnimating) {
+          [self->_timedLoader stopTimingWithCompletion:^{
+            completion(request, error);
           }];
-        });
+        } else {
+          completion(request, error);
+        }
       }];
     }
   }

+ 70 - 0
GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h

@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+/// An enumeration detailing the states of the timed loader.
+typedef NS_ENUM(NSUInteger, GIDTimedLoaderAnimationStatus) {
+  /// The timed loader has not started.
+  GIDTimedLoaderAnimationStatusNotStarted,
+  /// The timed loader's activity indicator is animating.
+  GIDTimedLoaderAnimationStatusAnimating,
+  /// The timed loader's activity indicator has stopped animating.
+  GIDTimedLoaderAnimationStatusStopped,
+};
+
+/// The minimum animation duration time for the timed loader's activity indicator.
+extern CFTimeInterval const kGIDTimedLoaderMinAnimationDuration;
+/// The maximum delay to wait before the time loader will display the loading activity indicator.
+extern CFTimeInterval const kGIDTimedLoaderMaxDelayBeforeAnimating;
+
+@class UIViewController;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// A type used to manage the presentation of a load screen for at least
+/// `kGIDTimedLoaderMinAnimationDuration` to prevent flashing.
+///
+/// `GIDTimedLoader` will also only show its loading screen until
+/// `kGIDTimedLoaderMaxDelayBeforeAnimating` has expired.
+@interface GIDTimedLoader : NSObject
+
+/// Created this timed loading controller with the provided presenting view controller, which will
+/// be used for presenting hte loading view controller with the activity indicator.
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/// Tells the controller to start keeping track of loading time.
+- (void)startTiming;
+
+/// Tells the controller to stop keeping track of loading time.
+///
+/// @param completion The block to invoke upon successfully stopping.
+/// @note Use the completion parameter to, for example, present the UI that should be shown after
+///     the work has completed.
+- (void)stopTimingWithCompletion:(void (^)(void))completion;
+
+@property(nonatomic) GIDTimedLoaderAnimationStatus animationStatus;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 116 - 0
GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.m

@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+
+#import "GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h"
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+@import UIKit;
+@import CoreMedia;
+#import "GoogleSignIn/Sources/GIDAppCheck/Implementations/GIDAppCheck.h"
+#import "GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h"
+
+CFTimeInterval const kGIDTimedLoaderMinAnimationDuration = 1.0;
+CFTimeInterval const kGIDTimedLoaderMaxDelayBeforeAnimating = 0.5;
+
+@interface GIDTimedLoader ()
+
+@property(nonatomic, strong) UIViewController *presentingViewController;
+@property(nonatomic, strong) GIDActivityIndicatorViewController *loadingViewController;
+@property(nonatomic, strong, nullable) NSTimer *loadingTimer;
+/// Timestamp representing when the loading view controller was presented and started animating
+@property(nonatomic) CFTimeInterval loadingTimeStamp;
+
+@end
+
+@implementation GIDTimedLoader
+
+- (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController {
+  if (self = [super init]) {
+    _presentingViewController = presentingViewController;
+    _loadingViewController = [[GIDActivityIndicatorViewController alloc] init];
+    _animationStatus = GIDTimedLoaderAnimationStatusNotStarted;
+  }
+  return self;
+}
+
+- (void)startTiming {
+  if (self.animationStatus == GIDTimedLoaderAnimationStatusAnimating) {
+    return;
+  }
+
+  self.animationStatus = GIDTimedLoaderAnimationStatusAnimating;
+  self.loadingTimer = [NSTimer scheduledTimerWithTimeInterval:kGIDTimedLoaderMaxDelayBeforeAnimating
+                                                       target:self
+                                                     selector:@selector(presentLoadingViewController)
+                                                     userInfo:nil
+                                                      repeats:NO];
+}
+
+- (void)presentLoadingViewController {
+  if (self.animationStatus == GIDTimedLoaderAnimationStatusStopped) {
+    return;
+  }
+  self.animationStatus = GIDTimedLoaderAnimationStatusAnimating;
+  self.loadingTimeStamp = CACurrentMediaTime();
+  dispatch_async(dispatch_get_main_queue(), ^{
+    // Since this loading VC may be reused, the activity indicator may have been stopped; restart it
+    [self.loadingViewController.activityIndicator startAnimating];
+    [self.presentingViewController presentViewController:self.loadingViewController
+                                                animated:YES
+                                              completion:nil];
+  });
+}
+
+- (void)stopTimingWithCompletion:(void (^)(void))completion {
+  if (self.animationStatus != GIDTimedLoaderAnimationStatusAnimating) {
+    return;
+  }
+
+  [self.loadingTimer invalidate];
+  self.loadingTimer = nil;
+
+  dispatch_time_t deadline = [self remainingDurationToAnimate];
+  dispatch_after(deadline, dispatch_get_main_queue(), ^{
+    self.animationStatus = GIDTimedLoaderAnimationStatusStopped;
+    [self.loadingViewController.activityIndicator stopAnimating];
+    [self.loadingViewController dismissViewControllerAnimated:YES completion:nil];
+    completion();
+  });
+}
+
+- (dispatch_time_t)remainingDurationToAnimate {
+  // If we are not animating, then no need to wait
+  if (self.animationStatus != GIDTimedLoaderAnimationStatusAnimating) {
+    return 0;
+  }
+
+  CFTimeInterval now = CACurrentMediaTime();
+  CFTimeInterval durationWaited = now - self.loadingTimeStamp;
+  // If we have already waited for the minimum animation duration, then no need to wait
+  if (durationWaited >= kGIDTimedLoaderMinAnimationDuration) {
+    return 0;
+  }
+
+  CFTimeInterval diff = kGIDTimedLoaderMinAnimationDuration - durationWaited;
+  int64_t diffNanos = diff * NSEC_PER_SEC;
+  dispatch_time_t timeToWait = dispatch_time(DISPATCH_TIME_NOW, diffNanos);
+  return timeToWait;
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 8 - 0
Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/project.pbxproj

@@ -18,6 +18,7 @@
 		1C96B5B2B34E31F1A1CEE95E /* Pods-AppAttestExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppAttestExample.release.xcconfig"; path = "Target Support Files/Pods-AppAttestExample/Pods-AppAttestExample.release.xcconfig"; sourceTree = "<group>"; };
 		73443A232A55F56900A4932E /* AppAttestExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppAttestExample.entitlements; sourceTree = "<group>"; };
 		738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayLoader.swift; sourceTree = "<group>"; };
+		73A065612A786D10007BC7FC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		73A464002A1C3B3400BA8528 /* AppAttestExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppAttestExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAttestExampleApp.swift; sourceTree = "<group>"; };
 		73A464052A1C3B3400BA8528 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -72,6 +73,7 @@
 				73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */,
 				73A464052A1C3B3400BA8528 /* ContentView.swift */,
 				738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */,
+				73A065612A786D10007BC7FC /* Info.plist */,
 				73A464092A1C3B3500BA8528 /* Preview Content */,
 			);
 			path = AppAttestExample;
@@ -339,10 +341,12 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = AppAttestExample/AppAttestExample.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Manual;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "\"AppAttestExample/Preview Content\"";
 				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = AppAttestExample/Info.plist;
@@ -359,6 +363,7 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
+				"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Experimental App 0 Dev";
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
@@ -373,10 +378,12 @@
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
 				CODE_SIGN_ENTITLEMENTS = AppAttestExample/AppAttestExample.entitlements;
 				CODE_SIGN_IDENTITY = "Apple Development";
+				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				CODE_SIGN_STYLE = Manual;
 				CURRENT_PROJECT_VERSION = 1;
 				DEVELOPMENT_ASSET_PATHS = "\"AppAttestExample/Preview Content\"";
 				DEVELOPMENT_TEAM = "";
+				"DEVELOPMENT_TEAM[sdk=iphoneos*]" = EQHXZ8M8AV;
 				ENABLE_PREVIEWS = YES;
 				GENERATE_INFOPLIST_FILE = YES;
 				INFOPLIST_FILE = AppAttestExample/Info.plist;
@@ -393,6 +400,7 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev;
 				PRODUCT_NAME = "$(TARGET_NAME)";
 				PROVISIONING_PROFILE_SPECIFIER = "";
+				"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "Experimental App 0 Dev";
 				SWIFT_EMIT_LOC_STRINGS = YES;
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";