|  | // 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, PlatformException; | 
|  | import 'package:meta/meta.dart' show visibleForTesting; | 
|  |  | 
|  | import 'src/common.dart'; | 
|  |  | 
|  | export 'src/common.dart'; | 
|  | export 'widgets.dart'; | 
|  |  | 
|  | enum SignInOption { standard, games } | 
|  |  | 
|  | class GoogleSignInAuthentication { | 
|  | GoogleSignInAuthentication._(this._data); | 
|  |  | 
|  | final Map<String, dynamic> _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(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 Map<String, dynamic> response = | 
|  | await GoogleSignIn.channel.invokeMapMethod<String, dynamic>( | 
|  | 'getTokens', | 
|  | <String, dynamic>{ | 
|  | '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 GoogleSignIn.channel.invokeMethod<void>( | 
|  | 'clearAuthCache', | 
|  | <String, dynamic>{'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] | 
|  | /// must not be used on iOS. | 
|  | /// | 
|  | /// 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, this.scopes, this.hostedDomain}); | 
|  |  | 
|  | /// Factory for creating default sign in user experience. | 
|  | factory GoogleSignIn.standard({List<String> scopes, String hostedDomain}) { | 
|  | return GoogleSignIn( | 
|  | signInOption: SignInOption.standard, | 
|  | scopes: scopes, | 
|  | hostedDomain: hostedDomain); | 
|  | } | 
|  |  | 
|  | /// Factory for creating sign in suitable for games. This option must not be | 
|  | /// used on iOS because the games API is not supported. | 
|  | 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'; | 
|  |  | 
|  | /// The [MethodChannel] over which this class communicates. | 
|  | @visibleForTesting | 
|  | static const MethodChannel channel = | 
|  | MethodChannel('plugins.flutter.io/google_sign_in'); | 
|  |  | 
|  | /// Option to determine the sign in user experience. [SignInOption.games] must | 
|  | /// not be used on iOS. | 
|  | 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(String method) async { | 
|  | await _ensureInitialized(); | 
|  |  | 
|  | final Map<String, dynamic> response = | 
|  | await channel.invokeMapMethod<String, dynamic>(method); | 
|  | return _setCurrentUser(response != null && response.isNotEmpty | 
|  | ? GoogleSignInAccount._(this, response) | 
|  | : null); | 
|  | } | 
|  |  | 
|  | GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) { | 
|  | if (currentUser != _currentUser) { | 
|  | _currentUser = currentUser; | 
|  | _currentUserController.add(_currentUser); | 
|  | } | 
|  | return _currentUser; | 
|  | } | 
|  |  | 
|  | Future<void> _ensureInitialized() { | 
|  | return _initialization ??= | 
|  | channel.invokeMethod<void>('init', <String, dynamic>{ | 
|  | 'signInOption': (signInOption ?? SignInOption.standard).toString(), | 
|  | 'scopes': scopes ?? <String>[], | 
|  | 'hostedDomain': hostedDomain, | 
|  | }) | 
|  | ..catchError((dynamic _) { | 
|  | // Invalidate initialization if it errored 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. | 
|  | }); | 
|  | 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]. | 
|  | Future<GoogleSignInAccount> _addMethodCall(String method) 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 is a sign in method, re-use the same authenticated user | 
|  | // instead of making extra call to the native side. | 
|  | const List<String> kSignInMethods = <String>[ | 
|  | 'signIn', | 
|  | 'signInSilently' | 
|  | ]; | 
|  | if (kSignInMethods.contains(method) && _currentUser != null) { | 
|  | return _currentUser; | 
|  | } else { | 
|  | return _callMethod(method); | 
|  | } | 
|  | }); | 
|  | } | 
|  | _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('signInSilently'); | 
|  | } 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 await channel.invokeMethod<bool>('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('signIn'); | 
|  | 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('signOut'); | 
|  |  | 
|  | /// Disconnects the current user from the app and revokes previous | 
|  | /// authentication. | 
|  | Future<GoogleSignInAccount> disconnect() => _addMethodCall('disconnect'); | 
|  | } |