From 3b757529ea19d2f4aeebf9c88ef84401856d9f89 Mon Sep 17 00:00:00 2001 From: Gabriel Terwesten Date: Thu, 7 Jul 2022 17:58:05 +0200 Subject: [PATCH] [google_sign_in] Implement Dart-based configuration and `serverClientId` (#6034) --- .../google_sign_in_android/CHANGELOG.md | 11 +- .../googlesignin/GoogleSignInPlugin.java | 57 +++-- .../googlesignin/GoogleSignInWrapper.java | 6 + .../googlesignin/GoogleSignInTest.java | 72 ++++++ .../example/lib/main.dart | 10 +- .../example/pubspec.yaml | 2 +- .../google_sign_in_android/pubspec.yaml | 4 +- .../google_sign_in_ios/CHANGELOG.md | 7 +- .../ios/RunnerTests/GoogleSignInTests.m | 57 +++++ .../google_sign_in_ios/example/lib/main.dart | 13 +- .../google_sign_in_ios/example/pubspec.yaml | 2 +- .../ios/Classes/FLTGoogleSignInPlugin.m | 46 +++- .../ios/Classes/FLTGoogleSignInPlugin_Test.h | 11 +- .../google_sign_in_ios/pubspec.yaml | 4 +- .../google_sign_in_web/CHANGELOG.md | 5 + .../google_sign_in_web/README.md | 3 + .../auth2_legacy_init_test.dart | 223 ++++++++++++++++++ .../example/integration_test/auth2_test.dart | 60 +++-- .../gapi_load_legacy_init_test.dart | 51 ++++ .../integration_test/gapi_load_test.dart | 9 +- .../lib/google_sign_in_web.dart | 35 ++- .../google_sign_in_web/pubspec.yaml | 4 +- 22 files changed, 609 insertions(+), 83 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart create mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index 67fb08f66e17..a91f0c0351c4 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,12 @@ +## 6.0.0 + +* Deprecates `clientId` and adds support for `serverClientId` instead. + Historically `clientId` was interpreted as `serverClientId`, but only on Android. On + other platforms it was interpreted as the OAuth `clientId` of the app. For backwards-compatibility + `clientId` will still be used as a server client ID if `serverClientId` is not provided. +* **BREAKING CHANGES**: + * Adds `serverClientId` parameter to `IDelegate.init` (Java). + ## 5.2.8 * Suppresses `deprecation` warnings (for using Android V1 embedding). @@ -13,4 +22,4 @@ ## 5.2.5 -* Splits from `video_player` as a federated implementation. +* Splits from `google_sign_in` as a federated implementation. diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 9bee8fad38d3..21640233f210 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -8,6 +8,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.google.android.gms.auth.GoogleAuthUtil; @@ -138,7 +139,9 @@ public void onMethodCall(MethodCall call, Result result) { List requestedScopes = call.argument("scopes"); String hostedDomain = call.argument("hostedDomain"); String clientId = call.argument("clientId"); - delegate.init(result, signInOption, requestedScopes, hostedDomain, clientId); + String serverClientId = call.argument("serverClientId"); + delegate.init( + result, signInOption, requestedScopes, hostedDomain, clientId, serverClientId); break; case METHOD_SIGN_IN_SILENTLY: @@ -194,7 +197,8 @@ public void init( String signInOption, List requestedScopes, String hostedDomain, - String clientId); + String clientId, + String serverClientId); /** * Returns the account information for the user who is signed in to this app. If no user is @@ -321,7 +325,8 @@ public void init( String signInOption, List requestedScopes, String hostedDomain, - String clientId) { + String clientId, + String serverClientId) { try { GoogleSignInOptions.Builder optionsBuilder; @@ -338,20 +343,38 @@ public void init( throw new IllegalStateException("Unknown signInOption"); } - // Only requests a clientId if google-services.json was present and parsed - // by the google-services Gradle script. - // TODO(jackson): Perhaps we should provide a mechanism to override this - // behavior. - int clientIdIdentifier = - context - .getResources() - .getIdentifier("default_web_client_id", "string", context.getPackageName()); + // The clientId parameter is not supported on Android. + // Android apps are identified by their package name and the SHA-1 of their signing key. + // https://developers.google.com/android/guides/client-auth + // https://developers.google.com/identity/sign-in/android/start#configure-a-google-api-project if (!Strings.isNullOrEmpty(clientId)) { - optionsBuilder.requestIdToken(clientId); - optionsBuilder.requestServerAuthCode(clientId); - } else if (clientIdIdentifier != 0) { - optionsBuilder.requestIdToken(context.getString(clientIdIdentifier)); - optionsBuilder.requestServerAuthCode(context.getString(clientIdIdentifier)); + if (Strings.isNullOrEmpty(serverClientId)) { + Log.w( + "google_sing_in", + "clientId is not supported on Android and is interpreted as serverClientId." + + "Use serverClientId instead to suppress this warning."); + serverClientId = clientId; + } else { + Log.w("google_sing_in", "clientId is not supported on Android and is ignored."); + } + } + + if (Strings.isNullOrEmpty(serverClientId)) { + // Only requests a clientId if google-services.json was present and parsed + // by the google-services Gradle script. + // TODO(jackson): Perhaps we should provide a mechanism to override this + // behavior. + int webClientIdIdentifier = + context + .getResources() + .getIdentifier("default_web_client_id", "string", context.getPackageName()); + if (webClientIdIdentifier != 0) { + serverClientId = context.getString(webClientIdIdentifier); + } + } + if (!Strings.isNullOrEmpty(serverClientId)) { + optionsBuilder.requestIdToken(serverClientId); + optionsBuilder.requestServerAuthCode(serverClientId); } for (String scope : requestedScopes) { optionsBuilder.requestScopes(new Scope(scope)); @@ -361,7 +384,7 @@ public void init( } this.requestedScopes = requestedScopes; - signInClient = GoogleSignIn.getClient(context, optionsBuilder.build()); + signInClient = googleSignInWrapper.getClient(context, optionsBuilder.build()); result.success(null); } catch (Exception e) { result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java index 5af0b50136ce..c035329f8e96 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java @@ -8,6 +8,8 @@ import android.content.Context; import com.google.android.gms.auth.api.signin.GoogleSignIn; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.api.Scope; /** @@ -21,6 +23,10 @@ */ public class GoogleSignInWrapper { + GoogleSignInClient getClient(Context context, GoogleSignInOptions options) { + return GoogleSignIn.getClient(context, options); + } + GoogleSignInAccount getLastSignedInAccount(Context context) { return GoogleSignIn.getLastSignedInAccount(context); } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 3b6ad960f548..11f8cda2e9b1 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -4,6 +4,7 @@ package io.flutter.plugins.googlesignin; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -12,7 +13,10 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.content.res.Resources; import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; import com.google.android.gms.common.api.Scope; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; @@ -21,6 +25,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -30,12 +35,14 @@ public class GoogleSignInTest { @Mock Context mockContext; + @Mock Resources mockResources; @Mock Activity mockActivity; @Mock PluginRegistry.Registrar mockRegistrar; @Mock BinaryMessenger mockMessenger; @Spy MethodChannel.Result result; @Mock GoogleSignInWrapper mockGoogleSignIn; @Mock GoogleSignInAccount account; + @Mock GoogleSignInClient mockClient; private GoogleSignInPlugin plugin; @Before @@ -44,6 +51,7 @@ public void setUp() { when(mockRegistrar.messenger()).thenReturn(mockMessenger); when(mockRegistrar.context()).thenReturn(mockContext); when(mockRegistrar.activity()).thenReturn(mockActivity); + when(mockContext.getResources()).thenReturn(mockResources); plugin = new GoogleSignInPlugin(); plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); plugin.setUpRegistrar(mockRegistrar); @@ -195,4 +203,68 @@ public void signInThrowsWithoutActivity() { plugin.onMethodCall(new MethodCall("signIn", null), null); } + + @Test + public void init_LoadsServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_InterpretsClientIdAsServerClientId() { + final String clientId = "fakeClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, null); + initAndAssertServerClientId(methodCall, clientId); + } + + @Test + public void init_ForwardsServerClientId() { + final String serverClientId = "fakeServerClientId"; + MethodCall methodCall = buildInitMethodCall(null, serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_IgnoresClientIdIfServerClientIdIsProvided() { + final String clientId = "fakeClientId"; + final String serverClientId = "fakeServerClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + public void initAndAssertServerClientId(MethodCall methodCall, String serverClientId) { + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(GoogleSignInOptions.class); + when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) + .thenReturn(mockClient); + plugin.onMethodCall(methodCall, result); + verify(result).success(null); + Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId()); + } + + private static MethodCall buildInitMethodCall(String clientId, String serverClientId) { + return buildInitMethodCall( + "SignInOption.standard", Collections.emptyList(), clientId, serverClientId); + } + + private static MethodCall buildInitMethodCall( + String signInOption, List scopes, String clientId, String serverClientId) { + HashMap arguments = new HashMap<>(); + arguments.put("signInOption", signInOption); + arguments.put("scopes", scopes); + if (clientId != null) { + arguments.put("clientId", clientId); + } + if (serverClientId != null) { + arguments.put("serverClientId", serverClientId); + } + return new MethodCall("init", arguments); + } } diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart index 526cf8b69ccf..5818b6040fcc 100644 --- a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -41,14 +41,16 @@ class SignInDemoState extends State { } Future _ensureInitialized() { - return _initialization ??= GoogleSignInPlatform.instance.init( + return _initialization ??= + GoogleSignInPlatform.instance.initWithParams(const SignInInitParameters( scopes: [ 'email', 'https://www.googleapis.com/auth/contacts.readonly', ], - )..catchError((dynamic _) { - _initialization = null; - }); + )) + ..catchError((dynamic _) { + _initialization = null; + }); } void _setUser(GoogleSignInUserData? user) { diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml index 3aa8a80ee585..4d12bbad7987 100644 --- a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_sign_in_platform_interface: ^2.1.0 + google_sign_in_platform_interface: ^2.2.0 http: ^0.13.0 dev_dependencies: diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index c4c30cd3e545..4424ce3ff2d9 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.2.8 +version: 6.0.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - google_sign_in_platform_interface: ^2.1.0 + google_sign_in_platform_interface: ^2.2.0 dev_dependencies: flutter_driver: 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 c17f1415b724..5ed38de5cb74 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,8 @@ +## 5.4.0 + +* Adds support for `serverClientId` configuration option. +* Makes `Google-Services.info` file optional. + ## 5.3.1 * Suppresses warnings for pre-iOS-13 codepaths. @@ -17,4 +22,4 @@ ## 5.2.5 -* Splits from `video_player` as a federated implementation. +* Splits from `google_sign_in` as a federated implementation. 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 7efd490f30fe..5738b7f1c1fc 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 @@ -96,6 +96,25 @@ - (void)testDisconnectIgnoresError { #pragma mark - Init +- (void)testInitNoClientIdError { + // Init plugin without GoogleService-Info.plist. + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn + withGoogleServiceProperties:nil]; + + // init call does not provide a clientId. + FlutterMethodCall *initMethodCall = [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{}]; + + XCTestExpectation *initExpectation = + [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:initMethodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"missing-config"); + [initExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + - (void)testInitGoogleServiceInfoPlist { FlutterMethodCall *initMethodCall = [FlutterMethodCall methodCallWithMethodName:@"init" @@ -133,6 +152,10 @@ - (void)testInitGoogleServiceInfoPlist { } - (void)testInitDynamicClientIdNullDomain { + // Init plugin without GoogleService-Info.plist. + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn + withGoogleServiceProperties:nil]; + FlutterMethodCall *initMethodCall = [FlutterMethodCall methodCallWithMethodName:@"init" arguments:@{@"hostedDomain" : [NSNull null], @"clientId" : @"mockClientId"}]; @@ -163,6 +186,40 @@ - (void)testInitDynamicClientIdNullDomain { callback:OCMOCK_ANY]); } +- (void)testInitDynamicServerClientIdNullDomain { + FlutterMethodCall *initMethodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{ + @"hostedDomain" : [NSNull null], + @"serverClientId" : @"mockServerClientId" + }]; + + 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]; + + // 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.serverClientID isEqualToString:@"mockServerClientId"]; + }] + presentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]] + hint:nil + additionalScopes:OCMOCK_ANY + callback:OCMOCK_ANY]); +} + #pragma mark - Is signed in - (void)testIsNotSignedIn { diff --git a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart index 526cf8b69ccf..e23935ded1da 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart @@ -31,7 +31,8 @@ class SignInDemo extends StatefulWidget { class SignInDemoState extends State { GoogleSignInUserData? _currentUser; String _contactText = ''; - // Future that completes when `init` has completed on the sign in instance. + // Future that completes when `initWithParams` has completed on the sign in + // instance. Future? _initialization; @override @@ -41,14 +42,16 @@ class SignInDemoState extends State { } Future _ensureInitialized() { - return _initialization ??= GoogleSignInPlatform.instance.init( + return _initialization ??= + GoogleSignInPlatform.instance.initWithParams(const SignInInitParameters( scopes: [ 'email', 'https://www.googleapis.com/auth/contacts.readonly', ], - )..catchError((dynamic _) { - _initialization = null; - }); + )) + ..catchError((dynamic _) { + _initialization = null; + }); } void _setUser(GoogleSignInUserData? user) { diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml index ed51e3b63a58..d17c929a989f 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_sign_in_platform_interface: ^2.1.0 + google_sign_in_platform_interface: ^2.2.0 http: ^0.13.0 dev_dependencies: 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 608cdc2bec6d..7beb604aaee3 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 @@ -14,6 +14,15 @@ static NSString *const kServerClientIdKey = @"SERVER_CLIENT_ID"; +static NSDictionary *loadGoogleServiceInfo() { + NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" + ofType:@"plist"]; + if (plistPath) { + return [[NSDictionary alloc] initWithContentsOfFile:plistPath]; + } + return nil; +} + // These error codes must match with ones declared on Android and Dart sides. static NSString *const kErrorReasonSignInRequired = @"sign_in_required"; static NSString *const kErrorReasonSignInCanceled = @"sign_in_canceled"; @@ -52,6 +61,9 @@ @interface FLTGoogleSignInPlugin () // sign in, sign out, and requesting additional scopes. @property(strong, readonly) GIDSignIn *signIn; +// The contents of GoogleService-Info.plist, if it exists. +@property(strong, nullable) NSDictionary *googleServiceProperties; + // Redeclared as not a designated initializer. - (instancetype)init; @@ -73,9 +85,15 @@ - (instancetype)init { } - (instancetype)initWithSignIn:(GIDSignIn *)signIn { + return [self initWithSignIn:signIn withGoogleServiceProperties:loadGoogleServiceInfo()]; +} + +- (instancetype)initWithSignIn:(GIDSignIn *)signIn + withGoogleServiceProperties:(nullable NSDictionary *)googleServiceProperties { self = [super init]; if (self) { _signIn = signIn; + _googleServiceProperties = googleServiceProperties; // 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. @@ -91,6 +109,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result if ([call.method isEqualToString:@"init"]) { GIDConfiguration *configuration = [self configurationWithClientIdArgument:call.arguments[@"clientId"] + serverClientIdArgument:call.arguments[@"serverClientId"] hostedDomainArgument:call.arguments[@"hostedDomain"]]; if (configuration != nil) { if ([call.arguments[@"scopes"] isKindOfClass:[NSArray class]]) { @@ -100,7 +119,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result result(nil); } else { result([FlutterError errorWithCode:@"missing-config" - message:@"GoogleService-Info.plist file not found" + message:@"GoogleService-Info.plist file not found and clientId " + @"was not provided programmatically." details:nil]); } } else if ([call.method isEqualToString:@"signInSilently"]) { @@ -113,6 +133,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result @try { GIDConfiguration *configuration = self.configuration ?: [self configurationWithClientIdArgument:nil + serverClientIdArgument:nil hostedDomainArgument:nil]; [self.signIn signInWithConfiguration:configuration presentingViewController:[self topViewController] @@ -198,25 +219,32 @@ - (void)signIn:(GIDSignIn *)signIn dismissViewController:(UIViewController *)vie #pragma mark - private methods -/// @return @c nil if GoogleService-Info.plist not found. +/// @return @c nil if GoogleService-Info.plist not found and clientId is not provided. - (GIDConfiguration *)configurationWithClientIdArgument:(id)clientIDArg + serverClientIdArgument:(id)serverClientIDArg hostedDomainArgument:(id)hostedDomainArg { - NSString *plistPath = [NSBundle.mainBundle pathForResource:@"GoogleService-Info" ofType:@"plist"]; - if (plistPath == nil) { + NSString *clientID; + BOOL hasDynamicClientId = [clientIDArg isKindOfClass:[NSString class]]; + if (hasDynamicClientId) { + clientID = clientIDArg; + } else if (self.googleServiceProperties) { + clientID = self.googleServiceProperties[kClientIdKey]; + } else { + // We couldn't resolve a clientId, without which we cannot create a GIDConfiguration. return nil; } - NSDictionary *plist = [[NSDictionary alloc] initWithContentsOfFile:plistPath]; - - BOOL hasDynamicClientId = [clientIDArg isKindOfClass:[NSString class]]; - NSString *clientID = hasDynamicClientId ? clientIDArg : plist[kClientIdKey]; + BOOL hasDynamicServerClientId = [serverClientIDArg isKindOfClass:[NSString class]]; + NSString *serverClientID = hasDynamicServerClientId + ? serverClientIDArg + : self.googleServiceProperties[kServerClientIdKey]; NSString *hostedDomain = nil; if (hostedDomainArg != [NSNull null]) { hostedDomain = hostedDomainArg; } return [[GIDConfiguration alloc] initWithClientID:clientID - serverClientID:plist[kServerClientIdKey] + serverClientID:serverClientID hostedDomain:hostedDomain openIDRealm:nil]; } diff --git a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h index f8d5be6f8522..17ddb7f616bc 100644 --- a/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h +++ b/packages/google_sign_in/google_sign_in_ios/ios/Classes/FLTGoogleSignInPlugin_Test.h @@ -6,12 +6,21 @@ #import +NS_ASSUME_NONNULL_BEGIN + @class GIDSignIn; /// Methods exposed for unit testing. @interface FLTGoogleSignInPlugin () /// Inject @c GIDSignIn for testing. -- (instancetype)initWithSignIn:(GIDSignIn *)signIn NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithSignIn:(GIDSignIn *)signIn; + +/// Inject @c GIDSignIn and @c googleServiceProperties for testing. +- (instancetype)initWithSignIn:(GIDSignIn *)signIn + withGoogleServiceProperties:(nullable NSDictionary *)googleServiceProperties + NS_DESIGNATED_INITIALIZER; @end + +NS_ASSUME_NONNULL_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 13f045b6006c..65c8928c1402 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_ios 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.3.1 +version: 5.4.0 environment: sdk: ">=2.14.0 <3.0.0" @@ -19,7 +19,7 @@ flutter: dependencies: flutter: sdk: flutter - google_sign_in_platform_interface: ^2.1.0 + google_sign_in_platform_interface: ^2.2.0 dev_dependencies: flutter_driver: 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 672b1b2ca857..12e6d9630f9c 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,8 @@ +## 0.10.2 + +* Migrates to new platform-interface `initWithParams` method. +* Throws when unsupported `serverClientId` option is provided. + ## 0.10.1+3 * Updates references to the obsolete master branch. diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 6f6dcf2dbf15..7c02379808da 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -63,8 +63,11 @@ GoogleSignIn _googleSignIn = GoogleSignIn( ], ); ``` + [Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). +Note that the `serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web. + You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. ```dart diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart new file mode 100644 index 000000000000..12f8f2f3f167 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart @@ -0,0 +1,223 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is a copy of `auth2_test.dart`, before it was migrated to the +// new `initWithParams` method, and is kept to ensure test coverage of the +// deprecated `init` method, until it is removed. + +import 'dart:html' as html; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:js/js_util.dart' as js_util; + +import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; +import 'src/test_utils.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + final GoogleSignInTokenData expectedTokenData = + GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); + + final GoogleSignInUserData expectedUserData = GoogleSignInUserData( + displayName: 'Foo Bar', + email: 'foo@example.com', + id: '123', + photoUrl: 'http://example.com/img.jpg', + idToken: expectedTokenData.idToken, + ); + + late GoogleSignInPlugin plugin; + + group('plugin.initialize() throws a catchable exception', () { + setUp(() { + // The pre-configured use case for the instances of the plugin in this test + gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); + plugin = GoogleSignInPlugin(); + }); + + testWidgets('initialize throws PlatformException', + (WidgetTester tester) async { + await expectLater( + plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ), + throwsA(isA())); + }); + + testWidgets('initialize forwards error code from JS', + (WidgetTester tester) async { + try { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + fail('plugin.initialize should have thrown an exception!'); + } catch (e) { + final String code = js_util.getProperty(e, 'code'); + expect(code, 'idpiframe_initialization_failed'); + } + }); + }); + + group('other methods also throw catchable exceptions on initialize fail', () { + // This function ensures that initialize gets called, but for some reason, + // we ignored that it has thrown stuff... + Future _discardInit() async { + try { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + } catch (e) { + // Noop so we can call other stuff + } + } + + setUp(() { + gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); + plugin = GoogleSignInPlugin(); + }); + + testWidgets('signInSilently throws', (WidgetTester tester) async { + await _discardInit(); + await expectLater( + plugin.signInSilently(), throwsA(isA())); + }); + + testWidgets('signIn throws', (WidgetTester tester) async { + await _discardInit(); + await expectLater(plugin.signIn(), throwsA(isA())); + }); + + testWidgets('getTokens throws', (WidgetTester tester) async { + await _discardInit(); + await expectLater(plugin.getTokens(email: 'test@example.com'), + throwsA(isA())); + }); + testWidgets('requestScopes', (WidgetTester tester) async { + await _discardInit(); + await expectLater(plugin.requestScopes(['newScope']), + throwsA(isA())); + }); + }); + + group('auth2 Init Successful', () { + setUp(() { + // The pre-configured use case for the instances of the plugin in this test + gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); + plugin = GoogleSignInPlugin(); + }); + + testWidgets('Init requires clientId', (WidgetTester tester) async { + expect(plugin.init(hostedDomain: ''), throwsAssertionError); + }); + + testWidgets("Init doesn't accept spaces in scopes", + (WidgetTester tester) async { + expect( + plugin.init( + hostedDomain: '', + clientId: '', + scopes: ['scope with spaces'], + ), + throwsAssertionError); + }); + + // See: https://github.com/flutter/flutter/issues/88084 + testWidgets('Init passes plugin_name parameter with the expected value', + (WidgetTester tester) async { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + + final Object? initParameters = + js_util.getProperty(html.window, 'gapi2.init.parameters'); + expect(initParameters, isNotNull); + + final Object? pluginNameParameter = + js_util.getProperty(initParameters!, 'plugin_name'); + expect(pluginNameParameter, isA()); + expect(pluginNameParameter, 'dart-google_sign_in_web'); + }); + + group('Successful .initialize, then', () { + setUp(() async { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + await plugin.initialized; + }); + + testWidgets('signInSilently', (WidgetTester tester) async { + final GoogleSignInUserData actualUser = + (await plugin.signInSilently())!; + + expect(actualUser, expectedUserData); + }); + + testWidgets('signIn', (WidgetTester tester) async { + final GoogleSignInUserData actualUser = (await plugin.signIn())!; + + expect(actualUser, expectedUserData); + }); + + testWidgets('getTokens', (WidgetTester tester) async { + final GoogleSignInTokenData actualToken = + await plugin.getTokens(email: expectedUserData.email); + + expect(actualToken, expectedTokenData); + }); + + testWidgets('requestScopes', (WidgetTester tester) async { + final bool scopeGranted = + await plugin.requestScopes(['newScope']); + + expect(scopeGranted, isTrue); + }); + }); + }); + + group('auth2 Init successful, but exception on signIn() method', () { + setUp(() async { + // The pre-configured use case for the instances of the plugin in this test + gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); + plugin = GoogleSignInPlugin(); + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + await plugin.initialized; + }); + + testWidgets('User aborts sign in flow, throws PlatformException', + (WidgetTester tester) async { + await expectLater(plugin.signIn(), throwsA(isA())); + }); + + testWidgets('User aborts sign in flow, error code is forwarded from JS', + (WidgetTester tester) async { + try { + await plugin.signIn(); + fail('plugin.signIn() should have thrown an exception!'); + } catch (e) { + final String code = js_util.getProperty(e, 'code'); + expect(code, 'popup_closed_by_user'); + } + }); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart index d8c7655a11c4..81d9f1489a23 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart @@ -30,32 +30,31 @@ void main() { late GoogleSignInPlugin plugin; - group('plugin.init() throws a catchable exception', () { + group('plugin.initWithParams() throws a catchable exception', () { setUp(() { // The pre-configured use case for the instances of the plugin in this test gapiUrl = toBase64Url(gapi_mocks.auth2InitError()); plugin = GoogleSignInPlugin(); }); - testWidgets('init throws PlatformException', (WidgetTester tester) async { + testWidgets('throws PlatformException', (WidgetTester tester) async { await expectLater( - plugin.init( + plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ), + )), throwsA(isA())); }); - testWidgets('init forwards error code from JS', - (WidgetTester tester) async { + testWidgets('forwards error code from JS', (WidgetTester tester) async { try { - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); - fail('plugin.init should have thrown an exception!'); + )); + fail('plugin.initWithParams should have thrown an exception!'); } catch (e) { final String code = js_util.getProperty(e, 'code'); expect(code, 'idpiframe_initialization_failed'); @@ -63,16 +62,17 @@ void main() { }); }); - group('other methods also throw catchable exceptions on init fail', () { - // This function ensures that init gets called, but for some reason, we - // ignored that it has thrown stuff... + group('other methods also throw catchable exceptions on initWithParams fail', + () { + // This function ensures that initWithParams gets called, but for some + // reason, we ignored that it has thrown stuff... Future _discardInit() async { try { - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); } catch (e) { // Noop so we can call other stuff } @@ -114,28 +114,40 @@ void main() { }); testWidgets('Init requires clientId', (WidgetTester tester) async { - expect(plugin.init(hostedDomain: ''), throwsAssertionError); + expect( + plugin.initWithParams(const SignInInitParameters(hostedDomain: '')), + throwsAssertionError); + }); + + testWidgets("Init doesn't accept serverClientId", + (WidgetTester tester) async { + expect( + plugin.initWithParams(const SignInInitParameters( + clientId: '', + serverClientId: '', + )), + throwsAssertionError); }); testWidgets("Init doesn't accept spaces in scopes", (WidgetTester tester) async { expect( - plugin.init( + plugin.initWithParams(const SignInInitParameters( hostedDomain: '', clientId: '', scopes: ['scope with spaces'], - ), + )), throwsAssertionError); }); // See: https://github.com/flutter/flutter/issues/88084 testWidgets('Init passes plugin_name parameter with the expected value', (WidgetTester tester) async { - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); final Object? initParameters = js_util.getProperty(html.window, 'gapi2.init.parameters'); @@ -147,13 +159,13 @@ void main() { expect(pluginNameParameter, 'dart-google_sign_in_web'); }); - group('Successful .init, then', () { + group('Successful .initWithParams, then', () { setUp(() async { - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); await plugin.initialized; }); @@ -191,11 +203,11 @@ void main() { // The pre-configured use case for the instances of the plugin in this test gapiUrl = toBase64Url(gapi_mocks.auth2SignInError()); plugin = GoogleSignInPlugin(); - await plugin.init( + await plugin.initWithParams(const SignInInitParameters( hostedDomain: 'foo', scopes: ['some', 'scope'], clientId: '1234', - ); + )); await plugin.initialized; }); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart new file mode 100644 index 000000000000..7bfef53f7a23 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is a copy of `gapi_load_test.dart`, before it was migrated to the +// new `initWithParams` method, and is kept to ensure test coverage of the +// deprecated `init` method, until it is removed. + +import 'dart:html' as html; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:google_sign_in_web/google_sign_in_web.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; +import 'src/test_utils.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess( + GoogleSignInUserData(email: 'test@test.com', id: '1234'))); + + testWidgets('Plugin is initialized after GAPI fully loads and init is called', + (WidgetTester tester) async { + expect( + html.querySelector('script[src^="data:"]'), + isNull, + reason: 'Mock script not present before instantiating the plugin', + ); + final GoogleSignInPlugin plugin = GoogleSignInPlugin(); + expect( + html.querySelector('script[src^="data:"]'), + isNotNull, + reason: 'Mock script should be injected', + ); + expect(() { + plugin.initialized; + }, throwsStateError, + reason: + 'The plugin should throw if checking for `initialized` before calling .init'); + await plugin.init(hostedDomain: '', clientId: ''); + await plugin.initialized; + expect( + plugin.initialized, + completes, + reason: 'The plugin should complete the future once initialized.', + ); + }); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart index 5da42283367f..fc753e20d92c 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart @@ -34,9 +34,12 @@ void main() { expect(() { plugin.initialized; }, throwsStateError, - reason: - 'The plugin should throw if checking for `initialized` before calling .init'); - await plugin.init(hostedDomain: '', clientId: ''); + reason: 'The plugin should throw if checking for `initialized` before ' + 'calling .initWithParams'); + await plugin.initWithParams(const SignInInitParameters( + hostedDomain: '', + clientId: '', + )); await plugin.initialized; expect( plugin.initialized, 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 ae6180d34acb..c305cae2a33d 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 @@ -41,13 +41,15 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { late Future _isAuthInitialized; bool _isInitCalled = false; - // This method throws if init hasn't been called at some point in the past. - // It is used by the [initialized] getter to ensure that users can't await - // on a Future that will never resolve. + // This method throws if init or initWithParams hasn't been called at some + // point in the past. It is used by the [initialized] getter to ensure that + // users can't await on a Future that will never resolve. void _assertIsInitCalled() { if (!_isInitCalled) { throw StateError( - 'GoogleSignInPlugin::init() must be called before any other method in this plugin.'); + 'GoogleSignInPlugin::init() or GoogleSignInPlugin::initWithParams() ' + 'must be called before any other method in this plugin.', + ); } } @@ -71,16 +73,29 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { SignInOption signInOption = SignInOption.standard, String? hostedDomain, String? clientId, - }) async { - final String? appClientId = clientId ?? _autoDetectedClientId; + }) { + return initWithParams(SignInInitParameters( + scopes: scopes, + signInOption: signInOption, + hostedDomain: hostedDomain, + clientId: clientId, + )); + } + + @override + Future initWithParams(SignInInitParameters params) async { + final String? appClientId = params.clientId ?? _autoDetectedClientId; assert( appClientId != null, 'ClientID not set. Either set it on a ' ' tag,' - ' or pass clientId when calling init()'); + ' or pass clientId when initializing GoogleSignIn'); + + assert(params.serverClientId == null, + 'serverClientId is not supported on Web.'); assert( - !scopes.any((String scope) => scope.contains(' ')), + !params.scopes.any((String scope) => scope.contains(' ')), "OAuth 2.0 Scopes for Google APIs can't contain spaces. " 'Check https://developers.google.com/identity/protocols/googlescopes ' 'for a list of valid OAuth 2.0 scopes.'); @@ -88,9 +103,9 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { await _isGapiInitialized; final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( - hosted_domain: hostedDomain, + hosted_domain: params.hostedDomain, // The js lib wants a space-separated list of values - scope: scopes.join(' '), + scope: params.scopes.join(' '), client_id: appClientId!, plugin_name: 'dart-google_sign_in_web', )); 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 907cc90c81eb..1dedd6de6666 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/plugins/tree/main/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.10.1+3 +version: 0.10.2 environment: sdk: ">=2.12.0 <3.0.0" @@ -22,7 +22,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - google_sign_in_platform_interface: ^2.0.0 + google_sign_in_platform_interface: ^2.2.0 js: ^0.6.3 dev_dependencies: