| // Copyright 2019 The Chromium 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 'dart:html' as html; |
| |
| import 'package:flutter/services.dart'; |
| import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; |
| import 'package:flutter_web_plugins/flutter_web_plugins.dart'; |
| import 'package:js/js.dart'; |
| import 'package:meta/meta.dart'; |
| |
| import 'src/generated/gapiauth2.dart' as auth2; |
| import 'src/load_gapi.dart' as gapi; |
| import 'src/utils.dart' show gapiUserToPluginUserData; |
| |
| const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]'; |
| const String _kClientIdAttributeName = 'content'; |
| |
| /// This is only exposed for testing. It shouldn't be accessed by users of the |
| /// plugin as it could break at any point. |
| @visibleForTesting |
| String gapiUrl = 'https://apis.google.com/js/platform.js'; |
| |
| /// Implementation of the google_sign_in plugin for Web. |
| class GoogleSignInPlugin extends GoogleSignInPlatform { |
| /// Constructs the plugin immediately and begins initializing it in the |
| /// background. |
| /// |
| /// The plugin is completely initialized when [initialized] completed. |
| GoogleSignInPlugin() { |
| _autoDetectedClientId = html |
| .querySelector(_kClientIdMetaSelector) |
| ?.getAttribute(_kClientIdAttributeName); |
| |
| _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()); |
| } |
| |
| Future<void> _isGapiInitialized; |
| Future<void> _isAuthInitialized; |
| bool _isInitCalled = false; |
| |
| // This method throws if init hasn't been called at some point in the past. |
| // It is used by the [initialized] getter to ensure that users can't await |
| // on a Future that will never resolve. |
| void _assertIsInitCalled() { |
| if (!_isInitCalled) { |
| throw StateError( |
| 'GoogleSignInPlugin::init() must be called before any other method in this plugin.'); |
| } |
| } |
| |
| /// A future that resolves when both GAPI and Auth2 have been correctly initialized. |
| @visibleForTesting |
| Future<void> get initialized { |
| _assertIsInitCalled(); |
| return Future.wait([_isGapiInitialized, _isAuthInitialized]); |
| } |
| |
| String _autoDetectedClientId; |
| |
| /// Factory method that initializes the plugin with [GoogleSignInPlatform]. |
| static void registerWith(Registrar registrar) { |
| GoogleSignInPlatform.instance = GoogleSignInPlugin(); |
| } |
| |
| @override |
| Future<void> init( |
| {@required String hostedDomain, |
| List<String> scopes = const <String>[], |
| SignInOption signInOption = SignInOption.standard, |
| String clientId}) async { |
| final String appClientId = clientId ?? _autoDetectedClientId; |
| assert( |
| appClientId != null, |
| 'ClientID not set. Either set it on a ' |
| '<meta name="google-signin-client_id" content="CLIENT_ID" /> tag,' |
| ' or pass clientId when calling init()'); |
| |
| assert( |
| !scopes.any((String scope) => scope.contains(' ')), |
| 'OAuth 2.0 Scopes for Google APIs can\'t contain spaces.' |
| 'Check https://developers.google.com/identity/protocols/googlescopes ' |
| 'for a list of valid OAuth 2.0 scopes.'); |
| |
| await _isGapiInitialized; |
| |
| final auth2.GoogleAuth auth = auth2.init(auth2.ClientConfig( |
| hosted_domain: hostedDomain, |
| // The js lib wants a space-separated list of values |
| scope: scopes.join(' '), |
| client_id: appClientId, |
| )); |
| |
| Completer<void> isAuthInitialized = Completer<void>(); |
| _isAuthInitialized = isAuthInitialized.future; |
| _isInitCalled = true; |
| |
| auth.then(allowInterop((auth2.GoogleAuth initializedAuth) { |
| // onSuccess |
| |
| // TODO: https://github.com/flutter/flutter/issues/48528 |
| // This plugin doesn't notify the app of external changes to the |
| // state of the authentication, i.e: if you logout elsewhere... |
| |
| isAuthInitialized.complete(); |
| }), allowInterop((dynamic reason) { |
| // onError |
| throw PlatformException( |
| code: 'google_sign_in', |
| message: reason.error, |
| details: reason.details, |
| ); |
| })); |
| |
| return null; |
| } |
| |
| @override |
| Future<GoogleSignInUserData> signInSilently() async { |
| await initialized; |
| |
| return gapiUserToPluginUserData( |
| await auth2.getAuthInstance().currentUser.get()); |
| } |
| |
| @override |
| Future<GoogleSignInUserData> signIn() async { |
| await initialized; |
| |
| return gapiUserToPluginUserData(await auth2.getAuthInstance().signIn()); |
| } |
| |
| @override |
| Future<GoogleSignInTokenData> getTokens( |
| {@required String email, bool shouldRecoverAuth}) async { |
| await initialized; |
| |
| final auth2.GoogleUser currentUser = |
| auth2.getAuthInstance()?.currentUser?.get(); |
| final auth2.AuthResponse response = currentUser.getAuthResponse(); |
| |
| return GoogleSignInTokenData( |
| idToken: response.id_token, accessToken: response.access_token); |
| } |
| |
| @override |
| Future<void> signOut() async { |
| await initialized; |
| |
| return auth2.getAuthInstance().signOut(); |
| } |
| |
| @override |
| Future<void> disconnect() async { |
| await initialized; |
| |
| final auth2.GoogleUser currentUser = |
| auth2.getAuthInstance()?.currentUser?.get(); |
| return currentUser.disconnect(); |
| } |
| |
| @override |
| Future<bool> isSignedIn() async { |
| await initialized; |
| |
| final auth2.GoogleUser currentUser = |
| auth2.getAuthInstance()?.currentUser?.get(); |
| return currentUser.isSignedIn(); |
| } |
| |
| @override |
| Future<void> clearAuthCache({String token}) async { |
| await initialized; |
| |
| return auth2.getAuthInstance().disconnect(); |
| } |
| |
| @override |
| Future<bool> requestScopes(List<String> scopes) async { |
| await initialized; |
| |
| final currentUser = auth2.getAuthInstance()?.currentUser?.get(); |
| |
| if (currentUser == null) return false; |
| |
| final grantedScopes = currentUser.getGrantedScopes(); |
| final missingScopes = |
| scopes.where((scope) => !grantedScopes.contains(scope)); |
| |
| if (missingScopes.isEmpty) return true; |
| |
| return currentUser |
| .grant(auth2.SigninOptions(scope: missingScopes.join(" "))) ?? |
| false; |
| } |
| } |