[google_sign_in_web] Migrate to the GIS SDK. (#6921)

* [google_sign_in_web] Migrate to GIS SDK.

* include_granted_scopes in requestScopes call.

* Remove the old JS-interop layer.

* Introduce a mockable GisSdkClient for tests.

* Split the people utils.

* Delete tests for the old code.

* Add some tests for the new code.

* More utils_test.dart

* Make jsifyAs reusable.

* Ignore the tester in utils_test.dart

* Make Clients overridable, and some renaming.

* Test people.dart

* Make autoDetectedClientId more testable.

* Add mockito.

* Comment about where to better split the code so GisSdkClient is testable too.

* Add google_sign_in_web_test.dart (and its mocks)

* dart format

* Log only in debug.

* Sync min sdk with package gis_web

* Add migration notes to the README.

* When the user is known upon signIn, remove friction.

* Do not ask for user selection again in the authorization popup
* Pass the email of the known user as a hint to the signIn method

* Address PR comments / checks.

* Update migration guide after comments from testers.

* Update README.md

* Remove package:jose from tests.

* Rename to Vincent Adultman

* _isJsSdkLoaded -> _jsSdkLoadedFuture

* Remove idToken comment.

* Link issue to split mocking better.

* Remove dependency in package:jwt_decoder

* Remove unneeded cast call.
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 85c46da..015334d 100644
--- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md
+++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md
@@ -1,5 +1,8 @@
-## NEXT
+## 0.11.0
 
+* **Breaking Change:** Migrates JS-interop to `package:google_identity_services_web`
+  * Uses the new Google Identity Authentication and Authorization JS SDKs. [Docs](https://developers.google.com/identity).
+    * Added "Migrating to v0.11" section to the `README.md`.
 * Updates minimum Flutter version to 3.0.
 
 ## 0.10.2+1
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 7c02379..64bfd7a 100644
--- a/packages/google_sign_in/google_sign_in_web/README.md
+++ b/packages/google_sign_in/google_sign_in_web/README.md
@@ -2,6 +2,122 @@
 
 The web implementation of [google_sign_in](https://pub.dev/packages/google_sign_in)
 
+## Migrating to v0.11 (Google Identity Services)
+
+The `google_sign_in_web` plugin is backed by the new Google Identity Services
+(GIS) JS SDK since version 0.11.0.
+
+The GIS SDK is used both for [Authentication](https://developers.google.com/identity/gsi/web/guides/overview)
+and [Authorization](https://developers.google.com/identity/oauth2/web/guides/overview) flows.
+
+The GIS SDK, however, doesn't behave exactly like the one being deprecated.
+Some concepts have experienced pretty drastic changes, and that's why this
+plugin required a major version update.
+
+### Differences between Google Identity Services SDK and Google Sign-In for Web SDK.
+
+The **Google Sign-In JavaScript for Web JS SDK** is set to be deprecated after
+March 31, 2023. **Google Identity Services (GIS) SDK** is the new solution to
+quickly and easily sign users into your app suing their Google accounts.
+
+* In the GIS SDK, Authentication and Authorization are now two separate concerns.
+  * Authentication (information about the current user) flows will not
+    authorize `scopes` anymore.
+  * Authorization (permissions for the app to access certain user information)
+    flows will not return authentication information.
+* The GIS SDK no longer has direct access to previously-seen users upon initialization.
+  * `signInSilently` now displays the One Tap UX for web.
+* The GIS SDK only provides an `idToken` (JWT-encoded info) when the user
+  successfully completes an authentication flow. In the plugin: `signInSilently`.
+* The plugin `signIn` method uses the Oauth "Implicit Flow" to Authorize the requested `scopes`.
+  * If the user hasn't `signInSilently`, they'll have to sign in as a first step
+    of the Authorization popup flow.
+  * If `signInSilently` was unsuccessful, the plugin will add extra `scopes` to
+    `signIn` and retrieve basic Profile information from the People API via a
+    REST call immediately after a successful authorization. In this case, the
+    `idToken` field of the `GoogleSignInUserData` will always be null.
+* The GIS SDK no longer handles sign-in state and user sessions, it only provides
+  Authentication credentials for the moment the user did authenticate.
+* The GIS SDK no longer is able to renew Authorization sessions on the web.
+  Once the token expires, API requests will begin to fail with unauthorized,
+  and user Authorization is required again.
+
+See more differences in the following migration guides:
+
+* Authentication > [Migrating from Google Sign-In](https://developers.google.com/identity/gsi/web/guides/migration)
+* Authorization > [Migrate to Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/migration-to-gis)
+
+### New use cases to take into account in your app
+
+#### Enable access to the People API for your GCP project
+
+Since the GIS SDK is separating Authentication from Authorization, the
+[Oauth Implicit pop-up flow](https://developers.google.com/identity/oauth2/web/guides/use-token-model)
+used to Authorize scopes does **not** return any Authentication information
+anymore (user credential / `idToken`).
+
+If the plugin is not able to Authenticate an user from `signInSilently` (the
+OneTap UX flow), it'll add extra `scopes` to those requested by the programmer
+so it can perform a [People API request](https://developers.google.com/people/api/rest/v1/people/get)
+to retrieve basic profile information about the user that is signed-in.
+
+The information retrieved from the People API is used to complete data for the
+[`GoogleSignInAccount`](https://pub.dev/documentation/google_sign_in/latest/google_sign_in/GoogleSignInAccount-class.html)
+object that is returned after `signIn` completes successfully.
+
+#### `signInSilently` always returns `null`
+
+Previous versions of this plugin were able to return a `GoogleSignInAccount`
+object that was fully populated (signed-in and authorized) from `signInSilently`
+because the former SDK equated "is authenticated" and "is authorized".
+
+With the GIS SDK, `signInSilently` only deals with user Authentication, so users
+retrieved "silently" will only contain an `idToken`, but not an `accessToken`.
+
+Only after `signIn` or `requestScopes`, a user will be fully formed.
+
+The GIS-backed plugin always returns `null` from `signInSilently`, to force apps
+that expect the former logic to perform a full `signIn`, which will result in a
+fully Authenticated and Authorized user, and making this migration easier.
+
+#### `idToken` is `null` in the `GoogleSignInAccount` object after `signIn`
+
+Since the GIS SDK is separating Authentication and Authorization, when a user
+fails to Authenticate through `signInSilently` and the plugin performs the
+fallback request to the People API described above,
+the returned `GoogleSignInUserData` object will contain basic profile information
+(name, email, photo, ID), but its `idToken` will be `null`.
+
+This is because JWT are cryptographically signed by Google Identity Services, and
+this plugin won't spoof that signature when it retrieves the information from a
+simple REST request.
+
+#### User Sessions
+
+Since the GIS SDK does _not_ manage user sessions anymore, apps that relied on
+this feature might break.
+
+If long-lived sessions are required, consider using some user authentication
+system that supports Google Sign In as a federated Authentication provider,
+like [Firebase Auth](https://firebase.google.com/docs/auth/flutter/federated-auth#google),
+or similar.
+
+#### Expired / Invalid Authorization Tokens
+
+Since the GIS SDK does _not_ auto-renew authorization tokens anymore, it's now
+the responsibility of your app to do so.
+
+Apps now need to monitor the status code of their REST API requests for response
+codes different to `200`. For example:
+
+* `401`: Missing or invalid access token.
+* `403`: Expired access token.
+
+In either case, your app needs to prompt the end user to `signIn` or
+`requestScopes`, to interactively renew the token.
+
+The GIS SDK limits authorization token duration to one hour (3600 seconds).
+
 ## Usage
 
 ### Import the package
@@ -12,7 +128,7 @@
 
 ### Web integration
 
-First, go through the instructions [here](https://developers.google.com/identity/sign-in/web/sign-in#before_you_begin) to create your Google Sign-In OAuth client ID.
+First, go through the instructions [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) to create your Google Sign-In OAuth client ID.
 
 On your `web/index.html` file, add the following `meta` tag, somewhere in the
 `head` of the document:
@@ -29,7 +145,10 @@
 2. Clicking "Edit" in the OAuth 2.0 Web application client that you created above.
 3. Adding the URIs you want to the **Authorized JavaScript origins**.
 
-For local development, may add a `localhost` entry, for example: `http://localhost:7357`
+For local development, you must add two `localhost` entries:
+
+* `http://localhost` and
+* `http://localhost:7357` (or any port that is free in your machine)
 
 #### Starting flutter in http://localhost:7357
 
@@ -45,40 +164,11 @@
 
 Read the rest of the instructions if you need to add extra APIs (like Google People API).
 
-
 ### Using the plugin
-Add the following import to your Dart code:
 
-```dart
-import 'package:google_sign_in/google_sign_in.dart';
-```
+See the [**Usage** instructions of `package:google_sign_in`](https://pub.dev/packages/google_sign_in#usage)
 
-Initialize GoogleSignIn with the scopes you want:
-
-```dart
-GoogleSignIn _googleSignIn = GoogleSignIn(
-  scopes: [
-    'email',
-    'https://www.googleapis.com/auth/contacts.readonly',
-  ],
-);
-```
-
-[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
-Future<void> _handleSignIn() async {
-  try {
-    await _googleSignIn.signIn();
-  } catch (error) {
-    print(error);
-  }
-}
-```
+Note that the **`serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web.**
 
 ## Example
 
@@ -86,7 +176,7 @@
 
 ## API details
 
-See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details.
+See [google_sign_in.dart](https://github.com/flutter/plugins/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details.
 
 ## Contributions and Testing
 
diff --git a/packages/google_sign_in/google_sign_in_web/example/build.yaml b/packages/google_sign_in/google_sign_in_web/example/build.yaml
new file mode 100644
index 0000000..db3104b
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/build.yaml
@@ -0,0 +1,6 @@
+targets:
+  $default:
+    sources:
+      - integration_test/*.dart
+      - lib/$lib$
+      - $package$
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
deleted file mode 100644
index 5dada90..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_legacy_init_test.dart
+++ /dev/null
@@ -1,223 +0,0 @@
-// 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: <String>['some', 'scope'],
-            clientId: '1234',
-          ),
-          throwsA(isA<PlatformException>()));
-    });
-
-    testWidgets('initialize forwards error code from JS',
-        (WidgetTester tester) async {
-      try {
-        await plugin.init(
-          hostedDomain: 'foo',
-          scopes: <String>['some', 'scope'],
-          clientId: '1234',
-        );
-        fail('plugin.initialize should have thrown an exception!');
-      } catch (e) {
-        final String code = js_util.getProperty<String>(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<void> discardInit() async {
-      try {
-        await plugin.init(
-          hostedDomain: 'foo',
-          scopes: <String>['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<PlatformException>()));
-    });
-
-    testWidgets('signIn throws', (WidgetTester tester) async {
-      await discardInit();
-      await expectLater(plugin.signIn(), throwsA(isA<PlatformException>()));
-    });
-
-    testWidgets('getTokens throws', (WidgetTester tester) async {
-      await discardInit();
-      await expectLater(plugin.getTokens(email: 'test@example.com'),
-          throwsA(isA<PlatformException>()));
-    });
-    testWidgets('requestScopes', (WidgetTester tester) async {
-      await discardInit();
-      await expectLater(plugin.requestScopes(<String>['newScope']),
-          throwsA(isA<PlatformException>()));
-    });
-  });
-
-  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: <String>['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: <String>['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<String>());
-      expect(pluginNameParameter, 'dart-google_sign_in_web');
-    });
-
-    group('Successful .initialize, then', () {
-      setUp(() async {
-        await plugin.init(
-          hostedDomain: 'foo',
-          scopes: <String>['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(<String>['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: <String>['some', 'scope'],
-        clientId: '1234',
-      );
-      await plugin.initialized;
-    });
-
-    testWidgets('User aborts sign in flow, throws PlatformException',
-        (WidgetTester tester) async {
-      await expectLater(plugin.signIn(), throwsA(isA<PlatformException>()));
-    });
-
-    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<String>(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
deleted file mode 100644
index 3e803b8..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart
+++ /dev/null
@@ -1,230 +0,0 @@
-// 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.
-
-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.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('throws PlatformException', (WidgetTester tester) async {
-      await expectLater(
-          plugin.initWithParams(const SignInInitParameters(
-            hostedDomain: 'foo',
-            scopes: <String>['some', 'scope'],
-            clientId: '1234',
-          )),
-          throwsA(isA<PlatformException>()));
-    });
-
-    testWidgets('forwards error code from JS', (WidgetTester tester) async {
-      try {
-        await plugin.initWithParams(const SignInInitParameters(
-          hostedDomain: 'foo',
-          scopes: <String>['some', 'scope'],
-          clientId: '1234',
-        ));
-        fail('plugin.initWithParams should have thrown an exception!');
-      } catch (e) {
-        final String code = js_util.getProperty<String>(e, 'code');
-        expect(code, 'idpiframe_initialization_failed');
-      }
-    });
-  });
-
-  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<void> discardInit() async {
-      try {
-        await plugin.initWithParams(const SignInInitParameters(
-          hostedDomain: 'foo',
-          scopes: <String>['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<PlatformException>()));
-    });
-
-    testWidgets('signIn throws', (WidgetTester tester) async {
-      await discardInit();
-      await expectLater(plugin.signIn(), throwsA(isA<PlatformException>()));
-    });
-
-    testWidgets('getTokens throws', (WidgetTester tester) async {
-      await discardInit();
-      await expectLater(plugin.getTokens(email: 'test@example.com'),
-          throwsA(isA<PlatformException>()));
-    });
-    testWidgets('requestScopes', (WidgetTester tester) async {
-      await discardInit();
-      await expectLater(plugin.requestScopes(<String>['newScope']),
-          throwsA(isA<PlatformException>()));
-    });
-  });
-
-  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.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.initWithParams(const SignInInitParameters(
-            hostedDomain: '',
-            clientId: '',
-            scopes: <String>['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.initWithParams(const SignInInitParameters(
-        hostedDomain: 'foo',
-        scopes: <String>['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<String>());
-      expect(pluginNameParameter, 'dart-google_sign_in_web');
-    });
-
-    group('Successful .initWithParams, then', () {
-      setUp(() async {
-        await plugin.initWithParams(const SignInInitParameters(
-          hostedDomain: 'foo',
-          scopes: <String>['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(<String>['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.initWithParams(const SignInInitParameters(
-        hostedDomain: 'foo',
-        scopes: <String>['some', 'scope'],
-        clientId: '1234',
-      ));
-      await plugin.initialized;
-    });
-
-    testWidgets('User aborts sign in flow, throws PlatformException',
-        (WidgetTester tester) async {
-      await expectLater(plugin.signIn(), throwsA(isA<PlatformException>()));
-    });
-
-    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<String>(e, 'code');
-        expect(code, 'popup_closed_by_user');
-      }
-    });
-  });
-}
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
deleted file mode 100644
index 7bfef53..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_legacy_init_test.dart
+++ /dev/null
@@ -1,51 +0,0 @@
-// 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
deleted file mode 100644
index fc753e2..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-// 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.
-
-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 .initWithParams');
-    await plugin.initWithParams(const SignInInitParameters(
-      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_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart
deleted file mode 100644
index 43eb9a5..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart
+++ /dev/null
@@ -1,13 +0,0 @@
-// 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.
-
-library gapi_mocks;
-
-import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
-
-import 'src/gapi.dart';
-import 'src/google_user.dart';
-import 'src/test_iife.dart';
-
-part 'src/auth2_init.dart';
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart
deleted file mode 100644
index 84f4e6e..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart
+++ /dev/null
@@ -1,109 +0,0 @@
-// 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.
-
-part of gapi_mocks;
-
-// JS mock of a gapi.auth2, with a successfully identified user
-String auth2InitSuccess(GoogleSignInUserData userData) => testIife('''
-${gapi()}
-
-var mockUser = ${googleUser(userData)};
-
-function GapiAuth2() {}
-GapiAuth2.prototype.init = function (initOptions) {
-  /*Leak the initOptions so we can look at them later.*/
-  window['gapi2.init.parameters'] = initOptions;
-  return {
-    then: (onSuccess, onError) => {
-      window.setTimeout(() => {
-        onSuccess(window.gapi.auth2);
-      }, 30);
-    },
-    currentUser: {
-      listen: (cb) => {
-        window.setTimeout(() => {
-          cb(mockUser);
-        }, 30);
-      }
-    }
-  }
-};
-
-GapiAuth2.prototype.getAuthInstance = function () {
-  return {
-    signIn: () => {
-      return new Promise((resolve, reject) => {
-        window.setTimeout(() => {
-          resolve(mockUser);
-        }, 30);
-      });
-    },
-    currentUser: {
-      get: () => mockUser,
-    },
-  }
-};
-
-window.gapi.auth2 = new GapiAuth2();
-''');
-
-String auth2InitError() => testIife('''
-${gapi()}
-
-function GapiAuth2() {}
-GapiAuth2.prototype.init = function (initOptions) {
-  return {
-    then: (onSuccess, onError) => {
-      window.setTimeout(() => {
-        onError({
-          error: 'idpiframe_initialization_failed',
-          details: 'This error was raised from a test.',
-        });
-      }, 30);
-    }
-  }
-};
-
-window.gapi.auth2 = new GapiAuth2();
-''');
-
-String auth2SignInError([String error = 'popup_closed_by_user']) => testIife('''
-${gapi()}
-
-var mockUser = null;
-
-function GapiAuth2() {}
-GapiAuth2.prototype.init = function (initOptions) {
-  return {
-    then: (onSuccess, onError) => {
-      window.setTimeout(() => {
-        onSuccess(window.gapi.auth2);
-      }, 30);
-    },
-    currentUser: {
-      listen: (cb) => {
-        window.setTimeout(() => {
-          cb(mockUser);
-        }, 30);
-      }
-    }
-  }
-};
-
-GapiAuth2.prototype.getAuthInstance = function () {
-  return {
-    signIn: () => {
-      return new Promise((resolve, reject) => {
-        window.setTimeout(() => {
-          reject({
-            error: '$error'
-          });
-        }, 30);
-      });
-    },
-  }
-};
-
-window.gapi.auth2 = new GapiAuth2();
-''');
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart
deleted file mode 100644
index 0e652c6..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart
+++ /dev/null
@@ -1,12 +0,0 @@
-// 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.
-
-// The JS mock of the global gapi object
-String gapi() => '''
-function Gapi() {};
-Gapi.prototype.load = function (script, cb) {
-  window.setTimeout(cb, 30);
-};
-window.gapi = new Gapi();
-''';
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart
deleted file mode 100644
index e5e6eb2..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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.
-
-import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
-
-// Creates the JS representation of some user data
-String googleUser(GoogleSignInUserData data) => '''
-{
-  getBasicProfile: () => {
-    return {
-      getName: () => '${data.displayName}',
-      getEmail: () => '${data.email}',
-      getId: () => '${data.id}',
-      getImageUrl: () => '${data.photoUrl}',
-    };
-  },
-  getAuthResponse: () => {
-    return {
-      id_token: '${data.idToken}',
-      access_token: 'access_${data.idToken}',
-    }
-  },
-  getGrantedScopes: () => 'some scope',
-  grant: () => true,
-  isSignedIn: () => {
-    return ${data != null ? 'true' : 'false'};
-  },
-}
-''';
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart
deleted file mode 100644
index c5aac36..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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.
-
-import 'package:google_sign_in_web/src/load_gapi.dart'
-    show kGapiOnloadCallbackFunctionName;
-
-// Wraps some JS mock code in an IIFE that ends by calling the onLoad dart callback.
-String testIife(String mock) => '''
-(function() {
-  $mock;
-  window['$kGapiOnloadCallbackFunctionName']();
-})();
-'''
-    .replaceAll(RegExp(r'\s{2,}'), '');
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart
deleted file mode 100644
index b9daac4..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart
+++ /dev/null
@@ -1,70 +0,0 @@
-// 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.
-
-import 'package:flutter_test/flutter_test.dart';
-import 'package:google_sign_in_web/src/js_interop/gapiauth2.dart' as gapi;
-import 'package:google_sign_in_web/src/utils.dart';
-import 'package:integration_test/integration_test.dart';
-
-void main() {
-  // The non-null use cases are covered by the auth2_test.dart file.
-  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
-  group('gapiUserToPluginUserData', () {
-    late FakeGoogleUser fakeUser;
-
-    setUp(() {
-      fakeUser = FakeGoogleUser();
-    });
-
-    testWidgets('null user -> null response', (WidgetTester tester) async {
-      expect(gapiUserToPluginUserData(null), isNull);
-    });
-
-    testWidgets('not signed-in user -> null response',
-        (WidgetTester tester) async {
-      expect(gapiUserToPluginUserData(fakeUser), isNull);
-    });
-
-    testWidgets('signed-in, but null profile user -> null response',
-        (WidgetTester tester) async {
-      fakeUser.setIsSignedIn(true);
-      expect(gapiUserToPluginUserData(fakeUser), isNull);
-    });
-
-    testWidgets('signed-in, null userId in profile user -> null response',
-        (WidgetTester tester) async {
-      fakeUser.setIsSignedIn(true);
-      fakeUser.setBasicProfile(FakeBasicProfile());
-      expect(gapiUserToPluginUserData(fakeUser), isNull);
-    });
-  });
-}
-
-class FakeGoogleUser extends Fake implements gapi.GoogleUser {
-  bool _isSignedIn = false;
-  gapi.BasicProfile? _basicProfile;
-
-  @override
-  bool isSignedIn() => _isSignedIn;
-  @override
-  gapi.BasicProfile? getBasicProfile() => _basicProfile;
-
-  // ignore: use_setters_to_change_properties
-  void setIsSignedIn(bool isSignedIn) {
-    _isSignedIn = isSignedIn;
-  }
-
-  // ignore: use_setters_to_change_properties
-  void setBasicProfile(gapi.BasicProfile basicProfile) {
-    _basicProfile = basicProfile;
-  }
-}
-
-class FakeBasicProfile extends Fake implements gapi.BasicProfile {
-  String? _id;
-
-  @override
-  String? getId() => _id;
-}
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
new file mode 100644
index 0000000..3dcc192
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart
@@ -0,0 +1,219 @@
+// 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.
+
+import 'package:flutter/services.dart' show PlatformException;
+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:google_sign_in_web/src/gis_client.dart';
+import 'package:google_sign_in_web/src/people.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart' as mockito;
+
+import 'google_sign_in_web_test.mocks.dart';
+import 'src/dom.dart';
+import 'src/person.dart';
+
+// Mock GisSdkClient so we can simulate any response from the JS side.
+@GenerateMocks(<Type>[], customMocks: <MockSpec<dynamic>>[
+  MockSpec<GisSdkClient>(onMissingStub: OnMissingStub.returnDefault),
+])
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('Constructor', () {
+    const String expectedClientId = '3xp3c73d_c113n7_1d';
+
+    testWidgets('Loads clientId when set in a meta', (_) async {
+      final GoogleSignInPlugin plugin = GoogleSignInPlugin(
+        debugOverrideLoader: true,
+      );
+
+      expect(plugin.autoDetectedClientId, isNull);
+
+      // Add it to the test page now, and try again
+      final DomHtmlMetaElement meta =
+          document.createElement('meta') as DomHtmlMetaElement
+            ..name = clientIdMetaName
+            ..content = expectedClientId;
+
+      document.head.appendChild(meta);
+
+      final GoogleSignInPlugin another = GoogleSignInPlugin(
+        debugOverrideLoader: true,
+      );
+
+      expect(another.autoDetectedClientId, expectedClientId);
+
+      // cleanup
+      meta.remove();
+    });
+  });
+
+  group('initWithParams', () {
+    late GoogleSignInPlugin plugin;
+    late MockGisSdkClient mockGis;
+
+    setUp(() {
+      plugin = GoogleSignInPlugin(
+        debugOverrideLoader: true,
+      );
+      mockGis = MockGisSdkClient();
+    });
+
+    testWidgets('initializes if all is OK', (_) async {
+      await plugin.initWithParams(
+        const SignInInitParameters(
+          clientId: 'some-non-null-client-id',
+          scopes: <String>['ok1', 'ok2', 'ok3'],
+        ),
+        overrideClient: mockGis,
+      );
+
+      expect(plugin.initialized, completes);
+    });
+
+    testWidgets('asserts clientId is not null', (_) async {
+      expect(() async {
+        await plugin.initWithParams(
+          const SignInInitParameters(),
+          overrideClient: mockGis,
+        );
+      }, throwsAssertionError);
+    });
+
+    testWidgets('asserts serverClientId must be null', (_) async {
+      expect(() async {
+        await plugin.initWithParams(
+          const SignInInitParameters(
+            clientId: 'some-non-null-client-id',
+            serverClientId: 'unexpected-non-null-client-id',
+          ),
+          overrideClient: mockGis,
+        );
+      }, throwsAssertionError);
+    });
+
+    testWidgets('asserts no scopes have any spaces', (_) async {
+      expect(() async {
+        await plugin.initWithParams(
+          const SignInInitParameters(
+            clientId: 'some-non-null-client-id',
+            scopes: <String>['ok1', 'ok2', 'not ok', 'ok3'],
+          ),
+          overrideClient: mockGis,
+        );
+      }, throwsAssertionError);
+    });
+
+    testWidgets('must be called for most of the API to work', (_) async {
+      expect(() async {
+        await plugin.signInSilently();
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.signIn();
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.getTokens(email: '');
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.signOut();
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.disconnect();
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.isSignedIn();
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.clearAuthCache(token: '');
+      }, throwsStateError);
+
+      expect(() async {
+        await plugin.requestScopes(<String>[]);
+      }, throwsStateError);
+    });
+  });
+
+  group('(with mocked GIS)', () {
+    late GoogleSignInPlugin plugin;
+    late MockGisSdkClient mockGis;
+    const SignInInitParameters options = SignInInitParameters(
+      clientId: 'some-non-null-client-id',
+      scopes: <String>['ok1', 'ok2', 'ok3'],
+    );
+
+    setUp(() {
+      plugin = GoogleSignInPlugin(
+        debugOverrideLoader: true,
+      );
+      mockGis = MockGisSdkClient();
+    });
+
+    group('signInSilently', () {
+      setUp(() {
+        plugin.initWithParams(options, overrideClient: mockGis);
+      });
+
+      testWidgets('always returns null, regardless of GIS response', (_) async {
+        final GoogleSignInUserData someUser = extractUserData(person)!;
+
+        mockito
+            .when(mockGis.signInSilently())
+            .thenAnswer((_) => Future<GoogleSignInUserData>.value(someUser));
+
+        expect(plugin.signInSilently(), completion(isNull));
+
+        mockito
+            .when(mockGis.signInSilently())
+            .thenAnswer((_) => Future<GoogleSignInUserData?>.value());
+
+        expect(plugin.signInSilently(), completion(isNull));
+      });
+    });
+
+    group('signIn', () {
+      setUp(() {
+        plugin.initWithParams(options, overrideClient: mockGis);
+      });
+
+      testWidgets('returns the signed-in user', (_) async {
+        final GoogleSignInUserData someUser = extractUserData(person)!;
+
+        mockito
+            .when(mockGis.signIn())
+            .thenAnswer((_) => Future<GoogleSignInUserData>.value(someUser));
+
+        expect(await plugin.signIn(), someUser);
+      });
+
+      testWidgets('returns null if no user is signed in', (_) async {
+        mockito
+            .when(mockGis.signIn())
+            .thenAnswer((_) => Future<GoogleSignInUserData?>.value());
+
+        expect(await plugin.signIn(), isNull);
+      });
+
+      testWidgets('converts inner errors to PlatformException', (_) async {
+        mockito.when(mockGis.signIn()).thenThrow('popup_closed');
+
+        try {
+          await plugin.signIn();
+          fail('signIn should have thrown an exception');
+        } catch (exception) {
+          expect(exception, isA<PlatformException>());
+          expect((exception as PlatformException).code, 'popup_closed');
+        }
+      });
+    });
+  });
+}
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart
new file mode 100644
index 0000000..b60dac9
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart
@@ -0,0 +1,125 @@
+// Mocks generated by Mockito 5.3.2 from annotations
+// in google_sign_in_web_integration_tests/integration_test/google_sign_in_web_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i4;
+
+import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'
+    as _i2;
+import 'package:google_sign_in_web/src/gis_client.dart' as _i3;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake
+    implements _i2.GoogleSignInTokenData {
+  _FakeGoogleSignInTokenData_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [GisSdkClient].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient {
+  @override
+  _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod(
+        Invocation.method(
+          #signInSilently,
+          [],
+        ),
+        returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(),
+        returnValueForMissingStub:
+            _i4.Future<_i2.GoogleSignInUserData?>.value(),
+      ) as _i4.Future<_i2.GoogleSignInUserData?>);
+  @override
+  _i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod(
+        Invocation.method(
+          #signIn,
+          [],
+        ),
+        returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(),
+        returnValueForMissingStub:
+            _i4.Future<_i2.GoogleSignInUserData?>.value(),
+      ) as _i4.Future<_i2.GoogleSignInUserData?>);
+  @override
+  _i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod(
+        Invocation.method(
+          #getTokens,
+          [],
+        ),
+        returnValue: _FakeGoogleSignInTokenData_0(
+          this,
+          Invocation.method(
+            #getTokens,
+            [],
+          ),
+        ),
+        returnValueForMissingStub: _FakeGoogleSignInTokenData_0(
+          this,
+          Invocation.method(
+            #getTokens,
+            [],
+          ),
+        ),
+      ) as _i2.GoogleSignInTokenData);
+  @override
+  _i4.Future<void> signOut() => (super.noSuchMethod(
+        Invocation.method(
+          #signOut,
+          [],
+        ),
+        returnValue: _i4.Future<void>.value(),
+        returnValueForMissingStub: _i4.Future<void>.value(),
+      ) as _i4.Future<void>);
+  @override
+  _i4.Future<void> disconnect() => (super.noSuchMethod(
+        Invocation.method(
+          #disconnect,
+          [],
+        ),
+        returnValue: _i4.Future<void>.value(),
+        returnValueForMissingStub: _i4.Future<void>.value(),
+      ) as _i4.Future<void>);
+  @override
+  _i4.Future<bool> isSignedIn() => (super.noSuchMethod(
+        Invocation.method(
+          #isSignedIn,
+          [],
+        ),
+        returnValue: _i4.Future<bool>.value(false),
+        returnValueForMissingStub: _i4.Future<bool>.value(false),
+      ) as _i4.Future<bool>);
+  @override
+  _i4.Future<void> clearAuthCache() => (super.noSuchMethod(
+        Invocation.method(
+          #clearAuthCache,
+          [],
+        ),
+        returnValue: _i4.Future<void>.value(),
+        returnValueForMissingStub: _i4.Future<void>.value(),
+      ) as _i4.Future<void>);
+  @override
+  _i4.Future<bool> requestScopes(List<String>? scopes) => (super.noSuchMethod(
+        Invocation.method(
+          #requestScopes,
+          [scopes],
+        ),
+        returnValue: _i4.Future<bool>.value(false),
+        returnValueForMissingStub: _i4.Future<bool>.value(false),
+      ) as _i4.Future<bool>);
+}
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart
new file mode 100644
index 0000000..e81ccb6
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart
@@ -0,0 +1,132 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_identity_services_web/oauth2.dart';
+import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
+import 'package:google_sign_in_web/src/people.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart' as http_test;
+import 'package:integration_test/integration_test.dart';
+
+import 'src/jsify_as.dart';
+import 'src/person.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('requestUserData', () {
+    const String expectedAccessToken = '3xp3c73d_4cc355_70k3n';
+
+    final TokenResponse fakeToken = jsifyAs(<String, Object?>{
+      'token_type': 'Bearer',
+      'access_token': expectedAccessToken,
+    });
+
+    testWidgets('happy case', (_) async {
+      final Completer<String> accessTokenCompleter = Completer<String>();
+
+      final http.Client mockClient = http_test.MockClient(
+        (http.Request request) async {
+          accessTokenCompleter.complete(request.headers['Authorization']);
+
+          return http.Response(
+            jsonEncode(person),
+            200,
+            headers: <String, String>{'content-type': 'application/json'},
+          );
+        },
+      );
+
+      final GoogleSignInUserData? user = await requestUserData(
+        fakeToken,
+        overrideClient: mockClient,
+      );
+
+      expect(user, isNotNull);
+      expect(user!.email, expectedPersonEmail);
+      expect(user.id, expectedPersonId);
+      expect(user.displayName, expectedPersonName);
+      expect(user.photoUrl, expectedPersonPhoto);
+      expect(user.idToken, isNull);
+      expect(
+        accessTokenCompleter.future,
+        completion('Bearer $expectedAccessToken'),
+      );
+    });
+
+    testWidgets('Unauthorized request - throws exception', (_) async {
+      final http.Client mockClient = http_test.MockClient(
+        (http.Request request) async {
+          return http.Response(
+            'Unauthorized',
+            403,
+          );
+        },
+      );
+
+      expect(() async {
+        await requestUserData(
+          fakeToken,
+          overrideClient: mockClient,
+        );
+      }, throwsA(isA<http.ClientException>()));
+    });
+  });
+
+  group('extractUserData', () {
+    testWidgets('happy case', (_) async {
+      final GoogleSignInUserData? user = extractUserData(person);
+
+      expect(user, isNotNull);
+      expect(user!.email, expectedPersonEmail);
+      expect(user.id, expectedPersonId);
+      expect(user.displayName, expectedPersonName);
+      expect(user.photoUrl, expectedPersonPhoto);
+      expect(user.idToken, isNull);
+    });
+
+    testWidgets('no name/photo - keeps going', (_) async {
+      final Map<String, Object?> personWithoutSomeData =
+          mapWithoutKeys(person, <String>{
+        'names',
+        'photos',
+      });
+
+      final GoogleSignInUserData? user = extractUserData(personWithoutSomeData);
+
+      expect(user, isNotNull);
+      expect(user!.email, expectedPersonEmail);
+      expect(user.id, expectedPersonId);
+      expect(user.displayName, isNull);
+      expect(user.photoUrl, isNull);
+      expect(user.idToken, isNull);
+    });
+
+    testWidgets('no userId - throws assertion error', (_) async {
+      final Map<String, Object?> personWithoutId =
+          mapWithoutKeys(person, <String>{
+        'resourceName',
+      });
+
+      expect(() {
+        extractUserData(personWithoutId);
+      }, throwsAssertionError);
+    });
+
+    testWidgets('no email - throws assertion error', (_) async {
+      final Map<String, Object?> personWithoutEmail =
+          mapWithoutKeys(person, <String>{
+        'emailAddresses',
+      });
+
+      expect(() {
+        extractUserData(personWithoutEmail);
+      }, throwsAssertionError);
+    });
+  });
+}
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart
new file mode 100644
index 0000000..f7d3152
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/dom.dart
@@ -0,0 +1,59 @@
+// 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.
+
+/*
+// DOM shim. This file contains everything we need from the DOM API written as
+// @staticInterop, so we don't need dart:html
+// https://developer.mozilla.org/en-US/docs/Web/API/
+//
+// (To be replaced by `package:web`)
+*/
+
+import 'package:js/js.dart';
+
+/// Document interface
+@JS()
+@staticInterop
+abstract class DomHtmlDocument {}
+
+/// Some methods of document
+extension DomHtmlDocumentExtension on DomHtmlDocument {
+  /// document.head
+  external DomHtmlElement get head;
+
+  /// document.createElement
+  external DomHtmlElement createElement(String tagName);
+}
+
+/// An instance of an HTMLElement
+@JS()
+@staticInterop
+abstract class DomHtmlElement {}
+
+/// (Some) methods of HtmlElement
+extension DomHtmlElementExtension on DomHtmlElement {
+  /// Node.appendChild
+  external DomHtmlElement appendChild(DomHtmlElement child);
+
+  /// Element.remove
+  external void remove();
+}
+
+/// An instance of an HTMLMetaElement
+@JS()
+@staticInterop
+abstract class DomHtmlMetaElement extends DomHtmlElement {}
+
+/// Some methods exclusive of Script elements
+extension DomHtmlMetaElementExtension on DomHtmlMetaElement {
+  external set name(String name);
+  external set content(String content);
+}
+
+// Getters
+
+/// window.document
+@JS()
+@staticInterop
+external DomHtmlDocument get document;
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart
new file mode 100644
index 0000000..82547b2
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jsify_as.dart
@@ -0,0 +1,10 @@
+// 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.
+
+import 'package:js/js_util.dart' as js_util;
+
+/// Converts a [data] object into a JS Object of type `T`.
+T jsifyAs<T>(Map<String, Object?> data) {
+  return js_util.jsify(data) as T;
+}
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart
new file mode 100644
index 0000000..72841c5
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/jwt_examples.dart
@@ -0,0 +1,46 @@
+// 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.
+
+import 'package:google_identity_services_web/id.dart';
+
+import 'jsify_as.dart';
+
+/// A CredentialResponse with null `credential`.
+final CredentialResponse nullCredential =
+    jsifyAs<CredentialResponse>(<String, Object?>{
+  'credential': null,
+});
+
+/// A CredentialResponse wrapping a known good JWT Token as its `credential`.
+final CredentialResponse goodCredential =
+    jsifyAs<CredentialResponse>(<String, Object?>{
+  'credential': goodJwtToken,
+});
+
+/// A JWT token with predefined values.
+///
+/// 'email': 'adultman@example.com',
+/// 'sub': '123456',
+/// 'name': 'Vincent Adultman',
+/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg',
+///
+/// Signed with HS256 and the private key: 'symmetric-encryption-is-weak'
+const String goodJwtToken =
+    'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.$goodPayload.lqzULA_U3YzEl_-fL7YLU-kFXmdD2ttJLTv-UslaNQ4';
+
+/// The payload of a JWT token that contains predefined values.
+///
+/// 'email': 'adultman@example.com',
+/// 'sub': '123456',
+/// 'name': 'Vincent Adultman',
+/// 'picture': 'https://thispersondoesnotexist.com/image?x=.jpg',
+const String goodPayload =
+    'eyJlbWFpbCI6ImFkdWx0bWFuQGV4YW1wbGUuY29tIiwic3ViIjoiMTIzNDU2IiwibmFtZSI6IlZpbmNlbnQgQWR1bHRtYW4iLCJwaWN0dXJlIjoiaHR0cHM6Ly90aGlzcGVyc29uZG9lc25vdGV4aXN0LmNvbS9pbWFnZT94PS5qcGcifQ';
+
+// More encrypted JWT Tokens may be created on https://jwt.io.
+//
+// First, decode the `goodJwtToken` above, modify to your heart's
+// content, and add a new credential here.
+//
+// (New tokens can also be created with `package:jose` and `dart:convert`.)
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart
new file mode 100644
index 0000000..2525596
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart
@@ -0,0 +1,66 @@
+// 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.
+
+const String expectedPersonId = '1234567890';
+const String expectedPersonName = 'Vincent Adultman';
+const String expectedPersonEmail = 'adultman@example.com';
+const String expectedPersonPhoto =
+    'https://thispersondoesnotexist.com/image?x=.jpg';
+
+/// A subset of https://developers.google.com/people/api/rest/v1/people#Person.
+final Map<String, Object?> person = <String, Object?>{
+  'resourceName': 'people/$expectedPersonId',
+  'emailAddresses': <Object?>[
+    <String, Object?>{
+      'metadata': <String, Object?>{
+        'primary': false,
+      },
+      'value': 'bad@example.com',
+    },
+    <String, Object?>{
+      'metadata': <String, Object?>{},
+      'value': 'nope@example.com',
+    },
+    <String, Object?>{
+      'metadata': <String, Object?>{
+        'primary': true,
+      },
+      'value': expectedPersonEmail,
+    },
+  ],
+  'names': <Object?>[
+    <String, Object?>{
+      'metadata': <String, Object?>{
+        'primary': true,
+      },
+      'displayName': expectedPersonName,
+    },
+    <String, Object?>{
+      'metadata': <String, Object?>{
+        'primary': false,
+      },
+      'displayName': 'Fakey McFakeface',
+    },
+  ],
+  'photos': <Object?>[
+    <String, Object?>{
+      'metadata': <String, Object?>{
+        'primary': true,
+      },
+      'url': expectedPersonPhoto,
+    },
+  ],
+};
+
+/// Returns a copy of [map] without the [keysToRemove].
+T mapWithoutKeys<T extends Map<String, Object?>>(
+  T map,
+  Set<String> keysToRemove,
+) {
+  return Map<String, Object?>.fromEntries(
+    map.entries.where((MapEntry<String, Object?> entry) {
+      return !keysToRemove.contains(entry.key);
+    }),
+  ) as T;
+}
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart
deleted file mode 100644
index 56aa61d..0000000
--- a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart
+++ /dev/null
@@ -1,10 +0,0 @@
-// 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.
-
-import 'dart:convert';
-
-String toBase64Url(String contents) {
-  // Open the file
-  return 'data:text/javascript;base64,${base64.encode(utf8.encode(contents))}';
-}
diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart
new file mode 100644
index 0000000..82701e5
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart
@@ -0,0 +1,173 @@
+// 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.
+
+import 'dart:convert';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:google_identity_services_web/id.dart';
+import 'package:google_identity_services_web/oauth2.dart';
+import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
+import 'package:google_sign_in_web/src/utils.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'src/jsify_as.dart';
+import 'src/jwt_examples.dart';
+
+void main() {
+  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+  group('gisResponsesToTokenData', () {
+    testWidgets('null objects -> no problem', (_) async {
+      final GoogleSignInTokenData tokens = gisResponsesToTokenData(null, null);
+
+      expect(tokens.accessToken, isNull);
+      expect(tokens.idToken, isNull);
+      expect(tokens.serverAuthCode, isNull);
+    });
+
+    testWidgets('non-null objects are correctly used', (_) async {
+      const String expectedIdToken = 'some-value-for-testing';
+      const String expectedAccessToken = 'another-value-for-testing';
+
+      final CredentialResponse credential =
+          jsifyAs<CredentialResponse>(<String, Object?>{
+        'credential': expectedIdToken,
+      });
+      final TokenResponse token = jsifyAs<TokenResponse>(<String, Object?>{
+        'access_token': expectedAccessToken,
+      });
+      final GoogleSignInTokenData tokens =
+          gisResponsesToTokenData(credential, token);
+
+      expect(tokens.accessToken, expectedAccessToken);
+      expect(tokens.idToken, expectedIdToken);
+      expect(tokens.serverAuthCode, isNull);
+    });
+  });
+
+  group('gisResponsesToUserData', () {
+    testWidgets('happy case', (_) async {
+      final GoogleSignInUserData data = gisResponsesToUserData(goodCredential)!;
+
+      expect(data.displayName, 'Vincent Adultman');
+      expect(data.id, '123456');
+      expect(data.email, 'adultman@example.com');
+      expect(data.photoUrl, 'https://thispersondoesnotexist.com/image?x=.jpg');
+      expect(data.idToken, goodJwtToken);
+    });
+
+    testWidgets('null response -> null', (_) async {
+      expect(gisResponsesToUserData(null), isNull);
+    });
+
+    testWidgets('null response.credential -> null', (_) async {
+      expect(gisResponsesToUserData(nullCredential), isNull);
+    });
+
+    testWidgets('invalid payload -> null', (_) async {
+      final CredentialResponse response =
+          jsifyAs<CredentialResponse>(<String, Object?>{
+        'credential': 'some-bogus.thing-that-is-not.valid-jwt',
+      });
+      expect(gisResponsesToUserData(response), isNull);
+    });
+  });
+
+  group('getJwtTokenPayload', () {
+    testWidgets('happy case -> data', (_) async {
+      final Map<String, Object?>? data = getJwtTokenPayload(goodJwtToken);
+
+      expect(data, isNotNull);
+      expect(data, containsPair('name', 'Vincent Adultman'));
+      expect(data, containsPair('email', 'adultman@example.com'));
+      expect(data, containsPair('sub', '123456'));
+      expect(
+          data,
+          containsPair(
+            'picture',
+            'https://thispersondoesnotexist.com/image?x=.jpg',
+          ));
+    });
+
+    testWidgets('null Token -> null', (_) async {
+      final Map<String, Object?>? data = getJwtTokenPayload(null);
+
+      expect(data, isNull);
+    });
+
+    testWidgets('Token not matching the format -> null', (_) async {
+      final Map<String, Object?>? data = getJwtTokenPayload('1234.4321');
+
+      expect(data, isNull);
+    });
+
+    testWidgets('Bad token that matches the format -> null', (_) async {
+      final Map<String, Object?>? data = getJwtTokenPayload('1234.abcd.4321');
+
+      expect(data, isNull);
+    });
+  });
+
+  group('decodeJwtPayload', () {
+    testWidgets('Good payload -> data', (_) async {
+      final Map<String, Object?>? data = decodeJwtPayload(goodPayload);
+
+      expect(data, isNotNull);
+      expect(data, containsPair('name', 'Vincent Adultman'));
+      expect(data, containsPair('email', 'adultman@example.com'));
+      expect(data, containsPair('sub', '123456'));
+      expect(
+          data,
+          containsPair(
+            'picture',
+            'https://thispersondoesnotexist.com/image?x=.jpg',
+          ));
+    });
+
+    testWidgets('Proper JSON payload -> data', (_) async {
+      final String payload = base64.encode(utf8.encode('{"properJson": true}'));
+
+      final Map<String, Object?>? data = decodeJwtPayload(payload);
+
+      expect(data, isNotNull);
+      expect(data, containsPair('properJson', true));
+    });
+
+    testWidgets('Not-normalized base-64 payload -> data', (_) async {
+      // This is the payload generated by the "Proper JSON payload" test, but
+      // we remove the leading "=" symbols so it's length is not a multiple of 4
+      // anymore!
+      final String payload = 'eyJwcm9wZXJKc29uIjogdHJ1ZX0='.replaceAll('=', '');
+
+      final Map<String, Object?>? data = decodeJwtPayload(payload);
+
+      expect(data, isNotNull);
+      expect(data, containsPair('properJson', true));
+    });
+
+    testWidgets('Invalid JSON payload -> null', (_) async {
+      final String payload = base64.encode(utf8.encode('{properJson: false}'));
+
+      final Map<String, Object?>? data = decodeJwtPayload(payload);
+
+      expect(data, isNull);
+    });
+
+    testWidgets('Non JSON payload -> null', (_) async {
+      final String payload = base64.encode(utf8.encode('not-json'));
+
+      final Map<String, Object?>? data = decodeJwtPayload(payload);
+
+      expect(data, isNull);
+    });
+
+    testWidgets('Non base-64 payload -> null', (_) async {
+      const String payload = 'not-base-64-at-all';
+
+      final Map<String, Object?>? data = decodeJwtPayload(payload);
+
+      expect(data, isNull);
+    });
+  });
+}
diff --git a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml
index 8485175..c739533 100644
--- a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml
+++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml
@@ -12,12 +12,15 @@
     path: ../
 
 dev_dependencies:
+  build_runner: ^2.1.1
   flutter_driver:
     sdk: flutter
   flutter_test:
     sdk: flutter
+  google_identity_services_web: ^0.2.0
   google_sign_in_platform_interface: ^2.2.0
   http: ^0.13.0
   integration_test:
     sdk: flutter
   js: ^0.6.3
+  mockito: ^5.3.2
diff --git a/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh b/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh
new file mode 100755
index 0000000..78bcdc0
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/example/regen_mocks.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/bash
+# 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.
+
+flutter pub get
+
+echo "(Re)generating mocks."
+
+flutter pub run build_runner build --delete-conflicting-outputs
diff --git a/packages/google_sign_in/google_sign_in_web/example/run_test.sh b/packages/google_sign_in/google_sign_in_web/example/run_test.sh
index 28877dc..fcac5f6 100755
--- a/packages/google_sign_in/google_sign_in_web/example/run_test.sh
+++ b/packages/google_sign_in/google_sign_in_web/example/run_test.sh
@@ -6,9 +6,11 @@
 if pgrep -lf chromedriver > /dev/null; then
   echo "chromedriver is running."
 
+  ./regen_mocks.sh
+
   if [ $# -eq 0 ]; then
     echo "No target specified, running all tests..."
-    find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome  --driver=test_driver/integration_test.dart --target='{}'
+    find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}'
   else
     echo "Running test target: $1..."
     set -x
@@ -17,7 +19,6 @@
 
   else
     echo "chromedriver is not running."
+    echo "Please, check the README.md for instructions on how to use run_test.sh"
 fi
 
-
-
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 5d75c0d..827b17c 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
@@ -5,23 +5,22 @@
 import 'dart:async';
 import 'dart:html' as html;
 
-import 'package:flutter/foundation.dart' show visibleForTesting;
-import 'package:flutter/services.dart';
+import 'package:flutter/foundation.dart' show visibleForTesting, kDebugMode;
+import 'package:flutter/services.dart' show PlatformException;
 import 'package:flutter_web_plugins/flutter_web_plugins.dart';
+import 'package:google_identity_services_web/loader.dart' as loader;
 import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
-import 'package:js/js.dart';
 
-import 'src/js_interop/gapiauth2.dart' as auth2;
-import 'src/load_gapi.dart' as gapi;
-import 'src/utils.dart' show gapiUserToPluginUserData;
+import 'src/gis_client.dart';
 
-const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]';
-const String _kClientIdAttributeName = 'content';
+/// The `name` of the meta-tag to define a ClientID in HTML.
+const String clientIdMetaName = 'google-signin-client_id';
 
-/// This is only exposed for testing. It shouldn't be accessed by users of the
-/// plugin as it could break at any point.
-@visibleForTesting
-String gapiUrl = 'https://apis.google.com/js/platform.js';
+/// The selector used to find the meta-tag that defines a ClientID in HTML.
+const String clientIdMetaSelector = 'meta[name=$clientIdMetaName]';
+
+/// The attribute name that stores the Client ID in the meta-tag that defines a Client ID in HTML.
+const String clientIdAttributeName = 'content';
 
 /// Implementation of the google_sign_in plugin for Web.
 class GoogleSignInPlugin extends GoogleSignInPlatform {
@@ -29,18 +28,24 @@
   /// background.
   ///
   /// The plugin is completely initialized when [initialized] completed.
-  GoogleSignInPlugin() {
-    _autoDetectedClientId = html
-        .querySelector(_kClientIdMetaSelector)
-        ?.getAttribute(_kClientIdAttributeName);
+  GoogleSignInPlugin({@visibleForTesting bool debugOverrideLoader = false}) {
+    autoDetectedClientId = html
+        .querySelector(clientIdMetaSelector)
+        ?.getAttribute(clientIdAttributeName);
 
-    _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init());
+    if (debugOverrideLoader) {
+      _jsSdkLoadedFuture = Future<bool>.value(true);
+    } else {
+      _jsSdkLoadedFuture = loader.loadWebSdk();
+    }
   }
 
-  late Future<void> _isGapiInitialized;
-  late Future<void> _isAuthInitialized;
+  late Future<void> _jsSdkLoadedFuture;
   bool _isInitCalled = false;
 
+  // The instance of [GisSdkClient] backing the plugin.
+  late GisSdkClient _gisClient;
+
   // 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.
@@ -53,14 +58,16 @@
     }
   }
 
-  /// A future that resolves when both GAPI and Auth2 have been correctly initialized.
+  /// A future that resolves when the SDK has been correctly loaded.
   @visibleForTesting
   Future<void> get initialized {
     _assertIsInitCalled();
-    return Future.wait(<Future<void>>[_isGapiInitialized, _isAuthInitialized]);
+    return _jsSdkLoadedFuture;
   }
 
-  String? _autoDetectedClientId;
+  /// Stores the client ID if it was set in a meta-tag of the page.
+  @visibleForTesting
+  late String? autoDetectedClientId;
 
   /// Factory method that initializes the plugin with [GoogleSignInPlatform].
   static void registerWith(Registrar registrar) {
@@ -83,8 +90,11 @@
   }
 
   @override
-  Future<void> initWithParams(SignInInitParameters params) async {
-    final String? appClientId = params.clientId ?? _autoDetectedClientId;
+  Future<void> initWithParams(
+    SignInInitParameters params, {
+    @visibleForTesting GisSdkClient? overrideClient,
+  }) async {
+    final String? appClientId = params.clientId ?? autoDetectedClientId;
     assert(
         appClientId != null,
         'ClientID not set. Either set it on a '
@@ -100,141 +110,95 @@
         'Check https://developers.google.com/identity/protocols/googlescopes '
         'for a list of valid OAuth 2.0 scopes.');
 
-    await _isGapiInitialized;
+    await _jsSdkLoadedFuture;
 
-    final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig(
-      hosted_domain: params.hostedDomain,
-      // The js lib wants a space-separated list of values
-      scope: params.scopes.join(' '),
-      client_id: appClientId!,
-      plugin_name: 'dart-google_sign_in_web',
-    ));
+    _gisClient = overrideClient ??
+        GisSdkClient(
+          clientId: appClientId!,
+          hostedDomain: params.hostedDomain,
+          initialScopes: List<String>.from(params.scopes),
+          loggingEnabled: kDebugMode,
+        );
 
-    final Completer<void> isAuthInitialized = Completer<void>();
-    _isAuthInitialized = isAuthInitialized.future;
     _isInitCalled = true;
-
-    auth.then(allowInterop((auth2.GoogleAuth initializedAuth) {
-      // onSuccess
-
-      // TODO(ditman): https://github.com/flutter/flutter/issues/48528
-      // This plugin doesn't notify the app of external changes to the
-      // state of the authentication, i.e: if you logout elsewhere...
-
-      isAuthInitialized.complete();
-    }), allowInterop((auth2.GoogleAuthInitFailureError reason) {
-      // onError
-      isAuthInitialized.completeError(PlatformException(
-        code: reason.error,
-        message: reason.details,
-        details:
-            'https://developers.google.com/identity/sign-in/web/reference#error_codes',
-      ));
-    }));
-
-    return _isAuthInitialized;
   }
 
   @override
   Future<GoogleSignInUserData?> signInSilently() async {
     await initialized;
 
-    return gapiUserToPluginUserData(
-        auth2.getAuthInstance()?.currentUser?.get());
+    // Since the new GIS SDK does *not* perform authorization at the same time as
+    // authentication (and every one of our users expects that), we need to tell
+    // the plugin that this failed regardless of the actual result.
+    //
+    // However, if this succeeds, we'll save a People API request later.
+    return _gisClient.signInSilently().then((_) => null);
   }
 
   @override
   Future<GoogleSignInUserData?> signIn() async {
     await initialized;
+
+    // This method mainly does oauth2 authorization, which happens to also do
+    // authentication if needed. However, the authentication information is not
+    // returned anymore.
+    //
+    // This method will synthesize authentication information from the People API
+    // if needed (or use the last identity seen from signInSilently).
     try {
-      return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn());
-    } on auth2.GoogleAuthSignInError catch (reason) {
+      return _gisClient.signIn();
+    } catch (reason) {
       throw PlatformException(
-        code: reason.error,
-        message: 'Exception raised from GoogleAuth.signIn()',
+        code: reason.toString(),
+        message: 'Exception raised from signIn',
         details:
-            'https://developers.google.com/identity/sign-in/web/reference#error_codes_2',
+            'https://developers.google.com/identity/oauth2/web/guides/error',
       );
     }
   }
 
   @override
-  Future<GoogleSignInTokenData> getTokens(
-      {required String email, bool? shouldRecoverAuth}) async {
+  Future<GoogleSignInTokenData> getTokens({
+    required String email,
+    bool? shouldRecoverAuth,
+  }) async {
     await initialized;
 
-    final auth2.GoogleUser? currentUser =
-        auth2.getAuthInstance()?.currentUser?.get();
-    final auth2.AuthResponse? response = currentUser?.getAuthResponse();
-
-    return GoogleSignInTokenData(
-        idToken: response?.id_token, accessToken: response?.access_token);
+    return _gisClient.getTokens();
   }
 
   @override
   Future<void> signOut() async {
     await initialized;
 
-    return auth2.getAuthInstance()?.signOut();
+    _gisClient.signOut();
   }
 
   @override
   Future<void> disconnect() async {
     await initialized;
 
-    final auth2.GoogleUser? currentUser =
-        auth2.getAuthInstance()?.currentUser?.get();
-
-    if (currentUser == null) {
-      return;
-    }
-
-    return currentUser.disconnect();
+    _gisClient.disconnect();
   }
 
   @override
   Future<bool> isSignedIn() async {
     await initialized;
 
-    final auth2.GoogleUser? currentUser =
-        auth2.getAuthInstance()?.currentUser?.get();
-
-    if (currentUser == null) {
-      return false;
-    }
-
-    return currentUser.isSignedIn();
+    return _gisClient.isSignedIn();
   }
 
   @override
   Future<void> clearAuthCache({required String token}) async {
     await initialized;
 
-    return auth2.getAuthInstance()?.disconnect();
+    _gisClient.clearAuthCache();
   }
 
   @override
   Future<bool> requestScopes(List<String> scopes) async {
     await initialized;
 
-    final auth2.GoogleUser? currentUser =
-        auth2.getAuthInstance()?.currentUser?.get();
-
-    if (currentUser == null) {
-      return false;
-    }
-
-    final String grantedScopes = currentUser.getGrantedScopes() ?? '';
-    final Iterable<String> missingScopes =
-        scopes.where((String scope) => !grantedScopes.contains(scope));
-
-    if (missingScopes.isEmpty) {
-      return true;
-    }
-
-    final Object? response = await currentUser
-        .grant(auth2.SigninOptions(scope: missingScopes.join(' ')));
-
-    return response != null;
+    return _gisClient.requestScopes(scopes);
   }
 }
diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart
new file mode 100644
index 0000000..3815322
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart
@@ -0,0 +1,310 @@
+// 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.
+import 'dart:async';
+
+// TODO(dit): Split `id` and `oauth2` "services" for mocking. https://github.com/flutter/flutter/issues/120657
+import 'package:google_identity_services_web/id.dart';
+import 'package:google_identity_services_web/oauth2.dart';
+import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
+// ignore: unnecessary_import
+import 'package:js/js.dart';
+import 'package:js/js_util.dart';
+
+import 'people.dart' as people;
+import 'utils.dart' as utils;
+
+/// A client to hide (most) of the interaction with the GIS SDK from the plugin.
+///
+/// (Overridable for testing)
+class GisSdkClient {
+  /// Create a GisSdkClient object.
+  GisSdkClient({
+    required List<String> initialScopes,
+    required String clientId,
+    bool loggingEnabled = false,
+    String? hostedDomain,
+  }) : _initialScopes = initialScopes {
+    if (loggingEnabled) {
+      id.setLogLevel('debug');
+    }
+    // Configure the Stream objects that are going to be used by the clients.
+    _configureStreams();
+
+    // Initialize the SDK clients we need.
+    _initializeIdClient(
+      clientId,
+      onResponse: _onCredentialResponse,
+    );
+
+    _tokenClient = _initializeTokenClient(
+      clientId,
+      hostedDomain: hostedDomain,
+      onResponse: _onTokenResponse,
+      onError: _onTokenError,
+    );
+  }
+
+  // Configure the credential (authentication) and token (authorization) response streams.
+  void _configureStreams() {
+    _tokenResponses = StreamController<TokenResponse>.broadcast();
+    _credentialResponses = StreamController<CredentialResponse>.broadcast();
+    _tokenResponses.stream.listen((TokenResponse response) {
+      _lastTokenResponse = response;
+    }, onError: (Object error) {
+      _lastTokenResponse = null;
+    });
+    _credentialResponses.stream.listen((CredentialResponse response) {
+      _lastCredentialResponse = response;
+    }, onError: (Object error) {
+      _lastCredentialResponse = null;
+    });
+  }
+
+  // Initializes the `id` SDK for the silent-sign in (authentication) client.
+  void _initializeIdClient(
+    String clientId, {
+    required CallbackFn onResponse,
+  }) {
+    // Initialize `id` for the silent-sign in code.
+    final IdConfiguration idConfig = IdConfiguration(
+      client_id: clientId,
+      callback: allowInterop(onResponse),
+      cancel_on_tap_outside: false,
+      auto_select: true, // Attempt to sign-in silently.
+    );
+    id.initialize(idConfig);
+  }
+
+  // Handle a "normal" credential (authentication) response.
+  //
+  // (Normal doesn't mean successful, this might contain `error` information.)
+  void _onCredentialResponse(CredentialResponse response) {
+    if (response.error != null) {
+      _credentialResponses.addError(response.error!);
+    } else {
+      _credentialResponses.add(response);
+    }
+  }
+
+  // Creates a `oauth2.TokenClient` used for authorization (scope) requests.
+  TokenClient _initializeTokenClient(
+    String clientId, {
+    String? hostedDomain,
+    required TokenClientCallbackFn onResponse,
+    required ErrorCallbackFn onError,
+  }) {
+    // Create a Token Client for authorization calls.
+    final TokenClientConfig tokenConfig = TokenClientConfig(
+      client_id: clientId,
+      hosted_domain: hostedDomain,
+      callback: allowInterop(_onTokenResponse),
+      error_callback: allowInterop(_onTokenError),
+      // `scope` will be modified by the `signIn` method, in case we need to
+      // backfill user Profile info.
+      scope: ' ',
+    );
+    return oauth2.initTokenClient(tokenConfig);
+  }
+
+  // Handle a "normal" token (authorization) response.
+  //
+  // (Normal doesn't mean successful, this might contain `error` information.)
+  void _onTokenResponse(TokenResponse response) {
+    if (response.error != null) {
+      _tokenResponses.addError(response.error!);
+    } else {
+      _tokenResponses.add(response);
+    }
+  }
+
+  // Handle a "not-directly-related-to-authorization" error.
+  //
+  // Token clients have an additional `error_callback` for miscellaneous
+  // errors, like "popup couldn't open" or "popup closed by user".
+  void _onTokenError(Object? error) {
+    // This is handled in a funky (js_interop) way because of:
+    // https://github.com/dart-lang/sdk/issues/50899
+    _tokenResponses.addError(getProperty(error!, 'type'));
+  }
+
+  /// Attempts to sign-in the user using the OneTap UX flow.
+  ///
+  /// If the user consents, to OneTap, the [GoogleSignInUserData] will be
+  /// generated from a proper [CredentialResponse], which contains `idToken`.
+  /// Else, it'll be synthesized by a request to the People API later, and the
+  /// `idToken` will be null.
+  Future<GoogleSignInUserData?> signInSilently() async {
+    final Completer<GoogleSignInUserData?> userDataCompleter =
+        Completer<GoogleSignInUserData?>();
+
+    // Ask the SDK to render the OneClick sign-in.
+    //
+    // And also handle its "moments".
+    id.prompt(allowInterop((PromptMomentNotification moment) {
+      _onPromptMoment(moment, userDataCompleter);
+    }));
+
+    return userDataCompleter.future;
+  }
+
+  // Handles "prompt moments" of the OneClick card UI.
+  //
+  // See: https://developers.google.com/identity/gsi/web/guides/receive-notifications-prompt-ui-status
+  Future<void> _onPromptMoment(
+    PromptMomentNotification moment,
+    Completer<GoogleSignInUserData?> completer,
+  ) async {
+    if (completer.isCompleted) {
+      return; // Skip once the moment has been handled.
+    }
+
+    if (moment.isDismissedMoment() &&
+        moment.getDismissedReason() ==
+            MomentDismissedReason.credential_returned) {
+      // Kick this part of the handler to the bottom of the JS event queue, so
+      // the _credentialResponses stream has time to propagate its last value,
+      // and we can use _lastCredentialResponse.
+      return Future<void>.delayed(Duration.zero, () {
+        completer
+            .complete(utils.gisResponsesToUserData(_lastCredentialResponse));
+      });
+    }
+
+    // In any other 'failed' moments, return null and add an error to the stream.
+    if (moment.isNotDisplayed() ||
+        moment.isSkippedMoment() ||
+        moment.isDismissedMoment()) {
+      final String reason = moment.getNotDisplayedReason()?.toString() ??
+          moment.getSkippedReason()?.toString() ??
+          moment.getDismissedReason()?.toString() ??
+          'unknown_error';
+
+      _credentialResponses.addError(reason);
+      completer.complete(null);
+    }
+  }
+
+  /// Starts an oauth2 "implicit" flow to authorize requests.
+  ///
+  /// The new GIS SDK does not return user authentication from this flow, so:
+  ///   * If [_lastCredentialResponse] is **not** null (the user has successfully
+  ///     `signInSilently`), we return that after this method completes.
+  ///   * If [_lastCredentialResponse] is null, we add [people.scopes] to the
+  ///     [_initialScopes], so we can retrieve User Profile information back
+  ///     from the People API (without idToken). See [people.requestUserData].
+  Future<GoogleSignInUserData?> signIn() async {
+    // If we already know the user, use their `email` as a `hint`, so they don't
+    // have to pick their user again in the Authorization popup.
+    final GoogleSignInUserData? knownUser =
+        utils.gisResponsesToUserData(_lastCredentialResponse);
+    // This toggles a popup, so `signIn` *must* be called with
+    // user activation.
+    _tokenClient.requestAccessToken(OverridableTokenClientConfig(
+      prompt: knownUser == null ? 'select_account' : '',
+      hint: knownUser?.email,
+      scope: <String>[
+        ..._initialScopes,
+        // If the user hasn't gone through the auth process,
+        // the plugin will attempt to `requestUserData` after,
+        // so we need extra scopes to retrieve that info.
+        if (_lastCredentialResponse == null) ...people.scopes,
+      ].join(' '),
+    ));
+
+    await _tokenResponses.stream.first;
+
+    return _computeUserDataForLastToken();
+  }
+
+  // This function returns the currently signed-in [GoogleSignInUserData].
+  //
+  // It'll do a request to the People API (if needed).
+  Future<GoogleSignInUserData?> _computeUserDataForLastToken() async {
+    // If the user hasn't authenticated, request their basic profile info
+    // from the People API.
+    //
+    // This synthetic response will *not* contain an `idToken` field.
+    if (_lastCredentialResponse == null && _requestedUserData == null) {
+      assert(_lastTokenResponse != null);
+      _requestedUserData = await people.requestUserData(_lastTokenResponse!);
+    }
+    // Complete user data either with the _lastCredentialResponse seen,
+    // or the synthetic _requestedUserData from above.
+    return utils.gisResponsesToUserData(_lastCredentialResponse) ??
+        _requestedUserData;
+  }
+
+  /// Returns a [GoogleSignInTokenData] from the latest seen responses.
+  GoogleSignInTokenData getTokens() {
+    return utils.gisResponsesToTokenData(
+      _lastCredentialResponse,
+      _lastTokenResponse,
+    );
+  }
+
+  /// Revokes the current authentication.
+  Future<void> signOut() async {
+    clearAuthCache();
+    id.disableAutoSelect();
+  }
+
+  /// Revokes the current authorization and authentication.
+  Future<void> disconnect() async {
+    if (_lastTokenResponse != null) {
+      oauth2.revoke(_lastTokenResponse!.access_token);
+    }
+    signOut();
+  }
+
+  /// Returns true if the client has recognized this user before.
+  Future<bool> isSignedIn() async {
+    return _lastCredentialResponse != null || _requestedUserData != null;
+  }
+
+  /// Clears all the cached results from authentication and authorization.
+  Future<void> clearAuthCache() async {
+    _lastCredentialResponse = null;
+    _lastTokenResponse = null;
+    _requestedUserData = null;
+  }
+
+  /// Requests the list of [scopes] passed in to the client.
+  ///
+  /// Keeps the previously granted scopes.
+  Future<bool> requestScopes(List<String> scopes) async {
+    _tokenClient.requestAccessToken(OverridableTokenClientConfig(
+      scope: scopes.join(' '),
+      include_granted_scopes: true,
+    ));
+
+    await _tokenResponses.stream.first;
+
+    return oauth2.hasGrantedAllScopes(_lastTokenResponse!, scopes);
+  }
+
+  // The scopes initially requested by the developer.
+  //
+  // We store this because we might need to add more at `signIn`. If the user
+  // doesn't `silentSignIn`, we expand this list to consult the People API to
+  // return some basic Authentication information.
+  final List<String> _initialScopes;
+
+  // The Google Identity Services client for oauth requests.
+  late TokenClient _tokenClient;
+
+  // Streams of credential and token responses.
+  late StreamController<CredentialResponse> _credentialResponses;
+  late StreamController<TokenResponse> _tokenResponses;
+
+  // The last-seen credential and token responses
+  CredentialResponse? _lastCredentialResponse;
+  TokenResponse? _lastTokenResponse;
+
+  // If the user *authenticates* (signs in) through oauth2, the SDK doesn't return
+  // identity information anymore, so we synthesize it by calling the PeopleAPI
+  // (if needed)
+  //
+  // (This is a synthetic _lastCredentialResponse)
+  GoogleSignInUserData? _requestedUserData;
+}
diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapi.dart
deleted file mode 100644
index 3be4b2d..0000000
--- a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapi.dart
+++ /dev/null
@@ -1,56 +0,0 @@
-// 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.
-
-/// Type definitions for Google API Client
-/// Project: https://github.com/google/google-api-javascript-client
-/// Definitions by: Frank M <https://github.com/sgtfrankieboy>, grant <https://github.com/grant>
-/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
-/// TypeScript Version: 2.3
-
-// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi
-
-// ignore_for_file: public_member_api_docs,
-// * public_member_api_docs originally undocumented because the file was
-//   autogenerated.
-
-@JS()
-library gapi;
-
-import 'package:js/js.dart';
-
-// Module gapi
-typedef LoadCallback = void Function(
-    [dynamic args1,
-    dynamic args2,
-    dynamic args3,
-    dynamic args4,
-    dynamic args5]);
-
-@anonymous
-@JS()
-abstract class LoadConfig {
-  external factory LoadConfig(
-      {LoadCallback callback,
-      Function? onerror,
-      num? timeout,
-      Function? ontimeout});
-  external LoadCallback get callback;
-  external set callback(LoadCallback v);
-  external Function? get onerror;
-  external set onerror(Function? v);
-  external num? get timeout;
-  external set timeout(num? v);
-  external Function? get ontimeout;
-  external set ontimeout(Function? v);
-}
-
-/*type CallbackOrConfig = LoadConfig | LoadCallback;*/
-/// Pragmatically initialize gapi class member.
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiloadlibraries-callbackorconfig
-@JS('gapi.load')
-external void load(
-    String apiName, dynamic /*LoadConfig|LoadCallback*/ callback);
-// End module gapi
-
-// Manually removed gapi.auth and gapi.client, unused by this plugin.
diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapiauth2.dart
deleted file mode 100644
index 35a2d08..0000000
--- a/packages/google_sign_in/google_sign_in_web/lib/src/js_interop/gapiauth2.dart
+++ /dev/null
@@ -1,497 +0,0 @@
-// 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.
-
-/// Type definitions for non-npm package Google Sign-In API 0.0
-/// Project: https://developers.google.com/identity/sign-in/web/
-/// Definitions by: Derek Lawless <https://github.com/flawless2011>
-/// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
-/// TypeScript Version: 2.3
-
-/// <reference types="gapi" />
-
-// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi.auth2
-
-// ignore_for_file: public_member_api_docs, non_constant_identifier_names,
-// * public_member_api_docs originally undocumented because the file was
-//   autogenerated.
-// * non_constant_identifier_names required to be able to use the same parameter
-//   names as the underlying library.
-
-@JS()
-library gapiauth2;
-
-import 'package:js/js.dart';
-import 'package:js/js_util.dart' show promiseToFuture;
-
-@anonymous
-@JS()
-class GoogleAuthInitFailureError {
-  external String get error;
-  external set error(String? value);
-
-  external String get details;
-  external set details(String? value);
-}
-
-@anonymous
-@JS()
-class GoogleAuthSignInError {
-  external String get error;
-  external set error(String value);
-}
-
-@anonymous
-@JS()
-class OfflineAccessResponse {
-  external String? get code;
-  external set code(String? value);
-}
-
-// Module gapi.auth2
-/// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account,
-/// get the user's current sign-in status, get specific data from the user's Google profile,
-/// request additional scopes, and sign out from the current account.
-@JS('gapi.auth2.GoogleAuth')
-class GoogleAuth {
-  external IsSignedIn get isSignedIn;
-  external set isSignedIn(IsSignedIn v);
-  external CurrentUser? get currentUser;
-  external set currentUser(CurrentUser? v);
-
-  /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if
-  /// initialization fails.
-  external dynamic then(dynamic Function(GoogleAuth googleAuth) onInit,
-      [dynamic Function(GoogleAuthInitFailureError reason) onFailure]);
-
-  /// Signs out all accounts from the application.
-  external dynamic signOut();
-
-  /// Revokes all of the scopes that the user granted.
-  external dynamic disconnect();
-
-  /// Attaches the sign-in flow to the specified container's click handler.
-  external dynamic attachClickHandler(
-      dynamic container,
-      SigninOptions options,
-      dynamic Function(GoogleUser googleUser) onsuccess,
-      dynamic Function(String reason) onfailure);
-}
-
-@anonymous
-@JS()
-abstract class _GoogleAuth {
-  external Promise<GoogleUser> signIn(
-      [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]);
-  external Promise<OfflineAccessResponse> grantOfflineAccess(
-      [OfflineAccessOptions? options]);
-}
-
-extension GoogleAuthExtensions on GoogleAuth {
-  Future<GoogleUser> signIn(
-      [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) {
-    final _GoogleAuth tt = this as _GoogleAuth;
-    return promiseToFuture(tt.signIn(options));
-  }
-
-  Future<OfflineAccessResponse> grantOfflineAccess(
-      [OfflineAccessOptions? options]) {
-    final _GoogleAuth tt = this as _GoogleAuth;
-    return promiseToFuture(tt.grantOfflineAccess(options));
-  }
-}
-
-@anonymous
-@JS()
-abstract class IsSignedIn {
-  /// Returns whether the current user is currently signed in.
-  external bool get();
-
-  /// Listen for changes in the current user's sign-in state.
-  external void listen(dynamic Function(bool signedIn) listener);
-}
-
-@anonymous
-@JS()
-abstract class CurrentUser {
-  /// Returns a GoogleUser object that represents the current user. Note that in a newly-initialized
-  /// GoogleAuth instance, the current user has not been set. Use the currentUser.listen() method or the
-  /// GoogleAuth.then() to get an initialized GoogleAuth instance.
-  external GoogleUser get();
-
-  /// Listen for changes in currentUser.
-  external void listen(dynamic Function(GoogleUser user) listener);
-}
-
-@anonymous
-@JS()
-abstract class SigninOptions {
-  external factory SigninOptions(
-      {String app_package_name,
-      bool fetch_basic_profile,
-      String prompt,
-      String scope,
-      String /*'popup'|'redirect'*/ ux_mode,
-      String redirect_uri,
-      String login_hint});
-
-  /// The package name of the Android app to install over the air.
-  /// See Android app installs from your web site:
-  /// https://developers.google.com/identity/sign-in/web/android-app-installs
-  external String? get app_package_name;
-  external set app_package_name(String? v);
-
-  /// Fetch users' basic profile information when they sign in.
-  /// Adds 'profile', 'email' and 'openid' to the requested scopes.
-  /// True if unspecified.
-  external bool? get fetch_basic_profile;
-  external set fetch_basic_profile(bool? v);
-
-  /// Specifies whether to prompt the user for re-authentication.
-  /// See OpenID Connect Request Parameters:
-  /// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters
-  external String? get prompt;
-  external set prompt(String? v);
-
-  /// The scopes to request, as a space-delimited string.
-  /// Optional if fetch_basic_profile is not set to false.
-  external String? get scope;
-  external set scope(String? v);
-
-  /// The UX mode to use for the sign-in flow.
-  /// By default, it will open the consent flow in a popup.
-  external String? /*'popup'|'redirect'*/ get ux_mode;
-  external set ux_mode(String? /*'popup'|'redirect'*/ v);
-
-  /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow.
-  /// The default redirect_uri is the current URL stripped of query parameters and hash fragment.
-  external String? get redirect_uri;
-  external set redirect_uri(String? v);
-
-  // When your app knows which user it is trying to authenticate, it can provide this parameter as a hint to the authentication server.
-  // Passing this hint suppresses the account chooser and either pre-fill the email box on the sign-in form, or select the proper session (if the user is using multiple sign-in),
-  // which can help you avoid problems that occur if your app logs in the wrong user account. The value can be either an email address or the sub string,
-  // which is equivalent to the user's Google ID.
-  // https://developers.google.com/identity/protocols/OpenIDConnect?hl=en#authenticationuriparameters
-  external String? get login_hint;
-  external set login_hint(String? v);
-}
-
-/// Definitions by: John <https://github.com/jhcao23>
-/// Interface that represents the different configuration parameters for the GoogleAuth.grantOfflineAccess(options) method.
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2offlineaccessoptions
-@anonymous
-@JS()
-abstract class OfflineAccessOptions {
-  external factory OfflineAccessOptions(
-      {String scope,
-      String /*'select_account'|'consent'*/ prompt,
-      String app_package_name});
-  external String? get scope;
-  external set scope(String? v);
-  external String? /*'select_account'|'consent'*/ get prompt;
-  external set prompt(String? /*'select_account'|'consent'*/ v);
-  external String? get app_package_name;
-  external set app_package_name(String? v);
-}
-
-/// Interface that represents the different configuration parameters for the gapi.auth2.init method.
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2clientconfig
-@anonymous
-@JS()
-abstract class ClientConfig {
-  external factory ClientConfig({
-    String client_id,
-    String cookie_policy,
-    String scope,
-    bool fetch_basic_profile,
-    String? hosted_domain,
-    String openid_realm,
-    String /*'popup'|'redirect'*/ ux_mode,
-    String redirect_uri,
-    String plugin_name,
-  });
-
-  /// The app's client ID, found and created in the Google Developers Console.
-  external String? get client_id;
-  external set client_id(String? v);
-
-  /// The domains for which to create sign-in cookies. Either a URI, single_host_origin, or none.
-  /// Defaults to single_host_origin if unspecified.
-  external String? get cookie_policy;
-  external set cookie_policy(String? v);
-
-  /// The scopes to request, as a space-delimited string. Optional if fetch_basic_profile is not set to false.
-  external String? get scope;
-  external set scope(String? v);
-
-  /// Fetch users' basic profile information when they sign in. Adds 'profile' and 'email' to the requested scopes. True if unspecified.
-  external bool? get fetch_basic_profile;
-  external set fetch_basic_profile(bool? v);
-
-  /// The Google Apps domain to which users must belong to sign in. This is susceptible to modification by clients,
-  /// so be sure to verify the hosted domain property of the returned user. Use GoogleUser.getHostedDomain() on the client,
-  /// and the hd claim in the ID Token on the server to verify the domain is what you expected.
-  external String? get hosted_domain;
-  external set hosted_domain(String? v);
-
-  /// Used only for OpenID 2.0 client migration. Set to the value of the realm that you are currently using for OpenID 2.0,
-  /// as described in <a href="https://developers.google.com/accounts/docs/OpenID#openid-connect">OpenID 2.0 (Migration)</a>.
-  external String? get openid_realm;
-  external set openid_realm(String? v);
-
-  /// The UX mode to use for the sign-in flow.
-  /// By default, it will open the consent flow in a popup.
-  external String? /*'popup'|'redirect'*/ get ux_mode;
-  external set ux_mode(String? /*'popup'|'redirect'*/ v);
-
-  /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow.
-  /// The default redirect_uri is the current URL stripped of query parameters and hash fragment.
-  external String? get redirect_uri;
-  external set redirect_uri(String? v);
-
-  /// Allows newly created Client IDs to use the Google Platform Library from now until the March 30th, 2023 deprecation date.
-  /// See: https://github.com/flutter/flutter/issues/88084
-  external String? get plugin_name;
-  external set plugin_name(String? v);
-}
-
-@JS('gapi.auth2.SigninOptionsBuilder')
-class SigninOptionsBuilder {
-  external dynamic setAppPackageName(String name);
-  external dynamic setFetchBasicProfile(bool fetch);
-  external dynamic setPrompt(String prompt);
-  external dynamic setScope(String scope);
-  external dynamic setLoginHint(String hint);
-}
-
-@anonymous
-@JS()
-abstract class BasicProfile {
-  external String? getId();
-  external String? getName();
-  external String? getGivenName();
-  external String? getFamilyName();
-  external String? getImageUrl();
-  external String? getEmail();
-}
-
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authresponse
-@anonymous
-@JS()
-abstract class AuthResponse {
-  external String? get access_token;
-  external set access_token(String? v);
-  external String? get id_token;
-  external set id_token(String? v);
-  external String? get login_hint;
-  external set login_hint(String? v);
-  external String? get scope;
-  external set scope(String? v);
-  external num? get expires_in;
-  external set expires_in(num? v);
-  external num? get first_issued_at;
-  external set first_issued_at(num? v);
-  external num? get expires_at;
-  external set expires_at(num? v);
-}
-
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig
-@anonymous
-@JS()
-abstract class AuthorizeConfig {
-  external factory AuthorizeConfig(
-      {String client_id,
-      String scope,
-      String response_type,
-      String prompt,
-      String cookie_policy,
-      String hosted_domain,
-      String login_hint,
-      String app_package_name,
-      String openid_realm,
-      bool include_granted_scopes});
-  external String get client_id;
-  external set client_id(String v);
-  external String get scope;
-  external set scope(String v);
-  external String? get response_type;
-  external set response_type(String? v);
-  external String? get prompt;
-  external set prompt(String? v);
-  external String? get cookie_policy;
-  external set cookie_policy(String? v);
-  external String? get hosted_domain;
-  external set hosted_domain(String? v);
-  external String? get login_hint;
-  external set login_hint(String? v);
-  external String? get app_package_name;
-  external set app_package_name(String? v);
-  external String? get openid_realm;
-  external set openid_realm(String? v);
-  external bool? get include_granted_scopes;
-  external set include_granted_scopes(bool? v);
-}
-
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeresponse
-@anonymous
-@JS()
-abstract class AuthorizeResponse {
-  external factory AuthorizeResponse(
-      {String access_token,
-      String id_token,
-      String code,
-      String scope,
-      num expires_in,
-      num first_issued_at,
-      num expires_at,
-      String error,
-      String error_subtype});
-  external String get access_token;
-  external set access_token(String v);
-  external String get id_token;
-  external set id_token(String v);
-  external String get code;
-  external set code(String v);
-  external String get scope;
-  external set scope(String v);
-  external num get expires_in;
-  external set expires_in(num v);
-  external num get first_issued_at;
-  external set first_issued_at(num v);
-  external num get expires_at;
-  external set expires_at(num v);
-  external String get error;
-  external set error(String v);
-  external String get error_subtype;
-  external set error_subtype(String v);
-}
-
-/// A GoogleUser object represents one user account.
-@anonymous
-@JS()
-abstract class GoogleUser {
-  /// Get the user's unique ID string.
-  external String? getId();
-
-  /// Returns true if the user is signed in.
-  external bool isSignedIn();
-
-  /// Get the user's Google Apps domain if the user signed in with a Google Apps account.
-  external String? getHostedDomain();
-
-  /// Get the scopes that the user granted as a space-delimited string.
-  external String? getGrantedScopes();
-
-  /// Get the user's basic profile information.
-  external BasicProfile? getBasicProfile();
-
-  /// Get the response object from the user's auth session.
-  // This returns an empty JS object when the user hasn't attempted to sign in.
-  external AuthResponse getAuthResponse([bool includeAuthorizationData]);
-
-  /// Returns true if the user granted the specified scopes.
-  external bool hasGrantedScopes(String scopes);
-
-  // Has the API for grant and grantOfflineAccess changed?
-  /// Request additional scopes to the user.
-  ///
-  /// See GoogleAuth.signIn() for the list of parameters and the error code.
-  external dynamic grant(
-      [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]);
-
-  /// Get permission from the user to access the specified scopes offline.
-  /// When you use GoogleUser.grantOfflineAccess(), the sign-in flow skips the account chooser step.
-  /// See GoogleUser.grantOfflineAccess().
-  external void grantOfflineAccess(String scopes);
-
-  /// Revokes all of the scopes that the user granted.
-  external void disconnect();
-}
-
-@anonymous
-@JS()
-abstract class _GoogleUser {
-  /// Forces a refresh of the access token, and then returns a Promise for the new AuthResponse.
-  external Promise<AuthResponse> reloadAuthResponse();
-}
-
-extension GoogleUserExtensions on GoogleUser {
-  Future<AuthResponse> reloadAuthResponse() {
-    final _GoogleUser tt = this as _GoogleUser;
-    return promiseToFuture(tt.reloadAuthResponse());
-  }
-}
-
-/// Initializes the GoogleAuth object.
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2initparams
-@JS('gapi.auth2.init')
-external GoogleAuth init(ClientConfig params);
-
-/// Returns the GoogleAuth object. You must initialize the GoogleAuth object with gapi.auth2.init() before calling this method.
-@JS('gapi.auth2.getAuthInstance')
-external GoogleAuth? getAuthInstance();
-
-/// Performs a one time OAuth 2.0 authorization.
-/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback
-@JS('gapi.auth2.authorize')
-external void authorize(
-    AuthorizeConfig params, void Function(AuthorizeResponse response) callback);
-// End module gapi.auth2
-
-// Module gapi.signin2
-@JS('gapi.signin2.render')
-external void render(
-    dynamic id,
-    dynamic
-        /*{
-    /**
-     * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation.
-     */
-    scope?: string;
-
-    /**
-     * The width of the button in pixels (default: 120).
-     */
-    width?: number;
-
-    /**
-     * The height of the button in pixels (default: 36).
-     */
-    height?: number;
-
-    /**
-     * Display long labels such as "Sign in with Google" rather than "Sign in" (default: false).
-     */
-    longtitle?: boolean;
-
-    /**
-     * The color theme of the button: either light or dark (default: light).
-     */
-    theme?: string;
-
-    /**
-     * The callback function to call when a user successfully signs in (default: none).
-     */
-    onsuccess?(user: auth2.GoogleUser): void;
-
-    /**
-     * The callback function to call when sign-in fails (default: none).
-     */
-    onfailure?(reason: { error: string }): void;
-
-    /**
-     * The package name of the Android app to install over the air. See
-     * <a href="https://developers.google.com/identity/sign-in/web/android-app-installs">Android app installs from your web site</a>.
-     * Optional. (default: none)
-     */
-    app_package_name?: string;
-  }*/
-        options);
-
-// End module gapi.signin2
-@JS()
-abstract class Promise<T> {
-  external factory Promise(
-      void Function(void Function(T result) resolve, Function reject) executor);
-}
diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart
deleted file mode 100644
index 57b9183..0000000
--- a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-// 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.
-
-@JS()
-library gapi_onload;
-
-import 'dart:async';
-
-import 'package:flutter/foundation.dart' show visibleForTesting;
-import 'package:js/js.dart';
-
-import 'js_interop/gapi.dart' as gapi;
-import 'utils.dart' show injectJSLibraries;
-
-@JS()
-external set gapiOnloadCallback(Function callback);
-
-// This name must match the external setter above
-/// This is only exposed for testing. It shouldn't be accessed by users of the
-/// plugin as it could break at any point.
-@visibleForTesting
-const String kGapiOnloadCallbackFunctionName = 'gapiOnloadCallback';
-String _addOnloadToScript(String url) => url.startsWith('data:')
-    ? url
-    : '$url?onload=$kGapiOnloadCallbackFunctionName';
-
-/// Injects the GAPI library by its [url], and other additional [libraries].
-///
-/// GAPI has an onload API where it'll call a callback when it's ready, JSONP style.
-Future<void> inject(String url, {List<String> libraries = const <String>[]}) {
-  // Inject the GAPI library, and configure the onload global
-  final Completer<void> gapiOnLoad = Completer<void>();
-  gapiOnloadCallback = allowInterop(() {
-    // Funnel the GAPI onload to a Dart future
-    gapiOnLoad.complete();
-  });
-
-  // Attach the onload callback to the main url
-  final List<String> allLibraries = <String>[
-    _addOnloadToScript(url),
-    ...libraries
-  ];
-
-  return Future.wait(
-      <Future<void>>[injectJSLibraries(allLibraries), gapiOnLoad.future]);
-}
-
-/// Initialize the global gapi object so 'auth2' can be used.
-/// Returns a promise that resolves when 'auth2' is ready.
-Future<void> init() {
-  final Completer<void> gapiLoadCompleter = Completer<void>();
-  gapi.load('auth2', allowInterop(() {
-    gapiLoadCompleter.complete();
-  }));
-
-  // After this resolves, we can use gapi.auth2!
-  return gapiLoadCompleter.future;
-}
diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/people.dart b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart
new file mode 100644
index 0000000..528dc89
--- /dev/null
+++ b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart
@@ -0,0 +1,152 @@
+// 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.
+
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:google_identity_services_web/oauth2.dart';
+import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
+import 'package:http/http.dart' as http;
+
+/// Basic scopes for self-id
+const List<String> scopes = <String>[
+  'https://www.googleapis.com/auth/userinfo.profile',
+  'https://www.googleapis.com/auth/userinfo.email',
+];
+
+/// People API to return my profile info...
+const String MY_PROFILE = 'https://content-people.googleapis.com/v1/people/me'
+    '?sources=READ_SOURCE_TYPE_PROFILE'
+    '&personFields=photos%2Cnames%2CemailAddresses';
+
+/// Requests user data from the People API using the given [tokenResponse].
+Future<GoogleSignInUserData?> requestUserData(
+  TokenResponse tokenResponse, {
+  @visibleForTesting http.Client? overrideClient,
+}) async {
+  // Request my profile from the People API.
+  final Map<String, Object?> person = await _doRequest(
+    MY_PROFILE,
+    tokenResponse,
+    overrideClient: overrideClient,
+  );
+
+  // Now transform the Person response into a GoogleSignInUserData.
+  return extractUserData(person);
+}
+
+/// Extracts user data from a Person resource.
+///
+/// See: https://developers.google.com/people/api/rest/v1/people#Person
+GoogleSignInUserData? extractUserData(Map<String, Object?> json) {
+  final String? userId = _extractUserId(json);
+  final String? email = _extractPrimaryField(
+    json['emailAddresses'] as List<Object?>?,
+    'value',
+  );
+
+  assert(userId != null);
+  assert(email != null);
+
+  return GoogleSignInUserData(
+    id: userId!,
+    email: email!,
+    displayName: _extractPrimaryField(
+      json['names'] as List<Object?>?,
+      'displayName',
+    ),
+    photoUrl: _extractPrimaryField(
+      json['photos'] as List<Object?>?,
+      'url',
+    ),
+    // Synthetic user data doesn't contain an idToken!
+  );
+}
+
+/// Extracts the ID from a Person resource.
+///
+/// The User ID looks like this:
+/// {
+///   'resourceName': 'people/PERSON_ID',
+///   ...
+/// }
+String? _extractUserId(Map<String, Object?> profile) {
+  final String? resourceName = profile['resourceName'] as String?;
+  return resourceName?.split('/').last;
+}
+
+/// Extracts the [fieldName] marked as 'primary' from a list of [values].
+///
+/// Values can be one of:
+/// * `emailAddresses`
+/// * `names`
+/// * `photos`
+///
+/// From a Person object.
+T? _extractPrimaryField<T>(List<Object?>? values, String fieldName) {
+  if (values != null) {
+    for (final Object? value in values) {
+      if (value != null && value is Map<String, Object?>) {
+        final bool isPrimary = _extractPath(
+          value,
+          path: <String>['metadata', 'primary'],
+          defaultValue: false,
+        );
+        if (isPrimary) {
+          return value[fieldName] as T?;
+        }
+      }
+    }
+  }
+
+  return null;
+}
+
+/// Attempts to get the property in [path] of type `T` from a deeply nested [source].
+///
+/// Returns [default] if the property is not found.
+T _extractPath<T>(
+  Map<String, Object?> source, {
+  required List<String> path,
+  required T defaultValue,
+}) {
+  final String valueKey = path.removeLast();
+  Object? data = source;
+  for (final String key in path) {
+    if (data != null && data is Map) {
+      data = data[key];
+    } else {
+      break;
+    }
+  }
+  if (data != null && data is Map) {
+    return (data[valueKey] ?? defaultValue) as T;
+  } else {
+    return defaultValue;
+  }
+}
+
+/// Gets from [url] with an authorization header defined by [token].
+///
+/// Attempts to [jsonDecode] the result.
+Future<Map<String, Object?>> _doRequest(
+  String url,
+  TokenResponse token, {
+  http.Client? overrideClient,
+}) async {
+  final Uri uri = Uri.parse(url);
+  final http.Client client = overrideClient ?? http.Client();
+  try {
+    final http.Response response =
+        await client.get(uri, headers: <String, String>{
+      'Authorization': '${token.token_type} ${token.access_token}',
+    });
+    if (response.statusCode != 200) {
+      throw http.ClientException(response.body, uri);
+    }
+    return jsonDecode(response.body) as Map<String, Object?>;
+  } finally {
+    client.close();
+  }
+}
diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart
index 45acb1f..c4bb9d4 100644
--- a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart
+++ b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart
@@ -2,59 +2,87 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'dart:async';
-import 'dart:html' as html;
+import 'dart:convert';
 
+import 'package:google_identity_services_web/id.dart';
+import 'package:google_identity_services_web/oauth2.dart';
 import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
 
-import 'js_interop/gapiauth2.dart' as auth2;
-
-/// Injects a list of JS [libraries] as `script` tags into a [target] [html.HtmlElement].
+/// A codec that can encode/decode JWT payloads.
 ///
-/// If [target] is not provided, it defaults to the web app's `head` tag (see `web/index.html`).
-/// [libraries] is a list of URLs that are used as the `src` attribute of `script` tags
-/// to which an `onLoad` listener is attached (one per URL).
+/// See https://www.rfc-editor.org/rfc/rfc7519#section-3
+final Codec<Object?, String> jwtCodec = json.fuse(utf8).fuse(base64);
+
+/// A RegExp that can match, and extract parts from a JWT Token.
 ///
-/// Returns a [Future] that resolves when all of the `script` tags `onLoad` events trigger.
-Future<void> injectJSLibraries(
-  List<String> libraries, {
-  html.HtmlElement? target,
-}) {
-  final List<Future<void>> loading = <Future<void>>[];
-  final List<html.HtmlElement> tags = <html.HtmlElement>[];
+/// A JWT token consists of 3 base-64 encoded parts of data separated by periods:
+///
+///   header.payload.signature
+///
+/// More info: https://regexr.com/789qc
+final RegExp jwtTokenRegexp = RegExp(
+    r'^(?<header>[^\.\s]+)\.(?<payload>[^\.\s]+)\.(?<signature>[^\.\s]+)$');
 
-  final html.Element targetElement = target ?? html.querySelector('head')!;
-
-  for (final String library in libraries) {
-    final html.ScriptElement script = html.ScriptElement()
-      ..async = true
-      ..defer = true
-      // ignore: unsafe_html
-      ..src = library;
-    // TODO(ditman): add a timeout race to fail this future
-    loading.add(script.onLoad.first);
-    tags.add(script);
+/// Decodes the `claims` of a JWT token and returns them as a Map.
+///
+/// JWT `claims` are stored as a JSON object in the `payload` part of the token.
+///
+/// (This method does not validate the signature of the token.)
+///
+/// See https://www.rfc-editor.org/rfc/rfc7519#section-3
+Map<String, Object?>? getJwtTokenPayload(String? token) {
+  if (token != null) {
+    final RegExpMatch? match = jwtTokenRegexp.firstMatch(token);
+    if (match != null) {
+      return decodeJwtPayload(match.namedGroup('payload'));
+    }
   }
 
-  targetElement.children.addAll(tags);
-  return Future.wait(loading);
+  return null;
 }
 
-/// Utility method that converts `currentUser` to the equivalent [GoogleSignInUserData].
+/// Decodes a JWT payload using the [jwtCodec].
+Map<String, Object?>? decodeJwtPayload(String? payload) {
+  try {
+    // Payload must be normalized before passing it to the codec
+    return jwtCodec.decode(base64.normalize(payload!)) as Map<String, Object?>?;
+  } catch (_) {
+    // Do nothing, we always return null for any failure.
+  }
+  return null;
+}
+
+/// Converts a [CredentialResponse] into a [GoogleSignInUserData].
 ///
-/// This method returns `null` when the [currentUser] is not signed in.
-GoogleSignInUserData? gapiUserToPluginUserData(auth2.GoogleUser? currentUser) {
-  final bool isSignedIn = currentUser?.isSignedIn() ?? false;
-  final auth2.BasicProfile? profile = currentUser?.getBasicProfile();
-  if (!isSignedIn || profile?.getId() == null) {
+/// May return `null`, if the `credentialResponse` is null, or its `credential`
+/// cannot be decoded.
+GoogleSignInUserData? gisResponsesToUserData(
+    CredentialResponse? credentialResponse) {
+  if (credentialResponse == null || credentialResponse.credential == null) {
+    return null;
+  }
+
+  final Map<String, Object?>? payload =
+      getJwtTokenPayload(credentialResponse.credential);
+
+  if (payload == null) {
     return null;
   }
 
   return GoogleSignInUserData(
-    displayName: profile?.getName(),
-    email: profile?.getEmail() ?? '',
-    id: profile?.getId() ?? '',
-    photoUrl: profile?.getImageUrl(),
-    idToken: currentUser?.getAuthResponse().id_token,
+    email: payload['email']! as String,
+    id: payload['sub']! as String,
+    displayName: payload['name']! as String,
+    photoUrl: payload['picture']! as String,
+    idToken: credentialResponse.credential,
+  );
+}
+
+/// Converts responses from the GIS library into TokenData for the plugin.
+GoogleSignInTokenData gisResponsesToTokenData(
+    CredentialResponse? credentialResponse, TokenResponse? tokenResponse) {
+  return GoogleSignInTokenData(
+    idToken: credentialResponse?.credential,
+    accessToken: tokenResponse?.access_token,
   );
 }
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 a9d3947..40e8b03 100644
--- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml
+++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml
@@ -3,10 +3,10 @@
   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.2+1
+version: 0.11.0
 
 environment:
-  sdk: ">=2.12.0 <3.0.0"
+  sdk: ">=2.17.0 <3.0.0"
   flutter: ">=3.0.0"
 
 flutter:
@@ -22,7 +22,9 @@
     sdk: flutter
   flutter_web_plugins:
     sdk: flutter
+  google_identity_services_web: ^0.2.0
   google_sign_in_platform_interface: ^2.2.0
+  http: ^0.13.5
   js: ^0.6.3
 
 dev_dependencies: