blob: ff0d8e4cea899c267668e33431e195259b46b46f [file] [log] [blame]
// Copyright 2013 The Flutter 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/foundation.dart' show visibleForTesting;
import 'package:flutter/services.dart';
import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
import 'package:js/js.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());
}
late Future<void> _isGapiInitialized;
late 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({
List<String> scopes = const <String>[],
SignInOption signInOption = SignInOption.standard,
String? hostedDomain,
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((auth2.GoogleAuthInitFailureError reason) {
// onError
isAuthInitialized.completeError(PlatformException(
code: reason.error,
message: reason.details,
details:
'https://developers.google.com/identity/sign-in/web/reference#error_codes',
));
}));
return _isAuthInitialized;
}
@override
Future<GoogleSignInUserData?> signInSilently() async {
await initialized;
return gapiUserToPluginUserData(
await auth2.getAuthInstance()?.currentUser?.get());
}
@override
Future<GoogleSignInUserData?> signIn() async {
await initialized;
try {
return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn());
} on auth2.GoogleAuthSignInError catch (reason) {
throw PlatformException(
code: reason.error,
message: 'Exception raised from GoogleAuth.signIn()',
details:
'https://developers.google.com/identity/sign-in/web/reference#error_codes_2',
);
}
}
@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();
if (currentUser == null) return;
return currentUser.disconnect();
}
@override
Future<bool> isSignedIn() async {
await initialized;
final auth2.GoogleUser? currentUser =
auth2.getAuthInstance()?.currentUser?.get();
if (currentUser == null) return false;
return currentUser.isSignedIn();
}
@override
Future<void> clearAuthCache({required 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;
final response = await currentUser
.grant(auth2.SigninOptions(scope: missingScopes.join(' ')));
return response != null;
}
}