diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index b69b1b1dc65c..e6ef94e47ed4 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart @@ -3,19 +3,29 @@ import 'dart:html' as html; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:js/js.dart'; + +import 'src/gapi.dart'; + +const String _kClientIdMetaSelector = 'meta[name=google-signin-client_id]'; +const String _kClientIdAttributeName = 'content'; +const List _kJsLibraries = [ + 'https://apis.google.com/js/platform.js' +]; /// Implementation of the google_sign_in plugin for Web class GoogleSignInPlugin { GoogleSignInPlugin() { - _jsLibrariesLoading = _injectJSLibraries([ - 'https://apis.google.com/js/platform.js', - ])..then(_initGapi); + _autoDetectedClientId = html + .querySelector(_kClientIdMetaSelector) + ?.getAttribute(_kClientIdAttributeName); + + _isGapiInitialized = _injectJSLibraries(_kJsLibraries).then((_) => _initGapi()); } - Future> _jsLibrariesLoading; + Future _isGapiInitialized; + String _autoDetectedClientId; - Future get isJsLoaded => _jsLibrariesLoading; - static void registerWith(Registrar registrar) { final MethodChannel channel = MethodChannel( 'plugins.flutter.io/google_sign_in', @@ -28,14 +38,50 @@ class GoogleSignInPlugin { Future handleMethodCall(MethodCall call) async { // Await for initialization promises to complete, then do your thing... - await isJsLoaded; + await _isGapiInitialized; - html.window.console.log('Doing things! $call'); + final GoogleAuth authInstance = gapi.auth2.getAuthInstance(); + final GoogleUser currentUser = authInstance?.currentUser?.get(); switch (call.method) { case 'init': - // Initialize the gapi - return _init(call.arguments); + _init(call.arguments); + return true; + break; + case 'signInSilently': + // TODO: convert call.arguments to an Auth2SignInOptions object (when needed) + await _signIn(Auth2SignInOptions( + prompt: 'none', + )); + return _currentUserToPluginMap(currentUser); + break; + case 'signIn': + // TODO: convert call.arguments to an Auth2SignInOptions object (when needed) + await _signIn(null); + return _currentUserToPluginMap(currentUser); + break; + case 'getTokens': + final Auth2AuthResponse response = currentUser.getAuthResponse(); + return { + 'idToken': response.id_token, + 'accessToken': response.access_token, + }; + break; + case 'signOut': + await _signOut(); + return null; + break; + case 'disconnect': + currentUser.disconnect(); + return null; + break; + case 'isSignedIn': + return currentUser.isSignedIn(); + break; + case 'clearAuthCache': + // We really don't keep any cache here, but let's try to be drastic: + authInstance.disconnect(); + return null; break; default: throw PlatformException( @@ -45,23 +91,52 @@ class GoogleSignInPlugin { } } + Map _currentUserToPluginMap(GoogleUser currentUser) { + assert(currentUser != null); + final Auth2BasicProfile profile = currentUser.getBasicProfile(); + return { + 'displayName': profile?.getName(), + 'email': profile?.getEmail(), + 'id': profile?.getId(), + 'photoUrl': profile?.getImageUrl(), + 'idToken': currentUser?.getAuthResponse()?.id_token, + }; + } + // Load the auth2 library - Future _init(Map arguments) { - // + GoogleAuth _init(dynamic arguments) => gapi.auth2.init(Auth2ClientConfig( + hosted_domain: arguments['hostedDomain'], + // The js lib wants a space-separated list of values + scope: arguments['scopes'].join(' '), + client_id: arguments['clientId'] ?? _autoDetectedClientId, + )); + + Future _signIn(Auth2SignInOptions signInOptions) async { + return html.promiseToFuture( + gapi.auth2.getAuthInstance().signIn(signInOptions)); + } + + Future _signOut() async { + return html.promiseToFuture(gapi.auth2.getAuthInstance().signOut()); } - Future _initGapi(List _) { - // JS-interop with the global gapi method and call gapi.load('auth2'), and wait for the - // promise to resolve... - + Future _initGapi() { + // JS-interop with the global gapi method and call gapi.load('auth2'), + // then wait for the promise to resolve... + final Completer gapiLoadCompleter = Completer(); + gapi.load('auth2', allowInterop(() { + gapiLoadCompleter.complete(); + })); + + // After this is resolved, we can use gapi.auth2! + return gapiLoadCompleter.future; } - /// Injects a bunch of libraries in the and returns a + /// Injects a bunch of libraries in the and returns a /// Future that resolves when all load. - Future> _injectJSLibraries(List libraries, { Duration timeout }) { - final List> loading = >[ - Future.delayed(const Duration(seconds: 10), () => true) // TODO: Remove this delay before submitting :) - ]; + Future _injectJSLibraries(List libraries, + {Duration timeout}) { + final List> loading = >[]; final List tags = []; libraries.forEach((String library) { @@ -69,7 +144,8 @@ class GoogleSignInPlugin { ..async = true ..defer = true ..src = library; - loading.add(script.onLoad.first); // TODO add a timeout race to fail this future + // TODO add a timeout race to fail this future + loading.add(script.onLoad.first); tags.add(script); }); html.querySelector('head').children.addAll(tags); diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/gapi.dart new file mode 100644 index 000000000000..cfeaf817e9f4 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/lib/src/gapi.dart @@ -0,0 +1,168 @@ +/// This library describes the global gapi object, and the 'auth2' module +/// as described here: https://developers.google.com/identity/sign-in/web/reference +@JS() +library gapi; + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:js/js.dart'; + +// Global window.gapi, we need to call `load('auth2', cb)` on it... +@JS('gapi') +external Gapi get gapi; + +@JS() +abstract class Gapi { + external void load(String module, Function callback); + external Auth2 get auth2; +} + +@JS() +abstract class Auth2 { + // https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams + external GoogleAuth init(Auth2ClientConfig params); + external GoogleAuth getAuthInstance(); + external void authorize( + Auth2AuthorizeConfig params, Auth2AuthorizeCallback callback); +} + +typedef Auth2AuthorizeCallback = void Function(Auth2AuthorizeResponse response); + +@JS() +abstract class Auth2CurrentUser { + external GoogleUser get(); + external void listen(Auth2CurrentUserListener listener); +} + +typedef Auth2CurrentUserListener = void Function(bool); + +@JS() +abstract class GoogleAuth { + external Future then(Function onInit, Function onError); + // Authentication: https://developers.google.com/identity/sign-in/web/reference#authentication + external Future signIn([Auth2SignInOptions options]); + external Future signOut(); + external void disconnect(); + // Offline access not implemented + external void attachClickHandler(html.HtmlElement container, + Auth2SignInOptions options, Function onSuccess, Function onFailure); + external Auth2CurrentUser get currentUser; +} + +// https://developers.google.com/identity/sign-in/web/reference#googleusergetbasicprofile +@JS() +abstract class Auth2BasicProfile { + external String getId(); + external String getName(); + external String getGivenName(); + external String getFamilyName(); + external String getImageUrl(); + external String getEmail(); +} + +// https://developers.google.com/identity/sign-in/web/reference#gapiauth2authresponse +@JS() +abstract class Auth2AuthResponse { + external String get access_token; + external String get id_token; + external String get scope; + external num get expires_in; + external num get first_issued_at; + external num get expires_at; +} + +// https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeresponse +@JS() +abstract class Auth2AuthorizeResponse { + external String get access_token; + external String get id_token; + external String get code; + external String get scope; + external int get expires_in; + external int get first_issued_at; + external int get expires_at; + external String get error; + external String get error_subtype; +} + +// https://developers.google.com/identity/sign-in/web/reference#users +@JS() +abstract class GoogleUser { + external String getId(); + external bool isSignedIn(); + external String getHostedDomain(); + external String getGrantedScopes(); + external Auth2BasicProfile getBasicProfile(); + external Auth2AuthResponse getAuthResponse([bool includeAuthorizationData]); + external Future reloadAuthResponse(); + external bool hasGrantedScopes(String scopes); + external void grant(Auth2SignInOptions options); + // Offline access not implemented + external void disconnect(); +} + +// https://developers.google.com/identity/sign-in/web/reference#gapiauth2signinoptions +@JS() +@anonymous +abstract class Auth2SignInOptions { + external factory Auth2SignInOptions( + {String prompt, String scope, String ux_mode, String redirect_uri}); + + external String get prompt; + external String get scope; + external String get ux_mode; + external String get redirect_uri; +} + +// https://developers.google.com/identity/sign-in/web/reference#gapiauth2clientconfig +@JS() +@anonymous +abstract class Auth2ClientConfig { + external factory Auth2ClientConfig({ + String client_id, + String cookie_policy, + String scope, + bool fetch_basic_profile, + String hosted_domain, + String open_id_realm, + String ux_mode, + String redirect_uri, + }); + + external String get client_id; + external String get cookie_policy; + external String get scope; + external bool get fetch_basic_profile; + external String get hosted_domain; + external String get open_id_realm; + external String get ux_mode; + external String get redirect_uri; +} + +// https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeconfig +@JS() +@anonymous +abstract class Auth2AuthorizeConfig { + external factory Auth2AuthorizeConfig({ + String client_id, + String scope, + String response_type, + String prompt, + String cookie_policy, + String hosted_domain, + String login_hint, + String open_id_realm, + bool include_granted_scopes, + }); + + external String get client_id; + external String get scope; + external String get response_type; + external String get prompt; + external String get cookie_policy; + external String get hosted_domain; + external String get login_hint; + external String get open_id_realm; + external bool get include_granted_scopes; +} diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 51a596eb056f..4963c7539e3e 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: flutter_web_plugins: sdk: flutter meta: ^1.1.7 + js: ^0.6.1 dev_dependencies: flutter_test: