diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 9edadf7b0469..86a3b565da21 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -2,6 +2,7 @@ * Updates tests to use a mock platform instead of relying on default method channel implementation internals. +* Removes example workaround to build for arm64 iOS simulators. ## 5.3.1 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Podfile b/packages/google_sign_in/google_sign_in/example/ios/Podfile index 56085c312df7..f7d6a5e68c3a 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Podfile +++ b/packages/google_sign_in/google_sign_in/example/ios/Podfile @@ -34,9 +34,5 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) - target.build_configurations.each do |build_configuration| - # GoogleSignIn does not support arm64 simulators. - build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' - end end end diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj index 8909bb9b31c6..6c698e15ba15 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj @@ -426,7 +426,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -448,7 +447,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", diff --git a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md index 90069d05f442..e5de49da7f22 100644 --- a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.3.0 + +* Supports arm64 iOS simulators by increasing GoogleSignIn dependency to version 6.2. + ## 5.2.7 * Fixes library_private_types_in_public_api, sort_child_properties_last and use_key_in_widget_constructors diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile b/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile index e577a3081fe8..5586baf49647 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Podfile @@ -28,6 +28,8 @@ require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelpe flutter_ios_podfile_setup target 'Runner' do + pod 'GoogleSignIn', :git => 'https://github.com/google/GoogleSignIn-iOS.git', :commit => '0dc783f36577f5bc249056f835d592ffae59d0ee' + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) target 'RunnerTests' do inherit! :search_paths @@ -39,9 +41,5 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) - target.build_configurations.each do |build_configuration| - # GoogleSignIn does not support arm64 simulators. - build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' - end end end diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj index f2bf4ebc514e..a7f2019ac311 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -384,7 +384,7 @@ ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleSignIn/GoogleSignIn.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( @@ -603,7 +603,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -625,7 +624,6 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -647,7 +645,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; @@ -662,7 +659,6 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; INFOPLIST_FILE = RunnerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m index 3bc08d18604a..7efd490f30fe 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/RunnerTests/GoogleSignInTests.m @@ -64,66 +64,103 @@ - (void)testSignOut { } - (void)testDisconnect { + [[self.mockSignIn stub] disconnectWithCallback:[OCMArg invokeBlockWithArgs:[NSNull null], nil]]; FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" arguments:nil]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; [self.plugin handleMethodCall:methodCall - result:^(id result){ + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result, @{}); + [expectation fulfill]; }]; - OCMVerify([self.mockSignIn disconnect]); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -#pragma mark - Init - -- (void)testInitGoogleServiceInfoPlist { - FlutterMethodCall *methodCall = [FlutterMethodCall - methodCallWithMethodName:@"init" - arguments:@{@"scopes" : @[ @"mockScope1" ], @"hostedDomain" : @"example.com"}]; +- (void)testDisconnectIgnoresError { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + [[self.mockSignIn stub] disconnectWithCallback:[OCMArg invokeBlockWithArgs:error, nil]]; + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" + arguments:nil]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; [self.plugin handleMethodCall:methodCall - result:^(id result) { - XCTAssertNil(result); + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result, @{}); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; - - id mockSignIn = self.mockSignIn; - OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); - OCMVerify([mockSignIn setHostedDomain:@"example.com"]); - - // Set in example app GoogleService-Info.plist. - OCMVerify([mockSignIn - setClientID:@"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"]); - OCMVerify([mockSignIn setServerClientID:@"YOUR_SERVER_CLIENT_ID"]); } -- (void)testInitNullDomain { - FlutterMethodCall *methodCall = +#pragma mark - Init + +- (void)testInitGoogleServiceInfoPlist { + FlutterMethodCall *initMethodCall = [FlutterMethodCall methodCallWithMethodName:@"init" - arguments:@{@"hostedDomain" : [NSNull null]}]; + arguments:@{@"hostedDomain" : @"example.com"}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; - OCMVerify([self.mockSignIn setHostedDomain:nil]); + + // Initialization values used in the next sign in request. + FlutterMethodCall *signInMethodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + [self.plugin handleMethodCall:signInMethodCall + result:^(id r){ + }]; + OCMVerify([self.mockSignIn + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + // Set in example app GoogleService-Info.plist. + return + [configuration.hostedDomain isEqualToString:@"example.com"] && + [configuration.clientID + isEqualToString: + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"] && + [configuration.serverClientID isEqualToString:@"YOUR_SERVER_CLIENT_ID"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]); } -- (void)testInitDynamicClientId { - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"init" - arguments:@{@"clientId" : @"mockClientId"}]; +- (void)testInitDynamicClientIdNullDomain { + FlutterMethodCall *initMethodCall = [FlutterMethodCall + methodCallWithMethodName:@"init" + arguments:@{@"hostedDomain" : [NSNull null], @"clientId" : @"mockClientId"}]; - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; - OCMVerify([self.mockSignIn setClientID:@"mockClientId"]); + + // Initialization values used in the next sign in request. + FlutterMethodCall *signInMethodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + [self.plugin handleMethodCall:signInMethodCall + result:^(id r){ + }]; + OCMVerify([self.mockSignIn + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + return configuration.hostedDomain == nil && + [configuration.clientID isEqualToString:@"mockClientId"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]); } #pragma mark - Is signed in @@ -161,59 +198,195 @@ - (void)testIsSignedIn { #pragma mark - Sign in silently - (void)testSignInSilently { - OCMExpect([self.mockSignIn restorePreviousSignIn]); + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUser userID]).andReturn(@"mockID"); + + [[self.mockSignIn stub] + restorePreviousSignInWithCallback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" arguments:nil]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; [self.plugin handleMethodCall:methodCall - result:^(id result){ + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"displayName"], [NSNull null]); + XCTAssertEqualObjects(result[@"email"], [NSNull null]); + XCTAssertEqualObjects(result[@"id"], @"mockID"); + XCTAssertEqualObjects(result[@"photoUrl"], [NSNull null]); + XCTAssertEqualObjects(result[@"serverAuthCode"], [NSNull null]); + [expectation fulfill]; }]; - OCMVerifyAll(self.mockSignIn); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testSignInSilentlyFailsConcurrently { +- (void)testSignInSilentlyWithError { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + + [[self.mockSignIn stub] + restorePreviousSignInWithCallback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" arguments:nil]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - - OCMExpect([self.mockSignIn restorePreviousSignIn]).andDo(^(NSInvocation *invocation) { - // Simulate calling the same method while the previous one is in flight. - [self.plugin handleMethodCall:methodCall - result:^(FlutterError *result) { - XCTAssertEqualObjects(result.code, @"concurrent-requests"); - [expectation fulfill]; - }]; - }); - [self.plugin handleMethodCall:methodCall - result:^(id result){ + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + [expectation fulfill]; }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; } #pragma mark - Sign in - (void)testSignIn { + id mockUser = OCMClassMock([GIDGoogleUser class]); + id mockUserProfile = OCMClassMock([GIDProfileData class]); + OCMStub([mockUserProfile name]).andReturn(@"mockDisplay"); + OCMStub([mockUserProfile email]).andReturn(@"mock@example.com"); + OCMStub([mockUserProfile hasImage]).andReturn(YES); + OCMStub([mockUserProfile imageURLWithDimension:1337]) + .andReturn([NSURL URLWithString:@"https://example.com/profile.png"]); + + OCMStub([mockUser profile]).andReturn(mockUserProfile); + OCMStub([mockUser userID]).andReturn(@"mockID"); + OCMStub([mockUser serverAuthCode]).andReturn(@"mockAuthCode"); + + [[self.mockSignIn expect] + signInWithConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *configuration) { + return [configuration.clientID + isEqualToString: + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:@[] + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin + handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"displayName"], @"mockDisplay"); + XCTAssertEqualObjects(result[@"email"], @"mock@example.com"); + XCTAssertEqualObjects(result[@"id"], @"mockID"); + XCTAssertEqualObjects(result[@"photoUrl"], @"https://example.com/profile.png"); + XCTAssertEqualObjects(result[@"serverAuthCode"], @"mockAuthCode"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + OCMVerifyAll(self.mockSignIn); +} + +- (void)testSignInWithInitializedScopes { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"scopes" : @[ @"initial1", @"initial2" ]}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUser userID]).andReturn(@"mockID"); + + [[self.mockSignIn expect] + signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:nil + additionalScopes:[OCMArg checkWithBlock:^BOOL(NSArray *scopes) { + return [[NSSet setWithArray:scopes] + isEqualToSet:[NSSet setWithObjects:@"initial1", @"initial2", nil]]; + }] + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" arguments:nil]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; [self.plugin handleMethodCall:methodCall - result:^(NSNumber *result){ + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"id"], @"mockID"); + [expectation fulfill]; }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; - id mockSignIn = self.mockSignIn; - OCMVerify([mockSignIn - setPresentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]]]); - OCMVerify([mockSignIn signIn]); + OCMVerifyAll(self.mockSignIn); +} + +- (void)testSignInAlreadyGranted { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUser userID]).andReturn(@"mockID"); + + [[self.mockSignIn stub] + signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:nil + additionalScopes:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; + + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeScopesAlreadyGranted + userInfo:nil]; + [[self.mockSignIn stub] addScopes:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"id"], @"mockID"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignInError { + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeCanceled + userInfo:nil]; + [[self.mockSignIn stub] + signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:nil + additionalScopes:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_canceled"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testSignInExecption { +- (void)testSignInException { FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" arguments:nil]; - OCMExpect([self.mockSignIn signIn]) + OCMExpect([self.mockSignIn signInWithConfiguration:OCMOCK_ANY + presentingViewController:OCMOCK_ANY + hint:OCMOCK_ANY + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]) .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); __block FlutterError *error; @@ -237,7 +410,7 @@ - (void)testGetTokens { OCMStub([mockAuthentication idToken]).andReturn(@"mockIdToken"); OCMStub([mockAuthentication accessToken]).andReturn(@"mockAccessToken"); [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:mockAuthentication, [NSNull null], nil]]; + doWithFreshTokens:[OCMArg invokeBlockWithArgs:mockAuthentication, [NSNull null], nil]]; OCMStub([mockUser authentication]).andReturn(mockAuthentication); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" @@ -262,7 +435,7 @@ - (void)testGetTokensNoAuthKeychainError { code:kGIDSignInErrorCodeHasNoAuthInKeychain userInfo:nil]; [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; OCMStub([mockUser authentication]).andReturn(mockAuthentication); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" @@ -287,7 +460,7 @@ - (void)testGetTokensCancelledError { code:kGIDSignInErrorCodeCanceled userInfo:nil]; [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; OCMStub([mockUser authentication]).andReturn(mockAuthentication); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" @@ -310,7 +483,7 @@ - (void)testGetTokensURLError { id mockAuthentication = OCMClassMock([GIDAuthentication class]); NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:nil]; [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; OCMStub([mockUser authentication]).andReturn(mockAuthentication); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" @@ -333,7 +506,7 @@ - (void)testGetTokensUnknownError { id mockAuthentication = OCMClassMock([GIDAuthentication class]); NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; [[mockAuthentication stub] - getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + doWithFreshTokens:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; OCMStub([mockUser authentication]).andReturn(mockAuthentication); FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" @@ -352,7 +525,12 @@ - (void)testGetTokensUnknownError { #pragma mark - Request scopes - (void)testRequestScopesResultErrorIfNotSignedIn { - OCMStub([self.mockSignIn currentUser]).andReturn(nil); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeNoCurrentUser + userInfo:nil]; + [[self.mockSignIn stub] addScopes:@[ @"mockScope1" ] + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" @@ -368,14 +546,16 @@ - (void)testRequestScopesResultErrorIfNotSignedIn { } - (void)testRequestScopesIfNoMissingScope { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeScopesAlreadyGranted + userInfo:nil]; + [[self.mockSignIn stub] addScopes:@[ @"mockScope1" ] + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; [self.plugin handleMethodCall:methodCall @@ -386,39 +566,50 @@ - (void)testRequestScopesIfNoMissingScope { [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testRequestScopesRequestsIfNotGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(@[]); - id mockSignIn = self.mockSignIn; - OCMStub([mockSignIn scopes]).andReturn(@[]); +- (void)testRequestScopesWithUnknownError { + NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; + [[self.mockSignIn stub] addScopes:@[ @"mockScope1" ] + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; [self.plugin handleMethodCall:methodCall - result:^(id r){ + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesException { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:nil]; + OCMExpect([self.mockSignIn addScopes:@[] presentingViewController:OCMOCK_ANY callback:OCMOCK_ANY]) + .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); - OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); - OCMVerify([mockSignIn signIn]); + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"request_scopes"); + XCTAssertEqualObjects(result.message, @"MockReason"); + XCTAssertEqualObjects(result.details, @"MockName"); + }]; } -- (void)testRequestScopesReturnsFalseIfNotGranted { - // Mock Google Signin internal calls +- (void)testRequestScopesReturnsFalseIfOnlySubsetGranted { GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(@[]); + NSArray *requestedScopes = @[ @"mockScope1", @"mockScope2" ]; + + // Only grant one of the two requested scopes. + OCMStub(mockUser.grantedScopes).andReturn(@[ @"mockScope1" ]); - OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { - [((NSObject *)self.plugin) signIn:self.mockSignIn - didSignInForUser:mockUser - withError:nil]; - }); + [[self.mockSignIn stub] addScopes:requestedScopes + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" @@ -433,20 +624,53 @@ - (void)testRequestScopesReturnsFalseIfNotGranted { [self waitForExpectationsWithTimeout:5.0 handler:nil]; } +- (void)testRequestsInitializedScopes { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"scopes" : @[ @"initial1", @"initial2" ]}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(id result) { + XCTAssertNil(result); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + // Include one of the initially requested scopes. + NSArray *addedScopes = @[ @"initial1", @"addScope1", @"addScope2" ]; + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : addedScopes}]; + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + + // All four scopes are requested. + [[self.mockSignIn verify] + addScopes:[OCMArg checkWithBlock:^BOOL(NSArray *scopes) { + return [[NSSet setWithArray:scopes] + isEqualToSet:[NSSet setWithObjects:@"initial1", @"initial2", + @"addScope1", @"addScope2", nil]]; + }] + presentingViewController:OCMOCK_ANY + callback:OCMOCK_ANY]; +} + - (void)testRequestScopesReturnsTrueIfGranted { - // Mock Google Signin internal calls GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - NSMutableArray *availableScopes = [NSMutableArray new]; - OCMStub(mockUser.grantedScopes).andReturn(availableScopes); - - OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { - [availableScopes addObject:@"mockScope1"]; - [((NSObject *)self.plugin) signIn:self.mockSignIn - didSignInForUser:mockUser - withError:nil]; - }); + NSArray *requestedScopes = @[ @"mockScope1", @"mockScope2" ]; + + // Grant both of the requested scopes. + OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); + + [[self.mockSignIn stub] addScopes:requestedScopes + presentingViewController:OCMOCK_ANY + callback:[OCMArg invokeBlockWithArgs:mockUser, [NSNull null], nil]]; FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"requestScopes" diff --git a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m index 5ad69e2ad052..6319923d767f 100644 --- a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin.m @@ -36,17 +36,18 @@ details:error.localizedDescription]; } -@interface FLTGoogleSignInPlugin () +@interface FLTGoogleSignInPlugin () + +@property(strong) GIDConfiguration *configuration; +@property(copy) NSSet *requestedScopes; @property(strong, readonly) GIDSignIn *signIn; // Redeclared as not a designated initializer. - (instancetype)init; + @end -@implementation FLTGoogleSignInPlugin { - FlutterResult _accountRequest; - NSArray *_additionalScopesRequest; -} +@implementation FLTGoogleSignInPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = @@ -65,11 +66,11 @@ - (instancetype)initWithSignIn:(GIDSignIn *)signIn { self = [super init]; if (self) { _signIn = signIn; - _signIn.delegate = self; // On the iOS simulator, we get "Broken pipe" errors after sign-in for some // unknown reason. We can avoid crashing the app by ignoring them. signal(SIGPIPE, SIG_IGN); + _requestedScopes = [[NSSet alloc] init]; } return self; } @@ -78,25 +79,14 @@ - (instancetype)initWithSignIn:(GIDSignIn *)signIn { - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([call.method isEqualToString:@"init"]) { - NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; - if (path) { - NSMutableDictionary *plist = - [[NSMutableDictionary alloc] initWithContentsOfFile:path]; - BOOL hasDynamicClientId = [call.arguments[@"clientId"] isKindOfClass:[NSString class]]; - - if (hasDynamicClientId) { - self.signIn.clientID = call.arguments[@"clientId"]; - } else { - self.signIn.clientID = plist[kClientIdKey]; - } - - self.signIn.serverClientID = plist[kServerClientIdKey]; - self.signIn.scopes = call.arguments[@"scopes"]; - if (call.arguments[@"hostedDomain"] == [NSNull null]) { - self.signIn.hostedDomain = nil; - } else { - self.signIn.hostedDomain = call.arguments[@"hostedDomain"]; + GIDConfiguration *configuration = + [self configurationClientIdArgument:call.arguments[@"clientId"] + hostedDomainArgument:call.arguments[@"hostedDomain"]]; + if (configuration != nil) { + if ([call.arguments[@"scopes"] isKindOfClass:[NSArray class]]) { + self.requestedScopes = [NSSet setWithArray:call.arguments[@"scopes"]]; } + self.configuration = configuration; result(nil); } else { result([FlutterError errorWithCode:@"missing-config" @@ -104,26 +94,30 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result details:nil]); } } else if ([call.method isEqualToString:@"signInSilently"]) { - if ([self setAccountRequest:result]) { - [self.signIn restorePreviousSignIn]; - } + [self.signIn restorePreviousSignInWithCallback:^(GIDGoogleUser *user, NSError *error) { + [self didSignInForUser:user result:result withError:error]; + }]; } else if ([call.method isEqualToString:@"isSignedIn"]) { result(@([self.signIn hasPreviousSignIn])); } else if ([call.method isEqualToString:@"signIn"]) { - self.signIn.presentingViewController = [self topViewController]; - - if ([self setAccountRequest:result]) { - @try { - [self.signIn signIn]; - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); - [e raise]; - } + @try { + GIDConfiguration *configuration = + self.configuration ?: [self configurationClientIdArgument:nil hostedDomainArgument:nil]; + [self.signIn signInWithConfiguration:configuration + presentingViewController:[self topViewController] + hint:nil + additionalScopes:self.requestedScopes.allObjects + callback:^(GIDGoogleUser *user, NSError *error) { + [self didSignInForUser:user result:result withError:error]; + }]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); + [e raise]; } } else if ([call.method isEqualToString:@"getTokens"]) { GIDGoogleUser *currentUser = self.signIn.currentUser; GIDAuthentication *auth = currentUser.authentication; - [auth getTokensWithHandler:^void(GIDAuthentication *authentication, NSError *error) { + [auth doWithFreshTokens:^void(GIDAuthentication *authentication, NSError *error) { result(error != nil ? getFlutterError(error) : @{ @"idToken" : authentication.idToken, @"accessToken" : authentication.accessToken, @@ -133,61 +127,49 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self.signIn signOut]; result(nil); } else if ([call.method isEqualToString:@"disconnect"]) { - if ([self setAccountRequest:result]) { - [self.signIn disconnect]; - } + [self.signIn disconnectWithCallback:^(NSError *error) { + [self respondWithAccount:@{} result:result error:nil]; + }]; } else if ([call.method isEqualToString:@"requestScopes"]) { - GIDGoogleUser *user = self.signIn.currentUser; - if (user == nil) { - result([FlutterError errorWithCode:@"sign_in_required" - message:@"No account to grant scopes." - details:nil]); - return; + id scopeArgument = call.arguments[@"scopes"]; + if ([scopeArgument isKindOfClass:[NSArray class]]) { + self.requestedScopes = [self.requestedScopes setByAddingObjectsFromArray:scopeArgument]; } - - NSArray *currentScopes = self.signIn.scopes; - NSArray *scopes = call.arguments[@"scopes"]; - NSArray *missingScopes = [scopes - filteredArrayUsingPredicate:[NSPredicate - predicateWithBlock:^BOOL(id scope, NSDictionary *bindings) { - return ![user.grantedScopes containsObject:scope]; - }]]; - - if (!missingScopes || !missingScopes.count) { - result(@(YES)); - return; - } - - if ([self setAccountRequest:result]) { - _additionalScopesRequest = missingScopes; - self.signIn.scopes = [currentScopes arrayByAddingObjectsFromArray:missingScopes]; - self.signIn.presentingViewController = [self topViewController]; - self.signIn.loginHint = user.profile.email; - @try { - [self.signIn signIn]; - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); - } + NSSet *requestedScopes = self.requestedScopes; + + @try { + [self.signIn addScopes:requestedScopes.allObjects + presentingViewController:[self topViewController] + callback:^(GIDGoogleUser *addedScopeUser, NSError *addedScopeError) { + if ([addedScopeError.domain isEqualToString:kGIDSignInErrorDomain] && + addedScopeError.code == kGIDSignInErrorCodeNoCurrentUser) { + result([FlutterError errorWithCode:@"sign_in_required" + message:@"No account to grant scopes." + details:nil]); + } else if ([addedScopeError.domain + isEqualToString:kGIDSignInErrorDomain] && + addedScopeError.code == + kGIDSignInErrorCodeScopesAlreadyGranted) { + // Scopes already granted, report success. + result(@YES); + } else if (addedScopeUser == nil) { + result(@NO); + } else { + NSSet *grantedScopes = + [NSSet setWithArray:addedScopeUser.grantedScopes]; + BOOL granted = [requestedScopes isSubsetOfSet:grantedScopes]; + result(@(granted)); + } + }]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); } } else { result(FlutterMethodNotImplemented); } } -- (BOOL)setAccountRequest:(FlutterResult)request { - if (_accountRequest != nil) { - request([FlutterError errorWithCode:@"concurrent-requests" - message:@"Concurrent requests to account signin" - details:nil]); - return NO; - } - _accountRequest = request; - return YES; -} - -- (BOOL)application:(UIApplication *)app - openURL:(NSURL *)url - options:(NSDictionary *)options { +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { return [self.signIn handleURL:url]; } @@ -203,57 +185,57 @@ - (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)vie [viewController dismissViewControllerAnimated:YES completion:nil]; } -#pragma mark - protocol +#pragma mark - private methods + +/// @return @c nil if GoogleService-Info.plist not found. +- (GIDConfiguration *)configurationClientIdArgument:(id)clientIDArg + hostedDomainArgument:(id)hostedDomainArg { + NSString *plistPath = [NSBundle.mainBundle pathForResource:@"GoogleService-Info" ofType:@"plist"]; + if (plistPath == nil) { + return nil; + } + + NSDictionary *plist = [[NSDictionary alloc] initWithContentsOfFile:plistPath]; + + BOOL hasDynamicClientId = [clientIDArg isKindOfClass:[NSString class]]; + NSString *clientID = hasDynamicClientId ? clientIDArg : plist[kClientIdKey]; + + NSString *hostedDomain = nil; + if (hostedDomainArg != [NSNull null]) { + hostedDomain = hostedDomainArg; + } + return [[GIDConfiguration alloc] initWithClientID:clientID + serverClientID:plist[kServerClientIdKey] + hostedDomain:hostedDomain + openIDRealm:nil]; +} -- (void)signIn:(GIDSignIn *)signIn - didSignInForUser:(GIDGoogleUser *)user - withError:(NSError *)error { +- (void)didSignInForUser:(GIDGoogleUser *)user + result:(FlutterResult)result + withError:(NSError *)error { if (error != nil) { // Forward all errors and let Dart side decide how to handle. - [self respondWithAccount:nil error:error]; + [self respondWithAccount:nil result:result error:error]; } else { - if (_additionalScopesRequest) { - bool granted = YES; - for (NSString *scope in _additionalScopesRequest) { - if (![user.grantedScopes containsObject:scope]) { - granted = NO; - break; - } - } - _accountRequest(@(granted)); - _accountRequest = nil; - _additionalScopesRequest = nil; - return; - } else { - NSURL *photoUrl; - if (user.profile.hasImage) { - // Placeholder that will be replaced by on the Dart side based on screen - // size - photoUrl = [user.profile imageURLWithDimension:1337]; - } - [self respondWithAccount:@{ - @"displayName" : user.profile.name ?: [NSNull null], - @"email" : user.profile.email ?: [NSNull null], - @"id" : user.userID ?: [NSNull null], - @"photoUrl" : [photoUrl absoluteString] ?: [NSNull null], - @"serverAuthCode" : user.serverAuthCode ?: [NSNull null] - } - error:nil]; + NSURL *photoUrl; + if (user.profile.hasImage) { + // Placeholder that will be replaced by on the Dart side based on screen + // size + photoUrl = [user.profile imageURLWithDimension:1337]; + } + [self respondWithAccount:@{ + @"displayName" : user.profile.name ?: [NSNull null], + @"email" : user.profile.email ?: [NSNull null], + @"id" : user.userID ?: [NSNull null], + @"photoUrl" : [photoUrl absoluteString] ?: [NSNull null], + @"serverAuthCode" : user.serverAuthCode ?: [NSNull null] } + result:result + error:nil]; } } -- (void)signIn:(GIDSignIn *)signIn - didDisconnectWithUser:(GIDGoogleUser *)user - withError:(NSError *)error { - [self respondWithAccount:@{} error:nil]; -} - -#pragma mark - private methods - -- (void)respondWithAccount:(NSDictionary *)account error:(NSError *)error { - FlutterResult result = _accountRequest; - _accountRequest = nil; +- (void)respondWithAccount:(id)account result:(FlutterResult)result error:(NSError *)error { result(error != nil ? getFlutterError(error) : account); } diff --git a/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec index f583f6cffbf0..18a213579a23 100644 --- a/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec +++ b/packages/google_sign_in/google_sign_in_ios/ios/google_sign_in_ios.podspec @@ -16,11 +16,8 @@ Enables Google Sign-In in Flutter apps. s.public_header_files = 'Classes/**/*.h' s.module_map = 'Classes/FLTGoogleSignInPlugin.modulemap' s.dependency 'Flutter' - s.dependency 'GoogleSignIn', '~> 5.0' + s.dependency 'GoogleSignIn', '~> 6.2' s.static_framework = true - s.platform = :ios, '9.0' - - # GoogleSignIn ~> 5.0 does not support arm64 simulators. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index b6f541b22ce1..5604ee6a5f18 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -1,8 +1,8 @@ name: google_sign_in_ios -description: Android implementation of the google_sign_in plugin. +description: iOS implementation of the google_sign_in plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.2.7 +version: 5.3.0 environment: sdk: ">=2.14.0 <3.0.0"