diff --git a/GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h b/GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h index f0c77be9..2124d1cc 100644 --- a/GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.h +++ b/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 diff --git a/GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.m b/GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.m index c5615ac2..fa9888e6 100644 --- a/GoogleSignIn/Sources/GIDAppCheck/UI/GIDActivityIndicatorViewController.m +++ b/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 @@ @implementation GIDActivityIndicatorViewController - (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]; diff --git a/GoogleSignIn/Sources/GIDSignIn.m b/GoogleSignIn/Sources/GIDSignIn.m index d2ea2175..bf832d3c 100644 --- a/GoogleSignIn/Sources/GIDSignIn.m +++ b/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 @@ @implementation GIDSignIn { 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 @@ - (void)authorizationRequestWithOptions:(GIDSignInInternalOptions *)options comp [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 _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 _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); + } }]; } } diff --git a/GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h b/GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h new file mode 100644 index 00000000..bc108df0 --- /dev/null +++ b/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 +#import + +#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 diff --git a/GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.m b/GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.m new file mode 100644 index 00000000..c009dccf --- /dev/null +++ b/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 diff --git a/Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/project.pbxproj b/Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/project.pbxproj index 342a46fe..15473767 100644 --- a/Samples/Swift/AppAttestExample/AppAttestExample.xcodeproj/project.pbxproj +++ b/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 = ""; }; 73443A232A55F56900A4932E /* AppAttestExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AppAttestExample.entitlements; sourceTree = ""; }; 738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayLoader.swift; sourceTree = ""; }; + 73A065612A786D10007BC7FC /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; 73A464052A1C3B3400BA8528 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -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";