From b6fe67e49fc280bfdde1a7e36b5b39e8661f06de Mon Sep 17 00:00:00 2001 From: David Iglesias Date: Tue, 10 Jan 2023 14:53:16 -0800 Subject: [PATCH] [gis_web] Adds id.renderButton JS-interop. (#3011) * [gis_web] Adds id.renderButton API. * Modernizes JS-interop so it's more compliant with dart2wasm. * Updates examples, tests and docs. * Bumps major version. * Add the GsiButtonDataExtension class. * Make oauth2 library more dart2wasm friendly. * Reimplement hasGrantedA[ny|ll]Scopes in Dart. * Fix oauth example. * Added troubleshooting section to README. * Add happy case tests for the oauth flow. * Fix typo in config constructors. * dart format * Add some error handling to the library * Add previously_granted_scopes field to overridable token config. Make scopes a List of Strings in the hasGranted[Any|All]Scopes method. --- .../google_identity_services_web/CHANGELOG.md | 8 + .../google_identity_services_web/README.md | 26 +- .../js_interop_gis_id_test.dart | 101 ----- .../integration_test/js_interop_id_test.dart | 57 +++ .../js_interop_oauth_test.dart | 124 ++++++ .../example/integration_test/utils.dart | 76 ++++ .../example/lib/main.dart | 13 +- .../example/lib/main_oauth.dart | 106 +++-- .../example/web/mock-gis.js | 31 +- .../lib/src/js_interop/dom.dart | 23 ++ .../src/js_interop/google_accounts_id.dart | 390 ++++++++++-------- .../js_interop/google_accounts_oauth2.dart | 253 ++++++------ .../lib/src/js_interop/shared.dart | 156 +++++++ .../google_identity_services_web/pubspec.yaml | 2 +- .../test/README.md | 5 + 15 files changed, 916 insertions(+), 455 deletions(-) delete mode 100644 packages/google_identity_services_web/example/integration_test/js_interop_gis_id_test.dart create mode 100644 packages/google_identity_services_web/example/integration_test/js_interop_id_test.dart create mode 100644 packages/google_identity_services_web/example/integration_test/js_interop_oauth_test.dart create mode 100644 packages/google_identity_services_web/example/integration_test/utils.dart diff --git a/packages/google_identity_services_web/CHANGELOG.md b/packages/google_identity_services_web/CHANGELOG.md index 0ec9358453..d84b822878 100644 --- a/packages/google_identity_services_web/CHANGELOG.md +++ b/packages/google_identity_services_web/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.0 + +* Adds `renderButton` API to `id.dart`. +* **Breaking Change:** Makes JS-interop API more `dart2wasm`-friendly. + * Removes external getters for function types + * Introduces an external getter for the whole libraries instead. + * Updates `README.md` with the new way of `import`ing the desired libraries. + ## 0.1.1 * Add optional `scope` to `OverridableTokenClientConfig` object. diff --git a/packages/google_identity_services_web/README.md b/packages/google_identity_services_web/README.md index 71bbb86657..8fc5c34dc7 100644 --- a/packages/google_identity_services_web/README.md +++ b/packages/google_identity_services_web/README.md @@ -65,9 +65,29 @@ behind a [conditional import/export](https://dart.dev/guides/libraries/create-li Once the SDK has been loaded, it can be used by importing the correct library: -* `import 'package:google_identity_services/id.dart' as id;` for Authentication -* `import 'package:google_identity_services/oauth2.dart' as oauth2;` for - Authorization. +* `import 'package:google_identity_services/id.dart';` for Authentication. + * This will expose an `id` JSObject that binds to `google.accounts.id`. +* `import 'package:google_identity_services/oauth2.dart';` for Authorization. + * This will expose an `oauth2` JSObject that binds to `google.accounts.oauth2`. + +### Troubleshooting + +Watch the browser's development tools JS console while using this package. +Information about errors during initialization and use of the library will be +displayed there. + +Some common issues identified so far: + +#### The given origin is not allowed for the given client ID + +> When you perform local tests or development, **you must add both** +> `http://localhost` and `http://localhost:` to the +> **Authorized JavaScript origins** box. +> The [Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) +> response header must also be set to `no-referrer-when-downgrade` when using +> http and localhost. + +* Read more: [Sign In with Google for Web - Setup - Get your Google API client ID](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid#get_your_google_api_client_id). ## Browser compatibility diff --git a/packages/google_identity_services_web/example/integration_test/js_interop_gis_id_test.dart b/packages/google_identity_services_web/example/integration_test/js_interop_gis_id_test.dart deleted file mode 100644 index b1c1b028fe..0000000000 --- a/packages/google_identity_services_web/example/integration_test/js_interop_gis_id_test.dart +++ /dev/null @@ -1,101 +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:async'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:google_identity_services_web/id.dart' as id; -import 'package:google_identity_services_web/src/js_interop/dom.dart'; - -import 'package:integration_test/integration_test.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart'; - -@JS('window') -external Object get domWindow; - -void main() async { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - setUpAll(() async { - // Load web/mock-gis.js in the page - await installGisMock(); - }); - - group('prompt', () { - testWidgets('supports a moment notification callback', (_) async { - id.initialize(id.IdConfiguration(client_id: 'testing_1-2-3')); - - final StreamController controller = - StreamController(); - - id.prompt(allowInterop(controller.add)); - - final id.PromptMomentNotification moment = await controller.stream.first; - - // These defaults are set in mock-gis.js - expect(moment.getMomentType(), id.MomentType.skipped); - expect(moment.getSkippedReason(), id.MomentSkippedReason.user_cancel); - }); - - testWidgets('calls config callback with credential response', (_) async { - const String expected = 'should_be_a_proper_jwt_token'; - setMockCredentialResponse(expected); - - final StreamController controller = - StreamController(); - - id.initialize(id.IdConfiguration( - client_id: 'testing_1-2-3', - callback: allowInterop(controller.add), - )); - - id.prompt(); - - final id.CredentialResponse response = await controller.stream.first; - - expect(response.credential, expected); - }); - }); -} - -/// Installs mock-gis.js in the page. -/// Returns a future that completes when the 'load' event of the script fires. -Future installGisMock() { - final Completer completer = Completer(); - final DomHtmlScriptElement script = - document.createElement('script') as DomHtmlScriptElement; - script.src = 'mock-gis.js'; - setProperty(script, 'type', 'module'); - callMethod(script, 'addEventListener', [ - 'load', - allowInterop((_) { - completer.complete(); - }) - ]); - document.head.appendChild(script); - return completer.future; -} - -void setMockCredentialResponse([String value = 'default_value']) { - callMethod( - _getGoogleAccountsId(), - 'setMockCredentialResponse', - [value, 'auto'], - ); -} - -Object _getGoogleAccountsId() { - return _getDeepProperty(domWindow, 'google.accounts.id'); -} - -// Attempts to retrieve a deeply nested property from a jsObject (or die tryin') -T _getDeepProperty(Object jsObject, String deepProperty) { - final List properties = deepProperty.split('.'); - return properties.fold( - jsObject, - (Object jsObj, String prop) => getProperty(jsObj, prop), - ) as T; -} diff --git a/packages/google_identity_services_web/example/integration_test/js_interop_id_test.dart b/packages/google_identity_services_web/example/integration_test/js_interop_id_test.dart new file mode 100644 index 0000000000..17ac7484b8 --- /dev/null +++ b/packages/google_identity_services_web/example/integration_test/js_interop_id_test.dart @@ -0,0 +1,57 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_identity_services_web/id.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:js/js.dart'; + +import 'utils.dart' as utils; + +void main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + // Load web/mock-gis.js in the page + await utils.installGisMock(); + }); + + group('prompt', () { + testWidgets('supports a moment notification callback', (_) async { + id.initialize(IdConfiguration(client_id: 'testing_1-2-3')); + + final StreamController controller = + StreamController(); + + id.prompt(allowInterop(controller.add)); + + final PromptMomentNotification moment = await controller.stream.first; + + // These defaults are set in mock-gis.js + expect(moment.getMomentType(), MomentType.skipped); + expect(moment.getSkippedReason(), MomentSkippedReason.user_cancel); + }); + + testWidgets('calls config callback with credential response', (_) async { + const String expected = 'should_be_a_proper_jwt_token'; + utils.setMockCredentialResponse(expected); + + final StreamController controller = + StreamController(); + + id.initialize(IdConfiguration( + client_id: 'testing_1-2-3', + callback: allowInterop(controller.add), + )); + + id.prompt(); + + final CredentialResponse response = await controller.stream.first; + + expect(response.credential, expected); + }); + }); +} diff --git a/packages/google_identity_services_web/example/integration_test/js_interop_oauth_test.dart b/packages/google_identity_services_web/example/integration_test/js_interop_oauth_test.dart new file mode 100644 index 0000000000..50a06676e4 --- /dev/null +++ b/packages/google_identity_services_web/example/integration_test/js_interop_oauth_test.dart @@ -0,0 +1,124 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:js/js.dart'; + +import 'utils.dart' as utils; + +void main() async { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + // Load web/mock-gis.js in the page + await utils.installGisMock(); + }); + + group('initTokenClient', () { + testWidgets('returns a tokenClient', (_) async { + final TokenClient client = oauth2.initTokenClient(TokenClientConfig( + client_id: 'for-tests', + callback: null, + scope: 'some_scope for_tests not_real', + )); + + expect(client, isNotNull); + }); + }); + + group('requestAccessToken', () { + testWidgets('passes through configuration', (_) async { + final StreamController controller = + StreamController(); + + final List scopes = ['some_scope', 'another', 'more']; + + final TokenClient client = oauth2.initTokenClient(TokenClientConfig( + client_id: 'for-tests', + callback: allowInterop(controller.add), + scope: scopes.join(' '), + )); + + utils.setMockTokenResponse(client, 'some-non-null-auth-token-value'); + + client.requestAccessToken(); + + final TokenResponse response = await controller.stream.first; + + expect(response, isNotNull); + expect(response.error, isNull); + expect(response.scope, scopes.join(' ')); + }); + + testWidgets('configuration can be overridden', (_) async { + final StreamController controller = + StreamController(); + + final List scopes = ['some_scope', 'another', 'more']; + + final TokenClient client = oauth2.initTokenClient(TokenClientConfig( + client_id: 'for-tests', + callback: allowInterop(controller.add), + scope: 'blank', + )); + + utils.setMockTokenResponse(client, 'some-non-null-auth-token-value'); + + client.requestAccessToken(OverridableTokenClientConfig( + scope: scopes.join(' '), + )); + + final TokenResponse response = await controller.stream.first; + + expect(response, isNotNull); + expect(response.error, isNull); + expect(response.scope, scopes.join(' ')); + }); + }); + + group('hasGranted...Scopes', () { + // mock-gis.js returns false for scopes that start with "not-granted-". + const String notGranted = 'not-granted-scope'; + + testWidgets('all scopes granted', (_) async { + final List scopes = ['some_scope', 'another', 'more']; + + final TokenResponse response = await utils.fakeAuthZWithScopes(scopes); + + final bool all = oauth2.hasGrantedAllScopes(response, scopes); + final bool any = oauth2.hasGrantedAnyScopes(response, scopes); + + expect(all, isTrue); + expect(any, isTrue); + }); + + testWidgets('some scopes granted', (_) async { + final List scopes = ['some_scope', notGranted, 'more']; + + final TokenResponse response = await utils.fakeAuthZWithScopes(scopes); + + final bool all = oauth2.hasGrantedAllScopes(response, scopes); + final bool any = oauth2.hasGrantedAnyScopes(response, scopes); + + expect(all, isFalse, reason: 'Scope: $notGranted should not be granted!'); + expect(any, isTrue); + }); + + testWidgets('no scopes granted', (_) async { + final List scopes = [notGranted, '$notGranted-2']; + + final TokenResponse response = await utils.fakeAuthZWithScopes(scopes); + + final bool all = oauth2.hasGrantedAllScopes(response, scopes); + final bool any = oauth2.hasGrantedAnyScopes(response, scopes); + + expect(all, isFalse); + expect(any, isFalse, reason: 'No scopes were granted.'); + }); + }); +} diff --git a/packages/google_identity_services_web/example/integration_test/utils.dart b/packages/google_identity_services_web/example/integration_test/utils.dart new file mode 100644 index 0000000000..889a0eee32 --- /dev/null +++ b/packages/google_identity_services_web/example/integration_test/utils.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:google_identity_services_web/oauth2.dart'; +import 'package:google_identity_services_web/src/js_interop/dom.dart'; +import 'package:js/js.dart'; +import 'package:js/js_util.dart'; + +@JS('window') +external Object get domWindow; + +/// Installs mock-gis.js in the page. +/// Returns a future that completes when the 'load' event of the script fires. +Future installGisMock() { + final Completer completer = Completer(); + final DomHtmlScriptElement script = + document.createElement('script') as DomHtmlScriptElement; + script.src = 'mock-gis.js'; + setProperty(script, 'type', 'module'); + callMethod(script, 'addEventListener', [ + 'load', + allowInterop((_) { + completer.complete(); + }) + ]); + document.head.appendChild(script); + return completer.future; +} + +/// Fakes authorization with the given scopes. +Future fakeAuthZWithScopes(List scopes) { + final StreamController controller = + StreamController(); + final TokenClient client = oauth2.initTokenClient(TokenClientConfig( + client_id: 'for-tests', + callback: allowInterop(controller.add), + scope: scopes.join(' '), + )); + setMockTokenResponse(client, 'some-non-null-auth-token-value'); + client.requestAccessToken(); + return controller.stream.first; +} + +/// Sets a mock TokenResponse value in a [client]. +void setMockTokenResponse(TokenClient client, [String? authToken]) { + callMethod( + client, + 'setMockTokenResponse', + [authToken], + ); +} + +/// Sets a mock credential response in `google.accounts.id`. +void setMockCredentialResponse([String value = 'default_value']) { + callMethod( + _getGoogleAccountsId(), + 'setMockCredentialResponse', + [value, 'auto'], + ); +} + +Object _getGoogleAccountsId() { + return _getDeepProperty(domWindow, 'google.accounts.id'); +} + +// Attempts to retrieve a deeply nested property from a jsObject (or die tryin') +T _getDeepProperty(Object jsObject, String deepProperty) { + final List properties = deepProperty.split('.'); + return properties.fold( + jsObject, + (Object jsObj, String prop) => getProperty(jsObj, prop), + ) as T; +} diff --git a/packages/google_identity_services_web/example/lib/main.dart b/packages/google_identity_services_web/example/lib/main.dart index 949eb51522..3dc8c0e26f 100644 --- a/packages/google_identity_services_web/example/lib/main.dart +++ b/packages/google_identity_services_web/example/lib/main.dart @@ -4,7 +4,7 @@ // ignore_for_file: avoid_print -import 'package:google_identity_services_web/id.dart' as id; +import 'package:google_identity_services_web/id.dart'; // #docregion use-loader import 'package:google_identity_services_web/loader.dart' as gis; // #enddocregion use-loader @@ -18,9 +18,8 @@ void main() async { // #enddocregion use-loader id.setLogLevel('debug'); - final id.IdConfiguration config = id.IdConfiguration( + final IdConfiguration config = IdConfiguration( client_id: 'your-client_id.apps.googleusercontent.com', - ux_mode: id.UxMode.popup, callback: allowInterop(onCredentialResponse), ); @@ -32,8 +31,8 @@ void main() async { /// Handles the ID token returned from the One Tap prompt. /// See: https://developers.google.com/identity/gsi/web/reference/js-reference#callback -void onCredentialResponse(id.CredentialResponse o) { - final Map? payload = jwt.JwtDecoder.tryDecode(o.credential); +void onCredentialResponse(CredentialResponse o) { + final Map? payload = jwt.JwtDecoder.tryDecode(o.credential!); if (payload != null) { print('Hello, ${payload["name"]}'); print(o.select_by); @@ -45,8 +44,8 @@ void onCredentialResponse(id.CredentialResponse o) { /// Handles Prompt UI status notifications. /// See: https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt -void onPromptMoment(id.PromptMomentNotification o) { - final id.MomentType type = o.getMomentType(); +void onPromptMoment(PromptMomentNotification o) { + final MomentType type = o.getMomentType(); print(type.runtimeType); print(type); print(type.index); diff --git a/packages/google_identity_services_web/example/lib/main_oauth.dart b/packages/google_identity_services_web/example/lib/main_oauth.dart index 041715f2bd..c54ab4cc60 100644 --- a/packages/google_identity_services_web/example/lib/main_oauth.dart +++ b/packages/google_identity_services_web/example/lib/main_oauth.dart @@ -4,16 +4,31 @@ // ignore_for_file: avoid_print -import 'package:google_identity_services_web/id.dart' as id show setLogLevel; +import 'dart:convert'; + +import 'package:google_identity_services_web/id.dart'; import 'package:google_identity_services_web/loader.dart' as gis; -import 'package:google_identity_services_web/oauth2.dart' as oauth2; +import 'package:google_identity_services_web/oauth2.dart'; import 'package:http/http.dart' as http; import 'package:js/js.dart' show allowInterop; +import 'package:js/js_util.dart' show getProperty; -/// The scopes to be requested +/// People API to return my profile info... +const String MY_PROFILE = + 'https://content-people.googleapis.com/v1/people/me?personFields=photos%2Cnames%2CemailAddresses'; + +/// People API to return all my connections. +const String MY_CONNECTIONS = + 'https://people.googleapis.com/v1/people/me/connections?requestMask.includeField=person.names'; + +/// Basic scopes for self-id const List scopes = [ - 'email', - 'profile', + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', +]; + +/// Scopes for the people API (read contacts) +const List myConnectionsScopes = [ 'https://www.googleapis.com/auth/contacts.readonly', ]; @@ -22,58 +37,77 @@ void main() async { id.setLogLevel('debug'); - final oauth2.TokenClientConfig config = oauth2.TokenClientConfig( + final TokenClientConfig config = TokenClientConfig( client_id: 'your-client_id.apps.googleusercontent.com', scope: scopes.join(' '), callback: allowInterop(onTokenResponse), + error_callback: allowInterop(onError), ); - final oauth2.OverridableTokenClientConfig overridableCfg = - oauth2.OverridableTokenClientConfig( - prompt: '', + final OverridableTokenClientConfig overridableCfg = + OverridableTokenClientConfig( + scope: (scopes + myConnectionsScopes).join(' '), ); - final oauth2.TokenClient client = oauth2.initTokenClient(config); + final TokenClient client = oauth2.initTokenClient(config); // Disable the Popup Blocker for this to work, or move this to a Button press. client.requestAccessToken(overridableCfg); } +/// Triggers when there's an error with the OAuth2 popup. +/// +/// We cannot use the proper type for `error` here yet, because of: +/// https://github.com/dart-lang/sdk/issues/50899 +Future onError(Object? error) async { + print('Error! ${getProperty(error!, "type")}'); +} + /// Handles the returned (auth) token response. /// See: https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse -Future onTokenResponse(oauth2.TokenResponse response) async { - if (response.error != null) { +Future onTokenResponse(TokenResponse token) async { + if (token.error != null) { print('Authorization error!'); - print(response.error); - print(response.error_description); - print(response.error_uri); - return; - } - - // Has granted all the scopes? - if (!oauth2.hasGrantedAllScopes(response, scopes[2])) { - print('The user has NOT granted the required scope!'); + print(token.error); + print(token.error_description); + print(token.error_uri); return; } // Attempt to do a request to the `people` API - final http.Response apiResponse = await http.get( - Uri.parse('https://people.googleapis.com/v1/people/me/connections' - '?requestMask.includeField=person.names'), - headers: { - 'Authorization': '${response.token_type} ${response.access_token}', - }, - ); - if (apiResponse.statusCode == 200) { - print('People API ${apiResponse.statusCode} OK!'); - } else { - print( - 'People API ${apiResponse.statusCode} Oops! Something wrong happened!'); + final Object? profile = await get(token, MY_PROFILE); + print(profile); + + // Has granted all the scopes? + if (!oauth2.hasGrantedAllScopes(token, myConnectionsScopes)) { + print('The user has NOT granted all the required scopes!'); + print('The next get will probably throw an exception!'); } - print(apiResponse.body); + + final Object? contacts = await get(token, MY_CONNECTIONS); + print(contacts); print('Revoking token...'); - oauth2.revokeToken(response.access_token, allowInterop((String status) { - print(status); + oauth2.revoke(token.access_token, + allowInterop((TokenRevocationResponse response) { + print(response.successful); + print(response.error); + print(response.error_description); })); } + +/// Gets from [url] with an authorization header defined by [token]. +/// +/// Attempts to [jsonDecode] the result. +Future get(TokenResponse token, String url) async { + final Uri uri = Uri.parse(url); + final http.Response response = await http.get(uri, headers: { + 'Authorization': '${token.token_type} ${token.access_token}', + }); + + if (response.statusCode != 200) { + throw http.ClientException(response.body, uri); + } + + return jsonDecode(response.body) as Object?; +} diff --git a/packages/google_identity_services_web/example/web/mock-gis.js b/packages/google_identity_services_web/example/web/mock-gis.js index 56701e2806..2e1346051f 100644 --- a/packages/google_identity_services_web/example/web/mock-gis.js +++ b/packages/google_identity_services_web/example/web/mock-gis.js @@ -94,17 +94,24 @@ class TokenClient { this.config = config; } requestAccessToken(overridableConfig) { - this.overridableConfig = overridableConfig; - let callback = this.overridableConfig.callback || this.config.callback; + this.config = {...this.config, ...overridableConfig}; + let callback = this.config.callback; if (!callback) { return; } callAsync(() => { - callback(this.tokenResponse); + callback({ + ...this.tokenResponse, + scope: this.config.scope, + }); }); } - setMockTokenResponse(tokenResponse) { - this.tokenResponse = tokenResponse; + setMockTokenResponse(access_token) { + this.tokenResponse = { + access_token: access_token, + token_type: access_token != null ? 'Bearer' : null, + error: access_token == null ? 'unauthorized' : null, + }; } } @@ -116,22 +123,24 @@ class Oauth2 { return new TokenClient(config); } hasGrantedAllScopes(tokenResponse, scope, ...scopes) { - return tokenResponse != null; + return tokenResponse != null && !scope.startsWith('not-granted-'); } hasGrantedAnyScopes(tokenResponse, scope, ...scopes) { - return tokenResponse != null; + return false; // Unused in the lib } revoke(accessToken, done) { if (!done) { return; } callAsync(() => { - done(); + done({ + success: true, + }); }) } } -function mockGis() { +(function() { let goog = { accounts: { id: new Id(), @@ -139,6 +148,4 @@ function mockGis() { } }; globalThis['google'] = goog; -} - -mockGis(); +}()); diff --git a/packages/google_identity_services_web/lib/src/js_interop/dom.dart b/packages/google_identity_services_web/lib/src/js_interop/dom.dart index 2cc121ddc5..ff5dfc96a9 100644 --- a/packages/google_identity_services_web/lib/src/js_interop/dom.dart +++ b/packages/google_identity_services_web/lib/src/js_interop/dom.dart @@ -88,6 +88,29 @@ extension DomHtmlScriptElementExtension on DomHtmlScriptElement { external set defer(bool defer); } +/// Error object +@JS('Error') +@staticInterop +abstract class DomError {} + +/// Methods on the error object +extension DomErrorExtension on DomError { + /// Error message. + external String? get message; + + /// Stack trace. + external String? get stack; + + /// Error name. This is determined by the constructor function. + external String get name; + + /// Error cause indicating the reason why the current error is thrown. + /// + /// This is usually another caught error, or the value provided as the `cause` + /// property of the Error constructor's second argument. + external Object? get cause; +} + /* // Trusted Types API (TrustedTypePolicy, TrustedScript, TrustedScriptURL) // https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypesAPI diff --git a/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart index 579662da12..a790fae78f 100644 --- a/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart +++ b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_id.dart @@ -9,54 +9,142 @@ // * non_constant_identifier_names required to be able to use the same parameter // names as the underlying library. -@JS('google.accounts.id') -library id; +@JS() +library google_accounts_id; import 'package:js/js.dart'; +import 'dom.dart'; import 'shared.dart'; -/// An undocumented method. Try with 'debug'. +/// Binding to the `google.accounts.id` JS global. +/// +/// See: https://developers.google.com/identity/gsi/web/reference/js-reference +@JS('google.accounts.id') +external GoogleAccountsId get id; + +/// The Dart definition of the `google.accounts.id` global. @JS() -external SetLogLevelFn get setLogLevel; +@staticInterop +abstract class GoogleAccountsId {} -/// -typedef SetLogLevelFn = void Function(String level); +/// The `google.accounts.id` methods +extension GoogleAccountsIdExtension on GoogleAccountsId { + /// An undocumented method. + /// + /// Try it with 'debug'. + external void setLogLevel(String level); -/* -// Method: google.accounts.id.initialize -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.initialize -*/ + /// Initializes the Sign In With Google client based on [IdConfiguration]. + /// + /// The `initialize` method creates a Sign In With Google client instance that + /// can be implicitly used by all modules in the same web page. + /// + /// * You only need to call the `initialize` method once even if you use + /// multiple modules (like One Tap, Personalized button, revocation, etc.) in + /// the same web page. + /// * If you do call the google.accounts.id.initialize method multiple times, + /// only the configurations in the last call will be remembered and used. + /// + /// You actually reset the configurations whenever you call the `initialize` + /// method, and all subsequent methods in the same web page will use the new + /// configurations immediately. + /// + /// WARNING: The `initialize` method should be called only once, even if you + /// use both One Tap and button in the same web page. + /// + /// Method: google.accounts.id.initialize + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.initialize + external void initialize(IdConfiguration idConfiguration); -/// Initializes the Sign In With Google client based on [IdConfiguration]. -/// -/// The `initialize` method creates a Sign In With Google client instance that -/// can be implicitly used by all modules in the same web page. -/// -/// * You only need to call the `initialize` method once even if you use -/// multiple modules (like One Tap, Personalized button, revocation, etc.) in -/// the same web page. -/// * If you do call the google.accounts.id.initialize method multiple times, -/// only the configurations in the last call will be remembered and used. -/// -/// You actually reset the configurations whenever you call the `initialize` -/// method, and all subsequent methods in the same web page will use the new -/// configurations immediately. -/// -/// WARNING: The `initialize` method should be called only once, even if you -/// use both One Tap and button in the same web page. -@JS() -external InitializeFn get initialize; + /// The `prompt` method displays the One Tap prompt or the browser native + /// credential manager after the [initialize] method is invoked. + /// + /// Normally, the `prompt` method is called on page load. Due to the session + /// status and user settings on the Google side, the One Tap prompt UI might + /// not be displayed. To get notified on the UI status for different moments, + /// pass a [PromptMomentListenerFn] to receive UI status notifications. + /// + /// Notifications are fired on the following moments: + /// + /// * Display moment: This occurs after the `prompt` method is called. The + /// notification contains a boolean value to indicate whether the UI is + /// displayed or not. + /// * Skipped moment: This occurs when the One Tap prompt is closed by an auto + /// cancel, a manual cancel, or when Google fails to issue a credential, such + /// as when the selected session has signed out of Google. + /// In these cases, we recommend that you continue on to the next identity + /// providers, if there are any. + /// * Dismissed moment: This occurs when Google successfully retrieves a + /// credential or a user wants to stop the credential retrieval flow. For + /// example, when the user begins to input their username and password in + /// your login dialog, you can call the [cancel] method to close the One Tap + /// prompt and trigger a dismissed moment. + /// + /// WARNING: When on a dismissed moment, do not try any of the next identity + /// providers. + /// + /// Method: google.accounts.id.prompt + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt + external void prompt([PromptMomentListenerFn momentListener]); -/// The type of the [initialize] function. -typedef InitializeFn = void Function(IdConfiguration idConfiguration); + /// Renders a Sign In With Google button in your web page. + /// + /// Method: google.accounts.id.renderButton + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.renderButton + external void renderButton( + DomHtmlElement parent, [ + GsiButtonConfiguration options, + ]); -/* -// Data type: IdConfiguration -// https://developers.google.com/identity/gsi/web/reference/js-reference#IdConfiguration -*/ + /// Record when the user signs out of your website in cookies. + /// + /// This prevents a UX dead loop. + /// + /// Method: google.accounts.id.disableAutoselect + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.disableAutoSelect + external void disableAutoSelect(); + + /// A wrapper for the `store` method of the browser's native credential manager API. + /// + /// It can only be used to store a Password [Credential]. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/store + /// + /// Method: google.accounts.id.storeCredential + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.storeCredential + external void storeCredential(Credential credential, [VoidFn fallback]); + + /// Cancels the One Tap flow. + /// + /// You can cancel the One Tap flow if you remove the prompt from the relying + /// party DOM. The cancel operation is ignored if a credential is already + /// selected. + /// + /// Method: google.accounts.id.cancel + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.cancel + external void cancel(); + + /// Revokes the OAuth grant used to share the ID token for the specified user. + /// + /// [hint] is the email address or unique ID of the user's Google Account. The + /// ID is the `sub` property of the [CredentialResponse.credential] payload. + /// + /// The optional [callback] is a function that gets called to report on the + /// success of the revocation call. + /// + /// The [callback] parameter must be manually wrapped in [allowInterop] + /// before being passed to the [revoke] function. + /// + /// Method: google.accounts.id.revoke + /// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.revoke + external void revoke(String hint, [RevocationResponseHandlerFn callback]); +} /// The configuration object for the [initialize] method. +/// +/// Data type: IdConfiguration +/// https://developers.google.com/identity/gsi/web/reference/js-reference#IdConfiguration @JS() @anonymous @staticInterop @@ -152,55 +240,13 @@ abstract class IdConfiguration { }); } -/* -// Method: google.accounts.id.prompt -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.prompt -*/ - -/// The `prompt` method displays the One Tap prompt or the browser native -/// credential manager after the [initialize] method is invoked. -/// -/// Normally, the `prompt` method is called on page load. Due to the session -/// status and user settings on the Google side, the One Tap prompt UI might -/// not be displayed. To get notified on the UI status for different moments, -/// pass a [PromptMomentListenerFn] to receive UI status notifications. -/// -/// Notifications are fired on the following moments: -/// -/// * Display moment: This occurs after the `prompt` method is called. The -/// notification contains a boolean value to indicate whether the UI is -/// displayed or not. -/// * Skipped moment: This occurs when the One Tap prompt is closed by an auto -/// cancel, a manual cancel, or when Google fails to issue a credential, such -/// as when the selected session has signed out of Google. -/// In these cases, we recommend that you continue on to the next identity -/// providers, if there are any. -/// * Dismissed moment: This occurs when Google successfully retrieves a -/// credential or a user wants to stop the credential retrieval flow. For -/// example, when the user begins to input their username and password in -/// your login dialog, you can call the [cancel] method to close the One Tap -/// prompt and trigger a dismissed moment. -/// -/// WARNING: When on a dismissed moment, do not try any of the next identity -/// providers. -@JS() -external PromptFn get prompt; - -/// The type of the [prompt] function. -/// -/// The [momentListener] parameter must be manually wrapped in [allowInterop] -/// before being passed to the [prompt] function. -typedef PromptFn = void Function([PromptMomentListenerFn momentListener]); - /// The type of the function that can be passed to [prompt] to listen for [PromptMomentNotification]s. typedef PromptMomentListenerFn = void Function(PromptMomentNotification moment); -/* -// Data type: PromptMomentNotification -// https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification -*/ - /// A moment (status) notification from the [prompt] method. +/// +/// Data type: PromptMomentNotification +/// https://developers.google.com/identity/gsi/web/reference/js-reference#PromptMomentNotification @JS() @staticInterop abstract class PromptMomentNotification {} @@ -246,25 +292,32 @@ extension PromptMomentNotificationExtension on PromptMomentNotification { maybeEnum(_getDismissedReason(), MomentDismissedReason.values); } -/* -// Data type: CredentialResponse -// https://developers.google.com/identity/gsi/web/reference/js-reference#CredentialResponse -*/ - /// The object passed as the parameter of your [CallbackFn]. +/// +/// Data type: CredentialResponse +/// https://developers.google.com/identity/gsi/web/reference/js-reference#CredentialResponse @JS() @staticInterop abstract class CredentialResponse {} /// The fields that are contained in the credential response object. extension CredentialResponseExtension on CredentialResponse { + /// The ClientID for this Credential. + external String? get client_id; + + /// Error while signing in. + external String? get error; + + /// Details of the error while signing in. + external String? get error_detail; + /// This field is the ID token as a base64-encoded JSON Web Token (JWT) /// string. /// /// See more: https://developers.google.com/identity/gsi/web/reference/js-reference#credential - external String get credential; + external String? get credential; @JS('select_by') - external String get _select_by; + external String? get _select_by; /// This field sets how the credential was selected. /// @@ -272,8 +325,8 @@ extension CredentialResponseExtension on CredentialResponse { /// to set the value. /// /// See more: https://developers.google.com/identity/gsi/web/reference/js-reference#select_by - CredentialSelectBy get select_by => - CredentialSelectBy.values.byName(_select_by); + CredentialSelectBy? get select_by => + maybeEnum(_select_by, CredentialSelectBy.values); } /// The type of the `callback` used to create an [IdConfiguration]. @@ -285,20 +338,69 @@ extension CredentialResponseExtension on CredentialResponse { /// attribute. typedef CallbackFn = void Function(CredentialResponse credentialResponse); -/* -// Method: google.accounts.id.renderButton -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.renderButton -// -// Data type: GsiButtonConfiguration -// https://developers.google.com/identity/gsi/web/reference/js-reference#GsiButtonConfiguration -// -// Question: Do we need to implement renderButton and its options? -*/ +/// The configuration object for the [renderButton] method. +/// +/// Data type: GsiButtonConfiguration +/// https://developers.google.com/identity/gsi/web/reference/js-reference#GsiButtonConfiguration +@JS() +@anonymous +@staticInterop +abstract class GsiButtonConfiguration { + /// Constructs an options object for the [renderButton] method. + /// + /// The following properties need to be manually wrapped in [allowInterop] + /// before being passed to this constructor: + external factory GsiButtonConfiguration({ + /// The button type. + ButtonType type, -/* -// Data type: Credential -// https://developers.google.com/identity/gsi/web/reference/js-reference#type-Credential -*/ + /// The button theme. + ButtonTheme theme, + + /// The button size. + ButtonSize size, + + /// The button text. + ButtonText text, + + /// The button shape. + ButtonShape shape, + + /// The Google logo alignment in the button. + ButtonLogoAlignment logo_alignment, + + /// The minimum button width, in pixels. + /// + /// The maximum width is 400 pixels. + double width, + + /// The pre-set locale of the button text. + /// + /// If not set, the browser's default locale or the Google session user's + /// preference is used. + String locale, + + /// A function to be called when the button is clicked. + GsiButtonClickListenerFn click_listener, + }); +} + +/// The object passed as an optional parameter to `click_listener` function. +@JS() +@staticInterop +abstract class GsiButtonData {} + +/// The fields that are contained in the button data. +extension GsiButtonDataExtension on GsiButtonData { + /// Nonce + external String? get nonce; + + /// State + external String? get state; +} + +/// The type of the [GsiButtonConfiguration] `click_listener` function. +typedef GsiButtonClickListenerFn = void Function(GsiButtonData? gsiButtonData); /// The object passed to the [NativeCallbackFn]. Represents a PasswordCredential /// that was returned by the Browser. @@ -307,6 +409,9 @@ typedef CallbackFn = void Function(CredentialResponse credentialResponse); /// in the browser through the [storeCredential] method. /// /// See also: https://developer.mozilla.org/en-US/docs/Web/API/PasswordCredential/PasswordCredential +/// +/// Data type: Credential +/// https://developers.google.com/identity/gsi/web/reference/js-reference#type-Credential @JS() @anonymous @staticInterop @@ -333,95 +438,22 @@ extension CredentialExtension on Credential { /// from the native Credential manager of the user's browser. typedef NativeCallbackFn = void Function(Credential credential); -/* -// Method: google.accounts.id.disableAutoselect -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.disableAutoSelect -*/ - -/// When the user signs out of your website, you need to call this method to -/// record the status in cookies. -/// -/// This prevents a UX dead loop. -@JS() -external VoidFn get disableAutoSelect; - -/* -// Method: google.accounts.id.storeCredential -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.storeCredential -*/ - -/// This method is a simple wrapper for the `store` method of the browser's -/// native credential manager API. -/// -/// It can only be used to store a Password [Credential]. -/// -/// See: https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/store -@JS() -external StoreCredentialFn get storeCredential; - -/// The type of the [storeCredential] function. -/// -/// The [callback] parameter must be manually wrapped in [allowInterop] -/// before being passed to the [storeCredential] function. -// Question: What's the type of the callback function??? VoidFn? -typedef StoreCredentialFn = void Function( - Credential credential, - Function? callback, -); - -/* -// Method: google.accounts.id.cancel -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.cancel -*/ - -/// You can cancel the One Tap flow if you remove the prompt from the relying -/// party DOM. The cancel operation is ignored if a credential is already -/// selected. -@JS() -external VoidFn get cancel; - /* // Library load callback: onGoogleLibraryLoad // https://developers.google.com/identity/gsi/web/reference/js-reference#onGoogleLibraryLoad // See: `load_callback.dart` and `loader.dart` */ -/* -// Method: google.accounts.id.revoke -// https://developers.google.com/identity/gsi/web/reference/js-reference#google.accounts.id.revoke -*/ - -/// The `revoke` method revokes the OAuth grant used to share the ID token for -/// the specified user. -@JS() -external RevokeFn get revoke; - -/// The type of the [revoke] function. -/// -/// [hint] is the email address or unique ID of the user's Google Account. The -/// ID is the `sub` property of the [CredentialResponse.credential] payload. -/// -/// The optional [callback] is a function that gets called to report on the -/// success of the revocation call. -/// -/// The [callback] parameter must be manually wrapped in [allowInterop] -/// before being passed to the [revoke] function. -typedef RevokeFn = void Function(String hint, - [RevocationResponseHandlerFn callback]); - /// The type of the `callback` function passed to [revoke], to be notified of /// the success of the revocation operation. typedef RevocationResponseHandlerFn = void Function( RevocationResponse revocationResponse, ); -/* -// Data type: RevocationResponse -// https://developers.google.com/identity/gsi/web/reference/js-reference#RevocationResponse -*/ - -/// The parameter passed to the optional [RevocationResponseHandlerFn] -/// `callback` of the [revoke] function. +/// The parameter passed to the `callback` of the [revoke] function. +/// +/// Data type: RevocationResponse +/// https://developers.google.com/identity/gsi/web/reference/js-reference#RevocationResponse @JS() @staticInterop abstract class RevocationResponse {} diff --git a/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart index b320580473..83061d0b9d 100644 --- a/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart +++ b/packages/google_identity_services_web/lib/src/js_interop/google_accounts_oauth2.dart @@ -9,34 +9,84 @@ // * non_constant_identifier_names required to be able to use the same parameter // names as the underlying library. -@JS('google.accounts.oauth2') -library oauth2; +@JS() +library google_accounts_oauth2; import 'package:js/js.dart'; +import 'dom.dart'; import 'shared.dart'; -// Code Client +/// Binding to the `google.accounts.oauth2` JS global. +/// +/// See: https://developers.google.com/identity/oauth2/web/reference/js-reference +@JS('google.accounts.oauth2') +external GoogleAccountsOauth2 get oauth2; -/* -// Method: google.accounts.oauth2.initCodeClient -// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initCodeClient -*/ - -/// The initCodeClient method initializes and returns a code client, with the -/// passed-in [config]. +/// The Dart definition of the `google.accounts.oauth2` global. @JS() -external InitCodeClientFn get initCodeClient; +@staticInterop +abstract class GoogleAccountsOauth2 {} -/// The type of the [initCodeClient] function. -typedef InitCodeClientFn = CodeClient Function(CodeClientConfig config); +/// The `google.accounts.oauth2` methods +extension GoogleAccountsOauth2Extension on GoogleAccountsOauth2 { + /// Initializes and returns a code client, with the passed-in [config]. + /// + /// Method: google.accounts.oauth2.initCodeClient + /// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initCodeClient + external CodeClient initCodeClient(CodeClientConfig config); -/* -// Data type: CodeClientConfig -// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeClientConfig -*/ + /// Initializes and returns a token client, with the passed-in [config]. + /// + /// Method: google.accounts.oauth2.initTokenClient + /// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initTokenClient + external TokenClient initTokenClient(TokenClientConfig config); + + // Method: google.accounts.oauth2.hasGrantedAllScopes + // https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAllScopes + @JS('hasGrantedAllScopes') + external bool _hasGrantedScope(TokenResponse token, String scope); + + /// Checks if hte user has granted **all** the specified [scopes]. + /// + /// [scopes] is a space-separated list of scope names. + /// + /// Method: google.accounts.oauth2.hasGrantedAllScopes + /// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAllScopes + bool hasGrantedAllScopes(TokenResponse tokenResponse, List scopes) { + return scopes + .every((String scope) => _hasGrantedScope(tokenResponse, scope)); + } + + /// Checks if hte user has granted **all** the specified [scopes]. + /// + /// [scopes] is a space-separated list of scope names. + /// + /// Method: google.accounts.oauth2.hasGrantedAllScopes + /// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAllScopes + bool hasGrantedAnyScopes(TokenResponse tokenResponse, List scopes) { + return scopes.any((String scope) => _hasGrantedScope(tokenResponse, scope)); + } + + /// Revokes all of the scopes that the user granted to the app. + /// + /// A valid [accessToken] is required to revoke permissions. + /// + /// The [done] callback is called once the revoke action is done. It must be + /// manually wrapped in [allowInterop] before being passed to this method. + /// + /// Method: google.accounts.oauth2.revoke + /// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.revoke + external void revoke( + String accessToken, [ + RevokeTokenDoneFn done, + ]); +} /// The configuration object for the [initCodeClient] method. +/// +/// Data type: CodeClientConfig +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeClientConfig @JS() @anonymous @staticInterop @@ -51,6 +101,7 @@ abstract class CodeClientConfig { String? redirect_uri, bool? auto_select, CodeClientCallbackFn? callback, + ErrorCallbackFn? error_callback, String? state, bool? enable_serial_consent, String? hint, @@ -60,14 +111,12 @@ abstract class CodeClientConfig { }); } -/* -// Data type: CodeClient -// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeClient -*/ - /// A client that can start the OAuth 2.0 Code UX flow. /// /// See: https://developers.google.com/identity/oauth2/web/guides/use-code-model +/// +/// Data type: CodeClient +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeClient @JS() @staticInterop abstract class CodeClient {} @@ -78,12 +127,10 @@ extension CodeClientExtension on CodeClient { external void requestCode(); } -/* -// Data type: CodeResponse -// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeResponse -*/ - /// The object passed as the parameter of your [CodeClientCallbackFn]. +/// +/// Data type: CodeResponse +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeResponse @JS() @staticInterop abstract class CodeResponse {} @@ -116,27 +163,10 @@ extension CodeResponseExtension on CodeResponse { /// The type of the `callback` function passed to [CodeClientConfig]. typedef CodeClientCallbackFn = void Function(CodeResponse response); -// Token Client - -/* -// Method: google.accounts.oauth2.initTokenClient -// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.initTokenClient -*/ - -/// The initCodeClient method initializes and returns a code client, with the -/// passed-in [config]. -@JS() -external InitTokenClientFn get initTokenClient; - -/// The type of the [initCodeClient] function. -typedef InitTokenClientFn = TokenClient Function(TokenClientConfig config); - -/* -// Data type: TokenClientConfig -// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClientConfig -*/ - /// The configuration object for the [initTokenClient] method. +/// +/// Data type: TokenClientConfig +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClientConfig @JS() @anonymous @staticInterop @@ -149,6 +179,7 @@ abstract class TokenClientConfig { required String client_id, required TokenClientCallbackFn? callback, required String scope, + ErrorCallbackFn? error_callback, String? prompt, bool? enable_serial_consent, String? hint, @@ -157,14 +188,12 @@ abstract class TokenClientConfig { }); } -/* -// Data type: TokenClient -// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClient -*/ - /// A client that can start the OAuth 2.0 Token UX flow. /// /// See: https://developers.google.com/identity/oauth2/web/guides/use-token-model +/// +/// Data type: TokenClient +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenClient @JS() @staticInterop abstract class TokenClient {} @@ -177,13 +206,10 @@ extension TokenClientExtension on TokenClient { ]); } -/* -// Data type: OverridableTokenClientConfig -// https://developers.google.com/identity/oauth2/web/reference/js-reference#OverridableTokenClientConfig -*/ - -/// The overridable configuration object for the -/// [TokenClientExtension.requestAccessToken] method. +/// The overridable configuration object for the [TokenClientExtension.requestAccessToken] method. +/// +/// Data type: OverridableTokenClientConfig +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#OverridableTokenClientConfig @JS() @anonymous @staticInterop @@ -229,15 +255,18 @@ abstract class OverridableTokenClientConfig { /// uses to maintain state between your authorization request and the /// authorization server's response. String? state, + + /// Preserves previously requested scopes in this new request. + /// + /// (Undocumented) + bool? include_granted_scopes, }); } -/* -// Data type: TokenResponse -// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse -*/ - /// The object passed as the parameter of your [TokenClientCallbackFn]. +/// +/// Data type: TokenResponse +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse @JS() @staticInterop abstract class TokenResponse {} @@ -283,63 +312,55 @@ extension TokenResponseExtension on TokenResponse { /// The type of the `callback` function passed to [TokenClientConfig]. typedef TokenClientCallbackFn = void Function(TokenResponse response); -/* -// Method: google.accounts.oauth2.hasGrantedAllScopes -// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAllScopes -*/ +/// The type of the `error_callback` in both oauth2 initXClient calls. +/// +/// (Currently undocumented) +/// +/// `error` should be of type [GoogleIdentityServicesError]?, but it cannot be +/// because of this DDC bug: https://github.com/dart-lang/sdk/issues/50899 +typedef ErrorCallbackFn = void Function(Object? error); -/// Checks if the user granted **all** the specified scopes. +/// An error returned by `initTokenClient` or `initDataClient`. +/// +/// Cannot be used: https://github.com/dart-lang/sdk/issues/50899 @JS() -external HasGrantedScopesFn get hasGrantedAllScopes; +@staticInterop +abstract class GoogleIdentityServicesError extends DomError {} -/* -// Method: google.accounts.oauth2.hasGrantedAnyScopes -// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.hasGrantedAnyScopes -*/ +/// Methods of the GoogleIdentityServicesError object. +/// +/// Cannot be used: https://github.com/dart-lang/sdk/issues/50899 +extension GoogleIdentityServicesErrorExtension on GoogleIdentityServicesError { + @JS('type') + external String get _type; + // String get _type => js_util.getProperty(this, 'type'); -/// Checks if the user granted **any** of the specified scopes. + /// The type of error + GoogleIdentityServicesErrorType get type => + GoogleIdentityServicesErrorType.values.byName(_type); +} + +/// The signature of the `done` function for [revoke]. +typedef RevokeTokenDoneFn = void Function(TokenRevocationResponse response); + +/// The parameter passed to the `callback` of the [revoke] function. +/// +/// Data type: RevocationResponse +/// https://developers.google.com/identity/oauth2/web/reference/js-reference#TokenResponse @JS() -external HasGrantedScopesFn get hasGrantedAnyScopes; +@staticInterop +abstract class TokenRevocationResponse {} -/// The signature for functions that check if any/all scopes have been granted. -/// -/// Used by [hasGrantedAllScopes] and [hasGrantedAnyScope]. -typedef HasGrantedScopesFn = bool Function( - TokenResponse tokenResponse, - String firstScope, [ - String? scope2, - String? scope3, - String? scope4, - String? scope5, - String? scope6, - String? scope7, - String? scope8, - String? scope9, - String? scope10, -]); +/// The fields that are contained in the [TokenRevocationResponse] object. +extension TokenRevocationResponseExtension on TokenRevocationResponse { + /// This field is a boolean value set to true if the revoke method call + /// succeeded or false on failure. + external bool get successful; -/* -// Method: google.accounts.oauth2.revoke -// https://developers.google.com/identity/oauth2/web/reference/js-reference#google.accounts.oauth2.revoke -*/ + /// This field is a string value and contains a detailed error message if the + /// revoke method call failed, it is undefined on success. + external String? get error; -/// The [revokeToken] method revokes all of the scopes that the user granted to -/// the app. A valid access token is required to revoke the permission. -/// -/// The `done` callback is called once the revoke action is done. -@JS('revoke') -external RevokeTokenFn get revokeToken; - -/// The signature of the [revokeToken] function. -/// -/// The (optional) [done] parameter must be manually wrapped in [allowInterop] -/// before being passed to the [revokeToken] function. -typedef RevokeTokenFn = void Function( - String accessToken, [ - RevokeTokenDoneFn done, -]); - -/// The signature of the `done` function for [revokeToken]. -/// -/// Work in progress here: b/248628502 -typedef RevokeTokenDoneFn = void Function(String jsonError); + /// The description of the error. + external String? get error_description; +} diff --git a/packages/google_identity_services_web/lib/src/js_interop/shared.dart b/packages/google_identity_services_web/lib/src/js_interop/shared.dart index 4e35f38413..0bd842b3e0 100644 --- a/packages/google_identity_services_web/lib/src/js_interop/shared.dart +++ b/packages/google_identity_services_web/lib/src/js_interop/shared.dart @@ -213,3 +213,159 @@ enum CredentialSelectBy { @override String toString() => _selectBy; } + +/// The type of button to be rendered. +/// +/// https://developers.google.com/identity/gsi/web/reference/js-reference#type +enum ButtonType { + /// A button with text or personalized information. + standard('standard'), + + /// An icon button without text. + icon('icon'); + + /// + const ButtonType(String type) : _type = type; + final String _type; + + @override + String toString() => _type; +} + +/// The theme of the button to be rendered. +/// +/// https://developers.google.com/identity/gsi/web/reference/js-reference#theme +enum ButtonTheme { + /// A standard button theme. + outline('outline'), + + /// A blue-filled button theme. + filled_blue('filled_blue'), + + /// A black-filled button theme. + filled_black('filled_black'); + + /// + const ButtonTheme(String theme) : _theme = theme; + final String _theme; + + @override + String toString() => _theme; +} + +/// The theme of the button to be rendered. +/// +/// https://developers.google.com/identity/gsi/web/reference/js-reference#size +enum ButtonSize { + /// A large button (about 40px tall). + large('large'), + + /// A medium-sized button (about 32px tall). + medium('medium'), + + /// A small button (about 20px tall). + small('small'); + + /// + const ButtonSize(String size) : _size = size; + final String _size; + + @override + String toString() => _size; +} + +/// The button text. +/// +/// https://developers.google.com/identity/gsi/web/reference/js-reference#text +enum ButtonText { + /// The button text is "Sign in with Google". + signin_with('signin_with'), + + /// The button text is "Sign up with Google". + signup_with('signup_with'), + + /// The button text is "Continue with Google". + continue_with('continue_with'), + + /// The button text is "Sign in". + signin('signin'); + + /// + const ButtonText(String text) : _text = text; + final String _text; + + @override + String toString() => _text; +} + +/// The button shape. +/// +/// https://developers.google.com/identity/gsi/web/reference/js-reference#shape +enum ButtonShape { + /// The rectangular-shaped button. + /// + /// If used for the [ButtonType.icon], then it's the same as [square]. + rectangular('rectangular'), + + /// The pill-shaped button. + /// + /// If used for the [ButtonType.icon], then it's the same as [circle]. + pill('pill'), + + /// The circle-shaped button. + /// + /// If used for the [ButtonType.standard], then it's the same as [pill]. + circle('circle'), + + /// The square-shaped button. + /// + /// If used for the [ButtonType.standard], then it's the same as [rectangular]. + square('square'); + + /// + const ButtonShape(String shape) : _shape = shape; + final String _shape; + + @override + String toString() => _shape; +} + +/// The type of button to be rendered. +/// +/// https://developers.google.com/identity/gsi/web/reference/js-reference#type +enum ButtonLogoAlignment { + /// Left-aligns the Google logo. + left('left'), + + /// Center-aligns the Google logo. + center('center'); + + /// + const ButtonLogoAlignment(String alignment) : _alignment = alignment; + final String _alignment; + + @override + String toString() => _alignment; +} + +/// The `type` of the error object passed into the `error_callback` function. +enum GoogleIdentityServicesErrorType { + /// Missing required parameter. + missing_required_parameter('missing_required_parameter'), + + /// The popup was closed before the flow was completed. + popup_closed('popup_closed'), + + /// Popup failed to open. + popup_failed_to_open('popup_failed_to_open'), + + /// Unknown error. + unknown('unknown'); + + /// + const GoogleIdentityServicesErrorType(String type) : _type = type; + final String _type; + + @override + String toString() => _type; +} diff --git a/packages/google_identity_services_web/pubspec.yaml b/packages/google_identity_services_web/pubspec.yaml index 3ba6774f79..365deda5bb 100644 --- a/packages/google_identity_services_web/pubspec.yaml +++ b/packages/google_identity_services_web/pubspec.yaml @@ -2,7 +2,7 @@ name: google_identity_services_web description: A Dart JS-interop layer for Google Identity Services. Google's new sign-in SDK for Web that supports multiple types of credentials. repository: https://github.com/flutter/packages/tree/main/packages/google_identity_services_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_identiy_services_web%22 -version: 0.1.1 +version: 0.2.0 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/packages/google_identity_services_web/test/README.md b/packages/google_identity_services_web/test/README.md index 8a783c2b32..38faf11403 100644 --- a/packages/google_identity_services_web/test/README.md +++ b/packages/google_identity_services_web/test/README.md @@ -1,3 +1,8 @@ # Tests Use `dart run tool/run_tests.dart` to run tests in this package. + +## Failed to run Chrome: No such file or directory + +Ensure the correct path to the Chrome executable is set in `dart_test.yaml`. It +may be other than `chrome` (for example, `google-chrome` in my machine).