diff --git a/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md index 4a3d1e45998..6ce508403a2 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md +++ b/packages/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md @@ -1,5 +1,11 @@ -## NEXT - +## 3.0.0 + +* **BREAKING CHANGES**: + * The extension method is now on `GoogleSignInClientAuthorization` instead of + `GoogleSignIn`, so it must be used after completing an authorization flow. + * The extension method has been renamed to `authClient`. + * The extension method now requires passing `scopes`, matching those used to + request the authorization. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 2.0.13 diff --git a/packages/extension_google_sign_in_as_googleapis_auth/README.md b/packages/extension_google_sign_in_as_googleapis_auth/README.md index ddc66711d40..4a6fb1aca1f 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/README.md +++ b/packages/extension_google_sign_in_as_googleapis_auth/README.md @@ -19,19 +19,19 @@ That object can then be used to create instances of `googleapis` API clients: ```dart -// Retrieve an [auth.AuthClient] from the current [GoogleSignIn] instance. -final auth.AuthClient? client = await _googleSignIn.authenticatedClient(); - -assert(client != null, 'Authenticated client missing!'); - -// Prepare a People Service authenticated client. -final PeopleServiceApi peopleApi = PeopleServiceApi(client!); -// Retrieve a list of the `names` of my `connections` -final ListConnectionsResponse response = - await peopleApi.people.connections.list( - 'people/me', - personFields: 'names', -); +import 'package:googleapis_auth/googleapis_auth.dart' as auth show AuthClient; +// ยทยทยท + // Retrieve an [auth.AuthClient] from a GoogleSignInClientAuthorization. + final auth.AuthClient client = authorization.authClient(scopes: scopes); + + // Prepare a People Service authenticated client. + final PeopleServiceApi peopleApi = PeopleServiceApi(client); + // Retrieve a list of connected contacts' names. + final ListConnectionsResponse response = + await peopleApi.people.connections.list( + 'people/me', + personFields: 'names', + ); ``` ## Example diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart b/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart index 369135371ed..dd59b43c7af 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart @@ -10,13 +10,12 @@ import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sig import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/people/v1.dart'; +// #docregion CreateAPIClient import 'package:googleapis_auth/googleapis_auth.dart' as auth show AuthClient; +// #enddocregion CreateAPIClient -final GoogleSignIn _googleSignIn = GoogleSignIn( - // Optional clientId - // clientId: '[YOUR_OAUTH_2_CLIENT_ID]', - scopes: [PeopleServiceApi.contactsReadonlyScope], -); +/// The scopes used by this example. +const List scopes = [PeopleServiceApi.contactsReadonlyScope]; void main() { runApp( @@ -38,37 +37,84 @@ class SignInDemo extends StatefulWidget { /// The state of the main widget. class SignInDemoState extends State { + late Future _signInInitialized; GoogleSignInAccount? _currentUser; + GoogleSignInClientAuthorization? _authorization; String _contactText = ''; @override void initState() { super.initState(); - _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) { + + final GoogleSignIn signIn = GoogleSignIn.instance; + _signInInitialized = signIn.initialize( + // Add your client IDs here as necessary for your supported platforms. + ); + signIn.authenticationEvents.listen((GoogleSignInAuthenticationEvent event) { + if (!mounted) { + return; + } setState(() { - _currentUser = account; + switch (event) { + case GoogleSignInAuthenticationEventSignIn(): + _currentUser = event.user; + case GoogleSignInAuthenticationEventSignOut(): + _currentUser = null; + _authorization = null; + } }); + if (_currentUser != null) { - _handleGetContact(); + _checkAuthorization(); } + }).onError((Object error) { + debugPrint(error.toString()); + }); + + _signInInitialized.then((void value) { + signIn.attemptLightweightAuthentication(); }); - _googleSignIn.signInSilently(); } - Future _handleGetContact() async { + void _updateAuthorization(GoogleSignInClientAuthorization? authorization) { + if (!mounted) { + return; + } setState(() { - _contactText = 'Loading contact info...'; + _authorization = authorization; }); -// #docregion CreateAPIClient - // Retrieve an [auth.AuthClient] from the current [GoogleSignIn] instance. - final auth.AuthClient? client = await _googleSignIn.authenticatedClient(); + if (authorization != null) { + unawaited(_handleGetContact(authorization)); + } + } - assert(client != null, 'Authenticated client missing!'); + Future _checkAuthorization() async { + _updateAuthorization( + await _currentUser?.authorizationClient.authorizationForScopes(scopes)); + } + + Future _requestAuthorization() async { + _updateAuthorization(await _currentUser?.authorizationClient + .authorizeScopes([PeopleServiceApi.contactsReadonlyScope])); + } + + Future _handleGetContact( + GoogleSignInClientAuthorization authorization) async { + if (!mounted) { + return; + } + setState(() { + _contactText = 'Loading contact info...'; + }); + + // #docregion CreateAPIClient + // Retrieve an [auth.AuthClient] from a GoogleSignInClientAuthorization. + final auth.AuthClient client = authorization.authClient(scopes: scopes); // Prepare a People Service authenticated client. - final PeopleServiceApi peopleApi = PeopleServiceApi(client!); - // Retrieve a list of the `names` of my `connections` + final PeopleServiceApi peopleApi = PeopleServiceApi(client); + // Retrieve a list of connected contacts' names. final ListConnectionsResponse response = await peopleApi.people.connections.list( 'people/me', @@ -79,13 +125,15 @@ class SignInDemoState extends State { final String? firstNamedContactName = _pickFirstNamedContact(response.connections); - setState(() { - if (firstNamedContactName != null) { - _contactText = 'I see you know $firstNamedContactName!'; - } else { - _contactText = 'No contacts to display.'; - } - }); + if (mounted) { + setState(() { + if (firstNamedContactName != null) { + _contactText = 'I see you know $firstNamedContactName!'; + } else { + _contactText = 'No contacts to display.'; + } + }); + } } String? _pickFirstNamedContact(List? connections) { @@ -102,51 +150,72 @@ class SignInDemoState extends State { Future _handleSignIn() async { try { - await _googleSignIn.signIn(); + await GoogleSignIn.instance.authenticate(); } catch (error) { - print(error); // ignore: avoid_print + debugPrint(error.toString()); } } - Future _handleSignOut() => _googleSignIn.disconnect(); + // Call disconnect rather than signOut to more fully reset the example app. + Future _handleSignOut() => GoogleSignIn.instance.disconnect(); Widget _buildBody() { - final GoogleSignInAccount? user = _currentUser; - if (user != null) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ListTile( - leading: GoogleUserCircleAvatar( - identity: user, - ), - title: Text(user.displayName ?? ''), - subtitle: Text(user.email), - ), - const Text('Signed in successfully.'), - Text(_contactText), - ElevatedButton( - onPressed: _handleSignOut, - child: const Text('SIGN OUT'), - ), - ElevatedButton( - onPressed: _handleGetContact, - child: const Text('REFRESH'), - ), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text('You are not currently signed in.'), - ElevatedButton( - onPressed: _handleSignIn, - child: const Text('SIGN IN'), - ), - ], - ); - } + return FutureBuilder( + future: _signInInitialized, + builder: (BuildContext context, AsyncSnapshot snapshot) { + final GoogleSignInAccount? user = _currentUser; + final GoogleSignInClientAuthorization? authorization = _authorization; + final List children; + if (snapshot.hasError) { + children = [ + const Text('Error initializing sign in.'), + ]; + } else if (snapshot.connectionState == ConnectionState.done) { + children = [ + if (user != null) ...[ + ListTile( + leading: GoogleUserCircleAvatar( + identity: user, + ), + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), + ), + const Text('Signed in successfully.'), + if (authorization != null) ...[ + Text(_contactText), + ElevatedButton( + onPressed: () => _handleGetContact(authorization), + child: const Text('REFRESH'), + ), + ] else ...[ + ElevatedButton( + onPressed: _requestAuthorization, + child: const Text('LOAD CONTACTS'), + ), + ], + ElevatedButton( + onPressed: _handleSignOut, + child: const Text('SIGN OUT'), + ), + ] else ...[ + const Text('You are not currently signed in.'), + ElevatedButton( + onPressed: _handleSignIn, + child: const Text('SIGN IN'), + ), + ], + ]; + } else { + children = [ + const CircularProgressIndicator(), + ]; + } + + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ); + }); } @override diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml index a3dab382448..ab40142c25a 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: path: ../ flutter: sdk: flutter - google_sign_in: ^6.0.0 + google_sign_in: ^7.0.0 googleapis: ">=10.1.0 <14.0.0" googleapis_auth: ^1.1.0 diff --git a/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart b/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart index e23c5084758..1d9091ee08a 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart +++ b/packages/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart @@ -5,34 +5,29 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis_auth/googleapis_auth.dart' as gapis; import 'package:http/http.dart' as http; -import 'package:meta/meta.dart'; -/// Extension on [GoogleSignIn] that adds an `authenticatedClient` method. -/// -/// This method can be used to retrieve an authenticated [gapis.AuthClient] -/// client that can be used with the rest of the `googleapis` libraries. -extension GoogleApisGoogleSignInAuth on GoogleSignIn { - /// Retrieve a `googleapis` authenticated client. - Future authenticatedClient({ - @visibleForTesting GoogleSignInAuthentication? debugAuthentication, - @visibleForTesting List? debugScopes, - }) async { - final GoogleSignInAuthentication? auth = - debugAuthentication ?? await currentUser?.authentication; - final String? oauthTokenString = auth?.accessToken; - if (oauthTokenString == null) { - return null; - } +/// Extension on [GoogleSignInClientAuthorization] that adds an +/// `authClient` method. +extension GoogleApisGoogleSignInAuth on GoogleSignInClientAuthorization { + /// Returns an authenticated [gapis.AuthClient] client that can be used with + /// the rest of the `googleapis` libraries. + /// + /// The [scopes] passed here should be the same as the scopes used to request + /// the authorization. Passing scopes here that have not been authorized will + /// likely result in API errors when using the client. + gapis.AuthClient authClient({ + required List scopes, + }) { final gapis.AccessCredentials credentials = gapis.AccessCredentials( gapis.AccessToken( 'Bearer', - oauthTokenString, - // TODO(kevmoo): Use the correct value once it's available from authentication - // See https://github.com/flutter/flutter/issues/80905 + accessToken, + // The underlying SDKs don't provide expiry information, so set an + // arbitrary distant-future time. DateTime.now().toUtc().add(const Duration(days: 365)), ), - null, // We don't have a refreshToken - debugScopes ?? scopes, + null, // The underlying SDKs don't provide a refresh token. + scopes, ); return gapis.authenticatedClient(http.Client(), credentials); diff --git a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml index 26a1b763e49..062a46e0027 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml @@ -8,7 +8,7 @@ name: extension_google_sign_in_as_googleapis_auth description: A bridge package between google_sign_in and googleapis_auth, to create Authenticated Clients from google_sign_in user credentials. repository: https://github.com/flutter/packages/tree/main/packages/extension_google_sign_in_as_googleapis_auth issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+extension_google_sign_in_as_googleapis_auth%22 -version: 2.0.13 +version: 3.0.0 environment: sdk: ^3.6.0 @@ -17,7 +17,7 @@ environment: dependencies: flutter: sdk: flutter - google_sign_in: ">=5.0.0 <7.0.0" + google_sign_in: ^7.0.0 googleapis_auth: '>=1.1.0 <3.0.0' http: ">=0.13.0 <2.0.0" meta: ^1.3.0 diff --git a/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart b/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart index c4c7939e241..5cf8429b1a1 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart +++ b/packages/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart @@ -8,50 +8,23 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis_auth/googleapis_auth.dart' as gapis; const String SOME_FAKE_ACCESS_TOKEN = 'this-is-something-not-null'; -const List DEBUG_FAKE_SCOPES = ['some-scope', 'another-scope']; -const List SIGN_IN_FAKE_SCOPES = [ - 'some-scope', - 'another-scope' -]; -class FakeGoogleSignIn extends Fake implements GoogleSignIn { - @override - final List scopes = SIGN_IN_FAKE_SCOPES; -} - -class FakeGoogleSignInAuthentication extends Fake - implements GoogleSignInAuthentication { +class FakeGoogleSignInClientAuthorization extends Fake + implements GoogleSignInClientAuthorization { @override final String accessToken = SOME_FAKE_ACCESS_TOKEN; } void main() { - final GoogleSignIn signIn = FakeGoogleSignIn(); - final FakeGoogleSignInAuthentication authMock = - FakeGoogleSignInAuthentication(); - - test('authenticatedClient returns an authenticated client', () async { - final gapis.AuthClient client = (await signIn.authenticatedClient( - debugAuthentication: authMock, - ))!; - expect(client, isA()); - }); - - test('authenticatedClient uses GoogleSignIn scopes by default', () async { - final gapis.AuthClient client = (await signIn.authenticatedClient( - debugAuthentication: authMock, - ))!; - expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN)); - expect(client.credentials.scopes, equals(SIGN_IN_FAKE_SCOPES)); - }); - - test('authenticatedClient returned client contains the passed-in credentials', + test('authClient returned client contains the expected information', () async { - final gapis.AuthClient client = (await signIn.authenticatedClient( - debugAuthentication: authMock, - debugScopes: DEBUG_FAKE_SCOPES, - ))!; + const List scopes = ['some-scope', 'another-scope']; + final FakeGoogleSignInClientAuthorization signInAuth = + FakeGoogleSignInClientAuthorization(); + final gapis.AuthClient client = signInAuth.authClient( + scopes: scopes, + ); expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN)); - expect(client.credentials.scopes, equals(DEBUG_FAKE_SCOPES)); + expect(client.credentials.scopes, equals(scopes)); }); }