소스 검색

Add macOS support including API changes, implementations and unit tests. (#104)

* Add macOS support including API changes, implementations and unit tests.

* Automate build and test on macOS.
pinlu 4 년 전
부모
커밋
61e8b8a874
31개의 변경된 파일728개의 추가작업 그리고 90개의 파일을 삭제
  1. 12 4
      .github/workflows/tests.yml
  2. 7 1
      GoogleSignIn.podspec
  3. 43 4
      GoogleSignIn/Sources/GIDAuthentication.m
  4. 4 0
      GoogleSignIn/Sources/GIDAuthentication_Private.h
  5. 5 0
      GoogleSignIn/Sources/GIDEMMErrorHandler.h
  6. 5 0
      GoogleSignIn/Sources/GIDEMMErrorHandler.m
  7. 5 0
      GoogleSignIn/Sources/GIDMDMPasscodeCache.h
  8. 5 0
      GoogleSignIn/Sources/GIDMDMPasscodeCache.m
  9. 4 0
      GoogleSignIn/Sources/GIDMDMPasscodeState.h
  10. 5 0
      GoogleSignIn/Sources/GIDMDMPasscodeState.m
  11. 5 0
      GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h
  12. 209 50
      GoogleSignIn/Sources/GIDSignIn.m
  13. 6 0
      GoogleSignIn/Sources/GIDSignInButton.m
  14. 29 3
      GoogleSignIn/Sources/GIDSignInInternalOptions.h
  15. 41 3
      GoogleSignIn/Sources/GIDSignInInternalOptions.m
  16. 5 0
      GoogleSignIn/Sources/NSBundle+GID3PAdditions.m
  17. 88 11
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h
  18. 5 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInButton.h
  19. 3 0
      GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h
  20. 23 1
      GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m
  21. 19 0
      GoogleSignIn/Tests/Unit/GIDConfigurationTest.m
  22. 5 0
      GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m
  23. 4 1
      GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m
  24. 20 0
      GoogleSignIn/Tests/Unit/GIDGoogleUserTest.m
  25. 6 0
      GoogleSignIn/Tests/Unit/GIDMDMPasscodeStateTests.m
  26. 50 2
      GoogleSignIn/Tests/Unit/GIDProfileDataTest.m
  27. 5 0
      GoogleSignIn/Tests/Unit/GIDSignInButtonTest.m
  28. 0 1
      GoogleSignIn/Tests/Unit/GIDSignInCallbackSchemesTest.m
  29. 12 0
      GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m
  30. 97 9
      GoogleSignIn/Tests/Unit/GIDSignInTest.m
  31. 1 0
      Package.swift

+ 12 - 4
.github/workflows/tests.yml

@@ -31,19 +31,27 @@ jobs:
 
   spm-build-test:
     runs-on: macOS-latest
+    strategy:
+      matrix:
+        sdk: ['macosx', 'iphonesimulator']
+        include:
+          - sdk: 'macosx'
+            destination: '"platform=OS X,arch=x86_64"'
+          - sdk: 'iphonesimulator'
+            destination: '"platform=iOS Simulator,name=iPhone 11"'
     steps:
     - uses: actions/checkout@v2
     - name: Build unit test target
       run: |
         xcodebuild \
           -scheme GoogleSignIn \
-          -sdk 'iphonesimulator' \
-          -destination 'platform=iOS Simulator,name=iPhone 11' \
+          -sdk ${{ matrix.sdk }} \
+          -destination ${{ matrix.destination }} \
           build-for-testing
     - name: Run unit test target
       run: |
         xcodebuild \
           -scheme GoogleSignIn \
-          -sdk 'iphonesimulator' \
-          -destination 'platform=iOS Simulator,name=iPhone 11' \
+          -sdk ${{ matrix.sdk }} \
+          -destination ${{ matrix.destination }} \
           test-without-building

+ 7 - 1
GoogleSignIn.podspec

@@ -13,7 +13,9 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
     :tag => s.version.to_s
   }
   ios_deployment_target = '9.0'
+  osx_deployment_target = '10.15'
   s.ios.deployment_target = ios_deployment_target
+  s.osx.deployment_target = osx_deployment_target
   s.prefix_header_file = false
   s.source_files = [
     'GoogleSignIn/Sources/**/*.[mh]',
@@ -29,6 +31,7 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
     'Security'
   ]
   s.ios.framework = 'UIKit'
+  s.osx.framework = 'AppKit'
   s.dependency 'AppAuth', '~> 1.4'
   s.dependency 'GTMAppAuth', '~> 1.0'
   s.dependency 'GTMSessionFetcher/Core', '~> 1.1'
@@ -41,7 +44,10 @@ The Google Sign-In SDK allows users to sign in with their Google account from th
     'DEFINES_MODULE' => 'YES'
   }
   s.test_spec 'unit' do |unit_tests|
-    unit_tests.platforms = {:ios => ios_deployment_target}
+    unit_tests.platforms = {
+      :ios => ios_deployment_target,
+      :osx => osx_deployment_target
+    }
     unit_tests.source_files = [
       'GoogleSignIn/Tests/Unit/**/*.[mh]',
     ]

+ 43 - 4
GoogleSignIn/Sources/GIDAuthentication.m

@@ -17,8 +17,12 @@
 #import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
 
 #import "GoogleSignIn/Sources/GIDSignInPreferences.h"
+
+#if TARGET_OS_IOS
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
+#endif
+
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
 
 #ifdef SWIFT_PACKAGE
@@ -53,6 +57,8 @@ static NSString *const kOldIOSSystemName = @"iPhone OS";
 // New UIDevice system name for iOS.
 static NSString *const kNewIOSSystemName = @"iOS";
 
+#if TARGET_OS_IOS
+
 // The specialized GTMAppAuthFetcherAuthorization delegate that handles potential EMM error
 // responses.
 @interface GTMAppAuthFetcherAuthorizationEMMChainedDelegate : NSObject
@@ -143,6 +149,8 @@ static NSString *const kNewIOSSystemName = @"iOS";
 
 @end
 
+#endif
+
 @implementation GIDAuthentication {
   // A queue for pending authentication handlers so we don't fire multiple requests in parallel.
   // Access to this ivar should be synchronized.
@@ -189,17 +197,24 @@ static NSString *const kNewIOSSystemName = @"iOS";
 
 #pragma mark - Private property accessors
 
+#if TARGET_OS_IOS
 - (NSString *)emmSupport {
   return
       _authState.lastAuthorizationResponse.request.additionalParameters[kEMMSupportParameterName];
 }
+#endif
 
 #pragma mark - Public methods
 
 - (id<GTMFetcherAuthorizationProtocol>)fetcherAuthorizer {
+#if TARGET_OS_IOS
   GTMAppAuthFetcherAuthorization *authorization = self.emmSupport ?
       [[GTMAppAuthFetcherAuthorizationWithEMMSupport alloc] initWithAuthState:_authState] :
       [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState];
+#else // TARGET_OS_OSX or TARGET_OS_MACCATALYST
+  GTMAppAuthFetcherAuthorization *authorization =
+      [[GTMAppAuthFetcherAuthorization alloc] initWithAuthState:_authState];
+#endif
   authorization.tokenRefreshDelegate = self;
   return authorization;
 }
@@ -221,10 +236,16 @@ static NSString *const kNewIOSSystemName = @"iOS";
     }
   }
   // This is the first handler in the queue, a fetch is needed.
+#if TARGET_OS_IOS
   OIDTokenRequest *tokenRefreshRequest =
-      [_authState tokenRefreshRequestWithAdditionalParameters:
-          [GIDAuthentication updatedEMMParametersWithParameters:
-              _authState.lastTokenResponse.request.additionalParameters]];
+    [_authState tokenRefreshRequestWithAdditionalParameters:
+        [GIDAuthentication updatedEMMParametersWithParameters:
+            _authState.lastTokenResponse.request.additionalParameters]];
+#else // TARGET_OS_OSX or TARGET_OS_MACCATALYST
+  OIDTokenRequest *tokenRefreshRequest =
+    [_authState tokenRefreshRequestWithAdditionalParameters:
+        _authState.lastTokenResponse.request.additionalParameters];
+#endif
   [OIDAuthorizationService performTokenRequest:tokenRefreshRequest
                  originalAuthorizationResponse:_authState.lastAuthorizationResponse
                                       callback:^(OIDTokenResponse *_Nullable tokenResponse,
@@ -244,6 +265,7 @@ static NSString *const kNewIOSSystemName = @"iOS";
         [self->_authState updateWithAuthorizationError:error];
       }
     }
+#if TARGET_OS_IOS
     [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *_Nullable error) {
       // Process the handler queue to call back.
       NSArray *authenticationHandlerQueue;
@@ -257,11 +279,23 @@ static NSString *const kNewIOSSystemName = @"iOS";
         });
       }
     }];
+#else // TARGET_OS_OSX or TARGET_OS_MACCATALYST
+    NSArray *authenticationHandlerQueue;
+    @synchronized(self->_authenticationHandlerQueue) {
+      authenticationHandlerQueue = [self->_authenticationHandlerQueue copy];
+      [self->_authenticationHandlerQueue removeAllObjects];
+    }
+    for (GIDAuthenticationAction action in authenticationHandlerQueue) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        action(error ? nil : self, error);
+      });
+    }
+#endif
   }];
 }
 
 #pragma mark - Private methods
-
+#if TARGET_OS_IOS
 + (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
                                 emmSupport:(nullable NSString *)emmSupport
                     isPasscodeInfoRequired:(BOOL)isPasscodeInfoRequired {
@@ -309,13 +343,18 @@ static NSString *const kNewIOSSystemName = @"iOS";
     completion(error);
   }
 }
+#endif
 
 #pragma mark - GTMAppAuthFetcherAuthorizationTokenRefreshDelegate
 
 - (nullable NSDictionary *)additionalRefreshParameters:
     (GTMAppAuthFetcherAuthorization *)authorization {
+#if TARGET_OS_IOS
   return [GIDAuthentication updatedEMMParametersWithParameters:
       authorization.authState.lastTokenResponse.request.additionalParameters];
+#else // TARGET_OS_MACCATALYST or TARGET_OS_OSX
+  return authorization.authState.lastTokenResponse.request.additionalParameters;
+#endif
 }
 
 #pragma mark - NSSecureCoding

+ 4 - 0
GoogleSignIn/Sources/GIDAuthentication_Private.h

@@ -32,11 +32,14 @@ NS_ASSUME_NONNULL_BEGIN
 // A representation of the state of the OAuth session for this instance.
 @property(nonatomic, readonly) OIDAuthState *authState;
 
+#if TARGET_OS_IOS
 // A string indicating support for Enterprise Mobility Management.
 @property(nonatomic, readonly) NSString *emmSupport;
+#endif
 
 - (instancetype)initWithAuthState:(OIDAuthState *)authState;
 
+#if TARGET_OS_IOS
 // Gets a new set of URL parameters that also contains EMM-related URL parameters if needed.
 + (NSDictionary *)parametersWithParameters:(NSDictionary *)parameters
                                 emmSupport:(nullable NSString *)emmSupport
@@ -48,6 +51,7 @@ NS_ASSUME_NONNULL_BEGIN
 // Handles potential EMM error from token fetch response.
 + (void)handleTokenFetchEMMError:(nullable NSError *)error
                       completion:(void (^)(NSError *_Nullable))completion;
+#endif
 
 @end
 

+ 5 - 0
GoogleSignIn/Sources/GIDEMMErrorHandler.h

@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS
 
 #import <Foundation/Foundation.h>
 
@@ -35,3 +38,5 @@ NS_ASSUME_NONNULL_BEGIN
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 5 - 0
GoogleSignIn/Sources/GIDEMMErrorHandler.m

@@ -11,6 +11,9 @@
 // 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
 
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
 
@@ -283,3 +286,5 @@ typedef enum {
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 5 - 0
GoogleSignIn/Sources/GIDMDMPasscodeCache.h

@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS
 
 #import <Foundation/Foundation.h>
 
@@ -39,3 +42,5 @@ NS_ASSUME_NONNULL_BEGIN
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 5 - 0
GoogleSignIn/Sources/GIDMDMPasscodeCache.m

@@ -11,6 +11,9 @@
 // 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
 
 #import "GoogleSignIn/Sources/GIDMDMPasscodeCache.h"
 
@@ -294,3 +297,5 @@ static const int64_t kObtainKeychainInfoWaitTime = 3 * NSEC_PER_SEC;
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 4 - 0
GoogleSignIn/Sources/GIDMDMPasscodeState.h

@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS
 
 #import <Foundation/Foundation.h>
 
@@ -48,3 +51,4 @@ NS_ASSUME_NONNULL_BEGIN
 
 NS_ASSUME_NONNULL_END
 
+#endif

+ 5 - 0
GoogleSignIn/Sources/GIDMDMPasscodeState.m

@@ -11,6 +11,9 @@
 // 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
 
 #import "GoogleSignIn/Sources/GIDMDMPasscodeState.h"
 
@@ -48,3 +51,5 @@ NS_ASSUME_NONNULL_BEGIN
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 5 - 0
GoogleSignIn/Sources/GIDMDMPasscodeState_Private.h

@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#import <TargetConditionals.h>
+
+#if TARGET_OS_IOS
 
 #import <Foundation/Foundation.h>
 
@@ -33,3 +36,5 @@ NS_ASSUME_NONNULL_BEGIN
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 209 - 50
GoogleSignIn/Sources/GIDSignIn.m

@@ -27,7 +27,9 @@
 #import "GoogleSignIn/Sources/GIDScopes.h"
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"
 #import "GoogleSignIn/Sources/GIDAuthStateMigration.h"
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#endif
 
 #import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
 #import "GoogleSignIn/Sources/GIDGoogleUser_Private.h"
@@ -50,10 +52,16 @@
 #import <AppAuth/OIDTokenRequest.h>
 #import <AppAuth/OIDTokenResponse.h>
 #import <AppAuth/OIDURLQueryComponent.h>
-#import <AppAuth/OIDAuthorizationService+IOS.h>
 #import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
 #import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #import <GTMSessionFetcher/GTMSessionFetcher.h>
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+#import <AppAuth/OIDAuthorizationService+IOS.h>
+#elif TARGET_OS_OSX
+#import <AppAuth/OIDAuthorizationService+Mac.h>
+#endif
+
 #endif
 
 NS_ASSUME_NONNULL_BEGIN
@@ -204,6 +212,8 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   return YES;
 }
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+
 - (void)signInWithConfiguration:(GIDConfiguration *)configuration
        presentingViewController:(UIViewController *)presentingViewController
                            hint:(nullable NSString *)hint
@@ -267,7 +277,100 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
                                        presentingViewController:presentingViewController
                                                       loginHint:self.currentUser.profile.email
-                                                   addScopesFlow:YES
+                                                  addScopesFlow:YES
+                                                       callback:callback];
+
+  NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
+  NSMutableSet<NSString *> *grantedScopes =
+      [NSMutableSet setWithArray:self.currentUser.grantedScopes];
+
+  // Check to see if all requested scopes have already been granted.
+  if ([requestedScopes isSubsetOfSet:grantedScopes]) {
+    // All requested scopes have already been granted, notify callback of failure.
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeScopesAlreadyGranted
+                                     userInfo:nil];
+    if (callback) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        callback(nil, error);
+      });
+    }
+    return;
+  }
+
+  // Use the union of granted and requested scopes.
+  [grantedScopes unionSet:requestedScopes];
+  options.scopes = [grantedScopes allObjects];
+
+  [self signInWithOptions:options];
+}
+
+#elif TARGET_OS_OSX
+
+- (void)signInWithConfiguration:(GIDConfiguration *)configuration
+               presentingWindow:(NSWindow *)presentingWindow
+                           hint:(nullable NSString *)hint
+                       callback:(nullable GIDSignInCallback)callback {
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
+                                               presentingWindow:presentingWindow
+                                                      loginHint:hint
+                                                   addScopesFlow:NO
+                                                       callback:callback];
+  [self signInWithOptions:options];
+}
+
+- (void)signInWithConfiguration:(GIDConfiguration *)configuration
+               presentingWindow:(NSWindow *)presentingWindow
+                       callback:(nullable GIDSignInCallback)callback {
+  [self signInWithConfiguration:configuration
+               presentingWindow:presentingWindow
+                           hint:nil
+                       callback:callback];
+}
+
+- (void)signInWithConfiguration:(GIDConfiguration *)configuration
+               presentingWindow:(NSWindow *)presentingWindow
+                           hint:(nullable NSString *)hint
+                         scopes:(nullable NSArray *)scopes
+                       callback:(nullable GIDSignInCallback)callback {
+  GIDSignInInternalOptions *options =
+    [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
+                                             presentingWindow:presentingWindow
+                                                    loginHint:hint
+                                                addScopesFlow:NO
+                                                       scopes:scopes
+                                                     callback:callback];
+  [self signInWithOptions:options];
+}
+
+- (void)addScopes:(NSArray<NSString *> *)scopes
+            presentingWindow:(NSWindow *)presentingWindow
+                    callback:(nullable GIDSignInCallback)callback {
+  // A currentUser must be available in order to complete this flow.
+  if (!self.currentUser) {
+    // No currentUser is set, notify callback of failure.
+    NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain
+                                         code:kGIDSignInErrorCodeNoCurrentUser
+                                     userInfo:nil];
+    if (callback) {
+      dispatch_async(dispatch_get_main_queue(), ^{
+        callback(nil, error);
+      });
+    }
+    return;
+  }
+
+  GIDConfiguration *configuration =
+      [[GIDConfiguration alloc] initWithClientID:self.currentUser.authentication.clientID
+                                  serverClientID:self.currentUser.serverClientID
+                                    hostedDomain:self.currentUser.hostedDomain
+                                     openIDRealm:self.currentUser.openIDRealm];
+  GIDSignInInternalOptions *options =
+      [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
+                                               presentingWindow:presentingWindow
+                                                      loginHint:self.currentUser.profile.email
+                                                  addScopesFlow:YES
                                                        callback:callback];
 
   NSSet<NSString *> *requestedScopes = [NSSet setWithArray:scopes];
@@ -295,6 +398,8 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   [self signInWithOptions:options];
 }
 
+#endif
+
 - (void)signOut {
   // Clear the current user if there is one.
   if (_currentUser) {
@@ -451,9 +556,9 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
                                              [schemes clientIdentifierScheme],
                                              kBrowserCallbackPath]];
   NSString *emmSupport;
-#if TARGET_OS_MACCATALYST
+#if TARGET_OS_MACCATALYST || TARGET_OS_OSX
   emmSupport = nil;
-#else
+#elif TARGET_OS_IOS
   emmSupport = [[self class] isOperatingSystemAtLeast9] ? kEMMVersion : nil;
 #endif
 
@@ -468,11 +573,17 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   if (options.configuration.hostedDomain) {
     additionalParameters[kHostedDomainParameter] = options.configuration.hostedDomain;
   }
+
+#if TARGET_OS_IOS
   [additionalParameters addEntriesFromDictionary:
       [GIDAuthentication parametersWithParameters:options.extraParams
                                        emmSupport:emmSupport
                            isPasscodeInfoRequired:NO]];
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+  [additionalParameters addEntriesFromDictionary:options.extraParams];
+#endif
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   OIDAuthorizationRequest *request =
       [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
                                                     clientId:options.configuration.clientID
@@ -480,65 +591,97 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
                                                  redirectURL:redirectURL
                                                 responseType:OIDResponseTypeCode
                                         additionalParameters:additionalParameters];
+
   _currentAuthorizationFlow = [OIDAuthorizationService
       presentAuthorizationRequest:request
          presentingViewController:options.presentingViewController
+                        callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
+                                   NSError *_Nullable error) {
+    [self processAuthorizationResponse:authorizationResponse
+                                 error:error
+                            emmSupport:emmSupport];
+  }];
+#elif TARGET_OS_OSX
+  OIDAuthorizationRequest *request =
+      [[OIDAuthorizationRequest alloc] initWithConfiguration:_appAuthConfiguration
+                                                    clientId:options.configuration.clientID
+                                                clientSecret:@""
+                                                      scopes:options.scopes
+                                                 redirectURL:redirectURL
+                                                responseType:OIDResponseTypeCode
+                                        additionalParameters:additionalParameters];
+
+  _currentAuthorizationFlow = [OIDAuthorizationService
+      presentAuthorizationRequest:request
+                 presentingWindow:options.presentingWindow
                          callback:^(OIDAuthorizationResponse *_Nullable authorizationResponse,
                                     NSError *_Nullable error) {
-    if (self->_restarting) {
-      // The auth flow is restarting, so the work here would be performed in the next round.
-      self->_restarting = NO;
-      return;
-    }
+    [self processAuthorizationResponse:authorizationResponse
+                                 error:error
+                            emmSupport:emmSupport];
+  }];
+#endif
 
-    GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
-    authFlow.emmSupport = emmSupport;
+}
 
-    if (authorizationResponse) {
-      if (authorizationResponse.authorizationCode.length) {
-        authFlow.authState = [[OIDAuthState alloc]
-            initWithAuthorizationResponse:authorizationResponse];
-        // perform auth code exchange
-        [self maybeFetchToken:authFlow fallback:nil];
-      } else {
-        // There was a failure, convert to appropriate error code.
-        NSString *errorString;
-        GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
-        NSDictionary<NSString *, NSObject *> *params = authorizationResponse.additionalParameters;
-
-        if (authFlow.emmSupport) {
-          [authFlow wait];
-          BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance]
-              handleErrorFromResponse:params
-                           completion:^{
-                             [authFlow next];
-                           }];
-          if (isEMMError) {
-            errorCode = kGIDSignInErrorCodeEMM;
-          }
-        }
-        errorString = (NSString *)params[kOAuth2ErrorKeyName];
-        if ([errorString isEqualToString:kOAuth2AccessDenied]) {
-          errorCode = kGIDSignInErrorCodeCanceled;
-        }
+- (void)processAuthorizationResponse:(OIDAuthorizationResponse *)authorizationResponse
+                               error:(NSError *)error
+                          emmSupport:(NSString *)emmSupport{
+  if (_restarting) {
+    // The auth flow is restarting, so the work here would be performed in the next round.
+    _restarting = NO;
+    return;
+  }
 
-        authFlow.error = [self errorWithString:errorString code:errorCode];
-      }
+  GIDAuthFlow *authFlow = [[GIDAuthFlow alloc] init];
+  authFlow.emmSupport = emmSupport;
+
+  if (authorizationResponse) {
+    if (authorizationResponse.authorizationCode.length) {
+      authFlow.authState = [[OIDAuthState alloc]
+          initWithAuthorizationResponse:authorizationResponse];
+      // perform auth code exchange
+      [self maybeFetchToken:authFlow fallback:nil];
     } else {
-      NSString *errorString = [error localizedDescription];
+      // There was a failure, convert to appropriate error code.
+      NSString *errorString;
       GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
-      if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
-        // The user has canceled the flow at the iOS modal dialog.
-        errorString = kUserCanceledError;
+      NSDictionary<NSString *, NSObject *> *params = authorizationResponse.additionalParameters;
+
+#if TARGET_OS_IOS
+      if (authFlow.emmSupport) {
+        [authFlow wait];
+        BOOL isEMMError = [[GIDEMMErrorHandler sharedInstance]
+            handleErrorFromResponse:params
+                         completion:^{
+                           [authFlow next];
+                         }];
+        if (isEMMError) {
+          errorCode = kGIDSignInErrorCodeEMM;
+        }
+      }
+#endif
+      errorString = (NSString *)params[kOAuth2ErrorKeyName];
+      if ([errorString isEqualToString:kOAuth2AccessDenied]) {
         errorCode = kGIDSignInErrorCodeCanceled;
       }
+
       authFlow.error = [self errorWithString:errorString code:errorCode];
     }
+  } else {
+    NSString *errorString = [error localizedDescription];
+    GIDSignInErrorCode errorCode = kGIDSignInErrorCodeUnknown;
+    if (error.code == OIDErrorCodeUserCanceledAuthorizationFlow) {
+      // The user has canceled the flow at the iOS modal dialog.
+      errorString = kUserCanceledError;
+      errorCode = kGIDSignInErrorCodeCanceled;
+    }
+    authFlow.error = [self errorWithString:errorString code:errorCode];
+  }
 
-    [self addDecodeIdTokenCallback:authFlow];
-    [self addSaveAuthCallback:authFlow];
-    [self addCompletionCallback:authFlow];
-  }];
+  [self addDecodeIdTokenCallback:authFlow];
+  [self addSaveAuthCallback:authFlow];
+  [self addCompletionCallback:authFlow];
 }
 
 // Perform authentication with the provided options.
@@ -559,7 +702,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
                                          code:kGIDSignInErrorCodeHasNoAuthInKeychain
                                      userInfo:nil];
     if (options.callback) {
-      self->_currentOptions = nil;
+      _currentOptions = nil;
       dispatch_async(dispatch_get_main_queue(), ^{
         options.callback(nil, error);
       });
@@ -596,6 +739,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   if (_currentOptions.configuration.openIDRealm) {
     additionalParameters[kOpenIDRealmParameter] = _currentOptions.configuration.openIDRealm;
   }
+#if TARGET_OS_IOS
   NSDictionary<NSString *, NSObject *> *params =
       authState.lastAuthorizationResponse.additionalParameters;
   NSString *passcodeInfoRequired = (NSString *)params[kEMMPasscodeInfoRequiredKeyName];
@@ -603,6 +747,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       [GIDAuthentication parametersWithParameters:@{}
                                        emmSupport:authFlow.emmSupport
                            isPasscodeInfoRequired:passcodeInfoRequired.length > 0]];
+#endif
   OIDTokenRequest *tokenRequest;
   if (!authState.lastTokenResponse.accessToken &&
       authState.lastAuthorizationResponse.authorizationCode) {
@@ -630,6 +775,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
       }
     }
 
+#if TARGET_OS_IOS
     if (authFlow.emmSupport) {
       [GIDAuthentication handleTokenFetchEMMError:error completion:^(NSError *error) {
         authFlow.error = error;
@@ -638,6 +784,9 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
     } else {
       [authFlow next];
     }
+#elif TARGET_OS_OSX || TARGET_OS_MACCATALYST
+    [authFlow next];
+#endif
   }];
 }
 
@@ -763,9 +912,15 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
   if (![@"restart_auth" isEqualToString:actionString]) {
     return NO;
   }
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   if (!_currentOptions.presentingViewController) {
     return NO;
   }
+#elif TARGET_OS_OSX
+  if (!_currentOptions.presentingWindow) {
+    return NO;
+  }
+#endif
   if (!_currentAuthorizationFlow) {
     return NO;
   }
@@ -826,7 +981,11 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 
 // Assert that the presenting view controller has been set.
 - (void)assertValidPresentingViewController {
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   if (!_currentOptions.presentingViewController) {
+#elif TARGET_OS_OSX
+  if (!_currentOptions.presentingWindow) {
+#endif
     // NOLINTNEXTLINE(google-objc-avoid-throwing-exception)
     [NSException raise:NSInvalidArgumentException
                 format:@"|presentingViewController| must be set."];
@@ -881,7 +1040,7 @@ static const NSTimeInterval kMinimumRestoredAccessTokenTimeToExpire = 600.0;
 // Set currentUser making appropriate KVO calls.
 - (void)setCurrentUserWithKVO:(GIDGoogleUser *_Nullable)user {
   [self willChangeValueForKey:NSStringFromSelector(@selector(currentUser))];
-  self->_currentUser = user;
+  _currentUser = user;
   [self didChangeValueForKey:NSStringFromSelector(@selector(currentUser))];
 }
 

+ 6 - 0
GoogleSignIn/Sources/GIDSignInButton.m

@@ -12,6 +12,10 @@
 // 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/Public/GoogleSignIn/GIDSignInButton.h"
 
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
@@ -639,3 +643,5 @@ static UIColor *colorForStyleState(GIDSignInButtonColorScheme style,
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 29 - 3
GoogleSignIn/Sources/GIDSignInInternalOptions.h

@@ -16,6 +16,12 @@
 
 #import <Foundation/Foundation.h>
 
+#if __has_include(<UIKit/UIKit.h>)
+#import <UIKit/UIKit.h>
+#elif __has_include(<AppKit/AppKit.h>)
+#import <AppKit/AppKit.h>
+#endif
+
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
 
 @class GIDConfiguration;
@@ -40,8 +46,13 @@ NS_ASSUME_NONNULL_BEGIN
 /// The configuration to use during the flow.
 @property(nonatomic, readonly, nullable) GIDConfiguration *configuration;
 
-/// The the view controller to use during the flow.
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+/// The view controller to use during the flow.
 @property(nonatomic, readonly, weak, nullable) UIViewController *presentingViewController;
+#elif TARGET_OS_OSX
+/// The window to use during the flow.
+@property(nonatomic, readonly, weak, nullable) NSWindow *presentingWindow;
+#endif
 
 /// The callback block to be called at the completion of the flow.
 @property(nonatomic, readonly, nullable) GIDSignInCallback callback;
@@ -53,9 +64,9 @@ NS_ASSUME_NONNULL_BEGIN
 @property(nonatomic, copy, nullable) NSString *loginHint;
 
 /// Creates the default options.
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
-                       presentingViewController:
-                           (nullable UIViewController *)presentingViewController
+                       presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                        callback:(nullable GIDSignInCallback)callback;
@@ -67,6 +78,21 @@ NS_ASSUME_NONNULL_BEGIN
                                          scopes:(nullable NSArray *)scopes
                                        callback:(nullable GIDSignInCallback)callback;
 
+#elif TARGET_OS_OSX
++ (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
+                               presentingWindow:(nullable NSWindow *)presentingWindow
+                                      loginHint:(nullable NSString *)loginHint
+                                  addScopesFlow:(BOOL)addScopesFlow
+                                       callback:(nullable GIDSignInCallback)callback;
+
++ (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
+                               presentingWindow:(nullable NSWindow *)presentingWindow
+                                      loginHint:(nullable NSString *)loginHint
+                                  addScopesFlow:(BOOL)addScopesFlow
+                                         scopes:(nullable NSArray *)scopes
+                                       callback:(nullable GIDSignInCallback)callback;
+#endif
+
 /// Creates the options to sign in silently.
 + (instancetype)silentOptionsWithCallback:(GIDSignInCallback)callback;
 

+ 41 - 3
GoogleSignIn/Sources/GIDSignInInternalOptions.m

@@ -14,26 +14,44 @@
 
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 
+#if __has_include(<UIKit/UIKit.h>)
+#import <UIKit/UIKit.h>
+#elif __has_include(<AppKit/AppKit.h>)
+#import <AppKit/AppKit.h>
+#endif
+
 #import "GoogleSignIn/Sources/GIDScopes.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
 @implementation GIDSignInInternalOptions
-
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
++ (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
+                       presentingViewController:(nullable UIViewController *)presentingViewController
+                                      loginHint:(nullable NSString *)loginHint
+                                  addScopesFlow:(BOOL)addScopesFlow
+                                         scopes:(nullable NSArray *)scopes
+                                       callback:(nullable GIDSignInCallback)callback {
+#elif TARGET_OS_OSX
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
-                       presentingViewController:
-                           (nullable UIViewController *)presentingViewController
+                               presentingWindow:(nullable NSWindow *)presentingWindow
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                          scopes:(nullable NSArray *)scopes
                                        callback:(nullable GIDSignInCallback)callback {
+#endif
+  
   GIDSignInInternalOptions *options = [[GIDSignInInternalOptions alloc] init];
   if (options) {
     options->_interactive = YES;
     options->_continuation = NO;
     options->_addScopesFlow = addScopesFlow;
     options->_configuration = configuration;
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
     options->_presentingViewController = presentingViewController;
+#elif TARGET_OS_OSX
+    options->_presentingWindow = presentingWindow;
+#endif
     options->_loginHint = loginHint;
     options->_callback = callback;
     options->_scopes = [GIDScopes scopesWithBasicProfile:scopes];
@@ -41,13 +59,25 @@ NS_ASSUME_NONNULL_BEGIN
   return options;
 }
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 + (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
                        presentingViewController:(nullable UIViewController *)presentingViewController
                                       loginHint:(nullable NSString *)loginHint
                                   addScopesFlow:(BOOL)addScopesFlow
                                        callback:(nullable GIDSignInCallback)callback {
+#else // TARGET_OS_OSX
++ (instancetype)defaultOptionsWithConfiguration:(nullable GIDConfiguration *)configuration
+                               presentingWindow:(nullable NSWindow *)presentingWindow
+                                      loginHint:(nullable NSString *)loginHint
+                                  addScopesFlow:(BOOL)addScopesFlow
+                                       callback:(nullable GIDSignInCallback)callback {
+#endif
     GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                                      presentingViewController:presentingViewController
+#elif TARGET_OS_OSX
+                                                             presentingWindow:presentingWindow
+#endif
                                                                     loginHint:loginHint
                                                                 addScopesFlow:addScopesFlow
                                                                        scopes:@[]
@@ -57,7 +87,11 @@ NS_ASSUME_NONNULL_BEGIN
 
 + (instancetype)silentOptionsWithCallback:(GIDSignInCallback)callback {
   GIDSignInInternalOptions *options = [self defaultOptionsWithConfiguration:nil
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                                    presentingViewController:nil
+#elif TARGET_OS_OSX
+                                                           presentingWindow:nil
+#endif
                                                                   loginHint:nil
                                                                addScopesFlow:NO
                                                                    callback:callback];
@@ -75,7 +109,11 @@ NS_ASSUME_NONNULL_BEGIN
     options->_continuation = continuation;
     options->_addScopesFlow = _addScopesFlow;
     options->_configuration = _configuration;
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
     options->_presentingViewController = _presentingViewController;
+#elif TARGET_OS_OSX
+    options->_presentingWindow = _presentingWindow;
+#endif
     options->_loginHint = _loginHint;
     options->_callback = _callback;
     options->_scopes = _scopes;

+ 5 - 0
GoogleSignIn/Sources/NSBundle+GID3PAdditions.m

@@ -15,7 +15,10 @@
 #import "GoogleSignIn/Sources/NSBundle+GID3PAdditions.h"
 
 #import <CoreText/CoreText.h>
+
+#if __has_include(<UIKit/UIKit.h>)
 #import <UIKit/UIKit.h>
+#endif
 
 #import "GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h"
 
@@ -47,10 +50,12 @@ NSString *const GoogleSignInBundleName = @"GoogleSignIn";
     NSArray* allFontNames = @[ @"Roboto-Bold" ];
     NSBundle* bundle = [self gid_frameworkBundle];
     for (NSString *fontName in allFontNames) {
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
       // Check to see if the font is already here, and skip registration if so.
       if ([UIFont fontWithName:fontName size:[UIFont systemFontSize]]) {  // size doesn't matter
         continue;
       }
+#endif
 
       // Load the font data file from the bundle.
       NSString *path = [bundle pathForResource:fontName ofType:@"ttf"];

+ 88 - 11
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignIn.h

@@ -15,7 +15,13 @@
  */
 
 #import <Foundation/Foundation.h>
+#import <TargetConditionals.h>
+
+#if __has_include(<UIKit/UIKit.h>)
 #import <UIKit/UIKit.h>
+#elif __has_include(<AppKit/AppKit.h>)
+#import <AppKit/AppKit.h>
+#endif
 
 @class GIDConfiguration;
 @class GIDGoogleUser;
@@ -89,7 +95,18 @@ typedef void (^GIDDisconnectCallback)(NSError *_Nullable error);
 ///     called asynchronously on the main queue.
 - (void)restorePreviousSignInWithCallback:(nullable GIDSignInCallback)callback;
 
-/// Starts an interactive sign-in flow using the provided configuration.
+/// Marks current user as being in the signed out state.
+- (void)signOut;
+
+/// Disconnects the current user from the app and revokes previous authentication. If the operation
+/// succeeds, the OAuth 2.0 token is also removed from keychain.
+///
+/// @param callback The optional `GIDDisconnectCallback` block that is called on completion.  This
+///     block will be called asynchronously on the main queue.
+- (void)disconnectWithCallback:(nullable GIDDisconnectCallback)callback;
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+/// Starts an interactive sign-in flow on iOS using the provided configuration.
 ///
 /// The callback will be called at the end of this process.  Any saved sign-in state will be
 /// replaced by the result of this flow.  Note that this method should not be called when the app is
@@ -107,7 +124,7 @@ typedef void (^GIDDisconnectCallback)(NSError *_Nullable error);
                        callback:(nullable GIDSignInCallback)callback
     NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
-/// Starts an interactive sign-in flow using the provided configuration and a login hint.
+/// Starts an interactive sign-in flow  on iOS using the provided configuration and a login hint.
 ///
 /// The callback will be called at the end of this process.  Any saved sign-in state will be
 /// replaced by the result of this flow.  Note that this method should not be called when the app is
@@ -128,7 +145,7 @@ typedef void (^GIDDisconnectCallback)(NSError *_Nullable error);
                        callback:(nullable GIDSignInCallback)callback
     NS_EXTENSION_UNAVAILABLE("The sign-in flow is not supported in App Extensions.");
 
-/// Starts an interactive sign-in flow using the provided configuration and a login hint.
+/// Starts an interactive sign-in flow on iOS using the provided configuration and a login hint.
 ///
 /// The callback will be called at the end of this process.  Any saved sign-in state will be
 /// replaced by the result of this flow.  Note that this method should not be called when the app is
@@ -150,7 +167,7 @@ typedef void (^GIDDisconnectCallback)(NSError *_Nullable error);
                          scopes:(nullable NSArray *)scopes
                        callback:(nullable GIDSignInCallback)callback;
 
-/// Starts an interactive consent flow to add scopes to the current user's grants.
+/// Starts an interactive consent flow on iOS to add scopes to the current user's grants.
 ///
 /// The callback will be called at the end of this process.  If successful, a new `GIDGoogleUser`
 /// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
@@ -166,15 +183,75 @@ typedef void (^GIDDisconnectCallback)(NSError *_Nullable error);
                     callback:(nullable GIDSignInCallback)callback
     NS_EXTENSION_UNAVAILABLE("The add scopes flow is not supported in App Extensions."); 
 
-/// Marks current user as being in the signed out state.
-- (void)signOut;
+#elif TARGET_OS_OSX
+/// Starts an interactive sign-in flow on macOS using the provided configuration.
+///
+/// The callback will be called at the end of this process.  Any saved sign-in state will be
+/// replaced by the result of this flow.  Note that this method should not be called when the app is
+/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
+/// `restorePreviousSignInWithCallback:` method to restore a previous sign-in.
+///
+/// @param configuration The configuration properties to be used for this flow.
+/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
+/// @param callback The `GIDSignInCallback` block that is called on completion.  This block will be
+///     called asynchronously on the main queue.
+- (void)signInWithConfiguration:(GIDConfiguration *)configuration
+               presentingWindow:(NSWindow *)presentingWindow
+                       callback:(nullable GIDSignInCallback)callback;
 
-/// Disconnects the current user from the app and revokes previous authentication. If the operation
-/// succeeds, the OAuth 2.0 token is also removed from keychain.
+/// Starts an interactive sign-in flow on macOS using the provided configuration and a login hint.
 ///
-/// @param callback The optional `GIDDisconnectCallback` block that is called on completion.  This
-///     block will be called asynchronously on the main queue.
-- (void)disconnectWithCallback:(nullable GIDDisconnectCallback)callback;
+/// The callback will be called at the end of this process.  Any saved sign-in state will be
+/// replaced by the result of this flow.  Note that this method should not be called when the app is
+/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
+/// `restorePreviousSignInWithCallback:` method to restore a previous sign-in.
+///
+/// @param configuration The configuration properties to be used for this flow.
+/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
+/// @param hint An optional hint for the authorization server, for example the user's ID or email
+///     address, to be prefilled if possible.
+/// @param callback The `GIDSignInCallback` block that is called on completion.  This block will be
+///     called asynchronously on the main queue.
+- (void)signInWithConfiguration:(GIDConfiguration *)configuration
+               presentingWindow:(NSWindow *)presentingWindow
+                           hint:(nullable NSString *)hint
+                       callback:(nullable GIDSignInCallback)callback;
+
+/// Starts an interactive sign-in flow on macOS using the provided configuration and a login hint.
+///
+/// The callback will be called at the end of this process.  Any saved sign-in state will be
+/// replaced by the result of this flow.  Note that this method should not be called when the app is
+/// starting up, (e.g in `application:didFinishLaunchingWithOptions:`); instead use the
+/// `restorePreviousSignInWithCallback:` method to restore a previous sign-in.
+///
+/// @param configuration The configuration properties to be used for this flow.
+/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
+/// @param hint An optional hint for the authorization server, for example the user's ID or email
+///     address, to be prefilled if possible.
+/// @param scopes a list of initial scopes
+/// @param callback The `GIDSignInCallback` block that is called on completion.  This block will be
+///     called asynchronously on the main queue.
+
+- (void)signInWithConfiguration:(GIDConfiguration *)configuration
+               presentingWindow:(NSWindow *)presentingWindow
+                           hint:(nullable NSString *)hint
+                         scopes:(nullable NSArray *)scopes
+                       callback:(nullable GIDSignInCallback)callback;
+
+/// Starts an interactive consent flow on macOS to add scopes to the current user's grants.
+///
+/// The callback will be called at the end of this process.  If successful, a new `GIDGoogleUser`
+/// instance will be returned reflecting the new scopes and saved sign-in state will be updated.
+///
+/// @param scopes The scopes to ask the user to consent to.
+/// @param presentingWindow The window used to supply `presentationContextProvider` for `ASWebAuthenticationSession`.
+/// @param callback The `GIDSignInCallback` block that is called on completion.  This block will be
+///     called asynchronously on the main queue.
+- (void)addScopes:(NSArray<NSString *> *)scopes
+       presentingWindow:(NSWindow *)presentingWindow
+               callback:(nullable GIDSignInCallback)callback;
+
+#endif
 
 @end
 

+ 5 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GIDSignInButton.h

@@ -13,6 +13,9 @@
  * 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>
 
@@ -61,3 +64,5 @@ typedef NS_ENUM(NSInteger, GIDSignInButtonColorScheme) {
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 3 - 0
GoogleSignIn/Sources/Public/GoogleSignIn/GoogleSignIn.h

@@ -13,10 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#import <TargetConditionals.h>
 
 #import "GIDAuthentication.h"
 #import "GIDConfiguration.h"
 #import "GIDGoogleUser.h"
 #import "GIDProfileData.h"
 #import "GIDSignIn.h"
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 #import "GIDSignInButton.h"
+#endif

+ 23 - 1
GoogleSignIn/Tests/Unit/GIDAuthenticationTest.m

@@ -152,19 +152,23 @@ _Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObserve
   _observedAuths = [[NSMutableArray alloc] init];
   _changesObserved = 0;
   _fakeSystemName = kNewIOSName;
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   [GULSwizzler swizzleClass:[UIDevice class]
                    selector:@selector(systemName)
             isClassSelector:NO
                   withBlock:^(id sender) { return self->_fakeSystemName; }];
+#endif
 }
 
 - (void)tearDown {
   [GULSwizzler unswizzleClass:[OIDAuthorizationService class]
                      selector:@selector(performTokenRequest:originalAuthorizationResponse:callback:)
               isClassSelector:YES];
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   [GULSwizzler unswizzleClass:[UIDevice class]
                      selector:@selector(systemName)
               isClassSelector:NO];
+#endif
   for (GIDAuthentication *auth in _observedAuths) {
     for (unsigned int i = 0; i < kNumberOfObservedProperties; ++i) {
       [auth removeObserver:self forKeyPath:kObservedProperties[i]];
@@ -212,12 +216,29 @@ _Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObserve
 }
 
 - (void)testCoding {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    GIDAuthentication *auth = [self auth];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth requiringSecureCoding:YES error:nil];
+    GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDAuthentication class]
+                                                                   fromData:data
+                                                                      error:nil];
+    XCTAssertEqualObjects(auth, newAuth);
+    XCTAssertTrue([GIDAuthentication supportsSecureCoding]);
+  } else {
+    XCTSkip(@"Required API is not available for this test.");
+  }
+}
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+// Deprecated in iOS 13 and moacOS 10.14
+- (void)testLegacyCoding {
   GIDAuthentication *auth = [self auth];
   NSData *data = [NSKeyedArchiver archivedDataWithRootObject:auth];
   GIDAuthentication *newAuth = [NSKeyedUnarchiver unarchiveObjectWithData:data];
   XCTAssertEqualObjects(auth, newAuth);
   XCTAssertTrue([GIDAuthentication supportsSecureCoding]);
 }
+#endif
 
 - (void)testFetcherAuthorizer {
   // This is really hard to test without assuming how GTMAppAuthFetcherAuthorization works
@@ -301,7 +322,7 @@ _Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObserve
 }
 
 #pragma mark - EMM Support
-
+#if TARGET_OS_IOS
 - (void)testEMMSupport {
   _additionalTokenRequestParameters = @{
     @"emm_support" : @"xyz",
@@ -463,6 +484,7 @@ _Static_assert(kChangeTypeEnd == (sizeof(kObservedProperties) / sizeof(*kObserve
   XCTAssertEqualObjects(auth.authState.lastTokenResponse.request.additionalParameters,
                         expectedParameters);
 }
+#endif
 
 #pragma mark - NSKeyValueObserving
 

+ 19 - 0
GoogleSignIn/Tests/Unit/GIDConfigurationTest.m

@@ -80,11 +80,30 @@
 }
 
 - (void)testCoding {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    GIDConfiguration *configuration = [GIDConfiguration testInstance];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:configuration
+                                         requiringSecureCoding:YES
+                                                         error:nil];
+    GIDConfiguration *newConfiguration = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDConfiguration class]
+                                                                           fromData:data
+                                                                              error:nil];
+    XCTAssertEqualObjects(configuration, newConfiguration);
+    XCTAssertTrue(GIDConfiguration.supportsSecureCoding);
+  }  else {
+    XCTSkip(@"Required API is not available for this test.");
+  }
+}
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+// Deprecated in iOS 13 and macOS 10.14
+- (void)testLegacyCoding {
   GIDConfiguration *configuration = [GIDConfiguration testInstance];
   NSData *data = [NSKeyedArchiver archivedDataWithRootObject:configuration];
   GIDConfiguration *newConfiguration = [NSKeyedUnarchiver unarchiveObjectWithData:data];
   XCTAssertEqualObjects(configuration, newConfiguration);
   XCTAssertTrue(GIDConfiguration.supportsSecureCoding);
 }
+#endif
 
 @end

+ 5 - 0
GoogleSignIn/Tests/Unit/GIDEMMErrorHandlerTest.m

@@ -11,6 +11,9 @@
 // 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>
 #import <XCTest/XCTest.h>
@@ -469,3 +472,5 @@ NS_ASSUME_NONNULL_BEGIN
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif

+ 4 - 1
GoogleSignIn/Tests/Unit/GIDFakeMainBundle.m

@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h"
 
+#if __has_include(<UIKit/UIKit.h>)
 #import <UIKit/UIKit.h>
+#endif
+
+#import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h"
 
 #import <GoogleUtilities/GULSwizzler.h>
 #import <GoogleUtilities/GULSwizzler+Unswizzle.h>

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

@@ -55,6 +55,25 @@
 }
 
 - (void)testCoding {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:[OIDAuthState testInstance]
+                                                       profileData:[GIDProfileData testInstance]];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:user
+                                         requiringSecureCoding:YES
+                                                         error:nil];
+    GIDGoogleUser *newUser = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDGoogleUser class]
+                                                               fromData:data
+                                                                  error:nil];
+    XCTAssertEqualObjects(user, newUser);
+    XCTAssertTrue(GIDGoogleUser.supportsSecureCoding);
+  }  else {
+    XCTSkip(@"Required API is not available for this test.");
+  }
+}
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+// Deprecated in iOS 13 and macOS 10.14
+- (void)testLegacyCoding {
   GIDGoogleUser *user = [[GIDGoogleUser alloc] initWithAuthState:[OIDAuthState testInstance]
                                                      profileData:[GIDProfileData testInstance]];
   NSData *data = [NSKeyedArchiver archivedDataWithRootObject:user];
@@ -62,5 +81,6 @@
   XCTAssertEqualObjects(user, newUser);
   XCTAssertTrue(GIDGoogleUser.supportsSecureCoding);
 }
+#endif
 
 @end

+ 6 - 0
GoogleSignIn/Tests/Unit/GIDMDMPasscodeStateTests.m

@@ -12,6 +12,10 @@
 // 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 <Foundation/Foundation.h>
 #import <LocalAuthentication/LocalAuthentication.h>
 #import <UIKit/UIKit.h>
@@ -174,3 +178,5 @@
 }
 
 @end
+
+#endif

+ 50 - 2
GoogleSignIn/Tests/Unit/GIDProfileDataTest.m

@@ -36,7 +36,7 @@ static NSString *const kFIFEAvatarURL2 =
 static NSString *const kFIFEAvatarURL2WithDimension =
     @"https://lh3.googleusercontent.com/a/default-user=s100";
 
-@interface GIDProfileDataOld : NSObject <NSCoding>
+@interface GIDProfileDataOld : NSObject <NSCoding, NSSecureCoding>
 @end
 
 @implementation GIDProfileDataOld {
@@ -70,6 +70,10 @@ static NSString *const kFIFEAvatarURL2WithDimension =
   [encoder encodeObject:_imageURL forKey:@"picture"];
 }
 
++ (BOOL)supportsSecureCoding {
+    return YES;
+}
+
 @end
 
 @interface GIDProfileDataTest : XCTestCase
@@ -89,6 +93,49 @@ static NSString *const kFIFEAvatarURL2WithDimension =
 }
 
 - (void)testCoding {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    GIDProfileData *profileData = [self profileData];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profileData
+                                         requiringSecureCoding:YES
+                                                         error:nil];
+    GIDProfileData *newProfileData = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDProfileData class]
+                                                                       fromData:data
+                                                                          error:nil];
+    XCTAssertEqualObjects(profileData, newProfileData);
+    XCTAssertTrue(GIDProfileData.supportsSecureCoding);
+  } else {
+    XCTSkip(@"Required API is not available for this test.");
+  }
+}
+
+- (void)testOldArchiveFormat {
+  if (@available(iOS 11, macOS 10.13, *)) {
+    GIDProfileDataOld *oldProfile = [[GIDProfileDataOld alloc] initWithEmail:kEmail
+                                                                        name:kName
+                                                                    imageURL:kFIFEImageURL];
+    [NSKeyedArchiver setClassName:@"GIDProfileData" forClass:[GIDProfileDataOld class]];
+    NSData *data = [NSKeyedArchiver archivedDataWithRootObject:oldProfile
+                                         requiringSecureCoding:YES
+                                                         error:nil];
+
+    GIDProfileData *profileData = [NSKeyedUnarchiver unarchivedObjectOfClass:[GIDProfileData class]
+                                                                    fromData:data
+                                                                       error:nil];
+    XCTAssertEqualObjects(profileData.email, kEmail);
+    XCTAssertEqualObjects(profileData.name, kName);
+    XCTAssertNil(profileData.givenName);
+    XCTAssertNil(profileData.familyName);
+    XCTAssertTrue(profileData.hasImage);
+    XCTAssertEqualObjects([profileData imageURLWithDimension:kDimension].absoluteString,
+                          kFIFEImageURLWithDimension);
+  } else {
+    XCTSkip(@"Required API is not available for this test.");
+  }
+}
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+// Deprecated in iOS 13 and macOS 10.14
+- (void)testLegacyCoding {
   GIDProfileData *profileData = [self profileData];
   NSData *data = [NSKeyedArchiver archivedDataWithRootObject:profileData];
   GIDProfileData *newProfileData = [NSKeyedUnarchiver unarchiveObjectWithData:data];
@@ -96,7 +143,7 @@ static NSString *const kFIFEAvatarURL2WithDimension =
   XCTAssertTrue(GIDProfileData.supportsSecureCoding);
 }
 
-- (void)testOldArchiveFormat {
+- (void)testOldArchiveFormatLegacy {
   GIDProfileDataOld *oldProfile = [[GIDProfileDataOld alloc] initWithEmail:kEmail
                                                                       name:kName
                                                                   imageURL:kFIFEImageURL];
@@ -111,6 +158,7 @@ static NSString *const kFIFEAvatarURL2WithDimension =
   XCTAssertEqualObjects([profileData imageURLWithDimension:kDimension].absoluteString,
                         kFIFEImageURLWithDimension);
 }
+#endif
 
 - (void)testImageURLWithDimension {
   GIDProfileData *profileData;

+ 5 - 0
GoogleSignIn/Tests/Unit/GIDSignInButtonTest.m

@@ -11,6 +11,9 @@
 // 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>
 #import <XCTest/XCTest.h>
@@ -202,3 +205,5 @@ static NSString * const kAppBundleId = @"FakeBundleID";
 }
 
 @end
+
+#endif

+ 0 - 1
GoogleSignIn/Tests/Unit/GIDSignInCallbackSchemesTest.m

@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#import <UIKit/UIKit.h>
 #import <XCTest/XCTest.h>
 
 #import "GoogleSignIn/Sources/GIDSignInCallbackSchemes.h"

+ 12 - 0
GoogleSignIn/Tests/Unit/GIDSignInInternalOptionsTest.m

@@ -31,13 +31,21 @@
 
 - (void)testDefaultOptions {
   id configuration = OCMStrictClassMock([GIDConfiguration class]);
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   id presentingViewController = OCMStrictClassMock([UIViewController class]);
+#elif TARGET_OS_OSX
+  id presentingWindow = OCMStrictClassMock([NSWindow class]);
+#endif
   NSString *loginHint = @"login_hint";
   GIDSignInCallback callback = ^(GIDGoogleUser * _Nullable user, NSError * _Nullable error) {};
   
   GIDSignInInternalOptions *options =
       [GIDSignInInternalOptions defaultOptionsWithConfiguration:configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
                                        presentingViewController:presentingViewController
+#elif TARGET_OS_OSX
+                                               presentingWindow:presentingWindow
+#endif
                                                       loginHint:loginHint
                                                    addScopesFlow:NO
                                                        callback:callback];
@@ -47,7 +55,11 @@
   XCTAssertNil(options.extraParams);
 
   OCMVerifyAll(configuration);
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   OCMVerifyAll(presentingViewController);
+#elif TARGET_OS_OSX
+  OCMVerifyAll(presentingWindow);
+#endif
 }
 
 - (void)testSilentOptions {

+ 97 - 9
GoogleSignIn/Tests/Unit/GIDSignInTest.m

@@ -11,9 +11,16 @@
 // 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>
 
-#import <SafariServices/SafariServices.h>
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 #import <UIKit/UIKit.h>
+#elif TARGET_OS_OSX
+#import <AppKit/AppKit.h>
+#endif
+
+#import <SafariServices/SafariServices.h>
+
 #import <XCTest/XCTest.h>
 
 // Test module imports
@@ -23,7 +30,11 @@
 #import "GoogleSignIn/Sources/GIDSignInInternalOptions.h"
 #import "GoogleSignIn/Sources/GIDSignIn_Private.h"
 #import "GoogleSignIn/Sources/GIDAuthentication_Private.h"
+
+#if TARGET_OS_IOS
 #import "GoogleSignIn/Sources/GIDEMMErrorHandler.h"
+#endif
+
 #import "GoogleSignIn/Tests/Unit/GIDFakeFetcher.h"
 #import "GoogleSignIn/Tests/Unit/GIDFakeFetcherService.h"
 #import "GoogleSignIn/Tests/Unit/GIDFakeMainBundle.h"
@@ -45,7 +56,13 @@
 #import <AppAuth/OIDTokenRequest.h>
 #import <AppAuth/OIDTokenResponse.h>
 #import <AppAuth/OIDURLQueryComponent.h>
+
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 #import <AppAuth/OIDAuthorizationService+IOS.h>
+#elif TARGET_OS_OSX
+#import <AppAuth/OIDAuthorizationService+Mac.h>
+#endif
+
 #import <GTMAppAuth/GTMAppAuthFetcherAuthorization+Keychain.h>
 #import <GTMAppAuth/GTMAppAuthFetcherAuthorization.h>
 #import <GTMSessionFetcher/GTMSessionFetcher.h>
@@ -73,7 +90,6 @@ static NSString * const kLanguage = @"FakeLanguage";
 static NSString * const kScope = @"FakeScope";
 static NSString * const kScope2 = @"FakeScope2";
 static NSString * const kAuthCode = @"FakeAuthCode";
-static NSString * const kPassword = @"FakePassword";
 static NSString * const kFakeKeychainName = @"FakeKeychainName";
 static NSString * const kUserEmail = @"FakeUserEmail";
 static NSString * const kVerifier = @"FakeVerifier";
@@ -140,6 +156,7 @@ static NSString *const kNewScope = @"newScope";
 /// Unique pointer value for KVO tests.
 static void *kTestObserverContext = &kTestObserverContext;
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
 // This category is used to allow the test to swizzle a private method.
 @interface UIViewController (Testing)
 
@@ -148,6 +165,7 @@ static void *kTestObserverContext = &kTestObserverContext;
 - (UIWindow *)_window;
 
 @end
+#endif
 
 // This class extension exposes GIDSignIn methods to our tests.
 @interface GIDSignIn ()
@@ -178,8 +196,13 @@ static void *kTestObserverContext = &kTestObserverContext;
   // Mock |GTMAppAuthFetcherAuthorization|.
   id _authorization;
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   // Mock |UIViewController|.
   id _presentingViewController;
+#elif TARGET_OS_OSX
+  // Mock |NSWindow|.
+  id _presentingWindow;
+#endif
 
   // Mock for |GIDGoogleUser|.
   id _user;
@@ -223,8 +246,13 @@ static void *kTestObserverContext = &kTestObserverContext;
   // The saved authorization request.
   OIDAuthorizationRequest *_savedAuthorizationRequest;
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   // The saved presentingViewController from the authorization request.
   UIViewController *_savedPresentingViewController;
+#elif TARGET_OS_OSX
+  // The saved presentingWindow from the authorization request.
+  NSWindow *_savedPresentingWindow;
+#endif
 
   // The saved authorization callback.
   OIDAuthorizationCallback _savedAuthorizationCallback;
@@ -249,7 +277,9 @@ static void *kTestObserverContext = &kTestObserverContext;
 
 - (void)setUp {
   [super setUp];
+#if TARGET_OS_IOS
   _isEligibleForEMM = [UIDevice currentDevice].systemVersion.integerValue >= 9;
+#endif
   _saveAuthorizationReturnValue = YES;
 
   // States
@@ -259,7 +289,11 @@ static void *kTestObserverContext = &kTestObserverContext;
   _changedKeyPaths = [[NSMutableSet alloc] init];
 
   // Mocks
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   _presentingViewController = OCMStrictClassMock([UIViewController class]);
+#elif TARGET_OS_OSX
+  _presentingWindow = OCMStrictClassMock([NSWindow class]);
+#endif
   _authState = OCMStrictClassMock([OIDAuthState class]);
   OCMStub([_authState alloc]).andReturn(_authState);
   OCMStub([_authState initWithAuthorizationResponse:OCMOCK_ANY]).andReturn(_authState);
@@ -283,7 +317,11 @@ static void *kTestObserverContext = &kTestObserverContext;
   _oidAuthorizationService = OCMStrictClassMock([OIDAuthorizationService class]);
   OCMStub([_oidAuthorizationService
       presentAuthorizationRequest:SAVE_TO_ARG_BLOCK(self->_savedAuthorizationRequest)
-         presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+           presentingViewController:SAVE_TO_ARG_BLOCK(self->_savedPresentingViewController)
+#elif TARGET_OS_OSX
+           presentingWindow:SAVE_TO_ARG_BLOCK(self->_savedPresentingWindow)
+#endif
                          callback:COPY_TO_ARG_BLOCK(self->_savedAuthorizationCallback)]);
   OCMStub([self->_oidAuthorizationService
       performTokenRequest:SAVE_TO_ARG_BLOCK(self->_savedTokenRequest)
@@ -325,11 +363,17 @@ static void *kTestObserverContext = &kTestObserverContext;
   OCMVerifyAll(_tokenResponse);
   OCMVerifyAll(_tokenRequest);
   OCMVerifyAll(_authorization);
-  OCMVerifyAll(_presentingViewController);
   OCMVerifyAll(_user);
   OCMVerifyAll(_authentication);
   OCMVerifyAll(_oidAuthorizationService);
 
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+  OCMVerifyAll(_presentingViewController);
+#elif TARGET_OS_OSX
+  OCMVerifyAll(_presentingWindow);
+#endif
+
+
   [_fakeMainBundle stopFaking];
   [super tearDown];
 
@@ -473,10 +517,14 @@ static void *kTestObserverContext = &kTestObserverContext;
 
 
   GIDSignInInternalOptions *options = [GIDSignInInternalOptions defaultOptionsWithConfiguration:nil
-                                                                      presentingViewController:nil
-                                                                                     loginHint:nil
-                                                                                 addScopesFlow:YES
-                                                                                      callback:nil];
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
+                                                                       presentingViewController:nil
+#elif TARGET_OS_OSX
+                                                                               presentingWindow:nil
+#endif
+                                                                                      loginHint:nil
+                                                                                  addScopesFlow:YES
+                                                                                       callback:nil];
 
   id profile = OCMStrictClassMock([GIDProfileData class]);
   OCMStub([profile email]).andReturn(kUserEmail);
@@ -783,9 +831,19 @@ static void *kTestObserverContext = &kTestObserverContext;
 }
 
 - (void)testPresentingViewControllerException {
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
   _presentingViewController = nil;
+#elif TARGET_OS_OSX
+  _presentingWindow = nil;
+#endif
+
+
   XCTAssertThrows([_signIn signInWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
                           presentingViewController:_presentingViewController
+#elif TARGET_OS_OSX
+                                  presentingWindow:_presentingWindow
+#endif
                                               hint:_hint
                                           callback:_callback]);
 }
@@ -798,7 +856,11 @@ static void *kTestObserverContext = &kTestObserverContext;
   BOOL threw = NO;
   @try {
     [_signIn signInWithConfiguration:configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
             presentingViewController:_presentingViewController
+#elif TARGET_OS_OSX
+                    presentingWindow:_presentingWindow
+#endif
                             callback:nil];
   } @catch (NSException *exception) {
     threw = YES;
@@ -814,7 +876,11 @@ static void *kTestObserverContext = &kTestObserverContext;
   BOOL threw = NO;
   @try {
     [_signIn signInWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
             presentingViewController:_presentingViewController
+#elif TARGET_OS_OSX
+                    presentingWindow:_presentingWindow
+#endif
                                 hint:_hint
                             callback:_callback];
   } @catch (NSException *exception) {
@@ -837,6 +903,8 @@ static void *kTestObserverContext = &kTestObserverContext;
 
 #pragma mark - EMM tests
 
+#if TARGET_OS_IOS
+
 - (void)testEmmSupportRequestParameters {
   [self OAuthLoginWithOptions:nil
                     authError:nil
@@ -976,6 +1044,8 @@ static void *kTestObserverContext = &kTestObserverContext;
   XCTAssertNil(_signIn.currentUser, @"should not have current user");
 }
 
+#endif
+
 #pragma mark - Helpers
 
 // Whether or not a fetcher has been started.
@@ -1071,7 +1141,10 @@ static void *kTestObserverContext = &kTestObserverContext;
     [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
     [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
     if (oldAccessToken) {
+#if TARGET_OS_IOS
+      // Corresponds to EMM support 
       [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
+#endif
       [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
       [[[_authState expect] andReturn:tokenResponse] lastTokenResponse];
       [[[_authState expect] andReturn:tokenRequest]
@@ -1090,11 +1163,19 @@ static void *kTestObserverContext = &kTestObserverContext;
     };
     if (options.addScopesFlow) {
       [_signIn addScopes:@[kNewScope]
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
         presentingViewController:_presentingViewController
-                        callback:callback];
+#elif TARGET_OS_OSX
+        presentingWindow:_presentingWindow
+#endif
+                callback:callback];
     } else {
       [_signIn signInWithConfiguration:_configuration
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
               presentingViewController:_presentingViewController
+#elif TARGET_OS_OSX
+                      presentingWindow:_presentingWindow
+#endif
                                   hint:_hint
                               callback:callback];
     }
@@ -1106,12 +1187,19 @@ static void *kTestObserverContext = &kTestObserverContext;
     NSDictionary<NSString *, NSObject *> *params = _savedAuthorizationRequest.additionalParameters;
     XCTAssertEqualObjects(params[@"include_granted_scopes"], @"true");
     XCTAssertNotNil(_savedAuthorizationCallback);
+#if TARGET_OS_IOS || TARGET_OS_MACCATALYST
     XCTAssertEqual(_savedPresentingViewController, _presentingViewController);
+#elif TARGET_OS_OSX
+    XCTAssertEqual(_savedPresentingWindow, _presentingWindow);
+#endif
 
     // maybeFetchToken
     if (!(authError || modalCancel)) {
       [[[_authState expect] andReturn:nil] lastTokenResponse];
+#if TARGET_OS_IOS
+      // Corresponds to EMM support
       [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
+#endif
       [[[_authState expect] andReturn:nil] lastTokenResponse];
       [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];
       [[[_authState expect] andReturn:authResponse] lastAuthorizationResponse];

+ 1 - 0
Package.swift

@@ -78,6 +78,7 @@ let package = Package(
         .linkedFramework("Foundation"),
         .linkedFramework("LocalAuthentication"),
         .linkedFramework("Security"),
+        .linkedFramework("AppKit", .when(platforms: [.macOS])),
         .linkedFramework("UIKit", .when(platforms: [.iOS])),
       ]
     ),