blob: cc8b941328b2dc19ebb21e76df666983a91abaec [file] [log] [blame]
// 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 MethodChannel;
import 'package:meta/meta.dart' show visibleForTesting;
import 'src/common.dart';
export 'src/common.dart';
export 'widgets.dart';
class GoogleSignInAuthentication {
final Map<String, String> _data;
GoogleSignInAuthentication._(this._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, Map<String, dynamic> data)
: displayName = data['displayName'],
email = data['email'],
id = data['id'],
photoUrl = data['photoUrl'],
_idToken = data['idToken'] {
assert(displayName != null);
assert(id != null);
}
@override
final String displayName;
@override
final String email;
@override
final String id;
@override
final String photoUrl;
final String _idToken;
final GoogleSignIn _googleSignIn;
Future<GoogleSignInAuthentication> get authentication async {
if (_googleSignIn.currentUser != this) {
throw new StateError('User is no longer signed in.');
}
final Map<String, String> response =
await GoogleSignIn.channel.invokeMethod(
'getTokens',
<String, dynamic>{'email': email},
);
// 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 new 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",
};
}
@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 {
/// The [MethodChannel] over which this class communicates.
@visibleForTesting
static const MethodChannel channel =
const MethodChannel('plugins.flutter.io/google_sign_in');
/// 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;
/// Initializes global sign-in configuration settings.
///
/// 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 [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.scopes, this.hostedDomain});
StreamController<GoogleSignInAccount> _currentUserController =
new 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<Null> _initialization;
Future<GoogleSignInAccount> _callMethod(String method) async {
if (_initialization == null) {
_initialization = channel.invokeMethod("init", <String, dynamic>{
'scopes': scopes ?? <String>[],
'hostedDomain': hostedDomain,
})
..catchError((dynamic _) {
// Invalidate initialization if it errored out.
_initialization = null;
});
}
await _initialization;
final Map<String, dynamic> response = await channel.invokeMethod(method);
return _setCurrentUser(response != null && response.isNotEmpty
? new GoogleSignInAccount._(this, response)
: null);
}
GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) {
if (currentUser != _currentUser) {
_currentUser = currentUser;
_currentUserController.add(_currentUser);
}
return _currentUser;
}
/// Keeps track of the most recently scheduled method call.
_MethodCompleter _lastMethodCompleter;
/// 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].
Future<GoogleSignInAccount> _addMethodCall(String method) {
if (_lastMethodCompleter == null) {
_lastMethodCompleter = new _MethodCompleter(method)
..complete(_callMethod(method));
return _lastMethodCompleter.future;
}
final _MethodCompleter completer = new _MethodCompleter(method);
_lastMethodCompleter.future.whenComplete(() {
// If after the last completed call currentUser is not null and requested
// method is a sign in method, re-use the same authenticated user
// instead of making extra call to the native side.
const List<String> kSignInMethods = const <String>[
'signIn',
'signInSilently'
];
if (kSignInMethods.contains(method) && _currentUser != null) {
completer.complete(_currentUser);
} else {
completer.complete(_callMethod(method));
}
}).catchError((dynamic _) {
// Ignore if previous call completed with an error.
});
_lastMethodCompleter = completer;
return _lastMethodCompleter.future;
}
/// 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].
Future<GoogleSignInAccount> signInSilently() {
return _addMethodCall('signInSilently').catchError((dynamic _) {
// ignore, we promised to be silent.
// TODO(goderbauer): revisit when the native side throws less aggressively.
});
}
/// 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() => _addMethodCall('signIn');
/// Marks current user as being in the signed out state.
Future<GoogleSignInAccount> signOut() => _addMethodCall('signOut');
/// Disconnects the current user from the app and revokes previous
/// authentication.
Future<GoogleSignInAccount> disconnect() => _addMethodCall('disconnect');
}
class _MethodCompleter {
final String method;
final Completer<GoogleSignInAccount> _completer =
new Completer<GoogleSignInAccount>();
_MethodCompleter(this.method);
void complete(FutureOr<GoogleSignInAccount> value) {
if (value is Future<GoogleSignInAccount>) {
value.then(_completer.complete, onError: _completer.completeError);
} else {
_completer.complete(value);
}
}
bool get isCompleted => _completer.isCompleted;
Future<GoogleSignInAccount> get future => _completer.future;
}