Skip to content
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

[google_sign_in] Adds "Button" Sign In for Web. #3461

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/google_sign_in/google_sign_in/CHANGELOG.md
@@ -1,5 +1,9 @@
## NEXT
## 6.1.0

* Exposes the new method `canAccessScopes`.
* Updates example app to separate Authentication from Authorization for those
platforms where scopes are not automatically granted upon signIn (like the web).
ditman marked this conversation as resolved.
Show resolved Hide resolved
* Updates README with information about these changes.
* Aligns Dart and Flutter SDK constraints.

## 6.0.2
Expand Down
78 changes: 77 additions & 1 deletion packages/google_sign_in/google_sign_in/README.md
Expand Up @@ -96,7 +96,15 @@ be an option.

### Web integration

For web integration details, see the
The new SDK used by the web has fully separated Authentication from Authorization,
so `signIn` and `signInSilently` no longer authorize Oauth `scopes`.

Flutter Apps must be able to detect what scopes have been granted by their users,
and if the grants are still valid.

Read below about **Working with scopes, and incremental authorization** for
general information about changes that may be needed on an app, and for more
specific web integration details, see the
[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web).

## Usage
Expand Down Expand Up @@ -139,6 +147,74 @@ Future<void> _handleSignIn() async {
}
```

In the web, you should use the **Google Sign In button** (and not the `signIn` method)
to guarantee that your user authentication contains a valid `idToken`.

For more details, take a look at the
[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web).

## Working with scopes, and incremental authorization.

### Checking if scopes have been granted

Users may (or may *not*) grant all the scopes that your application requests at
Sign In. In fact, in the web, no scopes are granted by signIn or silentSignIn anymore.

Your app must be able to:

* Detect if the authenticated user has authorized the scopes your app needs.
* Detect if the scopes that were granted a few minutes ago are still valid.

There's a new method that allows your app to check this:

```dart
final bool isAuthorized = await _googleSignIn.canAccessScopes(scopes);
```

### Requesting more scopes when needed

If your app determines that the user hasn't granted the scopes it requires, it
should initiate an Authorization request **from an user interaction** (like a
button press).

```dart
Future<void> _handleAuthorizeScopes() async {
final bool isAuthorized = await _googleSignIn.requestScopes(scopes);
if (isAuthorized) {
// Do things that only authorized users can do!
_handleGetContact(_currentUser!);
}
}
```

The `requestScopes` returns a `boolean` value that is `true` if the user has
granted all the requested scopes or `false` otherwise.

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
seconds (one hour), so your app needs to be able to handle failed REST requests,
and update its UI to prompt the user for a new Authorization round.

This can be done by combining the error responses from your REST requests with
the `canAccessScopes` and `requestScopes` methods described above.

For more details, take a look at the
[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web).

### My app didn't need any of this, what gives!?

The new web SDK implicitly grant access to `email`, `profile` and `openid` when
users complete the sign-in process (either via the One Tap UX or the Google Sign
In button).

If your app only needs an `idToken`, or only requests permissions to some of the
[OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect),
you might not need to implement any of the scope handling above.

## Example

Find the example wiring in the
Expand Down
98 changes: 80 additions & 18 deletions packages/google_sign_in/google_sign_in/example/lib/main.dart
Expand Up @@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// ignore_for_file: public_member_api_docs, avoid_print
// ignore_for_file: avoid_print

import 'dart:async';
import 'dart:convert' show json;
Expand All @@ -11,13 +11,18 @@ import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;

import 'src/sign_in_button.dart';

/// The scopes required by this application.
const List<String> scopes = <String>[
'email',
'https://www.googleapis.com/auth/contacts.readonly',
];

GoogleSignIn _googleSignIn = GoogleSignIn(
// Optional clientId
// clientId: '479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com',
scopes: <String>[
'email',
ditman marked this conversation as resolved.
Show resolved Hide resolved
'https://www.googleapis.com/auth/contacts.readonly',
],
// clientId: 'your-client_id.apps.googleusercontent.com',
scopes: scopes,
);

void main() {
Expand All @@ -29,31 +34,53 @@ void main() {
);
}

/// The SignInDemo app.
class SignInDemo extends StatefulWidget {
///
const SignInDemo({super.key});

@override
State createState() => SignInDemoState();
State createState() => _SignInDemoState();
}

class SignInDemoState extends State<SignInDemo> {
class _SignInDemoState extends State<SignInDemo> {
GoogleSignInAccount? _currentUser;
bool _isAuthorized = false; // has granted permissions?
String _contactText = '';

@override
void initState() {
super.initState();
_googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) {

_googleSignIn.onCurrentUserChanged
.listen((GoogleSignInAccount? account) async {
// Check if the account can access scopes...
bool isAuthorized = false;
if (account != null) {
isAuthorized = await _googleSignIn.canAccessScopes(scopes);
}

setState(() {
_currentUser = account;
_isAuthorized = isAuthorized;
});
if (_currentUser != null) {
_handleGetContact(_currentUser!);

// Now that we know that the user can access the required scopes, the app
// can call the REST API.
if (isAuthorized) {
_handleGetContact(account!);
}
});

// In the web, _googleSignIn.signInSilently() triggers the One Tap UX.
//
// It is recommended by Google Identity Services to render both the One Tap UX
// and the Google Sign In button together to "reduce friction and improve
// sign-in rates" ([docs](https://developers.google.com/identity/gsi/web/guides/display-button#html)).
_googleSignIn.signInSilently();
}

// Calls the People API REST endpoint for the signed-in user to retrieve information.
Future<void> _handleGetContact(GoogleSignInAccount user) async {
setState(() {
_contactText = 'Loading contact info...';
Expand Down Expand Up @@ -103,6 +130,10 @@ class SignInDemoState extends State<SignInDemo> {
return null;
}

// This is the on-click handler for the Sign In button that is rendered by Flutter.
//
// On the web, the on-click handler of the Sign In button is owned by the JS
// SDK, so this method can be considered mobile only.
Future<void> _handleSignIn() async {
try {
await _googleSignIn.signIn();
Expand All @@ -111,11 +142,28 @@ class SignInDemoState extends State<SignInDemo> {
}
}

// Prompts the user to authorize `scopes`.
//
// This action is **required** in platforms that don't perform Authentication
// and Authorization at the same time (like the web).
//
// On the web, this must be called from an user interaction (button click).
Future<void> _handleAuthorizeScopes() async {
final bool isAuthorized = await _googleSignIn.requestScopes(scopes);
setState(() {
_isAuthorized = isAuthorized;
});
if (isAuthorized) {
_handleGetContact(_currentUser!);
}
}

Future<void> _handleSignOut() => _googleSignIn.disconnect();

Widget _buildBody() {
final GoogleSignInAccount? user = _currentUser;
if (user != null) {
// The user is Authenticated
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expand All @@ -127,25 +175,39 @@ class SignInDemoState extends State<SignInDemo> {
subtitle: Text(user.email),
),
const Text('Signed in successfully.'),
Text(_contactText),
if (_isAuthorized) ...<Widget>[
// The user has Authorized all required scopes
Text(_contactText),
ElevatedButton(
child: const Text('REFRESH'),
onPressed: () => _handleGetContact(user),
),
],
if (!_isAuthorized) ...<Widget>[
// The user has NOT Authorized all required scopes.
// (Mobile users may never see this button!)
const Text('Additional permissions needed to read your contacts.'),
ElevatedButton(
onPressed: _handleAuthorizeScopes,
child: const Text('REQUEST PERMISSIONS'),
),
],
ElevatedButton(
onPressed: _handleSignOut,
child: const Text('SIGN OUT'),
),
ElevatedButton(
child: const Text('REFRESH'),
onPressed: () => _handleGetContact(user),
),
],
);
} else {
// The user is NOT Authenticated
return Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
const Text('You are not currently signed in.'),
ElevatedButton(
// This method is used to separate mobile from web code with conditional exports.
// See: src/sign_in_button.dart
buildSignInButton(
onPressed: _handleSignIn,
child: const Text('SIGN IN'),
),
],
);
Expand Down
@@ -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.

export 'sign_in_button/stub.dart'
if (dart.library.js_util) 'sign_in_button/web.dart'
if (dart.library.io) 'sign_in_button/mobile.dart';
@@ -0,0 +1,15 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

import 'stub.dart';

/// Renders a SIGN IN button that calls `handleSignIn` onclick.
Widget buildSignInButton({HandleSignInFn? onPressed}) {
return ElevatedButton(
onPressed: onPressed,
child: const Text('SIGN IN'),
);
}
@@ -0,0 +1,15 @@
// 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/material.dart';

/// The type of the onClick callback for the (mobile) Sign In Button.
typedef HandleSignInFn = Future<void> Function();

/// Renders a SIGN IN button that (maybe) calls the `handleSignIn` onclick.
Widget buildSignInButton({HandleSignInFn? onPressed}) {
return Container();
}
@@ -0,0 +1,15 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
import 'package:google_sign_in_web/google_sign_in_web.dart' as web;

import 'stub.dart';

/// Renders a web-only SIGN IN button.
Widget buildSignInButton({HandleSignInFn? onPressed}) {
return (GoogleSignInPlatform.instance as web.GoogleSignInPlugin)
.renderButton();
}
13 changes: 13 additions & 0 deletions packages/google_sign_in/google_sign_in/example/pubspec.yaml
Expand Up @@ -16,8 +16,21 @@ dependencies:
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
google_sign_in_platform_interface: ^2.2.0
google_sign_in_web: ^0.11.0
http: ^0.13.0

dependency_overrides:
google_identity_services_web:
git:
url: https://github.com/ditman/flutter-packages.git
ref: gis-web-fix-render-button-api
path: packages/google_identity_services_web
google_sign_in_platform_interface:
path: ../../google_sign_in_platform_interface
google_sign_in_web:
path: ../../google_sign_in_web

dev_dependencies:
espresso: ^0.2.0
flutter_driver:
Expand Down
Expand Up @@ -5,7 +5,7 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="google-signin-client_id" content="159623150305-q05bbbtsutr02abhips3suj7hujfk4bg.apps.googleusercontent.com" />
<meta name="google-signin-client_id" content="your-client_id.apps.googleusercontent.com">
<title>Google Sign-in Example</title>
</head>
<body>
Expand Down