[gis_web] Properly define optional args. Add scope to config. (#2688)

diff --git a/.cirrus.yml b/.cirrus.yml
index 9067fbb..c59853a 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -233,7 +233,12 @@
       build_script:
         - ./script/tool_runner.sh build-examples --web
       drive_script:
-        - ./script/tool_runner.sh drive-examples --web
+        # google_identity_services_web tests need Dart >=2.17, which is not available in stable.
+        - if [[ "$CHANNEL" == "master" ]]; then
+        -   ./script/tool_runner.sh drive-examples --web
+        - else
+        -   ./script/tool_runner.sh drive-examples --web --exclude=google_identity_services_web
+        - fi
     - name: web_benchmarks_test
       env:
         matrix:
diff --git a/packages/google_identity_services_web/CHANGELOG.md b/packages/google_identity_services_web/CHANGELOG.md
index 6073234..0ec9358 100644
--- a/packages/google_identity_services_web/CHANGELOG.md
+++ b/packages/google_identity_services_web/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.1.1
+
+* Add optional `scope` to `OverridableTokenClientConfig` object.
+* Mark some callbacks as optional properly.
+
 ## 0.1.0
 
 * Initial release.
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
new file mode 100644
index 0000000..b1c1b02
--- /dev/null
+++ b/packages/google_identity_services_web/example/integration_test/js_interop_gis_id_test.dart
@@ -0,0 +1,101 @@
+// 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<id.PromptMomentNotification> controller =
+          StreamController<id.PromptMomentNotification>();
+
+      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<id.CredentialResponse> controller =
+          StreamController<id.CredentialResponse>();
+
+      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<void> installGisMock() {
+  final Completer<void> completer = Completer<void>();
+  final DomHtmlScriptElement script =
+      document.createElement('script') as DomHtmlScriptElement;
+  script.src = 'mock-gis.js';
+  setProperty(script, 'type', 'module');
+  callMethod(script, 'addEventListener', <Object>[
+    'load',
+    allowInterop((_) {
+      completer.complete();
+    })
+  ]);
+  document.head.appendChild(script);
+  return completer.future;
+}
+
+void setMockCredentialResponse([String value = 'default_value']) {
+  callMethod(
+    _getGoogleAccountsId(),
+    'setMockCredentialResponse',
+    <Object>[value, 'auto'],
+  );
+}
+
+Object _getGoogleAccountsId() {
+  return _getDeepProperty<Object>(domWindow, 'google.accounts.id');
+}
+
+// Attempts to retrieve a deeply nested property from a jsObject (or die tryin')
+T _getDeepProperty<T>(Object jsObject, String deepProperty) {
+  final List<String> properties = deepProperty.split('.');
+  return properties.fold(
+    jsObject,
+    (Object jsObj, String prop) => getProperty<Object>(jsObj, prop),
+  ) as T;
+}
diff --git a/packages/google_identity_services_web/example/pubspec.yaml b/packages/google_identity_services_web/example/pubspec.yaml
index e7d6429..a3dcd80 100644
--- a/packages/google_identity_services_web/example/pubspec.yaml
+++ b/packages/google_identity_services_web/example/pubspec.yaml
@@ -17,8 +17,12 @@
 
 dev_dependencies:
   build_runner: ^2.1.10 # To extract README excerpts only.
+  flutter_driver:
+    sdk: flutter
   flutter_test:
     sdk: flutter
+  integration_test:
+    sdk: flutter
 
 flutter:
   uses-material-design: true
diff --git a/packages/google_identity_services_web/example/test_driver/integration_test.dart b/packages/google_identity_services_web/example/test_driver/integration_test.dart
new file mode 100644
index 0000000..4f10f2a
--- /dev/null
+++ b/packages/google_identity_services_web/example/test_driver/integration_test.dart
@@ -0,0 +1,7 @@
+// 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:integration_test/integration_test_driver.dart';
+
+Future<void> main() => integrationDriver();
diff --git a/packages/google_identity_services_web/example/web/mock-gis.js b/packages/google_identity_services_web/example/web/mock-gis.js
new file mode 100644
index 0000000..56701e2
--- /dev/null
+++ b/packages/google_identity_services_web/example/web/mock-gis.js
@@ -0,0 +1,144 @@
+// 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 used to mock the GIS library for integration tests under:
+// example/integration_test
+
+class PromptMomentNotification {
+  constructor(momentType, reason) {
+    this.momentType = momentType;
+    this.reason = reason;
+    this.getNotDisplayedReason = this._getReason;
+    this.getSkippedReason = this._getReason;
+    this.getDismissedReason = this._getReason;
+  }
+  getMomentType() { return this.momentType; }
+  _getReason() { return this.reason; }
+  isDismissedMoment() { return this.momentType === "dismissed" }
+  isDisplayMoment() { return this.momentType === "display" }
+  isSkippedMoment() { return this.momentType === "skipped" }
+  isDisplayed() { return this.isDisplayMoment() && !this.reason; }
+  isNotDisplayed() { return this.isDisplayMoment() && this.reason; }
+}
+
+const CREDENTIAL_RETURNED = new PromptMomentNotification("dismissed", "credential_returned");
+const USER_CANCEL = new PromptMomentNotification("skipped", "user_cancel");
+
+function callAsync(func, timeout = 100) {
+  window.setTimeout(func, timeout)
+}
+
+class Id {
+  initialize(config) {
+    this.config = config;
+  }
+  prompt(momentListener) {
+    callAsync(() => {
+      if (this.mockCredentialResponse) {
+        let callback = this.config.callback;
+        if (callback) {
+          callback(this.mockCredentialResponse);
+        }
+        if (momentListener) {
+          momentListener(CREDENTIAL_RETURNED);
+        }
+      } else if (momentListener) {
+        momentListener(USER_CANCEL);
+      }
+    });
+  }
+  setMockCredentialResponse(credential, select_by) {
+    this.mockCredentialResponse = {
+      credential: credential,
+      select_by: select_by,
+    };
+  }
+  disableAutoSelect() {}
+  storeCredential() {}
+  cancel() {}
+  revoke(hint, callback) {
+    this.mockCredentialResponse = null;
+    if (!callback) {
+      return;
+    }
+    callAsync(() => {
+      callback({
+        successful: true,
+        error: 'Revoked ' + hint,
+      });
+    })
+  }
+}
+
+class CodeClient {
+  constructor(config) {
+    this.config = config;
+  }
+  requestCode() {
+    let callback = this.config.callback;
+    if (!callback) {
+      return;
+    }
+    callAsync(() => {
+      callback(this.codeResponse);
+    });
+  }
+  setMockCodeResponse(codeResponse) {
+    this.codeResponse = codeResponse;
+  }
+}
+
+class TokenClient {
+  constructor(config) {
+    this.config = config;
+  }
+  requestAccessToken(overridableConfig) {
+    this.overridableConfig = overridableConfig;
+    let callback = this.overridableConfig.callback || this.config.callback;
+    if (!callback) {
+      return;
+    }
+    callAsync(() => {
+      callback(this.tokenResponse);
+    });
+  }
+  setMockTokenResponse(tokenResponse) {
+    this.tokenResponse = tokenResponse;
+  }
+}
+
+class Oauth2 {
+  initCodeClient(config) {
+    return new CodeClient(config);
+  }
+  initTokenClient(config) {
+    return new TokenClient(config);
+  }
+  hasGrantedAllScopes(tokenResponse, scope, ...scopes) {
+    return tokenResponse != null;
+  }
+  hasGrantedAnyScopes(tokenResponse, scope, ...scopes) {
+    return tokenResponse != null;
+  }
+  revoke(accessToken, done) {
+    if (!done) {
+      return;
+    }
+    callAsync(() => {
+      done();
+    })
+  }
+}
+
+function mockGis() {
+  let goog = {
+    accounts: {
+      id: new Id(),
+      oauth2: new Oauth2(),
+    }
+  };
+  globalThis['google'] = goog;
+}
+
+mockGis();
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 52fd806..579662d 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
@@ -190,7 +190,7 @@
 ///
 /// The [momentListener] parameter must be manually wrapped in [allowInterop]
 /// before being passed to the [prompt] function.
-typedef PromptFn = void Function(PromptMomentListenerFn? momentListener);
+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);
@@ -406,10 +406,8 @@
 ///
 /// The [callback] parameter must be manually wrapped in [allowInterop]
 /// before being passed to the [revoke] function.
