[gis_web] Introduce new GIS SDK JS-Interop package. (#2653)

diff --git a/.ci/scripts/custom_package_tests.sh b/.ci/scripts/custom_package_tests.sh
index 236300e..92d953b 100755
--- a/.ci/scripts/custom_package_tests.sh
+++ b/.ci/scripts/custom_package_tests.sh
@@ -5,9 +5,9 @@
 
 # Exclusions
 #
-# cross_file
+# script/configs/linux_only_custom_test.yaml
 #   Custom tests need Chrome. (They run in linux-custom_package_tests)
 
 dart pub global run flutter_plugin_tools custom-test \
    --packages-for-branch --log-timing \
-   --exclude=cross_file
+   --exclude=script/configs/linux_only_custom_test.yaml
diff --git a/.cirrus.yml b/.cirrus.yml
index fbd0689..1c059b6 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -261,12 +261,12 @@
       local_tests_script:
         # flutter_image
         #   https://github.com/flutter/flutter/issues/100387
-        # cross_file
-        #   Custom tests need Chrome. (They run in linux-custom_package_tests)
+        # script/configs/linux_only_custom_test.yaml
+        #   Custom tests need Chrome for these packages. (They run in linux-custom_package_tests)
         - if [[ "$CHANNEL" == "master" ]]; then
-        -   ./script/tool_runner.sh custom-test --exclude=cross_file
+        -   ./script/tool_runner.sh custom-test --exclude=script/configs/linux_only_custom_test.yaml
         - else
-        -   ./script/tool_runner.sh custom-test --exclude=cross_file,flutter_image
+        -   ./script/tool_runner.sh custom-test --exclude=flutter_image,script/configs/linux_only_custom_test.yaml
         - fi
     ### macOS desktop tasks ###
     - name: macos-platform_tests
diff --git a/.github/labeler.yml b/.github/labeler.yml
index e680066..b0553c8 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -37,6 +37,9 @@
 'p: go_router_builder':
   - packages/go_router_builder/**/*
 
+'p: google_identity_services':
+  - packages/google_indentity_services_web/**
+
 'p: imitation_game':
   - packages/imitation_game/**/*
 
diff --git a/packages/google_identity_services_web/AUTHORS b/packages/google_identity_services_web/AUTHORS
new file mode 100644
index 0000000..eb34297
--- /dev/null
+++ b/packages/google_identity_services_web/AUTHORS
@@ -0,0 +1,7 @@
+# Below is a list of people and organizations that have contributed
+# to the Flutter project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
+The Chromium Authors
diff --git a/packages/google_identity_services_web/CHANGELOG.md b/packages/google_identity_services_web/CHANGELOG.md
new file mode 100644
index 0000000..6073234
--- /dev/null
+++ b/packages/google_identity_services_web/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+* Initial release.
diff --git a/packages/google_identity_services_web/LICENSE b/packages/google_identity_services_web/LICENSE
new file mode 100644
index 0000000..c6823b8
--- /dev/null
+++ b/packages/google_identity_services_web/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2013 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/google_identity_services_web/README.md b/packages/google_identity_services_web/README.md
new file mode 100644
index 0000000..71bbb86
--- /dev/null
+++ b/packages/google_identity_services_web/README.md
@@ -0,0 +1,87 @@
+<?code-excerpt path-base="excerpts/packages/google_identity_services_web_example"?>
+
+# google_identity_services_web
+
+A JS-interop layer for Google Identity's Sign In With Google SDK.
+
+See the original JS SDK reference:
+
+* [Sign In With Google](https://developers.google.com/identity/gsi/web)
+
+## Usage
+
+This package is the Dart JS-interop layer of the new **Sign In With Google**
+SDK. Here's the API references for both of the sub-libraries:
+
+* `id.dart`: [Sign In With Google JavaScript API reference](https://developers.google.com/identity/gsi/web/reference/js-reference)
+* `oauth2.dart`: [Google 3P Authorization JavaScript Library for websites - API reference](https://developers.google.com/identity/oauth2/web/reference/js-reference)
+* `loader.dart`: An (optional) loader mechanism that installs the library and
+resolves a `Future<void>` when it's ready.
+
+### Loading the SDK
+
+There are two ways to load the JS SDK in your app.
+
+#### Modify your index.html (most performant)
+
+The most performant way is to modify your `web/index.html` file to insert a
+script tag [as recommended](https://developers.google.com/identity/gsi/web/guides/client-library).
+Place the `script` tag in the `<head>` of your site, next to the script tag that
+loads `flutter.js`, so the browser can downloaded both in parallel:
+
+<?code-excerpt "../../web/index-with-script-tag.html (script-tag)"?>
+```html
+<head>
+<!-- ··· -->
+  <!-- Include the GSI SDK below -->
+  <script src="https://accounts.google.com/gsi/client" async defer></script>
+  <!-- This script adds the flutter initialization JS code -->
+  <script src="flutter.js" defer></script>
+</head>
+```
+
+#### With the `loadWebSdk` function (on-demand)
+
+An alternative way, that downloads the SDK on demand, is to use the
+**`loadWebSdk`** function provided by the library. A simple location to embed
+this in a Flutter Web only app can be the `main.dart`:
+
+<?code-excerpt "main.dart (use-loader)"?>
+```dart
+import 'package:google_identity_services_web/loader.dart' as gis;
+// ···
+void main() async {
+  await gis.loadWebSdk(); // Load the GIS SDK
+  // The rest of your code...
+// ···
+}
+```
+
+(Note that the above won't compile for mobile apps, so if you're developing a
+cross-platform app, you'll probably need to hide the call to `loadWebSdk`
+behind a [conditional import/export](https://dart.dev/guides/libraries/create-library-packages#conditionally-importing-and-exporting-library-files).)
+
+### Using the SDK
+
+Once the SDK has been loaded, it can be used by importing the correct library:
+
+* `import 'package:google_identity_services/id.dart' as id;` for Authentication
+* `import 'package:google_identity_services/oauth2.dart' as oauth2;` for
+  Authorization.
+
+## Browser compatibility
+
+The new SDK is introducing concepts that are on track for standardization to
+most browsers, and it might not be compatible with older browsers.
+
+Refer to the official documentation site for the latest browser compatibility
+information of the underlying JS SDK:
+
+* **Sign In With Google > [Supported browsers and platforms](https://developers.google.com/identity/gsi/web/guides/supported-browsers)**
+
+## Testing
+
+This web-only package uses `dart:test` to test its features. They can be run
+with `dart test -p chrome`.
+
+_(Look at `test/README.md` and `tool/run_tests.dart` for more info.)_
diff --git a/packages/google_identity_services_web/dart_test.yaml b/packages/google_identity_services_web/dart_test.yaml
new file mode 100644
index 0000000..cdb656d
--- /dev/null
+++ b/packages/google_identity_services_web/dart_test.yaml
@@ -0,0 +1,6 @@
+# See https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#arguments
+override_platforms:
+  chrome:
+    settings:
+      executable: chrome
+      arguments: --no-sandbox
diff --git a/packages/google_identity_services_web/example/.gitignore b/packages/google_identity_services_web/example/.gitignore
new file mode 100644
index 0000000..24476c5
--- /dev/null
+++ b/packages/google_identity_services_web/example/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/google_identity_services_web/example/.metadata b/packages/google_identity_services_web/example/.metadata
new file mode 100644
index 0000000..9ff5c77
--- /dev/null
+++ b/packages/google_identity_services_web/example/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled.
+
+version:
+  revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+  channel: master
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+    - platform: android
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+    - platform: ios
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+    - platform: linux
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+    - platform: macos
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+    - platform: web
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+    - platform: windows
+      create_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+      base_revision: 822f6e3595d63f864ae2027ea37fd645b313b71b
+
+  # User provided section
+
+  # List of Local paths (relative to this file) that should be
+  # ignored by the migrate tool.
+  #
+  # Files that are not part of the templates will be ignored by default.
+  unmanaged_files:
+    - 'lib/main.dart'
+    - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/packages/google_identity_services_web/example/README.md b/packages/google_identity_services_web/example/README.md
new file mode 100644
index 0000000..a348670
--- /dev/null
+++ b/packages/google_identity_services_web/example/README.md
@@ -0,0 +1,4 @@
+# google_identity_services_web_example
+
+* `lib/main.dart`: An example on how to use the google_identiy_services_web `id` library from Dart.
+* `lib/main_oauth.dart`: An example for the `oauth2` library from the same SDK.
diff --git a/packages/google_identity_services_web/example/build.excerpt.yaml b/packages/google_identity_services_web/example/build.excerpt.yaml
new file mode 100644
index 0000000..8b673fd
--- /dev/null
+++ b/packages/google_identity_services_web/example/build.excerpt.yaml
@@ -0,0 +1,19 @@
+targets:
+  $default:
+    sources:
+      include:
+        - lib/**
+        - web/**
+        # Some default includes that aren't really used here but will prevent
+        # false-negative warnings:
+        - $package$
+        - lib/$lib$
+      exclude:
+        - '**/.*/**'
+        - '**/build/**'
+    builders:
+      code_excerpter|code_excerpter:
+        enabled: true
+        generate_for:
+          - '**/*.dart'
+          - '**/*.html'
diff --git a/packages/google_identity_services_web/example/lib/main.dart b/packages/google_identity_services_web/example/lib/main.dart
new file mode 100644
index 0000000..949eb51
--- /dev/null
+++ b/packages/google_identity_services_web/example/lib/main.dart
@@ -0,0 +1,56 @@
+// 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.
+
+// ignore_for_file: avoid_print
+
+import 'package:google_identity_services_web/id.dart' as id;
+// #docregion use-loader
+import 'package:google_identity_services_web/loader.dart' as gis;
+// #enddocregion use-loader
+import 'package:js/js.dart' show allowInterop;
+import 'package:jwt_decoder/jwt_decoder.dart' as jwt;
+
+// #docregion use-loader
+void main() async {
+  await gis.loadWebSdk(); // Load the GIS SDK
+  // The rest of your code...
+// #enddocregion use-loader
+  id.setLogLevel('debug');
+
+  final id.IdConfiguration config = id.IdConfiguration(
+    client_id: 'your-client_id.apps.googleusercontent.com',
+    ux_mode: id.UxMode.popup,
+    callback: allowInterop(onCredentialResponse),
+  );
+
+  id.initialize(config);
+  id.prompt(allowInterop(onPromptMoment));
+// #docregion use-loader
+}
+// #enddocregion use-loader
+
+/// Handles the ID token returned from the One Tap prompt.
+/// See: https://developers.google.com/identity/gsi/web/reference/js-reference#callback
+void onCredentialResponse(id.CredentialResponse o) {
+  final Map<String, dynamic>? payload = jwt.JwtDecoder.tryDecode(o.credential);
+  if (payload != null) {
+    print('Hello, ${payload["name"]}');
+    print(o.select_by);
+    print(payload);
+  } else {
+    print('Could not decode ${o.credential}');
+  }
+}
+
+/// Handles Prompt UI status notifications.
+/// See: https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt
+void onPromptMoment(id.PromptMomentNotification o) {
+  final id.MomentType type = o.getMomentType();
+  print(type.runtimeType);
+  print(type);
+  print(type.index);
+  print(o.getDismissedReason());
+  print(o.getNotDisplayedReason());
+  print(o.getSkippedReason());
+}
diff --git a/packages/google_identity_services_web/example/lib/main_oauth.dart b/packages/google_identity_services_web/example/lib/main_oauth.dart
new file mode 100644
index 0000000..041715f
--- /dev/null
+++ b/packages/google_identity_services_web/example/lib/main_oauth.dart
@@ -0,0 +1,79 @@
+// 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.
+
+// ignore_for_file: avoid_print
+
+import 'package:google_identity_services_web/id.dart' as id show setLogLevel;
+import 'package:google_identity_services_web/loader.dart' as gis;
+import 'package:google_identity_services_web/oauth2.dart' as oauth2;
+import 'package:http/http.dart' as http;
+import 'package:js/js.dart' show allowInterop;
+
+/// The scopes to be requested
+const List<String> scopes = <String>[
+  'email',
+  'profile',
+  'https://www.googleapis.com/auth/contacts.readonly',
+];
+
+void main() async {
+  await gis.loadWebSdk(); // Load the GIS SDK
+
+  id.setLogLevel('debug');
+
+  final oauth2.TokenClientConfig config = oauth2.TokenClientConfig(
+    client_id: 'your-client_id.apps.googleusercontent.com',
+    scope: scopes.join(' '),
+    callback: allowInterop(onTokenResponse),
+  );
+
+  final oauth2.OverridableTokenClientConfig overridableCfg =
+      oauth2.OverridableTokenClientConfig(
+    prompt: '',
+  );
+
+  final oauth2.TokenClient client = oauth2.initTokenClient(config);
+
+  // Disable the Popup Blocker for this to work, or move this to a Button press.
+  client.requestAccessToken(overridableCfg);
+}
+
+/// Handles the returned (auth) token response.
+/// See: https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse
+Future<void> onTokenResponse(oauth2.TokenResponse response) async {
+  if (response.error != null) {
+    print('Authorization error!');
+    print(response.error);
+    print(response.error_description);
+    print(response.error_uri);
+    return;
+  }
+
+  // Has granted all the scopes?
+  if (!oauth2.hasGrantedAllScopes(response, scopes[2])) {
+    print('The user has NOT granted the required scope!');
+    return;
+  }
+
+  // Attempt to do a request to the `people` API
+  final http.Response apiResponse = await http.get(
+    Uri.parse('https://people.googleapis.com/v1/people/me/connections'
+        '?requestMask.includeField=person.names'),
+    headers: <String, String>{
+      'Authorization': '${response.token_type} ${response.access_token}',
+    },
+  );
+  if (apiResponse.statusCode == 200) {
+    print('People API ${apiResponse.statusCode} OK!');
+  } else {
+    print(
+        'People API ${apiResponse.statusCode} Oops! Something wrong happened!');
+  }
+  print(apiResponse.body);
+
+  print('Revoking token...');
+  oauth2.revokeToken(response.access_token, allowInterop((String status) {
+    print(status);
+  }));
+}
diff --git a/packages/google_identity_services_web/example/pubspec.yaml b/packages/google_identity_services_web/example/pubspec.yaml
new file mode 100644
index 0000000..e7d6429
--- /dev/null
+++ b/packages/google_identity_services_web/example/pubspec.yaml
@@ -0,0 +1,24 @@
+name: google_identity_services_web_example
+description: An example for the google_identity_services_web package, OneTap.
+publish_to: 'none'
+version: 0.0.1
+
+environment:
+  sdk: '>=2.17.0 <3.0.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  google_identity_services_web:
+    path: ../
+  http: ^0.13.5
+  js: ^0.6.4
+  jwt_decoder: ^2.0.1
+
+dev_dependencies:
+  build_runner: ^2.1.10 # To extract README excerpts only.
+  flutter_test:
+    sdk: flutter
+
+flutter:
+  uses-material-design: true
diff --git a/packages/google_identity_services_web/example/web/favicon.png b/packages/google_identity_services_web/example/web/favicon.png
new file mode 100644
index 0000000..8aaa46a
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/favicon.png
Binary files differ
diff --git a/packages/google_identity_services_web/example/web/icons/Icon-192.png b/packages/google_identity_services_web/example/web/icons/Icon-192.png
new file mode 100644
index 0000000..b749bfe
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/icons/Icon-192.png
Binary files differ
diff --git a/packages/google_identity_services_web/example/web/icons/Icon-512.png b/packages/google_identity_services_web/example/web/icons/Icon-512.png
new file mode 100644
index 0000000..88cfd48
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/icons/Icon-512.png
Binary files differ
diff --git a/packages/google_identity_services_web/example/web/icons/Icon-maskable-192.png b/packages/google_identity_services_web/example/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..eb9b4d7
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/icons/Icon-maskable-192.png
Binary files differ
diff --git a/packages/google_identity_services_web/example/web/icons/Icon-maskable-512.png b/packages/google_identity_services_web/example/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..d69c566
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/icons/Icon-maskable-512.png
Binary files differ
diff --git a/packages/google_identity_services_web/example/web/index-with-script-tag.html b/packages/google_identity_services_web/example/web/index-with-script-tag.html
new file mode 100644
index 0000000..07441f7
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/index-with-script-tag.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html>
+<!--#docregion script-tag-->
+<head>
+<!--#enddocregion script-tag-->
+<base href="$FLUTTER_BASE_HREF">
+
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="A new Flutter project.">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="example">
+  <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+  <!-- Favicon -->
+  <link rel="icon" type="image/png" href="favicon.png"/>
+
+  <title>Authentication Example</title>
+  <link rel="manifest" href="manifest.json">
+
+  <script>
+    // The value below is injected by flutter build, do not touch.
+    var serviceWorkerVersion = null;
+  </script>
+<!--#docregion script-tag-->
+  <!-- Include the GSI SDK below -->
+  <script src="https://accounts.google.com/gsi/client" async defer></script>
+  <!-- This script adds the flutter initialization JS code -->
+  <script src="flutter.js" defer></script>
+</head>
+<!--#enddocregion script-tag-->
+<body>
+  <script>
+    window.addEventListener('load', function(ev) {
+      // Download main.dart.js
+      _flutter.loader.loadEntrypoint({
+        serviceWorker: {
+          serviceWorkerVersion: serviceWorkerVersion,
+        },
+        onEntrypointLoaded: function(engineInitializer) {
+          engineInitializer.autoStart();
+        }
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/packages/google_identity_services_web/example/web/index.html b/packages/google_identity_services_web/example/web/index.html
new file mode 100644
index 0000000..5727323
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/index.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<!-- 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. -->
+<html>
+<head>
+  <base href="$FLUTTER_BASE_HREF">
+
+  <meta charset="UTF-8">
+  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
+  <meta name="description" content="A new Flutter project.">
+
+  <!-- iOS meta tags & icons -->
+  <meta name="apple-mobile-web-app-capable" content="yes">
+  <meta name="apple-mobile-web-app-status-bar-style" content="black">
+  <meta name="apple-mobile-web-app-title" content="example">
+  <link rel="apple-touch-icon" href="icons/Icon-192.png">
+
+  <!-- Favicon -->
+  <link rel="icon" type="image/png" href="favicon.png"/>
+
+  <title>Authentication Example</title>
+  <link rel="manifest" href="manifest.json">
+
+  <script>
+    // The value below is injected by flutter build, do not touch.
+    var serviceWorkerVersion = null;
+  </script>
+  <!-- Trusted Types API config (disabled) -->
+  <!-- <meta http-equiv="Content-Security-Policy" content="trusted-types;"> -->
+  <!-- Load the GSI SDK -->
+  <!-- <script src="https://accounts.google.com/gsi/client" async defer></script> -->
+  <!-- This script adds the flutter initialization JS code -->
+  <script src="flutter.js" defer></script>
+</head>
+<body>
+  <script>
+    window.addEventListener('load', function(ev) {
+      // Download main.dart.js
+      _flutter.loader.loadEntrypoint({
+        serviceWorker: {
+          serviceWorkerVersion: serviceWorkerVersion,
+        },
+        onEntrypointLoaded: function(engineInitializer) {
+          engineInitializer.autoStart();
+        }
+      });
+    });
+  </script>
+</body>
+</html>
diff --git a/packages/google_identity_services_web/example/web/manifest.json b/packages/google_identity_services_web/example/web/manifest.json
new file mode 100644
index 0000000..096edf8
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/manifest.json
@@ -0,0 +1,35 @@
+{
+    "name": "example",
+    "short_name": "example",
+    "start_url": ".",
+    "display": "standalone",
+    "background_color": "#0175C2",
+    "theme_color": "#0175C2",
+    "description": "A new Flutter project.",
+    "orientation": "portrait-primary",
+    "prefer_related_applications": false,
+    "icons": [
+        {
+            "src": "icons/Icon-192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "icons/Icon-512.png",
+            "sizes": "512x512",
+            "type": "image/png"
+        },
+        {
+            "src": "icons/Icon-maskable-192.png",
+            "sizes": "192x192",
+            "type": "image/png",
+            "purpose": "maskable"
+        },
+        {
+            "src": "icons/Icon-maskable-512.png",
+            "sizes": "512x512",
+            "type": "image/png",
+            "purpose": "maskable"
+        }
+    ]
+}
diff --git a/packages/google_identity_services_web/lib/google_identity_services_web.dart b/packages/google_identity_services_web/lib/google_identity_services_web.dart
new file mode 100644
index 0000000..e3be3de
--- /dev/null
+++ b/packages/google_identity_services_web/lib/google_identity_services_web.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'loader.dart';
diff --git a/packages/google_identity_services_web/lib/id.dart b/packages/google_identity_services_web/lib/id.dart
new file mode 100644
index 0000000..3486212
--- /dev/null
+++ b/packages/google_identity_services_web/lib/id.dart
@@ -0,0 +1,14 @@
+// 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.
+
+export 'src/js_interop/google_accounts_id.dart';
+export 'src/js_interop/shared.dart'
+    show
+        CredentialSelectBy,
+        MomentDismissedReason,
+        MomentNotDisplayedReason,
+        MomentSkippedReason,
+        MomentType,
+        OneTapContext,
+        UxMode;
diff --git a/packages/google_identity_services_web/lib/loader.dart b/packages/google_identity_services_web/lib/loader.dart
new file mode 100644
index 0000000..906b7d1
--- /dev/null
+++ b/packages/google_identity_services_web/lib/loader.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'src/js_loader.dart' show TrustedTypesException, loadWebSdk;
diff --git a/packages/google_identity_services_web/lib/oauth2.dart b/packages/google_identity_services_web/lib/oauth2.dart
new file mode 100644
index 0000000..63db5bc
--- /dev/null
+++ b/packages/google_identity_services_web/lib/oauth2.dart
@@ -0,0 +1,6 @@
+// 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.
+
+export 'src/js_interop/google_accounts_oauth2.dart';
+export 'src/js_interop/shared.dart' show UxMode;
diff --git a/packages/google_identity_services_web/lib/src/js_interop/dom.dart b/packages/google_identity_services_web/lib/src/js_interop/dom.dart
new file mode 100644
index 0000000..2cc121d
--- /dev/null
+++ b/packages/google_identity_services_web/lib/src/js_interop/dom.dart
@@ -0,0 +1,164 @@
+// 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/
+*/
+
+import 'package:js/js.dart';
+
+/// Document interface
+@JS()
+@staticInterop
+@anonymous
+abstract class DomHtmlDocument {}
+
+/// Some methods of document
+extension DomHtmlDocumentExtension on DomHtmlDocument {
+  /// document.head
+  external DomHtmlElement get head;
+
+  /// document.createElement
+  external DomHtmlElement createElement(String tagName);
+}
+
+/// console interface
+@JS()
+@staticInterop
+@anonymous
+abstract class DomConsole {}
+
+/// The interface of window.console
+extension DomConsoleExtension on DomConsole {
+  /// console.debug
+  external DomConsoleDumpFn get debug;
+
+  /// console.info
+  external DomConsoleDumpFn get info;
+
+  /// console.log
+  external DomConsoleDumpFn get log;
+
+  /// console.warn
+  external DomConsoleDumpFn get warn;
+
+  /// console.error
+  external DomConsoleDumpFn get error;
+}
+
+/// Fakey variadic-type for console-dumping methods (like console.log or info).
+typedef DomConsoleDumpFn = void Function(
+  Object? arg, [
+  Object? arg2,
+  Object? arg3,
+  Object? arg4,
+  Object? arg5,
+  Object? arg6,
+  Object? arg7,
+  Object? arg8,
+  Object? arg9,
+  Object? arg10,
+]);
+
+/// An instance of an HTMLElement
+@JS()
+@staticInterop
+@anonymous
+abstract class DomHtmlElement {}
+
+/// (Some) methods of HtmlElement
+extension DomHtmlElementExtension on DomHtmlElement {
+  /// Node.appendChild
+  external DomHtmlElement appendChild(DomHtmlElement child);
+}
+
+/// An instance of an HTMLScriptElement
+@JS()
+@staticInterop
+@anonymous
+abstract class DomHtmlScriptElement extends DomHtmlElement {}
+
+/// Some methods exclusive of Script elements
+extension DomHtmlScriptElementExtension on DomHtmlScriptElement {
+  external set src(Object stringOrSafeScriptURL);
+  external set async(bool async);
+  external set defer(bool defer);
+}
+
+/*
+// Trusted Types API (TrustedTypePolicy, TrustedScript, TrustedScriptURL)
+// https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypesAPI
+*/
+
+/// A factory to create `TrustedTypePolicy` objects.
+@JS()
+@staticInterop
+@anonymous
+abstract class DomTrustedTypePolicyFactory {}
+
+/// (Some) methods of the [DomTrustedTypePolicyFactory]:
+extension DomTrustedTypePolicyFactoryExtension on DomTrustedTypePolicyFactory {
+  /// createPolicy
+  external DomTrustedTypePolicy createPolicy(
+    String policyName,
+    DomTrustedTypePolicyOptions? policyOptions,
+  );
+}
+
+/// Options to create a trusted type policy.
+@JS()
+@staticInterop
+@anonymous
+abstract class DomTrustedTypePolicyOptions {
+  /// Constructs a TrustedPolicyOptions object in JavaScript.
+  ///
+  /// The following properties need to be manually wrapped in [allowInterop]
+  /// before being passed to this constructor: [createScriptURL].
+  external factory DomTrustedTypePolicyOptions({
+    DomCreateScriptUrlOptionFn? createScriptURL,
+  });
+}
+
+/// Type of the function to configure createScriptURL
+typedef DomCreateScriptUrlOptionFn = String Function(String input);
+
+/// An instance of a TrustedTypePolicy
+@JS()
+@staticInterop
+@anonymous
+abstract class DomTrustedTypePolicy {}
+
+/// (Some) methods of the [DomTrustedTypePolicy]
+extension DomTrustedTypePolicyExtension on DomTrustedTypePolicy {
+  /// Create a `TrustedScriptURL` for the given [input].
+  external DomTrustedScriptUrl createScriptURL(String input);
+}
+
+/// An instance of a DomTrustedScriptUrl
+@JS()
+@staticInterop
+@anonymous
+abstract class DomTrustedScriptUrl {}
+
+// Getters
+
+/// window.document
+@JS()
+@staticInterop
+@anonymous
+external DomHtmlDocument get document;
+
+/// window.trustedTypes (may or may not be supported by the browser)
+@JS()
+@staticInterop
+@anonymous
+external DomTrustedTypePolicyFactory? get trustedTypes;
+
+/// window.console
+@JS()
+@staticInterop
+@anonymous
+external DomConsole get console;
diff --git a/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart
new file mode 100644
index 0000000..52fd806
--- /dev/null
+++ b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart
@@ -0,0 +1,440 @@
+// 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.
+
+// Authentication. API reference:
+// https://developers.google.com/identity/gsi/web/reference/js-reference
+
+// ignore_for_file: non_constant_identifier_names
+// * non_constant_identifier_names required to be able to use the same parameter
+//   names as the underlying library.
+
+@JS('google.accounts.id')
+library id;
+
+import 'package:js/js.dart';
+
+import 'shared.dart';
+
+/// An undocumented method. Try with 'debug'.
+@JS()
+external SetLogLevelFn get setLogLevel;
+
+///
+typedef SetLogLevelFn = void Function(String level);
+
+/*
+// Method: google.accounts.id.initialize
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.initialize
+*/
+
+/// Initializes the Sign In With Google client based on [IdConfiguration].
+///
+/// The `initialize` method creates a Sign In With Google client instance that
+/// can be implicitly used by all modules in the same web page.
+///
+/// * You only need to call the `initialize` method once even if you use
+///   multiple modules (like One Tap, Personalized button, revocation, etc.) in
+///   the same web page.
+/// * If you do call the google.accounts.id.initialize method multiple times,
+///   only the configurations in the last call will be remembered and used.
+///
+/// You actually reset the configurations whenever you call the `initialize`
+/// method, and all subsequent methods in the same web page will use the new
+/// configurations immediately.
+///
+/// WARNING: The `initialize` method should be called only once, even if you
+/// use both One Tap and button in the same web page.
+@JS()
+external InitializeFn get initialize;
+
+/// The type of the [initialize] function.
+typedef InitializeFn = void Function(IdConfiguration idConfiguration);
+
+/*
+// Data type: IdConfiguration
+// https://developers.google.com/identity/gsi/web/reference/js-reference#IdConfiguration
+*/
+
+/// The configuration object for the [initialize] method.
+@JS()
+@anonymous
+@staticInterop
+abstract class IdConfiguration {
+  /// Constructs a IdConfiguration object in JavaScript.
+  ///
+  /// The following properties need to be manually wrapped in [allowInterop]
+  /// before being passed to this constructor: [callback], [native_callback],
+  /// and [intermediate_iframe_close_callback].
+  external factory IdConfiguration({
+    /// Your application's client ID, which is found and created in the Google
+    /// Developers Console.
+    required String client_id,
+
+    /// Determines if an ID token is automatically returned without any user
+    /// interaction when there's only one Google session that has approved your
+    /// app before. The default value is `false`.
+    bool? auto_select,
+
+    /// The function that handles the ID token returned from the One Tap prompt
+    /// or the pop-up window. This attribute is required if Google One Tap or
+    /// the Sign In With Google button `popup` UX mode is used.
+    CallbackFn? callback,
+
+    /// This attribute is the URI of your login endpoint. May be omitted if the
+    /// current page is your login page, in which case the credential is posted
+    /// to this page by default.
+    ///
+    /// The ID token credential response is posted to your login endpoint when
+    /// a user clicks on the Sign In With Google button and `redirect` UX mode
+    /// is used.
+    ///
+    /// Your login endpoint must handle POST requests containing a credential
+    /// key with an ID token value in the body.
+    Uri? login_uri,
+
+    /// The function that handles the password credential returned from the
+    /// browser's native credential manager.
+    NativeCallbackFn? native_callback,
+
+    /// Whether or not to cancel the One Tap request if a user clicks outside
+    /// the prompt. The default value is `true`.
+    bool? cancel_on_tap_outside,
+
+    /// The DOM ID of the container element. If it's not set, the One Tap prompt
+    /// is displayed in the top-right corner of the window.
+    String? prompt_parent_id,
+
+    /// A random string used by the ID token to prevent replay attacks.
+    ///
+    /// Nonce length is limited to the maximum JWT size supported by your
+    /// environment, and individual browser and server HTTP size constraints.
+    String? nonce,
+
+    /// Changes the text of the title and messages in the One Tap prompt.
+    OneTapContext? context,
+
+    /// If you need to display One Tap in the parent domain and its subdomains,
+    /// pass the parent domain to this field so that a single shared-state
+    /// cookie is used.
+    ///
+    /// See: https://developers.google.com/identity/gsi/web/guides/subdomains
+    String? state_cookie_domain,
+
+    /// Set the UX flow used by the Sign In With Google button. The default
+    /// value is `popup`. **This attribute has no impact on the OneTap UX.**
+    UxMode? ux_mode,
+
+    /// The origins that are allowed to embed the intermediate iframe. One Tap
+    /// will run in the intermediate iframe mode if this field presents.
+    ///
+    /// Wildcard prefixes are also supported. Wildcard domains must begin with
+    /// a secure `https://` scheme, otherwise they'll be considered invalid.
+    List<String>? allowed_parent_origin,
+
+    /// Overrides the default intermediate iframe behavior when users manually
+    /// close One Tap by tapping on the 'X' button in the One Tap UI. The
+    /// default behavior is to remove the intermediate iframe from the DOM
+    /// immediately.
+    ///
+    /// The `intermediate_iframe_close_callback` field takes effect only in
+    /// intermediate iframe mode. And it has impact only to the intermediate
+    /// iframe, instead of the One Tap iframe. The One Tap UI is removed before
+    /// the callback is invoked.
+    Function? intermediate_iframe_close_callback,
+
+    /// determines if the upgraded One Tap UX should be enabled on browsers
+    /// that support Intelligent Tracking Prevention (ITP). The default value
+    /// is false.
+    ///
+    /// See: https://developers.google.com/identity/gsi/web/guides/features#upgraded_ux_on_itp_browsers
+    bool? itp_support,
+  });
+}
+
+/*
+// Method: google.accounts.id.prompt
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt
+*/
+
+/// The `prompt` method displays the One Tap prompt or the browser native
+/// credential manager after the [initialize] method is invoked.
+///
+/// Normally, the `prompt` method is called on page load. Due to the session
+/// status and user settings on the Google side, the One Tap prompt UI might
+/// not be displayed. To get notified on the UI status for different moments,
+/// pass a [PromptMomentListenerFn] to receive UI status notifications.
+///
+/// Notifications are fired on the following moments:
+///
+/// * Display moment: This occurs after the `prompt` method is called. The
+///   notification contains a boolean value to indicate whether the UI is
+///   displayed or not.
+/// * Skipped moment: This occurs when the One Tap prompt is closed by an auto
+///   cancel, a manual cancel, or when Google fails to issue a credential, such
+///   as when the selected session has signed out of Google.
+///   In these cases, we recommend that you continue on to the next identity
+///   providers, if there are any.
+/// * Dismissed moment: This occurs when Google successfully retrieves a
+///   credential or a user wants to stop the credential retrieval flow. For
+///   example, when the user begins to input their username and password in
+///   your login dialog, you can call the [cancel] method to close the One Tap
+///   prompt and trigger a dismissed moment.
+///
+///   WARNING: When on a dismissed moment, do not try any of the next identity
+///   providers.
+@JS()
+external PromptFn get prompt;
+
+/// The type of the [prompt] function.
+///
+/// The [momentListener] parameter must be manually wrapped in [allowInterop]
+/// before being passed to the [prompt] function.
+typedef PromptFn = void Function(PromptMomentListenerFn? momentListener);
+
+/// The type of the function that can be passed to [prompt] to listen for [PromptMomentNotification]s.
+typedef PromptMomentListenerFn = void Function(PromptMomentNotification moment);
+
+/*
+// Data type: PromptMomentNotification
+// https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification
+*/
+
+/// A moment (status) notification from the [prompt] method.
+@JS()
+@staticInterop
+abstract class PromptMomentNotification {}
+
+/// The methods of the [PromptMomentNotification] data type:
+extension PromptMomentNotificationExtension on PromptMomentNotification {
+  /// Is this notification for a display moment?
+  external bool isDisplayMoment();
+
+  /// Is this notification for a display moment, and the UI is displayed?
+  external bool isDisplayed();
+
+  /// Is this notification for a display moment, and the UI isn't displayed?
+  external bool isNotDisplayed();
+
+  /// Is this notification for a skipped moment?
+  external bool isSkippedMoment();
+
+  /// Is this notification for a dismissed moment?
+  external bool isDismissedMoment();
+  @JS('getMomentType')
+  external String _getMomentType();
+  @JS('getNotDisplayedReason')
+  external String? _getNotDisplayedReason();
+  @JS('getSkippedReason')
+  external String? _getSkippedReason();
+  @JS('getDismissedReason')
+  external String? _getDismissedReason();
+
+  /// The moment type.
+  MomentType getMomentType() => MomentType.values.byName(_getMomentType());
+
+  /// The detailed reason why the UI isn't displayed.
+  MomentNotDisplayedReason? getNotDisplayedReason() =>
+      maybeEnum(_getNotDisplayedReason(), MomentNotDisplayedReason.values);
+
+  /// The detailed reason for the skipped moment.
+  MomentSkippedReason? getSkippedReason() =>
+      maybeEnum(_getSkippedReason(), MomentSkippedReason.values);
+
+  /// The detailed reason for the dismissal.
+  MomentDismissedReason? getDismissedReason() =>
+      maybeEnum(_getDismissedReason(), MomentDismissedReason.values);
+}
+
+/*
+// Data type: CredentialResponse
+// https://developers.google.com/identity/gsi/web/reference/js-reference#CredentialResponse
+*/
+
+/// The object passed as the parameter of your [CallbackFn].
+@JS()
+@staticInterop
+abstract class CredentialResponse {}
+
+/// The fields that are contained in the credential response object.
+extension CredentialResponseExtension on CredentialResponse {
+  /// This field is the ID token as a base64-encoded JSON Web Token (JWT)
+  /// string.
+  ///
+  /// See more: https://developers.google.com/identity/gsi/web/reference/js-reference#credential
+  external String get credential;
+  @JS('select_by')
+  external String get _select_by;
+
+  /// This field sets how the credential was selected.
+  ///
+  /// The type of button used along with the session and consent state are used
+  /// to set the value.
+  ///
+  /// See more: https://developers.google.com/identity/gsi/web/reference/js-reference#select_by
+  CredentialSelectBy get select_by =>
+      CredentialSelectBy.values.byName(_select_by);
+}
+
+/// The type of the `callback` used to create an [IdConfiguration].
+///
+/// Describes a JavaScript function that handles ID tokens from
+/// [CredentialResponse]s.
+///
+/// Google One Tap and the Sign In With Google button popup UX mode use this
+/// attribute.
+typedef CallbackFn = void Function(CredentialResponse credentialResponse);
+
+/*
+// Method: google.accounts.id.renderButton
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.renderButton
+//
+// Data type: GsiButtonConfiguration
+// https://developers.google.com/identity/gsi/web/reference/js-reference#GsiButtonConfiguration
+//
+// Question: Do we need to implement renderButton and its options?
+*/
+
+/*
+// Data type: Credential
+// https://developers.google.com/identity/gsi/web/reference/js-reference#type-Credential
+*/
+
+/// The object passed to the [NativeCallbackFn]. Represents a PasswordCredential
+/// that was returned by the Browser.
+///
+/// `Credential` objects can also be programmatically created to be stored
+/// in the browser through the [storeCredential] method.
+///
+/// See also: https://developer.mozilla.org/en-US/docs/Web/API/PasswordCredential/PasswordCredential
+@JS()
+@anonymous
+@staticInterop
+abstract class Credential {
+  ///
+  external factory Credential({
+    required String id,
+    required String password,
+  });
+}
+
+/// The fields that are contained in the [Credential] object.
+extension CredentialExtension on Credential {
+  /// Identifies the user.
+  external String get id;
+
+  /// The password.
+  external String get password;
+}
+
+/// The type of the `native_callback` used to create an [IdConfiguration].
+///
+/// Describes a JavaScript function that handles password [Credential]s coming
+/// from the native Credential manager of the user's browser.
+typedef NativeCallbackFn = void Function(Credential credential);
+
+/*
+// Method: google.accounts.id.disableAutoselect
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.disableAutoSelect
+*/
+
+/// When the user signs out of your website, you need to call this method to
+/// record the status in cookies.
+///
+/// This prevents a UX dead loop.
+@JS()
+external VoidFn get disableAutoSelect;
+
+/*
+// Method: google.accounts.id.storeCredential
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.storeCredential
+*/
+
+/// This method is a simple wrapper for the `store` method of the browser's
+/// native credential manager API.
+///
+/// It can only be used to store a Password [Credential].
+///
+/// See: https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/store
+@JS()
+external StoreCredentialFn get storeCredential;
+
+/// The type of the [storeCredential] function.
+///
+/// The [callback] parameter must be manually wrapped in [allowInterop]
+/// before being passed to the [storeCredential] function.
+// Question: What's the type of the callback function??? VoidFn?
+typedef StoreCredentialFn = void Function(
+  Credential credential,
+  Function? callback,
+);
+
+/*
+// Method: google.accounts.id.cancel
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.cancel
+*/
+
+/// You can cancel the One Tap flow if you remove the prompt from the relying
+/// party DOM. The cancel operation is ignored if a credential is already
+/// selected.
+@JS()
+external VoidFn get cancel;
+
+/*
+// Library load callback: onGoogleLibraryLoad
+// https://developers.google.com/identity/gsi/web/reference/js-reference#onGoogleLibraryLoad
+// See: `load_callback.dart` and `loader.dart`
+*/
+
+/*
+// Method: google.accounts.id.revoke
+// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.revoke
+*/
+
+/// The `revoke` method revokes the OAuth grant used to share the ID token for
+/// the specified user.
+@JS()
+external RevokeFn get revoke;
+
+/// The type of the [revoke] function.
+///
+/// [hint] is the email address or unique ID of the user's Google Account. The
+/// ID is the `sub` property of the [CredentialResponse.credential] payload.
+///
+/// The optional [callback] is a function that gets called to report on the
+/// success of the revocation call.
+///
+/// The [callback] parameter must be manually wrapped in [allowInterop]
+/// before being passed to the [revoke] function.
+typedef RevokeFn = void Function(
+  String hint,
+  RevocationResponseHandlerFn? callback,
+);
+
+/// The type of the `callback` function passed to [revoke], to be notified of
+/// the success of the revocation operation.
+typedef RevocationResponseHandlerFn = void Function(
+  RevocationResponse revocationResponse,
+);
+
+/*
+// Data type: RevocationResponse
+// https://developers.google.com/identity/gsi/web/reference/js-reference#RevocationResponse
+*/
+
+/// The parameter passed to the optional [RevocationResponseHandlerFn]
+/// `callback` of the [revoke] function.
+@JS()
+@staticInterop
+abstract class RevocationResponse {}
+
+/// The fields that are contained in the [RevocationResponse] object.
+extension RevocationResponseExtension on RevocationResponse {
+  /// This field is a boolean value set to true if the revoke method call
+  /// succeeded or false on failure.
+  external bool get successful;
+
+  /// This field is a string value and contains a detailed error message if the
+  /// revoke method call failed, it is undefined on success.
+  external String? get error;
+}
diff --git a/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart
new file mode 100644
index 0000000..3986138
--- /dev/null
+++ b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart
@@ -0,0 +1,339 @@
+// 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.
+
+// Authorization. API reference:
+// https://developers.google.com/identity/oauth2/web/reference/js-reference
+
+// ignore_for_file: non_constant_identifier_names
+// * non_constant_identifier_names required to be able to use the same parameter
+//   names as the underlying library.
+
+@JS('google.accounts.oauth2')
+library oauth2;
+
+import 'package:js/js.dart';
+
+import 'shared.dart';
+
+// Code Client
+
+/*
+// Method: google.accounts.oauth2.initCodeClient
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initCodeClient
+*/
+
+/// The initCodeClient method initializes and returns a code client, with the
+/// passed-in [config].
+@JS()
+external InitCodeClientFn get initCodeClient;
+
+/// The type of the [initCodeClient] function.
+typedef InitCodeClientFn = CodeClient Function(CodeClientConfig config);
+
+/*
+// Data type: CodeClientConfig
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeClientConfig
+*/
+
+/// The configuration object for the [initCodeClient] method.
+@JS()
+@anonymous
+@staticInterop
+abstract class CodeClientConfig {
+  /// Constructs a CodeClientConfig object in JavaScript.
+  ///
+  /// The [callback] property must be wrapped in [allowInterop] before it's
+  /// passed to this constructor.
+  external factory CodeClientConfig({
+    required String client_id,
+    required String scope,
+    String? redirect_uri,
+    bool? auto_select,
+    CodeClientCallbackFn? callback,
+    String? state,
+    bool? enable_serial_consent,
+    String? hint,
+    String? hosted_domain,
+    UxMode? ux_mode,
+    bool? select_account,
+  });
+}
+
+/*
+// Data type: CodeClient
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeClient
+*/
+
+/// A client that can start the OAuth 2.0 Code UX flow.
+///
+/// See: https://developers.google.com/identity/oauth2/web/guides/use-code-model
+@JS()
+@staticInterop
+abstract class CodeClient {}
+
+/// The methods available on the [CodeClient].
+extension CodeClientExtension on CodeClient {
+  /// Starts the OAuth 2.0 Code UX flow.
+  external void requestCode();
+}
+
+/*
+// Data type: CodeResponse
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeResponse
+*/
+
+/// The object passed as the parameter of your [CodeClientCallbackFn].
+@JS()
+@staticInterop
+abstract class CodeResponse {}
+
+/// The fields that are contained in the code response object.
+extension CodeResponseExtension on CodeResponse {
+  /// The authorization code of a successful token response.
+  external String get code;
+
+  /// A space-delimited list of scopes that are approved by the user.
+  external String get scope;
+
+  /// The string value that your application uses to maintain state between your
+  /// authorization request and the response.
+  external String get state;
+
+  /// A single ASCII error code.
+  external String? get error;
+
+  /// Human-readable ASCII text providing additional information, used to assist
+  /// the client developer in understanding the error that occurred.
+  external String? get error_description;
+
+  /// A URI identifying a human-readable web page with information about the
+  /// error, used to provide the client developer with additional information
+  /// about the error.
+  external String? get error_uri;
+}
+
+/// The type of the `callback` function passed to [CodeClientConfig].
+typedef CodeClientCallbackFn = void Function(CodeResponse response);
+
+// Token Client
+
+/*
+// Method: google.accounts.oauth2.initTokenClient
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initTokenClient
+*/
+
+/// The initCodeClient method initializes and returns a code client, with the
+/// passed-in [config].
+@JS()
+external InitTokenClientFn get initTokenClient;
+
+/// The type of the [initCodeClient] function.
+typedef InitTokenClientFn = TokenClient Function(TokenClientConfig config);
+
+/*
+// Data type: TokenClientConfig
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClientConfig
+*/
+
+/// The configuration object for the [initTokenClient] method.
+@JS()
+@anonymous
+@staticInterop
+abstract class TokenClientConfig {
+  /// Constructs a TokenClientConfig object in JavaScript.
+  ///
+  /// The [callback] property must be wrapped in [allowInterop] before it's
+  /// passed to this constructor.
+  external factory TokenClientConfig({
+    required String client_id,
+    required TokenClientCallbackFn? callback,
+    required String scope,
+    String? prompt,
+    bool? enable_serial_consent,
+    String? hint,
+    String? hosted_domain,
+    String? state,
+  });
+}
+
+/*
+// Data type: TokenClient
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClient
+*/
+
+/// A client that can start the OAuth 2.0 Token UX flow.
+///
+/// See: https://developers.google.com/identity/oauth2/web/guides/use-token-model
+@JS()
+@staticInterop
+abstract class TokenClient {}
+
+/// The methods available on the [TokenClient].
+extension TokenClientExtension on TokenClient {
+  /// Starts the OAuth 2.0 Code UX flow.
+  external void requestAccessToken([
+    OverridableTokenClientConfig overrideConfig,
+  ]);
+}
+
+/*
+// Data type: OverridableTokenClientConfig
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#OverridableTokenClientConfig
+*/
+
+/// The overridable configuration object for the
+/// [TokenClientExtension.requestAccessToken] method.
+@JS()
+@anonymous
+@staticInterop
+abstract class OverridableTokenClientConfig {
+  /// Constructs an OverridableTokenClientConfig object in JavaScript.
+  ///
+  /// The [callback] property must be wrapped in [allowInterop] before it's
+  /// passed to this constructor.
+  external factory OverridableTokenClientConfig({
+    /// A space-delimited, case-sensitive list of prompts to present the user.
+    ///
+    /// See `prompt` in [TokenClientConfig].
+    String? prompt,
+
+    /// For clients created before 2019, when set to `false`, disables "more
+    /// granular Google Account permissions".
+    ///
+    /// This setting has no effect in newer clients.
+    ///
+    /// See: https://developers.googleblog.com/2018/10/more-granular-google-account.html
+    bool? enable_serial_consent,
+
+    /// 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-fills the email
+    /// box on the sign-in form, or selects 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.
+    ///
+    /// About Multiple Sign-in: https://support.google.com/accounts/answer/1721977
+    String? hint,
+
+    /// **Not recommended.** Specifies any string value that your application
+    /// uses to maintain state between your authorization request and the
+    /// authorization server's response.
+    String? state,
+  });
+}
+
+/*
+// Data type: TokenResponse
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse
+*/
+
+/// The object passed as the parameter of your [TokenClientCallbackFn].
+@JS()
+@staticInterop
+abstract class TokenResponse {}
+
+/// The fields that are contained in the code response object.
+extension TokenResponseExtension on TokenResponse {
+  /// The access token of a successful token response.
+  external String get access_token;
+
+  /// The lifetime in seconds of the access token.
+  external int get expires_in;
+
+  /// The hosted domain the signed-in user belongs to.
+  external String get hd;
+
+  /// The prompt value that was used from the possible list of values specified
+  /// by [TokenClientConfig] or [OverridableTokenClientConfig].
+  external String get prompt;
+
+  /// The type of the token issued.
+  external String get token_type;
+
+  /// A space-delimited list of scopes that are approved by the user.
+  external String get scope;
+
+  /// The string value that your application uses to maintain state between your
+  /// authorization request and the response.
+  external String get state;
+
+  /// A single ASCII error code.
+  external String? get error;
+
+  /// Human-readable ASCII text providing additional information, used to assist
+  /// the client developer in understanding the error that occurred.
+  external String? get error_description;
+
+  /// A URI identifying a human-readable web page with information about the
+  /// error, used to provide the client developer with additional information
+  /// about the error.
+  external String? get error_uri;
+}
+
+/// The type of the `callback` function passed to [TokenClientConfig].
+typedef TokenClientCallbackFn = void Function(TokenResponse response);
+
+/*
+// Method: google.accounts.oauth2.hasGrantedAllScopes
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAllScopes
+*/
+
+/// Checks if the user granted **all** the specified scopes.
+@JS()
+external HasGrantedScopesFn get hasGrantedAllScopes;
+
+/*
+// Method: google.accounts.oauth2.hasGrantedAnyScopes
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAnyScopes
+*/
+
+/// Checks if the user granted **any** of the specified scopes.
+@JS()
+external HasGrantedScopesFn get hasGrantedAnyScopes;
+
+/// The signature for functions that check if any/all scopes have been granted.
+///
+/// Used by [hasGrantedAllScopes] and [hasGrantedAnyScope].
+typedef HasGrantedScopesFn = bool Function(
+  TokenResponse tokenResponse,
+  String firstScope, [
+  String? scope2,
+  String? scope3,
+  String? scope4,
+  String? scope5,
+  String? scope6,
+  String? scope7,
+  String? scope8,
+  String? scope9,
+  String? scope10,
+]);
+
+/*
+// Method: google.accounts.oauth2.revoke
+// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.revoke
+*/
+
+/// The [revokeToken] method revokes all of the scopes that the user granted to
+/// the app. A valid access token is required to revoke the permission.
+///
+/// The `done` callback is called once the revoke action is done.
+@JS('revoke')
+external RevokeTokenFn get revokeToken;
+
+/// The signature of the [revokeToken] function.
+///
+/// The (optional) [done] parameter must be manually wrapped in [allowInterop]
+/// before being passed to the [revokeToken] function.
+typedef RevokeTokenFn = void Function(
+  String accessToken, [
+  RevokeTokenDoneFn? done,
+]);
+
+/// The signature of the `done` function for [revokeToken].
+///
+/// Work in progress here: b/248628502
+typedef RevokeTokenDoneFn = void Function(String jsonError);
diff --git a/packages/google_identity_services_web/lib/src/js_interop/load_callback.dart b/packages/google_identity_services_web/lib/src/js_interop/load_callback.dart
new file mode 100644
index 0000000..fa7c4ca
--- /dev/null
+++ b/packages/google_identity_services_web/lib/src/js_interop/load_callback.dart
@@ -0,0 +1,26 @@
+// 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.
+
+// Methods here are documented in the Google Identity authentication website,
+// but they don't really belong to either the authentication nor authorization
+// libraries.
+@JS()
+library id_load_callback;
+
+import 'package:js/js.dart';
+
+import 'shared.dart';
+
+/*
+// Library load callback: onGoogleLibraryLoad
+// https://developers.google.com/identity/gsi/web/reference/js-reference#onGoogleLibraryLoad
+*/
+
+/// Method called after the Sign In With Google JavaScript library is loaded.
+///
+/// The [callback] parameter must be manually wrapped in [allowInterop]
+/// before being set to the [onGoogleLibraryLoad] property.
+@JS()
+@staticInterop
+external set onGoogleLibraryLoad(VoidFn callback);
diff --git a/packages/google_identity_services_web/lib/src/js_interop/shared.dart b/packages/google_identity_services_web/lib/src/js_interop/shared.dart
new file mode 100644
index 0000000..4e35f38
--- /dev/null
+++ b/packages/google_identity_services_web/lib/src/js_interop/shared.dart
@@ -0,0 +1,215 @@
+// 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.
+
+/// Attempts to retrieve an enum value from [haystack] if [needle] is not null.
+T? maybeEnum<T extends Enum>(String? needle, List<T> haystack) {
+  if (needle == null) {
+    return null;
+  }
+  return haystack.byName(needle);
+}
+
+/// The type of several functions from the library, that don't receive
+/// parameters nor return anything.
+typedef VoidFn = void Function();
+
+/*
+// Enum: UX Mode
+// https://developers.google.com/identity/gsi/web/reference/js-reference#ux_mode
+// Used both by `oauth2.initCodeClient` and `id.initialize`.
+*/
+
+/// Use this enum to set the UX flow used by the Sign In With Google button.
+/// The default value is [popup].
+///
+/// This attribute has no impact on the OneTap UX.
+enum UxMode {
+  /// Performs sign-in UX flow in a pop-up window.
+  popup('popup'),
+
+  /// Performs sign-in UX flow by a full page redirection.
+  redirect('redirect');
+
+  ///
+  const UxMode(String uxMode) : _uxMode = uxMode;
+  final String _uxMode;
+
+  @override
+  String toString() => _uxMode;
+}
+
+/// Changes the text of the title and messages in the One Tap prompt.
+enum OneTapContext {
+  /// "Sign in with Google"
+  signin('signin'),
+
+  /// "Sign up with Google"
+  signup('signup'),
+
+  /// "Use with Google"
+  use('use');
+
+  ///
+  const OneTapContext(String context) : _context = context;
+  final String _context;
+
+  @override
+  String toString() => _context;
+}
+
+/// The detailed reason why the OneTap UI isn't displayed.
+enum MomentNotDisplayedReason {
+  /// Browser not supported.
+  ///
+  /// See https://developers.google.com/identity/gsi/web/guides/supported-browsers
+  browser_not_supported('browser_not_supported'),
+
+  /// Invalid Client.
+  ///
+  /// See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid
+  invalid_client('invalid_client'),
+
+  /// Missing client_id.
+  ///
+  /// See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid
+  missing_client_id('missing_client_id'),
+
+  /// The user has opted out, or they aren't signed in to a Google account.
+  ///
+  /// https://developers.google.com/identity/gsi/web/guides/features
+  opt_out_or_no_session('opt_out_or_no_session'),
+
+  /// Google One Tap can only be displayed in HTTPS domains.
+  ///
+  /// See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid
+  secure_http_required('secure_http_required'),
+
+  /// The user has previously closed the OneTap card.
+  ///
+  /// See https://developers.google.com/identity/gsi/web/guides/features#exponential_cooldown
+  suppressed_by_user('suppressed_by_user'),
+
+  /// The current `origin` is not associated with the Client ID.
+  ///
+  /// See https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid
+  unregistered_origin('unregistered_origin'),
+
+  /// Unknown reason
+  unknown_reason('unknown_reason');
+
+  ///
+  const MomentNotDisplayedReason(String reason) : _reason = reason;
+  final String _reason;
+
+  @override
+  String toString() => _reason;
+}
+
+/// The detailed reason for the skipped moment.
+enum MomentSkippedReason {
+  /// auto_cancel
+  auto_cancel('auto_cancel'),
+
+  /// user_cancel
+  user_cancel('user_cancel'),
+
+  /// tap_outside
+  tap_outside('tap_outside'),
+
+  /// issuing_failed
+  issuing_failed('issuing_failed');
+
+  ///
+  const MomentSkippedReason(String reason) : _reason = reason;
+  final String _reason;
+
+  @override
+  String toString() => _reason;
+}
+
+/// The detailed reason for the dismissal.
+enum MomentDismissedReason {
+  /// credential_returned
+  credential_returned('credential_returned'),
+
+  /// cancel_called
+  cancel_called('cancel_called'),
+
+  /// flow_restarted
+  flow_restarted('flow_restarted');
+
+  ///
+  const MomentDismissedReason(String reason) : _reason = reason;
+  final String _reason;
+
+  @override
+  String toString() => _reason;
+}
+
+/// The moment type.
+enum MomentType {
+  /// Display moment
+  display('display'),
+
+  /// Skipped moment
+  skipped('skipped'),
+
+  /// Dismissed moment
+  dismissed('dismissed');
+
+  ///
+  const MomentType(String type) : _type = type;
+  final String _type;
+
+  @override
+  String toString() => _type;
+}
+
+/// Represents how a credential was selected.
+enum CredentialSelectBy {
+  /// Automatic sign-in of a user with an existing session who had previously
+  /// granted consent to share credentials.
+  auto('auto'),
+
+  /// A user with an existing session who had previously granted consent
+  /// pressed the One Tap 'Continue as' button to share credentials.
+  user('user'),
+
+  /// A user with an existing session pressed the One Tap 'Continue as' button
+  /// to grant consent and share credentials. Applies only to Chrome v75 and
+  /// higher.
+  user_1tap('user_1tap'),
+
+  /// A user without an existing session pressed the One Tap 'Continue as'
+  /// button to select an account and then pressed the Confirm button in a
+  /// pop-up window to grant consent and share credentials. Applies to
+  /// non-Chromium based browsers.
+  user_2tap('user_2tap'),
+
+  /// A user with an existing session who previously granted consent pressed
+  /// the Sign In With Google button and selected a Google Account from
+  /// 'Choose an Account' to share credentials.
+  btn('btn'),
+
+  /// A user with an existing session pressed the Sign In With Google button
+  /// and pressed the Confirm button to grant consent and share credentials.
+  btn_confirm('btn_confirm'),
+
+  /// A user without an existing session who previously granted consent
+  /// pressed the Sign In With Google button to select a Google Account and
+  /// share credentials.
+  btn_add_session('btn_add_session'),
+
+  /// A user without an existing session first pressed the Sign In With Google
+  /// button to select a Google Account and then pressed the Confirm button to
+  /// consent and share credentials.
+  btn_confirm_add_session('btn_confirm_add_session');
+
+  ///
+  const CredentialSelectBy(String selectBy) : _selectBy = selectBy;
+  final String _selectBy;
+
+  @override
+  String toString() => _selectBy;
+}
diff --git a/packages/google_identity_services_web/lib/src/js_loader.dart b/packages/google_identity_services_web/lib/src/js_loader.dart
new file mode 100644
index 0000000..0f2dc7b
--- /dev/null
+++ b/packages/google_identity_services_web/lib/src/js_loader.dart
@@ -0,0 +1,67 @@
+// 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 'package:js/js.dart' show allowInterop;
+
+import 'js_interop/dom.dart';
+import 'js_interop/load_callback.dart';
+
+// The URL from which the script should be downloaded.
+const String _url = 'https://accounts.google.com/gsi/client';
+
+// The default TrustedPolicy name that will be used to inject the script.
+const String _defaultTrustedPolicyName = 'gis-dart';
+
+/// Loads the GIS SDK for web, using Trusted Types API when available.
+Future<void> loadWebSdk({
+  DomHtmlElement? target,
+  String trustedTypePolicyName = _defaultTrustedPolicyName,
+}) {
+  final Completer<void> completer = Completer<void>();
+  onGoogleLibraryLoad = allowInterop(() => completer.complete());
+
+  // If TrustedTypes are available, prepare a trusted URL.
+  DomTrustedScriptUrl? trustedUrl;
+  if (trustedTypes != null) {
+    console.debug(
+      'TrustedTypes available. Creating policy:',
+      trustedTypePolicyName,
+    );
+    final DomTrustedTypePolicyFactory factory = trustedTypes!;
+    try {
+      final DomTrustedTypePolicy policy = factory.createPolicy(
+          trustedTypePolicyName,
+          DomTrustedTypePolicyOptions(
+            createScriptURL: allowInterop((String url) => _url),
+          ));
+      trustedUrl = policy.createScriptURL(_url);
+    } catch (e) {
+      throw TrustedTypesException(e.toString());
+    }
+  }
+
+  final DomHtmlScriptElement script =
+      document.createElement('script') as DomHtmlScriptElement
+        ..src = trustedUrl ?? _url
+        ..async = true
+        ..defer = true;
+
+  (target ?? document.head).appendChild(script);
+
+  return completer.future;
+}
+
+/// Exception thrown if the Trusted Types feature is supported, enabled, and it
+/// has prevented this loader from injecting the JS SDK.
+class TrustedTypesException implements Exception {
+  ///
+  TrustedTypesException(this.message);
+
+  /// The message of the exception
+  final String message;
+  @override
+  String toString() => 'TrustedTypesException: $message';
+}
diff --git a/packages/google_identity_services_web/pubspec.yaml b/packages/google_identity_services_web/pubspec.yaml
new file mode 100644
index 0000000..d84a687
--- /dev/null
+++ b/packages/google_identity_services_web/pubspec.yaml
@@ -0,0 +1,16 @@
+name: google_identity_services_web
+description: A Dart JS-interop layer for Google Identity Services. Google's new sign-in SDK for Web that supports multiple types of credentials.
+repository: https://github.com/flutter/packages/tree/main/packages/google_identity_services_web
+issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_identiy_services_web%22
+version: 0.1.0
+
+environment:
+  sdk: ">=2.17.0 <3.0.0"
+
+dependencies:
+  js: ^0.6.4
+  meta: ^1.3.0
+
+dev_dependencies:
+  path: ^1.8.1
+  test: ^1.21.1
diff --git a/packages/google_identity_services_web/test/README.md b/packages/google_identity_services_web/test/README.md
new file mode 100644
index 0000000..8a783c2
--- /dev/null
+++ b/packages/google_identity_services_web/test/README.md
@@ -0,0 +1,3 @@
+# Tests
+
+Use `dart run tool/run_tests.dart` to run tests in this package.
diff --git a/packages/google_identity_services_web/test/js_loader_test.dart b/packages/google_identity_services_web/test/js_loader_test.dart
new file mode 100644
index 0000000..a9d857b
--- /dev/null
+++ b/packages/google_identity_services_web/test/js_loader_test.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.
+
+@TestOn('browser') // Uses package:js
+
+import 'package:google_identity_services_web/loader.dart';
+import 'package:google_identity_services_web/src/js_interop/dom.dart' as dom;
+import 'package:google_identity_services_web/src/js_loader.dart';
+
+import 'package:js/js_util.dart' as js_util;
+
+import 'package:test/test.dart';
+
+// NOTE: This file needs to be separated from the others because Content
+// Security Policies can never be *relaxed* once set.
+//
+// In order to not introduce a dependency in the order of the tests, we split
+// them in different files, depending on the strictness of their CSP:
+//
+// * js_loader_test.dart : default TT configuration (not enforced)
+// * js_loader_tt_custom_test.dart : TT are customized, but allowed
+// * js_loader_tt_forbidden_test.dart: TT are completely disallowed
+
+void main() {
+  group('loadWebSdk (no TrustedTypes)', () {
+    final dom.DomHtmlElement target = dom.document.createElement('div');
+
+    test('Injects script into desired target', () async {
+      loadWebSdk(target: target);
+
+      // Target now should have a child that is a script element
+      final Object children = js_util.getProperty<Object>(target, 'children');
+      final Object injected = js_util.callMethod<Object>(
+        children,
+        'item',
+        <Object>[0],
+      );
+      expect(injected, isA<dom.DomHtmlScriptElement>());
+
+      final dom.DomHtmlScriptElement script =
+          injected as dom.DomHtmlScriptElement;
+      expect(js_util.getProperty<bool>(script, 'defer'), isTrue);
+      expect(js_util.getProperty<bool>(script, 'async'), isTrue);
+      expect(
+        js_util.getProperty<String>(script, 'src'),
+        'https://accounts.google.com/gsi/client',
+      );
+    });
+
+    test('Completes when the script loads', () async {
+      final Future<void> loadFuture = loadWebSdk(target: target);
+
+      Future<void>.delayed(const Duration(milliseconds: 100), () {
+        // Simulate the library calling `window.onGoogleLibraryLoad`.
+        js_util.callMethod<void>(
+          js_util.globalThis,
+          'onGoogleLibraryLoad',
+          <Object>[],
+        );
+      });
+
+      await expectLater(loadFuture, completes);
+    });
+  });
+}
diff --git a/packages/google_identity_services_web/test/js_loader_tt_custom_test.dart b/packages/google_identity_services_web/test/js_loader_tt_custom_test.dart
new file mode 100644
index 0000000..6668be0
--- /dev/null
+++ b/packages/google_identity_services_web/test/js_loader_tt_custom_test.dart
@@ -0,0 +1,47 @@
+// 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.
+
+@TestOn('browser') // Uses package:js
+
+import 'package:google_identity_services_web/loader.dart';
+import 'package:google_identity_services_web/src/js_interop/dom.dart' as dom;
+
+import 'package:test/test.dart';
+
+import 'tools.dart';
+
+// NOTE: This file needs to be separated from the others because Content
+// Security Policies can never be *relaxed* once set.
+//
+// In order to not introduce a dependency in the order of the tests, we split
+// them in different files, depending on the strictness of their CSP:
+//
+// * js_loader_test.dart : default TT configuration (not enforced)
+// * js_loader_tt_custom_test.dart : TT are customized, but allowed
+// * js_loader_tt_forbidden_test.dart: TT are completely disallowed
+
+void main() {
+  group('loadWebSdk (TrustedTypes configured)', () {
+    final dom.DomHtmlElement target = dom.document.createElement('div');
+    injectMetaTag(<String, String>{
+      'http-equiv': 'Content-Security-Policy',
+      'content': "trusted-types my-custom-policy-name 'allow-duplicates';",
+    });
+
+    test('Wrong policy name: Fail with TrustedTypesException', () {
+      expect(() {
+        loadWebSdk(target: target);
+      }, throwsA(isA<TrustedTypesException>()));
+    });
+
+    test('Correct policy name: Completes', () {
+      final Future<void> done = loadWebSdk(
+        target: target,
+        trustedTypePolicyName: 'my-custom-policy-name',
+      );
+
+      expect(done, isA<Future<void>>());
+    });
+  });
+}
diff --git a/packages/google_identity_services_web/test/js_loader_tt_forbidden_test.dart b/packages/google_identity_services_web/test/js_loader_tt_forbidden_test.dart
new file mode 100644
index 0000000..e95b5f9
--- /dev/null
+++ b/packages/google_identity_services_web/test/js_loader_tt_forbidden_test.dart
@@ -0,0 +1,38 @@
+// 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.
+
+@TestOn('browser') // Uses package:js
+
+import 'package:google_identity_services_web/loader.dart';
+import 'package:google_identity_services_web/src/js_interop/dom.dart' as dom;
+
+import 'package:test/test.dart';
+
+import 'tools.dart';
+
+// NOTE: This file needs to be separated from the others because Content
+// Security Policies can never be *relaxed* once set.
+//
+// In order to not introduce a dependency in the order of the tests, we split
+// them in different files, depending on the strictness of their CSP:
+//
+// * js_loader_test.dart : default TT configuration (not enforced)
+// * js_loader_tt_custom_test.dart : TT are customized, but allowed
+// * js_loader_tt_forbidden_test.dart: TT are completely disallowed
+
+void main() {
+  group('loadWebSdk (TrustedTypes forbidden)', () {
+    final dom.DomHtmlElement target = dom.document.createElement('div');
+    injectMetaTag(<String, String>{
+      'http-equiv': 'Content-Security-Policy',
+      'content': "trusted-types 'none';",
+    });
+
+    test('Fail with TrustedTypesException', () {
+      expect(() {
+        loadWebSdk(target: target);
+      }, throwsA(isA<TrustedTypesException>()));
+    });
+  });
+}
diff --git a/packages/google_identity_services_web/test/only_chrome_tests_here_test.dart b/packages/google_identity_services_web/test/only_chrome_tests_here_test.dart
new file mode 100644
index 0000000..6c33ea9
--- /dev/null
+++ b/packages/google_identity_services_web/test/only_chrome_tests_here_test.dart
@@ -0,0 +1,18 @@
+// 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.
+
+// ignore_for_file: avoid_print
+
+@TestOn('vm')
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Tell the user where to find the real tests', () {
+    print('---');
+    print('This package uses `dart test -p chrome` for its tests.');
+    print('See `README.md` for more info.');
+    print('---');
+  });
+}
diff --git a/packages/google_identity_services_web/test/tools.dart b/packages/google_identity_services_web/test/tools.dart
new file mode 100644
index 0000000..dd3d68e
--- /dev/null
+++ b/packages/google_identity_services_web/test/tools.dart
@@ -0,0 +1,19 @@
+// 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/src/js_interop/dom.dart' as dom;
+import 'package:js/js_util.dart' as js_util;
+
+/// Injects a `<meta>` tag with the provided [attributes] into the [dom.document].
+void injectMetaTag(Map<String, String> attributes) {
+  final dom.DomHtmlElement meta = dom.document.createElement('meta');
+  for (final MapEntry<String, String> attribute in attributes.entries) {
+    js_util.callMethod(
+      meta,
+      'setAttribute',
+      <String>[attribute.key, attribute.value],
+    );
+  }
+  dom.document.head.appendChild(meta);
+}
diff --git a/packages/google_identity_services_web/tool/run_tests.dart b/packages/google_identity_services_web/tool/run_tests.dart
new file mode 100644
index 0000000..1997e46
--- /dev/null
+++ b/packages/google_identity_services_web/tool/run_tests.dart
@@ -0,0 +1,49 @@
+// 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.
+
+// Runs `dart test -p chrome` in the root of the google_identity_services_web package.
+//
+// Called from the custom-tests CI action.
+//
+// usage: dart run tool/run_tests.dart
+// (needs a `chrome` executable in $PATH, or a tweak to dart_test.yaml)
+import 'dart:io';
+import 'package:path/path.dart' as p;
+
+Future<void> main(List<String> args) async {
+  final Directory workingDir =
+      Directory(p.dirname(Platform.script.path)).parent;
+
+  final int status = await _runProcess(
+    'dart',
+    <String>[
+      'test',
+      '-p',
+      'chrome',
+    ],
+    workingDirectory: workingDir.path,
+  );
+
+  exit(status);
+}
+
+Future<Process> _streamOutput(Future<Process> processFuture) async {
+  final Process process = await processFuture;
+  stdout.addStream(process.stdout);
+  stderr.addStream(process.stderr);
+  return process;
+}
+
+Future<int> _runProcess(
+  String command,
+  List<String> arguments, {
+  String? workingDirectory,
+}) async {
+  final Process process = await _streamOutput(Process.start(
+    command,
+    arguments,
+    workingDirectory: workingDirectory,
+  ));
+  return process.exitCode;
+}
diff --git a/script/configs/linux_only_custom_test.yaml b/script/configs/linux_only_custom_test.yaml
new file mode 100644
index 0000000..8313d41
--- /dev/null
+++ b/script/configs/linux_only_custom_test.yaml
@@ -0,0 +1,9 @@
+# Packages that only support `custom-test` in linux, because that's
+# the only place where we install Chrome.
+#
+# This file is used to "--exclude" these packages from the relevant
+# mac/windows CI test runs.
+
+- cross_file
+- google_identity_services_web
+