Forráskód Böngészése

Use GTMAppAuth's new delegate protocol (#299)

mdmathias 2 éve
szülő
commit
55bf5d077b
34 módosított fájl, 694 hozzáadás és 396 törlés
  1. 2 3
      .github/workflows/unit_tests.yml
  2. 3 2
      GoogleSignIn.podspec
  3. 0 129
      GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m
  4. 10 4
      GoogleSignIn/Sources/GIDAuthStateMigration.h
  5. 45 23
      GoogleSignIn/Sources/GIDAuthStateMigration.m
  6. 9 5
      GoogleSignIn/Sources/GIDEMMSupport.h
  7. 34 0
      GoogleSignIn/Sources/GIDEMMSupport.m
  8. 17 24
      GoogleSignIn/Sources/GIDGoogleUser.m
  9. 3 6
      GoogleSignIn/Sources/GIDGoogleUser_Private.h
  10. 26 21
      GoogleSignIn/Sources/GIDSignIn.m
  11. 4 0
      GoogleSignIn/Sources/GIDSignIn_Private.h
  12. 2 4
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h
  13. 84 67
      GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m
  14. 1 16
      GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m
  15. 80 0
      GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m
  16. 4 11
      GoogleSignIn/Tests/Unit/GIDFailingOIDAuthState.h
  17. 54 0
      GoogleSignIn/Tests/Unit/GIDFailingOIDAuthState.m
  18. 8 2
      GoogleSignIn/Tests/Unit/GIDFakeFetcher.h
  19. 20 0
      GoogleSignIn/Tests/Unit/GIDFakeFetcher.m
  20. 13 1
      GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h
  21. 13 2
      GoogleSignIn/Tests/Unit/GIDFakeFetcherService.m
  22. 6 0
      GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h
  23. 3 2
      GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m
  24. 139 57
      GoogleSignIn/Tests/Unit/GIDSignInTest.m
  25. 1 0
      GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m
  26. 34 0
      GoogleSignIn/Tests/Unit/UIAlertAction+Testing.h
  27. 29 0
      GoogleSignIn/Tests/Unit/UIAlertAction+Testing.m
  28. 1 1
      GoogleSignInSwiftSupport.podspec
  29. 2 2
      Package.swift
  30. 1 0
      Samples/ObjC/SignInSample/Podfile
  31. 18 6
      Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj
  32. 18 0
      Samples/ObjC/SignInSample/Source/Empty.swift
  33. 8 8
      Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayForPod.xcodeproj/project.pbxproj
  34. 2 0
      Samples/Swift/DaysUntilBirthday/Podfile

+ 2 - 3
.github/workflows/unit_tests.yml

@@ -17,8 +17,7 @@ jobs:
         os: [macos-12]
         podspec: [GoogleSignIn.podspec, GoogleSignInSwiftSupport.podspec]
         flag: [
-          "", 
-          "--use-libraries", 
+          "",
           "--use-static-frameworks"
         ]
         include:
@@ -33,7 +32,7 @@ jobs:
     - name: Lint podspec using local source
       run: |
         pod lib lint ${{ matrix.podspec }} --verbose \
-           ${{ matrix.includePodspecFlag }}  ${{ matrix.flag }}
+           ${{ matrix.includePodspecFlag }} ${{ matrix.flag }}
 
   spm-build-test:
     runs-on: ${{ matrix.os }}

+ 3 - 2
GoogleSignIn.podspec

@@ -12,6 +12,7 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
     :git => 'https://github.com/google/GoogleSignIn-iOS.git',
     :tag => s.version.to_s
   }
+  s.swift_version = '4.0'
   ios_deployment_target = '10.0'
   osx_deployment_target = '10.15'
   s.ios.deployment_target = ios_deployment_target
@@ -32,8 +33,8 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
   ]
   s.ios.framework = 'UIKit'
   s.osx.framework = 'AppKit'
-  s.dependency 'AppAuth', '~> 1.5'
-  s.dependency 'GTMAppAuth', '>= 1.3', '< 3.0'
+  s.dependency 'AppAuth', '~> 1.6'
+  s.dependency 'GTMAppAuth', '~> 4.0'
   s.dependency 'GTMSessionFetcher/Core', '>= 1.1', '< 4.0'
   s.resource_bundle = {
     'GoogleSignIn' => ['GoogleSignIn/Sources/{Resources,Strings}/*']

+ 0 - 129
GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.m

@@ -1,129 +0,0 @@
-/*
- * Copyright 2022 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 <TargetConditionals.h>
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
-#import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h"
-
-#import "GoogleSignIn/Sources/GIDEMMSupport.h"
-
-#ifdef SWIFT_PACKAGE
-@import AppAuth;
-@import GTMAppAuth;
-#else
-#import <AppAuth/AppAuth.h>
-#import <GTMAppAuth/GTMAppAuth.h>
-#endif
-
-NS_ASSUME_NONNULL_BEGIN
-
-// The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error
-// responses.
-@interface GIDAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject
-
-// Initializes with chained delegate and selector.
-- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector;
-
-// The callback method for GTMAppAuthFetcherAuthorization to invoke.
-- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
-               request:(NSMutableURLRequest *)request
-     finishedWithError:(nullable NSError *)error;
-
-@end
-
-@implementation GIDAppAuthFetcherAuthorizationEMMChainedDelegate {
-  // We use a weak reference here to match GTMAppAuthFetcherAuthorization.
-  __weak id _delegate;
-  SEL _selector;
-  // We need to maintain a reference to the chained delegate because GTMAppAuthFetcherAuthorization
-  // only keeps a weak reference.
-  GIDAppAuthFetcherAuthorizationEMMChainedDelegate *_retained_self;
-}
-
-- (instancetype)initWithDelegate:(id)delegate selector:(SEL)selector {
-  self = [super init];
-  if (self) {
-    _delegate = delegate;
-    _selector = selector;
-    _retained_self = self;
-  }
-  return self;
-}
-
-- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
-               request:(NSMutableURLRequest *)request
-     finishedWithError:(nullable NSError *)error {
-  [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
-    if (!self->_delegate || !self->_selector) {
-      return;
-    }
-    NSMethodSignature *signature = [self->_delegate methodSignatureForSelector:self->_selector];
-    if (!signature) {
-      return;
-    }
-    id argument1 = auth;
-    id argument2 = request;
-    id argument3 = error;
-    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
-    [invocation setTarget:self->_delegate];  // index 0
-    [invocation setSelector:self->_selector];  // index 1
-    [invocation setArgument:&argument1 atIndex:2];
-    [invocation setArgument:&argument2 atIndex:3];
-    [invocation setArgument:&argument3 atIndex:4];
-    [invocation invoke];
-  }];
-  // Prepare to deallocate the chained delegate instance because the above block will retain the
-  // iVar references it uses.
-  _retained_self = nil;
-}
-
-@end
-
-@implementation GIDAppAuthFetcherAuthorizationWithEMMSupport
-
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-implementations"
-- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
-                delegate:(id)delegate
-       didFinishSelector:(SEL)sel {
-#pragma clang diagnostic pop
-  GIDAppAuthFetcherAuthorizationEMMChainedDelegate *chainedDelegate =
-      [[GIDAppAuthFetcherAuthorizationEMMChainedDelegate alloc] initWithDelegate:delegate
-                                                                        selector:sel];
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-  [super authorizeRequest:request
-                 delegate:chainedDelegate
-        didFinishSelector:@selector(authentication:request:finishedWithError:)];
-#pragma clang diagnostic pop
-}
-
-- (void)authorizeRequest:(nullable NSMutableURLRequest *)request
-       completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler {
-  [super authorizeRequest:request completionHandler:^(NSError *_Nullable error) {
-    [GIDEMMSupport handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
-      handler(error);
-    }];
-  }];
-}
-
-@end
-
-NS_ASSUME_NONNULL_END
-
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 10 - 4
GoogleSignIn/Sources/GIDAuthStateMigration.h

@@ -16,14 +16,20 @@
 
 #import <Foundation/Foundation.h>
 
+@class GTMKeychainStore;
+@class GTMAuthSession;
+
 NS_ASSUME_NONNULL_BEGIN
 
-// A class providing migration support for auth state saved by older versions of the SDK.
+/// A class providing migration support for auth state saved by older versions of the SDK.
 @interface GIDAuthStateMigration : NSObject
 
-// Perform a one-time migration for auth state saved by GPPSignIn 1.x or GIDSignIn 1.0 - 4.x to the
-// GTMAppAuth storage introduced in GIDSignIn 5.0.
-+ (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
+/// Creates an instance of this migration type with the keychain storage wrapper it will use.
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore NS_DESIGNATED_INITIALIZER;
+
+/// Perform a one-time migration for auth state saved by GPPSignIn 1.x or GIDSignIn 1.0 - 4.x to the
+/// GTMAppAuth storage introduced in GIDSignIn 5.0.
+- (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
                        callbackPath:(NSString *)callbackPath
                        keychainName:(NSString *)keychainName
                      isFreshInstall:(BOOL)isFreshInstall;

+ 45 - 23
GoogleSignIn/Sources/GIDAuthStateMigration.m

@@ -16,13 +16,12 @@
 
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 
+@import GTMAppAuth;
+
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
-@import GTMAppAuth;
 #else
 #import <AppAuth/AppAuth.h>
-#import <GTMAppAuth/GTMAppAuth.h>
-#import <GTMAppAuth/GTMKeychain.h>
 #endif
 
 NS_ASSUME_NONNULL_BEGIN
@@ -39,9 +38,28 @@ static NSString *const kGenericAttribute = @"OAuth";
 // Keychain service name used to store the last used fingerprint value.
 static NSString *const kFingerprintService = @"fingerprint";
 
+@interface GIDAuthStateMigration ()
+
+@property (nonatomic, strong) GTMKeychainStore *keychainStore;
+
+@end
+
 @implementation GIDAuthStateMigration
 
-+ (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
+  self = [super init];
+  if (self) {
+    _keychainStore = keychainStore;
+  }
+  return self;
+}
+
+- (instancetype)init {
+  GTMKeychainStore *keychainStore = [[GTMKeychainStore alloc] initWithItemName:@"auth"];
+  return [self initWithKeychainStore:keychainStore];
+}
+
+- (void)migrateIfNeededWithTokenURL:(NSURL *)tokenURL
                        callbackPath:(NSString *)callbackPath
                        keychainName:(NSString *)keychainName
                      isFreshInstall:(BOOL)isFreshInstall {
@@ -55,14 +73,15 @@ static NSString *const kFingerprintService = @"fingerprint";
   // action and go on to mark the migration check as having been performed.
   if (!isFreshInstall) {
     // Attempt migration
-    GTMAppAuthFetcherAuthorization *authorization =
-        [self extractAuthorizationWithTokenURL:tokenURL callbackPath:callbackPath];
+    GTMAuthSession *authSession =
+        [self extractAuthSessionWithTokenURL:tokenURL callbackPath:callbackPath];
 
     // If migration was successful, save our migrated state to the keychain.
-    if (authorization) {
+    if (authSession) {
+      NSError *err;
+      [self.keychainStore saveAuthSession:authSession error:&err];
       // If we're unable to save to the keychain, return without marking migration performed.
-      if (![GTMAppAuthFetcherAuthorization saveAuthorization:authorization
-                                           toKeychainForName:keychainName]) {
+      if (err) {
         return;
       };
     }
@@ -72,10 +91,10 @@ static NSString *const kFingerprintService = @"fingerprint";
   [defaults setBool:YES forKey:kMigrationCheckPerformedKey];
 }
 
-// Returns a |GTMAppAuthFetcherAuthorization| object containing any old auth state or |nil| if none
+// Returns a |GTMAuthSession| object containing any old auth state or |nil| if none
 // was found or the migration failed.
-+ (nullable GTMAppAuthFetcherAuthorization *)
-    extractAuthorizationWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath {
+- (nullable GTMAuthSession *)extractAuthSessionWithTokenURL:(NSURL *)tokenURL
+                                               callbackPath:(NSString *)callbackPath {
   // Retrieve the last used fingerprint.
   NSString *fingerprint = [GIDAuthStateMigration passwordForService:kFingerprintService];
   if (!fingerprint) {
@@ -83,8 +102,10 @@ static NSString *const kFingerprintService = @"fingerprint";
   }
 
   // Retrieve the GTMOAuth2 persistence string.
-  NSString *GTMOAuth2PersistenceString = [GTMKeychain passwordFromKeychainForName:fingerprint];
-  if (!GTMOAuth2PersistenceString) {
+  NSError *passwordError;
+  NSString *GTMOAuth2PersistenceString =
+      [self.keychainStore.keychainHelper passwordForService:fingerprint error:&passwordError];
+  if (passwordError) {
     return nil;
   }
 
@@ -126,16 +147,17 @@ static NSString *const kFingerprintService = @"fingerprint";
                          additionalTokenRequestParameters];
   }
 
-  // Use |GTMOAuth2KeychainCompatibility| to generate a |GTMAppAuthFetcherAuthorization| from the
+  // Use |GTMOAuth2Compatibility| to generate a |GTMAuthSession| from the
   // persistence string, redirect URI, client ID, and token endpoint URL.
-  GTMAppAuthFetcherAuthorization *authorization = [GTMOAuth2KeychainCompatibility
-      authorizeFromPersistenceString:persistenceString
-                            tokenURL:tokenURL
-                         redirectURI:redirectURI
-                            clientID:clientID
-                        clientSecret:nil];
-
-  return authorization;
+  GTMAuthSession *authSession =
+      [GTMOAuth2Compatibility authSessionForPersistenceString:persistenceString
+                                                     tokenURL:tokenURL
+                                                  redirectURI:redirectURI
+                                                     clientID:clientID
+                                                 clientSecret:nil
+                                                        error:nil];
+
+  return authSession;
 }
 
 // Returns the password string for a given service string stored by an old version of the SDK or

+ 9 - 5
GoogleSignIn/Sources/GIDEMMSupport.h

@@ -20,19 +20,23 @@
 
 #import <Foundation/Foundation.h>
 
+@import GTMAppAuth;
+
 NS_ASSUME_NONNULL_BEGIN
 
-// A class to support EMM (Enterprise Mobility Management).
-@interface GIDEMMSupport : NSObject
+/// A class to support EMM (Enterprise Mobility Management).
+@interface GIDEMMSupport : NSObject<GTMAuthSessionDelegate>
+
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
 
-// Handles potential EMM error from token fetch response.
+/// Handles potential EMM error from token fetch response.
 + (void)handleTokenFetchEMMError:(nullable NSError *)error
                       completion:(void (^)(NSError *_Nullable))completion;
 
-// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed.
+/// Gets a new set of URL parameters that contains updated EMM-related URL parameters if needed.
 + (NSDictionary *)updatedEMMParametersWithParameters:(NSDictionary *)parameters;
 
-// Gets a new set of URL parameters that also contains EMM-related URL parameters if needed.
+/// Gets a new set of URL parameters that also contains EMM-related URL parameters if needed.
 + (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
                                 emmSupport:(nullable NSString *)emmSupport
                     isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired;

+ 34 - 0
GoogleSignIn/Sources/GIDEMMSupport.m

@@ -44,8 +44,26 @@ static NSString *const kOldIOSSystemName = @"iPhone OS";
 // New UIDevice system name for iOS.
 static NSString *const kNewIOSSystemName = @"iOS";
 
+// The error key in the server response.
+static NSString *const kErrorKey = @"error";
+
+// Optional separator between error prefix and the payload.
+static NSString *const kErrorPayloadSeparator = @":";
+
+// A list for recognized error codes.
+typedef NS_ENUM(NSInteger, ErrorCode) {
+  ErrorCodeNone = 0,
+  ErrorCodeDeviceNotCompliant,
+  ErrorCodeScreenlockRequired,
+  ErrorCodeAppVerificationRequired,
+};
+
 @implementation GIDEMMSupport
 
+- (instancetype)init {
+  return [super init];
+}
+
 + (void)handleTokenFetchEMMError:(nullable NSError *)error
                       completion:(void (^)(NSError *_Nullable))completion {
   NSDictionary *errorJSON = error.userInfo[OIDOAuthErrorResponseErrorKey];
@@ -94,6 +112,22 @@ static NSString *const kNewIOSSystemName = @"iOS";
   return allParameters;
 }
 
+#pragma mark - GTMAuthSessionDelegate
+
+- (nullable NSDictionary<NSString *,NSString *> *)
+additionalTokenRefreshParametersForAuthSession:(GTMAuthSession *)authSession {
+  return [GIDEMMSupport updatedEMMParametersWithParameters:
+          authSession.authState.lastTokenResponse.additionalParameters];
+}
+
+- (void)updateErrorForAuthSession:(GTMAuthSession *)authSession
+                    originalError:(NSError *)originalError
+                       completion:(void (^)(NSError * _Nullable))completion {
+  [GIDEMMSupport handleTokenFetchEMMError:originalError completion:^(NSError *_Nullable error) {
+    completion(error);
+  }];
+}
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 17 - 24
GoogleSignIn/Sources/GIDGoogleUser.m

@@ -19,7 +19,6 @@
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDConfiguration.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
 
-#import "GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDAuthentication.h"
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDProfileData_Private.h"
@@ -27,6 +26,8 @@
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
 #import "GoogleSignIn/Sources/GIDToken_Private.h"
 
+@import GTMAppAuth;
+
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
 #else
@@ -52,6 +53,14 @@ static NSString *const kEMMSupportParameterName = @"emm_support";
 // Minimal time interval before expiration for the access token or it needs to be refreshed.
 static NSTimeInterval const kMinimalTimeToExpire = 60.0;
 
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+@interface GIDGoogleUser ()
+
+@property (nonatomic, strong) id<GTMAuthSessionDelegate> authSessionDelegate;
+
+@end
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
 @implementation GIDGoogleUser {
   GIDConfiguration *_cachedConfiguration;
   
@@ -179,8 +188,8 @@ static NSTimeInterval const kMinimalTimeToExpire = 60.0;
   }];
 }
 
-- (OIDAuthState *) authState{
-  return ((GTMAppAuthFetcherAuthorization *)self.fetcherAuthorizer).authState;
+- (OIDAuthState *)authState {
+  return ((GTMAuthSession *)self.fetcherAuthorizer).authState;
 }
 
 - (void)addScopes:(NSArray<NSString *> *)scopes
@@ -228,17 +237,13 @@ static NSTimeInterval const kMinimalTimeToExpire = 60.0;
     _tokenRefreshHandlerQueue = [[NSMutableArray alloc] init];
     _profile = profileData;
     
+    GTMAuthSession *authSession = [[GTMAuthSession alloc] initWithAuthState:authState];
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-    GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ?
-        [[GIDAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:authState] :
-        [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-    GTMAppAuthFetcherAuthorization *authorization =
-        [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
+    _authSessionDelegate = [[GIDEMMSupport alloc] init];
+    authSession.delegate = _authSessionDelegate;
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-    authorization.tokenRefreshDelegate = self;
-    authorization.authState.stateChangeDelegate = self;
-    self.fetcherAuthorizer = authorization;
+    authSession.authState.stateChangeDelegate = self;
+    _fetcherAuthorizer = authSession;
     
     [self updateTokensWithAuthState:authState];
   }
@@ -304,18 +309,6 @@ static NSTimeInterval const kMinimalTimeToExpire = 60.0;
   return nil;
 }
 
-#pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate
-
-- (nullable NSDictionary *)additionalRefreshParameters:
-    (GTMAppAuthFetcherAuthorization *)authorization {
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-  return [GIDEMMSupport updatedEMMParametersWithParameters:
-      authorization.authState.lastTokenResponse.request.additionalParameters];
-#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
-  return authorization.authState.lastTokenResponse.request.additionalParameters;
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-}
-
 #pragma mark - OIDAuthStateChangeDelegate
 
 - (void)didChangeState:(OIDAuthState *)state {

+ 3 - 6
GoogleSignIn/Sources/GIDGoogleUser_Private.h

@@ -18,10 +18,8 @@
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
-@import GTMAppAuth;
 #else
 #import <AppAuth/AppAuth.h>
-#import <GTMAppAuth/GTMAppAuth.h>
 #endif
 
 @class OIDAuthState;
@@ -31,9 +29,8 @@ NS_ASSUME_NONNULL_BEGIN
 /// A completion block that takes a `GIDGoogleUser` or an error if the attempt to refresh tokens was unsuccessful.
 typedef void (^GIDGoogleUserCompletion)(GIDGoogleUser *_Nullable user, NSError *_Nullable error);
 
-// Internal methods for the class that are not part of the public API.
-@interface GIDGoogleUser () <GTMAppAuthFetcherAuthorizationTokenRefreshDelegate,
-                             OIDAuthStateChangeDelegate>
+/// Internal methods for the class that are not part of the public API.
+@interface GIDGoogleUser () <OIDAuthStateChangeDelegate>
 
 @property(nonatomic, readwrite) GIDToken *accessToken;
 
@@ -41,7 +38,7 @@ typedef void (^GIDGoogleUserCompletion)(GIDGoogleUser *_Nullable user, NSError *
 
 @property(nonatomic, readwrite, nullable) GIDToken *idToken;
 
-// A representation of the state of the OAuth session for this instance.
+/// A representation of the state of the OAuth session for this instance.
 @property(nonatomic, readonly) OIDAuthState *authState;
 
 #pragma clang diagnostic push

+ 26 - 21
GoogleSignIn/Sources/GIDSignIn.m

@@ -36,9 +36,10 @@
 #import "GoogleSignIn/Sources/GIDProfileData_Private.h"
 #import "GoogleSignIn/Sources/GIDSignInResult_Private.h"
 
+@import GTMAppAuth;
+
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
-@import GTMAppAuth;
 @import GTMSessionFetcherCore;
 #else
 #import <AppAuth/OIDAuthState.h>
@@ -53,8 +54,6 @@
 #import <AppAuth/OIDTokenRequest.h>
 #import <AppAuth/OIDTokenResponse.h>
 #import <AppAuth/OIDURLQueryComponent.h>
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #import <GTMSessionFetcher/GTMSessionFetcher.h>
 
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
@@ -166,6 +165,8 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
   id<OIDExternalUserAgentSession> _currentAuthorizationFlow;
   // Flag to indicate that the auth flow is restarting.
   BOOL _restarting;
+  // Keychain manager for GTMAppAuth
+  GTMKeychainStore *_keychainStore;
 }
 
 #pragma mark - Public methods
@@ -449,7 +450,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 
 #pragma mark - Private methods
 
-- (id)initPrivate {
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore {
   self = [super init];
   if (self) {
     // Get the bundle of the current executable.
@@ -459,7 +460,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     if (bundle) {
       _configuration = [GIDSignIn configurationFromBundle:bundle];
     }
-    
+
     // Check to see if the 3P app is being run for the first time after a fresh install.
     BOOL isFreshInstall = [self isFreshInstall];
 
@@ -475,18 +476,27 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     _appAuthConfiguration = [[OIDServiceConfiguration alloc]
         initWithAuthorizationEndpoint:[NSURL URLWithString:authorizationEnpointURL]
                         tokenEndpoint:[NSURL URLWithString:tokenEndpointURL]];
+    _keychainStore = keychainStore;
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
     // Perform migration of auth state from old (before 5.0) versions of the SDK if needed.
-    [GIDAuthStateMigration migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint
-                                          callbackPath:kBrowserCallbackPath
-                                          keychainName:kGTMAppAuthKeychainName
-                                        isFreshInstall:isFreshInstall];
+    GIDAuthStateMigration *migration =
+        [[GIDAuthStateMigration alloc] initWithKeychainStore:_keychainStore];
+    [migration migrateIfNeededWithTokenURL:_appAuthConfiguration.tokenEndpoint
+                              callbackPath:kBrowserCallbackPath
+                              keychainName:kGTMAppAuthKeychainName
+                            isFreshInstall:isFreshInstall];
 #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST
   }
   return self;
 }
 
+- (instancetype)initPrivate {
+  GTMKeychainStore *keychainStore =
+      [[GTMKeychainStore alloc] initWithItemName:kGTMAppAuthKeychainName];
+  return [self initWithKeychainStore:keychainStore];
+}
+
 // Does sanity check for parameters and then authenticates if necessary.
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options {
   // Options for continuation are not the options we want to cache. The purpose of caching the
@@ -872,8 +882,7 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
     withCompletionHandler:(void (^)(NSData *, NSError *))handler {
   NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:URL];
   GTMSessionFetcher *fetcher;
-  GTMAppAuthFetcherAuthorization *authorization =
-      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
+  GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState];
   id<GTMSessionFetcherServiceProtocol> fetcherService = authorization.fetcherService;
   if (fetcherService) {
     fetcher = [fetcherService fetcherWithRequest:request];
@@ -977,22 +986,18 @@ static NSString *const kConfigOpenIDRealmKey = @"GIDOpenIDRealm";
 }
 
 - (void)removeAllKeychainEntries {
-  [GTMAppAuthFetcherAuthorization removeAuthorizationFromKeychainForName:kGTMAppAuthKeychainName
-                                               useDataProtectionKeychain:YES];
+  [_keychainStore removeAuthSessionWithError:nil];
 }
 
 - (BOOL)saveAuthState:(OIDAuthState *)authState {
-  GTMAppAuthFetcherAuthorization *authorization =
-      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:authState];
-  return [GTMAppAuthFetcherAuthorization saveAuthorization:authorization
-                                         toKeychainForName:kGTMAppAuthKeychainName
-                                 useDataProtectionKeychain:YES];
+  GTMAuthSession *authorization = [[GTMAuthSession alloc] initWithAuthState:authState];
+  NSError *error;
+  [_keychainStore saveAuthSession:authorization error:&error];
+  return error == nil;
 }
 
 - (OIDAuthState *)loadAuthState {
-  GTMAppAuthFetcherAuthorization *authorization =
-      [GTMAppAuthFetcherAuthorization authorizationFromKeychainForName:kGTMAppAuthKeychainName
-                                             useDataProtectionKeychain:YES];
+  GTMAuthSession *authorization = [_keychainStore retrieveAuthSessionWithError:nil];
   return authorization.authState;
 }
 

+ 4 - 0
GoogleSignIn/Sources/GIDSignIn_Private.h

@@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @class GIDGoogleUser;
 @class GIDSignInInternalOptions;
+@class GTMKeychainStore;
 
 /// Represents a completion block that takes a `GIDSignInResult` on success or an error if the
 /// operation was unsuccessful.
@@ -46,6 +47,9 @@ typedef void (^GIDDisconnectCompletion)(NSError *_Nullable error);
 /// Private initializer for |GIDSignIn|.
 - (instancetype)initPrivate;
 
+/// Private initializer taking a `GTMKeychainStore` to use during tests.
+- (instancetype)initWithKeychainStore:(GTMKeychainStore *)keychainStore;
+
 /// Authenticates with extra options.
 - (void)signInWithOptions:(GIDSignInInternalOptions *)options;
 

+ 2 - 4
GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h

@@ -23,12 +23,10 @@
 #import <AppKit/AppKit.h>
 #endif
 
-// We have to import GTMAppAuth because forward declaring the protocol does
-// not generate the `fetcherAuthorizer` property below for Swift.
 #ifdef SWIFT_PACKAGE
-@import GTMAppAuth;
+@import GTMSessionFetcherCore;
 #else
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
+#import <GTMSessionFetcher/GTMSessionFetcher.h>
 #endif
 
 @class GIDConfiguration;

+ 84 - 67
GoogleSignIn/Tests/Unit/GIDAuthStateMigrationTest.m

@@ -17,15 +17,13 @@
 #import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 
+@import GTMAppAuth;
+
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
-@import GTMAppAuth;
 @import OCMock;
 #else
 #import <AppAuth/AppAuth.h>
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
-#import <GTMAppAuth/GTMKeychain.h>
-#import <GTMAppAuth/GTMOAuth2KeychainCompatibility.h>
 #import <OCMock/OCMock.h>
 #endif
 
@@ -58,11 +56,15 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface GIDAuthStateMigration ()
 
-+ (nullable GTMAppAuthFetcherAuthorization *)
-    extractAuthorizationWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath;
-
 + (nullable NSString *)passwordForService:(NSString *)service;
 
+/// Returns a `GTMAuthSession` given the provided token URL.
+///
+/// This method enables using an instance of `GIDAuthStateMigration` that is created with a fake
+/// `GTMKeychainStore` and thereby minimizes mocking.
+- (nullable GTMAuthSession *)
+    extractAuthSessionWithTokenURL:(NSURL *)tokenURL callbackPath:(NSString *)callbackPath;
+
 @end
 
 @interface GIDAuthStateMigrationTest : XCTestCase
@@ -72,22 +74,24 @@ NS_ASSUME_NONNULL_BEGIN
   id _mockUserDefaults;
   id _mockGTMAppAuthFetcherAuthorization;
   id _mockGIDAuthStateMigration;
-  id _mockGTMKeychain;
+  id _mockGTMKeychainStore;
+  id _mockKeychainHelper;
   id _mockNSBundle;
   id _mockGIDSignInCallbackSchemes;
-  id _mockGTMOAuth2KeychainCompatibility;
+  id _mockGTMOAuth2Compatibility;
 }
 
 - (void)setUp {
   [super setUp];
 
-  _mockUserDefaults = OCMStrictClassMock([NSUserDefaults class]);
-  _mockGTMAppAuthFetcherAuthorization = OCMStrictClassMock([GTMAppAuthFetcherAuthorization class]);
+  _mockUserDefaults = OCMClassMock([NSUserDefaults class]);
+  _mockGTMAppAuthFetcherAuthorization = OCMStrictClassMock([GTMAuthSession class]);
   _mockGIDAuthStateMigration = OCMStrictClassMock([GIDAuthStateMigration class]);
-  _mockGTMKeychain = OCMStrictClassMock([GTMKeychain class]);
+  _mockGTMKeychainStore = OCMStrictClassMock([GTMKeychainStore class]);
+  _mockKeychainHelper = OCMProtocolMock(@protocol(GTMKeychainHelper));
   _mockNSBundle = OCMStrictClassMock([NSBundle class]);
   _mockGIDSignInCallbackSchemes = OCMStrictClassMock([GIDSignInCallbackSchemes class]);
-  _mockGTMOAuth2KeychainCompatibility = OCMStrictClassMock([GTMOAuth2KeychainCompatibility class]);
+  _mockGTMOAuth2Compatibility = OCMStrictClassMock([GTMOAuth2Compatibility class]);
 }
 
 - (void)tearDown {
@@ -97,14 +101,16 @@ NS_ASSUME_NONNULL_BEGIN
   [_mockGTMAppAuthFetcherAuthorization stopMocking];
   [_mockGIDAuthStateMigration verify];
   [_mockGIDAuthStateMigration stopMocking];
-  [_mockGTMKeychain verify];
-  [_mockGTMKeychain stopMocking];
+  [_mockGTMKeychainStore verify];
+  [_mockGTMKeychainStore stopMocking];
+  [_mockKeychainHelper verify];
+  [_mockKeychainHelper stopMocking];
   [_mockNSBundle verify];
   [_mockNSBundle stopMocking];
   [_mockGIDSignInCallbackSchemes verify];
   [_mockGIDSignInCallbackSchemes stopMocking];
-  [_mockGTMOAuth2KeychainCompatibility verify];
-  [_mockGTMOAuth2KeychainCompatibility stopMocking];
+  [_mockGTMOAuth2Compatibility verify];
+  [_mockGTMOAuth2Compatibility stopMocking];
 
   [super tearDown];
 }
@@ -113,44 +119,49 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)testMigrateIfNeeded_NoPreviousMigration {
   [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
-  [[[_mockUserDefaults expect] andReturnValue:@NO]
-      boolForKey:kMigrationCheckPerformedKey];
-  [[[_mockGIDAuthStateMigration expect] andReturn:_mockGTMAppAuthFetcherAuthorization]
-      extractAuthorizationWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath];
-  [[[_mockGTMAppAuthFetcherAuthorization expect] andReturnValue:@YES]
-      saveAuthorization:_mockGTMAppAuthFetcherAuthorization toKeychainForName:kKeychainName];
+  [[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kMigrationCheckPerformedKey];
   [[_mockUserDefaults expect] setBool:YES forKey:kMigrationCheckPerformedKey];
 
-  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
-                                        callbackPath:kCallbackPath
-                                        keychainName:kKeychainName
-                                      isFreshInstall:NO];
+  [[_mockGTMKeychainStore expect] saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef];
+
+  [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint];
+
+  GIDAuthStateMigration *migration =
+      [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
+  [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                            callbackPath:kCallbackPath
+                            keychainName:kKeychainName
+                          isFreshInstall:NO];
 }
 
 - (void)testMigrateIfNeeded_HasPreviousMigration {
   [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
-  [[[_mockUserDefaults expect] andReturnValue:@YES]
-      boolForKey:kMigrationCheckPerformedKey];
-
-  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
-                                        callbackPath:kCallbackPath
-                                        keychainName:kKeychainName
-                                      isFreshInstall:NO];
+  [[[_mockUserDefaults expect] andReturnValue:@YES] boolForKey:kMigrationCheckPerformedKey];
+  [[_mockUserDefaults reject] setBool:YES forKey:kMigrationCheckPerformedKey];
+
+  GIDAuthStateMigration *migration =
+      [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
+  [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                            callbackPath:kCallbackPath
+                            keychainName:kKeychainName
+                          isFreshInstall:NO];
 }
 
 - (void)testMigrateIfNeeded_KeychainFailure {
   [[[_mockUserDefaults stub] andReturn:_mockUserDefaults] standardUserDefaults];
-  [[[_mockUserDefaults expect] andReturnValue:@NO]
-      boolForKey:kMigrationCheckPerformedKey];
-  [[[_mockGIDAuthStateMigration expect] andReturn:_mockGTMAppAuthFetcherAuthorization]
-      extractAuthorizationWithTokenURL:[NSURL URLWithString:kTokenURL] callbackPath:kCallbackPath];
-  [[[_mockGTMAppAuthFetcherAuthorization expect] andReturnValue:[NSNumber numberWithBool:NO]]
-      saveAuthorization:_mockGTMAppAuthFetcherAuthorization toKeychainForName:kKeychainName];
-
-  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
-                                        callbackPath:kCallbackPath
-                                        keychainName:kKeychainName
-                                      isFreshInstall:NO];
+  [[[_mockUserDefaults expect] andReturnValue:@NO] boolForKey:kMigrationCheckPerformedKey];
+
+  NSError *keychainSaveError = [NSError new];
+  OCMStub([_mockGTMKeychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainSaveError]]);
+
+  [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint];
+
+  GIDAuthStateMigration *migration =
+      [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
+  [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                            callbackPath:kCallbackPath
+                            keychainName:kKeychainName
+                          isFreshInstall:NO];
 }
 
 - (void)testMigrateIfNeeded_isFreshInstall {
@@ -159,18 +170,36 @@ NS_ASSUME_NONNULL_BEGIN
       boolForKey:kMigrationCheckPerformedKey];
   [[_mockUserDefaults expect] setBool:YES forKey:kMigrationCheckPerformedKey];
 
-  [GIDAuthStateMigration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
-                                        callbackPath:kCallbackPath
-                                        keychainName:kKeychainName
-                                      isFreshInstall:YES];
+  GIDAuthStateMigration *migration =
+      [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
+  [migration migrateIfNeededWithTokenURL:[NSURL URLWithString:kTokenURL]
+                            callbackPath:kCallbackPath
+                            keychainName:kKeychainName
+                          isFreshInstall:YES];
 }
 
 - (void)testExtractAuthorization {
-  [self extractAuthorizationWithFingerprint:kSavedFingerprint];
+  [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint];
+
+  GIDAuthStateMigration *migration =
+      [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
+  GTMAuthSession *authorization =
+      [migration extractAuthSessionWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                   callbackPath:kCallbackPath];
+
+  XCTAssertNotNil(authorization);
 }
 
 - (void)testExtractAuthorization_HostedDomain {
-  [self extractAuthorizationWithFingerprint:kSavedFingerprint_HostedDomain];
+  [self setUpCommonExtractAuthorizationMocksWithFingerPrint:kSavedFingerprint_HostedDomain];
+
+  GIDAuthStateMigration *migration =
+      [[GIDAuthStateMigration alloc] initWithKeychainStore:_mockGTMKeychainStore];
+  GTMAuthSession *authorization =
+      [migration extractAuthSessionWithTokenURL:[NSURL URLWithString:kTokenURL]
+                                   callbackPath:kCallbackPath];
+
+  XCTAssertNotNil(authorization);
 }
 
 #pragma mark - Helpers
@@ -180,12 +209,12 @@ NS_ASSUME_NONNULL_BEGIN
   return [NSString stringWithFormat:@"%@%@", fingerprint, kAdditionalTokenRequestParametersPostfix];
 }
 
-// The parameterized extractAuthorization test.
-- (void)extractAuthorizationWithFingerprint:(NSString *)fingerprint {
+- (void)setUpCommonExtractAuthorizationMocksWithFingerPrint:(NSString *)fingerprint {
   [[[_mockGIDAuthStateMigration expect] andReturn:fingerprint]
       passwordForService:kFingerprintService];
-  [[[_mockGTMKeychain expect] andReturn:kGTMOAuth2PersistenceString]
-      passwordFromKeychainForName:fingerprint];
+  (void)[[[_mockKeychainHelper expect] andReturn:kGTMOAuth2PersistenceString]
+      passwordForService:fingerprint error:OCMArg.anyObjectRef];
+  [[[_mockGTMKeychainStore expect] andReturn:_mockKeychainHelper] keychainHelper];
   [[[_mockNSBundle expect] andReturn:_mockNSBundle] mainBundle];
   [[[_mockNSBundle expect] andReturn:kBundleID] bundleIdentifier];
   [[[_mockGIDSignInCallbackSchemes expect] andReturn:_mockGIDSignInCallbackSchemes] alloc];
@@ -194,18 +223,6 @@ NS_ASSUME_NONNULL_BEGIN
   [[[_mockGIDSignInCallbackSchemes expect] andReturn:kDotReversedClientID] clientIdentifierScheme];
   [[[_mockGIDAuthStateMigration expect] andReturn:kAdditionalTokenRequestParameters]
       passwordForService:[self additionalTokenRequestParametersKeyFromFingerprint:fingerprint]];
-  [[[_mockGTMOAuth2KeychainCompatibility expect] andReturn:_mockGTMAppAuthFetcherAuthorization]
-      authorizeFromPersistenceString:kFinalPersistenceString
-                            tokenURL:[NSURL URLWithString:kTokenURL]
-                         redirectURI:kRedirectURI
-                            clientID:kClientID
-                        clientSecret:nil];
-
-  GTMAppAuthFetcherAuthorization *authorization =
-      [GIDAuthStateMigration extractAuthorizationWithTokenURL:[NSURL URLWithString:kTokenURL]
-                                                 callbackPath:kCallbackPath];
-
-  XCTAssertNotNil(authorization);
 }
 
 @end

+ 1 - 16
GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m

@@ -21,6 +21,7 @@
 
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 #import "GoogleSignIn/Sources/GIDSignInStrings.h"
+#import "GoogleSignIn/Tests/Unit/UIAlertAction+Testing.h"
 
 #ifdef SWIFT_PACKAGE
 @import GoogleUtilities_MethodSwizzler;
@@ -34,22 +35,6 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-// Addtional methods added to UIAlertAction for testing.
-@interface UIAlertAction (Testing)
-
-// Returns the handler block for this alert action.
-- (void (^)(UIAlertAction *))actionHandler;
-
-@end
-
-@implementation UIAlertAction (Testing)
-
-- (void (^)(UIAlertAction *))actionHandler {
-  return [self valueForKey:@"handler"];
-}
-
-@end
-
 // Unit test for GIDEMMErrorHandler.
 @interface GIDEMMErrorHandlerTest : XCTestCase
 @end

+ 80 - 0
GoogleSignIn/Tests/Unit/GIDEMMSupportTest.m

@@ -18,14 +18,21 @@
 
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
+#import <UIKit/UIKit.h>
 #import <XCTest/XCTest.h>
 
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
 
+#import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
 
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
+#import "GoogleSignIn/Tests/Unit/GIDFailingOIDAuthState.h"
+#import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h"
+#import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h"
+#import "GoogleSignIn/Tests/Unit/UIAlertAction+Testing.h"
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
@@ -53,10 +60,61 @@ static NSString *const kDeviceOSKey = @"device_os";
 static NSString *const kEMMPasscodeInfoKey = @"emm_passcode_info";
 
 @interface GIDEMMSupportTest : XCTestCase
+  // The view controller that has been presented, if any.
+@property(nonatomic, strong, nullable) UIViewController *presentedViewController;
+
 @end
 
 @implementation GIDEMMSupportTest
 
+- (void)testEMMSupportDelegate {
+  [self setupSwizzlers];
+  XCTestExpectation *emmErrorExpectation = [self expectationWithDescription:@"EMM AppAuth error"];
+
+  GIDFailingOIDAuthState *failingAuthState = [GIDFailingOIDAuthState testInstance];
+  GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:failingAuthState profileData:nil];
+  GIDFakeFetcherService *fakeFetcherService = [[GIDFakeFetcherService alloc]
+                                                initWithAuthorizer:user.fetcherAuthorizer];
+
+  NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@""]];
+  GTMSessionFetcher *fetcher = [fakeFetcherService fetcherWithRequest:request];
+
+  [fetcher beginFetchWithCompletionHandler:^(NSData * _Nullable data, NSError * _Nullable error) {
+    XCTAssertNotNil(error);
+    NSDictionary<NSString *, id> *userInfo = @{
+      @"OIDOAuthErrorResponseErrorKey": @{@"error": @"emm_passcode_required"},
+      NSUnderlyingErrorKey: [NSError errorWithDomain:@"SomeUnderlyingError" code:0 userInfo:nil]
+    };
+    NSError *expectedError = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                                 code:kGIDSignInErrorCodeEMM
+                                             userInfo:userInfo];
+    XCTAssertEqualObjects(expectedError, error);
+    [emmErrorExpectation fulfill];
+  }];
+
+  // Wait for the code under test to be executed on the main thread.
+  XCTestExpectation *mainThreadExpectation =
+      [self expectationWithDescription:@"wait for main thread"];
+  dispatch_async(dispatch_get_main_queue(), ^() {
+    [mainThreadExpectation fulfill];
+  });
+  [self waitForExpectations:@[mainThreadExpectation] timeout:1];
+
+  XCTAssertTrue([_presentedViewController isKindOfClass:[UIAlertController class]]);
+  UIAlertController *alert = (UIAlertController *)_presentedViewController;
+  XCTAssertNotNil(alert.title);
+  XCTAssertNotNil(alert.message);
+  XCTAssertEqual(alert.actions.count, 2);
+
+  // Pretend to touch the "Cancel" button.
+  UIAlertAction *action = alert.actions[0];
+  XCTAssertEqualObjects(action.title, @"Cancel");
+  action.actionHandler(action);
+
+  [self waitForExpectations:@[emmErrorExpectation] timeout:1];
+  [self unswizzle];
+}
+
 - (void)testUpdatedEMMParametersWithParameters_NoEMMKey {
   NSDictionary *originalParameters = @{
     @"not_emm_support_key" : @"xyz",
@@ -222,6 +280,28 @@ static NSString *const kEMMPasscodeInfoKey = @"emm_passcode_info";
   return [UIDevice currentDevice].systemVersion;
 }
 
+- (void)setupSwizzlers {
+  UIWindow *fakeKeyWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
+  [GULSwizzler swizzleClass:[GIDEMMErrorHandler class]
+                   selector:@selector(keyWindow)
+            isClassSelector:NO
+                  withBlock:^() { return fakeKeyWindow; }];
+  [GULSwizzler swizzleClass:[UIViewController class]
+                   selector:@selector(presentViewController:animated:completion:)
+            isClassSelector:NO
+                  withBlock:^(id obj, id arg1) { self->_presentedViewController = arg1; }];
+}
+
+- (void)unswizzle {
+  [GULSwizzler unswizzleClass:[GIDEMMErrorHandler class]
+                     selector:@selector(keyWindow)
+              isClassSelector:NO];
+  [GULSwizzler unswizzleClass:[UIViewController class]
+                     selector:@selector(presentViewController:animated:completion:)
+              isClassSelector:NO];
+  self.presentedViewController = nil;
+}
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 4 - 11
GoogleSignIn/Sources/GIDAppAuthFetcherAuthorizationWithEMMSupport.h → GoogleSignIn/Tests/Unit/GIDFailingOIDAuthState.h

@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 Google LLC
+ * 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.
@@ -14,23 +14,16 @@
  * limitations under the License.
  */
 
-#import <TargetConditionals.h>
-
-#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
-
 #ifdef SWIFT_PACKAGE
-@import GTMAppAuth;
+@import AppAuth;
 #else
-#import <GTMAppAuth/GTMAppAuth.h>
+#import <AppAuth/OIDAuthState.h>
 #endif
 
 NS_ASSUME_NONNULL_BEGIN
 
-// A specialized GTMAppAuthFetcherAuthorization subclass with EMM support.
-@interface GIDAppAuthFetcherAuthorizationWithEMMSupport : GTMAppAuthFetcherAuthorization
+@interface GIDFailingOIDAuthState : OIDAuthState
 
 @end
 
 NS_ASSUME_NONNULL_END
-
-#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 54 - 0
GoogleSignIn/Tests/Unit/GIDFailingOIDAuthState.m

@@ -0,0 +1,54 @@
+/*
+ * 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 "GIDFailingOIDAuthState.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthState+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDAuthorizationResponse+Testing.h"
+#import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
+
+@implementation GIDFailingOIDAuthState
+
++ (instancetype)testInstance {
+  OIDAuthorizationResponse *testAuthResponse = [OIDAuthorizationResponse testInstance];
+  OIDTokenResponse *testTokenResponse = [OIDTokenResponse testInstance];
+
+  return [[GIDFailingOIDAuthState alloc] initWithAuthorizationResponse:testAuthResponse
+                                                         tokenResponse:testTokenResponse];
+}
+
+- (void)performActionWithFreshTokens:(OIDAuthStateAction)action
+         additionalRefreshParameters:
+    (nullable NSDictionary<NSString *, NSString *> *)additionalParameters {
+  [self performActionWithFreshTokens:action
+         additionalRefreshParameters:additionalParameters
+                       dispatchQueue:dispatch_get_main_queue()];
+}
+
+// Forces the OIDAuthState to fail with the error below to simulate an error handled by EMM support
+- (void)performActionWithFreshTokens:(OIDAuthStateAction)action
+         additionalRefreshParameters:(NSDictionary<NSString *,NSString *> *)additionalParameters
+                       dispatchQueue:(dispatch_queue_t)dispatchQueue {
+  NSDictionary<NSString *, id> *userInfo = @{
+    @"OIDOAuthErrorResponseErrorKey": @{@"error": @"emm_passcode_required"},
+    NSUnderlyingErrorKey: [NSError errorWithDomain:@"SomeUnderlyingError" code:0 userInfo:nil]
+  };
+  NSError *error = [NSError errorWithDomain:@"org.openid.appauth.resourceserver"
+                                       code:0
+                                   userInfo:userInfo];
+  action(nil, nil, error);
+}
+
+@end

+ 8 - 2
GoogleSignIn/Tests/Unit/GIDFakeFetcher.h

@@ -20,14 +20,20 @@
 #import <GTMSessionFetcher/GTMSessionFetcher.h>
 #endif
 
-// A fake |GTMHTTPFetcher| for testing.
+/// A fake |GTMHTTPFetcher| for testing.
 @interface GIDFakeFetcher : GTMSessionFetcher
 
-// The URL of the fetching request.
+/// The URL of the fetching request.
 - (NSURL *)requestURL;
 
 // Emulates server returning with data and/or error.
 - (void)didFinishWithData:(NSData *)data error:(NSError *)error;
 
 - (instancetype)initWithRequest:(NSURLRequest *)request;
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated"
+- (instancetype)initWithRequest:(NSURLRequest *)request
+                     authorizer:(id<GTMFetcherAuthorizationProtocol>)authorizer;
+#pragma clang diagnostic pop
 @end

+ 20 - 0
GoogleSignIn/Tests/Unit/GIDFakeFetcher.m

@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@import GTMAppAuth;
 #import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h"
 
 typedef void (^FetchCompletionHandler)(NSData *, NSError *);
@@ -29,6 +30,17 @@ typedef void (^FetchCompletionHandler)(NSData *, NSError *);
   return self;
 }
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated"
+- (instancetype)initWithRequest:(NSURLRequest *)request
+                     authorizer:(id<GTMFetcherAuthorizationProtocol>)authorizer {
+#pragma clang diagnostic pop
+  self = [self initWithRequest:request];
+  if (self) {
+    self.authorizer = authorizer;
+  }
+  return self;
+}
 
 - (void)beginFetchWithDelegate:(id)delegate didFinishSelector:(SEL)finishedSEL {
   [NSException raise:@"NotImplementedException" format:@"Implement this method if it is used"];
@@ -39,6 +51,14 @@ typedef void (^FetchCompletionHandler)(NSData *, NSError *);
     [NSException raise:NSInvalidArgumentException format:@"Attempted start fetch again"];
   }
   _handler = [handler copy];
+  [self authorizeRequestWithCompletion:handler];
+}
+
+- (void)authorizeRequestWithCompletion:(FetchCompletionHandler)completion {
+  NSMutableURLRequest *mutableRequest = [NSMutableURLRequest requestWithURL:self.request.URL];
+  [self.authorizer authorizeRequest:mutableRequest completionHandler:^(NSError * _Nullable error) {
+    completion(nil, error);
+  }];
 }
 
 - (NSURL *)requestURL {

+ 13 - 1
GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h

@@ -23,7 +23,19 @@
 // A fake |GTMHTTPFetcherService| for testing.
 @interface GIDFakeFetcherService : NSObject<GTMSessionFetcherServiceProtocol>
 
-// Returns the list of |GPPFakeFetcher| objects that have been created.
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+/// Creates an instance of this fake with an authorizer.
+- initWithAuthorizer:(id<GTMFetcherAuthorizationProtocol>)authorizer;
+#pragma clang diagnostic pop
+
+/// Returns the list of |GPPFakeFetcher| objects that have been created.
 - (NSArray *)fetchers;
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated"
+/// The instance to use for authorizing requeests.
+@property (nonatomic, strong) id<GTMFetcherAuthorizationProtocol> authorizer;
+#pragma clang diagnostic pop
+
 @end

+ 13 - 2
GoogleSignIn/Tests/Unit/GIDFakeFetcherService.m

@@ -13,7 +13,6 @@
 // limitations under the License.
 
 #import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h"
-
 #import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h"
 
 @implementation GIDFakeFetcherService {
@@ -32,6 +31,17 @@
   return self;
 }
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated"
+- (instancetype)initWithAuthorizer:(id<GTMFetcherAuthorizationProtocol>)authorizer {
+#pragma clang diagnostic pop
+  self = [self init];
+  if (self) {
+    self.authorizer = authorizer;
+  }
+  return self;
+}
+
 - (BOOL)fetcherShouldBeginFetching:(GTMSessionFetcher *)fetcher {
   return YES;
 }
@@ -50,7 +60,8 @@
 }
 
 - (GTMSessionFetcher *)fetcherWithRequest:(NSURLRequest *)request {
-  GIDFakeFetcher *fetcher = [[GIDFakeFetcher alloc] initWithRequest:request];
+  GIDFakeFetcher *fetcher = [[GIDFakeFetcher alloc] initWithRequest:request
+                                                         authorizer:self.authorizer];
   [_fetchers addObject:fetcher];
   return fetcher;
 }

+ 6 - 0
GoogleSignIn/Tests/Unit/GIDGoogleUser+Testing.h

@@ -16,6 +16,12 @@
 
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDGoogleUser.h"
 
+#ifdef SWIFT_PACKAGE
+@import AppAuthCore;
+#else
+#import <AppAuth/AppAuthCore.h>
+#endif
+
 @interface GIDGoogleUser (Testing)
 
 - (BOOL)isEqual:(id)object;

+ 3 - 2
GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m

@@ -30,6 +30,8 @@
 #import "GoogleSignIn/Tests/Unit/OIDTokenRequest+Testing.h"
 #import "GoogleSignIn/Tests/Unit/OIDTokenResponse+Testing.h"
 
+@import GTMAppAuth;
+
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
 @import GoogleUtilities_MethodSwizzler;
@@ -47,7 +49,6 @@
 #import <AppAuth/OIDTokenResponse.h>
 #import <GoogleUtilities/GULSwizzler.h>
 #import <GoogleUtilities/GULSwizzler+Unswizzle.h>
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #import <OCMock/OCMock.h>
 #endif
 
@@ -216,7 +217,7 @@ static NSString *const kNewScope = @"newScope";
 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
   id<GTMFetcherAuthorizationProtocol> fetcherAuthorizer = user.fetcherAuthorizer;
 #pragma clang diagnostic pop
-  XCTAssertTrue([fetcherAuthorizer isKindOfClass:[GTMAppAuthFetcherAuthorization class]]);
+  XCTAssertTrue([fetcherAuthorizer isKindOfClass:[GTMAuthSession class]]);
   XCTAssertTrue([fetcherAuthorizer canAuthorize]);
 }
 

+ 139 - 57
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -26,6 +26,8 @@
 // Test module imports
 @import GoogleSignIn;
 
+@import GTMAppAuth;
+
 #import "GoogleSignIn/Sources/GIDEMMSupport.h"
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
 #import "GoogleSignIn/Sources/GIDSignIn_Private.h"
@@ -43,7 +45,6 @@
 
 #ifdef SWIFT_PACKAGE
 @import AppAuth;
-@import GTMAppAuth;
 @import GTMSessionFetcherCore;
 @import OCMock;
 #else
@@ -63,8 +64,6 @@
 #import <AppAuth/OIDAuthorizationService+Mac.h>
 #endif // TARGET_OS_IOS || TARGET_OS_MACCATALYST
 
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
-#import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #import <GTMSessionFetcher/GTMSessionFetcher.h>
 #import <OCMock/OCMock.h>
 #endif
@@ -190,9 +189,12 @@ static NSString *const kNewScope = @"newScope";
   // Mock |OIDTokenRequest|.
   id _tokenRequest;
 
-  // Mock |GTMAppAuthFetcherAuthorization|.
+  // Mock |GTMAuthSession|.
   id _authorization;
 
+  // Mock |GTMKeychainStore|.
+  id _keychainStore;
+
 #if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   // Mock |UIViewController|.
   id _presentingViewController;
@@ -289,23 +291,19 @@ static NSString *const kNewScope = @"newScope";
   OCMStub([_authState initWithAuthorizationResponse:OCMOCK_ANY]).andReturn(_authState);
   _tokenResponse = OCMStrictClassMock([OIDTokenResponse class]);
   _tokenRequest = OCMStrictClassMock([OIDTokenRequest class]);
-  _authorization = OCMStrictClassMock([GTMAppAuthFetcherAuthorization class]);
-  OCMStub([_authorization authorizationFromKeychainForName:OCMOCK_ANY
-                                 useDataProtectionKeychain:YES]).andReturn(_authorization);
+  _authorization = OCMStrictClassMock([GTMAuthSession class]);
+  _keychainStore = OCMStrictClassMock([GTMKeychainStore class]);
+  OCMStub(
+    [_keychainStore retrieveAuthSessionWithItemName:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andReturn(_authorization);
+  OCMStub([_keychainStore retrieveAuthSessionWithError:nil]).andReturn(_authorization);
   OCMStub([_authorization alloc]).andReturn(_authorization);
   OCMStub([_authorization initWithAuthState:OCMOCK_ANY]).andReturn(_authorization);
-  OCMStub([_authorization saveAuthorization:OCMOCK_ANY
-                          toKeychainForName:OCMOCK_ANY
-                  useDataProtectionKeychain:YES])
-      .andDo(^(NSInvocation *invocation) {
-        self->_keychainSaved = self->_saveAuthorizationReturnValue;
-        [invocation setReturnValue:&self->_saveAuthorizationReturnValue];
-      });
-  OCMStub([_authorization removeAuthorizationFromKeychainForName:OCMOCK_ANY
-                                       useDataProtectionKeychain:YES])
-      .andDo(^(NSInvocation *invocation) {
-        self->_keychainRemoved = YES;
-      });
+  OCMStub(
+    [_keychainStore removeAuthSessionWithError:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainRemoved = YES;
+  });
   _user = OCMStrictClassMock([GIDGoogleUser class]);
   _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
   OCMStub([_oidAuthorizationService
@@ -330,7 +328,7 @@ static NSString *const kNewScope = @"newScope";
   [[NSUserDefaults standardUserDefaults] setBool:YES
                                           forKey:kAppHasRunBeforeKey];
 
-  _signIn = [[GIDSignIn alloc] initPrivate];
+  _signIn = [[GIDSignIn alloc] initWithKeychainStore:_keychainStore];
   _hint = nil;
 
   __weak GIDSignInTest *weakSelf = self;
@@ -414,7 +412,9 @@ static NSString *const kNewScope = @"newScope";
 
 - (void)testRestorePreviousSignInNoRefresh_hasPreviousUser {
   [[[_authorization stub] andReturn:_authState] authState];
-  [[_authorization expect] setTokenRefreshDelegate:OCMOCK_ANY];
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+  [[_authorization expect] setDelegate:OCMOCK_ANY];
+#endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST
   OCMStub([_authState lastTokenResponse]).andReturn(_tokenResponse);
   OCMStub([_authState refreshToken]).andReturn(kRefreshToken);
   [[_authState expect] setStateChangeDelegate:OCMOCK_ANY];
@@ -438,10 +438,6 @@ static NSString *const kNewScope = @"newScope";
   
   [_signIn restorePreviousSignInNoRefresh];
 
-  [_authorization verify];
-  [_authState verify];
-  [_tokenResponse verify];
-  [_tokenRequest verify];
   [idTokenDecoded verify];
   XCTAssertEqual(_signIn.currentUser.userID, kFakeGaiaID);
 
@@ -514,6 +510,7 @@ static NSString *const kNewScope = @"newScope";
 
 - (void)testRestorePreviousSignInWhenCompletionIsNil {
   [[[_authorization expect] andReturn:_authState] authState];
+  [[_keychainStore expect] saveAuthSession:OCMOCK_ANY error:[OCMArg anyObjectRef]];
   [[[_authState expect] andReturnValue:[NSNumber numberWithBool:YES]] isAuthorized];
 
   OIDTokenResponse *tokenResponse =
@@ -537,6 +534,12 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testOAuthLogin {
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -548,6 +551,12 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testOAuthLogin_RestoredSignIn {
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -559,6 +568,12 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testOAuthLogin_RestoredSignInOldAccessToken {
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -571,7 +586,13 @@ static NSString *const kNewScope = @"newScope";
 
 - (void)testOAuthLogin_AdditionalScopes {
   NSString *expectedScopeString;
-  
+
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -617,6 +638,11 @@ static NSString *const kNewScope = @"newScope";
 
 - (void)testAddScopes {
   // Restore the previous sign-in account. This is the preparation for adding scopes.
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -677,6 +703,12 @@ static NSString *const kNewScope = @"newScope";
                                                         hostedDomain:nil
                                                          openIDRealm:kOpenIDRealm];
 
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -693,6 +725,12 @@ static NSString *const kNewScope = @"newScope";
 - (void)testOAuthLogin_LoginHint {
   _hint = kUserEmail;
 
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -712,6 +750,12 @@ static NSString *const kNewScope = @"newScope";
                                                         hostedDomain:kHostedDomain
                                                          openIDRealm:nil];
 
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -754,6 +798,16 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testOAuthLogin_KeychainError {
+  // This error is going be overidden by `-[GIDSignIn errorWithString:code:]`
+  // We just need to fill in the error so that happens.
+  NSError *keychainError = [NSError errorWithDomain:@"com.googleSignIn.throwAway"
+                                               code:1
+                                           userInfo:nil];
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:[OCMArg setTo:keychainError]]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -770,6 +824,16 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testSignOut {
+#if TARGET_OS_IOS || !TARGET_OS_MACCATALYST
+//  OCMStub([_authorization authState]).andReturn(_authState);
+#endif // TARGET_OS_IOS || !TARGET_OS_MACCATALYST
+  OCMStub([_authorization fetcherService]).andReturn(_fetcherService);
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   // Sign in a user so that we can then sign them out.
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
@@ -786,8 +850,7 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertNil(_signIn.currentUser, @"should not have a current user");
   XCTAssertTrue(_keychainRemoved, @"should remove keychain");
 
-  OCMVerify([_authorization removeAuthorizationFromKeychainForName:kKeychainName
-                                         useDataProtectionKeychain:YES]);
+  OCMVerify([_keychainStore removeAuthSessionWithError:OCMArg.anyObjectRef]);
 }
 
 - (void)testNotHandleWrongScheme {
@@ -811,14 +874,16 @@ static NSString *const kNewScope = @"newScope";
   [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
   [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
   [[[_authorization expect] andReturn:_fetcherService] fetcherService];
-  XCTestExpectation *expectation =
+  XCTestExpectation *accessTokenExpectation =
       [self expectationWithDescription:@"Callback called with nil error"];
   [_signIn disconnectWithCompletion:^(NSError * _Nullable error) {
     if (error == nil) {
-      [expectation fulfill];
+      [accessTokenExpectation fulfill];
     }
   }];
-  [self verifyAndRevokeToken:kAccessToken hasCallback:YES];
+  [self verifyAndRevokeToken:kAccessToken
+                 hasCallback:YES
+      waitingForExpectations:@[accessTokenExpectation]];
   [_authorization verify];
   [_authState verify];
   [_tokenResponse verify];
@@ -831,7 +896,7 @@ static NSString *const kNewScope = @"newScope";
   [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
   [[[_authorization expect] andReturn:_fetcherService] fetcherService];
   [_signIn disconnectWithCompletion:nil];
-  [self verifyAndRevokeToken:kAccessToken hasCallback:NO];
+  [self verifyAndRevokeToken:kAccessToken hasCallback:NO waitingForExpectations:@[]];
   [_authorization verify];
   [_authState verify];
   [_tokenResponse verify];
@@ -845,14 +910,16 @@ static NSString *const kNewScope = @"newScope";
   [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
   [[[_tokenResponse expect] andReturn:kRefreshToken] refreshToken];
   [[[_authorization expect] andReturn:_fetcherService] fetcherService];
-  XCTestExpectation *expectation =
+  XCTestExpectation *refreshTokenExpectation =
       [self expectationWithDescription:@"Callback called with nil error"];
   [_signIn disconnectWithCompletion:^(NSError * _Nullable error) {
     if (error == nil) {
-      [expectation fulfill];
+      [refreshTokenExpectation fulfill];
     }
   }];
-  [self verifyAndRevokeToken:kRefreshToken hasCallback:YES];
+  [self verifyAndRevokeToken:kRefreshToken
+                 hasCallback:YES
+      waitingForExpectations:@[refreshTokenExpectation]];
   [_authorization verify];
   [_authState verify];
   [_tokenResponse verify];
@@ -864,18 +931,18 @@ static NSString *const kNewScope = @"newScope";
   [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
   [[[_tokenResponse expect] andReturn:kAccessToken] accessToken];
   [[[_authorization expect] andReturn:_fetcherService] fetcherService];
-  XCTestExpectation *expectation =
+  XCTestExpectation *errorExpectation =
       [self expectationWithDescription:@"Callback called with an error"];
   [_signIn disconnectWithCompletion:^(NSError * _Nullable error) {
     if (error != nil) {
-      [expectation fulfill];
+      [errorExpectation fulfill];
     }
   }];
   XCTAssertTrue([self isFetcherStarted], @"should start fetching");
   // Emulate result back from server.
   NSError *error = [self error];
   [self didFetch:nil error:error];
-  [self waitForExpectationsWithTimeout:1 handler:nil];
+  [self waitForExpectations:@[errorExpectation] timeout:1];
   [_authorization verify];
   [_authState verify];
   [_tokenResponse verify];
@@ -905,14 +972,14 @@ static NSString *const kNewScope = @"newScope";
   [[[_tokenResponse expect] andReturn:nil] accessToken];
   [[[_authState expect] andReturn:_tokenResponse] lastTokenResponse];
   [[[_tokenResponse expect] andReturn:nil] refreshToken];
-  XCTestExpectation *expectation =
+  XCTestExpectation *noTokensExpectation =
       [self expectationWithDescription:@"Callback called with nil error"];
   [_signIn disconnectWithCompletion:^(NSError * _Nullable error) {
     if (error == nil) {
-      [expectation fulfill];
+      [noTokensExpectation fulfill];
     }
   }];
-  [self waitForExpectationsWithTimeout:1 handler:nil];
+  [self waitForExpectations:@[noTokensExpectation] timeout:1];
   XCTAssertFalse([self isFetcherStarted], @"should not fetch");
   XCTAssertTrue(_keychainRemoved, @"keychain should be removed");
   [_authorization verify];
@@ -1008,6 +1075,12 @@ static NSString *const kNewScope = @"newScope";
 #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
 
 - (void)testEmmSupportRequestParameters {
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -1053,6 +1126,12 @@ static NSString *const kNewScope = @"newScope";
 }
 
 - (void)testEmmPasscodeInfo {
+  OCMStub(
+    [_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]
+  ).andDo(^(NSInvocation *invocation) {
+    self->_keychainSaved = self->_saveAuthorizationReturnValue;
+  });
+
   [self OAuthLoginWithAddScopesFlow:NO
                           authError:nil
                          tokenError:nil
@@ -1181,7 +1260,9 @@ static NSString *const kNewScope = @"newScope";
 }
 
 // Verifies a fetcher has started for revoking token and emulates a server response.
-- (void)verifyAndRevokeToken:(NSString *)token hasCallback:(BOOL)hasCallback {
+- (void)verifyAndRevokeToken:(NSString *)token
+                 hasCallback:(BOOL)hasCallback
+      waitingForExpectations:(NSArray<XCTestExpectation *> *)expectations {
   XCTAssertTrue([self isFetcherStarted], @"should start fetching");
   NSURL *url = [self fetchedURL];
   XCTAssertEqualObjects([url scheme], @"https", @"scheme must match");
@@ -1197,10 +1278,10 @@ static NSString *const kNewScope = @"newScope";
                         @"Environment logging parameter should match");
   // Emulate result back from server.
   [self didFetch:nil error:nil];
+  XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name");
   if (hasCallback) {
-    [self waitForExpectationsWithTimeout:1 handler:nil];
+    [self waitForExpectations:expectations timeout:1];
   }
-  XCTAssertTrue(_keychainRemoved, @"should clear saved keychain name");
 }
 
 - (void)OAuthLoginWithAddScopesFlow:(BOOL)addScopesFlow
@@ -1281,10 +1362,11 @@ static NSString *const kNewScope = @"newScope";
           tokenRefreshRequestWithAdditionalParameters:[OCMArg any]];
     }
   } else {
-    XCTestExpectation *expectation = [self expectationWithDescription:@"Callback called"];
+    XCTestExpectation *newAccessTokenExpectation =
+        [self expectationWithDescription:@"Callback called"];
     GIDSignInCompletion completion = ^(GIDSignInResult *_Nullable signInResult,
                                        NSError * _Nullable error) {
-      [expectation fulfill];
+      [newAccessTokenExpectation fulfill];
       if (signInResult) {
         XCTAssertEqualObjects(signInResult.serverAuthCode, kServerAuthCode);
       } else {
@@ -1367,10 +1449,11 @@ static NSString *const kNewScope = @"newScope";
   }
 
   if (restoredSignIn && oldAccessToken) {
-    XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
+    XCTestExpectation *callbackShouldBeCalledExpectation =
+        [self expectationWithDescription:@"Callback should be called"];
     [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user,
                                                    NSError * _Nullable error) {
-      [expectation fulfill];
+      [callbackShouldBeCalledExpectation fulfill];
       XCTAssertNil(error, @"should have no error");
     }];
   }
@@ -1423,10 +1506,10 @@ static NSString *const kNewScope = @"newScope";
   }
 
   if (restoredSignIn && !oldAccessToken) {
-    XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
+    XCTestExpectation *restoredSignInExpectation = [self expectationWithDescription:@"Callback should be called"];
     [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user,
                                                    NSError * _Nullable error) {
-      [expectation fulfill];
+      [restoredSignInExpectation fulfill];
       XCTAssertNil(error, @"should have no error");
     }];
   } else {
@@ -1463,11 +1546,12 @@ static NSString *const kNewScope = @"newScope";
   __block GIDGoogleUserCompletion completion;
   [[_user expect] refreshTokensIfNeededWithCompletion:SAVE_TO_ARG_BLOCK(completion)];
 
-  XCTestExpectation *expectation = [self expectationWithDescription:@"Callback should be called"];
+  XCTestExpectation *restorePreviousSignInExpectation =
+      [self expectationWithDescription:@"Callback should be called"];
 
   [_signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser * _Nullable user,
                                                  NSError * _Nullable error) {
-    [expectation fulfill];
+    [restorePreviousSignInExpectation fulfill];
     XCTAssertNil(error, @"should have no error");
   }];
 
@@ -1478,11 +1562,9 @@ static NSString *const kNewScope = @"newScope";
   XCTAssertFalse(_keychainSaved, @"should not save to keychain again");
   
   if (restoredSignIn) {
-    OCMVerify([_authorization authorizationFromKeychainForName:kKeychainName
-                                     useDataProtectionKeychain:YES]);
-    OCMVerify([_authorization saveAuthorization:OCMOCK_ANY
-                              toKeychainForName:kKeychainName
-                      useDataProtectionKeychain:YES]);
+    // Ignore the return value
+    OCMVerify((void)[_keychainStore retrieveAuthSessionWithError:OCMArg.anyObjectRef]);
+    OCMVerify([_keychainStore saveAuthSession:OCMOCK_ANY error:OCMArg.anyObjectRef]);
   }
 }
 

+ 1 - 0
GoogleSignIn/Tests/Unit/OIDAuthState+Testing.m

@@ -46,4 +46,5 @@
   return [self testInstanceWithTokenResponse:newResponse];
 }
 
+
 @end

+ 34 - 0
GoogleSignIn/Tests/Unit/UIAlertAction+Testing.h

@@ -0,0 +1,34 @@
+
+// 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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Helper category for testing EMM support
+@interface UIAlertAction (Testing)
+
+/// Returns the handler block for this alert action.
+- (void (^)(UIAlertAction *))actionHandler;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 29 - 0
GoogleSignIn/Tests/Unit/UIAlertAction+Testing.m

@@ -0,0 +1,29 @@
+// 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 <TargetConditionals.h>
+
+#if TARGET_OS_IOS && !TARGET_OS_MACCATALYST
+
+#import "UIAlertAction+Testing.h"
+
+@implementation UIAlertAction (Testing)
+
+- (void (^)(UIAlertAction *))actionHandler {
+  return [self valueForKey:@"handler"];
+}
+
+@end
+
+#endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST

+ 1 - 1
GoogleSignInSwiftSupport.podspec

@@ -33,6 +33,6 @@ Pod::Spec.new do |s|
     unit_tests.source_files = [
       'GoogleSignInSwift/Tests/Unit/*.swift',
     ]
-    unit_tests.requires_app_host = false
+    unit_tests.requires_app_host = true
   end
 end

+ 2 - 2
Package.swift

@@ -44,11 +44,11 @@ let package = Package(
     .package(
       name: "AppAuth",
       url: "https://github.com/openid/AppAuth-iOS.git",
-      "1.5.0" ..< "2.0.0"),
+      "1.6.0" ..< "2.0.0"),
     .package(
       name: "GTMAppAuth",
       url: "https://github.com/google/GTMAppAuth.git",
-      "1.3.0" ..< "3.0.0"),
+      from: "4.0.0"),
     .package(
       name: "GTMSessionFetcher",
       url: "https://github.com/google/gtm-session-fetcher.git",

+ 1 - 0
Samples/ObjC/SignInSample/Podfile

@@ -1,4 +1,5 @@
 platform :ios, '10.0'
+use_frameworks!
 
 target 'SampleForPod' do
   pod 'GoogleSignIn', :path => '../../../', :testspecs => ['unit']

+ 18 - 6
Samples/ObjC/SignInSample/SignInSampleForPod.xcodeproj/project.pbxproj

@@ -7,7 +7,8 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		CC887BA946C6A9FDA3DE81B5 /* Pods_SampleForPod.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 550F5407FA6F32CFDE2C3C75 /* Pods_SampleForPod.framework */; };
+		1D721EFBF29E4B5D3EA5BE4D /* Pods_SampleForPod.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2675052AF6D7076DCBF751BB /* Pods_SampleForPod.framework */; };
+		7314214E29E7348800D35433 /* Empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7314214D29E7348800D35433 /* Empty.swift */; };
 		D926A5701BC3236100ADECE6 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = D926A5381BC3236100ADECE6 /* AppDelegate.m */; };
 		D926A5711BC3236100ADECE6 /* AuthInspectorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D926A53A1BC3236100ADECE6 /* AuthInspectorViewController.m */; };
 		D926A5721BC3236100ADECE6 /* DataPickerState.m in Sources */ = {isa = PBXBuildFile; fileRef = D926A53C1BC3236100ADECE6 /* DataPickerState.m */; };
@@ -24,7 +25,8 @@
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
-		550F5407FA6F32CFDE2C3C75 /* Pods_SampleForPod.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleForPod.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		2675052AF6D7076DCBF751BB /* Pods_SampleForPod.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SampleForPod.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		7314214D29E7348800D35433 /* Empty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Empty.swift; sourceTree = "<group>"; };
 		C1B5D38B282AFE460068D12B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
 		D926A51D1BC31FE000ADECE6 /* SignInSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SignInSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		D926A5371BC3236100ADECE6 /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Source/AppDelegate.h; sourceTree = SOURCE_ROOT; };
@@ -90,7 +92,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				CC887BA946C6A9FDA3DE81B5 /* Pods_SampleForPod.framework in Frameworks */,
+				1D721EFBF29E4B5D3EA5BE4D /* Pods_SampleForPod.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -109,7 +111,7 @@
 		BB0E907DD016FB9CB6DC01B5 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
-				550F5407FA6F32CFDE2C3C75 /* Pods_SampleForPod.framework */,
+				2675052AF6D7076DCBF751BB /* Pods_SampleForPod.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";
@@ -130,6 +132,7 @@
 				D926A56D1BC3236100ADECE6 /* SignInViewController.h */,
 				D926A56E1BC3236100ADECE6 /* SignInViewController.m */,
 				D926A56F1BC3236100ADECE6 /* SignInViewController.xib */,
+				7314214D29E7348800D35433 /* Empty.swift */,
 			);
 			path = Source;
 			sourceTree = "<group>";
@@ -178,7 +181,7 @@
 				D926A5191BC31FE000ADECE6 /* Sources */,
 				D926A51A1BC31FE000ADECE6 /* Frameworks */,
 				D926A51B1BC31FE000ADECE6 /* Resources */,
-				819CCDEFD018D376E185A11E /* [CP] Embed Pods Frameworks */,
+				CA434359EF3A93E8F17342C4 /* [CP] Embed Pods Frameworks */,
 			);
 			buildRules = (
 			);
@@ -200,6 +203,7 @@
 				TargetAttributes = {
 					D926A51C1BC31FE000ADECE6 = {
 						CreatedOnToolsVersion = 7.0;
+						LastSwiftMigration = 1420;
 					};
 				};
 			};
@@ -296,7 +300,7 @@
 			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
 			showEnvVarsInLog = 0;
 		};
-		819CCDEFD018D376E185A11E /* [CP] Embed Pods Frameworks */ = {
+		CA434359EF3A93E8F17342C4 /* [CP] Embed Pods Frameworks */ = {
 			isa = PBXShellScriptBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
@@ -332,6 +336,7 @@
 				D926A5721BC3236100ADECE6 /* DataPickerState.m in Sources */,
 				D926A5711BC3236100ADECE6 /* AuthInspectorViewController.m in Sources */,
 				D926A57D1BC3236100ADECE6 /* SignInViewController.m in Sources */,
+				7314214E29E7348800D35433 /* Empty.swift in Sources */,
 				D926A5701BC3236100ADECE6 /* AppDelegate.m in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -496,11 +501,15 @@
 			baseConfigurationReference = FE1798D23522F57E24875676 /* Pods-SampleForPod.debug.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.google.SignInSample;
 				PRODUCT_NAME = SignInSample;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
 			};
 			name = Debug;
 		};
@@ -509,11 +518,14 @@
 			baseConfigurationReference = DAD21F7A41C90200270A7376 /* Pods-SampleForPod.release.xcconfig */;
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				DEVELOPMENT_TEAM = "";
 				INFOPLIST_FILE = "$(SRCROOT)/SignInSample-Info.plist";
 				IPHONEOS_DEPLOYMENT_TARGET = 10.0;
 				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
 				PRODUCT_BUNDLE_IDENTIFIER = com.google.SignInSample;
 				PRODUCT_NAME = SignInSample;
+				SWIFT_VERSION = 5.0;
 			};
 			name = Release;
 		};

+ 18 - 0
Samples/ObjC/SignInSample/Source/Empty.swift

@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
+
+
+// This is necessary for `SignInSample`'s `Podfile` to use `use_frameworks! :linkage => :static`

+ 8 - 8
Samples/Swift/DaysUntilBirthday/DaysUntilBirthdayForPod.xcodeproj/project.pbxproj

@@ -7,8 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		5965643074F45661539BC2DF /* libPods-DaysUntilBirthdayForPod (iOS).a in Frameworks */ = {isa = PBXBuildFile; fileRef = F163BB5E1E81280A79B4E535 /* libPods-DaysUntilBirthdayForPod (iOS).a */; };
-		7023CA98EF5DD94278069A12 /* libPods-DaysUntilBirthdayForPod (macOS).a in Frameworks */ = {isa = PBXBuildFile; fileRef = AE0FE424C7B3C57CF8808F8A /* libPods-DaysUntilBirthdayForPod (macOS).a */; };
+		4C6A2BA2321434ACBD2AF201 /* Pods_DaysUntilBirthdayForPod__macOS_.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 926A15D393D684C68E09FB0F /* Pods_DaysUntilBirthdayForPod__macOS_.framework */; };
 		7345AD032703D9470020AFB1 /* DaysUntilBirthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD022703D9470020AFB1 /* DaysUntilBirthday.swift */; };
 		7345AD052703D9470020AFB1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7345AD042703D9470020AFB1 /* ContentView.swift */; };
 		7345AD072703D9480020AFB1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; };
@@ -37,6 +36,7 @@
 		73DB41952805FC5F0028B8D3 /* Birthday.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739FCC47270E659A00C92042 /* Birthday.swift */; };
 		73DB419628060A9A0028B8D3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7345AD062703D9480020AFB1 /* Assets.xcassets */; };
 		C1B5D38D282AFE870068D12B /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = C1B5D38C282AFE870068D12B /* README.md */; };
+		EEEEE201864437604E97D3B4 /* Pods_DaysUntilBirthdayForPod__iOS_.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F329EC9CE4A0A4AF2280834F /* Pods_DaysUntilBirthdayForPod__iOS_.framework */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXFileReference section */
@@ -62,10 +62,10 @@
 		739FCC45270E467600C92042 /* BirthdayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BirthdayView.swift; sourceTree = "<group>"; };
 		739FCC47270E659A00C92042 /* Birthday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Birthday.swift; sourceTree = "<group>"; };
 		7542DB53C46DFA9B71387188 /* Pods-DaysUntilBirthdayForPod (iOS).release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DaysUntilBirthdayForPod (iOS).release.xcconfig"; path = "Target Support Files/Pods-DaysUntilBirthdayForPod (iOS)/Pods-DaysUntilBirthdayForPod (iOS).release.xcconfig"; sourceTree = "<group>"; };
-		AE0FE424C7B3C57CF8808F8A /* libPods-DaysUntilBirthdayForPod (macOS).a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DaysUntilBirthdayForPod (macOS).a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		926A15D393D684C68E09FB0F /* Pods_DaysUntilBirthdayForPod__macOS_.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DaysUntilBirthdayForPod__macOS_.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		B7CC09BA1B68EB1881A08E87 /* Pods-DaysUntilBirthdayForPod(macOS).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DaysUntilBirthdayForPod(macOS).debug.xcconfig"; path = "Target Support Files/Pods-DaysUntilBirthdayForPod(macOS)/Pods-DaysUntilBirthdayForPod(macOS).debug.xcconfig"; sourceTree = "<group>"; };
 		C1B5D38C282AFE870068D12B /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
-		F163BB5E1E81280A79B4E535 /* libPods-DaysUntilBirthdayForPod (iOS).a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-DaysUntilBirthdayForPod (iOS).a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		F329EC9CE4A0A4AF2280834F /* Pods_DaysUntilBirthdayForPod__iOS_.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DaysUntilBirthdayForPod__iOS_.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		F84E43064819F374C9789E49 /* Pods-DaysUntilBirthdayForPod (macOS).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DaysUntilBirthdayForPod (macOS).debug.xcconfig"; path = "Target Support Files/Pods-DaysUntilBirthdayForPod (macOS)/Pods-DaysUntilBirthdayForPod (macOS).debug.xcconfig"; sourceTree = "<group>"; };
 		FE2F2ABC2800D9C1005EA17F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		FE71738027ECFAF400910319 /* DaysUntilBirthdayForPod (macOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "DaysUntilBirthdayForPod (macOS).app"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -79,7 +79,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				5965643074F45661539BC2DF /* libPods-DaysUntilBirthdayForPod (iOS).a in Frameworks */,
+				EEEEE201864437604E97D3B4 /* Pods_DaysUntilBirthdayForPod__iOS_.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -87,7 +87,7 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				7023CA98EF5DD94278069A12 /* libPods-DaysUntilBirthdayForPod (macOS).a in Frameworks */,
+				4C6A2BA2321434ACBD2AF201 /* Pods_DaysUntilBirthdayForPod__macOS_.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -174,8 +174,8 @@
 		FE71738F27ECFB3300910319 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
-				F163BB5E1E81280A79B4E535 /* libPods-DaysUntilBirthdayForPod (iOS).a */,
-				AE0FE424C7B3C57CF8808F8A /* libPods-DaysUntilBirthdayForPod (macOS).a */,
+				F329EC9CE4A0A4AF2280834F /* Pods_DaysUntilBirthdayForPod__iOS_.framework */,
+				926A15D393D684C68E09FB0F /* Pods_DaysUntilBirthdayForPod__macOS_.framework */,
 			);
 			name = Frameworks;
 			sourceTree = "<group>";

+ 2 - 0
Samples/Swift/DaysUntilBirthday/Podfile

@@ -2,6 +2,8 @@ pod 'GoogleSignIn', :path => '../../../', :testspecs => ['unit']
 pod 'GoogleSignInSwiftSupport', :path => '../../../', :testspecs => ['unit']
 project 'DaysUntilBirthdayForPod.xcodeproj'
 
+use_frameworks! :linkage => :static
+
 target 'DaysUntilBirthdayForPod (iOS)' do
   platform :ios, '14.0'
 end