-typedef RevokeFn = void Function(
-  String hint,
-  RevocationResponseHandlerFn? callback,
-);
+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.
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 3986138..b320580 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
@@ -219,6 +219,12 @@
     /// About Multiple Sign-in: https://support.google.com/accounts/answer/1721977
     String? hint,
 
+    /// A space-delimited list of scopes that identify the resources that your
+    /// application could access on the user's behalf. These values inform the
+    /// consent screen that Google displays to the user.
+    // b/251971390
+    String? scope,
+
     /// **Not recommended.** Specifies any string value that your application
     /// uses to maintain state between your authorization request and the
     /// authorization server's response.
@@ -330,7 +336,7 @@
 /// before being passed to the [revokeToken] function.
 typedef RevokeTokenFn = void Function(
   String accessToken, [
-  RevokeTokenDoneFn? done,
+  RevokeTokenDoneFn done,
 ]);
 
 /// The signature of the `done` function for [revokeToken].
diff --git a/packages/google_identity_services_web/pubspec.yaml b/packages/google_identity_services_web/pubspec.yaml
index d84a687..3ba6774 100644
--- a/packages/google_identity_services_web/pubspec.yaml
+++ b/packages/google_identity_services_web/pubspec.yaml
@@ -2,7 +2,7 @@
 description: A Dart JS-interop layer for Google Identity Services. Google's new sign-in SDK for Web that supports multiple types of credentials.
 repository: https://github.com/flutter/packages/tree/main/packages/google_identity_services_web
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_identiy_services_web%22
-version: 0.1.0
+version: 0.1.1
 
 environment:
   sdk: ">=2.17.0 <3.0.0"