Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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];
Expand Down
57 changes: 28 additions & 29 deletions GoogleSignIn/Sources/GIDSignIn.m
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<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);
}
}];
}
}
Expand Down
70 changes: 70 additions & 0 deletions GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.h
Original file line number Diff line number Diff line change
@@ -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 changes: 116 additions & 0 deletions GoogleSignIn/Sources/GIDTimedLoader/GIDTimedLoader.m
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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>"; };
Expand Down Expand Up @@ -72,6 +73,7 @@
73A464032A1C3B3400BA8528 /* AppAttestExampleApp.swift */,
73A464052A1C3B3400BA8528 /* ContentView.swift */,
738D5F722A26BC3B00A7F11B /* BirthdayLoader.swift */,
73A065612A786D10007BC7FC /* Info.plist */,
73A464092A1C3B3500BA8528 /* Preview Content */,
);
path = AppAttestExample;
Expand Down Expand Up @@ -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;
Expand All @@ -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";
Expand All @@ -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;
Expand All @@ -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";
Expand Down