[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: