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 685d26d56c1..683a4d601eb 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.1 + +* Throws a more actionable error when init is called more than once +* Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. + ## NEXT * Updates minimum supported SDK version to Flutter 3.32/Dart 3.8. 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 874bc8a2232..e102d652117 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 ab0b9b5ea5c..7e16c260aff 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,10 +49,14 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @visibleForTesting GisSdkClient? debugOverrideGisSdkClient, @visibleForTesting StreamController? debugAuthenticationController, - }) : _gisSdkClient = debugOverrideGisSdkClient, - _authenticationController = + }) : _authenticationController = debugAuthenticationController ?? StreamController.broadcast() { + // Only set _gisSdkClient if debugOverrideGisSdkClient is provided + if (debugOverrideGisSdkClient != null) { + _gisSdkClient = debugOverrideGisSdkClient; + } + autoDetectedClientId = web.document .querySelector(clientIdMetaSelector) ?.getAttribute(clientIdAttributeName); @@ -68,51 +72,28 @@ 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; - - // 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.', - ); - } - } + // Using late final ensures it can only be set once and throws if accessed before initialization. + late final GisSdkClient _gisSdkClient; /// 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 +106,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,11 +127,9 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { 'serverClientId is not supported on Web.', ); - _initCalled = Completer(); - await _jsSdkLoadedFuture; - _gisSdkClient ??= GisSdkClient( + _gisSdkClient = GisSdkClient( clientId: appClientId!, nonce: params.nonce, hostedDomain: params.hostedDomain, @@ -150,15 +137,15 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { loggingEnabled: kDebugMode, ); - _initCalled!.complete(); // Signal that `init` is fully done. + _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 +170,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 +203,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 +212,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 +236,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 +267,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); }, ); } 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 cd849dcdc5c..bcb4222e1ea 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.0 +version: 1.1.1 environment: sdk: ^3.8.0