[google_sign_in] Add implementations of requestScopes. (#2599)

diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md
index 8a9743f..a17b384 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 6c9bedd..ebebfa0 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 @@
   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 @@
         delegate.isSignedIn(result);
         break;
 
+      case METHOD_REQUEST_SCOPES:
+        List<String> scopes = call.argument("scopes");
+        delegate.requestScopes(result, scopes);
+        break;
+
       default:
         result.notImplemented();
     }
@@ -153,6 +163,9 @@
 
     /** 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<String> scopes);
   }
 
   /**
@@ -167,6 +180,7 @@
   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 @@
 
     private final PluginRegistry.Registrar registrar;
     private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1);
+    private final GoogleSignInWrapper googleSignInWrapper;
 
     private GoogleSignInClient signInClient;
     private List<String> 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 @@
       result.success(value);
     }
 
+    @Override
+    public void requestScopes(Result result, List<String> 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<Scope> 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<GoogleSignInAccount> completedTask) {
       try {
         GoogleSignInAccount account = completedTask.getResult(ApiException.class);
@@ -527,9 +574,37 @@
             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.
+ *
+ * <p>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.
+ *
+ * <p>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 5b1c569..e6da1a0 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 @@
             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 0000000..bd8e37a
--- /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<String, List<String>> 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<String, List<String>> 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<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    ArgumentCaptor<ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>();
+    arguments.put("scopes", Collections.singletonList("requestedScope"));
+    MethodCall methodCall = new MethodCall("requestScopes", arguments);
+    Scope requestedScope = new Scope("requestedScope");
+
+    ArgumentCaptor<ActivityResultListener> 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 0000000..1f0955d
--- /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 0000000..64d65ca
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in/example/ios/GoogleSignInPluginTest/Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
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 0790c1b..9049fcd 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 @@
 
 @implementation FLTGoogleSignInPlugin {
   FlutterResult _accountRequest;
+  NSArray *_additionalScopesRequest;
 }
 
 + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
@@ -121,6 +122,40 @@
     // 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 @@
     // 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];
+    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];
     }
-    [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];
   }
 }
 
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 0000000..ca18614
--- /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<FlutterBinaryMessenger> *mockBinaryMessenger;
+@property(strong, nonatomic) NSObject<FlutterPluginRegistrar> *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<GIDSignInDelegate> *)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<GIDSignInDelegate> *)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 73533c6..0468c5a 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 @@
 
   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 0975337..7402c7a 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 @@
   /// authentication.
   Future<GoogleSignInAccount> disconnect() =>
       _addMethodCall(GoogleSignInPlatform.instance.disconnect);
+
+  /// Requests the user grants additional Oauth [scopes].
+  Future<bool> requestScopes(List<String> 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 5ef91f7..860369d 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 @@
 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 @@
         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 a85fb0f..898c27f 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 @@
       'signOut': null,
       'disconnect': null,
       'isSignedIn': true,
+      'requestScopes': true,
       'getTokens': <dynamic, dynamic>{
         'idToken': '123',
         'accessToken': '456',
@@ -379,6 +380,27 @@
         ],
       );
     });
+
+    test('requestScopes returns true once new scope is granted', () async {
+      await googleSignIn.signIn();
+      final result = await googleSignIn.requestScopes(['testScope']);
+
+      expect(result, isTrue);
+      expect(
+        log,
+        <Matcher>[
+          isMethodCall('init', arguments: <String, dynamic>{
+            'signInOption': 'SignInOption.standard',
+            'scopes': <String>[],
+            'hostedDomain': null,
+          }),
+          isMethodCall('signIn', arguments: null),
+          isMethodCall('requestScopes', arguments: <String, dynamic>{
+            '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 1b1492b..6f186fd 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 4004d47..bb43ba1 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 @@
 
     return auth2.getAuthInstance().disconnect();
   }
+
+  @override
+  Future<bool> requestScopes(List<String> 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 574cd95..9f2ce26 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 @@
 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 @@
         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 7a3a012..40bc8a4 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 @@
 
       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 9f2b7b9..15993bb 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 @@
       access_token: 'access_${data.idToken}',
     }
   },
+  getGrantedScopes: () => 'some scope',
+  grant: () => true,
 }
 ''';