diff --git a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index 37a1a52576f3..88805f9eee8f 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md @@ -1,5 +1,9 @@ -## NEXT +## 3.0.0 +* **BREAKING CHANGE**: Overhauls the entire API surface to better abstract the + current set of underlying platform SDKs, and to use structured errors. See + API doc comments for details on the behaviors that platform implementations + must implement. * Updates minimum supported SDK version to Flutter 3.27/Dart 3.6. ## 2.5.0 diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart index 110097f0dcb6..a68e66665376 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart @@ -4,13 +4,10 @@ import 'dart:async'; -import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'src/method_channel_google_sign_in.dart'; import 'src/types.dart'; -export 'src/method_channel_google_sign_in.dart'; export 'src/types.dart'; /// The interface that implementations of google_sign_in must implement. @@ -27,134 +24,151 @@ abstract class GoogleSignInPlatform extends PlatformInterface { static final Object _token = Object(); - /// Only mock implementations should set this to `true`. + /// The instance of [GoogleSignInPlatform] to use. /// - /// Mockito mocks implement this class with `implements` which is forbidden - /// (see class docs). This property provides a backdoor for mocks to skip the - /// verification that the class isn't implemented with `implements`. - @visibleForTesting - @Deprecated('Use MockPlatformInterfaceMixin instead') - bool get isMock => false; - - /// The default instance of [GoogleSignInPlatform] to use. - /// - /// Platform-specific plugins should override this with their own + /// Platform-implementations should override this with their own /// platform-specific class that extends [GoogleSignInPlatform] when they /// register themselves. /// /// Defaults to [MethodChannelGoogleSignIn]. static GoogleSignInPlatform get instance => _instance; - static GoogleSignInPlatform _instance = MethodChannelGoogleSignIn(); + static GoogleSignInPlatform _instance = _PlaceholderImplementation(); - // TODO(amirh): Extract common platform interface logic. - // https://github.com/flutter/flutter/issues/43368 static set instance(GoogleSignInPlatform instance) { - if (!instance.isMock) { - PlatformInterface.verify(instance, _token); - } + PlatformInterface.verify(instance, _token); _instance = instance; } - /// Initializes the plugin. Deprecated: call [initWithParams] instead. + /// Initializes the plugin with specified [params]. You must call this method + /// before calling other methods. /// - /// 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. + /// See: /// - /// 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: + /// * [InitParameters] + Future init(InitParameters params); + + /// Attempts to sign in without an explicit user intent. /// - /// The [signInOption] determines the user experience. [SigninOption.games] is - /// only supported on Android. + /// This is intended to support the use case where the user might be expected + /// to be signed in, but hasn't explicitly requested sign in, such as when + /// launching an application that is intended to be used while signed in. /// - /// See: - /// https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams - Future init({ - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String? hostedDomain, - String? clientId, - }) async { - throw UnimplementedError('init() has not been implemented.'); - } + /// This may be silent, or may show minimal UI, depending on the platform and + /// the context. + Future? attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params); - /// Initializes the plugin with specified [params]. You must call this method - /// before calling other methods. + /// Returns true if the platform implementation supports the [authenticate] + /// method. /// - /// See: + /// The default is true, but platforms that cannot support [authenticate] can + /// override this to return false, throw [UnsupportedError] from + /// [authenticate], and provide a different, platform-specific authentication + /// flow. + bool supportsAuthenticate(); + + /// Signs in with explicit user intent. /// - /// * [SignInInitParameters] - Future initWithParams(SignInInitParameters params) async { - await init( - scopes: params.scopes, - signInOption: params.signInOption, - hostedDomain: params.hostedDomain, - clientId: params.clientId, - ); - } + /// This is intended to support the use case where the user has expressed + /// an explicit intent to sign in. + Future authenticate(AuthenticateParameters params); - /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. - Future signInSilently() async { - throw UnimplementedError('signInSilently() has not been implemented.'); - } + /// Whether or not authorization calls that could show UI must be called from + /// a user interaction, such as a button press, on the current platform. + /// + /// Platforms that can fail to show UI without an active user interaction, + /// such as a web implementations that uses popups, should return false. + bool authorizationRequiresUserInteraction(); - /// Signs in the user with the options specified to [init]. - Future signIn() async { - throw UnimplementedError('signIn() has not been implemented.'); + /// Returns the tokens used to authenticate other API calls from a client. + /// + /// This should only return null if prompting would be necessary but [params] + /// do not allow it, otherwise any failure should return an error. + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params); + + /// Returns the tokens used to authenticate other API calls from a server. + /// + /// This should only return null if prompting would be necessary but [params] + /// do not allow it, otherwise any failure should return an error. + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params); + + /// Signs out previously signed in accounts. + Future signOut(SignOutParams params); + + /// Revokes all of the scopes that all signed in users granted, and then signs + /// them out. + Future disconnect(DisconnectParams params); + + /// Returns a stream of authentication events. + /// + /// If this is not overridden, the app-facing package will assume that the + /// futures returned by [attemptLightweightAuthentication], [authenticate], + /// and [signOut] are the only sources of authentication-related events. + /// Implementations that have other sources should override this and provide + /// a stream with all authentication and sign-out events. + /// These will normally come from asynchronous flows, like the authenticate + /// and signOut methods, as well as potentially from platform-specific methods + /// (such as the Google Sign-In Button Widget from the Web implementation). + /// + /// Implementations should never intentionally call `addError` for this + /// stream, and should instead use AuthenticationEventException. This is to + /// ensure via the type system that implementations are always sending + /// [GoogleSignInException] for know failure cases. + Stream? get authenticationEvents => null; +} + +/// An implementation of GoogleSignInPlatform that throws unimplemented errors, +/// to use as a default instance if no platform implementation has been +/// registered. +class _PlaceholderImplementation extends GoogleSignInPlatform { + @override + Future init(InitParameters params) { + throw UnimplementedError(); } - /// Returns the Tokens used to authenticate other API calls. - Future getTokens( - {required String email, bool? shouldRecoverAuth}) async { - throw UnimplementedError('getTokens() has not been implemented.'); + @override + Future attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) { + throw UnimplementedError(); } - /// Signs out the current account from the application. - Future signOut() async { - throw UnimplementedError('signOut() has not been implemented.'); + @override + bool supportsAuthenticate() { + throw UnimplementedError(); } - /// Revokes all of the scopes that the user granted. - Future disconnect() async { - throw UnimplementedError('disconnect() has not been implemented.'); + @override + Future authenticate(AuthenticateParameters params) { + throw UnimplementedError(); } - /// Returns whether the current user is currently signed in. - Future isSignedIn() async { - throw UnimplementedError('isSignedIn() has not been implemented.'); + @override + bool authorizationRequiresUserInteraction() { + throw UnimplementedError(); } - /// Clears any cached information that the plugin may be holding on to. - Future clearAuthCache({required String token}) async { - throw UnimplementedError('clearAuthCache() has not been implemented.'); + @override + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params) { + throw UnimplementedError(); } - /// Requests the user grants additional Oauth [scopes]. - /// - /// Scopes should come from the full list - /// [here](https://developers.google.com/identity/protocols/googlescopes). - Future requestScopes(List scopes) async { - throw UnimplementedError('requestScopes() has not been implemented.'); + @override + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params) { + throw UnimplementedError(); } - /// Checks if the current user has granted access to all the specified [scopes]. - /// - /// Optionally, an [accessToken] can be passed for applications where a - /// long-lived token may be cached (like the web). - Future canAccessScopes( - List scopes, { - String? accessToken, - }) async { - throw UnimplementedError('canAccessScopes() has not been implemented.'); + @override + Future signOut(SignOutParams params) { + throw UnimplementedError(); } - /// Returns a stream of [GoogleSignInUserData] authentication events. - /// - /// These will normally come from asynchronous flows, like the Google Sign-In - /// Button Widget from the Web implementation, and will be funneled directly - /// to the `onCurrentUserChanged` Stream of the plugin. - Stream? get userDataEvents => null; + @override + Future disconnect(DisconnectParams params) { + throw UnimplementedError(); + } } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart deleted file mode 100644 index fde29aeb8e4d..000000000000 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart +++ /dev/null @@ -1,101 +0,0 @@ -// 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 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; - -import '../google_sign_in_platform_interface.dart'; -import 'utils.dart'; - -/// An implementation of [GoogleSignInPlatform] that uses method channels. -class MethodChannelGoogleSignIn extends GoogleSignInPlatform { - /// This is only exposed for test purposes. It shouldn't be used by clients of - /// the plugin as it may break or change at any time. - @visibleForTesting - MethodChannel channel = - const MethodChannel('plugins.flutter.io/google_sign_in'); - - @override - Future init({ - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String? hostedDomain, - String? clientId, - }) { - return initWithParams(SignInInitParameters( - scopes: scopes, - signInOption: signInOption, - hostedDomain: hostedDomain, - clientId: clientId)); - } - - @override - Future initWithParams(SignInInitParameters params) { - return channel.invokeMethod('init', { - 'signInOption': params.signInOption.toString(), - 'scopes': params.scopes, - 'hostedDomain': params.hostedDomain, - 'clientId': params.clientId, - 'serverClientId': params.serverClientId, - 'forceCodeForRefreshToken': params.forceCodeForRefreshToken, - }); - } - - @override - Future signInSilently() { - return channel - .invokeMapMethod('signInSilently') - .then(getUserDataFromMap); - } - - @override - Future signIn() { - return channel - .invokeMapMethod('signIn') - .then(getUserDataFromMap); - } - - @override - Future getTokens( - {required String email, bool? shouldRecoverAuth = true}) { - return channel - .invokeMapMethod('getTokens', { - 'email': email, - 'shouldRecoverAuth': shouldRecoverAuth, - }).then((Map? result) => getTokenDataFromMap(result!)); - } - - @override - Future signOut() { - return channel.invokeMapMethod('signOut'); - } - - @override - Future disconnect() { - return channel.invokeMapMethod('disconnect'); - } - - @override - Future isSignedIn() async { - return (await channel.invokeMethod('isSignedIn'))!; - } - - @override - Future clearAuthCache({required String token}) { - return channel.invokeMethod( - 'clearAuthCache', - {'token': token}, - ); - } - - @override - Future requestScopes(List scopes) async { - return (await channel.invokeMethod( - 'requestScopes', - >{'scopes': scopes}, - ))!; - } -} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index 057927d5164b..047462fe22ea 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart @@ -2,24 +2,72 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart' show immutable; -/// Default configuration options to use when signing in. +/// An exception throws by the plugin when there is authenication or +/// authorization failure, or some other error. +@immutable +class GoogleSignInException implements Exception { + /// Crceates a new exception with the given information. + const GoogleSignInException( + {required this.code, this.description, this.details}); + + /// The type of failure. + final GoogleSignInExceptionCode code; + + /// A human-readable description of the failure. + final String? description; + + /// Any additional details about the failure. + final Object? details; + + @override + String toString() => + 'GoogleSignInException(code $code, $description, $details)'; +} + +/// Types of [GoogleSignInException]s, as indicated by +/// [GoogleSignInException.code]. /// -/// See also https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInOptions -enum SignInOption { - /// Default configuration. Provides stable user ID and basic profile information. +/// Adding new values to this enum in the future will *not* be considered a +/// breaking change, so clients should not assume they can exhaustively match +/// exception codes. Clients should always include a default or other fallback. +enum GoogleSignInExceptionCode { + /// A catch-all for implemenatations that need to return a code that does not + /// have a corresponding known code. /// - /// See also https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInOptions.html#DEFAULT_SIGN_IN. - standard, + /// Whenever possible, implementators should update the platform interface to + /// add new codes instead of using this type. When it is used, the + /// [GoogleSignInException.description] should have information allowing + /// developers to understand the issue. + unknownError, - /// Recommended configuration for Games sign in. + /// The operation was canceled by the user. + canceled, + + /// The operation was interrupted for a reason other than being intentionally + /// canceled by the user. + interrupted, + + /// The client is misconfigured. /// - /// This is currently only supported on Android and will throw an error if used - /// on other platforms. + /// The [GoogleSignInException.description] should include details about the + /// configuration problem. + clientConfigurationError, + + /// The underlying auth SDK is unavailable or misconfigured. + providerConfigurationError, + + /// UI needed to be displayed, but could not be. /// - /// See also https://developers.google.com/android/reference/com/google/android/gms/auth/api/signin/GoogleSignInOptions.html#public-static-final-googlesigninoptions-default_games_sign_in. - games + /// For example, this can be returned on Android if a call tries to show UI + /// when no Activity is available. + uiUnavailable, + + /// An operation was attempted on a user who is not the current user, on a + /// platform where the SDK only supports a single user being signed in at a + /// time. + userMismatch, } /// The parameters to use when initializing the sign in process. @@ -27,29 +75,15 @@ enum SignInOption { /// See: /// https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams @immutable -class SignInInitParameters { +class InitParameters { /// The parameters to use when initializing the sign in process. - const SignInInitParameters({ - this.scopes = const [], - this.signInOption = SignInOption.standard, - this.hostedDomain, + const InitParameters({ this.clientId, this.serverClientId, - this.forceCodeForRefreshToken = false, - this.forceAccountName, + this.nonce, + this.hostedDomain, }); - /// The list of OAuth scope codes to request when signing in. - final List scopes; - - /// The user experience to use when signing in. [SignInOption.games] is - /// only supported on Android. - final SignInOption signInOption; - - /// Restricts sign in to accounts of the user in the specified domain. - /// By default, the list of accounts will not be restricted. - final String? hostedDomain; - /// The OAuth client ID of the app. /// /// The default is null, which means that the client ID will be sourced from a @@ -74,128 +108,320 @@ class SignInInitParameters { /// where you can find the details about the configuration files. final String? serverClientId; - /// If true, ensures the authorization code can be exchanged for an access - /// token. + /// An optional nonce for added security in ID token requests. + final String? nonce; + + /// A hosted domain to restrict accounts to. + /// + /// The default is null, meaning no restriction. /// - /// This is only used on Android. - final bool forceCodeForRefreshToken; + /// How this restriction is interpreted if provided may vary by platform. + // This is in init paramater because different platforms apply it at different + // stages, and there is no expected use case for an instance varying in + // hosting restriction across calls, so this allows each implemented to handle + // it however best applies to its underlying SDK. + final String? hostedDomain; +} + +/// Parameters for the attemptLightweightAuthentication method. +@immutable +class AttemptLightweightAuthenticationParameters { + /// Creates new authentication parameters. + const AttemptLightweightAuthenticationParameters(); - /// Can be used to explicitly set an account name on the underlying platform sign-in API. + // This class exists despite currently being empty to allow future addition of + // parameters without breaking changes. +} + +/// Parameters for the authenticate method. +@immutable +class AuthenticateParameters { + /// Creates new authentication parameters. + const AuthenticateParameters({this.scopeHint = const []}); + + /// A list of scopes that the application will attempt to use/request + /// immediately. + /// + /// Implementations should ignore this paramater unless the underlying SDK + /// provides a combined authentication+authorization UI flow. Clients are + /// responsible for triggering an explicit authorization flow if authorization + /// isn't granted. + final List scopeHint; +} + +/// Common elements of authorization method parameters. +/// +/// Fields should be added here if they would apply to most or all authorization +/// requests, in particular if they apply to both +/// [ClientAuthorizationTokensForScopesParameters] and +/// [ServerAuthorizationTokensForScopesParameters]. +@immutable +class AuthorizationRequestDetails { + /// Creates a new authorization request specification. + const AuthorizationRequestDetails({ + required this.scopes, + required this.userId, + required this.email, + required this.promptIfUnauthorized, + }); + + /// The scopes to be authorized. + final List scopes; + + /// The account to authorize. + /// + /// If this is not specified, the platform implementation will determine the + /// account, and the method of doing so may vary by platform. For instance, + /// it may use the last account that was signed in, or it may prompt for + /// authentication as part of the authorization flow. + final String? userId; + + /// The email address of the account to authorize. + /// + /// Some platforms reference accounts by email at the SDK level, so this + /// should be provided if userId is provided. + final String? email; + + /// Whether to allow showing UI if the authorizations are not already + /// available without UI. /// - /// This should only be set on Android; other platforms may throw. - final String? forceAccountName; + /// Implementations should guarantee the 'false' behavior; if an underlying + /// SDK method may or may not show UI, and the wrapper cannot reliably + /// determine in advance, it should fail rather than call that method if + /// this parameter is false. + final bool promptIfUnauthorized; +} + +/// Parameters for the clientAuthorizationTokensForScopes method. +// +// This is distinct from [AuthorizationRequestDetails] to allow for divergence +// in method paramaters in the future without breaking changes. +@immutable +class ClientAuthorizationTokensForScopesParameters { + /// Creates a new parameter object with the given details. + const ClientAuthorizationTokensForScopesParameters({ + required this.request, + }); + + /// Details about the authorization request. + final AuthorizationRequestDetails request; +} + +/// Parameters for the serverAuthorizationTokensForScopes method. +// +// This is distinct from [AuthorizationRequestDetails] to allow for divergence +// in method paramaters in the future without breaking changes. +@immutable +class ServerAuthorizationTokensForScopesParameters { + /// Creates a new parameter object with the given details. + const ServerAuthorizationTokensForScopesParameters({ + required this.request, + }); + + /// Details about the authorization request. + final AuthorizationRequestDetails request; } -/// Holds information about the signed in user. +/// Holds information about the signed-in user. +@immutable class GoogleSignInUserData { /// Uses the given data to construct an instance. - GoogleSignInUserData({ + const GoogleSignInUserData({ required this.email, required this.id, this.displayName, this.photoUrl, - this.idToken, - this.serverAuthCode, }); - /// The display name of the signed in user. + /// The user's display name. /// /// Not guaranteed to be present for all users, even when configured. - String? displayName; + final String? displayName; - /// The email address of the signed in user. + /// The user's email address. /// - /// Applications should not key users by email address since a Google account's - /// email address can change. Use [id] as a key instead. + /// Applications should not key users by email address since a Google + /// account's email address can change. Use [id] as a key instead. /// - /// _Important_: Do not use this returned email address to communicate the - /// currently signed in user to your backend server. Instead, send an ID token - /// which can be securely validated on the server. See [idToken]. - String email; + /// This should not be used to communicate the currently signed in user to a + /// backend server. Instead, send an ID token which can be securely validated + /// on the server. See [AuthenticationTokenData.idToken]. + final String email; - /// The unique ID for the Google account. + /// The user's unique account ID. /// /// This is the preferred unique key to use for a user record. /// - /// _Important_: Do not use this returned Google ID to communicate the - /// currently signed in user to your backend server. Instead, send an ID token - /// which can be securely validated on the server. See [idToken]. - String id; + /// This should not be used to communicate the currently signed in user to a + /// backend server. Instead, send an ID token which can be securely validated + /// on the server. See [AuthenticationTokenData.idToken]. + final String id; - /// The photo url of the signed in user if the user has a profile picture. + /// The user's profile picture URL. /// /// Not guaranteed to be present for all users, even when configured. - String? photoUrl; + final String? photoUrl; + + @override + int get hashCode => Object.hash(displayName, email, id, photoUrl); + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is GoogleSignInUserData && + other.displayName == displayName && + other.email == email && + other.id == id && + other.photoUrl == photoUrl; + } +} + +/// Holds tokens that result from authentication. +@immutable +class AuthenticationTokenData { + /// Creates authentication data with the given tokens. + const AuthenticationTokenData({ + required this.idToken, + }); /// A token that can be sent to your own server to verify the authentication /// data. - String? idToken; - - /// Server auth code used to access Google Login - String? serverAuthCode; + final String? idToken; @override - // TODO(stuartmorgan): Make this class immutable in the next breaking change. - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => - Object.hash(displayName, email, id, photoUrl, idToken, serverAuthCode); + int get hashCode => idToken.hashCode; @override - // TODO(stuartmorgan): Make this class immutable in the next breaking change. - // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (identical(this, other)) { - return true; - } - if (other is! GoogleSignInUserData) { + if (other.runtimeType != runtimeType) { return false; } - final GoogleSignInUserData otherUserData = other; - return otherUserData.displayName == displayName && - otherUserData.email == email && - otherUserData.id == id && - otherUserData.photoUrl == photoUrl && - otherUserData.idToken == idToken && - otherUserData.serverAuthCode == serverAuthCode; + return other is AuthenticationTokenData && other.idToken == idToken; } } -/// Holds authentication data after sign in. -class GoogleSignInTokenData { - /// Build `GoogleSignInTokenData`. - GoogleSignInTokenData({ - this.idToken, - this.accessToken, - this.serverAuthCode, +/// Holds tokens that result from authorization for a client endpoint. +@immutable +class ClientAuthorizationTokenData { + /// Creates authorization data with the given tokens. + const ClientAuthorizationTokenData({ + required this.accessToken, }); - /// An OpenID Connect ID token for the authenticated user. - String? idToken; - /// The OAuth2 access token used to access Google services. - String? accessToken; - - /// Server auth code used to access Google Login - String? serverAuthCode; + final String accessToken; @override - // TODO(stuartmorgan): Make this class immutable in the next breaking change. - // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hash(idToken, accessToken, serverAuthCode); + int get hashCode => accessToken.hashCode; @override - // TODO(stuartmorgan): Make this class immutable in the next breaking change. - // ignore: avoid_equals_and_hash_code_on_mutable_classes bool operator ==(Object other) { - if (identical(this, other)) { - return true; + if (other.runtimeType != runtimeType) { + return false; } - if (other is! GoogleSignInTokenData) { + return other is ClientAuthorizationTokenData && + other.accessToken == accessToken; + } +} + +/// Holds tokens that result from authorization for a server endpoint. +@immutable +class ServerAuthorizationTokenData { + /// Creates authorization data with the given tokens. + const ServerAuthorizationTokenData({ + required this.serverAuthCode, + }); + + /// Auth code to provide to a backend server to exchange for access or + /// refresh tokens. + final String serverAuthCode; + + @override + int get hashCode => serverAuthCode.hashCode; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { return false; } - final GoogleSignInTokenData otherTokenData = other; - return otherTokenData.idToken == idToken && - otherTokenData.accessToken == accessToken && - otherTokenData.serverAuthCode == serverAuthCode; + return other is ServerAuthorizationTokenData && + other.serverAuthCode == serverAuthCode; } } + +/// Return value for authentication request methods. +/// +/// Contains information about the authenticated user, as well as authentication +/// tokens. +@immutable +class AuthenticationResults { + /// Creates a new result object. + const AuthenticationResults( + {required this.user, required this.authenticationTokens}); + + /// The user that was authenticated. + final GoogleSignInUserData user; + + /// Authentication tokens for the signed-in user. + final AuthenticationTokenData authenticationTokens; +} + +/// Parameters for the signOut method. +@immutable +class SignOutParams { + /// Creates new sign-out parameters. + const SignOutParams(); + + // This class exists despite currently being empty to allow future addition of + // parameters without breaking changes. +} + +/// Parameters for the disconnect method. +@immutable +class DisconnectParams { + /// Creates new disconnect parameters. + const DisconnectParams(); + + // This class exists despite currently being empty to allow future addition of + // parameters without breaking changes. +} + +/// A base class for authentication event streams. +@immutable +sealed class AuthenticationEvent { + const AuthenticationEvent(); +} + +/// A sign-in event, corresponding to an authentication flow completing +/// successfully. +@immutable +class AuthenticationEventSignIn extends AuthenticationEvent { + /// Creates an event for a successful sign in. + const AuthenticationEventSignIn( + {required this.user, required this.authenticationTokens}); + + /// The user that was authenticated. + final GoogleSignInUserData user; + + /// Authentication tokens for the signed-in user. + final AuthenticationTokenData authenticationTokens; +} + +/// A sign-out event, corresponding to a user having been signed out. +/// +/// Implicit sign-outs (for example, due to server-side authentication +/// revocation, or timeouts) are not guaranteed to send events. +@immutable +class AuthenticationEventSignOut extends AuthenticationEvent {} + +/// An authentication failure that resulted in an exception. +@immutable +class AuthenticationEventException extends AuthenticationEvent { + /// Creates an exception event. + const AuthenticationEventException(this.exception); + + /// The exception thrown during authentication. + final GoogleSignInException exception; +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart deleted file mode 100644 index 6f03a6c357fe..000000000000 --- a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -// 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 '../google_sign_in_platform_interface.dart'; - -/// Converts user data coming from native code into the proper platform interface type. -GoogleSignInUserData? getUserDataFromMap(Map? data) { - if (data == null) { - return null; - } - return GoogleSignInUserData( - email: data['email']! as String, - id: data['id']! as String, - displayName: data['displayName'] as String?, - photoUrl: data['photoUrl'] as String?, - idToken: data['idToken'] as String?, - serverAuthCode: data['serverAuthCode'] as String?); -} - -/// Converts token data coming from native code into the proper platform interface type. -GoogleSignInTokenData getTokenDataFromMap(Map data) { - return GoogleSignInTokenData( - idToken: data['idToken'] as String?, - accessToken: data['accessToken'] as String?, - serverAuthCode: data['serverAuthCode'] as String?, - ); -} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index b4bebcced493..b399bb38f69f 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/flutter/packages/tree/main/packages/google_sign_i issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.5.0 +version: 3.0.0 environment: sdk: ^3.6.0 diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart index 057f13cb26f5..f536bd2e6bb1 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart +++ b/packages/google_sign_in/google_sign_in_platform_interface/test/google_sign_in_platform_interface_test.dart @@ -8,15 +8,8 @@ import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { - // Store the initial instance before any tests change it. - final GoogleSignInPlatform initialInstance = GoogleSignInPlatform.instance; - - group('$GoogleSignInPlatform', () { - test('$MethodChannelGoogleSignIn is the default instance', () { - expect(initialInstance, isA()); - }); - - test('Cannot be implemented with `implements`', () { + group('GoogleSignInPlatform', () { + test('cannot be implemented with `implements`', () { expect(() { GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); // In versions of `package:plugin_platform_interface` prior to fixing @@ -29,71 +22,125 @@ void main() { }, throwsA(anything)); }); - test('Can be extended', () { + test('can be extended', () { GoogleSignInPlatform.instance = ExtendsGoogleSignInPlatform(); }); - test('Can be mocked with `implements`', () { - GoogleSignInPlatform.instance = ModernMockImplementation(); + test('can be mocked with `implements`', () { + GoogleSignInPlatform.instance = MockImplementation(); }); - test('still supports legacy isMock', () { - GoogleSignInPlatform.instance = LegacyIsMockImplementation(); - }); - }); - - group('GoogleSignInTokenData', () { - test('can be compared by == operator', () { - final GoogleSignInTokenData firstInstance = GoogleSignInTokenData( - accessToken: 'accessToken', - idToken: 'idToken', - serverAuthCode: 'serverAuthCode', - ); - final GoogleSignInTokenData secondInstance = GoogleSignInTokenData( - accessToken: 'accessToken', - idToken: 'idToken', - serverAuthCode: 'serverAuthCode', - ); - expect(firstInstance == secondInstance, isTrue); + test('implements authenticationEvents to return null by default', () { + // This uses ExtendsGoogleSignInPlatform since that's within the control + // of the test file, and doesn't override authenticationEvents; using + // the default `.instance` would only validate that the placeholder has + // this behavior, which could be implemented in the subclass. + expect(ExtendsGoogleSignInPlatform().authenticationEvents, null); }); }); group('GoogleSignInUserData', () { test('can be compared by == operator', () { - final GoogleSignInUserData firstInstance = GoogleSignInUserData( + const GoogleSignInUserData firstInstance = GoogleSignInUserData( email: 'email', id: 'id', displayName: 'displayName', photoUrl: 'photoUrl', - idToken: 'idToken', - serverAuthCode: 'serverAuthCode', ); - final GoogleSignInUserData secondInstance = GoogleSignInUserData( + const GoogleSignInUserData secondInstance = GoogleSignInUserData( email: 'email', id: 'id', displayName: 'displayName', photoUrl: 'photoUrl', + ); + expect(firstInstance == secondInstance, isTrue); + }); + }); + + group('AuthenticationTokenData', () { + test('can be compared by == operator', () { + const AuthenticationTokenData firstInstance = AuthenticationTokenData( + idToken: 'idToken', + ); + const AuthenticationTokenData secondInstance = AuthenticationTokenData( idToken: 'idToken', - serverAuthCode: 'serverAuthCode', ); expect(firstInstance == secondInstance, isTrue); }); }); -} -class LegacyIsMockImplementation extends Mock implements GoogleSignInPlatform { - @override - bool get isMock => true; + group('ClientAuthorizationTokenData', () { + test('can be compared by == operator', () { + const ClientAuthorizationTokenData firstInstance = + ClientAuthorizationTokenData( + accessToken: 'accessToken', + ); + const ClientAuthorizationTokenData secondInstance = + ClientAuthorizationTokenData( + accessToken: 'accessToken', + ); + expect(firstInstance == secondInstance, isTrue); + }); + }); + + group('ServerAuthorizationTokenData', () { + test('can be compared by == operator', () { + const ServerAuthorizationTokenData firstInstance = + ServerAuthorizationTokenData( + serverAuthCode: 'serverAuthCode', + ); + const ServerAuthorizationTokenData secondInstance = + ServerAuthorizationTokenData( + serverAuthCode: 'serverAuthCode', + ); + expect(firstInstance == secondInstance, isTrue); + }); + }); } -class ModernMockImplementation extends Mock +class MockImplementation extends Mock with MockPlatformInterfaceMixin - implements GoogleSignInPlatform { - @override - bool get isMock => false; -} + implements GoogleSignInPlatform {} class ImplementsGoogleSignInPlatform extends Mock implements GoogleSignInPlatform {} -class ExtendsGoogleSignInPlatform extends GoogleSignInPlatform {} +class ExtendsGoogleSignInPlatform extends GoogleSignInPlatform { + @override + Future? attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) async { + return null; + } + + @override + bool supportsAuthenticate() => false; + + @override + Future authenticate(AuthenticateParameters params) { + throw UnimplementedError(); + } + + @override + bool authorizationRequiresUserInteraction() => false; + + @override + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params) async { + return null; + } + + @override + Future disconnect(DisconnectParams params) async {} + + @override + Future init(InitParameters params) async {} + + @override + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params) async { + return null; + } + + @override + Future signOut(SignOutParams params) async {} +} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart deleted file mode 100644 index 52e792a9c254..000000000000 --- a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart +++ /dev/null @@ -1,174 +0,0 @@ -// 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 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_platform_interface/src/utils.dart'; - -const Map kUserData = { - 'email': 'john.doe@gmail.com', - 'id': '8162538176523816253123', - 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', - 'displayName': 'John Doe', - 'idToken': '123', - 'serverAuthCode': '789', -}; - -const Map kTokenData = { - 'idToken': '123', - 'accessToken': '456', - 'serverAuthCode': '789', -}; - -const Map kDefaultResponses = { - 'init': null, - 'signInSilently': kUserData, - 'signIn': kUserData, - 'signOut': null, - 'disconnect': null, - 'isSignedIn': true, - 'getTokens': kTokenData, - 'requestScopes': true, -}; - -final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); -final GoogleSignInTokenData kToken = - getTokenDataFromMap(kTokenData as Map); - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelGoogleSignIn', () { - final MethodChannelGoogleSignIn googleSignIn = MethodChannelGoogleSignIn(); - final MethodChannel channel = googleSignIn.channel; - - final List log = []; - late Map - responses; // Some tests mutate some kDefaultResponses - - setUp(() { - responses = Map.from(kDefaultResponses); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }); - log.clear(); - }); - - test('signInSilently transforms platform data to GoogleSignInUserData', - () async { - final dynamic response = await googleSignIn.signInSilently(); - expect(response, kUser); - }); - test('signInSilently Exceptions -> throws', () async { - responses['signInSilently'] = Exception('Not a user'); - expect(googleSignIn.signInSilently(), - throwsA(isInstanceOf())); - }); - - test('signIn transforms platform data to GoogleSignInUserData', () async { - final dynamic response = await googleSignIn.signIn(); - expect(response, kUser); - }); - test('signIn Exceptions -> throws', () async { - responses['signIn'] = Exception('Not a user'); - expect(googleSignIn.signIn(), throwsA(isInstanceOf())); - }); - - test('getTokens transforms platform data to GoogleSignInTokenData', - () async { - final dynamic response = await googleSignIn.getTokens( - email: 'example@example.com', shouldRecoverAuth: false); - expect(response, kToken); - expect( - log[0], - isMethodCall('getTokens', arguments: { - 'email': 'example@example.com', - 'shouldRecoverAuth': false, - })); - }); - - test('Other functions pass through arguments to the channel', () async { - final Map tests = { - () { - googleSignIn.init( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId'); - }: isMethodCall('init', arguments: { - 'hostedDomain': 'example.com', - 'scopes': ['two', 'scopes'], - 'signInOption': 'SignInOption.games', - 'clientId': 'fakeClientId', - 'serverClientId': null, - 'forceCodeForRefreshToken': false, - }), - () { - googleSignIn.getTokens( - email: 'example@example.com', shouldRecoverAuth: false); - }: isMethodCall('getTokens', arguments: { - 'email': 'example@example.com', - 'shouldRecoverAuth': false, - }), - () { - googleSignIn.clearAuthCache(token: 'abc'); - }: isMethodCall('clearAuthCache', arguments: { - 'token': 'abc', - }), - () { - googleSignIn.requestScopes(['newScope', 'anotherScope']); - }: isMethodCall('requestScopes', arguments: { - 'scopes': ['newScope', 'anotherScope'], - }), - googleSignIn.signOut: isMethodCall('signOut', arguments: null), - googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), - googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), - }; - - for (final void Function() f in tests.keys) { - f(); - } - - expect(log, tests.values); - }); - - test('canAccessScopes is unimplemented', () async { - expect(() async { - await googleSignIn - .canAccessScopes(['someScope'], accessToken: 'token'); - }, throwsUnimplementedError); - }); - - test('userDataEvents returns null', () async { - expect(googleSignIn.userDataEvents, isNull); - }); - - test('initWithParams passes through arguments to the channel', () async { - await googleSignIn.initWithParams(const SignInInitParameters( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId', - serverClientId: 'fakeServerClientId', - forceCodeForRefreshToken: true)); - expect(log, [ - isMethodCall('init', arguments: { - 'hostedDomain': 'example.com', - 'scopes': ['two', 'scopes'], - 'signInOption': 'SignInOption.games', - 'clientId': 'fakeClientId', - 'serverClientId': 'fakeServerClientId', - 'forceCodeForRefreshToken': true, - }), - ]); - }); - }); -} diff --git a/script/configs/exclude_all_packages_app.yaml b/script/configs/exclude_all_packages_app.yaml index ca15045d59d9..080d15b97e96 100644 --- a/script/configs/exclude_all_packages_app.yaml +++ b/script/configs/exclude_all_packages_app.yaml @@ -11,3 +11,6 @@ # This is a permanent entry, as it should never be a direct app dependency. - plugin_platform_interface +# Breaking change in the process of being landed. This will be removed +# once all the layers have landed. +- google_sign_in_platform_interface