diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 8a9743f10702..a17b384666f4 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,7 @@ +## 4.3.0 + +* Add support for method introduced in `google_sign_in_platform_interface` 1.1.0. + ## 4.2.0 * Migrate to AndroidX. diff --git a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 6c9bedde1038..ebebfa0294ab 100755 --- a/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -6,7 +6,9 @@ import android.accounts.Account; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import androidx.annotation.VisibleForTesting; import com.google.android.gms.auth.GoogleAuthUtil; import com.google.android.gms.auth.UserRecoverableAuthException; import com.google.android.gms.auth.api.signin.GoogleSignIn; @@ -27,6 +29,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,17 +49,19 @@ public class GoogleSignInPlugin implements MethodCallHandler { private static final String METHOD_DISCONNECT = "disconnect"; private static final String METHOD_IS_SIGNED_IN = "isSignedIn"; private static final String METHOD_CLEAR_AUTH_CACHE = "clearAuthCache"; + private static final String METHOD_REQUEST_SCOPES = "requestScopes"; private final IDelegate delegate; public static void registerWith(PluginRegistry.Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); - final GoogleSignInPlugin instance = new GoogleSignInPlugin(registrar); + final GoogleSignInPlugin instance = + new GoogleSignInPlugin(registrar, new GoogleSignInWrapper()); channel.setMethodCallHandler(instance); } - private GoogleSignInPlugin(PluginRegistry.Registrar registrar) { - delegate = new Delegate(registrar); + GoogleSignInPlugin(PluginRegistry.Registrar registrar, GoogleSignInWrapper googleSignInWrapper) { + delegate = new Delegate(registrar, googleSignInWrapper); } @Override @@ -100,6 +105,11 @@ public void onMethodCall(MethodCall call, Result result) { delegate.isSignedIn(result); break; + case METHOD_REQUEST_SCOPES: + List scopes = call.argument("scopes"); + delegate.requestScopes(result, scopes); + break; + default: result.notImplemented(); } @@ -153,6 +163,9 @@ public void init( /** Checks if there is a signed in user. */ public void isSignedIn(Result result); + + /** Prompts the user to grant an additional Oauth scopes. */ + public void requestScopes(final Result result, final List scopes); } /** @@ -167,6 +180,7 @@ public void init( public static final class Delegate implements IDelegate, PluginRegistry.ActivityResultListener { private static final int REQUEST_CODE_SIGNIN = 53293; private static final int REQUEST_CODE_RECOVER_AUTH = 53294; + @VisibleForTesting static final int REQUEST_CODE_REQUEST_SCOPE = 53295; private static final String ERROR_REASON_EXCEPTION = "exception"; private static final String ERROR_REASON_STATUS = "status"; @@ -183,13 +197,15 @@ public static final class Delegate implements IDelegate, PluginRegistry.Activity private final PluginRegistry.Registrar registrar; private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1); + private final GoogleSignInWrapper googleSignInWrapper; private GoogleSignInClient signInClient; private List requestedScopes; private PendingOperation pendingOperation; - public Delegate(PluginRegistry.Registrar registrar) { + public Delegate(PluginRegistry.Registrar registrar, GoogleSignInWrapper googleSignInWrapper) { this.registrar = registrar; + this.googleSignInWrapper = googleSignInWrapper; registrar.addActivityResultListener(this); } @@ -343,6 +359,37 @@ public void isSignedIn(final Result result) { result.success(value); } + @Override + public void requestScopes(Result result, List scopes) { + checkAndSetPendingOperation(METHOD_REQUEST_SCOPES, result); + + GoogleSignInAccount account = googleSignInWrapper.getLastSignedInAccount(registrar.context()); + if (account == null) { + result.error(ERROR_REASON_SIGN_IN_REQUIRED, "No account to grant scopes.", null); + return; + } + + List wrappedScopes = new ArrayList<>(); + + for (String scope : scopes) { + Scope wrappedScope = new Scope(scope); + if (!googleSignInWrapper.hasPermissions(account, wrappedScope)) { + wrappedScopes.add(wrappedScope); + } + } + + if (wrappedScopes.isEmpty()) { + result.success(true); + return; + } + + googleSignInWrapper.requestPermissions( + registrar.activity(), + REQUEST_CODE_REQUEST_SCOPE, + account, + wrappedScopes.toArray(new Scope[0])); + } + private void onSignInResult(Task completedTask) { try { GoogleSignInAccount account = completedTask.getResult(ApiException.class); @@ -527,9 +574,37 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { finishWithError(ERROR_REASON_SIGN_IN_FAILED, "Signin failed"); } return true; + case REQUEST_CODE_REQUEST_SCOPE: + finishWithSuccess(resultCode == Activity.RESULT_OK); + return true; default: return false; } } } } + +/** + * A wrapper object that calls static method in GoogleSignIn. + * + *

Because GoogleSignIn uses static method mostly, which is hard for unit testing. We use this + * wrapper class to use instance method which calls the corresponding GoogleSignIn static methods. + * + *

Warning! This class should stay true that each method calls a GoogleSignIn static method with + * the same name and same parameters. + */ +class GoogleSignInWrapper { + + GoogleSignInAccount getLastSignedInAccount(Context context) { + return GoogleSignIn.getLastSignedInAccount(context); + } + + boolean hasPermissions(GoogleSignInAccount account, Scope scope) { + return GoogleSignIn.hasPermissions(account, scope); + } + + void requestPermissions( + Activity activity, int requestCode, GoogleSignInAccount account, Scope[] scopes) { + GoogleSignIn.requestPermissions(activity, requestCode, account, scopes); + } +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle index 5b1c5699d48d..e6da1a0aebf5 100755 --- a/packages/google_sign_in/google_sign_in/example/android/app/build.gradle +++ b/packages/google_sign_in/google_sign_in/example/android/app/build.gradle @@ -47,8 +47,18 @@ android { signingConfig signingConfigs.debug } } + + testOptions { + unitTests.returnDefaultValues = true + } } flutter { source '../..' } + +dependencies { + implementation 'com.google.android.gms:play-services-auth:16.0.1' + testImplementation'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:2.17.0' +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java b/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java new file mode 100644 index 000000000000..bd8e37ae3a28 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java @@ -0,0 +1,136 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesignin; + +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.common.api.Scope; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; +import io.flutter.plugins.googlesignin.GoogleSignInPlugin.Delegate; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class GoogleSignInPluginTests { + + @Mock Context mockContext; + @Mock Activity mockActivity; + @Mock PluginRegistry.Registrar mockRegistrar; + @Mock BinaryMessenger mockMessenger; + @Spy MethodChannel.Result result; + @Mock GoogleSignInWrapper mockGoogleSignIn; + @Mock GoogleSignInAccount account; + private GoogleSignInPlugin plugin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockRegistrar.messenger()).thenReturn(mockMessenger); + when(mockRegistrar.context()).thenReturn(mockContext); + when(mockRegistrar.activity()).thenReturn(mockActivity); + plugin = new GoogleSignInPlugin(mockRegistrar, mockGoogleSignIn); + } + + @Test + public void requestScopes_ResultErrorIfAccountIsNull() { + MethodCall methodCall = new MethodCall("requestScopes", null); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + plugin.onMethodCall(methodCall, result); + verify(result).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test + public void requestScopes_ResultTrueIfAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); + + plugin.onMethodCall(methodCall, result); + verify(result).success(true); + } + + @Test + public void requestScopes_RequestsPermissionIfNotGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + + verify(mockGoogleSignIn) + .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); + } + + @Test + public void requestScopes_ReturnsFalseIfPermissionDenied() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_CANCELED, new Intent()); + + verify(result).success(false); + } + + @Test + public void requestScopes_ReturnsTrueIfPermissionGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result).success(true); + } +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000000..1f0955d450f0 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/packages/google_sign_in/google_sign_in/example/ios/GoogleSignInPluginTest/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/GoogleSignInPluginTest/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/ios/GoogleSignInPluginTest/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m index 0790c1b8cf65..9049fcd62a33 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m @@ -37,6 +37,7 @@ @interface FLTGoogleSignInPlugin () @implementation FLTGoogleSignInPlugin { FlutterResult _accountRequest; + NSArray *_additionalScopesRequest; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -121,6 +122,40 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result // There's nothing to be done here on iOS since the expired/invalid // tokens are refreshed automatically by getTokensWithHandler. result(nil); + } else if ([call.method isEqualToString:@"requestScopes"]) { + GIDGoogleUser *user = [GIDSignIn sharedInstance].currentUser; + if (user == nil) { + result([FlutterError errorWithCode:@"sign_in_required" + message:@"No account to grant scopes." + details:nil]); + return; + } + + NSArray *currentScopes = [GIDSignIn sharedInstance].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; + [GIDSignIn sharedInstance].scopes = + [currentScopes arrayByAddingObjectsFromArray:missingScopes]; + [GIDSignIn sharedInstance].presentingViewController = [self topViewController]; + [GIDSignIn sharedInstance].loginHint = user.profile.email; + @try { + [[GIDSignIn sharedInstance] signIn]; + } @catch (NSException *e) { + result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); + } + } } else { result(FlutterMethodNotImplemented); } @@ -162,19 +197,33 @@ - (void)signIn:(GIDSignIn *)signIn // Forward all errors and let Dart side decide how to handle. [self respondWithAccount:nil error:error]; } 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], + 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], + } + error:nil]; } - error:nil]; } } diff --git a/packages/google_sign_in/google_sign_in/ios/Tests/GoogleSignInPluginTest.m b/packages/google_sign_in/google_sign_in/ios/Tests/GoogleSignInPluginTest.m new file mode 100644 index 000000000000..ca1861431b86 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Tests/GoogleSignInPluginTest.m @@ -0,0 +1,154 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; + +@import XCTest; +@import google_sign_in; +@import GoogleSignIn; +@import OCMock; + +@interface FLTGoogleSignInPluginTest : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; +@property(strong, nonatomic) NSObject *mockPluginRegistrar; +@property(strong, nonatomic) FLTGoogleSignInPlugin *plugin; +@property(strong, nonatomic) GIDSignIn *mockSharedInstance; + +@end + +@implementation FLTGoogleSignInPluginTest + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + self.mockSharedInstance = [OCMockObject partialMockForObject:[GIDSignIn sharedInstance]]; + OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); + self.plugin = [[FLTGoogleSignInPlugin alloc] init]; + [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; +} + +- (void)tearDown { + [((OCMockObject *)self.mockSharedInstance) stopMocking]; + [super tearDown]; +} + +- (void)testRequestScopesResultErrorIfNotSignedIn { + OCMStub(self.mockSharedInstance.currentUser).andReturn(nil); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqualObjects([((FlutterError *)result) code], @"sign_in_required"); +} + +- (void)testRequestScopesIfNoMissingScope { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue([result boolValue]); +} + +- (void)testRequestScopesRequestsIfNotGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(@[]); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + [self.plugin handleMethodCall:methodCall + result:^(id r){ + }]; + + XCTAssertTrue([self.mockSharedInstance.scopes containsObject:@"mockScope1"]); + OCMVerify([self.mockSharedInstance signIn]); +} + +- (void)testRequestScopesReturnsFalseIfNotGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(@[]); + + OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { + [((NSObject *)self.plugin) signIn:self.mockSharedInstance + didSignInForUser:mockUser + withError:nil]; + }); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertFalse([result boolValue]); +} + +- (void)testRequestScopesReturnsTrueIfGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + NSMutableArray *availableScopes = [NSMutableArray new]; + OCMStub(mockUser.grantedScopes).andReturn(availableScopes); + + OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { + [availableScopes addObject:@"mockScope1"]; + [((NSObject *)self.plugin) signIn:self.mockSharedInstance + didSignInForUser:mockUser + withError:nil]; + }); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue([result boolValue]); +} + +@end diff --git a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec index 73533c6a0db6..0468c5a24807 100755 --- a/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec +++ b/packages/google_sign_in/google_sign_in/ios/google_sign_in.podspec @@ -20,4 +20,9 @@ Enables Google Sign-In in Flutter apps. s.platform = :ios, '8.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + + s.test_spec 'Tests' do |test_spec| + test_spec.source_files = 'Tests/**/*' + test_spec.dependency 'OCMock','3.5' + end end diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 09753377ac76..7402c7a69816 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -367,4 +367,10 @@ class GoogleSignIn { /// authentication. Future disconnect() => _addMethodCall(GoogleSignInPlatform.instance.disconnect); + + /// Requests the user grants additional Oauth [scopes]. + Future requestScopes(List scopes) async { + await _ensureInitialized(); + return GoogleSignInPlatform.instance.requestScopes(scopes); + } } diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 5ef91f743142..860369da8c40 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in -version: 4.2.0 +version: 4.3.0 flutter: plugin: @@ -16,7 +16,7 @@ flutter: default_package: google_sign_in_web dependencies: - google_sign_in_platform_interface: ^1.0.0 + google_sign_in_platform_interface: ^1.1.0 flutter: sdk: flutter meta: ^1.0.4 diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index a85fb0f27e42..898c27fd9f7e 100755 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -32,6 +32,7 @@ void main() { 'signOut': null, 'disconnect': null, 'isSignedIn': true, + 'requestScopes': true, 'getTokens': { 'idToken': '123', 'accessToken': '456', @@ -379,6 +380,27 @@ void main() { ], ); }); + + test('requestScopes returns true once new scope is granted', () async { + await googleSignIn.signIn(); + final result = await googleSignIn.requestScopes(['testScope']); + + expect(result, isTrue); + expect( + log, + [ + isMethodCall('init', arguments: { + 'signInOption': 'SignInOption.standard', + 'scopes': [], + 'hostedDomain': null, + }), + isMethodCall('signIn', arguments: null), + isMethodCall('requestScopes', arguments: { + 'scopes': ['testScope'], + }), + ], + ); + }); }); group('GoogleSignIn with fake backend', () { diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index 1b1492b35c56..6f186fddd704 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.9.0 + +* Add support for methods introduced in `google_sign_in_platform_interface` 1.1.0. + ## 0.8.4 * Remove all `fakeConstructor$` from the generated facade. JS interop classes do not support non-external constructors. diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index 4004d47d5551..bb43ba100c5d 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -176,4 +176,23 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { return auth2.getAuthInstance().disconnect(); } + + @override + Future requestScopes(List scopes) async { + await initialized; + + final currentUser = auth2.getAuthInstance()?.currentUser?.get(); + + if (currentUser == null) return false; + + final grantedScopes = currentUser.getGrantedScopes(); + final missingScopes = + scopes.where((scope) => !grantedScopes.contains(scope)); + + if (missingScopes.isEmpty) return true; + + return currentUser + .grant(auth2.SigninOptions(scope: missingScopes.join(" "))) ?? + false; + } } diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 574cd95f3cca..9f2ce2636b21 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_web description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web -version: 0.8.4 +version: 0.9.0 flutter: plugin: @@ -12,7 +12,7 @@ flutter: fileName: google_sign_in_web.dart dependencies: - google_sign_in_platform_interface: ^1.0.0 + google_sign_in_platform_interface: ^1.1.0 flutter: sdk: flutter flutter_web_plugins: diff --git a/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart index 7a3a01227169..40bc8a404d06 100644 --- a/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart +++ b/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart @@ -73,5 +73,11 @@ void main() { expect(actualToken, expectedTokenData); }); + + test('requestScopes', () async { + bool scopeGranted = await plugin.requestScopes(['newScope']); + + expect(scopeGranted, isTrue); + }); }); } diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart index 9f2b7b9bf6fa..15993bb56d8a 100644 --- a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart +++ b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/google_user.dart @@ -21,5 +21,7 @@ String googleUser(GoogleSignInUserData data) => ''' access_token: 'access_${data.idToken}', } }, + getGrantedScopes: () => 'some scope', + grant: () => true, } ''';