Skip to content

[google_sign_in] Redesign API for current identity SDKs #9267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

stuartmorgan-g
Copy link
Contributor

This is a full overhaul of the google_sign_in API, with breaking changes for all component packages—including the platform interface. The usual model of adding the new approach while keeping the old one is not viable here, as the underlying SDKs have changed significantly since the original API was designed. Web already had some only-partially-compatible shims for this reason, and Android would have had to do something similar; see flutter/flutter#119300 and flutter/flutter#154205, and the design doc for more background.

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2 3

@ycv005

This comment was marked as off-topic.

@stuartmorgan-g

This comment was marked as off-topic.

@stuartmorgan-g
Copy link
Contributor Author

Looks like this will need either #9168 or a localized SDK 35 change.

@stuartmorgan-g stuartmorgan-g added the override: allow breaking change Override the check preventing breaking changes to platform interfaces label May 19, 2025
Copy link
Member

@ditman ditman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I have anything blocking. I took a look at google_sign_in, google_sign_in_platform_interface and google_sign_in_web.

isAuthorized = await _googleSignIn.canAccessScopes(scopes);
}
const List<String> scopes = <String>[
'email',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'email' seems to me like an "old school" scope docs, maybe don't show it in this example, or replace by 'https://www.googleapis.com/auth/userinfo.email'?


Once your app determines that the current user `isAuthorized` to access the
services for which you need `scopes`, it can proceed normally.

### Authorization expiration

In the web, **the `accessToken` is no longer refreshed**. It expires after 3600
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon re-reading this... is this paragraph is a little bit incomplete? IIRC The authentication token (idToken) is also not refreshed, so both authorization and authentication end up expiring, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks that way. I can adjust the README accordingly, but maybe you just didn't mention it because normally (IIUC) clients don't need to keep using ID tokens they way they do access tokens.

user = event.user;
case GoogleSignInAuthenticationEventSignOut():
user = null;
case GoogleSignInAuthenticationEventException():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not specifically related to this example, but... why are we adding something that looks like an "error" event through the onData callback, instead of adding them as an error to the stream so they can be dealt with by a separate onError handler?

Adding an error to the stream also makes the case where the user is doing await stream.first throwy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I just got carried away with structured error returns on iOS and Android. I'll revisit this for both layers of streams.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO I would only change the user-facing stream. In the implementation of the plugin we're already looking at the continuous stream of events coming from the platform code, so maybe the platform -> plugin stream can continue be a single one, with error events ("it never crashes") but the plugin -> app stream can emit errors normally? But yeah, if there's a benefit to making the platform -> plugin also emit errors, I won't oppose it changing :)


/// The scopes required by this application.
// #docregion Initialize
// #docregion CheckAuthorization
const List<String> scopes = <String>[
'email',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Mentioned this in the readme above, but again, this 'email' scope might not be needed.)

await user.authorizationClient.authorizationHeaders(scopes);
if (headers == null) {
setState(() {
_contactText = 'Failed to construct authorization headers.';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be _errorMessage, probably?

@@ -27,134 +24,129 @@ abstract class GoogleSignInPlatform extends PlatformInterface {

static final Object _token = Object();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use this opportunity to clean up this _token and the set instance setter tricks and make the abstract class GoogleSignInPlatform an abstract base class? Should this be a separate technical debt cleanup issue? (Are there any downsides to this that I'm not seeing? :S)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did seriously consider doing this, since it can only be done during a breaking change to the platform interface.

I may still tackle it here for that reason, but the downside is that I would have to replace the Mockito-generated mocks with manual mocks, and rewrite the tests accordingly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the downside is that I would have to replace the Mockito-generated mocks with manual mocks, and rewrite the tests accordingly.

Ah yes...

Why is this change considered breaking, though? Is it just in case people are using "implements" for their own federated versions of the plugin, or mocks? If you're using the class "as intended" (with extends), your code shouldn't need to change by us moving it to base class. Would it?

One upside of the extra work, should you want to tackle it, is that the mocks that you create here can be distributed as a testing library from the package, so those users who were creating their own mocks can use officially maintained mock classes that we vend. Similar to: https://pub.dev/documentation/http/1.4.0/testing

.thenAnswer((_) => Future<GoogleSignInUserData>.value(someUser));
group('clientAuthorizationTokensForScopes', () {
const String someAccessToken = '50m3_4cc35_70k3n';
const List<String> scopes = <String>['scope1', 'scope2'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to retain the 'asserts no scopes have any spaces' functionality and test here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was confused by the fact that the web implementation was asserting this in the first place. Is there some web-specific extra requirement on scopes? Is having spaces that shouldn't be there different from just typoing the scope?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code comes from the first version of the plugin, and this is why I added this, when asked about "why not put this in the app-facing package":

flutter/plugins#2280 (comment)

[Spaces are] a problem here because we use the space as a "join" character to pass the scopes to JS. In the mobile versions, the List is preserved all the way through.

I just checked the API docs again, here: https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#redirecting and scope is still "A space-delimited list of scopes that identify the resources that your application could access on the user's behalf."

IMO we should still assert (and test the assert) that scopes don't contain spaces, but it doesn't look like a super big source of problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting; I'll put it back with a comment explaining it. I wonder if the mobile SDKs fail under the hood, or if they escape the spaces.

@@ -52,7 +52,6 @@ void main() {
expect(user.id, expectedPersonId);
expect(user.displayName, expectedPersonName);
expect(user.photoUrl, expectedPersonPhoto);
expect(user.idToken, isNull);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we no longer did the People API fallback on the web? Maybe this whole test can be removed as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did indeed just forget to delete these files.

@@ -12,7 +12,6 @@ import 'package:web/web.dart' as web;

import 'button_configuration.dart'
show GSIButtonConfiguration, convertButtonConfiguration;
import 'people.dart' as people;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this people.dart file is now dead code, you can maybe delete it and its tests?

Copy link
Contributor

@camsim99 camsim99 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the Android implementation!

Future<PlatformGoogleIdTokenCredential?> _authenticate({
required bool filterToAuthorized,
required bool autoSelectEnabled,
required bool useButtonFlow,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if useButtonFlow is true, then basically the other options are ignored? Would it be helpful to warn users here about that?

Comment on lines +376 to +380
null,
0,
0,
0,
null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can you leave a comment for each of these params about what they are since it's not obvious?

}
Future<void> disconnect(DisconnectParams params) async {
// TODO(stuartmorgan): Implement this once Credential Manager adds the
// necessary API (or temporarily implement it with the deprecated SDK).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we file an issue to track this since this is sorta a regression?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding a test for attachToActivity since it seems that the call to addActivityResultListener is critical? Maybe disposeActivity for the same reason.

ResultCompat.asCompatCallback(
reply -> {
// This is never called, since this test doesn't trigger the getCredentialsAsync callback.
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: could throw an exception to ensure it's not called.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding a test for the various parameter options for authorize?

@@ -13,3 +13,28 @@ should add it to your `pubspec.yaml` as usual.

[1]: https://pub.dev/packages/google_sign_in
[2]: https://flutter.dev/to/endorsed-federated-plugin

## Integration
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we consider linking to the credentials docs on this? https://developer.android.com/identity/sign-in/credential-manager-siwg#set-google for the non-firebase option? If it's more confusing than helpful because some steps don't apply, maybe not though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment