Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
2 contributors

Users who have contributed to this file

@ditman @mehmetf
352 lines (299 sloc) 12.3 KB
// Copyright 2017, the Flutter project authors. Please see the AUTHORS file
// for details. 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 'dart:ui' show hashValues;
import 'package:flutter/services.dart' show PlatformException;
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
import 'src/common.dart';
export 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'
show SignInOption;
export 'src/common.dart';
export 'widgets.dart';
class GoogleSignInAuthentication {
GoogleSignInAuthentication._(this._data);
final GoogleSignInTokenData _data;
/// An OpenID Connect ID token that identifies the user.
String get idToken => _data.idToken;
/// The OAuth2 access token to access Google services.
String get accessToken => _data.accessToken;
@override
String toString() => 'GoogleSignInAuthentication:$_data';
}
class GoogleSignInAccount implements GoogleIdentity {
GoogleSignInAccount._(this._googleSignIn, GoogleSignInUserData data)
: displayName = data.displayName,
email = data.email,
id = data.id,
photoUrl = data.photoUrl,
_idToken = data.idToken {
assert(id != null);
}
// These error codes must match with ones declared on Android and iOS sides.
/// Error code indicating there was a failed attempt to recover user authentication.
static const String kFailedToRecoverAuthError = 'failed_to_recover_auth';
/// Error indicating that authentication can be recovered with user action;
static const String kUserRecoverableAuthError = 'user_recoverable_auth';
@override
final String displayName;
@override
final String email;
@override
final String id;
@override
final String photoUrl;
final String _idToken;
final GoogleSignIn _googleSignIn;
/// Retrieve [GoogleSignInAuthentication] for this account.
///
/// [shouldRecoverAuth] sets whether to attempt to recover authentication if
/// user action is needed. If an attempt to recover authentication fails a
/// [PlatformException] is thrown with possible error code
/// [kFailedToRecoverAuthError].
///
/// Otherwise, if [shouldRecoverAuth] is false and the authentication can be
/// recovered by user action a [PlatformException] is thrown with error code
/// [kUserRecoverableAuthError].
Future<GoogleSignInAuthentication> get authentication async {
if (_googleSignIn.currentUser != this) {
throw StateError('User is no longer signed in.');
}
final GoogleSignInTokenData response =
await GoogleSignInPlatform.instance.getTokens(
email: email,
shouldRecoverAuth: true,
);
// On Android, there isn't an API for refreshing the idToken, so re-use
// the one we obtained on login.
if (response.idToken == null) {
response.idToken = _idToken;
}
return GoogleSignInAuthentication._(response);
}
Future<Map<String, String>> get authHeaders async {
final String token = (await authentication).accessToken;
return <String, String>{
"Authorization": "Bearer $token",
"X-Goog-AuthUser": "0",
};
}
/// Clears any client side cache that might be holding invalid tokens.
///
/// If client runs into 401 errors using a token, it is expected to call
/// this method and grab `authHeaders` once again.
Future<void> clearAuthCache() async {
final String token = (await authentication).accessToken;
await GoogleSignInPlatform.instance.clearAuthCache(token: token);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other)) return true;
if (other is! GoogleSignInAccount) return false;
final GoogleSignInAccount otherAccount = other;
return displayName == otherAccount.displayName &&
email == otherAccount.email &&
id == otherAccount.id &&
photoUrl == otherAccount.photoUrl &&
_idToken == otherAccount._idToken;
}
@override
int get hashCode => hashValues(displayName, email, id, photoUrl, _idToken);
@override
String toString() {
final Map<String, dynamic> data = <String, dynamic>{
'displayName': displayName,
'email': email,
'id': id,
'photoUrl': photoUrl,
};
return 'GoogleSignInAccount:$data';
}
}
/// GoogleSignIn allows you to authenticate Google users.
class GoogleSignIn {
/// Initializes global sign-in configuration settings.
///
/// The [signInOption] determines the user experience. [SigninOption.games]
/// is only supported on Android.
///
/// The list of [scopes] are OAuth scope codes to request when signing in.
/// These scope codes will determine the level of data access that is granted
/// to your application by the user. The full list of available scopes can
/// be found here:
/// <https://developers.google.com/identity/protocols/googlescopes>
///
/// The [hostedDomain] argument specifies a hosted domain restriction. By
/// setting this, sign in will be restricted to accounts of the user in the
/// specified domain. By default, the list of accounts will not be restricted.
GoogleSignIn({
this.signInOption = SignInOption.standard,
this.scopes = const <String>[],
this.hostedDomain,
});
/// Factory for creating default sign in user experience.
factory GoogleSignIn.standard({
List<String> scopes = const <String>[],
String hostedDomain,
}) {
return GoogleSignIn(
signInOption: SignInOption.standard,
scopes: scopes,
hostedDomain: hostedDomain);
}
/// Factory for creating sign in suitable for games. This option is only
/// supported on Android.
factory GoogleSignIn.games() {
return GoogleSignIn(signInOption: SignInOption.games);
}
// These error codes must match with ones declared on Android and iOS sides.
/// Error code indicating there is no signed in user and interactive sign in
/// flow is required.
static const String kSignInRequiredError = 'sign_in_required';
/// Error code indicating that interactive sign in process was canceled by the
/// user.
static const String kSignInCanceledError = 'sign_in_canceled';
/// Error code indicating that attempt to sign in failed.
static const String kSignInFailedError = 'sign_in_failed';
/// Option to determine the sign in user experience. [SignInOption.games] is
/// only supported on Android.
final SignInOption signInOption;
/// The list of [scopes] are OAuth scope codes requested when signing in.
final List<String> scopes;
/// Domain to restrict sign-in to.
final String hostedDomain;
StreamController<GoogleSignInAccount> _currentUserController =
StreamController<GoogleSignInAccount>.broadcast();
/// Subscribe to this stream to be notified when the current user changes.
Stream<GoogleSignInAccount> get onCurrentUserChanged =>
_currentUserController.stream;
// Future that completes when we've finished calling `init` on the native side
Future<void> _initialization;
Future<GoogleSignInAccount> _callMethod(Function method) async {
await _ensureInitialized();
final dynamic response = await method();
return _setCurrentUser(response != null && response is GoogleSignInUserData
? GoogleSignInAccount._(this, response)
: null);
}
GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) {
if (currentUser != _currentUser) {
_currentUser = currentUser;
_currentUserController.add(_currentUser);
}
return _currentUser;
}
Future<void> _ensureInitialized() {
return _initialization ??= GoogleSignInPlatform.instance.init(
signInOption: signInOption,
scopes: scopes,
hostedDomain: hostedDomain,
)..catchError((dynamic _) {
// Invalidate initialization if it errors out.
_initialization = null;
});
}
/// The most recently scheduled method call.
Future<void> _lastMethodCall;
/// Returns a [Future] that completes with a success after [future], whether
/// it completed with a value or an error.
static Future<void> _waitFor(Future<void> future) {
final Completer<void> completer = Completer<void>();
future.whenComplete(completer.complete).catchError((dynamic _) {
// Ignore if previous call completed with an error.
// TODO: Should we log errors here, if debug or similar?
});
return completer.future;
}
/// Adds call to [method] in a queue for execution.
///
/// At most one in flight call is allowed to prevent concurrent (out of order)
/// updates to [currentUser] and [onCurrentUserChanged].
///
/// The optional, named parameter [canSkipCall] lets the plugin know that the
/// method call may be skipped, if there's already [_currentUser] information.
/// This is used from the [signIn] and [signInSilently] methods.
Future<GoogleSignInAccount> _addMethodCall(
Function method, {
bool canSkipCall = false,
}) async {
Future<GoogleSignInAccount> response;
if (_lastMethodCall == null) {
response = _callMethod(method);
} else {
response = _lastMethodCall.then((_) {
// If after the last completed call `currentUser` is not `null` and requested
// method can be skipped (`canSkipCall`), re-use the same authenticated user
// instead of making extra call to the native side.
if (canSkipCall && _currentUser != null) {
return _currentUser;
}
return _callMethod(method);
});
}
// Add the current response to the currently running Promise of all pending responses
_lastMethodCall = _waitFor(response);
return response;
}
/// The currently signed in account, or null if the user is signed out.
GoogleSignInAccount get currentUser => _currentUser;
GoogleSignInAccount _currentUser;
/// Attempts to sign in a previously authenticated user without interaction.
///
/// Returned Future resolves to an instance of [GoogleSignInAccount] for a
/// successful sign in or `null` if there is no previously authenticated user.
/// Use [signIn] method to trigger interactive sign in process.
///
/// Authentication process is triggered only if there is no currently signed in
/// user (that is when `currentUser == null`), otherwise this method returns
/// a Future which resolves to the same user instance.
///
/// Re-authentication can be triggered only after [signOut] or [disconnect].
///
/// When [suppressErrors] is set to `false` and an error occurred during sign in
/// returned Future completes with [PlatformException] whose `code` can be
/// either [kSignInRequiredError] (when there is no authenticated user) or
/// [kSignInFailedError] (when an unknown error occurred).
Future<GoogleSignInAccount> signInSilently({
bool suppressErrors = true,
}) async {
try {
return await _addMethodCall(GoogleSignInPlatform.instance.signInSilently,
canSkipCall: true);
} catch (_) {
if (suppressErrors) {
return null;
} else {
rethrow;
}
}
}
/// Returns a future that resolves to whether a user is currently signed in.
Future<bool> isSignedIn() async {
await _ensureInitialized();
return GoogleSignInPlatform.instance.isSignedIn();
}
/// Starts the interactive sign-in process.
///
/// Returned Future resolves to an instance of [GoogleSignInAccount] for a
/// successful sign in or `null` in case sign in process was aborted.
///
/// Authentication process is triggered only if there is no currently signed in
/// user (that is when `currentUser == null`), otherwise this method returns
/// a Future which resolves to the same user instance.
///
/// Re-authentication can be triggered only after [signOut] or [disconnect].
Future<GoogleSignInAccount> signIn() {
final Future<GoogleSignInAccount> result =
_addMethodCall(GoogleSignInPlatform.instance.signIn, canSkipCall: true);
bool isCanceled(dynamic error) =>
error is PlatformException && error.code == kSignInCanceledError;
return result.catchError((dynamic _) => null, test: isCanceled);
}
/// Marks current user as being in the signed out state.
Future<GoogleSignInAccount> signOut() =>
_addMethodCall(GoogleSignInPlatform.instance.signOut);
/// Disconnects the current user from the app and revokes previous
/// authentication.
Future<GoogleSignInAccount> disconnect() =>
_addMethodCall(GoogleSignInPlatform.instance.disconnect);
}
You can’t perform that action at this time.