From 3965fde83b12221e792f9c4ceab71671f4339da3 Mon Sep 17 00:00:00 2001 From: Srivats Venkataraman <42980667+srivats22@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:17:07 -0500 Subject: [PATCH 1/4] This PR Fixes: https://github.com/flutter/flutter/issues/167410, where _initCalled was being performed twice on the web Based on the discussion comments I have removed the calles to _initCalled in the google_sign_in_web package **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. --- .../google_sign_in_web_test.dart | 59 +++++----- .../lib/google_sign_in_web.dart | 110 ++++++++---------- 2 files changed, 79 insertions(+), 90 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart index 874bc8a22324..e102d6521175 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -65,8 +65,36 @@ void main() { await plugin.init( const InitParameters(clientId: 'some-non-null-client-id'), ); + }); + + testWidgets('throws if init is called twice', (_) async { + await plugin.init( + const InitParameters(clientId: 'some-non-null-client-id'), + ); + + // Calling init() a second time should throw state error + expect( + () => plugin.init( + const InitParameters(clientId: 'some-non-null-client-id'), + ), + throwsStateError, + ); + }); + + testWidgets('throws if init is called twice synchronously', (_) async { + final Future firstInit = plugin.init( + const InitParameters(clientId: 'some-non-null-client-id'), + ); + + // Calling init() a second time synchronously should throw state error + expect( + () => plugin.init( + const InitParameters(clientId: 'some-non-null-client-id'), + ), + throwsStateError, + ); - expect(plugin.initialized, completes); + await firstInit; }); testWidgets('asserts clientId is not null', (_) async { @@ -85,35 +113,6 @@ void main() { ); }, throwsAssertionError); }); - - testWidgets('must be called for most of the API to work', (_) async { - expect(() async { - await plugin.attemptLightweightAuthentication( - const AttemptLightweightAuthenticationParameters(), - ); - }, throwsStateError); - - expect(() async { - await plugin.clientAuthorizationTokensForScopes( - const ClientAuthorizationTokensForScopesParameters( - request: AuthorizationRequestDetails( - scopes: [], - userId: null, - email: null, - promptIfUnauthorized: false, - ), - ), - ); - }, throwsStateError); - - expect(() async { - await plugin.signOut(const SignOutParams()); - }, throwsStateError); - - expect(() async { - await plugin.disconnect(const DisconnectParams()); - }, throwsStateError); - }); }); group('support queries', () { 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 ab0b9b5ea5c0..8358f5d1ff47 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 @@ -49,7 +49,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @visibleForTesting GisSdkClient? debugOverrideGisSdkClient, @visibleForTesting StreamController? debugAuthenticationController, - }) : _gisSdkClient = debugOverrideGisSdkClient, + }) : _debugOverrideGisSdkClient = debugOverrideGisSdkClient, _authenticationController = debugAuthenticationController ?? StreamController.broadcast() { @@ -68,51 +68,31 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { // A future that completes when the JS loader is done. late Future _jsSdkLoadedFuture; - // A future that completes when the `init` call is done. - Completer? _initCalled; + + /// A completer used to track whether [init] has finished. + final Completer _initCalled = Completer(); + + /// A boolean flag to track if [init] has been called. + /// + /// This is used to prevent race conditions when [init] is called multiple + /// times without awaiting. + bool _isInitCalled = false; // A StreamController to communicate status changes from the GisSdkClient. final StreamController _authenticationController; // The instance of [GisSdkClient] backing the plugin. - GisSdkClient? _gisSdkClient; + // Using late final ensures it can only be set once and throws if accessed before initialization. + late final GisSdkClient _gisSdkClient; - // A convenience getter to avoid using ! when accessing _gisSdkClient, and - // providing a slightly better error message when it is Null. - GisSdkClient get _gisClient { - assert( - _gisSdkClient != null, - 'GIS Client not initialized. ' - 'GoogleSignInPlugin::init() or GoogleSignInPlugin::initWithParams() ' - 'must be called before any other method in this plugin.', - ); - return _gisSdkClient!; - } - - // 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 (_initCalled == null) { - throw StateError( - 'GoogleSignInPlugin::init() or GoogleSignInPlugin::initWithParams() ' - 'must be called before any other method in this plugin.', - ); - } - } + // An optional override for the GisSdkClient, used for testing. + final GisSdkClient? _debugOverrideGisSdkClient; /// A future that resolves when the plugin is fully initialized. /// /// This ensures that the SDK has been loaded, and that the `init` method /// has finished running. - @visibleForTesting - Future get initialized { - _assertIsInitCalled(); - return Future.wait(>[ - _jsSdkLoadedFuture, - _initCalled!.future, - ]); - } + Future get _initialized => _initCalled.future; /// Stores the client ID if it was set in a meta-tag of the page. @visibleForTesting @@ -125,6 +105,14 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @override Future init(InitParameters params) async { + // Throw if init() is called more than once + if (_isInitCalled) { + throw StateError( + 'init() has already been called. Calling init() more than once results in undefined behavior.', + ); + } + _isInitCalled = true; + final String? appClientId = params.clientId ?? autoDetectedClientId; assert( appClientId != null, @@ -138,27 +126,27 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { 'serverClientId is not supported on Web.', ); - _initCalled = Completer(); - await _jsSdkLoadedFuture; - _gisSdkClient ??= GisSdkClient( - clientId: appClientId!, - nonce: params.nonce, - hostedDomain: params.hostedDomain, - authenticationController: _authenticationController, - loggingEnabled: kDebugMode, - ); - - _initCalled!.complete(); // Signal that `init` is fully done. + _gisSdkClient = + _debugOverrideGisSdkClient ?? + GisSdkClient( + clientId: appClientId!, + nonce: params.nonce, + hostedDomain: params.hostedDomain, + authenticationController: _authenticationController, + loggingEnabled: kDebugMode, + ); + + _initCalled.complete(); } @override Future? attemptLightweightAuthentication( AttemptLightweightAuthenticationParameters params, ) { - initialized.then((void value) { - _gisClient.requestOneTap(); + _initialized.then((void value) { + _gisSdkClient.requestOneTap(); }); // One tap does not necessarily return immediately, and may never return, // so clients should not await it. Return null to signal that. @@ -183,26 +171,26 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @override Future signOut(SignOutParams params) async { - await initialized; + await _initialized; - await _gisClient.signOut(); + await _gisSdkClient.signOut(); } @override Future disconnect(DisconnectParams params) async { - await initialized; + await _initialized; - await _gisClient.disconnect(); + await _gisSdkClient.disconnect(); } @override Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params, ) async { - await initialized; + await _initialized; _validateScopes(params.request.scopes); - final String? token = await _gisClient.requestScopes( + final String? token = await _gisSdkClient.requestScopes( params.request.scopes, promptIfUnauthorized: params.request.promptIfUnauthorized, userHint: params.request.userId, @@ -216,7 +204,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future serverAuthorizationTokensForScopes( ServerAuthorizationTokensForScopesParameters params, ) async { - await initialized; + await _initialized; _validateScopes(params.request.scopes); // There is no way to know whether the flow will prompt in advance, so @@ -225,7 +213,9 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { return null; } - final String? code = await _gisClient.requestServerAuthCode(params.request); + final String? code = await _gisSdkClient.requestServerAuthCode( + params.request, + ); return code == null ? null : ServerAuthorizationTokenData(serverAuthCode: code); @@ -247,8 +237,8 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future clearAuthorizationToken( ClearAuthorizationTokenParams params, ) async { - await initialized; - return _gisClient.clearAuthorizationToken(params.accessToken); + await _initialized; + return _gisSdkClient.clearAuthorizationToken(params.accessToken); } @override @@ -278,13 +268,13 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { configuration ?? GSIButtonConfiguration(); return FutureBuilder( key: Key(config.hashCode.toString()), - future: initialized, + future: _initialized, builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { return FlexHtmlElementView( viewType: 'gsi_login_button', onElementCreated: (Object element) { - _gisClient.renderButton(element, config); + _gisSdkClient.renderButton(element, config); }, ); } From e05b627fd86aefb0ce7e2b292811a45a4465b48c Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Tue, 3 Mar 2026 17:53:39 +0530 Subject: [PATCH 2/4] Fix FutureBuilder to use connectionState.done for renderButton --- .../google_sign_in_web/lib/google_sign_in_web.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8358f5d1ff47..e31c10a8d283 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 @@ -270,7 +270,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { key: Key(config.hashCode.toString()), future: _initialized, builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { + if (snapshot.connectionState == ConnectionState.done) { return FlexHtmlElementView( viewType: 'gsi_login_button', onElementCreated: (Object element) { From 76c6c368008536d9bb45ecf85b65b0f8599b2e34 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Tue, 3 Mar 2026 18:02:08 +0530 Subject: [PATCH 3/4] [google_sign_in_web] Add integration test for renderButton --- .../integration_test/web_only_test.dart | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart index dd86cd8cbe5c..5e0260e23d5e 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart @@ -2,11 +2,12 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.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' show GoogleSignInPlugin; +import 'package:google_sign_in_web/src/flexible_size_html_element_view.dart'; import 'package:google_sign_in_web/src/gis_client.dart'; import 'package:google_sign_in_web/web_only.dart' as web; import 'package:integration_test/integration_test.dart'; @@ -60,6 +61,22 @@ void main() { expect(button, isNotNull); }); + + testWidgets('renderButton shows loading then renders button', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MaterialApp(home: Scaffold(body: web.renderButton())), + ); + + expect(find.text('Getting ready'), findsOneWidget); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + expect(find.text('Getting ready'), findsNothing); + + expect(find.byType(FlexHtmlElementView), findsOneWidget); + }); }); } From beabfa0dc07edb561d4e11cc02ddc4375612cc09 Mon Sep 17 00:00:00 2001 From: puneetkukreja98 Date: Tue, 3 Mar 2026 18:12:32 +0530 Subject: [PATCH 4/4] [google_sign_in web] Bump version to 1.1.3 and update CHANGELOG --- packages/google_sign_in/google_sign_in_web/CHANGELOG.md | 5 +++++ packages/google_sign_in/google_sign_in_web/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) 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 fdbfe4ac60ad..d7dc4334d508 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 @@ +## 1.1.3 + +* Restored previously reverted changes to prevent `_initCalled` from being invoked twice on web. +* Fixed `renderButton` being stuck on "Getting ready" on web by correcting the `FutureBuilder` state check to use `ConnectionState.done`. + ## 1.1.2 * Reverts "Throws a more actionable error when init is called more than once." 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 30ff5db731af..c824fbdc9fa5 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/packages/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: 1.1.2 +version: 1.1.3 environment: sdk: ^3.9.0