From 432a627c15563e3d35271a796b91219245e19895 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 12 Feb 2025 16:25:11 -0500 Subject: [PATCH 01/52] Initial PoC using CredentialManager --- .../android/build.gradle | 16 +- .../googlesignin/GoogleSignInPlugin.java | 689 +++++--------- .../googlesignin/GoogleSignInWrapper.java | 42 - .../plugins/googlesignin/Messages.java | 874 ------------------ .../flutter/plugins/googlesignin/Messages.kt | 540 +++++++++++ .../plugins/googlesignin/ResultUtils.kt | 37 + .../example/android/app/build.gradle | 1 + .../example/android/build.gradle | 4 + .../example/lib/main.dart | 2 + .../lib/google_sign_in_android.dart | 299 ++++-- .../lib/src/messages.g.dart | 580 +++++++----- .../pigeons/messages.dart | 209 +++-- 12 files changed, 1565 insertions(+), 1728 deletions(-) delete mode 100644 packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java delete mode 100644 packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java create mode 100644 packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt create mode 100644 packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt diff --git a/packages/google_sign_in/google_sign_in_android/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle index 74e7499377b..fe930e9faf7 100644 --- a/packages/google_sign_in/google_sign_in_android/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -2,6 +2,7 @@ group 'io.flutter.plugins.googlesignin' version '1.0-SNAPSHOT' buildscript { + ext.kotlin_version = '2.1.10' repositories { google() mavenCentral() @@ -9,6 +10,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.5.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -20,6 +22,7 @@ rootProject.allprojects { } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { namespace 'io.flutter.plugins.googlesignin' @@ -35,6 +38,14 @@ android { targetCompatibility JavaVersion.VERSION_11 } + kotlinOptions { + jvmTarget = '11' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + lintOptions { checkAllWarnings true warningsAsErrors true @@ -56,7 +67,10 @@ android { } dependencies { - implementation 'com.google.android.gms:play-services-auth:21.0.0' + implementation 'androidx.credentials:credentials:1.3.0' + implementation 'androidx.credentials:credentials-play-services-auth:1.3.0' + implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1' + implementation 'com.google.android.gms:play-services-auth:21.3.0' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:5.2.0' } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 46294212703..06858796ce8 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -7,36 +7,48 @@ import android.accounts.Account; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; +import android.content.IntentSender; +import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.auth.GoogleAuthUtil; -import com.google.android.gms.auth.UserRecoverableAuthException; -import com.google.android.gms.auth.api.signin.GoogleSignIn; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; -import com.google.android.gms.auth.api.signin.GoogleSignInStatusCodes; +import androidx.credentials.ClearCredentialStateRequest; +import androidx.credentials.Credential; +import androidx.credentials.CredentialManager; +import androidx.credentials.CredentialManagerCallback; +import androidx.credentials.CustomCredential; +import androidx.credentials.GetCredentialRequest; +import androidx.credentials.GetCredentialResponse; +import androidx.credentials.exceptions.ClearCredentialException; +import androidx.credentials.exceptions.GetCredentialCancellationException; +import androidx.credentials.exceptions.GetCredentialException; +import androidx.credentials.exceptions.GetCredentialInterruptedException; +import androidx.credentials.exceptions.GetCredentialProviderConfigurationException; +import androidx.credentials.exceptions.GetCredentialUnsupportedException; +import androidx.credentials.exceptions.NoCredentialException; +import com.google.android.gms.auth.api.identity.AuthorizationRequest; +import com.google.android.gms.auth.api.identity.AuthorizationResult; +import com.google.android.gms.auth.api.identity.Identity; import com.google.android.gms.common.api.ApiException; -import com.google.android.gms.common.api.CommonStatusCodes; import com.google.android.gms.common.api.Scope; -import com.google.android.gms.tasks.RuntimeExecutionException; -import com.google.android.gms.tasks.Task; +import com.google.android.libraries.identity.googleid.GetGoogleIdOption; +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential; +import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugins.googlesignin.Messages.FlutterError; -import io.flutter.plugins.googlesignin.Messages.GoogleSignInApi; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executors; +import kotlin.Result; +import kotlin.Unit; +import kotlin.jvm.functions.Function1; /** Google sign-in plugin for Flutter. */ public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware { @@ -45,19 +57,18 @@ public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware { private ActivityPluginBinding activityPluginBinding; @VisibleForTesting - public void initInstance( - @NonNull BinaryMessenger messenger, - @NonNull Context context, - @NonNull GoogleSignInWrapper googleSignInWrapper) { + public void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) { this.messenger = messenger; - delegate = new Delegate(context, googleSignInWrapper); - GoogleSignInApi.setUp(messenger, delegate); + delegate = new Delegate(context); + CredentialManagerApi.Companion.setUp(messenger, delegate); + AuthorizationClientApi.Companion.setUp(messenger, delegate); } private void dispose() { delegate = null; if (messenger != null) { - GoogleSignInApi.setUp(messenger, null); + CredentialManagerApi.Companion.setUp(messenger, null); + AuthorizationClientApi.Companion.setUp(messenger, null); messenger = null; } } @@ -76,8 +87,7 @@ private void disposeActivity() { @Override public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - initInstance( - binding.getBinaryMessenger(), binding.getApplicationContext(), new GoogleSignInWrapper()); + initInstance(binding.getBinaryMessenger(), binding.getApplicationContext()); } @Override @@ -111,37 +121,24 @@ public void onDetachedFromActivity() { * class for use in other plugins that wrap basic sign-in functionality. * *

All methods in this class assume that they are run to completion before any other method is - * invoked. In this context, "run to completion" means that their {@link Messages.Result} argument - * has been completed (either successfully or in error). This class provides no synchronization - * constructs to guarantee such behavior; callers are responsible for providing such guarantees. + * invoked. In this context, "run to completion" means that their callback argument has been + * completed (either successfully or in error). This class provides no synchronization constructs + * to guarantee such behavior; callers are responsible for providing such guarantees. */ - public static class Delegate implements PluginRegistry.ActivityResultListener, GoogleSignInApi { - private static final int REQUEST_CODE_SIGNIN = 53293; - private static final int REQUEST_CODE_RECOVER_AUTH = 53294; - @VisibleForTesting static final int REQUEST_CODE_REQUEST_SCOPE = 53295; - - private static final String ERROR_REASON_EXCEPTION = "exception"; - private static final String ERROR_REASON_STATUS = "status"; - // These error codes must match with ones declared on iOS and Dart sides. - private static final String ERROR_REASON_SIGN_IN_CANCELED = "sign_in_canceled"; - private static final String ERROR_REASON_SIGN_IN_REQUIRED = "sign_in_required"; - private static final String ERROR_REASON_NETWORK_ERROR = "network_error"; - private static final String ERROR_REASON_SIGN_IN_FAILED = "sign_in_failed"; - private static final String ERROR_FAILURE_TO_RECOVER_AUTH = "failed_to_recover_auth"; - private static final String ERROR_USER_RECOVERABLE_AUTH = "user_recoverable_auth"; + public static class Delegate + implements PluginRegistry.ActivityResultListener, + CredentialManagerApi, + AuthorizationClientApi { + private static final int REQUEST_CODE_AUTHORIZE = 53294; private final @NonNull Context context; // Only set activity for v2 embedder. Always access activity from getActivity() method. private @Nullable Activity activity; - private final GoogleSignInWrapper googleSignInWrapper; - private GoogleSignInClient signInClient; - private List requestedScopes; - private PendingOperation pendingOperation; + private Function1, Unit> pendingAuthorizationCallback; - public Delegate(@NonNull Context context, @NonNull GoogleSignInWrapper googleSignInWrapper) { + public Delegate(@NonNull Context context) { this.context = context; - this.googleSignInWrapper = googleSignInWrapper; } public void setActivity(@Nullable Activity activity) { @@ -153,86 +150,15 @@ public void setActivity(@Nullable Activity activity) { return activity; } - private void checkAndSetPendingOperation( - String method, - Messages.Result userDataResult, - Messages.VoidResult voidResult, - Messages.Result boolResult, - Messages.Result stringResult, - Object data) { - if (pendingOperation != null) { - throw new IllegalStateException( - "Concurrent operations detected: " + pendingOperation.method + ", " + method); - } - pendingOperation = - new PendingOperation(method, userDataResult, voidResult, boolResult, stringResult, data); - } - - private void checkAndSetPendingSignInOperation( - String method, @NonNull Messages.Result result) { - checkAndSetPendingOperation(method, result, null, null, null, null); - } - - private void checkAndSetPendingVoidOperation( - String method, @NonNull Messages.VoidResult result) { - checkAndSetPendingOperation(method, null, result, null, null, null); - } - - private void checkAndSetPendingBoolOperation( - String method, @NonNull Messages.Result result) { - checkAndSetPendingOperation(method, null, null, result, null, null); - } - - private void checkAndSetPendingStringOperation( - String method, @NonNull Messages.Result result, @Nullable Object data) { - checkAndSetPendingOperation(method, null, null, null, result, data); - } - - private void checkAndSetPendingAccessTokenOperation( - String method, Messages.Result result, @NonNull Object data) { - checkAndSetPendingStringOperation(method, result, data); - } - - /** - * Initializes this delegate so that it is ready to perform other operations. The Dart code - * guarantees that this will be called and completed before any other methods are invoked. - */ @Override - public void init(@NonNull Messages.InitParams params) { + public void getCredential( + @NonNull GetCredentialRequestParams params, + @NonNull Function1, Unit> callback) { try { - GoogleSignInOptions.Builder optionsBuilder; - - switch (params.getSignInType()) { - case GAMES: - optionsBuilder = - new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN); - break; - case STANDARD: - optionsBuilder = - new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail(); - break; - default: - throw new IllegalStateException("Unknown signInOption"); - } - - // The clientId parameter is not supported on Android. - // Android apps are identified by their package name and the SHA-1 of their signing key. - // https://developers.google.com/android/guides/client-auth - // https://developers.google.com/identity/sign-in/android/start#configure-a-google-api-project String serverClientId = params.getServerClientId(); - if (!isNullOrEmpty(params.getClientId()) && isNullOrEmpty(serverClientId)) { - Log.w( - "google_sign_in", - "clientId is not supported on Android and is interpreted as serverClientId. " - + "Use serverClientId instead to suppress this warning."); - serverClientId = params.getClientId(); - } - if (isNullOrEmpty(serverClientId)) { - // Only requests a clientId if google-services.json was present and parsed - // by the google-services Gradle script. - // TODO(jackson): Perhaps we should provide a mechanism to override this - // behavior. + // If the required server client ID wasn't explicitly provided, check whether it was in + // a google-services.json parsed by the google-services Gradle script. @SuppressLint("DiscouragedApi") int webClientIdIdentifier = context @@ -242,344 +168,233 @@ public void init(@NonNull Messages.InitParams params) { serverClientId = context.getString(webClientIdIdentifier); } } - if (!isNullOrEmpty(serverClientId)) { - optionsBuilder.requestIdToken(serverClientId); - optionsBuilder.requestServerAuthCode( - serverClientId, params.getForceCodeForRefreshToken()); - } - requestedScopes = params.getScopes(); - for (String scope : requestedScopes) { - optionsBuilder.requestScopes(new Scope(scope)); - } - if (!isNullOrEmpty(params.getHostedDomain())) { - optionsBuilder.setHostedDomain(params.getHostedDomain()); - } - - String forceAccountName = params.getForceAccountName(); - if (!isNullOrEmpty(forceAccountName)) { - optionsBuilder.setAccountName(forceAccountName); + if (isNullOrEmpty(serverClientId)) { + ResultUtilsKt.completeWithGetCredentialFailure( + callback, + new GetCredentialFailure( + GetCredentialFailureType.MISSING_SERVER_CLIENT_ID, + "CredentialManager requires a serverClientId.", + null)); + return; } - signInClient = googleSignInWrapper.getClient(context, optionsBuilder.build()); - } catch (Exception e) { - throw new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null); - } - } - - /** - * Returns the account information for the user who is signed in to this app. If no user is - * signed in, tries to sign the user in without displaying any user interface. - */ - @Override - public void signInSilently(@NonNull Messages.Result result) { - checkAndSetPendingSignInOperation("signInSilently", result); - Task task = signInClient.silentSignIn(); - if (task.isComplete()) { - // There's immediate result available. - onSignInResult(task); - } else { - task.addOnCompleteListener(this::onSignInResult); - } - } - - /** - * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes - * were requested. - */ - @Override - public void signIn(@NonNull Messages.Result result) { - if (getActivity() == null) { - throw new IllegalStateException("signIn needs a foreground activity"); - } - checkAndSetPendingSignInOperation("signIn", result); - - Intent signInIntent = signInClient.getSignInIntent(); - getActivity().startActivityForResult(signInIntent, REQUEST_CODE_SIGNIN); - } - - /** - * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently - * sign back in. - */ - @Override - public void signOut(@NonNull Messages.VoidResult result) { - checkAndSetPendingVoidOperation("signOut", result); - - signInClient - .signOut() - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - finishWithSuccess(); + GetGoogleIdOption.Builder optionBuilder = + new GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(params.getFilterToAuthorized()) + .setAutoSelectEnabled(params.getAutoSelectEnabled()) + .setServerClientId(serverClientId); + GetGoogleIdOption googleIdOption = optionBuilder.build(); + GetCredentialRequest request = + new GetCredentialRequest.Builder().addCredentialOption(googleIdOption).build(); + CredentialManager credentialManager = CredentialManager.create(context); + credentialManager.getCredentialAsync( + context, + request, + null, + Executors.newSingleThreadExecutor(), + new CredentialManagerCallback<>() { + @Override + public void onResult(GetCredentialResponse response) { + Credential credential = response.getCredential(); + if (credential instanceof CustomCredential + && credential + .getType() + .equals(GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL)) { + GoogleIdTokenCredential googleIdTokenCredential = + GoogleIdTokenCredential.createFrom(credential.getData()); + Uri profilePictureUri = googleIdTokenCredential.getProfilePictureUri(); + ResultUtilsKt.completeWithGetGetCredentialResult( + callback, + new GetCredentialSuccess( + new PlatformGoogleIdTokenCredential( + googleIdTokenCredential.getDisplayName(), + googleIdTokenCredential.getFamilyName(), + googleIdTokenCredential.getGivenName(), + googleIdTokenCredential.getId(), + googleIdTokenCredential.getIdToken(), + profilePictureUri == null ? null : profilePictureUri.toString()))); } else { - finishWithError(ERROR_REASON_STATUS, "Failed to signout."); + ResultUtilsKt.completeWithGetCredentialFailure( + callback, + new GetCredentialFailure( + GetCredentialFailureType.UNEXPECTED_CREDENTIAL_TYPE, + "Unexpected credential type: " + credential, + null)); } - }); - } - - /** Signs the user out, and revokes their credentials. */ - @Override - public void disconnect(@NonNull Messages.VoidResult result) { - checkAndSetPendingVoidOperation("disconnect", result); + } - signInClient - .revokeAccess() - .addOnCompleteListener( - task -> { - if (task.isSuccessful()) { - finishWithSuccess(); + @Override + public void onError(@NonNull GetCredentialException e) { + GetCredentialFailureType type; + if (e instanceof GetCredentialCancellationException) { + type = GetCredentialFailureType.CANCELED; + } else if (e instanceof GetCredentialInterruptedException) { + type = GetCredentialFailureType.INTERRUPTED; + } else if (e instanceof GetCredentialProviderConfigurationException) { + type = GetCredentialFailureType.PROVIDER_CONFIGURATION_ISSUE; + } else if (e instanceof GetCredentialUnsupportedException) { + type = GetCredentialFailureType.UNSUPPORTED; + } else if (e instanceof NoCredentialException) { + type = GetCredentialFailureType.NO_CREDENTIAL; } else { - finishWithError(ERROR_REASON_STATUS, "Failed to disconnect."); + type = GetCredentialFailureType.UNKNOWN; } - }); + // Errors are reported through the return value as structured data, rather than + // a Result error's PlatformException. + ResultUtilsKt.completeWithGetCredentialFailure( + callback, new GetCredentialFailure(type, e.getMessage(), null)); + } + }); + } catch (RuntimeException e) { + ResultUtilsKt.completeWithGetCredentialFailure( + callback, + new GetCredentialFailure( + GetCredentialFailureType.UNKNOWN, + e.getMessage(), + "Cause: " + e.getCause() + ", Stacktrace: " + Log.getStackTraceString(e))); + } } - /** Checks if there is a signed in user. */ - @NonNull @Override - public Boolean isSignedIn() { - return GoogleSignIn.getLastSignedInAccount(context) != null; + public void clearCredentialState(@NonNull Function1, Unit> callback) { + CredentialManager credentialManager = CredentialManager.create(context); + credentialManager.clearCredentialStateAsync( + new ClearCredentialStateRequest(), + null, + Executors.newSingleThreadExecutor(), + new CredentialManagerCallback<>() { + @Override + public void onResult(Void result) { + ResultUtilsKt.completeWithClearCredentialStateSuccess(callback); + } + + @Override + public void onError(@NonNull ClearCredentialException e) { + // TODO(stuartmorgan): Consider a non-exception callback. + ResultUtilsKt.completeWithClearCredentialStateError( + callback, new FlutterError("Clear Failed", e.getMessage(), null)); + } + }); } @Override - public void requestScopes( - @NonNull List scopes, @NonNull Messages.Result result) { - checkAndSetPendingBoolOperation("requestScopes", result); - - GoogleSignInAccount account = googleSignInWrapper.getLastSignedInAccount(context); - if (account == null) { - finishWithError(ERROR_REASON_SIGN_IN_REQUIRED, "No account to grant scopes."); - return; - } - - List wrappedScopes = new ArrayList<>(); - - for (String scope : scopes) { - Scope wrappedScope = new Scope(scope); - if (!googleSignInWrapper.hasPermissions(account, wrappedScope)) { - wrappedScopes.add(wrappedScope); - } - } - - if (wrappedScopes.isEmpty()) { - finishWithBoolean(true); - return; - } - - googleSignInWrapper.requestPermissions( - getActivity(), REQUEST_CODE_REQUEST_SCOPE, account, wrappedScopes.toArray(new Scope[0])); - } - - private void onSignInResult(Task completedTask) { + public void authorize( + @NonNull PlatformAuthorizationRequest params, + boolean promptIfUnauthorized, + @NonNull Function1, Unit> callback) { try { - GoogleSignInAccount account = completedTask.getResult(ApiException.class); - onSignInAccount(account); - } catch (ApiException e) { - // Forward all errors and let Dart decide how to handle. - String errorCode = errorCodeForStatus(e.getStatusCode()); - finishWithError(errorCode, e.toString()); - } catch (RuntimeExecutionException e) { - finishWithError(ERROR_REASON_EXCEPTION, e.toString()); - } - } - - private void onSignInAccount(GoogleSignInAccount account) { - final Messages.UserData.Builder builder = - new Messages.UserData.Builder() - // TODO(stuartmorgan): Test with games sign-in; according to docs these could be null - // as the games login request is currently constructed, but the public Dart API - // assumes they are non-null, so the sign-in query may need to change to - // include requestEmail() and requestProfile(). - .setEmail(account.getEmail()) - .setId(account.getId()) - .setIdToken(account.getIdToken()) - .setServerAuthCode(account.getServerAuthCode()) - .setDisplayName(account.getDisplayName()); - if (account.getPhotoUrl() != null) { - builder.setPhotoUrl(account.getPhotoUrl().toString()); - } - finishWithUserData(builder.build()); - } - - private String errorCodeForStatus(int statusCode) { - switch (statusCode) { - case GoogleSignInStatusCodes.SIGN_IN_CANCELLED: - return ERROR_REASON_SIGN_IN_CANCELED; - case CommonStatusCodes.SIGN_IN_REQUIRED: - return ERROR_REASON_SIGN_IN_REQUIRED; - case CommonStatusCodes.NETWORK_ERROR: - return ERROR_REASON_NETWORK_ERROR; - case GoogleSignInStatusCodes.SIGN_IN_CURRENTLY_IN_PROGRESS: - case GoogleSignInStatusCodes.SIGN_IN_FAILED: - case CommonStatusCodes.INVALID_ACCOUNT: - case CommonStatusCodes.INTERNAL_ERROR: - default: - return ERROR_REASON_SIGN_IN_FAILED; + List requestedScopes = new ArrayList<>(); + for (String scope : params.getScopes()) { + requestedScopes.add(new Scope(scope)); + } + AuthorizationRequest.Builder authorizationRequestBuilder = + AuthorizationRequest.builder().setRequestedScopes(requestedScopes); + if (params.getHostedDomain() != null) { + authorizationRequestBuilder.filterByHostedDomain(params.getHostedDomain()); + } + if (params.getServerClientIdForForcedRefreshToken() != null) { + authorizationRequestBuilder.requestOfflineAccess( + params.getServerClientIdForForcedRefreshToken(), true); + } + if (params.getAccountEmail() != null) { + authorizationRequestBuilder.setAccount(new Account(params.getAccountEmail(), "com.google")); + } + AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build(); + Identity.getAuthorizationClient(context) + .authorize(authorizationRequest) + .addOnSuccessListener( + authorizationResult -> { + if (authorizationResult.hasResolution()) { + if (promptIfUnauthorized) { + Activity activity = getActivity(); + if (activity == null) { + ResultUtilsKt.completeWithAuthorizeFailure( + callback, + new AuthorizeFailure(AuthorizeFailureType.NO_ACTIVITY, null, null)); + return; + } + // Prompt for access. `callback` will be resolved in onActivityResult. + // There must be a pending intent if hasResolution() was true. + PendingIntent pendingIntent = + Objects.requireNonNull(authorizationResult.getPendingIntent()); + try { + pendingAuthorizationCallback = callback; + activity.startIntentSenderForResult( + pendingIntent.getIntentSender(), + REQUEST_CODE_AUTHORIZE, + null, + 0, + 0, + 0, + null); + } catch (IntentSender.SendIntentException e) { + pendingAuthorizationCallback = null; + ResultUtilsKt.completeWithAuthorizeFailure( + callback, + new AuthorizeFailure( + AuthorizeFailureType.PENDING_INTENT_EXCEPTION, + e.getMessage(), + null)); + } + } else { + ResultUtilsKt.completeWithAuthorizeFailure( + callback, + new AuthorizeFailure(AuthorizeFailureType.UNAUTHORIZED, null, null)); + } + } else { + ResultUtilsKt.completeWithAuthorizationResult( + callback, + new PlatformAuthorizationResult( + authorizationResult.getAccessToken(), + authorizationResult.getServerAuthCode(), + authorizationResult.getGrantedScopes())); + } + }) + .addOnFailureListener( + e -> + ResultUtilsKt.completeWithAuthorizeFailure( + callback, + new AuthorizeFailure( + AuthorizeFailureType.AUTHORIZE_FAILURE, e.getMessage(), null))); + } catch (RuntimeException e) { + ResultUtilsKt.completeWithAuthorizeFailure( + callback, + new AuthorizeFailure( + AuthorizeFailureType.API_EXCEPTION, + e.getMessage(), + "Cause: " + e.getCause() + ", Stacktrace: " + Log.getStackTraceString(e))); } } - private void finishWithSuccess() { - Objects.requireNonNull(pendingOperation.voidResult).success(); - pendingOperation = null; - } - - private void finishWithBoolean(Boolean value) { - Objects.requireNonNull(pendingOperation.boolResult).success(value); - pendingOperation = null; - } - - private void finishWithUserData(Messages.UserData data) { - Objects.requireNonNull(pendingOperation.userDataResult).success(data); - pendingOperation = null; - } - - private void finishWithError(String errorCode, String errorMessage) { - if (pendingOperation.voidResult != null) { - Objects.requireNonNull(pendingOperation.voidResult) - .error(new FlutterError(errorCode, errorMessage, null)); - } else { - Messages.Result result; - if (pendingOperation.userDataResult != null) { - result = pendingOperation.userDataResult; - } else if (pendingOperation.boolResult != null) { - result = pendingOperation.boolResult; + @Override + public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == REQUEST_CODE_AUTHORIZE) { + if (pendingAuthorizationCallback != null) { + try { + AuthorizationResult authorizationResult = + Identity.getAuthorizationClient(context).getAuthorizationResultFromIntent(data); + ResultUtilsKt.completeWithAuthorizationResult( + pendingAuthorizationCallback, + new PlatformAuthorizationResult( + authorizationResult.getAccessToken(), + authorizationResult.getServerAuthCode(), + authorizationResult.getGrantedScopes())); + return true; + } catch (ApiException e) { + ResultUtilsKt.completeWithAuthorizeFailure( + pendingAuthorizationCallback, + new AuthorizeFailure(AuthorizeFailureType.API_EXCEPTION, e.getMessage(), null)); + } + pendingAuthorizationCallback = null; } else { - result = pendingOperation.stringResult; + Log.e("google_sign_in", "Unexpected authorization result callback"); } - Objects.requireNonNull(result).error(new FlutterError(errorCode, errorMessage, null)); } - pendingOperation = null; + return false; } private static boolean isNullOrEmpty(@Nullable String s) { return s == null || s.isEmpty(); } - - private static class PendingOperation { - final @NonNull String method; - final @Nullable Messages.Result userDataResult; - final @Nullable Messages.VoidResult voidResult; - final @Nullable Messages.Result boolResult; - final @Nullable Messages.Result stringResult; - final @Nullable Object data; - - PendingOperation( - @NonNull String method, - @Nullable Messages.Result userDataResult, - @Nullable Messages.VoidResult voidResult, - @Nullable Messages.Result boolResult, - @Nullable Messages.Result stringResult, - @Nullable Object data) { - assert (userDataResult != null - || voidResult != null - || boolResult != null - || stringResult != null); - this.method = method; - this.userDataResult = userDataResult; - this.voidResult = voidResult; - this.boolResult = boolResult; - this.stringResult = stringResult; - this.data = data; - } - } - - /** - * Clears the token kept in the client side cache. - * - *

Runs on a background task queue. - */ - @Override - public void clearAuthCache(@NonNull String token) { - try { - GoogleAuthUtil.clearToken(context, token); - } catch (Exception e) { - throw new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null); - } - } - - /** - * Gets an OAuth access token with the scopes that were specified during initialization for the - * user with the specified email address. - * - *

Runs on a background task queue. - * - *

If shouldRecoverAuth is set to true and user needs to recover authentication for method to - * complete, the method will attempt to recover authentication and rerun method. - */ - @Override - public void getAccessToken( - @NonNull String email, - @NonNull Boolean shouldRecoverAuth, - @NonNull Messages.Result result) { - try { - Account account = new Account(email, "com.google"); - String scopesStr = "oauth2:" + String.join(" ", requestedScopes); - String token = GoogleAuthUtil.getToken(context, account, scopesStr); - result.success(token); - } catch (UserRecoverableAuthException e) { - // This method runs in a background task queue; hop to the main thread for interactions with - // plugin state and activities. - final Handler handler = new Handler(Looper.getMainLooper()); - handler.post( - () -> { - if (shouldRecoverAuth && pendingOperation == null) { - Activity activity = getActivity(); - if (activity == null) { - result.error( - new FlutterError( - ERROR_USER_RECOVERABLE_AUTH, - "Cannot recover auth because app is not in foreground. " - + e.getLocalizedMessage(), - null)); - } else { - checkAndSetPendingAccessTokenOperation("getTokens", result, email); - Intent recoveryIntent = e.getIntent(); - activity.startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH); - } - } else { - result.error( - new FlutterError(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null)); - } - }); - } catch (Exception e) { - result.error(new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null)); - } - } - - @Override - public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (pendingOperation == null) { - return false; - } - switch (requestCode) { - case REQUEST_CODE_RECOVER_AUTH: - if (resultCode == Activity.RESULT_OK) { - // Recover the previous result and data and attempt to get tokens again. - Messages.Result result = Objects.requireNonNull(pendingOperation.stringResult); - String email = (String) Objects.requireNonNull(pendingOperation.data); - pendingOperation = null; - getAccessToken(email, false, result); - } else { - finishWithError( - ERROR_FAILURE_TO_RECOVER_AUTH, "Failed attempt to recover authentication"); - } - return true; - case REQUEST_CODE_SIGNIN: - // Whether resultCode is OK or not, the Task returned by GoogleSigIn will determine - // failure with better specifics which are extracted in onSignInResult method. - if (data != null) { - onSignInResult(GoogleSignIn.getSignedInAccountFromIntent(data)); - } else { - // data is null which is highly unusual for a sign in result. - finishWithError(ERROR_REASON_SIGN_IN_FAILED, "Signin failed"); - } - return true; - case REQUEST_CODE_REQUEST_SCOPE: - finishWithBoolean(resultCode == Activity.RESULT_OK); - return true; - default: - return false; - } - } } } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java deleted file mode 100644 index c035329f8e9..00000000000 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java +++ /dev/null @@ -1,42 +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. - -package io.flutter.plugins.googlesignin; - -import android.app.Activity; -import android.content.Context; -import com.google.android.gms.auth.api.signin.GoogleSignIn; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; -import com.google.android.gms.common.api.Scope; - -/** - * A wrapper object that calls static method in GoogleSignIn. - * - *

Because GoogleSignIn uses static method mostly, which is hard for unit testing. We use this - * wrapper class to use instance method which calls the corresponding GoogleSignIn static methods. - * - *

Warning! This class should stay true that each method calls a GoogleSignIn static method with - * the same name and same parameters. - */ -public class GoogleSignInWrapper { - - GoogleSignInClient getClient(Context context, GoogleSignInOptions options) { - return GoogleSignIn.getClient(context, options); - } - - GoogleSignInAccount getLastSignedInAccount(Context context) { - return GoogleSignIn.getLastSignedInAccount(context); - } - - boolean hasPermissions(GoogleSignInAccount account, Scope scope) { - return GoogleSignIn.hasPermissions(account, scope); - } - - void requestPermissions( - Activity activity, int requestCode, GoogleSignInAccount account, Scope[] scopes) { - GoogleSignIn.requestPermissions(activity, requestCode, account, scopes); - } -} diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java deleted file mode 100644 index 56adfecf16a..00000000000 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java +++ /dev/null @@ -1,874 +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. -// Autogenerated from Pigeon (v24.2.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -package io.flutter.plugins.googlesignin; - -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.RetentionPolicy.CLASS; - -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.plugin.common.BasicMessageChannel; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MessageCodec; -import io.flutter.plugin.common.StandardMessageCodec; -import java.io.ByteArrayOutputStream; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** Generated class from Pigeon. */ -@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) -public class Messages { - - /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ - public static class FlutterError extends RuntimeException { - - /** The error code. */ - public final String code; - - /** The error details. Must be a datatype supported by the api codec. */ - public final Object details; - - public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { - super(message); - this.code = code; - this.details = details; - } - } - - @NonNull - protected static ArrayList wrapError(@NonNull Throwable exception) { - ArrayList errorList = new ArrayList<>(3); - if (exception instanceof FlutterError) { - FlutterError error = (FlutterError) exception; - errorList.add(error.code); - errorList.add(error.getMessage()); - errorList.add(error.details); - } else { - errorList.add(exception.toString()); - errorList.add(exception.getClass().getSimpleName()); - errorList.add( - "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); - } - return errorList; - } - - @Target(METHOD) - @Retention(CLASS) - @interface CanIgnoreReturnValue {} - - /** Pigeon version of SignInOption. */ - public enum SignInType { - /** Default configuration. */ - STANDARD(0), - /** Recommended configuration for game sign in. */ - GAMES(1); - - final int index; - - SignInType(final int index) { - this.index = index; - } - } - - /** - * Pigeon version of SignInInitParams. - * - *

See SignInInitParams for details. - * - *

Generated class from Pigeon that represents data sent in messages. - */ - public static final class InitParams { - private @NonNull List scopes; - - public @NonNull List getScopes() { - return scopes; - } - - public void setScopes(@NonNull List setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"scopes\" is null."); - } - this.scopes = setterArg; - } - - private @NonNull SignInType signInType; - - public @NonNull SignInType getSignInType() { - return signInType; - } - - public void setSignInType(@NonNull SignInType setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"signInType\" is null."); - } - this.signInType = setterArg; - } - - private @Nullable String hostedDomain; - - public @Nullable String getHostedDomain() { - return hostedDomain; - } - - public void setHostedDomain(@Nullable String setterArg) { - this.hostedDomain = setterArg; - } - - private @Nullable String clientId; - - public @Nullable String getClientId() { - return clientId; - } - - public void setClientId(@Nullable String setterArg) { - this.clientId = setterArg; - } - - private @Nullable String serverClientId; - - public @Nullable String getServerClientId() { - return serverClientId; - } - - public void setServerClientId(@Nullable String setterArg) { - this.serverClientId = setterArg; - } - - private @NonNull Boolean forceCodeForRefreshToken; - - public @NonNull Boolean getForceCodeForRefreshToken() { - return forceCodeForRefreshToken; - } - - public void setForceCodeForRefreshToken(@NonNull Boolean setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"forceCodeForRefreshToken\" is null."); - } - this.forceCodeForRefreshToken = setterArg; - } - - private @Nullable String forceAccountName; - - public @Nullable String getForceAccountName() { - return forceAccountName; - } - - public void setForceAccountName(@Nullable String setterArg) { - this.forceAccountName = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - InitParams() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - InitParams that = (InitParams) o; - return scopes.equals(that.scopes) - && signInType.equals(that.signInType) - && Objects.equals(hostedDomain, that.hostedDomain) - && Objects.equals(clientId, that.clientId) - && Objects.equals(serverClientId, that.serverClientId) - && forceCodeForRefreshToken.equals(that.forceCodeForRefreshToken) - && Objects.equals(forceAccountName, that.forceAccountName); - } - - @Override - public int hashCode() { - return Objects.hash( - scopes, - signInType, - hostedDomain, - clientId, - serverClientId, - forceCodeForRefreshToken, - forceAccountName); - } - - public static final class Builder { - - private @Nullable List scopes; - - @CanIgnoreReturnValue - public @NonNull Builder setScopes(@NonNull List setterArg) { - this.scopes = setterArg; - return this; - } - - private @Nullable SignInType signInType; - - @CanIgnoreReturnValue - public @NonNull Builder setSignInType(@NonNull SignInType setterArg) { - this.signInType = setterArg; - return this; - } - - private @Nullable String hostedDomain; - - @CanIgnoreReturnValue - public @NonNull Builder setHostedDomain(@Nullable String setterArg) { - this.hostedDomain = setterArg; - return this; - } - - private @Nullable String clientId; - - @CanIgnoreReturnValue - public @NonNull Builder setClientId(@Nullable String setterArg) { - this.clientId = setterArg; - return this; - } - - private @Nullable String serverClientId; - - @CanIgnoreReturnValue - public @NonNull Builder setServerClientId(@Nullable String setterArg) { - this.serverClientId = setterArg; - return this; - } - - private @Nullable Boolean forceCodeForRefreshToken; - - @CanIgnoreReturnValue - public @NonNull Builder setForceCodeForRefreshToken(@NonNull Boolean setterArg) { - this.forceCodeForRefreshToken = setterArg; - return this; - } - - private @Nullable String forceAccountName; - - @CanIgnoreReturnValue - public @NonNull Builder setForceAccountName(@Nullable String setterArg) { - this.forceAccountName = setterArg; - return this; - } - - public @NonNull InitParams build() { - InitParams pigeonReturn = new InitParams(); - pigeonReturn.setScopes(scopes); - pigeonReturn.setSignInType(signInType); - pigeonReturn.setHostedDomain(hostedDomain); - pigeonReturn.setClientId(clientId); - pigeonReturn.setServerClientId(serverClientId); - pigeonReturn.setForceCodeForRefreshToken(forceCodeForRefreshToken); - pigeonReturn.setForceAccountName(forceAccountName); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(7); - toListResult.add(scopes); - toListResult.add(signInType); - toListResult.add(hostedDomain); - toListResult.add(clientId); - toListResult.add(serverClientId); - toListResult.add(forceCodeForRefreshToken); - toListResult.add(forceAccountName); - return toListResult; - } - - static @NonNull InitParams fromList(@NonNull ArrayList pigeonVar_list) { - InitParams pigeonResult = new InitParams(); - Object scopes = pigeonVar_list.get(0); - pigeonResult.setScopes((List) scopes); - Object signInType = pigeonVar_list.get(1); - pigeonResult.setSignInType((SignInType) signInType); - Object hostedDomain = pigeonVar_list.get(2); - pigeonResult.setHostedDomain((String) hostedDomain); - Object clientId = pigeonVar_list.get(3); - pigeonResult.setClientId((String) clientId); - Object serverClientId = pigeonVar_list.get(4); - pigeonResult.setServerClientId((String) serverClientId); - Object forceCodeForRefreshToken = pigeonVar_list.get(5); - pigeonResult.setForceCodeForRefreshToken((Boolean) forceCodeForRefreshToken); - Object forceAccountName = pigeonVar_list.get(6); - pigeonResult.setForceAccountName((String) forceAccountName); - return pigeonResult; - } - } - - /** - * Pigeon version of GoogleSignInUserData. - * - *

See GoogleSignInUserData for details. - * - *

Generated class from Pigeon that represents data sent in messages. - */ - public static final class UserData { - private @Nullable String displayName; - - public @Nullable String getDisplayName() { - return displayName; - } - - public void setDisplayName(@Nullable String setterArg) { - this.displayName = setterArg; - } - - private @NonNull String email; - - public @NonNull String getEmail() { - return email; - } - - public void setEmail(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"email\" is null."); - } - this.email = setterArg; - } - - private @NonNull String id; - - public @NonNull String getId() { - return id; - } - - public void setId(@NonNull String setterArg) { - if (setterArg == null) { - throw new IllegalStateException("Nonnull field \"id\" is null."); - } - this.id = setterArg; - } - - private @Nullable String photoUrl; - - public @Nullable String getPhotoUrl() { - return photoUrl; - } - - public void setPhotoUrl(@Nullable String setterArg) { - this.photoUrl = setterArg; - } - - private @Nullable String idToken; - - public @Nullable String getIdToken() { - return idToken; - } - - public void setIdToken(@Nullable String setterArg) { - this.idToken = setterArg; - } - - private @Nullable String serverAuthCode; - - public @Nullable String getServerAuthCode() { - return serverAuthCode; - } - - public void setServerAuthCode(@Nullable String setterArg) { - this.serverAuthCode = setterArg; - } - - /** Constructor is non-public to enforce null safety; use Builder. */ - UserData() {} - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - UserData that = (UserData) o; - return Objects.equals(displayName, that.displayName) - && email.equals(that.email) - && id.equals(that.id) - && Objects.equals(photoUrl, that.photoUrl) - && Objects.equals(idToken, that.idToken) - && Objects.equals(serverAuthCode, that.serverAuthCode); - } - - @Override - public int hashCode() { - return Objects.hash(displayName, email, id, photoUrl, idToken, serverAuthCode); - } - - public static final class Builder { - - private @Nullable String displayName; - - @CanIgnoreReturnValue - public @NonNull Builder setDisplayName(@Nullable String setterArg) { - this.displayName = setterArg; - return this; - } - - private @Nullable String email; - - @CanIgnoreReturnValue - public @NonNull Builder setEmail(@NonNull String setterArg) { - this.email = setterArg; - return this; - } - - private @Nullable String id; - - @CanIgnoreReturnValue - public @NonNull Builder setId(@NonNull String setterArg) { - this.id = setterArg; - return this; - } - - private @Nullable String photoUrl; - - @CanIgnoreReturnValue - public @NonNull Builder setPhotoUrl(@Nullable String setterArg) { - this.photoUrl = setterArg; - return this; - } - - private @Nullable String idToken; - - @CanIgnoreReturnValue - public @NonNull Builder setIdToken(@Nullable String setterArg) { - this.idToken = setterArg; - return this; - } - - private @Nullable String serverAuthCode; - - @CanIgnoreReturnValue - public @NonNull Builder setServerAuthCode(@Nullable String setterArg) { - this.serverAuthCode = setterArg; - return this; - } - - public @NonNull UserData build() { - UserData pigeonReturn = new UserData(); - pigeonReturn.setDisplayName(displayName); - pigeonReturn.setEmail(email); - pigeonReturn.setId(id); - pigeonReturn.setPhotoUrl(photoUrl); - pigeonReturn.setIdToken(idToken); - pigeonReturn.setServerAuthCode(serverAuthCode); - return pigeonReturn; - } - } - - @NonNull - ArrayList toList() { - ArrayList toListResult = new ArrayList<>(6); - toListResult.add(displayName); - toListResult.add(email); - toListResult.add(id); - toListResult.add(photoUrl); - toListResult.add(idToken); - toListResult.add(serverAuthCode); - return toListResult; - } - - static @NonNull UserData fromList(@NonNull ArrayList pigeonVar_list) { - UserData pigeonResult = new UserData(); - Object displayName = pigeonVar_list.get(0); - pigeonResult.setDisplayName((String) displayName); - Object email = pigeonVar_list.get(1); - pigeonResult.setEmail((String) email); - Object id = pigeonVar_list.get(2); - pigeonResult.setId((String) id); - Object photoUrl = pigeonVar_list.get(3); - pigeonResult.setPhotoUrl((String) photoUrl); - Object idToken = pigeonVar_list.get(4); - pigeonResult.setIdToken((String) idToken); - Object serverAuthCode = pigeonVar_list.get(5); - pigeonResult.setServerAuthCode((String) serverAuthCode); - return pigeonResult; - } - } - - private static class PigeonCodec extends StandardMessageCodec { - public static final PigeonCodec INSTANCE = new PigeonCodec(); - - private PigeonCodec() {} - - @Override - protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { - switch (type) { - case (byte) 129: - { - Object value = readValue(buffer); - return value == null ? null : SignInType.values()[((Long) value).intValue()]; - } - case (byte) 130: - return InitParams.fromList((ArrayList) readValue(buffer)); - case (byte) 131: - return UserData.fromList((ArrayList) readValue(buffer)); - default: - return super.readValueOfType(type, buffer); - } - } - - @Override - protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { - if (value instanceof SignInType) { - stream.write(129); - writeValue(stream, value == null ? null : ((SignInType) value).index); - } else if (value instanceof InitParams) { - stream.write(130); - writeValue(stream, ((InitParams) value).toList()); - } else if (value instanceof UserData) { - stream.write(131); - writeValue(stream, ((UserData) value).toList()); - } else { - super.writeValue(stream, value); - } - } - } - - /** Asynchronous error handling return type for non-nullable API method returns. */ - public interface Result { - /** Success case callback method for handling returns. */ - void success(@NonNull T result); - - /** Failure case callback method for handling errors. */ - void error(@NonNull Throwable error); - } - - /** Asynchronous error handling return type for nullable API method returns. */ - public interface NullableResult { - /** Success case callback method for handling returns. */ - void success(@Nullable T result); - - /** Failure case callback method for handling errors. */ - void error(@NonNull Throwable error); - } - - /** Asynchronous error handling return type for void API method returns. */ - public interface VoidResult { - /** Success case callback method for handling returns. */ - void success(); - - /** Failure case callback method for handling errors. */ - void error(@NonNull Throwable error); - } - - /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ - public interface GoogleSignInApi { - /** Initializes a sign in request with the given parameters. */ - void init(@NonNull InitParams params); - - /** Starts a silent sign in. */ - void signInSilently(@NonNull Result result); - - /** Starts a sign in with user interaction. */ - void signIn(@NonNull Result result); - - /** Requests the access token for the current sign in. */ - void getAccessToken( - @NonNull String email, @NonNull Boolean shouldRecoverAuth, @NonNull Result result); - - /** Signs out the current user. */ - void signOut(@NonNull VoidResult result); - - /** Revokes scope grants to the application. */ - void disconnect(@NonNull VoidResult result); - - /** Returns whether the user is currently signed in. */ - @NonNull - Boolean isSignedIn(); - - /** Clears the authentication caching for the given token, requiring a new sign in. */ - void clearAuthCache(@NonNull String token); - - /** Requests access to the given scopes. */ - void requestScopes(@NonNull List scopes, @NonNull Result result); - - /** The codec used by GoogleSignInApi. */ - static @NonNull MessageCodec getCodec() { - return PigeonCodec.INSTANCE; - } - - /** - * Sets up an instance of `GoogleSignInApi` to handle messages through the `binaryMessenger`. - */ - static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable GoogleSignInApi api) { - setUp(binaryMessenger, "", api); - } - - static void setUp( - @NonNull BinaryMessenger binaryMessenger, - @NonNull String messageChannelSuffix, - @Nullable GoogleSignInApi api) { - messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix; - BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue(); - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.init" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - InitParams paramsArg = (InitParams) args.get(0); - try { - api.init(paramsArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.signInSilently" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - Result resultCallback = - new Result() { - public void success(UserData result) { - wrapped.add(0, result); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.signInSilently(resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.signIn" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - Result resultCallback = - new Result() { - public void success(UserData result) { - wrapped.add(0, result); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.signIn(resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.getAccessToken" - + messageChannelSuffix, - getCodec(), - taskQueue); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - String emailArg = (String) args.get(0); - Boolean shouldRecoverAuthArg = (Boolean) args.get(1); - Result resultCallback = - new Result() { - public void success(String result) { - wrapped.add(0, result); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.getAccessToken(emailArg, shouldRecoverAuthArg, resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.signOut" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - VoidResult resultCallback = - new VoidResult() { - public void success() { - wrapped.add(0, null); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.signOut(resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.disconnect" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - VoidResult resultCallback = - new VoidResult() { - public void success() { - wrapped.add(0, null); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.disconnect(resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.isSignedIn" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - try { - Boolean output = api.isSignedIn(); - wrapped.add(0, output); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.clearAuthCache" - + messageChannelSuffix, - getCodec(), - taskQueue); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - String tokenArg = (String) args.get(0); - try { - api.clearAuthCache(tokenArg); - wrapped.add(0, null); - } catch (Throwable exception) { - wrapped = wrapError(exception); - } - reply.reply(wrapped); - }); - } else { - channel.setMessageHandler(null); - } - } - { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.requestScopes" - + messageChannelSuffix, - getCodec()); - if (api != null) { - channel.setMessageHandler( - (message, reply) -> { - ArrayList wrapped = new ArrayList<>(); - ArrayList args = (ArrayList) message; - List scopesArg = (List) args.get(0); - Result resultCallback = - new Result() { - public void success(Boolean result) { - wrapped.add(0, result); - reply.reply(wrapped); - } - - public void error(Throwable error) { - ArrayList wrappedError = wrapError(error); - reply.reply(wrappedError); - } - }; - - api.requestScopes(scopesArg, resultCallback); - }); - } else { - channel.setMessageHandler(null); - } - } - } - } -} diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt new file mode 100644 index 00000000000..009541c6842 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt @@ -0,0 +1,540 @@ +// 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. +// Autogenerated from Pigeon (v24.2.1), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package io.flutter.plugins.googlesignin + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +import io.flutter.plugin.common.StandardMessageCodec +import java.io.ByteArrayOutputStream +import java.nio.ByteBuffer + +private fun wrapResult(result: Any?): List { + return listOf(result) +} + +private fun wrapError(exception: Throwable): List { + return if (exception is FlutterError) { + listOf(exception.code, exception.message, exception.details) + } else { + listOf( + exception.javaClass.simpleName, + exception.toString(), + "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)) + } +} + +/** + * Error class for passing custom error details to Flutter via a thrown PlatformException. + * + * @property code The error code. + * @property message The error message. + * @property details The error details. Must be a datatype supported by the api codec. + */ +class FlutterError( + val code: String, + override val message: String? = null, + val details: Any? = null +) : Throwable() + +enum class GetCredentialFailureType(val raw: Int) { + /** Indicates that a credential was returned, but it was not of the expected type. */ + UNEXPECTED_CREDENTIAL_TYPE(0), + /** Indicates that a server client ID was not provided. */ + MISSING_SERVER_CLIENT_ID(1), + /** The request was internally interrupted. */ + INTERRUPTED(2), + /** The request was canceled by the user. */ + CANCELED(3), + /** No matching credential was found. */ + NO_CREDENTIAL(4), + /** The provider was not properly configured. */ + PROVIDER_CONFIGURATION_ISSUE(5), + /** The credential manager is not supported on this device. */ + UNSUPPORTED(6), + /** The request failed for an unknown reason. */ + UNKNOWN(7); + + companion object { + fun ofRaw(raw: Int): GetCredentialFailureType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +enum class AuthorizeFailureType(val raw: Int) { + /** + * Indicates that the requested types are not currently authorized. + * + * This is returned only if promptIfUnauthorized is false, indicating that the user would need to + * be prompted for authorization. + */ + UNAUTHORIZED(0), + /** Indicates that the call to AuthorizationClient.authorize itself failed. */ + AUTHORIZE_FAILURE(1), + /** + * Corresponds to SendIntentException, indicating that the pending intent is no longer available. + */ + PENDING_INTENT_EXCEPTION(2), + /** + * Corresponds to an SendIntentException in onActivityResult, indicating that either authorization + * failed, or the result was not available for some reason. + */ + API_EXCEPTION(3), + /** + * Indicates that the user needs to be prompted for authorization, but there is no current + * activity to prompt in. + */ + NO_ACTIVITY(4); + + companion object { + fun ofRaw(raw: Int): AuthorizeFailureType? { + return values().firstOrNull { it.raw == raw } + } + } +} + +/** + * The information necessary to build a an authorization request. + * + * Corresponds to the native AuthorizationRequest object, but only contains the fields used by this + * plugin. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformAuthorizationRequest( + val scopes: List, + val hostedDomain: String? = null, + val accountEmail: String? = null, + /** If set, adds a call to requestOfflineAccess(this string, true); */ + val serverClientIdForForcedRefreshToken: String? = null +) { + companion object { + fun fromList(pigeonVar_list: List): PlatformAuthorizationRequest { + val scopes = pigeonVar_list[0] as List + val hostedDomain = pigeonVar_list[1] as String? + val accountEmail = pigeonVar_list[2] as String? + val serverClientIdForForcedRefreshToken = pigeonVar_list[3] as String? + return PlatformAuthorizationRequest( + scopes, hostedDomain, accountEmail, serverClientIdForForcedRefreshToken) + } + } + + fun toList(): List { + return listOf( + scopes, + hostedDomain, + accountEmail, + serverClientIdForForcedRefreshToken, + ) + } +} + +/** + * The information necessary to build a credential request. + * + * Combines the parts of the native GetCredentialRequest and CredentialOption classes that are used + * for this plugin. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class GetCredentialRequestParams( + val filterToAuthorized: Boolean, + val autoSelectEnabled: Boolean, + val serverClientId: String? = null +) { + companion object { + fun fromList(pigeonVar_list: List): GetCredentialRequestParams { + val filterToAuthorized = pigeonVar_list[0] as Boolean + val autoSelectEnabled = pigeonVar_list[1] as Boolean + val serverClientId = pigeonVar_list[2] as String? + return GetCredentialRequestParams(filterToAuthorized, autoSelectEnabled, serverClientId) + } + } + + fun toList(): List { + return listOf( + filterToAuthorized, + autoSelectEnabled, + serverClientId, + ) + } +} + +/** + * Pigeon equivalent of the native GoogleIdTokenCredential. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformGoogleIdTokenCredential( + val displayName: String? = null, + val familyName: String? = null, + val givenName: String? = null, + val id: String, + val idToken: String, + val profilePictureUri: String? = null +) { + companion object { + fun fromList(pigeonVar_list: List): PlatformGoogleIdTokenCredential { + val displayName = pigeonVar_list[0] as String? + val familyName = pigeonVar_list[1] as String? + val givenName = pigeonVar_list[2] as String? + val id = pigeonVar_list[3] as String + val idToken = pigeonVar_list[4] as String + val profilePictureUri = pigeonVar_list[5] as String? + return PlatformGoogleIdTokenCredential( + displayName, familyName, givenName, id, idToken, profilePictureUri) + } + } + + fun toList(): List { + return listOf( + displayName, + familyName, + givenName, + id, + idToken, + profilePictureUri, + ) + } +} + +/** + * The response from a `getCredential` call. + * + * This is not the same as a native GetCredentialResponse since modeling the response type hierarchy + * and two-part callback in this interface layer would add a lot of complexity that is not needed + * for the plugin's use case. It is instead a processed version of the results of those callbacks. + * + * Generated class from Pigeon that represents data sent in messages. This class should not be + * extended by any user class outside of the generated file. + */ +sealed class GetCredentialResult +/** + * An authentication failure. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class GetCredentialFailure( + /** The type of failure. */ + val type: GetCredentialFailureType, + /** The message associated with the failure, if any. */ + val message: String? = null, + /** Extra details about the failure, if any. */ + val details: String? = null +) : GetCredentialResult() { + companion object { + fun fromList(pigeonVar_list: List): GetCredentialFailure { + val type = pigeonVar_list[0] as GetCredentialFailureType + val message = pigeonVar_list[1] as String? + val details = pigeonVar_list[2] as String? + return GetCredentialFailure(type, message, details) + } + } + + fun toList(): List { + return listOf( + type, + message, + details, + ) + } +} + +/** + * A successful authentication result. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class GetCredentialSuccess(val credential: PlatformGoogleIdTokenCredential) : + GetCredentialResult() { + companion object { + fun fromList(pigeonVar_list: List): GetCredentialSuccess { + val credential = pigeonVar_list[0] as PlatformGoogleIdTokenCredential + return GetCredentialSuccess(credential) + } + } + + fun toList(): List { + return listOf( + credential, + ) + } +} + +/** + * The response from an `authorize` call. + * + * Generated class from Pigeon that represents data sent in messages. This class should not be + * extended by any user class outside of the generated file. + */ +sealed class AuthorizeResult +/** + * An authorization failure + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class AuthorizeFailure( + /** The type of failure. */ + val type: AuthorizeFailureType, + /** The message associated with the failure, if any. */ + val message: String? = null, + /** Extra details about the failure, if any. */ + val details: String? = null +) : AuthorizeResult() { + companion object { + fun fromList(pigeonVar_list: List): AuthorizeFailure { + val type = pigeonVar_list[0] as AuthorizeFailureType + val message = pigeonVar_list[1] as String? + val details = pigeonVar_list[2] as String? + return AuthorizeFailure(type, message, details) + } + } + + fun toList(): List { + return listOf( + type, + message, + details, + ) + } +} + +/** + * A successful authorization result. + * + * Corresponds to a native AuthorizationResult. + * + * Generated class from Pigeon that represents data sent in messages. + */ +data class PlatformAuthorizationResult( + val accessToken: String? = null, + val serverAuthCode: String? = null, + val grantedScopes: List +) : AuthorizeResult() { + companion object { + fun fromList(pigeonVar_list: List): PlatformAuthorizationResult { + val accessToken = pigeonVar_list[0] as String? + val serverAuthCode = pigeonVar_list[1] as String? + val grantedScopes = pigeonVar_list[2] as List + return PlatformAuthorizationResult(accessToken, serverAuthCode, grantedScopes) + } + } + + fun toList(): List { + return listOf( + accessToken, + serverAuthCode, + grantedScopes, + ) + } +} + +private open class MessagesPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { GetCredentialFailureType.ofRaw(it.toInt()) } + } + 130.toByte() -> { + return (readValue(buffer) as Long?)?.let { AuthorizeFailureType.ofRaw(it.toInt()) } + } + 131.toByte() -> { + return (readValue(buffer) as? List)?.let { PlatformAuthorizationRequest.fromList(it) } + } + 132.toByte() -> { + return (readValue(buffer) as? List)?.let { GetCredentialRequestParams.fromList(it) } + } + 133.toByte() -> { + return (readValue(buffer) as? List)?.let { + PlatformGoogleIdTokenCredential.fromList(it) + } + } + 134.toByte() -> { + return (readValue(buffer) as? List)?.let { GetCredentialFailure.fromList(it) } + } + 135.toByte() -> { + return (readValue(buffer) as? List)?.let { GetCredentialSuccess.fromList(it) } + } + 136.toByte() -> { + return (readValue(buffer) as? List)?.let { AuthorizeFailure.fromList(it) } + } + 137.toByte() -> { + return (readValue(buffer) as? List)?.let { PlatformAuthorizationResult.fromList(it) } + } + else -> super.readValueOfType(type, buffer) + } + } + + override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { + when (value) { + is GetCredentialFailureType -> { + stream.write(129) + writeValue(stream, value.raw) + } + is AuthorizeFailureType -> { + stream.write(130) + writeValue(stream, value.raw) + } + is PlatformAuthorizationRequest -> { + stream.write(131) + writeValue(stream, value.toList()) + } + is GetCredentialRequestParams -> { + stream.write(132) + writeValue(stream, value.toList()) + } + is PlatformGoogleIdTokenCredential -> { + stream.write(133) + writeValue(stream, value.toList()) + } + is GetCredentialFailure -> { + stream.write(134) + writeValue(stream, value.toList()) + } + is GetCredentialSuccess -> { + stream.write(135) + writeValue(stream, value.toList()) + } + is AuthorizeFailure -> { + stream.write(136) + writeValue(stream, value.toList()) + } + is PlatformAuthorizationResult -> { + stream.write(137) + writeValue(stream, value.toList()) + } + else -> super.writeValue(stream, value) + } + } +} + +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface CredentialManagerApi { + fun getCredential( + params: GetCredentialRequestParams, + callback: (Result) -> Unit + ) + + fun clearCredentialState(callback: (Result) -> Unit) + + companion object { + /** The codec used by CredentialManagerApi. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** + * Sets up an instance of `CredentialManagerApi` to handle messages through the + * `binaryMessenger`. + */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: CredentialManagerApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getCredential$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val paramsArg = args[0] as GetCredentialRequestParams + api.getCredential(paramsArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.clearCredentialState$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.clearCredentialState { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface AuthorizationClientApi { + fun authorize( + params: PlatformAuthorizationRequest, + promptIfUnauthorized: Boolean, + callback: (Result) -> Unit + ) + + companion object { + /** The codec used by AuthorizationClientApi. */ + val codec: MessageCodec by lazy { MessagesPigeonCodec() } + /** + * Sets up an instance of `AuthorizationClientApi` to handle messages through the + * `binaryMessenger`. + */ + @JvmOverloads + fun setUp( + binaryMessenger: BinaryMessenger, + api: AuthorizationClientApi?, + messageChannelSuffix: String = "" + ) { + val separatedMessageChannelSuffix = + if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.google_sign_in_android.AuthorizationClientApi.authorize$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val paramsArg = args[0] as PlatformAuthorizationRequest + val promptIfUnauthorizedArg = args[1] as Boolean + api.authorize(paramsArg, promptIfUnauthorizedArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt new file mode 100644 index 00000000000..5e7b8f50098 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt @@ -0,0 +1,37 @@ +package io.flutter.plugins.googlesignin + +fun completeWithGetGetCredentialResult( + callback: (Result) -> Unit, + result: GetCredentialResult +) { + callback(Result.success(result)) +} + +fun completeWithGetCredentialFailure( + callback: (Result) -> Unit, + failure: GetCredentialFailure +) { + callback(Result.success(failure)) +} + +fun completeWithClearCredentialStateSuccess(callback: (Result) -> Unit) { + callback(Result.success(Unit)) +} + +fun completeWithClearCredentialStateError(callback: (Result) -> Unit, failure: FlutterError) { + callback(Result.failure(failure)) +} + +fun completeWithAuthorizationResult( + callback: (Result) -> Unit, + result: PlatformAuthorizationResult +) { + callback(Result.success(result)) +} + +fun completeWithAuthorizeFailure( + callback: (Result) -> Unit, + failure: AuthorizeFailure +) { + callback(Result.success(failure)) +} diff --git a/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle index 3c1e0dee00b..d4ecd0ece47 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/example/android/app/build.gradle @@ -2,6 +2,7 @@ plugins { id "com.android.application" id "org.jetbrains.kotlin.android" id "dev.flutter.flutter-gradle-plugin" + id 'com.google.gms.google-services' } def localProperties = new Properties() diff --git a/packages/google_sign_in/google_sign_in_android/example/android/build.gradle b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle index b812ba73567..92a39343e0d 100644 --- a/packages/google_sign_in/google_sign_in_android/example/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/example/android/build.gradle @@ -1,3 +1,7 @@ +plugins { + id 'com.google.gms.google-services' version '4.4.2' apply false +} + allprojects { repositories { // See https://github.com/flutter/flutter/blob/master/docs/ecosystem/Plugins-and-Packages-repository-structure.md#gradle-structure for more info. diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart index 9403f62f619..d03019da185 100644 --- a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -47,6 +47,8 @@ class SignInDemoState extends State { 'email', 'https://www.googleapis.com/auth/contacts.readonly', ], + // The example app uses the parsing of values from google-services.json + // to provide the serverClientId, otherwise it would be required here. )) ..catchError((dynamic _) { _initialization = null; diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index a064fba20de..5798d77a681 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -5,18 +5,41 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:flutter/services.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'src/messages.g.dart'; +// These are magic string values that match the previous implementation on +// Android, docs in the app-facing package, and/or implementations on other +// platforms. +// TODO(stuartmorgan): Replace these with structured errors defined in the +// platform interface when reworking the API surface. +const String _errorCodeSignInCanceled = 'sign_in_canceled'; +const String _errorCodeSignInRequired = 'sign_in_required'; +const String _errorCodeSignInFailed = 'sign_in_failed'; +const String _errorCodeUserRecoverableAuth = 'user_recoverable_auth'; +const String _errorCodeIncorrectConfiguration = 'incorrect_configuration'; + /// Android implementation of [GoogleSignInPlatform]. class GoogleSignInAndroid extends GoogleSignInPlatform { /// Creates a new plugin implementation instance. GoogleSignInAndroid({ - @visibleForTesting GoogleSignInApi? api, - }) : _api = api ?? GoogleSignInApi(); + @visibleForTesting CredentialManagerApi? credentialManaagerApi, + @visibleForTesting AuthorizationClientApi? authorizationClientApi, + }) : _credentialManaagerApi = + credentialManaagerApi ?? CredentialManagerApi(), + _authorizationClientApi = + authorizationClientApi ?? AuthorizationClientApi(); + + final CredentialManagerApi _credentialManaagerApi; + final AuthorizationClientApi _authorizationClientApi; - final GoogleSignInApi _api; + String? _serverClientId; + String? _hostedDomain; + List _desiredScopes = []; + bool _forceCodeForRefreshToken = false; + String? _forcedAccountName; /// Registers this class as the default instance of [GoogleSignInPlatform]. static void registerWith() { @@ -41,84 +64,254 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { } @override - Future initWithParams(SignInInitParameters params) { - return _api.init(InitParams( - signInType: _signInTypeForOption(params.signInOption), - scopes: params.scopes, - hostedDomain: params.hostedDomain, - clientId: params.clientId, - serverClientId: params.serverClientId, - forceCodeForRefreshToken: params.forceCodeForRefreshToken, - forceAccountName: params.forceAccountName, - )); + Future initWithParams(SignInInitParameters params) async { + _desiredScopes = params.scopes; + _serverClientId = params.serverClientId; + // The clientId parameter is not supported on Android. + // Android apps are identified by their package name and the SHA-1 of their signing key. + _hostedDomain = params.hostedDomain; + _forceCodeForRefreshToken = params.forceCodeForRefreshToken; + _forcedAccountName = params.forceAccountName; + // TODO(stuartmorgan): Consider adding a prepareGetCredentials call here. } @override - Future signInSilently() { - return _api.signInSilently().then(_signInUserDataFromChannelData); + Future signInSilently() async { + // Attempt to authorize without user interaction. + final PlatformGoogleIdTokenCredential? credential = await _authenticate( + filterToAuthorized: true, + autoSelectEnabled: true, + ); + if (credential == null) { + return null; + } + + // For behavioral compatibility with the current plugin API, also attempt + // to authorize scopes silently. + // TODO(stuartmorgan): Restructure the plugin API to eliminate the need for + // this; see https://github.com/flutter/flutter/issues/119300. + final PlatformAuthorizationResult? authorization = await _authorize( + promptIfUnauthorized: false, + scopes: _desiredScopes, + accountEmail: _forcedAccountName); + if (authorization == null) { + return null; + } + + return GoogleSignInUserData( + email: credential.id, + id: credential.id, + idToken: credential.idToken, + serverAuthCode: authorization.serverAuthCode, + displayName: credential.displayName, + photoUrl: credential.profilePictureUri); } @override - Future signIn() { - return _api.signIn().then(_signInUserDataFromChannelData); + Future signIn() async { + // Attempt to authorize without user interaction. + PlatformGoogleIdTokenCredential? credential = await _authenticate( + filterToAuthorized: true, + autoSelectEnabled: true, + ); + // If no auto-sign-in is available, prompt for an account. + credential ??= credential = await _authenticate( + filterToAuthorized: false, + autoSelectEnabled: false, + throwGoogleSignInCompatExceptions: true, + ); + if (credential == null) { + return null; + } + + // For behavioral compatibility with the current plugin API, also attempt + // to authorize scopes. + // TODO(stuartmorgan): Restructure the plugin API to eliminate the need for + // this; see https://github.com/flutter/flutter/issues/119300. + final PlatformAuthorizationResult? authorization = await _authorize( + promptIfUnauthorized: true, + scopes: _desiredScopes, + accountEmail: _forcedAccountName); + if (authorization == null) { + return null; + } + + return GoogleSignInUserData( + email: credential.id, + id: credential.id, + idToken: credential.idToken, + serverAuthCode: authorization.serverAuthCode, + displayName: credential.displayName, + photoUrl: credential.profilePictureUri); + } + + Future _authenticate( + {required bool filterToAuthorized, + required bool autoSelectEnabled, + bool throwGoogleSignInCompatExceptions = false}) async { + final GetCredentialResult authnResult = + await _credentialManaagerApi.getCredential(GetCredentialRequestParams( + filterToAuthorized: filterToAuthorized, + autoSelectEnabled: autoSelectEnabled, + serverClientId: _serverClientId)); + switch (authnResult) { + case GetCredentialFailure(): + if (throwGoogleSignInCompatExceptions) { + switch (authnResult.type) { + // Most failures don't map directly to an existing failure, so use + // the previous Google Sign-In's catch-all for most cases. + case GetCredentialFailureType.unexpectedCredentialType: + case GetCredentialFailureType.interrupted: + case GetCredentialFailureType.noCredential: + case GetCredentialFailureType.providerConfigurationIssue: + case GetCredentialFailureType.unsupported: + case GetCredentialFailureType.unknown: + throw PlatformException( + code: _errorCodeSignInFailed, message: authnResult.message); + case GetCredentialFailureType.canceled: + throw PlatformException( + code: _errorCodeSignInCanceled, message: authnResult.message); + case GetCredentialFailureType.missingServerClientId: + throw PlatformException( + code: _errorCodeIncorrectConfiguration, + message: 'serverClientId must be provided on Android'); + } + } + return null; + case GetCredentialSuccess(): + return authnResult.credential; + } + } + + Future _authorize( + {required bool promptIfUnauthorized, + required List scopes, + String? accountEmail, + bool throwGoogleSignInCompatExceptions = false}) async { + final AuthorizeResult authzResult = await _authorizationClientApi.authorize( + PlatformAuthorizationRequest( + scopes: scopes, + hostedDomain: _hostedDomain, + serverClientIdForForcedRefreshToken: + _forceCodeForRefreshToken ? _serverClientId : null, + accountEmail: accountEmail, + ), + promptIfUnauthorized: promptIfUnauthorized); + switch (authzResult) { + case AuthorizeFailure(): + if (throwGoogleSignInCompatExceptions) { + switch (authzResult.type) { + case AuthorizeFailureType.unauthorized: + // This is the closest error code in the legacy system, since it + // would be resolved by calling signIn if the user allowed access. + throw PlatformException( + code: _errorCodeSignInRequired, message: authzResult.message); + case AuthorizeFailureType.noActivity: + throw PlatformException( + code: _errorCodeUserRecoverableAuth, + message: authzResult.message); + // Map everything else to the catch-all error for now. + case AuthorizeFailureType.authorizeFailure: + case AuthorizeFailureType.pendingIntentException: + case AuthorizeFailureType.apiException: + throw PlatformException( + code: _errorCodeSignInFailed, message: authzResult.message); + } + } + return null; + case PlatformAuthorizationResult(): + return authzResult; + } } @override Future getTokens( - {required String email, bool? shouldRecoverAuth = true}) { - return _api - .getAccessToken(email, shouldRecoverAuth ?? true) - .then((String result) => GoogleSignInTokenData( - accessToken: result, - )); + {required String email, bool? shouldRecoverAuth = true}) async { + final bool promptIfUnauthorized = shouldRecoverAuth ?? false; + // TODO(stuartmorgan): Eliminate or restructure this method in the new API, + // since it mixes tokens from different steps. + // See https://github.com/flutter/flutter/issues/119300. + final PlatformAuthorizationResult? authorization = await _authorize( + promptIfUnauthorized: promptIfUnauthorized, + scopes: _desiredScopes, + accountEmail: email, + throwGoogleSignInCompatExceptions: true); + if (authorization == null) { + // This is explicitly documented behavior in the app-facing package, + // unfortunately, so replicate it here. + throw PlatformException( + code: promptIfUnauthorized + ? 'failed_to_recover_auth' + : 'user_recoverable_auth'); + } + + return GoogleSignInTokenData( + // idToken isn't available here; the app-facing code already caches it + // for that reason, so for now just rely on that. After an API rework, + // that shouldn't be necessary. + accessToken: authorization.accessToken, + serverAuthCode: authorization.serverAuthCode, + ); } @override Future signOut() { - return _api.signOut(); + return _credentialManaagerApi.clearCredentialState(); } @override - Future disconnect() { - return _api.disconnect(); + Future disconnect() async { + // This was a Google Sign-In API that does not appear to have a Credential + // Manager equivalent; just sign out instead. + return signOut(); } @override - Future isSignedIn() { - return _api.isSignedIn(); + Future isSignedIn() async { + // TODO(stuartmorgan): Eliminate or restructure this method in the new API, + // since this concept doesn't seem to exist any more. + // See https://github.com/flutter/flutter/issues/119300. + // For now, attempt a silent sign-in and see if it works. + return (await _authenticate( + filterToAuthorized: true, autoSelectEnabled: true)) != + null; } @override - Future clearAuthCache({required String token}) { - return _api.clearAuthCache(token); + Future clearAuthCache({required String token}) async { + // This was a Google Sign-In API that does not appear to have a Credential + // Manager equivalent. } @override - Future requestScopes(List scopes) { - return _api.requestScopes(scopes); - } - - SignInType _signInTypeForOption(SignInOption option) { - switch (option) { - case SignInOption.standard: - return SignInType.standard; - case SignInOption.games: - return SignInType.games; + Future requestScopes(List scopes) async { + final AuthorizeResult result = await _authorizationClientApi.authorize( + PlatformAuthorizationRequest( + scopes: scopes, hostedDomain: _hostedDomain), + promptIfUnauthorized: true); + switch (result) { + case AuthorizeFailure(): + // TODO(stuartmorgan): Look into how failure should be communicated better. + return false; + case PlatformAuthorizationResult(): + return true; } - // Handle the case where a new type is added to the platform interface in - // the future, and this version of the package is used with it. - // ignore: dead_code - throw UnimplementedError('Unsupported sign in option: $option'); } - GoogleSignInUserData _signInUserDataFromChannelData(UserData data) { - return GoogleSignInUserData( - email: data.email, - id: data.id, - displayName: data.displayName, - photoUrl: data.photoUrl, - idToken: data.idToken, - serverAuthCode: data.serverAuthCode, - ); + @override + Future canAccessScopes( + List scopes, { + String? accessToken, + }) async { + final AuthorizeResult result = await _authorizationClientApi.authorize( + PlatformAuthorizationRequest( + scopes: scopes, hostedDomain: _hostedDomain), + promptIfUnauthorized: false); + switch (result) { + case AuthorizeFailure(): + return false; + case PlatformAuthorizationResult(): + return true; + } } } diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart index 610741deb7b..71602bb5305 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v24.2.0), do not edit directly. +// Autogenerated from Pigeon (v24.2.1), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -18,114 +18,313 @@ PlatformException _createConnectionError(String channelName) { ); } -/// Pigeon version of SignInOption. -enum SignInType { - /// Default configuration. - standard, +enum GetCredentialFailureType { + /// Indicates that a credential was returned, but it was not of the expected + /// type. + unexpectedCredentialType, - /// Recommended configuration for game sign in. - games, + /// Indicates that a server client ID was not provided. + missingServerClientId, + + /// The request was internally interrupted. + interrupted, + + /// The request was canceled by the user. + canceled, + + /// No matching credential was found. + noCredential, + + /// The provider was not properly configured. + providerConfigurationIssue, + + /// The credential manager is not supported on this device. + unsupported, + + /// The request failed for an unknown reason. + unknown, } -/// Pigeon version of SignInInitParams. +enum AuthorizeFailureType { + /// Indicates that the requested types are not currently authorized. + /// + /// This is returned only if promptIfUnauthorized is false, indicating that + /// the user would need to be prompted for authorization. + unauthorized, + + /// Indicates that the call to AuthorizationClient.authorize itself failed. + authorizeFailure, + + /// Corresponds to SendIntentException, indicating that the pending intent is + /// no longer available. + pendingIntentException, + + /// Corresponds to an SendIntentException in onActivityResult, indicating that + /// either authorization failed, or the result was not available for some + /// reason. + apiException, + + /// Indicates that the user needs to be prompted for authorization, but there + /// is no current activity to prompt in. + noActivity, +} + +/// The information necessary to build a an authorization request. /// -/// See SignInInitParams for details. -class InitParams { - InitParams({ - this.scopes = const [], - this.signInType = SignInType.standard, +/// Corresponds to the native AuthorizationRequest object, but only contains +/// the fields used by this plugin. +class PlatformAuthorizationRequest { + PlatformAuthorizationRequest({ + required this.scopes, this.hostedDomain, - this.clientId, - this.serverClientId, - this.forceCodeForRefreshToken = false, - this.forceAccountName, + this.accountEmail, + this.serverClientIdForForcedRefreshToken, }); List scopes; - SignInType signInType; - String? hostedDomain; - String? clientId; - - String? serverClientId; - - bool forceCodeForRefreshToken; + String? accountEmail; - String? forceAccountName; + /// If set, adds a call to requestOfflineAccess(this string, true); + String? serverClientIdForForcedRefreshToken; Object encode() { return [ scopes, - signInType, hostedDomain, - clientId, - serverClientId, - forceCodeForRefreshToken, - forceAccountName, + accountEmail, + serverClientIdForForcedRefreshToken, ]; } - static InitParams decode(Object result) { + static PlatformAuthorizationRequest decode(Object result) { result as List; - return InitParams( + return PlatformAuthorizationRequest( scopes: (result[0] as List?)!.cast(), - signInType: result[1]! as SignInType, - hostedDomain: result[2] as String?, - clientId: result[3] as String?, - serverClientId: result[4] as String?, - forceCodeForRefreshToken: result[5]! as bool, - forceAccountName: result[6] as String?, + hostedDomain: result[1] as String?, + accountEmail: result[2] as String?, + serverClientIdForForcedRefreshToken: result[3] as String?, ); } } -/// Pigeon version of GoogleSignInUserData. +/// The information necessary to build a credential request. /// -/// See GoogleSignInUserData for details. -class UserData { - UserData({ +/// Combines the parts of the native GetCredentialRequest and CredentialOption +/// classes that are used for this plugin. +class GetCredentialRequestParams { + GetCredentialRequestParams({ + required this.filterToAuthorized, + required this.autoSelectEnabled, + this.serverClientId, + }); + + bool filterToAuthorized; + + bool autoSelectEnabled; + + String? serverClientId; + + Object encode() { + return [ + filterToAuthorized, + autoSelectEnabled, + serverClientId, + ]; + } + + static GetCredentialRequestParams decode(Object result) { + result as List; + return GetCredentialRequestParams( + filterToAuthorized: result[0]! as bool, + autoSelectEnabled: result[1]! as bool, + serverClientId: result[2] as String?, + ); + } +} + +/// Pigeon equivalent of the native GoogleIdTokenCredential. +class PlatformGoogleIdTokenCredential { + PlatformGoogleIdTokenCredential({ this.displayName, - required this.email, + this.familyName, + this.givenName, required this.id, - this.photoUrl, - this.idToken, - this.serverAuthCode, + required this.idToken, + this.profilePictureUri, }); String? displayName; - String email; + String? familyName; - String id; + String? givenName; - String? photoUrl; + String id; - String? idToken; + String idToken; - String? serverAuthCode; + String? profilePictureUri; Object encode() { return [ displayName, - email, + familyName, + givenName, id, - photoUrl, idToken, - serverAuthCode, + profilePictureUri, ]; } - static UserData decode(Object result) { + static PlatformGoogleIdTokenCredential decode(Object result) { result as List; - return UserData( + return PlatformGoogleIdTokenCredential( displayName: result[0] as String?, - email: result[1]! as String, - id: result[2]! as String, - photoUrl: result[3] as String?, - idToken: result[4] as String?, - serverAuthCode: result[5] as String?, + familyName: result[1] as String?, + givenName: result[2] as String?, + id: result[3]! as String, + idToken: result[4]! as String, + profilePictureUri: result[5] as String?, + ); + } +} + +/// The response from a `getCredential` call. +/// +/// This is not the same as a native GetCredentialResponse since modeling the +/// response type hierarchy and two-part callback in this interface layer would +/// add a lot of complexity that is not needed for the plugin's use case. It is +/// instead a processed version of the results of those callbacks. +sealed class GetCredentialResult {} + +/// An authentication failure. +class GetCredentialFailure extends GetCredentialResult { + GetCredentialFailure({ + required this.type, + this.message, + this.details, + }); + + /// The type of failure. + GetCredentialFailureType type; + + /// The message associated with the failure, if any. + String? message; + + /// Extra details about the failure, if any. + String? details; + + Object encode() { + return [ + type, + message, + details, + ]; + } + + static GetCredentialFailure decode(Object result) { + result as List; + return GetCredentialFailure( + type: result[0]! as GetCredentialFailureType, + message: result[1] as String?, + details: result[2] as String?, + ); + } +} + +/// A successful authentication result. +class GetCredentialSuccess extends GetCredentialResult { + GetCredentialSuccess({ + required this.credential, + }); + + PlatformGoogleIdTokenCredential credential; + + Object encode() { + return [ + credential, + ]; + } + + static GetCredentialSuccess decode(Object result) { + result as List; + return GetCredentialSuccess( + credential: result[0]! as PlatformGoogleIdTokenCredential, + ); + } +} + +/// The response from an `authorize` call. +sealed class AuthorizeResult {} + +/// An authorization failure +class AuthorizeFailure extends AuthorizeResult { + AuthorizeFailure({ + required this.type, + this.message, + this.details, + }); + + /// The type of failure. + AuthorizeFailureType type; + + /// The message associated with the failure, if any. + String? message; + + /// Extra details about the failure, if any. + String? details; + + Object encode() { + return [ + type, + message, + details, + ]; + } + + static AuthorizeFailure decode(Object result) { + result as List; + return AuthorizeFailure( + type: result[0]! as AuthorizeFailureType, + message: result[1] as String?, + details: result[2] as String?, + ); + } +} + +/// A successful authorization result. +/// +/// Corresponds to a native AuthorizationResult. +class PlatformAuthorizationResult extends AuthorizeResult { + PlatformAuthorizationResult({ + this.accessToken, + this.serverAuthCode, + required this.grantedScopes, + }); + + String? accessToken; + + String? serverAuthCode; + + List grantedScopes; + + Object encode() { + return [ + accessToken, + serverAuthCode, + grantedScopes, + ]; + } + + static PlatformAuthorizationResult decode(Object result) { + result as List; + return PlatformAuthorizationResult( + accessToken: result[0] as String?, + serverAuthCode: result[1] as String?, + grantedScopes: (result[2] as List?)!.cast(), ); } } @@ -137,15 +336,33 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is SignInType) { + } else if (value is GetCredentialFailureType) { buffer.putUint8(129); writeValue(buffer, value.index); - } else if (value is InitParams) { + } else if (value is AuthorizeFailureType) { buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is UserData) { + writeValue(buffer, value.index); + } else if (value is PlatformAuthorizationRequest) { buffer.putUint8(131); writeValue(buffer, value.encode()); + } else if (value is GetCredentialRequestParams) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is PlatformGoogleIdTokenCredential) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is GetCredentialFailure) { + buffer.putUint8(134); + writeValue(buffer, value.encode()); + } else if (value is GetCredentialSuccess) { + buffer.putUint8(135); + writeValue(buffer, value.encode()); + } else if (value is AuthorizeFailure) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is PlatformAuthorizationResult) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -156,22 +373,35 @@ class _PigeonCodec extends StandardMessageCodec { switch (type) { case 129: final int? value = readValue(buffer) as int?; - return value == null ? null : SignInType.values[value]; + return value == null ? null : GetCredentialFailureType.values[value]; case 130: - return InitParams.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : AuthorizeFailureType.values[value]; case 131: - return UserData.decode(readValue(buffer)!); + return PlatformAuthorizationRequest.decode(readValue(buffer)!); + case 132: + return GetCredentialRequestParams.decode(readValue(buffer)!); + case 133: + return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!); + case 134: + return GetCredentialFailure.decode(readValue(buffer)!); + case 135: + return GetCredentialSuccess.decode(readValue(buffer)!); + case 136: + return AuthorizeFailure.decode(readValue(buffer)!); + case 137: + return PlatformAuthorizationResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } } } -class GoogleSignInApi { - /// Constructor for [GoogleSignInApi]. The [binaryMessenger] named argument is +class CredentialManagerApi { + /// Constructor for [CredentialManagerApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GoogleSignInApi( + CredentialManagerApi( {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = @@ -182,43 +412,20 @@ class GoogleSignInApi { final String pigeonVar_messageChannelSuffix; - /// Initializes a sign in request with the given parameters. - Future init(InitParams params) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.init$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([params]) as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } - - /// Starts a silent sign in. - Future signInSilently() async { + Future getCredential( + GetCredentialRequestParams params) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.signInSilently$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getCredential$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([params]); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -233,82 +440,22 @@ class GoogleSignInApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as UserData?)!; + return (pigeonVar_replyList[0] as GetCredentialResult?)!; } } - /// Starts a sign in with user interaction. - Future signIn() async { + Future clearCredentialState() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.signIn$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.clearCredentialState$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as UserData?)!; - } - } - - /// Requests the access token for the current sign in. - Future getAccessToken(String email, bool shouldRecoverAuth) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.getAccessToken$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = await pigeonVar_channel - .send([email, shouldRecoverAuth]) as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as String?)!; - } - } - - /// Signs out the current user. - Future signOut() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.signOut$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -321,100 +468,37 @@ class GoogleSignInApi { return; } } +} - /// Revokes scope grants to the application. - Future disconnect() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.disconnect$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } +class AuthorizationClientApi { + /// Constructor for [AuthorizationClientApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + AuthorizationClientApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; - /// Returns whether the user is currently signed in. - Future isSignedIn() async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.isSignedIn$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send(null) as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as bool?)!; - } - } + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - /// Clears the authentication caching for the given token, requiring a - /// new sign in. - Future clearAuthCache(String token) async { - final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.clearAuthCache$pigeonVar_messageChannelSuffix'; - final BasicMessageChannel pigeonVar_channel = - BasicMessageChannel( - pigeonVar_channelName, - pigeonChannelCodec, - binaryMessenger: pigeonVar_binaryMessenger, - ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([token]) as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } - } + final String pigeonVar_messageChannelSuffix; - /// Requests access to the given scopes. - Future requestScopes(List scopes) async { + Future authorize(PlatformAuthorizationRequest params, + {required bool promptIfUnauthorized}) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.requestScopes$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.AuthorizationClientApi.authorize$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); + final Future pigeonVar_sendFuture = + pigeonVar_channel.send([params, promptIfUnauthorized]); final List? pigeonVar_replyList = - await pigeonVar_channel.send([scopes]) as List?; + await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -429,7 +513,7 @@ class GoogleSignInApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (pigeonVar_replyList[0] as bool?)!; + return (pigeonVar_replyList[0] as AuthorizeResult?)!; } } } diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart index cdb92ea3337..e97d75a42a7 100644 --- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -6,101 +6,164 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', - javaOut: - 'android/src/main/java/io/flutter/plugins/googlesignin/Messages.java', - javaOptions: JavaOptions(package: 'io.flutter.plugins.googlesignin'), + kotlinOut: + 'android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt', + kotlinOptions: KotlinOptions(package: 'io.flutter.plugins.googlesignin'), copyrightHeader: 'pigeons/copyright.txt', )) -/// Pigeon version of SignInOption. -enum SignInType { - /// Default configuration. - standard, - - /// Recommended configuration for game sign in. - games, +/// The information necessary to build a an authorization request. +/// +/// Corresponds to the native AuthorizationRequest object, but only contains +/// the fields used by this plugin. +class PlatformAuthorizationRequest { + PlatformAuthorizationRequest({required this.scopes, this.hostedDomain}); + List scopes; + String? hostedDomain; + String? accountEmail; + + /// If set, adds a call to requestOfflineAccess(this string, true); + String? serverClientIdForForcedRefreshToken; } -/// Pigeon version of SignInInitParams. +/// The information necessary to build a credential request. /// -/// See SignInInitParams for details. -class InitParams { - /// The parameters to use when initializing the sign in process. - const InitParams({ - this.scopes = const [], - this.signInType = SignInType.standard, - this.hostedDomain, - this.clientId, +/// Combines the parts of the native GetCredentialRequest and CredentialOption +/// classes that are used for this plugin. +class GetCredentialRequestParams { + GetCredentialRequestParams({ + required this.filterToAuthorized, + required this.autoSelectEnabled, this.serverClientId, - this.forceCodeForRefreshToken = false, - this.forceAccountName, }); + bool filterToAuthorized; + bool autoSelectEnabled; + String? serverClientId; +} - final List scopes; - final SignInType signInType; - final String? hostedDomain; - final String? clientId; - final String? serverClientId; - final bool forceCodeForRefreshToken; - final String? forceAccountName; +/// Pigeon equivalent of the native GoogleIdTokenCredential. +class PlatformGoogleIdTokenCredential { + String? displayName; + String? familyName; + String? givenName; + late String id; + late String idToken; + String? profilePictureUri; } -/// Pigeon version of GoogleSignInUserData. +enum GetCredentialFailureType { + /// Indicates that a credential was returned, but it was not of the expected + /// type. + unexpectedCredentialType, + + /// Indicates that a server client ID was not provided. + missingServerClientId, + + // Types from https://developer.android.com/reference/android/credentials/GetCredentialException + /// The request was internally interrupted. + interrupted, + + /// The request was canceled by the user. + canceled, + + /// No matching credential was found. + noCredential, + + /// The provider was not properly configured. + providerConfigurationIssue, + + /// The credential manager is not supported on this device. + unsupported, + + /// The request failed for an unknown reason. + unknown, +} + +/// The response from a `getCredential` call. /// -/// See GoogleSignInUserData for details. -class UserData { - UserData({ - required this.email, - required this.id, - this.displayName, - this.photoUrl, - this.idToken, - this.serverAuthCode, - }); +/// This is not the same as a native GetCredentialResponse since modeling the +/// response type hierarchy and two-part callback in this interface layer would +/// add a lot of complexity that is not needed for the plugin's use case. It is +/// instead a processed version of the results of those callbacks. +sealed class GetCredentialResult {} + +/// An authentication failure. +class GetCredentialFailure extends GetCredentialResult { + /// The type of failure. + late GetCredentialFailureType type; + + /// The message associated with the failure, if any. + String? message; + + /// Extra details about the failure, if any. + String? details; +} - final String? displayName; - final String email; - final String id; - final String? photoUrl; - final String? idToken; - final String? serverAuthCode; +/// A successful authentication result. +class GetCredentialSuccess extends GetCredentialResult { + late PlatformGoogleIdTokenCredential credential; } -@HostApi() -abstract class GoogleSignInApi { - /// Initializes a sign in request with the given parameters. - void init(InitParams params); +enum AuthorizeFailureType { + /// Indicates that the requested types are not currently authorized. + /// + /// This is returned only if promptIfUnauthorized is false, indicating that + /// the user would need to be prompted for authorization. + unauthorized, - /// Starts a silent sign in. - @async - UserData signInSilently(); + /// Indicates that the call to AuthorizationClient.authorize itself failed. + authorizeFailure, - /// Starts a sign in with user interaction. - @async - UserData signIn(); + /// Corresponds to SendIntentException, indicating that the pending intent is + /// no longer available. + pendingIntentException, - /// Requests the access token for the current sign in. - @async - @TaskQueue(type: TaskQueueType.serialBackgroundThread) - String getAccessToken(String email, bool shouldRecoverAuth); + /// Corresponds to an SendIntentException in onActivityResult, indicating that + /// either authorization failed, or the result was not available for some + /// reason. + apiException, - /// Signs out the current user. - @async - void signOut(); + /// Indicates that the user needs to be prompted for authorization, but there + /// is no current activity to prompt in. + noActivity, +} - /// Revokes scope grants to the application. - @async - void disconnect(); +/// The response from an `authorize` call. +sealed class AuthorizeResult {} + +/// An authorization failure +class AuthorizeFailure extends AuthorizeResult { + /// The type of failure. + late AuthorizeFailureType type; + + /// The message associated with the failure, if any. + String? message; + + /// Extra details about the failure, if any. + String? details; +} - /// Returns whether the user is currently signed in. - bool isSignedIn(); +/// A successful authorization result. +/// +/// Corresponds to a native AuthorizationResult. +class PlatformAuthorizationResult extends AuthorizeResult { + String? accessToken; + String? serverAuthCode; + late List grantedScopes; +} + +@HostApi() +abstract class CredentialManagerApi { + @async + GetCredentialResult getCredential(GetCredentialRequestParams params); - /// Clears the authentication caching for the given token, requiring a - /// new sign in. - @TaskQueue(type: TaskQueueType.serialBackgroundThread) - void clearAuthCache(String token); + @async + void clearCredentialState(); +} - /// Requests access to the given scopes. +@HostApi() +abstract class AuthorizationClientApi { @async - bool requestScopes(List scopes); + AuthorizeResult authorize(PlatformAuthorizationRequest params, + {required bool promptIfUnauthorized}); } From df1cf872c3354be0bec0edfe9b55040600b670c1 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 20 Mar 2025 15:05:35 -0400 Subject: [PATCH 02/52] Pathify everything for local development --- .../example/pubspec.yaml | 4 ++++ .../pubspec.yaml | 4 ++++ .../google_sign_in/google_sign_in/example/pubspec.yaml | 7 +++++++ packages/google_sign_in/google_sign_in/pubspec.yaml | 7 +++++++ .../google_sign_in_android/example/pubspec.yaml | 4 ++++ .../google_sign_in/google_sign_in_android/pubspec.yaml | 4 ++++ .../google_sign_in/google_sign_in_ios/example/pubspec.yaml | 4 ++++ packages/google_sign_in/google_sign_in_ios/pubspec.yaml | 4 ++++ .../google_sign_in/google_sign_in_web/example/pubspec.yaml | 4 ++++ packages/google_sign_in/google_sign_in_web/pubspec.yaml | 4 ++++ 10 files changed, 46 insertions(+) diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml index 2cae99f0f90..4f089757bb6 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in: {path: ../../../packages/google_sign_in/google_sign_in} diff --git a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml index d3a29c234fe..f76c5e6c964 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml @@ -33,3 +33,7 @@ topics: false_secrets: - example/android/app/google-services.json - example/ios/Runner/GoogleService-Info.plist +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in: {path: ../../packages/google_sign_in/google_sign_in} diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index 5335068734a..6061b2dc8f9 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -28,3 +28,10 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_android: {path: ../../../../packages/google_sign_in/google_sign_in_android} + google_sign_in_ios: {path: ../../../../packages/google_sign_in/google_sign_in_ios} + google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} + google_sign_in_web: {path: ../../../../packages/google_sign_in/google_sign_in_web} diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index c256a4e6105..3e5d7b652d2 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -47,3 +47,10 @@ false_secrets: - /example/ios/RunnerTests/GoogleService-Info.plist - /example/ios/RunnerTests/GoogleSignInTests.m - /example/macos/Runner/Info.plist +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_android: {path: ../../../packages/google_sign_in/google_sign_in_android} + google_sign_in_ios: {path: ../../../packages/google_sign_in/google_sign_in_ios} + google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} + google_sign_in_web: {path: ../../../packages/google_sign_in/google_sign_in_web} diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml index 0a2eb7e2358..7563978fbab 100644 --- a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -28,3 +28,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index 05197dda1f0..f4444cf82a3 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -37,3 +37,7 @@ topics: false_secrets: - /example/android/app/google-services.json - /example/lib/main.dart +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml index d687148ca0e..7a56b9a321c 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -27,3 +27,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index 87f3cfffab8..f73ec169ac2 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -44,3 +44,7 @@ false_secrets: - /example/ios/Runner/Info.plist - /example/lib/main.dart - /example/macos/Runner/Info.plist +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml index fbc10d72532..665db5488c0 100644 --- a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml @@ -26,3 +26,7 @@ dev_dependencies: flutter: uses-material-design: true +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} 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 9ddbd479d24..bba9aa07f15 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -34,3 +34,7 @@ dev_dependencies: topics: - authentication - google-sign-in +# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. +# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins +dependency_overrides: + google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} From 6a1ec7c06ac451abe0aa43183313ddb50916d857 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 20 Mar 2025 15:23:53 -0400 Subject: [PATCH 03/52] Remove method channel implementation --- .../google_sign_in_platform_interface.dart | 26 +-- .../src/method_channel_google_sign_in.dart | 101 ---------- ...oogle_sign_in_platform_interface_test.dart | 25 +-- .../method_channel_google_sign_in_test.dart | 174 ------------------ 4 files changed, 9 insertions(+), 317 deletions(-) delete mode 100644 packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart delete mode 100644 packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart 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 110097f0dcb..131dbe8f2b7 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,32 +24,19 @@ 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; } @@ -158,3 +142,5 @@ abstract class GoogleSignInPlatform extends PlatformInterface { /// to the `onCurrentUserChanged` Stream of the plugin. Stream? get userDataEvents => null; } + +class _PlaceholderImplementation extends GoogleSignInPlatform {} 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 fde29aeb8e4..00000000000 --- 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/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 057f13cb26f..851c0e40062 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,14 +8,7 @@ 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`', () { expect(() { GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); @@ -34,11 +27,7 @@ void main() { }); test('Can be mocked with `implements`', () { - GoogleSignInPlatform.instance = ModernMockImplementation(); - }); - - test('still supports legacy isMock', () { - GoogleSignInPlatform.instance = LegacyIsMockImplementation(); + GoogleSignInPlatform.instance = MockImplementation(); }); }); @@ -81,17 +70,9 @@ void main() { }); } -class LegacyIsMockImplementation extends Mock implements GoogleSignInPlatform { - @override - bool get isMock => true; -} - -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 {} 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 52e792a9c25..00000000000 --- 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, - }), - ]); - }); - }); -} From 49342c524b1c7258330e8e92842181ed064cc79a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 20 Mar 2025 15:25:33 -0400 Subject: [PATCH 04/52] First pass rework of interface, impls, and app-facing API Also reworks app-facing package example app for testing. --- .../xcshareddata/swiftpm/Package.resolved | 69 ++ .../google_sign_in/example/lib/main.dart | 268 ++++--- .../xcshareddata/swiftpm/Package.resolved | 42 + .../xcshareddata/swiftpm/Package.resolved | 42 + .../google_sign_in/lib/google_sign_in.dart | 718 +++++++++--------- .../google_sign_in/lib/src/event_types.dart | 47 ++ .../src/{common.dart => identity_types.dart} | 3 - .../google_sign_in/lib/src/token_types.dart | 57 ++ .../google_sign_in/lib/widgets.dart | 2 +- .../googlesignin/GoogleSignInPlugin.java | 37 +- .../flutter/plugins/googlesignin/Messages.kt | 31 +- .../lib/google_sign_in_android.dart | 396 ++++------ .../lib/src/messages.g.dart | 32 +- .../pigeons/messages.dart | 7 + .../FLTGoogleSignInPlugin.m | 341 +++++---- .../FLTGoogleSignInPlugin_Test.h | 10 - .../include/google_sign_in_ios/messages.g.h | 157 ++-- .../Sources/google_sign_in_ios/messages.g.m | 449 +++++++---- .../xcshareddata/swiftpm/Package.resolved | 50 ++ .../xcshareddata/xcschemes/Runner.xcscheme | 2 + .../xcshareddata/swiftpm/Package.resolved | 50 ++ .../lib/google_sign_in_ios.dart | 239 ++++-- .../lib/src/messages.g.dart | 530 ++++++++----- .../google_sign_in_ios/pigeons/messages.dart | 138 +++- .../google_sign_in_platform_interface.dart | 166 ++-- .../lib/src/types.dart | 430 ++++++++--- .../lib/src/utils.dart | 28 - ...oogle_sign_in_platform_interface_test.dart | 8 +- .../lib/google_sign_in_web.dart | 205 ++--- .../lib/src/gis_client.dart | 522 +++++-------- .../google_sign_in_web/lib/src/utils.dart | 31 +- .../google_sign_in_web/lib/web_only.dart | 7 - 32 files changed, 3033 insertions(+), 2081 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 packages/google_sign_in/google_sign_in/lib/src/event_types.dart rename packages/google_sign_in/google_sign_in/lib/src/{common.dart => identity_types.dart} (94%) create mode 100644 packages/google_sign_in/google_sign_in/lib/src/token_types.dart create mode 100644 packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..160aa48fc26 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,69 @@ +{ + "originHash" : "fd06f65309a465a6cb6f442cd439d2481f4f7bb167b133bd83bf27b3b6b211e8", + "pins" : [ + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "65fb3f1aa6ffbfdc79c4e22178a55cd91561f5e9", + "version" : "8.0.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + } + ], + "version" : 3 +} diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 576ec36feff..58fa31da798 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -7,27 +7,26 @@ import 'dart:async'; import 'dart:convert' show json; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:http/http.dart' as http; import 'src/sign_in_button.dart'; +/// To run this example, replace this value with your client ID, and/or +/// update the relevant configuration files, as described in the README. +String? clientId; + +/// To run this example, replace this value with your server client ID, and/or +/// update the relevant configuration files, as described in the README. +String? serverClientId; + /// The scopes required by this application. -// #docregion Initialize const List scopes = [ 'email', 'https://www.googleapis.com/auth/contacts.readonly', ]; -GoogleSignIn _googleSignIn = GoogleSignIn( - // Optional clientId - // clientId: 'your-client_id.apps.googleusercontent.com', - scopes: scopes, -); -// #enddocregion Initialize - void main() { runApp( const MaterialApp( @@ -50,40 +49,63 @@ class _SignInDemoState extends State { GoogleSignInAccount? _currentUser; bool _isAuthorized = false; // has granted permissions? String _contactText = ''; + String _errorMessage = ''; + String _serverAuthCode = ''; @override void initState() { super.initState(); - _googleSignIn.onCurrentUserChanged - .listen((GoogleSignInAccount? account) async { -// #docregion CanAccessScopes - // In mobile, being authenticated means being authorized... - bool isAuthorized = account != null; - // However, on web... - if (kIsWeb && account != null) { - isAuthorized = await _googleSignIn.canAccessScopes(scopes); - } -// #enddocregion CanAccessScopes + final GoogleSignIn signIn = GoogleSignIn.instance; + unawaited(signIn + .initialize(clientId: clientId, serverClientId: serverClientId) + .then((_) { + GoogleSignIn.instance.authenticationEvents + .listen(_handleAuthenticationEvent); - setState(() { - _currentUser = account; - _isAuthorized = isAuthorized; - }); + /// This example always uses the stream-based approach to determining + /// which UI state to show, rather than using the future returned here, + /// if any, to conditionally skip directly to the signed-in state. + signIn.attemptLightweightAuthentication(); + })); + } - // Now that we know that the user can access the required scopes, the app - // can call the REST API. - if (isAuthorized) { - unawaited(_handleGetContact(account!)); - } + Future _handleAuthenticationEvent( + GoogleSignInAuthenticationEvent event) async { + GoogleSignInAccount? user; + String error = ''; + switch (event) { + case GoogleSignInAuthenticationEventSignIn(): + user = event.user; + case GoogleSignInAuthenticationEventSignOut(): + user = null; + case GoogleSignInAuthenticationEventException(): + user = null; + final GoogleSignInException e = event.exception; + error = 'GoogleSignInException ${e.code}: ${e.description}'; + } + + // #docregion CanAccessScopes + // Check for existing authorization. + bool isAuthorized = false; + if (user != null) { + final GoogleSignInClientAuthorization? authorization = + await user.authorizationClient.authorizationForScopes(scopes); + isAuthorized = authorization != null; + } + // #enddocregion CanAccessScopes + + setState(() { + _currentUser = user; + _isAuthorized = isAuthorized; + _errorMessage = error; }); - // In the web, _googleSignIn.signInSilently() triggers the One Tap UX. - // - // It is recommended by Google Identity Services to render both the One Tap UX - // and the Google Sign In button together to "reduce friction and improve - // sign-in rates" ([docs](https://developers.google.com/identity/gsi/web/guides/display-button#html)). - _googleSignIn.signInSilently(); + // Now that we know that the user can access the required scopes, the app + // can call the REST API. + if (user != null && isAuthorized) { + unawaited(_handleGetContact(user)); + } } // Calls the People API REST endpoint for the signed-in user to retrieve information. @@ -91,10 +113,18 @@ class _SignInDemoState extends State { setState(() { _contactText = 'Loading contact info...'; }); + final Map? headers = + await user.authorizationClient.authorizationHeaders(scopes); + if (headers == null) { + setState(() { + _contactText = 'Failed to construct authorization headers.'; + }); + return; + } final http.Response response = await http.get( Uri.parse('https://people.googleapis.com/v1/people/me/connections' '?requestMask.includeField=person.names'), - headers: await user.authHeaders, + headers: headers, ); if (response.statusCode != 200) { setState(() { @@ -143,87 +173,125 @@ class _SignInDemoState extends State { // #docregion SignIn Future _handleSignIn() async { try { - await _googleSignIn.signIn(); - } catch (error) { - print(error); + await GoogleSignIn.instance.authenticate(); + } catch (e) { + _errorMessage = e.toString(); } } // #enddocregion SignIn // Prompts the user to authorize `scopes`. // - // This action is **required** in platforms that don't perform Authentication - // and Authorization at the same time (like the web). - // // On the web, this must be called from an user interaction (button click). // #docregion RequestScopes - Future _handleAuthorizeScopes() async { - final bool isAuthorized = await _googleSignIn.requestScopes(scopes); - // #enddocregion RequestScopes - setState(() { - _isAuthorized = isAuthorized; - }); - // #docregion RequestScopes - if (isAuthorized) { + Future _handleAuthorizeScopes(GoogleSignInAccount user) async { + try { + // The returned tokens are ignored here since _handleGetContact uses the + // authorizationHeaders method to re-read the token cached here. + await user.authorizationClient.authorizeScopes(scopes); + + // #enddocregion RequestScopes + setState(() { + _isAuthorized = true; + _errorMessage = ''; + }); + // #docregion RequestScopes unawaited(_handleGetContact(_currentUser!)); + // #enddocregion RequestScopes + } on GoogleSignInException catch (e) { + _errorMessage = 'GoogleSignInException ${e.code}: ${e.description}'; + } + } + + // Requests a server auth code for the authorized scopes. + // + // On the web, this must be called from an user interaction (button click). + // #docregion RequestScopes + Future _handleGetAuthCode(GoogleSignInAccount user) async { + try { + final GoogleSignInServerAuthorization? serverAuth = + await user.authorizationClient.authorizeServer(scopes); + + // #enddocregion RequestScopes + setState(() { + _serverAuthCode = serverAuth == null ? '' : serverAuth.serverAuthCode; + }); + } on GoogleSignInException catch (e) { + _errorMessage = 'GoogleSignInException ${e.code}: ${e.description}'; } - // #enddocregion RequestScopes } - Future _handleSignOut() => _googleSignIn.disconnect(); + Future _handleSignOut() async { + // Disconnect instead of just signing out, to reset the example state as + // much as possible. + await GoogleSignIn.instance.disconnect(); + } Widget _buildBody() { final GoogleSignInAccount? user = _currentUser; - if (user != null) { - // The user is Authenticated - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ListTile( - leading: GoogleUserCircleAvatar( - identity: user, - ), - title: Text(user.displayName ?? ''), - subtitle: Text(user.email), - ), - const Text('Signed in successfully.'), - if (_isAuthorized) ...[ - // The user has Authorized all required scopes - Text(_contactText), - ElevatedButton( - child: const Text('REFRESH'), - onPressed: () => _handleGetContact(user), - ), - ], - if (!_isAuthorized) ...[ - // The user has NOT Authorized all required scopes. - // (Mobile users may never see this button!) - const Text('Additional permissions needed to read your contacts.'), - ElevatedButton( - onPressed: _handleAuthorizeScopes, - child: const Text('REQUEST PERMISSIONS'), - ), - ], + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (user != null) + ..._buildAuthenticatedWidgets(user) + else + ..._buildUnauthenticatedWidgets(), + if (_errorMessage.isNotEmpty) Text(_errorMessage), + ], + ); + } + + /// Returns the list of widgets to include if the user is authenticated. + List _buildAuthenticatedWidgets(GoogleSignInAccount user) { + return [ + // The user is Authenticated. + ListTile( + leading: GoogleUserCircleAvatar( + identity: user, + ), + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), + ), + const Text('Signed in successfully.'), + if (_isAuthorized) ...[ + // The user has Authorized all required scopes. + if (_contactText.isNotEmpty) Text(_contactText), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + if (_serverAuthCode.isEmpty) ElevatedButton( - onPressed: _handleSignOut, - child: const Text('SIGN OUT'), - ), - ], - ); - } else { - // The user is NOT Authenticated - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text('You are not currently signed in.'), - // This method is used to separate mobile from web code with conditional exports. - // See: src/sign_in_button.dart - buildSignInButton( - onPressed: _handleSignIn, - ), - ], - ); - } + child: const Text('REQUEST SERVER CODE'), + onPressed: () => _handleGetAuthCode(user), + ) + else + Text('Server auth code:\n$_serverAuthCode'), + ] else ...[ + // The user has NOT Authorized all required scopes. + const Text('Authorization needed to read your contacts.'), + ElevatedButton( + onPressed: () => _handleAuthorizeScopes(user), + child: const Text('REQUEST PERMISSIONS'), + ), + ], + ElevatedButton( + onPressed: _handleSignOut, + child: const Text('SIGN OUT'), + ), + ]; + } + + /// Returns the list of widgets to include if the user is not authenticated. + List _buildUnauthenticatedWidgets() { + return [ + const Text('You are not currently signed in.'), + // This method is used to separate mobile from web code with conditional exports. + // See: src/sign_in_button.dart + buildSignInButton( + onPressed: _handleSignIn, + ), + ]; } @override diff --git a/packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..2ae8b21eb12 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "7220edc89d961f8413c785deacf1702210b58380c136ba1241e754a6e1eab3fb", + "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + } + ], + "version" : 3 +} diff --git a/packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..2ae8b21eb12 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,42 @@ +{ + "originHash" : "7220edc89d961f8413c785deacf1702210b58380c136ba1241e754a6e1eab3fb", + "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + } + ], + "version" : 3 +} diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 93565ad052b..e0af08455fc 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -5,58 +5,37 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart' show PlatformException; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'src/common.dart'; +import 'src/event_types.dart'; +import 'src/identity_types.dart'; +import 'src/token_types.dart'; export 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' - show SignInOption; - -export 'src/common.dart'; + show GoogleSignInException; +export 'src/event_types.dart'; +export 'src/identity_types.dart'; +export 'src/token_types.dart'; export 'widgets.dart'; -/// Holds authentication tokens after sign in. -class GoogleSignInAuthentication { - GoogleSignInAuthentication._(this._data); - - final GoogleSignInTokenData _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; - - /// Server auth code used to access Google Login - @Deprecated('Use the `GoogleSignInAccount.serverAuthCode` property instead') - String? get serverAuthCode => _data.serverAuthCode; - - @override - String toString() => 'GoogleSignInAuthentication:$_data'; -} - -/// Holds fields describing a signed in user's identity, following -/// [GoogleSignInUserData]. +/// Represents a signed-in Google account, providing account information as well +/// as utilities for obtaining authentication and authorization tokens. /// -/// [id] is guaranteed to be non-null. +/// Although the API of the plugin is structured to allow for the possibility +/// of multiple signed in users, the underlying Google Sign In SDKs on each +/// platform do not all currently support multiple users in practice. For best +/// cross-platform results, clients should not call [authenticate] to obtain a +/// new [GoogleSignInAccount] instance until after a call to [signOut]. @immutable class GoogleSignInAccount implements GoogleIdentity { - GoogleSignInAccount._(this._googleSignIn, GoogleSignInUserData data) - : displayName = data.displayName, - email = data.email, - id = data.id, - photoUrl = data.photoUrl, - serverAuthCode = data.serverAuthCode, - _idToken = data.idToken; - - // 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'; + GoogleSignInAccount._( + GoogleSignInUserData userData, + AuthenticationTokenData tokenData, + ) : displayName = userData.displayName, + email = userData.email, + id = userData.id, + photoUrl = userData.photoUrl, + _authenticationTokens = tokenData; @override final String? displayName; @@ -70,61 +49,22 @@ class GoogleSignInAccount implements GoogleIdentity { @override final String? photoUrl; - @override - final String? serverAuthCode; - - final String? _idToken; - final GoogleSignIn _googleSignIn; + final AuthenticationTokenData _authenticationTokens; - /// Retrieve [GoogleSignInAuthentication] for this account. + /// Returns authentication tokens 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 get authentication async { - if (_googleSignIn.currentUser != this) { - throw StateError('User is no longer signed in.'); - } - - final GoogleSignInTokenData response = - await GoogleSignInPlatform.instance.getTokens( - email: email, - shouldRecoverAuth: true, - ); - - // On Android, there isn't an API for refreshing the idToken, so re-use - // the one we obtained on login. - response.idToken ??= _idToken; - - return GoogleSignInAuthentication._(response); + /// This returns the authentication information that was returned at the time + /// of the initial authentication. Clients are strongly encouraged to use this + /// information immediately after authentication, as tokens are subject to + /// expiration, and obtaining new tokens requires re-authenticating. + GoogleSignInAuthentication get authentication { + return GoogleSignInAuthentication(idToken: _authenticationTokens.idToken); } - /// Convenience method returning a `` map of HTML Authorization - /// headers, containing the current `authentication.accessToken`. - /// - /// See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization. - Future> get authHeaders async { - final String? token = (await authentication).accessToken; - return { - 'Authorization': 'Bearer $token', - // TODO(kevmoo): Use the correct value once it's available from authentication - // See https://github.com/flutter/flutter/issues/80905 - '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 clearAuthCache() async { - final String token = (await authentication).accessToken!; - await GoogleSignInPlatform.instance.clearAuthCache(token: token); + /// Returns a client that can be used to request authorization tokens for + /// this user. + GoogleSignInAuthorizationClient get authorizationClient { + return GoogleSignInAuthorizationClient._(this); } @override @@ -140,13 +80,13 @@ class GoogleSignInAccount implements GoogleIdentity { email == otherAccount.email && id == otherAccount.id && photoUrl == otherAccount.photoUrl && - serverAuthCode == otherAccount.serverAuthCode && - _idToken == otherAccount._idToken; + _authenticationTokens.idToken == + otherAccount._authenticationTokens.idToken; } @override - int get hashCode => - Object.hash(displayName, email, id, photoUrl, _idToken, serverAuthCode); + int get hashCode => Object.hash( + displayName, email, id, photoUrl, _authenticationTokens.idToken); @override String toString() { @@ -155,321 +95,377 @@ class GoogleSignInAccount implements GoogleIdentity { 'email': email, 'id': id, 'photoUrl': photoUrl, - 'serverAuthCode': serverAuthCode }; return 'GoogleSignInAccount:$data'; } } -/// GoogleSignIn allows you to authenticate Google users. -class GoogleSignIn { - /// Initializes global sign-in configuration settings. +/// A utility for requesting authorization tokens. +/// +/// If the instance was obtained from a [GoogleSignInAccount], any requests +/// issued by this client will be for tokens for that account. +/// +/// If the instance was obtained directly from [GoogleSignIn], the request will +/// not be limited to a specific user, and the behavior will depend on the +/// platform and the current application state. Examples include: +/// - If there is an active authentication session in the application already, +/// the authorization tokens may be associated for that user. +/// - If no user has been authenticated, this may trigger a combined +/// authentication+authorization flow. In that case, whether +/// [GoogleSignIn]'s authenticationEvents stream will be informed of the +/// authentication depends on the platform implementation. You should not +/// assume the user information or authenication tokens will be available in +/// this case. +class GoogleSignInAuthorizationClient { + GoogleSignInAuthorizationClient._(GoogleIdentity? user) + : _userId = user?.id, + _userEmail = user?.email; + + final String? _userId; + final String? _userEmail; + + /// Requests client authorization tokens if they can be returned without user + /// interaction. /// - /// The [signInOption] determines the user experience. [SigninOption.games] - /// is only supported on Android. + /// If authorization would require user interaction, this returns null, in + /// which case [authorizeScopes] should be used instead. + Future authorizationForScopes( + List scopes) async { + return _authorizeClient(scopes, promptIfUnauthorized: false); + } + + /// Requests that the user authorize the given scopes, and either returns the + /// resulting client authorization tokens, or throws an exception with failure + /// details. /// - /// 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: - /// + /// This should only be called from a context where user interaction is + /// allowed (for example, during a user event handler on web, or while the + /// app is foregrounded on mobile). + Future authorizeScopes( + List scopes) async { + final GoogleSignInClientAuthorization? authz = + await _authorizeClient(scopes, promptIfUnauthorized: true); + // The platform interface documents that null should only be returned for + // cases where prompting isn't requested, so if this happens it's a bug + // in the platform implementation. + if (authz == null) { + throw const GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: 'Platform returned null unexpectedly.'); + } + return authz; + } + + /// Convenience method returning a `` map of HTML + /// authorization headers, containing the access token for the given scopes. /// - /// 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. + /// Returns null if the given scopes are not authorized, or there is no + /// currently valid authorization token available, and + /// [promptIfNecessary] is false. /// - /// The [forceCodeForRefreshToken] is used on Android to ensure the authentication - /// code can be exchanged for a refresh token after the first request. - GoogleSignIn({ - this.signInOption = SignInOption.standard, - this.scopes = const [], - this.hostedDomain, - this.clientId, - this.serverClientId, - this.forceCodeForRefreshToken = false, - this.forceAccountName, - }) { - // Start initializing. - if (kIsWeb) { - // Start initializing the plugin ASAP, so the `userDataEvents` Stream for - // the web can be used without calling any other methods of the plugin - // (like `silentSignIn` or `isSignedIn`). - unawaited(_ensureInitialized()); + /// See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization. + Future?> authorizationHeaders(List scopes, + {bool promptIfNecessary = false}) async { + final GoogleSignInClientAuthorization? authz = + await authorizationForScopes(scopes) ?? + (promptIfNecessary ? await authorizeScopes(scopes) : null); + if (authz == null) { + return null; } + return { + 'Authorization': 'Bearer ${authz.accessToken}', + 'X-Goog-AuthUser': '0', + }; } - /// Factory for creating default sign in user experience. - factory GoogleSignIn.standard({ - List scopes = const [], - String? hostedDomain, - }) { - return GoogleSignIn(scopes: scopes, hostedDomain: hostedDomain); + /// Requests that the user authorize the given scopes for server use. + /// + /// In addition to throwing an exception for authorization failures, this can + /// return null if the server authorization tokens are not available. For + /// intance, some platforms only provide a valid server auth token on initial + /// login. Clients requiring a server auth token should not rely on being able + /// to re-request server auth tokens at arbitrary times, and should instead + /// store the token when it is first available, and manage refreshes on + /// the server side using that token. + /// + /// This should only be called from a context where user interaction is + /// allowed (for example, during a user event handler on web, or while the + /// app is foregrounded on mobile). + Future authorizeServer( + List scopes) async { + final ServerAuthorizationTokenData? tokens = + await GoogleSignInPlatform.instance.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _userId, + email: _userEmail, + promptIfUnauthorized: true))); + return tokens == null + ? null + : GoogleSignInServerAuthorization( + serverAuthCode: tokens.serverAuthCode); } - /// Factory for creating sign in suitable for games. This option is only - /// supported on Android. - factory GoogleSignIn.games() { - return GoogleSignIn(signInOption: SignInOption.games); + Future _authorizeClient(List scopes, + {required bool promptIfUnauthorized}) async { + final ClientAuthorizationTokenData? tokens = + await GoogleSignInPlatform.instance.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _userId, + email: _userEmail, + promptIfUnauthorized: promptIfUnauthorized))); + return tokens == null + ? null + : GoogleSignInClientAuthorization(accessToken: tokens.accessToken); } +} - // 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 network error. Retrying should resolve the problem. - static const String kNetworkError = 'network_error'; - - /// Error code indicating that attempt to sign in failed. - static const String kSignInFailedError = 'sign_in_failed'; - - /// Option to determine the sign in user experience. [SignInOption.games] is - /// only supported on Android. - final SignInOption signInOption; - - /// The list of [scopes] are OAuth scope codes requested when signing in. - final List scopes; - - /// Domain to restrict sign-in to. - final String? hostedDomain; +/// GoogleSignIn allows you to authenticate Google users. +class GoogleSignIn { + GoogleSignIn._(); - /// Client ID being used to connect to google sign-in. + /// Returns the single [GoogleSignIn] instance. /// - /// This option is not supported on all platforms (e.g. Android). It is - /// optional if file-based configuration is used. - /// - /// The value specified here has precedence over a value from a configuration - /// file. - final String? clientId; + /// [initialize] must be called on this instance, and its future allowed to + /// complete, before any other methods on the object are called. + static final GoogleSignIn instance = GoogleSignIn._(); - /// Client ID of the backend server to which the app needs to authenticate - /// itself. + /// Initializes the sign in manager with the given configuration. /// - /// Optional and not supported on all platforms (e.g. web). By default, it - /// is initialized from a configuration file if available. + /// Clients must call this method exactly once, and wait for its future to + /// complete, before calling any other methods on this object. /// - /// The value specified here has precedence over a value from a configuration - /// file. + /// [clientId] is the identifier for your client application, as provided by + /// the Google Sign In server configuration, if any. This does not need to be + /// provided on platforms that do not require a client identifier, or if it is + /// provided via application-level configuration files. See the README for + /// details. If provided, it will take precedence over any value in a + /// configuration file. /// - /// [GoogleSignInAuthentication.idToken] and - /// [GoogleSignInAccount.serverAuthCode] will be specific to the backend - /// server. - final String? serverClientId; - - /// Force the authorization code to be valid for a refresh token every time. Only needed on Android. - final bool forceCodeForRefreshToken; - - /// Explicitly specifies the account name to be used in sign-in. Must only be set on Android. - final String? forceAccountName; - - final StreamController _currentUserController = - StreamController.broadcast(); - - /// Subscribe to this stream to be notified when the current user changes. - Stream get onCurrentUserChanged { - return _currentUserController.stream; - } - - Future _callMethod( - Future Function() method) async { - await _ensureInitialized(); - - final dynamic response = await method(); - - return _setCurrentUser(response != null && response is GoogleSignInUserData - ? GoogleSignInAccount._(this, response) - : null); - } - - // Sets the current user, and propagates it through the _currentUserController. - GoogleSignInAccount? _setCurrentUser(GoogleSignInAccount? currentUser) { - if (currentUser != _currentUser) { - _currentUser = currentUser; - _currentUserController.add(_currentUser); - } - return _currentUser; - } - - // Future that completes when `init` has completed on the native side. - Future? _initialization; - - // Performs initialization, guarding it with the _initialization future. - Future _ensureInitialized() async { - _initialization ??= _doInitialization().catchError((Object e) { - // Invalidate initialization if it errors out. - _initialization = null; - // ignore: only_throw_errors - throw e; - }); - return _initialization; - } - - // Actually performs the initialization. - // - // This method calls initWithParams, and then, if the plugin instance has a - // userDataEvents Stream, connects it to the [_setCurrentUser] method. - Future _doInitialization() async { - await GoogleSignInPlatform.instance.initWithParams(SignInInitParameters( - signInOption: signInOption, - scopes: scopes, - hostedDomain: hostedDomain, + /// [serverClientId] is the identifier for your application's server-side + /// component, as provided by the Google Sign In server configuration, if any. + /// Depending on the platform, this value may be unused, optional, or + /// required. See the README for details. If provided, it will take precedence + /// over any value in a configuration file. + /// + /// If provided, [hostedDomain] restricts account selection to accounts in + /// that domain. + Future initialize({ + String? clientId, + String? serverClientId, + String? hostedDomain, + }) async { + await GoogleSignInPlatform.instance.init(InitParameters( clientId: clientId, serverClientId: serverClientId, - forceCodeForRefreshToken: forceCodeForRefreshToken, - forceAccountName: forceAccountName, + hostedDomain: hostedDomain, )); - unawaited(GoogleSignInPlatform.instance.userDataEvents - ?.map((GoogleSignInUserData? userData) { - return userData != null ? GoogleSignInAccount._(this, userData) : null; - }).forEach(_setCurrentUser)); - } - - /// The most recently scheduled method call. - Future? _lastMethodCall; - - /// Returns a [Future] that completes with a success after [future], whether - /// it completed with a value or an error. - static Future _waitFor(Future future) { - final Completer completer = Completer(); - future.whenComplete(completer.complete).catchError((dynamic _) { - // Ignore if previous call completed with an error. - // TODO(ditman): Should we log errors here, if debug or similar? - }); - return completer.future; + final Stream? platformAuthEvents = + GoogleSignInPlatform.instance.authenticationEvents; + if (platformAuthEvents == null) { + _createAuthenticationStreamEvents = true; + } else { + unawaited(platformAuthEvents.forEach(_translateAuthenticationEvent)); + } } - /// Adds call to [method] in a queue for execution. + /// Converts [event] into a corresponding event using the app-facing package + /// types. /// - /// At most one in flight call is allowed to prevent concurrent (out of order) - /// updates to [currentUser] and [onCurrentUserChanged]. + /// The platform interface types are intentionally not exposed to clients to + /// avoid platform interface package changes immediately transferring to the + /// public API without being able to control how they are exposed. /// - /// The optional, named parameter [canSkipCall] lets the plugin know that the - /// method call may be skipped, if there's already [_currentUser] information. - /// This is used from the [signIn] and [signInSilently] methods. - Future _addMethodCall( - Future Function() method, { - bool canSkipCall = false, - }) async { - Future response; - if (_lastMethodCall == null) { - response = _callMethod(method); - } else { - response = _lastMethodCall!.then((_) { - // If after the last completed call `currentUser` is not `null` and requested - // method can be skipped (`canSkipCall`), re-use the same authenticated user - // instead of making extra call to the native side. - if (canSkipCall && _currentUser != null) { - return _currentUser; - } - return _callMethod(method); - }); + /// This uses a convert-and-add approach rather than `map` so that new types + /// that don't have handlers yet can be dropped rather than causing errors. + void _translateAuthenticationEvent(AuthenticationEvent event) { + switch (event) { + case AuthenticationEventSignIn(): + _authenticationStreamController.add( + GoogleSignInAuthenticationEventSignIn( + user: GoogleSignInAccount._( + event.user, event.authenticationTokens))); + case AuthenticationEventSignOut(): + _authenticationStreamController + .add(GoogleSignInAuthenticationEventSignOut()); + case AuthenticationEventException(): + _authenticationStreamController + .add(GoogleSignInAuthenticationEventException(event.exception)); } - // Add the current response to the currently running Promise of all pending responses - _lastMethodCall = _waitFor(response); - return response; } - /// The currently signed in account, or null if the user is signed out. - GoogleSignInAccount? get currentUser => _currentUser; - GoogleSignInAccount? _currentUser; + /// Subscribe to this stream to be notified when sign in (authentication) and + /// sign out events happen. + Stream get authenticationEvents { + return _authenticationStreamController.stream; + } + + final StreamController + _authenticationStreamController = + StreamController.broadcast(); + + // Whether this package is responsible for creating stream events from + // authentication calls. This is true iff the platform instance returns null + // for authenticationEvents. + bool _createAuthenticationStreamEvents = false; - /// Attempts to sign in a previously authenticated user without interaction. + /// Attempts to sign in a previously authenticated user with minimal + /// 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. + /// The amount of allowable UI is up to the platform to determine, but it + /// should be minimal. Possible examples include FedCM on the web, and One Tap + /// on Android. Platforms may even show no UI, and only sign in if a previous + /// sign-in is being restored. This method is intended to be called as soon + /// as the application needs to know if the user is signed in, often at + /// initial launch. /// - /// Authentication is triggered 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. + /// Use [authenticate] instead to trigger a full interactive sign in process. /// - /// Re-authentication can be triggered after [signOut] or [disconnect]. It can - /// also be triggered by setting [reAuthenticate] to `true` if a new ID token - /// is required. + /// There are two possible return modes: + /// - If a Future is returned, applications could reasonably `await` that + /// future before deciding whether to display UI in a signed in or signed + /// out mode. For example, a platform where this method only restores + /// existing sign-ins would return a future, as either way it will resolve + /// quickly. + /// - If null is returned, applications must rely on [authenticationEvents] to + /// know when a sign-in occurs, and cannot rely on receiving a notification + /// that this call has *not* resulted in a sign-in in any reasonable amount + /// of time. In this mode, applications should assume a signed out mode + /// until/unless a sign-in event arrives on the stream. FedCM on the web + /// would be an example of this mode. /// - /// When [suppressErrors] is set to `false` and an error occurred during sign in - /// returned Future completes with [PlatformException] whose `code` can be - /// one of [kSignInRequiredError] (when there is no authenticated user) , - /// [kNetworkError] (when a network error occurred) or [kSignInFailedError] - /// (when an unknown error occurred). - Future signInSilently({ - bool suppressErrors = true, - bool reAuthenticate = false, + /// If a Future is returned, it resolves to an instance of + /// [GoogleSignInAccount] for a successful sign in or null if the attempt + /// implicitly did not result in any authentication. A [GoogleSignInException] + /// will be thrown if there was a failure (such as a client configuration + /// error). By default, this will not throw any of the following: + /// - [GoogleSignInExceptionCode.canceled] + /// - [GoogleSignInExceptionCode.interrupted] + /// - [GoogleSignInExceptionCode.uiUnavailable] + /// and will instead return null in those cases. To receive exceptions + /// for those cases instead, set [reportAllExceptions] to true. + Future? attemptLightweightAuthentication({ + bool reportAllExceptions = false, }) async { try { - return await _addMethodCall(GoogleSignInPlatform.instance.signInSilently, - canSkipCall: !reAuthenticate); - } catch (_) { - if (suppressErrors) { + final Future? future = + GoogleSignInPlatform.instance.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + if (future == null) { return null; - } else { - rethrow; } - } - } + final AuthenticationResults? result = await future; + if (result == null) { + return Future.value(); + } - /// Returns a future that resolves to whether a user is currently signed in. - Future isSignedIn() async { - await _ensureInitialized(); - return GoogleSignInPlatform.instance.isSignedIn(); + final GoogleSignInAccount account = + GoogleSignInAccount._(result.user, result.authenticationTokens); + if (_createAuthenticationStreamEvents) { + _authenticationStreamController + .add(GoogleSignInAuthenticationEventSignIn(user: account)); + } + return account; + } on GoogleSignInException catch (e) { + if (_createAuthenticationStreamEvents) { + _authenticationStreamController + .add(GoogleSignInAuthenticationEventException(e)); + } + + if (!reportAllExceptions) { + switch (e.code) { + case GoogleSignInExceptionCode.canceled: + case GoogleSignInExceptionCode.interrupted: + case GoogleSignInExceptionCode.uiUnavailable: + return null; + // Only specific types are ignored, everything else should rethrow. + // ignore: no_default_cases + default: + rethrow; + } + } + rethrow; + } } - /// Starts the interactive sign-in process. + /// Whether or not the current platform supports the [authenticate] method. /// - /// Returned Future resolves to an instance of [GoogleSignInAccount] for a - /// successful sign in or `null` in case sign in process was aborted. + /// If this returns false, [authenticate] will throw an UnsupportedError if + /// called. See the platform-specific documentation for the package to + /// determine how authentication his handled. For instance, the platform may + /// provide platform-controlled sign-in UI elements that must be used instead + /// of application-specific UI. + bool supportsAuthenticate() => + GoogleSignInPlatform.instance.supportsAuthenticate(); + + /// Starts an 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. + /// Returns a [GoogleSignInAccount] with valid authentication tokens for a + /// successful sign in, or throws a [GoogleSignInException] for any other + /// outcome, with details in the exception. /// - /// Re-authentication can be triggered only after [signOut] or [disconnect]. - Future signIn() { - final Future result = - _addMethodCall(GoogleSignInPlatform.instance.signIn, canSkipCall: true); - 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 signOut() => - _addMethodCall(GoogleSignInPlatform.instance.signOut); - - /// Disconnects the current user from the app and revokes previous - /// authentication. - Future disconnect() => - _addMethodCall(GoogleSignInPlatform.instance.disconnect); - - /// Requests the user grants additional Oauth [scopes]. - Future requestScopes(List scopes) async { - await _ensureInitialized(); - return GoogleSignInPlatform.instance.requestScopes(scopes); + /// If you will immediately be requesting authorization tokens, you can pass + /// [scopeHint] to indicate a preference for a combined + /// authentication+authorization flow on platforms that support it. Best + /// practice for Google Sign In flows is to separate authentication and + /// authorization, so not all platforms support a combined flow, and those + /// that do not will ignore [scopeHint]. You should always assume that + /// [GoogleSignInAuthorizationClient.authorizationForScopes] could return null + /// even if you pass a [scopeHint] here. + Future authenticate( + {List scopeHint = const []}) async { + try { + final AuthenticationResults result = await GoogleSignInPlatform.instance + .authenticate(AuthenticateParameters(scopeHint: scopeHint)); + final GoogleSignInAccount account = + GoogleSignInAccount._(result.user, result.authenticationTokens); + if (_createAuthenticationStreamEvents) { + _authenticationStreamController + .add(GoogleSignInAuthenticationEventSignIn(user: account)); + } + return account; + } on GoogleSignInException catch (e) { + if (_createAuthenticationStreamEvents) { + _authenticationStreamController + .add(GoogleSignInAuthenticationEventException(e)); + } + rethrow; + } } - /// Checks if the current user has granted access to all the specified [scopes]. + /// Returns a client that can be used to request authorization tokens for + /// some user. /// - /// Optionally, an [accessToken] can be passed to perform this check. This - /// may be useful when an application holds on to a cached, potentially - /// long-lived [accessToken]. - Future canAccessScopes( - List scopes, { - String? accessToken, - }) async { - await _ensureInitialized(); + /// In most cases, authorization tokens should be obtained via + /// [GoogleSignInAccount.authorizationClient] rather than this method, as this + /// will provied only authorization tokens, without any corresponding user + /// information or authentication tokens. + /// + /// See [GoogleSignInAuthorizationClient] for details. + GoogleSignInAuthorizationClient get authorizationClient { + return GoogleSignInAuthorizationClient._(null); + } - final String? token = - accessToken ?? (await _currentUser?.authentication)?.accessToken; + /// Signs out any currently signed in user(s). + Future signOut() { + if (_createAuthenticationStreamEvents) { + _authenticationStreamController + .add(GoogleSignInAuthenticationEventSignOut()); + } + return GoogleSignInPlatform.instance.signOut(const SignOutParams()); + } - return GoogleSignInPlatform.instance.canAccessScopes( - scopes, - accessToken: token, - ); + /// Disconnects any currently authorized users from the app, revoking previous + /// authorization. + Future disconnect() async { + // TODO(stuartmorgan): Consider making a per-user disconnect option once + // the Android implementation is available so that we can see how it is + // structured. In practice, currently the plugin only fully supports a + // single user at a time, so the distinction is mostly theoretical for now. + await GoogleSignInPlatform.instance.disconnect(const DisconnectParams()); } } diff --git a/packages/google_sign_in/google_sign_in/lib/src/event_types.dart b/packages/google_sign_in/google_sign_in/lib/src/event_types.dart new file mode 100644 index 00000000000..3c24d2f82df --- /dev/null +++ b/packages/google_sign_in/google_sign_in/lib/src/event_types.dart @@ -0,0 +1,47 @@ +// 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/foundation.dart'; + +import '../google_sign_in.dart'; + +export 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' + show GoogleSignInException; + +/// A base class for authentication event streams. +@immutable +sealed class GoogleSignInAuthenticationEvent { + const GoogleSignInAuthenticationEvent(); +} + +/// A sign-in event, corresponding to an authentication flow completing +/// successfully. +@immutable +class GoogleSignInAuthenticationEventSignIn + extends GoogleSignInAuthenticationEvent { + /// Creates an event for a successful sign in. + const GoogleSignInAuthenticationEventSignIn({required this.user}); + + /// The user that was authenticated. + final GoogleSignInAccount user; +} + +/// 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 GoogleSignInAuthenticationEventSignOut + extends GoogleSignInAuthenticationEvent {} + +/// An authentication failure that resulted in an exception. +@immutable +class GoogleSignInAuthenticationEventException + extends GoogleSignInAuthenticationEvent { + /// Creates an exception event. + const GoogleSignInAuthenticationEventException(this.exception); + + /// The exception thrown during authentication. + final GoogleSignInException exception; +} diff --git a/packages/google_sign_in/google_sign_in/lib/src/common.dart b/packages/google_sign_in/google_sign_in/lib/src/identity_types.dart similarity index 94% rename from packages/google_sign_in/google_sign_in/lib/src/common.dart rename to packages/google_sign_in/google_sign_in/lib/src/identity_types.dart index 8a1d4dcb354..068403e7462 100644 --- a/packages/google_sign_in/google_sign_in/lib/src/common.dart +++ b/packages/google_sign_in/google_sign_in/lib/src/identity_types.dart @@ -34,7 +34,4 @@ abstract class GoogleIdentity { /// /// Not guaranteed to be present for all users, even when configured. String? get photoUrl; - - /// Server auth code used to access Google Login - String? get serverAuthCode; } diff --git a/packages/google_sign_in/google_sign_in/lib/src/token_types.dart b/packages/google_sign_in/google_sign_in/lib/src/token_types.dart new file mode 100644 index 00000000000..79e05c8aec5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/lib/src/token_types.dart @@ -0,0 +1,57 @@ +// 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/foundation.dart'; + +/// Holds authentication tokens. +/// +/// Currently there is only an idToken, but this wrapper class allows for the +/// posibility of adding additional information in the future without breaking +/// changes. +@immutable +class GoogleSignInAuthentication { + /// Creates a new token container with the given tokens. + const GoogleSignInAuthentication({required this.idToken}); + + /// An OpenID Connect ID token that identifies the user. + final String? idToken; + + @override + String toString() => 'GoogleSignInAuthentication: $idToken'; +} + +/// Holds client authorization tokens. +/// +/// Currently there is only an accessToken, but this wrapper class allows for +/// the posibility of adding additional information in the future without +/// breaking changes. +@immutable +class GoogleSignInClientAuthorization { + /// Creates a new token container with the given tokens. + const GoogleSignInClientAuthorization({required this.accessToken}); + + /// The OAuth2 access token to access Google services. + final String accessToken; + + @override + String toString() => 'GoogleSignInClientAuthorization: $accessToken'; +} + +/// Holds server authorization tokens. +/// +/// Currently there is only a serverAuthCode, but this wrapper class allows for +/// the posibility of adding additional information in the future without +/// breaking changes. +@immutable +class GoogleSignInServerAuthorization { + /// Creates a new token container with the given tokens. + const GoogleSignInServerAuthorization({required this.serverAuthCode}); + + /// Auth code to provide to a backend server to exchange for access or + /// refresh tokens. + final String serverAuthCode; + + @override + String toString() => 'GoogleSignInServerAuthorization: $serverAuthCode'; +} diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index ab04de4319d..f5c210b7f57 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -6,8 +6,8 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; -import 'src/common.dart'; import 'src/fife.dart' as fife; +import 'src/identity_types.dart'; /// Builds a CircleAvatar profile image of the appropriate resolution class GoogleUserCircleAvatar extends StatelessWidget { diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 06858796ce8..8d4fcdc6dac 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -150,25 +150,26 @@ public void setActivity(@Nullable Activity activity) { return activity; } + @Override + public @Nullable String getGoogleServicesJsonServerClientId() { + @SuppressLint("DiscouragedApi") + int webClientIdIdentifier = + context + .getResources() + .getIdentifier("default_web_client_id", "string", context.getPackageName()); + if (webClientIdIdentifier != 0) { + return context.getString(webClientIdIdentifier); + } + return null; + } + @Override public void getCredential( @NonNull GetCredentialRequestParams params, @NonNull Function1, Unit> callback) { try { String serverClientId = params.getServerClientId(); - if (isNullOrEmpty(serverClientId)) { - // If the required server client ID wasn't explicitly provided, check whether it was in - // a google-services.json parsed by the google-services Gradle script. - @SuppressLint("DiscouragedApi") - int webClientIdIdentifier = - context - .getResources() - .getIdentifier("default_web_client_id", "string", context.getPackageName()); - if (webClientIdIdentifier != 0) { - serverClientId = context.getString(webClientIdIdentifier); - } - } - if (isNullOrEmpty(serverClientId)) { + if (serverClientId == null || serverClientId.isEmpty()) { ResultUtilsKt.completeWithGetCredentialFailure( callback, new GetCredentialFailure( @@ -297,7 +298,8 @@ public void authorize( params.getServerClientIdForForcedRefreshToken(), true); } if (params.getAccountEmail() != null) { - authorizationRequestBuilder.setAccount(new Account(params.getAccountEmail(), "com.google")); + authorizationRequestBuilder.setAccount( + new Account(params.getAccountEmail(), "com.google")); } AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build(); Identity.getAuthorizationClient(context) @@ -310,7 +312,8 @@ public void authorize( if (activity == null) { ResultUtilsKt.completeWithAuthorizeFailure( callback, - new AuthorizeFailure(AuthorizeFailureType.NO_ACTIVITY, null, null)); + new AuthorizeFailure( + AuthorizeFailureType.NO_ACTIVITY, "No activity available", null)); return; } // Prompt for access. `callback` will be resolved in onActivityResult. @@ -392,9 +395,5 @@ public boolean onActivityResult(int requestCode, int resultCode, @Nullable Inten } return false; } - - private static boolean isNullOrEmpty(@Nullable String s) { - return s == null || s.isEmpty(); - } } } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt index 009541c6842..5edd189f256 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v24.2.1), do not edit directly. +// Autogenerated from Pigeon (v24.2.2), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -417,6 +417,15 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface CredentialManagerApi { + /** + * Returns the server client ID parsed from google-services.json by the google-services Gradle + * script, if any. + * + * This is not part of CredentialManager, but is included here for convenience since + * CredentialManager requires a server client ID. + */ + fun getGoogleServicesJsonServerClientId(): String? + fun getCredential( params: GetCredentialRequestParams, callback: (Result) -> Unit @@ -439,6 +448,26 @@ interface CredentialManagerApi { ) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = + BasicMessageChannel( + binaryMessenger, + "dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getGoogleServicesJsonServerClientId$separatedMessageChannelSuffix", + codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + val wrapped: List = + try { + listOf(api.getGoogleServicesJsonServerClientId()) + } catch (exception: Throwable) { + wrapError(exception) + } + reply.reply(wrapped) + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel( diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 5798d77a681..fd7947acda4 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -3,24 +3,13 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'src/messages.g.dart'; -// These are magic string values that match the previous implementation on -// Android, docs in the app-facing package, and/or implementations on other -// platforms. -// TODO(stuartmorgan): Replace these with structured errors defined in the -// platform interface when reworking the API surface. -const String _errorCodeSignInCanceled = 'sign_in_canceled'; -const String _errorCodeSignInRequired = 'sign_in_required'; -const String _errorCodeSignInFailed = 'sign_in_failed'; -const String _errorCodeUserRecoverableAuth = 'user_recoverable_auth'; -const String _errorCodeIncorrectConfiguration = 'incorrect_configuration'; - /// Android implementation of [GoogleSignInPlatform]. class GoogleSignInAndroid extends GoogleSignInPlatform { /// Creates a new plugin implementation instance. @@ -37,9 +26,6 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { String? _serverClientId; String? _hostedDomain; - List _desiredScopes = []; - bool _forceCodeForRefreshToken = false; - String? _forcedAccountName; /// Registers this class as the default instance of [GoogleSignInPlatform]. static void registerWith() { @@ -47,68 +33,29 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { } @override - Future init({ - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String? hostedDomain, - String? clientId, - String? forceAccountName, - }) { - return initWithParams(SignInInitParameters( - signInOption: signInOption, - scopes: scopes, - hostedDomain: hostedDomain, - clientId: clientId, - forceAccountName: forceAccountName, - )); - } - - @override - Future initWithParams(SignInInitParameters params) async { - _desiredScopes = params.scopes; - _serverClientId = params.serverClientId; + Future init(InitParameters params) async { + _hostedDomain = params.hostedDomain; + _serverClientId = params.serverClientId ?? + await _credentialManaagerApi.getGoogleServicesJsonServerClientId(); // The clientId parameter is not supported on Android. // Android apps are identified by their package name and the SHA-1 of their signing key. - _hostedDomain = params.hostedDomain; - _forceCodeForRefreshToken = params.forceCodeForRefreshToken; - _forcedAccountName = params.forceAccountName; - // TODO(stuartmorgan): Consider adding a prepareGetCredentials call here. } @override - Future signInSilently() async { - // Attempt to authorize without user interaction. + Future attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) async { final PlatformGoogleIdTokenCredential? credential = await _authenticate( filterToAuthorized: true, autoSelectEnabled: true, ); - if (credential == null) { - return null; - } - - // For behavioral compatibility with the current plugin API, also attempt - // to authorize scopes silently. - // TODO(stuartmorgan): Restructure the plugin API to eliminate the need for - // this; see https://github.com/flutter/flutter/issues/119300. - final PlatformAuthorizationResult? authorization = await _authorize( - promptIfUnauthorized: false, - scopes: _desiredScopes, - accountEmail: _forcedAccountName); - if (authorization == null) { - return null; - } - - return GoogleSignInUserData( - email: credential.id, - id: credential.id, - idToken: credential.idToken, - serverAuthCode: authorization.serverAuthCode, - displayName: credential.displayName, - photoUrl: credential.profilePictureUri); + return credential == null + ? null + : _authenticationResultFromPlatformCredential(credential); } @override - Future signIn() async { + Future authenticate( + AuthenticateParameters params) async { // Attempt to authorize without user interaction. PlatformGoogleIdTokenCredential? credential = await _authenticate( filterToAuthorized: true, @@ -118,37 +65,56 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { credential ??= credential = await _authenticate( filterToAuthorized: false, autoSelectEnabled: false, - throwGoogleSignInCompatExceptions: true, + throwForNoAuth: true, ); + // It's not clear from the documentation if this can happen; if it does, + // no information is available if (credential == null) { - return null; + throw const GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: 'Authenticate returned no credential without an error'); } + return _authenticationResultFromPlatformCredential(credential); + } - // For behavioral compatibility with the current plugin API, also attempt - // to authorize scopes. - // TODO(stuartmorgan): Restructure the plugin API to eliminate the need for - // this; see https://github.com/flutter/flutter/issues/119300. - final PlatformAuthorizationResult? authorization = await _authorize( - promptIfUnauthorized: true, - scopes: _desiredScopes, - accountEmail: _forcedAccountName); - if (authorization == null) { - return null; - } + @override + Future signOut(SignOutParams params) { + return _credentialManaagerApi.clearCredentialState(); + } - return GoogleSignInUserData( - email: credential.id, - id: credential.id, - idToken: credential.idToken, - serverAuthCode: authorization.serverAuthCode, - displayName: credential.displayName, - photoUrl: credential.profilePictureUri); + @override + Future disconnect(DisconnectParams params) async { + // TODO(stuartmorgan): Implement this once Credential Manager adds the + // necessary API (or temporarily implement it with the deprecated SDK). + + await signOut(const SignOutParams()); + } + + @override + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params) async { + final (:String? accessToken, :String? serverAuthCode) = + await _authorize(params.request, requestOfflineAccess: false); + return accessToken == null + ? null + : ClientAuthorizationTokenData(accessToken: accessToken); } - Future _authenticate( - {required bool filterToAuthorized, - required bool autoSelectEnabled, - bool throwGoogleSignInCompatExceptions = false}) async { + @override + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params) async { + final (:String? accessToken, :String? serverAuthCode) = + await _authorize(params.request, requestOfflineAccess: true); + return serverAuthCode == null + ? null + : ServerAuthorizationTokenData(serverAuthCode: serverAuthCode); + } + + Future _authenticate({ + required bool filterToAuthorized, + required bool autoSelectEnabled, + bool throwForNoAuth = false, + }) async { final GetCredentialResult authnResult = await _credentialManaagerApi.getCredential(GetCredentialRequestParams( filterToAuthorized: filterToAuthorized, @@ -156,162 +122,134 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { serverClientId: _serverClientId)); switch (authnResult) { case GetCredentialFailure(): - if (throwGoogleSignInCompatExceptions) { - switch (authnResult.type) { - // Most failures don't map directly to an existing failure, so use - // the previous Google Sign-In's catch-all for most cases. - case GetCredentialFailureType.unexpectedCredentialType: - case GetCredentialFailureType.interrupted: - case GetCredentialFailureType.noCredential: - case GetCredentialFailureType.providerConfigurationIssue: - case GetCredentialFailureType.unsupported: - case GetCredentialFailureType.unknown: - throw PlatformException( - code: _errorCodeSignInFailed, message: authnResult.message); - case GetCredentialFailureType.canceled: - throw PlatformException( - code: _errorCodeSignInCanceled, message: authnResult.message); - case GetCredentialFailureType.missingServerClientId: - throw PlatformException( - code: _errorCodeIncorrectConfiguration, - message: 'serverClientId must be provided on Android'); - } + String? message = authnResult.message; + final GoogleSignInExceptionCode code; + switch (authnResult.type) { + case GetCredentialFailureType.noCredential: + if (throwForNoAuth) { + code = GoogleSignInExceptionCode.unknownError; + message = 'No credential available: $message'; + } else { + return null; + } + case GetCredentialFailureType.unexpectedCredentialType: + // This should not actually be possible in practice, so it is + // grouped under providerConfigurationError instead of given a + // distinct code. + code = GoogleSignInExceptionCode.providerConfigurationError; + message = 'Unexpected credential type: $message'; + case GetCredentialFailureType.interrupted: + code = GoogleSignInExceptionCode.interrupted; + case GetCredentialFailureType.providerConfigurationIssue: + code = GoogleSignInExceptionCode.providerConfigurationError; + case GetCredentialFailureType.unsupported: + code = GoogleSignInExceptionCode.providerConfigurationError; + message = 'Credential Manager not supported. $message'; + case GetCredentialFailureType.canceled: + code = GoogleSignInExceptionCode.canceled; + case GetCredentialFailureType.missingServerClientId: + code = GoogleSignInExceptionCode.clientConfigurationError; + message = 'serverClientId must be provided on Android'; + case GetCredentialFailureType.unknown: + code = GoogleSignInExceptionCode.unknownError; } - return null; + throw GoogleSignInException( + code: code, description: message, details: authnResult.details); case GetCredentialSuccess(): return authnResult.credential; } } - Future _authorize( - {required bool promptIfUnauthorized, - required List scopes, - String? accountEmail, - bool throwGoogleSignInCompatExceptions = false}) async { - final AuthorizeResult authzResult = await _authorizationClientApi.authorize( + Future<({String? accessToken, String? serverAuthCode})> _authorize( + AuthorizationRequestDetails request, + {required bool requestOfflineAccess}) async { + final AuthorizeResult result = await _authorizationClientApi.authorize( PlatformAuthorizationRequest( - scopes: scopes, - hostedDomain: _hostedDomain, - serverClientIdForForcedRefreshToken: - _forceCodeForRefreshToken ? _serverClientId : null, - accountEmail: accountEmail, - ), - promptIfUnauthorized: promptIfUnauthorized); - switch (authzResult) { + scopes: request.scopes, + accountEmail: request.email, + hostedDomain: _hostedDomain, + serverClientIdForForcedRefreshToken: + requestOfflineAccess ? _serverClientId : null), + promptIfUnauthorized: request.promptIfUnauthorized); + switch (result) { case AuthorizeFailure(): - if (throwGoogleSignInCompatExceptions) { - switch (authzResult.type) { - case AuthorizeFailureType.unauthorized: - // This is the closest error code in the legacy system, since it - // would be resolved by calling signIn if the user allowed access. - throw PlatformException( - code: _errorCodeSignInRequired, message: authzResult.message); - case AuthorizeFailureType.noActivity: - throw PlatformException( - code: _errorCodeUserRecoverableAuth, - message: authzResult.message); - // Map everything else to the catch-all error for now. - case AuthorizeFailureType.authorizeFailure: - case AuthorizeFailureType.pendingIntentException: - case AuthorizeFailureType.apiException: - throw PlatformException( - code: _errorCodeSignInFailed, message: authzResult.message); - } + final GoogleSignInExceptionCode code; + switch (result.type) { + case AuthorizeFailureType.unauthorized: + // This indicates that there was no existing authorization and + // prompting wasn't allowed, so just return null. + return (accessToken: null, serverAuthCode: null); + case AuthorizeFailureType.pendingIntentException: + code = GoogleSignInExceptionCode.canceled; + case AuthorizeFailureType.authorizeFailure: + case AuthorizeFailureType.apiException: + code = GoogleSignInExceptionCode.unknownError; + case AuthorizeFailureType.noActivity: + code = GoogleSignInExceptionCode.uiUnavailable; } - return null; + throw GoogleSignInException( + code: code, description: result.message, details: result.details); case PlatformAuthorizationResult(): - return authzResult; + final String? accessToken = result.accessToken; + if (accessToken == null) { + return (accessToken: null, serverAuthCode: null); + } + return ( + accessToken: accessToken, + serverAuthCode: result.serverAuthCode, + ); } } - @override - Future getTokens( - {required String email, bool? shouldRecoverAuth = true}) async { - final bool promptIfUnauthorized = shouldRecoverAuth ?? false; - // TODO(stuartmorgan): Eliminate or restructure this method in the new API, - // since it mixes tokens from different steps. - // See https://github.com/flutter/flutter/issues/119300. - final PlatformAuthorizationResult? authorization = await _authorize( - promptIfUnauthorized: promptIfUnauthorized, - scopes: _desiredScopes, - accountEmail: email, - throwGoogleSignInCompatExceptions: true); - if (authorization == null) { - // This is explicitly documented behavior in the app-facing package, - // unfortunately, so replicate it here. - throw PlatformException( - code: promptIfUnauthorized - ? 'failed_to_recover_auth' - : 'user_recoverable_auth'); - } - - return GoogleSignInTokenData( - // idToken isn't available here; the app-facing code already caches it - // for that reason, so for now just rely on that. After an API rework, - // that shouldn't be necessary. - accessToken: authorization.accessToken, - serverAuthCode: authorization.serverAuthCode, + AuthenticationResults _authenticationResultFromPlatformCredential( + PlatformGoogleIdTokenCredential credential) { + // GoogleIdTokenCredential's ID field is documented to return the + // email address, not what the other platform SDKs call an ID. + // The account ID returned by other platform SDKs and the legacy + // Google Sign In for Android SDK is no longer directly exposed, so it + // need to be extracted from the token. See + // https://stackoverflow.com/a/78064720. + // The ID should always be availabe from the token, but if for some reason + // it can't be extracted, use the email address instead as a reasonable + // fallback method of identifying the account. + final String email = credential.id; + final String userId = _idFromIdToken(credential.idToken) ?? email; + + return AuthenticationResults( + user: GoogleSignInUserData( + email: email, + id: userId, + displayName: credential.displayName, + photoUrl: credential.profilePictureUri), + authenticationTokens: + AuthenticationTokenData(idToken: credential.idToken), ); } +} - @override - Future signOut() { - return _credentialManaagerApi.clearCredentialState(); - } - - @override - Future disconnect() async { - // This was a Google Sign-In API that does not appear to have a Credential - // Manager equivalent; just sign out instead. - return signOut(); - } - - @override - Future isSignedIn() async { - // TODO(stuartmorgan): Eliminate or restructure this method in the new API, - // since this concept doesn't seem to exist any more. - // See https://github.com/flutter/flutter/issues/119300. - // For now, attempt a silent sign-in and see if it works. - return (await _authenticate( - filterToAuthorized: true, autoSelectEnabled: true)) != - null; - } - - @override - Future clearAuthCache({required String token}) async { - // This was a Google Sign-In API that does not appear to have a Credential - // Manager equivalent. - } - - @override - Future requestScopes(List scopes) async { - final AuthorizeResult result = await _authorizationClientApi.authorize( - PlatformAuthorizationRequest( - scopes: scopes, hostedDomain: _hostedDomain), - promptIfUnauthorized: true); - switch (result) { - case AuthorizeFailure(): - // TODO(stuartmorgan): Look into how failure should be communicated better. - return false; - case PlatformAuthorizationResult(): - return true; - } - } - - @override - Future canAccessScopes( - List scopes, { - String? accessToken, - }) async { - final AuthorizeResult result = await _authorizationClientApi.authorize( - PlatformAuthorizationRequest( - scopes: scopes, hostedDomain: _hostedDomain), - promptIfUnauthorized: false); - switch (result) { - case AuthorizeFailure(): - return false; - case PlatformAuthorizationResult(): - return true; +/// A codec that can encode/decode JWT payloads. +/// +/// See https://www.rfc-editor.org/rfc/rfc7519#section-3 +final Codec _jwtCodec = json.fuse(utf8).fuse(base64); + +/// Extracts the user ID from an idToken. +/// +/// See https://stackoverflow.com/a/78064720 +String? _idFromIdToken(String idToken) { + final RegExp jwtTokenRegexp = RegExp( + r'^(?
[^\.\s]+)\.(?[^\.\s]+)\.(?[^\.\s]+)$'); + final RegExpMatch? match = jwtTokenRegexp.firstMatch(idToken); + final String? payload = match?.namedGroup('payload'); + if (payload != null) { + try { + final Map? contents = + _jwtCodec.decode(base64.normalize(payload)) as Map?; + if (contents != null) { + return contents['sub'] as String?; + } + } catch (_) { + return null; } } + return null; } diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart index 71602bb5305..6ffff824565 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v24.2.1), do not edit directly. +// Autogenerated from Pigeon (v24.2.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -412,6 +412,36 @@ class CredentialManagerApi { final String pigeonVar_messageChannelSuffix; + /// Returns the server client ID parsed from google-services.json by the + /// google-services Gradle script, if any. + /// + /// This is not part of CredentialManager, but is included here for + /// convenience since CredentialManager requires a server client ID. + Future getGoogleServicesJsonServerClientId() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getGoogleServicesJsonServerClientId$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return (pigeonVar_replyList[0] as String?); + } + } + Future getCredential( GetCredentialRequestParams params) async { final String pigeonVar_channelName = diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart index e97d75a42a7..3b2e4fb2f3c 100644 --- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -154,6 +154,13 @@ class PlatformAuthorizationResult extends AuthorizeResult { @HostApi() abstract class CredentialManagerApi { + /// Returns the server client ID parsed from google-services.json by the + /// google-services Gradle script, if any. + /// + /// This is not part of CredentialManager, but is included here for + /// convenience since CredentialManager requires a server client ID. + String? getGoogleServicesJsonServerClientId(); + @async GetCredentialResult getCredential(GetCredentialRequestParams params); diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m index 79b80778880..e23b5d216d3 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m @@ -14,7 +14,7 @@ static NSString *const kServerClientIdKey = @"SERVER_CLIENT_ID"; -static NSDictionary *loadGoogleServiceInfo(void) { +static NSDictionary *FSILoadGoogleServiceInfo(void) { NSString *plistPath = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; if (plistPath) { @@ -23,35 +23,85 @@ return nil; } -// These error codes must match with ones declared on Android and Dart sides. -static NSString *const kErrorReasonSignInRequired = @"sign_in_required"; -static NSString *const kErrorReasonSignInCanceled = @"sign_in_canceled"; -static NSString *const kErrorReasonNetworkError = @"network_error"; -static NSString *const kErrorReasonSignInFailed = @"sign_in_failed"; - -static FlutterError *getFlutterError(NSError *error) { - NSString *errorCode; - if (error.code == kGIDSignInErrorCodeHasNoAuthInKeychain) { - errorCode = kErrorReasonSignInRequired; - } else if (error.code == kGIDSignInErrorCodeCanceled) { - errorCode = kErrorReasonSignInCanceled; - } else if ([error.domain isEqualToString:NSURLErrorDomain]) { - errorCode = kErrorReasonNetworkError; +/// Deep-converts values to something that can be safely encoded with the standard message codec, +/// for use in making NSError userInfo values safe to send as FlutterError details. +/// +/// Unexpected types are converted to a +static id FSISanitizedUserInfo(id value) { + if ([value isKindOfClass:[NSError class]]) { + NSError *error = value; + return @{ + @"domain" : error.domain, + @"code" : [NSString stringWithFormat:@"%d", error.code], + @"localizedDescription" : error.localizedDescription, + @"userInfo" : FSISanitizedUserInfo(error.userInfo), + }; + } else if ([value isKindOfClass:[NSString class]]) { + return value; + } else if ([value isKindOfClass:[NSURL class]]) { + return [value absoluteString]; + } else if ([value isKindOfClass:[NSNumber class]]) { + return value; + } else if ([value isKindOfClass:[NSArray class]]) { + NSArray *array = value; + NSMutableArray *safeValues = [[NSMutableArray alloc] initWithCapacity:array.count]; + for (id item in array) { + [safeValues addObject:FSISanitizedUserInfo(item)]; + } + return safeValues; + } else if ([value isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = value; + NSMutableDictionary *safeValues = [[NSMutableDictionary alloc] initWithCapacity:dict.count]; + for (id key in dict) { + safeValues[key] = FSISanitizedUserInfo(dict[key]); + } + return safeValues; } else { - errorCode = kErrorReasonSignInFailed; + return [NSString stringWithFormat:@"[Unsupported type: %@]", NSStringFromClass([value class])]; + } +} + +/// Maps an NSError to a corresponding FlutterError. +/// +/// This should only be used when an error can't be recognized and mapped to a +/// GoogleSignInErrorCode. +static FlutterError *FSIFlutterErrorForNSError(NSError *error) { + return [FlutterError errorWithCode:[NSString stringWithFormat:@"%@: %d", error.domain, error.code] + message:error.localizedDescription + details:FSISanitizedUserInfo(error.userInfo)]; +} + +/// Maps a GIDSignInErrorCode to the corresponding Pigeon GoogleSignInErrorCode +static FSIGoogleSignInErrorCode FSIPigeonErrorCodeForGIDSignInErrorCode(NSInteger code) { + switch (code) { + case kGIDSignInErrorCodeKeychain: + return FSIGoogleSignInErrorCodeKeychainError; + case kGIDSignInErrorCodeCanceled: + return FSIGoogleSignInErrorCodeCanceled; + case kGIDSignInErrorCodeHasNoAuthInKeychain: + return FSIGoogleSignInErrorCodeNoAuthInKeychain; + case kGIDSignInErrorCodeEMM: + return FSIGoogleSignInErrorCodeEemError; + case kGIDSignInErrorCodeScopesAlreadyGranted: + return FSIGoogleSignInErrorCodeScopesAlreadyGranted; + case kGIDSignInErrorCodeMismatchWithCurrentUser: + return FSIGoogleSignInErrorCodeUserMismatch; + case kGIDSignInErrorCodeUnknown: + default: + return FSIGoogleSignInErrorCodeUnknown; } - return [FlutterError errorWithCode:errorCode - message:error.domain - details:error.localizedDescription]; } @interface FLTGoogleSignInPlugin () // The contents of GoogleService-Info.plist, if it exists. -@property(strong, nullable) NSDictionary *googleServiceProperties; +@property(nonatomic, nullable) NSDictionary *googleServiceProperties; // The plugin registrar, for querying views. -@property(strong, nonnull) id registrar; +@property(nonatomic) id registrar; + +// A mapping of user IDs to GIDGoogleUser instances to use for follow-up calls. +@property(nonatomic) NSMutableDictionary *usersByIdentifier; @end @@ -60,7 +110,7 @@ @implementation FLTGoogleSignInPlugin + (void)registerWithRegistrar:(NSObject *)registrar { FLTGoogleSignInPlugin *instance = [[FLTGoogleSignInPlugin alloc] initWithRegistrar:registrar]; [registrar addApplicationDelegate:instance]; - FSIGoogleSignInApiSetup(registrar.messenger, instance); + SetUpFSIGoogleSignInApi(registrar.messenger, instance); } - (instancetype)initWithRegistrar:(NSObject *)registrar { @@ -71,7 +121,7 @@ - (instancetype)initWithSignIn:(GIDSignIn *)signIn registrar:(NSObject *)registrar { return [self initWithSignIn:signIn registrar:registrar - googleServiceProperties:loadGoogleServiceInfo()]; + googleServiceProperties:FSILoadGoogleServiceInfo()]; } - (instancetype)initWithSignIn:(GIDSignIn *)signIn @@ -82,11 +132,11 @@ - (instancetype)initWithSignIn:(GIDSignIn *)signIn _signIn = signIn; _registrar = registrar; _googleServiceProperties = googleServiceProperties; + _usersByIdentifier = [[NSMutableDictionary alloc] init]; // On the iOS simulator, we get "Broken pipe" errors after sign-in for some // unknown reason. We can avoid crashing the app by ignoring them. signal(SIGPIPE, SIG_IGN); - _requestedScopes = [[NSSet alloc] init]; } return self; } @@ -101,7 +151,7 @@ - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDiction - (BOOL)handleOpenURLs:(NSArray *)urls { BOOL handled = NO; for (NSURL *url in urls) { - handled = handled || [self.signIn handleURL:url]; + handled = [self.signIn handleURL:url] || handled; } return handled; } @@ -109,62 +159,40 @@ - (BOOL)handleOpenURLs:(NSArray *)urls { #pragma mark - FSIGoogleSignInApi -- (void)initializeSignInWithParameters:(nonnull FSIInitParams *)params - error:(FlutterError *_Nullable __autoreleasing *_Nonnull)error { +- (void)configureWithParameters:(FSIPlatformConfigurationParams *)params + error:(FlutterError *_Nullable *_Nonnull)error { + // If configuration information was passed from Dart, or present in GoogleService-Info.plist, + // use that. Otherwise, keep the default configuration, which GIDSignIn will automatically + // populate from Info.plist values (the recommended configuration method). GIDConfiguration *configuration = [self configurationWithClientIdentifier:params.clientId serverClientIdentifier:params.serverClientId hostedDomain:params.hostedDomain]; - self.requestedScopes = [NSSet setWithArray:params.scopes]; - if (configuration != nil) { - self.configuration = configuration; + if (configuration) { + self.signIn.configuration = configuration; } } -- (void)signInSilentlyWithCompletion:(nonnull void (^)(FSIUserData *_Nullable, - FlutterError *_Nullable))completion { +- (void)restorePreviousSignInWithCompletion:(nonnull void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion { + __weak typeof(self) weakSelf = self; [self.signIn restorePreviousSignInWithCompletion:^(GIDGoogleUser *_Nullable user, NSError *_Nullable error) { - if (user != nil) { - [self didSignInForUser:user withServerAuthCode:nil completion:completion]; - } else { - // Forward all errors and let Dart side decide how to handle. - completion(nil, getFlutterError(error)); - } + [weakSelf handleAuthResultWithUser:user serverAuthCode:nil error:error completion:completion]; }]; } -- (nullable NSNumber *)isSignedInWithError: - (FlutterError *_Nullable __autoreleasing *_Nonnull)error { - return @([self.signIn hasPreviousSignIn]); -} - -- (void)signInWithCompletion:(nonnull void (^)(FSIUserData *_Nullable, - FlutterError *_Nullable))completion { +- (void)signInWithScopeHint:(NSArray *)scopeHint + completion:(nonnull void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion { @try { - // If the configuration settings are passed from the Dart API, use those. - // Otherwise, use settings from the GoogleService-Info.plist if available. - // If neither are available, do not set the configuration - GIDSignIn will automatically use - // settings from the Info.plist (which is the recommended method). - if (!self.configuration && self.googleServiceProperties) { - self.configuration = [self configurationWithClientIdentifier:nil - serverClientIdentifier:nil - hostedDomain:nil]; - } - if (self.configuration) { - self.signIn.configuration = self.configuration; - } - + __weak typeof(self) weakSelf = self; [self signInWithHint:nil - additionalScopes:self.requestedScopes.allObjects + additionalScopes:scopeHint completion:^(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error) { - if (signInResult) { - [self didSignInForUser:signInResult.user - withServerAuthCode:signInResult.serverAuthCode - completion:completion]; - } else { - // Forward all errors and let Dart side decide how to handle. - completion(nil, getFlutterError(error)); - } + [weakSelf handleAuthResultWithUser:signInResult.user + serverAuthCode:signInResult.serverAuthCode + error:error + completion:completion]; }]; } @catch (NSException *e) { completion(nil, [FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); @@ -172,79 +200,81 @@ - (void)signInWithCompletion:(nonnull void (^)(FSIUserData *_Nullable, } } -- (void)getAccessTokenWithCompletion:(nonnull void (^)(FSITokenData *_Nullable, - FlutterError *_Nullable))completion { - GIDGoogleUser *currentUser = self.signIn.currentUser; - [currentUser refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *_Nullable user, - NSError *_Nullable error) { - if (error) { - completion(nil, getFlutterError(error)); - } else { - completion([FSITokenData makeWithIdToken:user.idToken.tokenString - accessToken:user.accessToken.tokenString], - nil); - } +- (void)getRefreshedAuthorizationTokensForUser:(NSString *)userId + completion: + (nonnull void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion { + GIDGoogleUser *user = self.usersByIdentifier[userId]; + if (user == nil) { + completion( + [FSISignInResult + makeWithSuccess:nil + error:[FSISignInFailure makeWithType:FSIGoogleSignInErrorCodeUserMismatch + message:@"The user is no longer signed in." + details:nil]], + nil); + return; + } + + __weak typeof(self) weakSelf = self; + [user refreshTokensIfNeededWithCompletion:^(GIDGoogleUser *_Nullable refreshedUser, + NSError *_Nullable error) { + [weakSelf handleAuthResultWithUser:refreshedUser + serverAuthCode:nil + error:error + completion:completion]; }]; } +- (void)addScopes:(nonnull NSArray *)scopes + forUser:(NSString *)userId + completion: + (nonnull void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion { + GIDGoogleUser *user = self.usersByIdentifier[userId]; + if (user == nil) { + completion( + [FSISignInResult + makeWithSuccess:nil + error:[FSISignInFailure makeWithType:FSIGoogleSignInErrorCodeUserMismatch + message:@"The user is no longer signed in." + details:nil]], + nil); + return; + } + + @try { + __weak typeof(self) weakSelf = self; + [self addScopes:scopes + forGoogleSignInUser:user + completion:^(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error) { + [weakSelf handleAuthResultWithUser:signInResult.user + serverAuthCode:signInResult.serverAuthCode + error:error + completion:completion]; + }]; + } @catch (NSException *e) { + completion(nil, [FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); + } +} + - (void)signOutWithError:(FlutterError *_Nullable *_Nonnull)error { [self.signIn signOut]; + [self.usersByIdentifier removeAllObjects]; } - (void)disconnectWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { [self.signIn disconnectWithCompletion:^(NSError *_Nullable error) { - // TODO(stuartmorgan): This preserves the pre-Pigeon-migration behavior, but it's unclear why - // 'error' is being ignored here. - completion(nil); + completion(error ? FSIFlutterErrorForNSError(error) : nil); }]; } -- (void)requestScopes:(nonnull NSArray *)scopes - completion:(nonnull void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion { - self.requestedScopes = [self.requestedScopes setByAddingObjectsFromArray:scopes]; - NSSet *requestedScopes = self.requestedScopes; - - @try { - GIDGoogleUser *currentUser = self.signIn.currentUser; - if (currentUser == nil) { - completion(nil, [FlutterError errorWithCode:@"sign_in_required" - message:@"No account to grant scopes." - details:nil]); - } - [self addScopes:requestedScopes.allObjects - completion:^(GIDSignInResult *_Nullable signInResult, NSError *_Nullable addedScopeError) { - BOOL granted = NO; - FlutterError *error = nil; - - if ([addedScopeError.domain isEqualToString:kGIDSignInErrorDomain] && - addedScopeError.code == kGIDSignInErrorCodeMismatchWithCurrentUser) { - error = [FlutterError errorWithCode:@"mismatch_user" - message:@"There is an operation on a previous " - @"user. Try signing in again." - details:nil]; - } else if ([addedScopeError.domain isEqualToString:kGIDSignInErrorDomain] && - addedScopeError.code == kGIDSignInErrorCodeScopesAlreadyGranted) { - // Scopes already granted, report success. - granted = YES; - } else if (signInResult.user) { - NSSet *grantedScopes = - [NSSet setWithArray:signInResult.user.grantedScopes]; - granted = [requestedScopes isSubsetOfSet:grantedScopes]; - } - completion(error == nil ? @(granted) : nil, error); - }]; - } @catch (NSException *e) { - completion(nil, [FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); - } -} - #pragma mark - private methods // Wraps the iOS and macOS sign in display methods. - (void)signInWithHint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes - completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, - NSError *_Nullable error))completion { + completion:(void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion { #if TARGET_OS_OSX [self.signIn signInWithPresentingWindow:self.registrar.view.window hint:hint @@ -260,13 +290,13 @@ - (void)signInWithHint:(nullable NSString *)hint // Wraps the iOS and macOS scope addition methods. - (void)addScopes:(NSArray *)scopes - completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, - NSError *_Nullable error))completion { - GIDGoogleUser *currentUser = self.signIn.currentUser; + forGoogleSignInUser:(GIDGoogleUser *)user + completion:(void (^)(GIDSignInResult *_Nullable signInResult, + NSError *_Nullable error))completion { #if TARGET_OS_OSX - [currentUser addScopes:scopes presentingWindow:self.registrar.view.window completion:completion]; + [user addScopes:scopes presentingWindow:self.registrar.view.window completion:completion]; #else - [currentUser addScopes:scopes + [user addScopes:scopes presentingViewController:[self topViewController] completion:completion]; #endif @@ -291,23 +321,54 @@ - (GIDConfiguration *)configurationWithClientIdentifier:(NSString *)runtimeClien openIDRealm:nil]; } +- (void)handleAuthResultWithUser:(nullable GIDGoogleUser *)user + serverAuthCode:(nullable NSString *)serverAuthCode + error:(nullable NSError *)error + completion:(void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion { + if (user) { + [self didSignInForUser:user withServerAuthCode:serverAuthCode completion:completion]; + } else { + // Convert expected errors into structured failure return, and everything else + // into a generic error. + if (error.domain == kGIDSignInErrorDomain) { + completion( + [FSISignInResult + makeWithSuccess:nil + error:[FSISignInFailure + makeWithType:FSIPigeonErrorCodeForGIDSignInErrorCode(error.code) + message:error.localizedDescription + details:FSISanitizedUserInfo(error.userInfo)]], + nil); + } else { + completion(nil, FSIFlutterErrorForNSError(error)); + } + } +} + - (void)didSignInForUser:(GIDGoogleUser *)user - withServerAuthCode:(NSString *_Nullable)serverAuthCode - completion: - (nonnull void (^)(FSIUserData *_Nullable, FlutterError *_Nullable))completion { - NSURL *photoUrl; + withServerAuthCode:(nullable NSString *)serverAuthCode + completion:(void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion { + self.usersByIdentifier[user.userID] = user; + + NSURL *photoURL; if (user.profile.hasImage) { // Placeholder that will be replaced by on the Dart side based on screen size. - photoUrl = [user.profile imageURLWithDimension:1337]; + photoURL = [user.profile imageURLWithDimension:1337]; } - completion([FSIUserData makeWithDisplayName:user.profile.name - email:user.profile.email - userId:user.userID - photoUrl:photoUrl.absoluteString - serverAuthCode:serverAuthCode - idToken:user.idToken.tokenString], - nil); + FSIUserData *userData = [FSIUserData makeWithDisplayName:user.profile.name + email:user.profile.email + userId:user.userID + photoUrl:photoURL.absoluteString + idToken:user.idToken.tokenString]; + FSISignInResult *result = + [FSISignInResult makeWithSuccess:[FSISignInSuccess makeWithUser:userData + accessToken:user.accessToken.tokenString + grantedScopes:user.grantedScopes + serverAuthCode:serverAuthCode] + error:nil]; + completion(result, nil); } #if TARGET_OS_IOS diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h index b145d028ad9..48918e552fb 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h @@ -15,16 +15,6 @@ NS_ASSUME_NONNULL_BEGIN /// Methods exposed for unit testing. @interface FLTGoogleSignInPlugin () -// Configuration wrapping Google Cloud Console, Google Apps, OpenID, -// and other initialization metadata. -@property(strong) GIDConfiguration *configuration; - -// Permissions requested during at sign in "init" method call -// unioned with scopes requested later with incremental authorization -// "requestScopes" method call. -// The "email" and "profile" base scopes are always implicitly requested. -@property(copy) NSSet *requestedScopes; - // Instance used to manage Google Sign In authentication including // sign in, sign out, and requesting additional scopes. @property(strong, readonly) GIDSignIn *signIn; diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h index 745c1ec9180..b12ff4e3698 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v11.0.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @@ -13,29 +13,55 @@ NS_ASSUME_NONNULL_BEGIN -@class FSIInitParams; +/// Enum mapping of known codes from +/// https://developers.google.com/identity/sign-in/ios/reference/Enums/GIDSignInErrorCode +typedef NS_ENUM(NSUInteger, FSIGoogleSignInErrorCode) { + /// Either the underlying kGIDSignInErrorCodeUnknown, or a code that isn't + /// a known code mapped to a value below. + FSIGoogleSignInErrorCodeUnknown = 0, + /// kGIDSignInErrorCodeKeychain; an error reading or writing to keychain. + FSIGoogleSignInErrorCodeKeychainError = 1, + /// kGIDSignInErrorCodeHasNoAuthInKeychain; no auth present in the keychain. + /// + /// For restorePreviousSignIn, this indicates that there is no sign in to + /// restore. + FSIGoogleSignInErrorCodeNoAuthInKeychain = 2, + /// kGIDSignInErrorCodeCanceled; the request was canceled by the user. + FSIGoogleSignInErrorCodeCanceled = 3, + /// kGIDSignInErrorCodeEMM; an enterprise management error occurred. + FSIGoogleSignInErrorCodeEemError = 4, + /// kGIDSignInErrorCodeScopesAlreadyGranted; the requested scopes have already + /// been granted. + FSIGoogleSignInErrorCodeScopesAlreadyGranted = 5, + /// kGIDSignInErrorCodeMismatchWithCurrentUser; an operation was requested on + /// a non-current user. + FSIGoogleSignInErrorCodeUserMismatch = 6, +}; + +/// Wrapper for FSIGoogleSignInErrorCode to allow for nullability. +@interface FSIGoogleSignInErrorCodeBox : NSObject +@property(nonatomic, assign) FSIGoogleSignInErrorCode value; +- (instancetype)initWithValue:(FSIGoogleSignInErrorCode)value; +@end + +@class FSIPlatformConfigurationParams; @class FSIUserData; -@class FSITokenData; +@class FSISignInResult; +@class FSISignInFailure; +@class FSISignInSuccess; -/// Pigeon version of SignInInitParams. -/// -/// See SignInInitParams for details. -@interface FSIInitParams : NSObject -/// `init` unavailable to enforce nonnull fields, see the `make` class method. -- (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithScopes:(NSArray *)scopes - hostedDomain:(nullable NSString *)hostedDomain - clientId:(nullable NSString *)clientId - serverClientId:(nullable NSString *)serverClientId; -@property(nonatomic, strong) NSArray *scopes; -@property(nonatomic, copy, nullable) NSString *hostedDomain; +@interface FSIPlatformConfigurationParams : NSObject ++ (instancetype)makeWithClientId:(nullable NSString *)clientId + serverClientId:(nullable NSString *)serverClientId + hostedDomain:(nullable NSString *)hostedDomain; @property(nonatomic, copy, nullable) NSString *clientId; @property(nonatomic, copy, nullable) NSString *serverClientId; +@property(nonatomic, copy, nullable) NSString *hostedDomain; @end -/// Pigeon version of GoogleSignInUserData. +/// Pigeon version of GoogleSignInUserData + AuthenticationTokenData. /// -/// See GoogleSignInUserData for details. +/// See GoogleSignInUserData and AuthenticationTokenData for details. @interface FSIUserData : NSObject /// `init` unavailable to enforce nonnull fields, see the `make` class method. - (instancetype)init NS_UNAVAILABLE; @@ -43,55 +69,96 @@ NS_ASSUME_NONNULL_BEGIN email:(NSString *)email userId:(NSString *)userId photoUrl:(nullable NSString *)photoUrl - serverAuthCode:(nullable NSString *)serverAuthCode idToken:(nullable NSString *)idToken; @property(nonatomic, copy, nullable) NSString *displayName; @property(nonatomic, copy) NSString *email; @property(nonatomic, copy) NSString *userId; @property(nonatomic, copy, nullable) NSString *photoUrl; -@property(nonatomic, copy, nullable) NSString *serverAuthCode; @property(nonatomic, copy, nullable) NSString *idToken; @end -/// Pigeon version of GoogleSignInTokenData. +/// The response from an auth call. +@interface FSISignInResult : NSObject ++ (instancetype)makeWithSuccess:(nullable FSISignInSuccess *)success + error:(nullable FSISignInFailure *)error; +/// The success result, if any. /// -/// See GoogleSignInTokenData for details. -@interface FSITokenData : NSObject -+ (instancetype)makeWithIdToken:(nullable NSString *)idToken - accessToken:(nullable NSString *)accessToken; -@property(nonatomic, copy, nullable) NSString *idToken; -@property(nonatomic, copy, nullable) NSString *accessToken; +/// Exactly one of success and error will be non-nil. +@property(nonatomic, strong, nullable) FSISignInSuccess *success; +/// The error result, if any. +/// +/// Exactly one of success and error will be non-nil. +@property(nonatomic, strong, nullable) FSISignInFailure *error; @end -/// The codec used by FSIGoogleSignInApi. -NSObject *FSIGoogleSignInApiGetCodec(void); +/// An sign in failure. +@interface FSISignInFailure : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithType:(FSIGoogleSignInErrorCode)type + message:(nullable NSString *)message + details:(nullable id)details; +/// The type of failure. +@property(nonatomic, assign) FSIGoogleSignInErrorCode type; +/// The message associated with the failure, if any. +@property(nonatomic, copy, nullable) NSString *message; +/// Extra details about the failure, if any. +@property(nonatomic, strong, nullable) id details; +@end + +/// A successful auth result. +/// +/// Corresponds to the information in a native GIDSignInResult. Because of the +/// structure of the Google Sign In SDK, this has information corresponding to +/// both authn and authz steps, even though incremental authorization is +/// supported. +@interface FSISignInSuccess : NSObject +/// `init` unavailable to enforce nonnull fields, see the `make` class method. +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)makeWithUser:(FSIUserData *)user + accessToken:(NSString *)accessToken + grantedScopes:(NSArray *)grantedScopes + serverAuthCode:(nullable NSString *)serverAuthCode; +@property(nonatomic, strong) FSIUserData *user; +@property(nonatomic, copy) NSString *accessToken; +@property(nonatomic, copy) NSArray *grantedScopes; +@property(nonatomic, copy, nullable) NSString *serverAuthCode; +@end + +/// The codec used by all APIs. +NSObject *FSIGetMessagesCodec(void); @protocol FSIGoogleSignInApi -/// Initializes a sign in request with the given parameters. -- (void)initializeSignInWithParameters:(FSIInitParams *)params - error:(FlutterError *_Nullable *_Nonnull)error; -/// Starts a silent sign in. -- (void)signInSilentlyWithCompletion:(void (^)(FSIUserData *_Nullable, - FlutterError *_Nullable))completion; +/// Configures the sign in object with application-level parameters. +- (void)configureWithParameters:(FSIPlatformConfigurationParams *)params + error:(FlutterError *_Nullable *_Nonnull)error; +/// Attempts to restore an existing sign-in, if any, with minimal user +/// interaction. +- (void)restorePreviousSignInWithCompletion:(void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion; /// Starts a sign in with user interaction. -- (void)signInWithCompletion:(void (^)(FSIUserData *_Nullable, FlutterError *_Nullable))completion; +- (void)signInWithScopeHint:(NSArray *)scopeHint + completion: + (void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion; /// Requests the access token for the current sign in. -- (void)getAccessTokenWithCompletion:(void (^)(FSITokenData *_Nullable, - FlutterError *_Nullable))completion; +- (void)getRefreshedAuthorizationTokensForUser:(NSString *)userId + completion:(void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion; +/// Requests authorization of the given additional scopes. +- (void)addScopes:(NSArray *)scopes + forUser:(NSString *)userId + completion:(void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion; /// Signs out the current user. - (void)signOutWithError:(FlutterError *_Nullable *_Nonnull)error; /// Revokes scope grants to the application. - (void)disconnectWithCompletion:(void (^)(FlutterError *_Nullable))completion; -/// Returns whether the user is currently signed in. -/// -/// @return `nil` only when `error != nil`. -- (nullable NSNumber *)isSignedInWithError:(FlutterError *_Nullable *_Nonnull)error; -/// Requests access to the given scopes. -- (void)requestScopes:(NSArray *)scopes - completion:(void (^)(NSNumber *_Nullable, FlutterError *_Nullable))completion; @end -extern void FSIGoogleSignInApiSetup(id binaryMessenger, +extern void SetUpFSIGoogleSignInApi(id binaryMessenger, NSObject *_Nullable api); +extern void SetUpFSIGoogleSignInApiWithSuffix(id binaryMessenger, + NSObject *_Nullable api, + NSString *messageChannelSuffix); + NS_ASSUME_NONNULL_END diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m index 2ec6ea32d0e..cdacda48fcf 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m @@ -1,7 +1,7 @@ // 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. -// Autogenerated from Pigeon (v11.0.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "./include/google_sign_in_ios/messages.g.h" @@ -16,7 +16,7 @@ #error File requires ARC to be enabled. #endif -static NSArray *wrapResult(id result, FlutterError *error) { +static NSArray *wrapResult(id result, FlutterError *error) { if (error) { return @[ error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] @@ -24,59 +24,79 @@ } return @[ result ?: [NSNull null] ]; } -static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { + +static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { id result = array[key]; return (result == [NSNull null]) ? nil : result; } -@interface FSIInitParams () -+ (FSIInitParams *)fromList:(NSArray *)list; -+ (nullable FSIInitParams *)nullableFromList:(NSArray *)list; -- (NSArray *)toList; +/// Enum mapping of known codes from +/// https://developers.google.com/identity/sign-in/ios/reference/Enums/GIDSignInErrorCode +@implementation FSIGoogleSignInErrorCodeBox +- (instancetype)initWithValue:(FSIGoogleSignInErrorCode)value { + self = [super init]; + if (self) { + _value = value; + } + return self; +} +@end + +@interface FSIPlatformConfigurationParams () ++ (FSIPlatformConfigurationParams *)fromList:(NSArray *)list; ++ (nullable FSIPlatformConfigurationParams *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end @interface FSIUserData () -+ (FSIUserData *)fromList:(NSArray *)list; -+ (nullable FSIUserData *)nullableFromList:(NSArray *)list; -- (NSArray *)toList; ++ (FSIUserData *)fromList:(NSArray *)list; ++ (nullable FSIUserData *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end -@interface FSITokenData () -+ (FSITokenData *)fromList:(NSArray *)list; -+ (nullable FSITokenData *)nullableFromList:(NSArray *)list; -- (NSArray *)toList; +@interface FSISignInResult () ++ (FSISignInResult *)fromList:(NSArray *)list; ++ (nullable FSISignInResult *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; @end -@implementation FSIInitParams -+ (instancetype)makeWithScopes:(NSArray *)scopes - hostedDomain:(nullable NSString *)hostedDomain - clientId:(nullable NSString *)clientId - serverClientId:(nullable NSString *)serverClientId { - FSIInitParams *pigeonResult = [[FSIInitParams alloc] init]; - pigeonResult.scopes = scopes; - pigeonResult.hostedDomain = hostedDomain; +@interface FSISignInFailure () ++ (FSISignInFailure *)fromList:(NSArray *)list; ++ (nullable FSISignInFailure *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@interface FSISignInSuccess () ++ (FSISignInSuccess *)fromList:(NSArray *)list; ++ (nullable FSISignInSuccess *)nullableFromList:(NSArray *)list; +- (NSArray *)toList; +@end + +@implementation FSIPlatformConfigurationParams ++ (instancetype)makeWithClientId:(nullable NSString *)clientId + serverClientId:(nullable NSString *)serverClientId + hostedDomain:(nullable NSString *)hostedDomain { + FSIPlatformConfigurationParams *pigeonResult = [[FSIPlatformConfigurationParams alloc] init]; pigeonResult.clientId = clientId; pigeonResult.serverClientId = serverClientId; + pigeonResult.hostedDomain = hostedDomain; return pigeonResult; } -+ (FSIInitParams *)fromList:(NSArray *)list { - FSIInitParams *pigeonResult = [[FSIInitParams alloc] init]; - pigeonResult.scopes = GetNullableObjectAtIndex(list, 0); - NSAssert(pigeonResult.scopes != nil, @""); - pigeonResult.hostedDomain = GetNullableObjectAtIndex(list, 1); - pigeonResult.clientId = GetNullableObjectAtIndex(list, 2); - pigeonResult.serverClientId = GetNullableObjectAtIndex(list, 3); ++ (FSIPlatformConfigurationParams *)fromList:(NSArray *)list { + FSIPlatformConfigurationParams *pigeonResult = [[FSIPlatformConfigurationParams alloc] init]; + pigeonResult.clientId = GetNullableObjectAtIndex(list, 0); + pigeonResult.serverClientId = GetNullableObjectAtIndex(list, 1); + pigeonResult.hostedDomain = GetNullableObjectAtIndex(list, 2); return pigeonResult; } -+ (nullable FSIInitParams *)nullableFromList:(NSArray *)list { - return (list) ? [FSIInitParams fromList:list] : nil; ++ (nullable FSIPlatformConfigurationParams *)nullableFromList:(NSArray *)list { + return (list) ? [FSIPlatformConfigurationParams fromList:list] : nil; } -- (NSArray *)toList { +- (NSArray *)toList { return @[ - (self.scopes ?: [NSNull null]), - (self.hostedDomain ?: [NSNull null]), - (self.clientId ?: [NSNull null]), - (self.serverClientId ?: [NSNull null]), + self.clientId ?: [NSNull null], + self.serverClientId ?: [NSNull null], + self.hostedDomain ?: [NSNull null], ]; } @end @@ -86,98 +106,175 @@ + (instancetype)makeWithDisplayName:(nullable NSString *)displayName email:(NSString *)email userId:(NSString *)userId photoUrl:(nullable NSString *)photoUrl - serverAuthCode:(nullable NSString *)serverAuthCode idToken:(nullable NSString *)idToken { FSIUserData *pigeonResult = [[FSIUserData alloc] init]; pigeonResult.displayName = displayName; pigeonResult.email = email; pigeonResult.userId = userId; pigeonResult.photoUrl = photoUrl; - pigeonResult.serverAuthCode = serverAuthCode; pigeonResult.idToken = idToken; return pigeonResult; } -+ (FSIUserData *)fromList:(NSArray *)list { ++ (FSIUserData *)fromList:(NSArray *)list { FSIUserData *pigeonResult = [[FSIUserData alloc] init]; pigeonResult.displayName = GetNullableObjectAtIndex(list, 0); pigeonResult.email = GetNullableObjectAtIndex(list, 1); - NSAssert(pigeonResult.email != nil, @""); pigeonResult.userId = GetNullableObjectAtIndex(list, 2); - NSAssert(pigeonResult.userId != nil, @""); pigeonResult.photoUrl = GetNullableObjectAtIndex(list, 3); - pigeonResult.serverAuthCode = GetNullableObjectAtIndex(list, 4); - pigeonResult.idToken = GetNullableObjectAtIndex(list, 5); + pigeonResult.idToken = GetNullableObjectAtIndex(list, 4); return pigeonResult; } -+ (nullable FSIUserData *)nullableFromList:(NSArray *)list { ++ (nullable FSIUserData *)nullableFromList:(NSArray *)list { return (list) ? [FSIUserData fromList:list] : nil; } -- (NSArray *)toList { +- (NSArray *)toList { return @[ - (self.displayName ?: [NSNull null]), - (self.email ?: [NSNull null]), - (self.userId ?: [NSNull null]), - (self.photoUrl ?: [NSNull null]), - (self.serverAuthCode ?: [NSNull null]), - (self.idToken ?: [NSNull null]), + self.displayName ?: [NSNull null], + self.email ?: [NSNull null], + self.userId ?: [NSNull null], + self.photoUrl ?: [NSNull null], + self.idToken ?: [NSNull null], ]; } @end -@implementation FSITokenData -+ (instancetype)makeWithIdToken:(nullable NSString *)idToken - accessToken:(nullable NSString *)accessToken { - FSITokenData *pigeonResult = [[FSITokenData alloc] init]; - pigeonResult.idToken = idToken; +@implementation FSISignInResult ++ (instancetype)makeWithSuccess:(nullable FSISignInSuccess *)success + error:(nullable FSISignInFailure *)error { + FSISignInResult *pigeonResult = [[FSISignInResult alloc] init]; + pigeonResult.success = success; + pigeonResult.error = error; + return pigeonResult; +} ++ (FSISignInResult *)fromList:(NSArray *)list { + FSISignInResult *pigeonResult = [[FSISignInResult alloc] init]; + pigeonResult.success = GetNullableObjectAtIndex(list, 0); + pigeonResult.error = GetNullableObjectAtIndex(list, 1); + return pigeonResult; +} ++ (nullable FSISignInResult *)nullableFromList:(NSArray *)list { + return (list) ? [FSISignInResult fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + self.success ?: [NSNull null], + self.error ?: [NSNull null], + ]; +} +@end + +@implementation FSISignInFailure ++ (instancetype)makeWithType:(FSIGoogleSignInErrorCode)type + message:(nullable NSString *)message + details:(nullable id)details { + FSISignInFailure *pigeonResult = [[FSISignInFailure alloc] init]; + pigeonResult.type = type; + pigeonResult.message = message; + pigeonResult.details = details; + return pigeonResult; +} ++ (FSISignInFailure *)fromList:(NSArray *)list { + FSISignInFailure *pigeonResult = [[FSISignInFailure alloc] init]; + FSIGoogleSignInErrorCodeBox *boxedFSIGoogleSignInErrorCode = GetNullableObjectAtIndex(list, 0); + pigeonResult.type = boxedFSIGoogleSignInErrorCode.value; + pigeonResult.message = GetNullableObjectAtIndex(list, 1); + pigeonResult.details = GetNullableObjectAtIndex(list, 2); + return pigeonResult; +} ++ (nullable FSISignInFailure *)nullableFromList:(NSArray *)list { + return (list) ? [FSISignInFailure fromList:list] : nil; +} +- (NSArray *)toList { + return @[ + [[FSIGoogleSignInErrorCodeBox alloc] initWithValue:self.type], + self.message ?: [NSNull null], + self.details ?: [NSNull null], + ]; +} +@end + +@implementation FSISignInSuccess ++ (instancetype)makeWithUser:(FSIUserData *)user + accessToken:(NSString *)accessToken + grantedScopes:(NSArray *)grantedScopes + serverAuthCode:(nullable NSString *)serverAuthCode { + FSISignInSuccess *pigeonResult = [[FSISignInSuccess alloc] init]; + pigeonResult.user = user; pigeonResult.accessToken = accessToken; + pigeonResult.grantedScopes = grantedScopes; + pigeonResult.serverAuthCode = serverAuthCode; return pigeonResult; } -+ (FSITokenData *)fromList:(NSArray *)list { - FSITokenData *pigeonResult = [[FSITokenData alloc] init]; - pigeonResult.idToken = GetNullableObjectAtIndex(list, 0); ++ (FSISignInSuccess *)fromList:(NSArray *)list { + FSISignInSuccess *pigeonResult = [[FSISignInSuccess alloc] init]; + pigeonResult.user = GetNullableObjectAtIndex(list, 0); pigeonResult.accessToken = GetNullableObjectAtIndex(list, 1); + pigeonResult.grantedScopes = GetNullableObjectAtIndex(list, 2); + pigeonResult.serverAuthCode = GetNullableObjectAtIndex(list, 3); return pigeonResult; } -+ (nullable FSITokenData *)nullableFromList:(NSArray *)list { - return (list) ? [FSITokenData fromList:list] : nil; ++ (nullable FSISignInSuccess *)nullableFromList:(NSArray *)list { + return (list) ? [FSISignInSuccess fromList:list] : nil; } -- (NSArray *)toList { +- (NSArray *)toList { return @[ - (self.idToken ?: [NSNull null]), - (self.accessToken ?: [NSNull null]), + self.user ?: [NSNull null], + self.accessToken ?: [NSNull null], + self.grantedScopes ?: [NSNull null], + self.serverAuthCode ?: [NSNull null], ]; } @end -@interface FSIGoogleSignInApiCodecReader : FlutterStandardReader +@interface FSIMessagesPigeonCodecReader : FlutterStandardReader @end -@implementation FSIGoogleSignInApiCodecReader +@implementation FSIMessagesPigeonCodecReader - (nullable id)readValueOfType:(UInt8)type { switch (type) { - case 128: - return [FSIInitParams fromList:[self readValue]]; - case 129: - return [FSITokenData fromList:[self readValue]]; + case 129: { + NSNumber *enumAsNumber = [self readValue]; + return enumAsNumber == nil + ? nil + : [[FSIGoogleSignInErrorCodeBox alloc] initWithValue:[enumAsNumber integerValue]]; + } case 130: + return [FSIPlatformConfigurationParams fromList:[self readValue]]; + case 131: return [FSIUserData fromList:[self readValue]]; + case 132: + return [FSISignInResult fromList:[self readValue]]; + case 133: + return [FSISignInFailure fromList:[self readValue]]; + case 134: + return [FSISignInSuccess fromList:[self readValue]]; default: return [super readValueOfType:type]; } } @end -@interface FSIGoogleSignInApiCodecWriter : FlutterStandardWriter +@interface FSIMessagesPigeonCodecWriter : FlutterStandardWriter @end -@implementation FSIGoogleSignInApiCodecWriter +@implementation FSIMessagesPigeonCodecWriter - (void)writeValue:(id)value { - if ([value isKindOfClass:[FSIInitParams class]]) { - [self writeByte:128]; - [self writeValue:[value toList]]; - } else if ([value isKindOfClass:[FSITokenData class]]) { + if ([value isKindOfClass:[FSIGoogleSignInErrorCodeBox class]]) { + FSIGoogleSignInErrorCodeBox *box = (FSIGoogleSignInErrorCodeBox *)value; [self writeByte:129]; + [self writeValue:(value == nil ? [NSNull null] : [NSNumber numberWithInteger:box.value])]; + } else if ([value isKindOfClass:[FSIPlatformConfigurationParams class]]) { + [self writeByte:130]; [self writeValue:[value toList]]; } else if ([value isKindOfClass:[FSIUserData class]]) { - [self writeByte:130]; + [self writeByte:131]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FSISignInResult class]]) { + [self writeByte:132]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FSISignInFailure class]]) { + [self writeByte:133]; + [self writeValue:[value toList]]; + } else if ([value isKindOfClass:[FSISignInSuccess class]]) { + [self writeByte:134]; [self writeValue:[value toList]]; } else { [super writeValue:value]; @@ -185,66 +282,82 @@ - (void)writeValue:(id)value { } @end -@interface FSIGoogleSignInApiCodecReaderWriter : FlutterStandardReaderWriter +@interface FSIMessagesPigeonCodecReaderWriter : FlutterStandardReaderWriter @end -@implementation FSIGoogleSignInApiCodecReaderWriter +@implementation FSIMessagesPigeonCodecReaderWriter - (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FSIGoogleSignInApiCodecWriter alloc] initWithData:data]; + return [[FSIMessagesPigeonCodecWriter alloc] initWithData:data]; } - (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FSIGoogleSignInApiCodecReader alloc] initWithData:data]; + return [[FSIMessagesPigeonCodecReader alloc] initWithData:data]; } @end -NSObject *FSIGoogleSignInApiGetCodec(void) { +NSObject *FSIGetMessagesCodec(void) { static FlutterStandardMessageCodec *sSharedObject = nil; static dispatch_once_t sPred = 0; dispatch_once(&sPred, ^{ - FSIGoogleSignInApiCodecReaderWriter *readerWriter = - [[FSIGoogleSignInApiCodecReaderWriter alloc] init]; + FSIMessagesPigeonCodecReaderWriter *readerWriter = + [[FSIMessagesPigeonCodecReaderWriter alloc] init]; sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; }); return sSharedObject; } - -void FSIGoogleSignInApiSetup(id binaryMessenger, +void SetUpFSIGoogleSignInApi(id binaryMessenger, NSObject *api) { - /// Initializes a sign in request with the given parameters. + SetUpFSIGoogleSignInApiWithSuffix(binaryMessenger, api, @""); +} + +void SetUpFSIGoogleSignInApiWithSuffix(id binaryMessenger, + NSObject *api, + NSString *messageChannelSuffix) { + messageChannelSuffix = messageChannelSuffix.length > 0 + ? [NSString stringWithFormat:@".%@", messageChannelSuffix] + : @""; + /// Configures the sign in object with application-level parameters. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.init" + initWithName:[NSString + stringWithFormat: + @"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.configure", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(initializeSignInWithParameters:error:)], + NSCAssert([api respondsToSelector:@selector(configureWithParameters:error:)], @"FSIGoogleSignInApi api (%@) doesn't respond to " - @"@selector(initializeSignInWithParameters:error:)", + @"@selector(configureWithParameters:error:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - FSIInitParams *arg_params = GetNullableObjectAtIndex(args, 0); + NSArray *args = message; + FSIPlatformConfigurationParams *arg_params = GetNullableObjectAtIndex(args, 0); FlutterError *error; - [api initializeSignInWithParameters:arg_params error:&error]; + [api configureWithParameters:arg_params error:&error]; callback(wrapResult(nil, error)); }]; } else { [channel setMessageHandler:nil]; } } - /// Starts a silent sign in. + /// Attempts to restore an existing sign-in, if any, with minimal user + /// interaction. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signInSilently" + initWithName:[NSString stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios." + @"GoogleSignInApi.restorePreviousSignIn", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(signInSilentlyWithCompletion:)], + NSCAssert([api respondsToSelector:@selector(restorePreviousSignInWithCompletion:)], @"FSIGoogleSignInApi api (%@) doesn't respond to " - @"@selector(signInSilentlyWithCompletion:)", + @"@selector(restorePreviousSignInWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - [api signInSilentlyWithCompletion:^(FSIUserData *_Nullable output, - FlutterError *_Nullable error) { + [api restorePreviousSignInWithCompletion:^(FSISignInResult *_Nullable output, + FlutterError *_Nullable error) { callback(wrapResult(output, error)); }]; }]; @@ -255,17 +368,26 @@ void FSIGoogleSignInApiSetup(id binaryMessenger, /// Starts a sign in with user interaction. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signIn" + initWithName: + [NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signIn", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(signInWithCompletion:)], - @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(signInWithCompletion:)", + NSCAssert([api respondsToSelector:@selector(signInWithScopeHint:completion:)], + @"FSIGoogleSignInApi api (%@) doesn't respond to " + @"@selector(signInWithScopeHint:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - [api signInWithCompletion:^(FSIUserData *_Nullable output, FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + NSArray *args = message; + NSArray *arg_scopeHint = GetNullableObjectAtIndex(args, 0); + [api signInWithScopeHint:arg_scopeHint + completion:^(FSISignInResult *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; } else { [channel setMessageHandler:nil]; @@ -274,100 +396,103 @@ void FSIGoogleSignInApiSetup(id binaryMessenger, /// Requests the access token for the current sign in. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.getAccessToken" + initWithName:[NSString + stringWithFormat:@"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios." + @"GoogleSignInApi.getRefreshedAuthorizationTokens", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getAccessTokenWithCompletion:)], + NSCAssert([api respondsToSelector:@selector(getRefreshedAuthorizationTokensForUser: + completion:)], @"FSIGoogleSignInApi api (%@) doesn't respond to " - @"@selector(getAccessTokenWithCompletion:)", + @"@selector(getRefreshedAuthorizationTokensForUser:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - [api getAccessTokenWithCompletion:^(FSITokenData *_Nullable output, - FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + NSArray *args = message; + NSString *arg_userId = GetNullableObjectAtIndex(args, 0); + [api getRefreshedAuthorizationTokensForUser:arg_userId + completion:^(FSISignInResult *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } - /// Signs out the current user. + /// Requests authorization of the given additional scopes. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signOut" + initWithName:[NSString + stringWithFormat: + @"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.addScopes", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(signOutWithError:)], - @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(signOutWithError:)", + NSCAssert([api respondsToSelector:@selector(addScopes:forUser:completion:)], + @"FSIGoogleSignInApi api (%@) doesn't respond to " + @"@selector(addScopes:forUser:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - FlutterError *error; - [api signOutWithError:&error]; - callback(wrapResult(nil, error)); - }]; - } else { - [channel setMessageHandler:nil]; - } - } - /// Revokes scope grants to the application. - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.disconnect" - binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; - if (api) { - NSCAssert( - [api respondsToSelector:@selector(disconnectWithCompletion:)], - @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(disconnectWithCompletion:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - [api disconnectWithCompletion:^(FlutterError *_Nullable error) { - callback(wrapResult(nil, error)); - }]; + NSArray *args = message; + NSArray *arg_scopes = GetNullableObjectAtIndex(args, 0); + NSString *arg_userId = GetNullableObjectAtIndex(args, 1); + [api addScopes:arg_scopes + forUser:arg_userId + completion:^(FSISignInResult *_Nullable output, FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; } else { [channel setMessageHandler:nil]; } } - /// Returns whether the user is currently signed in. + /// Signs out the current user. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.isSignedIn" + initWithName:[NSString + stringWithFormat: + @"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signOut", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(isSignedInWithError:)], - @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(isSignedInWithError:)", + NSCAssert([api respondsToSelector:@selector(signOutWithError:)], + @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(signOutWithError:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { FlutterError *error; - NSNumber *output = [api isSignedInWithError:&error]; - callback(wrapResult(output, error)); + [api signOutWithError:&error]; + callback(wrapResult(nil, error)); }]; } else { [channel setMessageHandler:nil]; } } - /// Requests access to the given scopes. + /// Revokes scope grants to the application. { FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.requestScopes" + initWithName:[NSString + stringWithFormat: + @"%@%@", + @"dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.disconnect", + messageChannelSuffix] binaryMessenger:binaryMessenger - codec:FSIGoogleSignInApiGetCodec()]; + codec:FSIGetMessagesCodec()]; if (api) { NSCAssert( - [api respondsToSelector:@selector(requestScopes:completion:)], - @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(requestScopes:completion:)", + [api respondsToSelector:@selector(disconnectWithCompletion:)], + @"FSIGoogleSignInApi api (%@) doesn't respond to @selector(disconnectWithCompletion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - NSArray *arg_scopes = GetNullableObjectAtIndex(args, 0); - [api requestScopes:arg_scopes - completion:^(NSNumber *_Nullable output, FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + [api disconnectWithCompletion:^(FlutterError *_Nullable error) { + callback(wrapResult(nil, error)); + }]; }]; } else { [channel setMessageHandler:nil]; diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000000..3dc8b8235fe --- /dev/null +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,50 @@ +{ + "originHash" : "4b36b64d9a071f871b5e5ecf435f3e097843030f79026880b7d9ef1f081d563d", + "pins" : [ + { + "identity" : "appauth-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/openid/AppAuth-iOS.git", + "state" : { + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" + } + }, + { + "identity" : "googlesignin-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleSignIn-iOS.git", + "state" : { + "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", + "version" : "7.1.0" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", + "version" : "3.5.0" + } + }, + { + "identity" : "gtmappauth", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GTMAppAuth.git", + "state" : { + "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", + "version" : "4.1.1" + } + }, + { + "identity" : "ocmock", + "kind" : "remoteSourceControl", + "location" : "https://github.com/erikdoe/ocmock", + "state" : { + "revision" : "fe1661a3efed11831a6452f4b1a0c5e6ddc08c3d" + } + } + ], + "version" : 3 +} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 330fa765f0d..6bf8b2f1ace 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> init({ - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String? hostedDomain, - String? clientId, - }) { - return initWithParams(SignInInitParameters( - signInOption: signInOption, - scopes: scopes, - hostedDomain: hostedDomain, - clientId: clientId, - )); + Future init(InitParameters params) async { + await _api.configure(PlatformConfigurationParams( + clientId: params.clientId, + serverClientId: params.serverClientId, + hostedDomain: params.hostedDomain)); } @override - Future initWithParams(SignInInitParameters params) { - if (params.signInOption == SignInOption.games) { - throw PlatformException( - code: 'unsupported-options', - message: 'Games sign in is not supported on iOS'); + Future attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) async { + final SignInResult result = await _api.restorePreviousSignIn(); + + if (result.error?.type == GoogleSignInErrorCode.noAuthInKeychain) { + return null; } - if (params.forceAccountName != null) { - throw ArgumentError('Force account name is not supported on iOS'); + + final SignInFailure? failure = result.error; + if (failure != null) { + throw GoogleSignInException( + code: _exceptionCodeForErrorPlatformErrorCode(failure.type), + description: failure.message, + details: failure.details); } - return _api.init(InitParams( - scopes: params.scopes, - hostedDomain: params.hostedDomain, - clientId: params.clientId, - serverClientId: params.serverClientId, - )); + + // The native code must never return a null success and a null error. + // Switching the native implementation to Swift and using sealed classes + // in the Pigeon definition (see Android's messages.dart) will allow + // enforcing this via the type system instead of force unwrapping. + final SignInSuccess success = result.success!; + return _authenticationResultsFromSignInSuccess(success); } @override - Future signInSilently() { - return _api.signInSilently().then(_signInUserDataFromChannelData); + Future authenticate( + AuthenticateParameters params) async { + final SignInResult result = await _api.signIn(params.scopeHint); + + // This should never happen; the corresponding native error code is + // documented as being specific to restorePreviousSignIn. + if (result.error?.type == GoogleSignInErrorCode.noAuthInKeychain) { + throw const GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: 'No auth reported during interactive sign in.'); + } + + final SignInFailure? failure = result.error; + if (failure != null) { + throw GoogleSignInException( + code: _exceptionCodeForErrorPlatformErrorCode(failure.type), + description: failure.message, + details: failure.details); + } + + // The native code must never return a null success and a null error. + // Switching the native implementation to Swift and using sealed classes + // in the Pigeon definition (see Android's messages.dart) will allow + // enforcing this via the type system instead of force unwrapping. + final SignInSuccess success = result.success!; + return _authenticationResultsFromSignInSuccess(success); } @override - Future signIn() { - return _api.signIn().then(_signInUserDataFromChannelData); + Future signOut(SignOutParams params) { + return _api.signOut(); } @override - Future getTokens( - {required String email, bool? shouldRecoverAuth = true}) { - return _api.getAccessToken().then(_signInTokenDataFromChannelData); + Future disconnect(DisconnectParams params) async { + await _api.disconnect(); + await signOut(const SignOutParams()); } @override - Future signOut() { - return _api.signOut(); + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params) async { + final (:String? accessToken, :String? serverAuthCode) = + await _getAuthorizationTokens(params.request); + return accessToken == null + ? null + : ClientAuthorizationTokenData(accessToken: accessToken); } @override - Future disconnect() { - return _api.disconnect(); + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params) async { + final (:String? accessToken, :String? serverAuthCode) = + await _getAuthorizationTokens(params.request); + return serverAuthCode == null + ? null + : ServerAuthorizationTokenData(serverAuthCode: serverAuthCode); } - @override - Future isSignedIn() { - return _api.isSignedIn(); + Future<({String? accessToken, String? serverAuthCode})> + _getAuthorizationTokens(AuthorizationRequestDetails request) async { + String? userId = request.userId; + + // The Google Sign In SDK requires authentication before authorization, so + // if the authentication isn't associated with an existing sign-in user + // run the authentication flow first. + if (userId == null) { + SignInResult result = await _api.restorePreviousSignIn(); + final SignInSuccess? success = result.success; + if (success == null) { + // There's no existing sign-in to use, so return the results of the + // combined authn+authz flow. + result = await _api.signIn(request.scopes); + return _processAuthorizationResult(result); + } else { + // Discard the authentication information, and extract the user ID to + // pass back to the authorization step so that it can re-associate + // with the currently signed in user on the native side. + userId = success.user.userId; + } + } + + SignInResult result = request.promptIfUnauthorized + ? await _api.addScopes(request.scopes, userId) + : await _api.getRefreshedAuthorizationTokens(userId); + if (request.promptIfUnauthorized && + result.error?.type == GoogleSignInErrorCode.scopesAlreadyGranted) { + // The Google Sign In SDK returns an error when requesting scopes that are + // already authorized, so in that case request updated tokens instead to + // construct a valid token response. + result = await _api.getRefreshedAuthorizationTokens(userId); + } + if (result.error?.type == GoogleSignInErrorCode.noAuthInKeychain) { + return (accessToken: null, serverAuthCode: null); + } + + // If re-using an existing authorization, ensure that it has all of the + // requested scopes before returning it, as the list of requested scopes + // may have changed since the last authorization. + if (!request.promptIfUnauthorized) { + final SignInSuccess? success = result.success; + // Don't validate the OpenID Connect scopes (see + // https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect + // for details), as they should always be available, and the granted + // scopes may not report them with the same string as the request. + // For example, requesting 'email' can instead result in the grant + // 'https://www.googleapis.com/auth/userinfo.email'. + const Set openIdConnectScopes = { + 'email', + 'openid', + 'profile' + }; + if (success != null) { + if (request.scopes.any((String scope) => + !openIdConnectScopes.contains(scope) && + !success.grantedScopes.contains(scope))) { + return (accessToken: null, serverAuthCode: null); + } + } + } + + return _processAuthorizationResult(result); } - @override - Future clearAuthCache({required String token}) async { - // There's nothing to be done here on iOS since the expired/invalid - // tokens are refreshed automatically by getTokens. + Future<({String? accessToken, String? serverAuthCode})> + _processAuthorizationResult(SignInResult result) async { + final SignInFailure? failure = result.error; + if (failure != null) { + throw GoogleSignInException( + code: _exceptionCodeForErrorPlatformErrorCode(failure.type), + description: failure.message, + details: failure.details); + } + + return _authorizationTokenDataFromSignInSuccess(result.success); } - @override - Future requestScopes(List scopes) { - return _api.requestScopes(scopes); + AuthenticationResults _authenticationResultsFromSignInSuccess( + SignInSuccess result) { + final UserData userData = result.user; + final GoogleSignInUserData user = GoogleSignInUserData( + email: userData.email, + id: userData.userId, + displayName: userData.displayName, + photoUrl: userData.photoUrl); + return AuthenticationResults( + user: user, + authenticationTokens: + AuthenticationTokenData(idToken: userData.idToken)); } - GoogleSignInUserData _signInUserDataFromChannelData(UserData data) { - return GoogleSignInUserData( - email: data.email, - id: data.userId, - displayName: data.displayName, - photoUrl: data.photoUrl, - serverAuthCode: data.serverAuthCode, - idToken: data.idToken, + ({String? accessToken, String? serverAuthCode}) + _authorizationTokenDataFromSignInSuccess(SignInSuccess? result) { + return ( + accessToken: result?.accessToken, + serverAuthCode: result?.serverAuthCode ); } - GoogleSignInTokenData _signInTokenDataFromChannelData(TokenData data) { - return GoogleSignInTokenData( - idToken: data.idToken, - accessToken: data.accessToken, - ); + GoogleSignInExceptionCode _exceptionCodeForErrorPlatformErrorCode( + GoogleSignInErrorCode code) { + return switch (code) { + GoogleSignInErrorCode.unknown => GoogleSignInExceptionCode.unknownError, + GoogleSignInErrorCode.keychainError => + GoogleSignInExceptionCode.providerConfigurationError, + GoogleSignInErrorCode.canceled => GoogleSignInExceptionCode.canceled, + GoogleSignInErrorCode.eemError => + GoogleSignInExceptionCode.providerConfigurationError, + GoogleSignInErrorCode.userMismatch => + GoogleSignInExceptionCode.userMismatch, + // These should never be mapped to a GoogleSignInException; the caller + // should handle them. + GoogleSignInErrorCode.noAuthInKeychain => throw StateError( + '_exceptionCodeForErrorPlatformErrorCode called with no auth.'), + GoogleSignInErrorCode.scopesAlreadyGranted => throw StateError( + '_exceptionCodeForErrorPlatformErrorCode called with scopes already granted.'), + }; } } diff --git a/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart index 21dd2ebf8e2..f5607f5d64e 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart @@ -1,9 +1,9 @@ // 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. -// Autogenerated from Pigeon (v11.0.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.4), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -11,55 +11,84 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; -/// Pigeon version of SignInInitParams. -/// -/// See SignInInitParams for details. -class InitParams { - InitParams({ - required this.scopes, - this.hostedDomain, +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + +/// Enum mapping of known codes from +/// https://developers.google.com/identity/sign-in/ios/reference/Enums/GIDSignInErrorCode +enum GoogleSignInErrorCode { + /// Either the underlying kGIDSignInErrorCodeUnknown, or a code that isn't + /// a known code mapped to a value below. + unknown, + + /// kGIDSignInErrorCodeKeychain; an error reading or writing to keychain. + keychainError, + + /// kGIDSignInErrorCodeHasNoAuthInKeychain; no auth present in the keychain. + /// + /// For restorePreviousSignIn, this indicates that there is no sign in to + /// restore. + noAuthInKeychain, + + /// kGIDSignInErrorCodeCanceled; the request was canceled by the user. + canceled, + + /// kGIDSignInErrorCodeEMM; an enterprise management error occurred. + eemError, + + /// kGIDSignInErrorCodeScopesAlreadyGranted; the requested scopes have already + /// been granted. + scopesAlreadyGranted, + + /// kGIDSignInErrorCodeMismatchWithCurrentUser; an operation was requested on + /// a non-current user. + userMismatch, +} + +class PlatformConfigurationParams { + PlatformConfigurationParams({ this.clientId, this.serverClientId, + this.hostedDomain, }); - List scopes; - - String? hostedDomain; - String? clientId; String? serverClientId; + String? hostedDomain; + Object encode() { return [ - scopes, - hostedDomain, clientId, serverClientId, + hostedDomain, ]; } - static InitParams decode(Object result) { + static PlatformConfigurationParams decode(Object result) { result as List; - return InitParams( - scopes: (result[0] as List?)!.cast(), - hostedDomain: result[1] as String?, - clientId: result[2] as String?, - serverClientId: result[3] as String?, + return PlatformConfigurationParams( + clientId: result[0] as String?, + serverClientId: result[1] as String?, + hostedDomain: result[2] as String?, ); } } -/// Pigeon version of GoogleSignInUserData. +/// Pigeon version of GoogleSignInUserData + AuthenticationTokenData. /// -/// See GoogleSignInUserData for details. +/// See GoogleSignInUserData and AuthenticationTokenData for details. class UserData { UserData({ this.displayName, required this.email, required this.userId, this.photoUrl, - this.serverAuthCode, this.idToken, }); @@ -71,8 +100,6 @@ class UserData { String? photoUrl; - String? serverAuthCode; - String? idToken; Object encode() { @@ -81,7 +108,6 @@ class UserData { email, userId, photoUrl, - serverAuthCode, idToken, ]; } @@ -93,53 +119,145 @@ class UserData { email: result[1]! as String, userId: result[2]! as String, photoUrl: result[3] as String?, - serverAuthCode: result[4] as String?, - idToken: result[5] as String?, + idToken: result[4] as String?, ); } } -/// Pigeon version of GoogleSignInTokenData. +/// The response from an auth call. +class SignInResult { + SignInResult({ + this.success, + this.error, + }); + + /// The success result, if any. + /// + /// Exactly one of success and error will be non-nil. + SignInSuccess? success; + + /// The error result, if any. + /// + /// Exactly one of success and error will be non-nil. + SignInFailure? error; + + Object encode() { + return [ + success, + error, + ]; + } + + static SignInResult decode(Object result) { + result as List; + return SignInResult( + success: result[0] as SignInSuccess?, + error: result[1] as SignInFailure?, + ); + } +} + +/// An sign in failure. +class SignInFailure { + SignInFailure({ + required this.type, + this.message, + this.details, + }); + + /// The type of failure. + GoogleSignInErrorCode type; + + /// The message associated with the failure, if any. + String? message; + + /// Extra details about the failure, if any. + Object? details; + + Object encode() { + return [ + type, + message, + details, + ]; + } + + static SignInFailure decode(Object result) { + result as List; + return SignInFailure( + type: result[0]! as GoogleSignInErrorCode, + message: result[1] as String?, + details: result[2], + ); + } +} + +/// A successful auth result. /// -/// See GoogleSignInTokenData for details. -class TokenData { - TokenData({ - this.idToken, - this.accessToken, +/// Corresponds to the information in a native GIDSignInResult. Because of the +/// structure of the Google Sign In SDK, this has information corresponding to +/// both authn and authz steps, even though incremental authorization is +/// supported. +class SignInSuccess { + SignInSuccess({ + required this.user, + required this.accessToken, + required this.grantedScopes, + this.serverAuthCode, }); - String? idToken; + UserData user; + + String accessToken; - String? accessToken; + List grantedScopes; + + String? serverAuthCode; Object encode() { return [ - idToken, + user, accessToken, + grantedScopes, + serverAuthCode, ]; } - static TokenData decode(Object result) { + static SignInSuccess decode(Object result) { result as List; - return TokenData( - idToken: result[0] as String?, - accessToken: result[1] as String?, + return SignInSuccess( + user: result[0]! as UserData, + accessToken: result[1]! as String, + grantedScopes: (result[2] as List?)!.cast(), + serverAuthCode: result[3] as String?, ); } } -class _GoogleSignInApiCodec extends StandardMessageCodec { - const _GoogleSignInApiCodec(); +class _PigeonCodec extends StandardMessageCodec { + const _PigeonCodec(); @override void writeValue(WriteBuffer buffer, Object? value) { - if (value is InitParams) { - buffer.putUint8(128); - writeValue(buffer, value.encode()); - } else if (value is TokenData) { + if (value is int) { + buffer.putUint8(4); + buffer.putInt64(value); + } else if (value is GoogleSignInErrorCode) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PlatformConfigurationParams) { + buffer.putUint8(130); writeValue(buffer, value.encode()); } else if (value is UserData) { - buffer.putUint8(130); + buffer.putUint8(131); + writeValue(buffer, value.encode()); + } else if (value is SignInResult) { + buffer.putUint8(132); + writeValue(buffer, value.encode()); + } else if (value is SignInFailure) { + buffer.putUint8(133); + writeValue(buffer, value.encode()); + } else if (value is SignInSuccess) { + buffer.putUint8(134); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -149,12 +267,19 @@ class _GoogleSignInApiCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 128: - return InitParams.decode(readValue(buffer)!); case 129: - return TokenData.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : GoogleSignInErrorCode.values[value]; case 130: + return PlatformConfigurationParams.decode(readValue(buffer)!); + case 131: return UserData.decode(readValue(buffer)!); + case 132: + return SignInResult.decode(readValue(buffer)!); + case 133: + return SignInFailure.decode(readValue(buffer)!); + case 134: + return SignInSuccess.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); } @@ -165,217 +290,210 @@ class GoogleSignInApi { /// Constructor for [GoogleSignInApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - GoogleSignInApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; - - static const MessageCodec codec = _GoogleSignInApiCodec(); - - /// Initializes a sign in request with the given parameters. - Future init(InitParams arg_params) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.init', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_params]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + GoogleSignInApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + /// Configures the sign in object with application-level parameters. + Future configure(PlatformConfigurationParams params) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.configure$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([params]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; } } - /// Starts a silent sign in. - Future signInSilently() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signInSilently', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + /// Attempts to restore an existing sign-in, if any, with minimal user + /// interaction. + Future restorePreviousSignIn() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.restorePreviousSignIn$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as UserData?)!; + return (pigeonVar_replyList[0] as SignInResult?)!; } } /// Starts a sign in with user interaction. - Future signIn() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signIn', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + Future signIn(List scopeHint) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signIn$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([scopeHint]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as UserData?)!; + return (pigeonVar_replyList[0] as SignInResult?)!; } } /// Requests the access token for the current sign in. - Future getAccessToken() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.getAccessToken', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + Future getRefreshedAuthorizationTokens(String userId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.getRefreshedAuthorizationTokens$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send([userId]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyList[0] == null) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as TokenData?)!; + return (pigeonVar_replyList[0] as SignInResult?)!; } } - /// Signs out the current user. - Future signOut() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signOut', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { + /// Requests authorization of the given additional scopes. + Future addScopes(List scopes, String userId) async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.addScopes$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = await pigeonVar_channel + .send([scopes, userId]) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); - } else if (replyList.length > 1) { + } else if (pigeonVar_replyList[0] == null) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', ); } else { - return; + return (pigeonVar_replyList[0] as SignInResult?)!; } } - /// Revokes scope grants to the application. - Future disconnect() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.disconnect', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + /// Signs out the current user. + Future signOut() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signOut$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { return; } } - /// Returns whether the user is currently signed in. - Future isSignedIn() async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.isSignedIn', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send(null) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { - throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], - ); - } else if (replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (replyList[0] as bool?)!; - } - } - - /// Requests access to the given scopes. - Future requestScopes(List arg_scopes) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.requestScopes', - codec, - binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_scopes]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { - throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], - ); - } else if (replyList[0] == null) { + /// Revokes scope grants to the application. + Future disconnect() async { + final String pigeonVar_channelName = + 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.disconnect$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = + BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], ); } else { - return (replyList[0] as bool?)!; + return; } } } diff --git a/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart index e7de686ee72..7060c0197be 100644 --- a/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart @@ -7,44 +7,36 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', objcHeaderOut: - 'darwin/google_sign_in_ios/Sources/google_sign_in/include/google_sign_in/messages.g.h', + 'darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h', objcSourceOut: - 'darwin/google_sign_in_ios/Sources/google_sign_in/messages.g.m', + 'darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m', objcOptions: ObjcOptions( prefix: 'FSI', - headerIncludePath: './include/google_sign_in/messages.g.h', + headerIncludePath: './include/google_sign_in_ios/messages.g.h', ), copyrightHeader: 'pigeons/copyright.txt', )) - -/// Pigeon version of SignInInitParams. -/// -/// See SignInInitParams for details. -class InitParams { - /// The parameters to use when initializing the sign in process. - const InitParams({ - this.scopes = const [], - this.hostedDomain, +class PlatformConfigurationParams { + PlatformConfigurationParams({ this.clientId, this.serverClientId, + this.hostedDomain, }); - final List scopes; - final String? hostedDomain; final String? clientId; final String? serverClientId; + final String? hostedDomain; } -/// Pigeon version of GoogleSignInUserData. +/// Pigeon version of GoogleSignInUserData + AuthenticationTokenData. /// -/// See GoogleSignInUserData for details. +/// See GoogleSignInUserData and AuthenticationTokenData for details. class UserData { UserData({ required this.email, required this.userId, this.displayName, this.photoUrl, - this.serverAuthCode, this.idToken, }); @@ -52,40 +44,110 @@ class UserData { final String email; final String userId; final String? photoUrl; - final String? serverAuthCode; final String? idToken; } -/// Pigeon version of GoogleSignInTokenData. +/// Enum mapping of known codes from +/// https://developers.google.com/identity/sign-in/ios/reference/Enums/GIDSignInErrorCode +enum GoogleSignInErrorCode { + /// Either the underlying kGIDSignInErrorCodeUnknown, or a code that isn't + /// a known code mapped to a value below. + unknown, + + /// kGIDSignInErrorCodeKeychain; an error reading or writing to keychain. + keychainError, + + /// kGIDSignInErrorCodeHasNoAuthInKeychain; no auth present in the keychain. + /// + /// For restorePreviousSignIn, this indicates that there is no sign in to + /// restore. + noAuthInKeychain, + + /// kGIDSignInErrorCodeCanceled; the request was canceled by the user. + canceled, + + /// kGIDSignInErrorCodeEMM; an enterprise management error occurred. + eemError, + + /// kGIDSignInErrorCodeScopesAlreadyGranted; the requested scopes have already + /// been granted. + scopesAlreadyGranted, + + /// kGIDSignInErrorCodeMismatchWithCurrentUser; an operation was requested on + /// a non-current user. + userMismatch, +} + +/// The response from an auth call. +// TODO(stuartmorgan): Switch to a sealed base class with two subclasses instead +// of using composition when the plugin is migrated to Swift. +class SignInResult { + /// The success result, if any. + /// + /// Exactly one of success and error will be non-nil. + SignInSuccess? success; + + /// The error result, if any. + /// + /// Exactly one of success and error will be non-nil. + SignInFailure? error; +} + +/// An sign in failure. +class SignInFailure { + /// The type of failure. + late GoogleSignInErrorCode type; + + /// The message associated with the failure, if any. + String? message; + + /// Extra details about the failure, if any. + Object? details; +} + +/// A successful auth result. /// -/// See GoogleSignInTokenData for details. -class TokenData { - TokenData({ - this.idToken, - this.accessToken, - }); +/// Corresponds to the information in a native GIDSignInResult. Because of the +/// structure of the Google Sign In SDK, this has information corresponding to +/// both authn and authz steps, even though incremental authorization is +/// supported. +class SignInSuccess { + late UserData user; - final String? idToken; - final String? accessToken; + late String accessToken; + + late List grantedScopes; + + // This is set only on a new sign in or scope grant, not a restored sign-in. + // See https://github.com/google/GoogleSignIn-iOS/issues/202 + String? serverAuthCode; } @HostApi() abstract class GoogleSignInApi { - /// Initializes a sign in request with the given parameters. - @ObjCSelector('initializeSignInWithParameters:') - void init(InitParams params); + /// Configures the sign in object with application-level parameters. + @ObjCSelector('configureWithParameters:') + void configure(PlatformConfigurationParams params); - /// Starts a silent sign in. + /// Attempts to restore an existing sign-in, if any, with minimal user + /// interaction. @async - UserData signInSilently(); + SignInResult restorePreviousSignIn(); /// Starts a sign in with user interaction. @async - UserData signIn(); + @ObjCSelector('signInWithScopeHint:') + SignInResult signIn(List scopeHint); /// Requests the access token for the current sign in. @async - TokenData getAccessToken(); + @ObjCSelector('getRefreshedAuthorizationTokensForUser:') + SignInResult getRefreshedAuthorizationTokens(String userId); + + /// Requests authorization of the given additional scopes. + @async + @ObjCSelector('addScopes:forUser:') + SignInResult addScopes(List scopes, String userId); /// Signs out the current user. void signOut(); @@ -93,12 +155,4 @@ abstract class GoogleSignInApi { /// Revokes scope grants to the application. @async void disconnect(); - - /// Returns whether the user is currently signed in. - bool isSignedIn(); - - /// Requests access to the given scopes. - @async - @ObjCSelector('requestScopes:') - bool requestScopes(List scopes); } 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 131dbe8f2b7..80a46f3e837 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 @@ -40,107 +40,113 @@ abstract class GoogleSignInPlatform extends PlatformInterface { _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. + /// Signs in with explicit user intent. /// - /// See: + /// This is intended to support the use case where the user has expressed + /// an explicit intent to sign in. + Future authenticate(AuthenticateParameters params); + + /// Returns true if the platform implementation supports the [authenticate] + /// method. /// - /// * [SignInInitParameters] - Future initWithParams(SignInInitParameters params) async { - await init( - scopes: params.scopes, - signInOption: params.signInOption, - hostedDomain: params.hostedDomain, - clientId: params.clientId, - ); - } + /// 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() => true; - /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. - Future signInSilently() async { - throw UnimplementedError('signInSilently() 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); - /// 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 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); - /// Returns the Tokens used to authenticate other API calls. - Future getTokens( - {required String email, bool? shouldRecoverAuth}) async { - throw UnimplementedError('getTokens() has not been implemented.'); - } + /// Signs out previously signed in accounts. + Future signOut(SignOutParams params); + + /// Revokes all of the scopes that all signed in users granted, and then 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). + Stream? get authenticationEvents => null; +} - /// Signs out the current account from the application. - Future signOut() async { - throw UnimplementedError('signOut() has not been implemented.'); +/// 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(); } - /// Revokes all of the scopes that the user granted. - Future disconnect() async { - throw UnimplementedError('disconnect() has not been implemented.'); + @override + Future attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) { + throw UnimplementedError(); } - /// Returns whether the current user is currently signed in. - Future isSignedIn() async { - throw UnimplementedError('isSignedIn() has not been implemented.'); + @override + Future authenticate(AuthenticateParameters params) { + 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(); + } } - -class _PlaceholderImplementation extends GoogleSignInPlatform {} 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 057927d5164..d3e6d50e9b5 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() => + 'CredentialException(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, + + /// The operation was canceled by the user. + canceled, - /// Recommended configuration for Games sign in. + /// 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,14 @@ 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.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 +107,317 @@ 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. + /// A hosted domain to restrict accounts to. + /// + /// The default is null, meaning no restriction. + /// + /// 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(); + + // 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. /// - /// This is only used on Android. - final bool forceCodeForRefreshToken; + /// Some platforms reference accounts by email at the SDK level, so this + /// should be provided if userId is provided. + final String? email; - /// Can be used to explicitly set an account name on the underlying platform sign-in API. + /// 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 6f03a6c357f..00000000000 --- 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/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 851c0e40062..6ad48e0c226 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 @@ -33,12 +33,12 @@ void main() { group('GoogleSignInTokenData', () { test('can be compared by == operator', () { - final GoogleSignInTokenData firstInstance = GoogleSignInTokenData( + const GoogleSignInTokenData firstInstance = GoogleSignInTokenData( accessToken: 'accessToken', idToken: 'idToken', serverAuthCode: 'serverAuthCode', ); - final GoogleSignInTokenData secondInstance = GoogleSignInTokenData( + const GoogleSignInTokenData secondInstance = GoogleSignInTokenData( accessToken: 'accessToken', idToken: 'idToken', serverAuthCode: 'serverAuthCode', @@ -49,7 +49,7 @@ void main() { group('GoogleSignInUserData', () { test('can be compared by == operator', () { - final GoogleSignInUserData firstInstance = GoogleSignInUserData( + const GoogleSignInUserData firstInstance = GoogleSignInUserData( email: 'email', id: 'id', displayName: 'displayName', @@ -57,7 +57,7 @@ void main() { idToken: 'idToken', serverAuthCode: 'serverAuthCode', ); - final GoogleSignInUserData secondInstance = GoogleSignInUserData( + const GoogleSignInUserData secondInstance = GoogleSignInUserData( email: 'email', id: 'id', displayName: 'displayName', 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 e6d3abc9e58..6988cd8a3bb 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,12 +3,10 @@ // found in the LICENSE file. import 'dart:async'; -import 'dart:js_interop'; import 'dart:ui_web' as ui_web; import 'package:flutter/foundation.dart' show kDebugMode, visibleForTesting; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show PlatformException; import 'package:flutter/widgets.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:google_identity_services_web/loader.dart' as loader; @@ -50,10 +48,10 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @visibleForTesting bool debugOverrideLoader = false, @visibleForTesting GisSdkClient? debugOverrideGisSdkClient, @visibleForTesting - StreamController? debugOverrideUserDataController, + StreamController? debugAuthenticationController, }) : _gisSdkClient = debugOverrideGisSdkClient, - _userDataController = debugOverrideUserDataController ?? - StreamController.broadcast() { + _authenticationController = debugAuthenticationController ?? + StreamController.broadcast() { autoDetectedClientId = web.document .querySelector(clientIdMetaSelector) ?.getAttribute(clientIdAttributeName); @@ -73,7 +71,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Completer? _initCalled; // A StreamController to communicate status changes from the GisSdkClient. - final StreamController _userDataController; + final StreamController _authenticationController; // The instance of [GisSdkClient] backing the plugin. GisSdkClient? _gisSdkClient; @@ -104,8 +102,8 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { /// A future that resolves when the plugin is fully initialized. /// - /// This ensures that the SDK has been loaded, and that the `initWithParams` - /// method has finished running. + /// This ensures that the SDK has been loaded, and that the `init` method + /// has finished running. @visibleForTesting Future get initialized { _assertIsInitCalled(); @@ -123,22 +121,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @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) async { + Future init(InitParameters params) async { final String? appClientId = params.clientId ?? autoDetectedClientId; assert( appClientId != null, @@ -149,15 +132,6 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { assert(params.serverClientId == null, 'serverClientId is not supported on Web.'); - assert( - !params.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.'); - - assert(params.forceAccountName == null, - 'forceAccountName is not supported on Web.'); - _initCalled = Completer(); await _jsSdkLoadedFuture; @@ -165,146 +139,117 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { _gisSdkClient ??= GisSdkClient( clientId: appClientId!, hostedDomain: params.hostedDomain, - initialScopes: List.from(params.scopes), - userDataController: _userDataController, + authenticationController: _authenticationController, loggingEnabled: kDebugMode, ); _initCalled!.complete(); // Signal that `init` is fully done. } - // Register a factory for the Button HtmlElementView. - void _registerButtonFactory() { - ui_web.platformViewRegistry.registerViewFactory( - 'gsi_login_button', - (int viewId) { - final web.Element element = web.document.createElement('div'); - element.setAttribute('style', - 'width: 100%; height: 100%; overflow: hidden; display: flex; flex-wrap: wrap; align-content: center; justify-content: center;'); - element.id = 'sign_in_button_$viewId'; - return element; - }, - ); - } - - /// Render the GSI button web experience. - Widget renderButton({GSIButtonConfiguration? configuration}) { - final GSIButtonConfiguration config = - configuration ?? GSIButtonConfiguration(); - return FutureBuilder( - key: Key(config.hashCode.toString()), - future: initialized, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return FlexHtmlElementView( - viewType: 'gsi_login_button', - onElementCreated: (Object element) { - _gisClient.renderButton(element, config); - }); - } - return const Text('Getting ready'); - }, - ); - } - @override - Future signInSilently() async { + Future attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) async { await initialized; // The new user is being injected from the `userDataEvents` Stream. - return _gisClient.signInSilently(); - } - - @override - Future signIn() async { - if (kDebugMode) { - web.console.warn( - "The `signIn` method is discouraged on the web because it can't reliably provide an `idToken`.\n" - 'Use `signInSilently` and `renderButton` to authenticate your users instead.\n' - 'Read more: https://pub.dev/packages/google_sign_in_web' - .toJS); - } - await initialized; + _gisClient.requestOneTap(); - // This method mainly does oauth2 authorization, which happens to also do - // authentication if needed. However, the authentication information is not - // returned anymore. - // - // This method will synthesize authentication information from the People API - // if needed (or use the last identity seen from signInSilently). - try { - return _gisClient.signIn(); - } catch (reason) { - throw PlatformException( - code: reason.toString(), - message: 'Exception raised from signIn', - details: - 'https://developers.google.com/identity/oauth2/web/guides/error', - ); - } + // One tap does not necessarily return immediately, and may never return, + // so clients should not await it. Return null to signal that. + return null; } @override - Future getTokens({ - required String email, - bool? shouldRecoverAuth, - }) async { - await initialized; + bool supportsAuthenticate() => false; - return _gisClient.getTokens(); + @override + Future authenticate( + AuthenticateParameters params) async { + throw UnimplementedError('authenticate is not supported on the web. ' + 'Instead, use renderButton to create a sign-in widget.'); } @override - Future signOut() async { + Future signOut(SignOutParams params) async { await initialized; await _gisClient.signOut(); } @override - Future disconnect() async { + Future disconnect(DisconnectParams params) async { await initialized; await _gisClient.disconnect(); } @override - Future isSignedIn() async { + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params) async { await initialized; - return _gisClient.isSignedIn(); + final String? token = await _gisClient.requestScopes(params.request.scopes, + promptIfUnauthorized: params.request.promptIfUnauthorized, + userHint: params.request.userId); + return token == null + ? null + : ClientAuthorizationTokenData(accessToken: token); } @override - Future clearAuthCache({required String token}) async { + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params) async { await initialized; - await _gisClient.clearAuthCache(); - } - - @override - Future requestScopes(List scopes) async { - await initialized; + // There is no way to know whether the flow will prompt in advance, so + // always return null if prompting isn't allowed. + if (!params.request.promptIfUnauthorized) { + return null; + } - return _gisClient.requestScopes(scopes); + final String? code = await _gisClient.requestServerAuthCode(params.request); + return code == null + ? null + : ServerAuthorizationTokenData(serverAuthCode: code); } @override - Future canAccessScopes(List scopes, - {String? accessToken}) async { - await initialized; + Stream get authenticationEvents => + _authenticationController.stream; - return _gisClient.canAccessScopes(scopes, accessToken); - } + // -------- - @override - Stream? get userDataEvents => - _userDataController.stream; + // Register a factory for the Button HtmlElementView. + void _registerButtonFactory() { + ui_web.platformViewRegistry.registerViewFactory( + 'gsi_login_button', + (int viewId) { + final web.Element element = web.document.createElement('div'); + element.setAttribute('style', + 'width: 100%; height: 100%; overflow: hidden; display: flex; flex-wrap: wrap; align-content: center; justify-content: center;'); + element.id = 'sign_in_button_$viewId'; + return element; + }, + ); + } - /// Requests server auth code from GIS Client per: - /// https://developers.google.com/identity/oauth2/web/guides/use-code-model#initialize_a_code_client - Future requestServerAuthCode() async { - await initialized; - return _gisClient.requestServerAuthCode(); + /// Render the GSI button web experience. + Widget renderButton({GSIButtonConfiguration? configuration}) { + final GSIButtonConfiguration config = + configuration ?? GSIButtonConfiguration(); + return FutureBuilder( + key: Key(config.hashCode.toString()), + future: initialized, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return FlexHtmlElementView( + viewType: 'gsi_login_button', + onElementCreated: (Object element) { + _gisClient.renderButton(element, config); + }); + } + return const Text('Getting ready'); + }, + ); } } diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart index 421ce1a187f..c3844642f6d 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart @@ -12,7 +12,6 @@ import 'package:web/web.dart' as web; import 'button_configuration.dart' show GSIButtonConfiguration, convertButtonConfiguration; -import 'people.dart' as people; import 'utils.dart' as utils; /// A client to hide (most) of the interaction with the GIS SDK from the plugin. @@ -21,44 +20,27 @@ import 'utils.dart' as utils; class GisSdkClient { /// Create a GisSdkClient object. GisSdkClient({ - required List initialScopes, required String clientId, - required StreamController userDataController, + required StreamController authenticationController, bool loggingEnabled = false, String? hostedDomain, - }) : _initialScopes = initialScopes, + }) : _clientId = clientId, + _hostedDomain = hostedDomain, _loggingEnabled = loggingEnabled, - _userDataEventsController = userDataController { + _authenticationController = authenticationController { if (_loggingEnabled) { id.setLogLevel('debug'); } - // Configure the Stream objects that are going to be used by the clients. - _configureStreams(); + _configureAuthenticationStream(); - // Initialize the SDK clients we need. + // Initialize the authentication SDK client. Authorization clients will be + // created as one-offs as needed. _initializeIdClient( clientId, onResponse: _onCredentialResponse, hostedDomain: hostedDomain, useFedCM: true, ); - - _tokenClient = _initializeTokenClient( - clientId, - hostedDomain: hostedDomain, - onResponse: _onTokenResponse, - onError: _onTokenError, - ); - - if (initialScopes.isNotEmpty) { - _codeClient = _initializeCodeClient( - clientId, - hostedDomain: hostedDomain, - onResponse: _onCodeResponse, - onError: _onCodeError, - scopes: initialScopes, - ); - } } void _logIfEnabled(String message, [List? more]) { @@ -69,55 +51,31 @@ class GisSdkClient { } } - // Configure the credential (authentication) and token (authorization) response streams. - void _configureStreams() { - _tokenResponses = StreamController.broadcast(); + // Configure the credential (authentication) response stream. + void _configureAuthenticationStream() { _credentialResponses = StreamController.broadcast(); - _codeResponses = StreamController.broadcast(); - - _tokenResponses.stream.listen((TokenResponse response) { - _lastTokenResponse = response; - _lastTokenResponseExpiration = - DateTime.now().add(Duration(seconds: response.expires_in!)); - }, onError: (Object error) { - _logIfEnabled('Error on TokenResponse:', [error.toString()]); - _lastTokenResponse = null; - }); - - _codeResponses.stream.listen((CodeResponse response) { - _lastCodeResponse = response; - }, onError: (Object error) { - _logIfEnabled('Error on CodeResponse:', [error.toString()]); - _lastCodeResponse = null; - }); - - _credentialResponses.stream.listen((CredentialResponse response) { - _lastCredentialResponse = response; - }, onError: (Object error) { - _logIfEnabled('Error on CredentialResponse:', [error.toString()]); - _lastCredentialResponse = null; - }); // In the future, the userDataEvents could propagate null userDataEvents too. _credentialResponses.stream - .map(utils.gisResponsesToUserData) - .handleError(_cleanCredentialResponsesStreamErrors) - .forEach(_userDataEventsController.add); + .map(utils.gisResponsesToAuthenticationEvent) + .handleError(_convertCredentialResponsesStreamErrors) + .forEach(_authenticationController.add); } // This function handles the errors that on the _credentialResponses Stream. // - // Most of the time, these errors are part of the flow (like when One Tap UX - // cannot be rendered), and the stream of userDataEvents doesn't care about - // them. - // // (This has been separated to a function so the _configureStreams formatting // looks a little bit better) - void _cleanCredentialResponsesStreamErrors(Object error) { - _logIfEnabled( - 'Removing error from `userDataEvents`:', - [error.toString()], - ); + void _convertCredentialResponsesStreamErrors(Object error) { + _logIfEnabled('Error on CredentialResponse:', [error.toString()]); + if (error is GoogleSignInException) { + _authenticationController.add(AuthenticationEventException(error)); + } else { + _authenticationController.add(AuthenticationEventException( + GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: error.toString()))); + } } // Initializes the `id` SDK for the silent-sign in (authentication) client. @@ -144,8 +102,9 @@ class GisSdkClient { // // (Normal doesn't mean successful, this might contain `error` information.) void _onCredentialResponse(CredentialResponse response) { - if (response.error != null) { - _credentialResponses.addError(response.error!); + final String? error = response.error; + if (error != null) { + _credentialResponses.addError(error); } else { _credentialResponses.add(response); } @@ -154,99 +113,53 @@ class GisSdkClient { // Creates a `oauth2.TokenClient` used for authorization (scope) requests. TokenClient _initializeTokenClient( String clientId, { + required List scopes, + String? userHint, String? hostedDomain, required TokenClientCallbackFn onResponse, required ErrorCallbackFn onError, }) { // Create a Token Client for authorization calls. final TokenClientConfig tokenConfig = TokenClientConfig( + prompt: userHint == null ? '' : 'select_account', client_id: clientId, + login_hint: userHint, hd: hostedDomain, - callback: _onTokenResponse, - error_callback: _onTokenError, - // This is here only to satisfy the initialization of the JS TokenClient. - // In reality, `scope` is always overridden when calling `requestScopes` - // (or the deprecated `signIn`) through an [OverridableTokenClientConfig] - // object. - scope: [' '], // Fake (but non-empty) list of scopes. + callback: onResponse, + error_callback: onError, + scope: scopes, ); return oauth2.initTokenClient(tokenConfig); } - // Handle a "normal" token (authorization) response. - // - // (Normal doesn't mean successful, this might contain `error` information.) - void _onTokenResponse(TokenResponse response) { - if (response.error != null) { - _tokenResponses.addError(response.error!); - } else { - _tokenResponses.add(response); - } - } - - // Handle a "not-directly-related-to-authorization" error. - // - // Token clients have an additional `error_callback` for miscellaneous - // errors, like "popup couldn't open" or "popup closed by user". - void _onTokenError(GoogleIdentityServicesError? error) { - if (error != null) { - _tokenResponses.addError(error.type); - } - } - // Creates a `oauth2.CodeClient` used for authorization (scope) requests. - CodeClient _initializeCodeClient( - String clientId, { - String? hostedDomain, + CodeClient _initializeCodeClient({ + String? userHint, required List scopes, required CodeClientCallbackFn onResponse, required ErrorCallbackFn onError, }) { // Create a Token Client for authorization calls. final CodeClientConfig codeConfig = CodeClientConfig( - client_id: clientId, - hd: hostedDomain, - callback: _onCodeResponse, - error_callback: _onCodeError, + client_id: _clientId, + login_hint: userHint, + hd: _hostedDomain, + callback: onResponse, + error_callback: onError, scope: scopes, - select_account: true, + select_account: userHint == null, + include_granted_scopes: true, ux_mode: UxMode.popup, ); return oauth2.initCodeClient(codeConfig); } - void _onCodeResponse(CodeResponse response) { - if (response.error != null) { - _codeResponses.addError(response.error!); - } else { - _codeResponses.add(response); - } - } - - void _onCodeError(GoogleIdentityServicesError? error) { - if (error != null) { - _codeResponses.addError(error.type); - } - } - /// Attempts to sign-in the user using the OneTap UX flow. - /// - /// If the user consents, to OneTap, the [GoogleSignInUserData] will be - /// generated from a proper [CredentialResponse], which contains `idToken`. - /// Else, it'll be synthesized by a request to the People API later, and the - /// `idToken` will be null. - Future signInSilently() async { - final Completer userDataCompleter = - Completer(); - + void requestOneTap() { // Ask the SDK to render the OneClick sign-in. // // And also handle its "moments". - id.prompt((PromptMomentNotification moment) { - _onPromptMoment(moment, userDataCompleter); - }); - - return userDataCompleter.future; + id.prompt(_onPromptMoment); } // Handles "prompt moments" of the OneClick card UI. @@ -254,36 +167,37 @@ class GisSdkClient { // See: https://developers.google.com/identity/gsi/web/guides/receive-notifications-prompt-ui-status Future _onPromptMoment( PromptMomentNotification moment, - Completer completer, ) async { - if (completer.isCompleted) { - return; // Skip once the moment has been handled. + if (moment.isDismissedMoment()) { + final MomentDismissedReason? reason = moment.getDismissedReason(); + switch (reason) { + case MomentDismissedReason.credential_returned: + // Nothing to do here, as the success handler will run. + break; + case MomentDismissedReason.cancel_called: + _credentialResponses.addError(const GoogleSignInException( + code: GoogleSignInExceptionCode.canceled)); + case MomentDismissedReason.flow_restarted: + // Ignore, as this is not a final state. + break; + case MomentDismissedReason.unknown_reason: + case null: + _credentialResponses.addError(GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: 'dismissed: $reason')); + } + return; } - if (moment.isDismissedMoment() && - moment.getDismissedReason() == - MomentDismissedReason.credential_returned) { - // Kick this part of the handler to the bottom of the JS event queue, so - // the _credentialResponses stream has time to propagate its last value, - // and we can use _lastCredentialResponse. - return Future.delayed(Duration.zero, () { - completer - .complete(utils.gisResponsesToUserData(_lastCredentialResponse)); - }); + if (moment.isSkippedMoment()) { + // getSkippedReason is not used in the exception details here, per + // https://developers.google.com/identity/gsi/web/guides/fedcm-migration + _credentialResponses.addError(const GoogleSignInException( + code: GoogleSignInExceptionCode.canceled)); } - // In any other 'failed' moments, return null and add an error to the stream. - if (moment.isNotDisplayed() || - moment.isSkippedMoment() || - moment.isDismissedMoment()) { - final String reason = moment.getNotDisplayedReason()?.toString() ?? - moment.getSkippedReason()?.toString() ?? - moment.getDismissedReason()?.toString() ?? - 'unknown_error'; - - _credentialResponses.addError(reason); - completer.complete(null); - } + // isNotDisplayed is intentionally ignored, per + // https://developers.google.com/identity/gsi/web/guides/fedcm-migration } /// Calls `id.renderButton` on [parent] with the given [options]. @@ -296,208 +210,162 @@ class GisSdkClient { /// Requests a server auth code per: /// https://developers.google.com/identity/oauth2/web/guides/use-code-model#initialize_a_code_client - Future requestServerAuthCode() async { - // TODO(dit): Enable granular authorization, https://github.com/flutter/flutter/issues/139406 - assert(_codeClient != null, - 'CodeClient not initialized correctly. Ensure the `scopes` list passed to `init()` or `initWithParams()` is not empty!'); - if (_codeClient == null) { - return null; - } - _codeClient!.requestCode(); - final CodeResponse response = await _codeResponses.stream.first; - return response.code; - } - - // TODO(dit): Clean this up. https://github.com/flutter/flutter/issues/137727 - // - /// Starts an oauth2 "implicit" flow to authorize requests. - /// - /// The new GIS SDK does not return user authentication from this flow, so: - /// * If [_lastCredentialResponse] is **not** null (the user has successfully - /// `signInSilently`), we return that after this method completes. - /// * If [_lastCredentialResponse] is null, we add [people.scopes] to the - /// [_initialScopes], so we can retrieve User Profile information back - /// from the People API (without idToken). See [people.requestUserData]. - @Deprecated( - 'Use `renderButton` instead. See: https://pub.dev/packages/google_sign_in_web#migrating-to-v011-and-v012-google-identity-services') - Future signIn() async { - // Warn users that this method will be removed. - web.console.warn( - 'The google_sign_in plugin `signIn` method is deprecated on the web, and will be removed in Q2 2024. Please use `renderButton` instead. See: ' - 'https://pub.dev/packages/google_sign_in_web#migrating-to-v011-and-v012-google-identity-services' - .toJS); - // If we already know the user, use their `email` as a `hint`, so they don't - // have to pick their user again in the Authorization popup. - final GoogleSignInUserData? knownUser = - utils.gisResponsesToUserData(_lastCredentialResponse); - // This toggles a popup, so `signIn` *must* be called with - // user activation. - _tokenClient.requestAccessToken(OverridableTokenClientConfig( - prompt: knownUser == null ? 'select_account' : '', - login_hint: knownUser?.email, - scope: [ - ..._initialScopes, - // If the user hasn't gone through the auth process, - // the plugin will attempt to `requestUserData` after, - // so we need extra scopes to retrieve that info. - if (_lastCredentialResponse == null) ...people.scopes, - ], - )); - - await _tokenResponses.stream.first; - - return _computeUserDataForLastToken(); - } + Future requestServerAuthCode( + AuthorizationRequestDetails request) async { + final Completer<(String? code, Exception? e)> completer = + Completer<(String? code, Exception? e)>(); + final CodeClient codeClient = _initializeCodeClient( + userHint: request.userId, + onResponse: (CodeResponse response) { + final String? error = response.error; + if (error == null) { + completer.complete((response.code, null)); + } else { + completer.complete(( + null, + GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: response.error_description, + details: 'code: $error') + )); + } + }, + onError: (GoogleIdentityServicesError? error) { + completer.complete((null, _exceptionForGisError(error))); + }, + scopes: request.scopes, + ); - // This function returns the currently signed-in [GoogleSignInUserData]. - // - // It'll do a request to the People API (if needed). - // - // TODO(dit): Clean this up. https://github.com/flutter/flutter/issues/137727 - Future _computeUserDataForLastToken() async { - // If the user hasn't authenticated, request their basic profile info - // from the People API. - // - // This synthetic response will *not* contain an `idToken` field. - if (_lastCredentialResponse == null && _requestedUserData == null) { - assert(_lastTokenResponse != null); - _requestedUserData = await people.requestUserData(_lastTokenResponse!); + codeClient.requestCode(); + final (String? code, Exception? e) = await completer.future; + if (e != null) { + throw e; } - // Complete user data either with the _lastCredentialResponse seen, - // or the synthetic _requestedUserData from above. - return utils.gisResponsesToUserData(_lastCredentialResponse) ?? - _requestedUserData; - } - - /// Returns a [GoogleSignInTokenData] from the latest seen responses. - GoogleSignInTokenData getTokens() { - return utils.gisResponsesToTokenData( - _lastCredentialResponse, - _lastTokenResponse, - _lastCodeResponse, - ); + return code; } /// Revokes the current authentication. Future signOut() async { - await clearAuthCache(); + _lastClientAuthorizationByUser.clear(); id.disableAutoSelect(); + _authenticationController.add(AuthenticationEventSignOut()); } - /// Revokes the current authorization and authentication. + /// Revokes all cached authorization tokens. Future disconnect() async { - if (_lastTokenResponse != null) { - oauth2.revoke(_lastTokenResponse!.access_token!); - } + _lastClientAuthorizationByUser.values + .map(((TokenResponse?, DateTime?) auth) => auth.$1?.access_token) + .nonNulls + .forEach(oauth2.revoke); + _lastClientAuthorizationByUser.clear(); await signOut(); } - /// Returns true if the client has recognized this user before, and the last-seen - /// credential is not expired. - Future isSignedIn() async { - bool isSignedIn = false; - if (_lastCredentialResponse != null) { - final DateTime? expiration = utils - .getCredentialResponseExpirationTimestamp(_lastCredentialResponse); - // All Google ID Tokens provide an "exp" date. If the method above cannot - // extract `expiration`, it's because `_lastCredentialResponse`'s contents - // are unexpected (or wrong) in any way. - // - // Users are considered to be signedIn when the last CredentialResponse - // exists and has an expiration date in the future. - // - // Users are not signed in in any other case. - // - // See: https://developers.google.com/identity/openid-connect/openid-connect#an-id-tokens-payload - isSignedIn = expiration?.isAfter(DateTime.now()) ?? false; - } - - return isSignedIn || _requestedUserData != null; - } - - /// Clears all the cached results from authentication and authorization. - Future clearAuthCache() async { - _lastCredentialResponse = null; - _lastTokenResponse = null; - _requestedUserData = null; - _lastCodeResponse = null; - } - - /// Requests the list of [scopes] passed in to the client. + /// Requests the given list of [scopes], and returns the resulting + /// authorization token if successful. /// /// Keeps the previously granted scopes. - Future requestScopes(List scopes) async { - // If we already know the user, use their `email` as a `hint`, so they don't - // have to pick their user again in the Authorization popup. - final GoogleSignInUserData? knownUser = - utils.gisResponsesToUserData(_lastCredentialResponse); - - _tokenClient.requestAccessToken(OverridableTokenClientConfig( - prompt: knownUser == null ? 'select_account' : '', - login_hint: knownUser?.email, - scope: scopes, - include_granted_scopes: true, - )); + Future requestScopes(List scopes, + {required bool promptIfUnauthorized, String? userHint}) async { + // If there's a usable cached token, return that. + final (TokenResponse? cachedResponse, DateTime? cacheExpiration) = + _lastClientAuthorizationByUser[userHint] ?? (null, null); + if (cachedResponse != null) { + final bool isTokenValid = + cacheExpiration?.isAfter(DateTime.now()) ?? false; + if (isTokenValid && oauth2.hasGrantedAllScopes(cachedResponse, scopes)) { + return cachedResponse.access_token; + } + } + + if (!promptIfUnauthorized) { + return null; + } - await _tokenResponses.stream.first; + final Completer<(String? token, Exception? e)> completer = + Completer<(String? token, Exception? e)>(); + final TokenClient tokenClient = _initializeTokenClient( + _clientId, + scopes: scopes, + userHint: userHint, + hostedDomain: _hostedDomain, + onResponse: (TokenResponse response) { + final String? error = response.error; + if (error == null) { + final String? token = response.access_token; + if (token == null) { + _lastClientAuthorizationByUser.remove(userHint); + } else { + final DateTime expiration = + DateTime.now().add(Duration(seconds: response.expires_in!)); + _lastClientAuthorizationByUser[userHint] = (response, expiration); + } + completer.complete((response.access_token, null)); + } else { + _lastClientAuthorizationByUser.remove(userHint); + completer.complete(( + null, + GoogleSignInException( + code: GoogleSignInExceptionCode.unknownError, + description: response.error_description, + details: 'code: $error') + )); + } + }, + onError: (GoogleIdentityServicesError? error) { + _lastClientAuthorizationByUser.remove(userHint); + completer.complete((null, _exceptionForGisError(error))); + }, + ); + tokenClient.requestAccessToken(); - return oauth2.hasGrantedAllScopes(_lastTokenResponse!, scopes); + final (String? token, Exception? e) = await completer.future; + if (e != null) { + throw e; + } + return token; } - /// Checks if the passed-in `accessToken` can access all `scopes`. - /// - /// This validates that the `accessToken` is the same as the last seen - /// token response, that the token is not expired, then uses that response to - /// check if permissions are still granted. - Future canAccessScopes(List scopes, String? accessToken) async { - if (accessToken != null && _lastTokenResponse != null) { - if (accessToken == _lastTokenResponse!.access_token) { - final bool isTokenValid = - _lastTokenResponseExpiration?.isAfter(DateTime.now()) ?? false; - return isTokenValid && - oauth2.hasGrantedAllScopes(_lastTokenResponse!, scopes); - } + GoogleSignInException _exceptionForGisError( + GoogleIdentityServicesError? error) { + final GoogleSignInExceptionCode code; + switch (error?.type) { + case GoogleIdentityServicesErrorType.missing_required_parameter: + code = GoogleSignInExceptionCode.clientConfigurationError; + case GoogleIdentityServicesErrorType.popup_closed: + code = GoogleSignInExceptionCode.canceled; + case GoogleIdentityServicesErrorType.popup_failed_to_open: + code = GoogleSignInExceptionCode.uiUnavailable; + case GoogleIdentityServicesErrorType.unknown: + case null: + code = GoogleSignInExceptionCode.unknownError; } - return false; + return GoogleSignInException( + code: code, + description: error?.message ?? 'SDK returned no error details'); } final bool _loggingEnabled; - // The scopes initially requested by the developer. - // - // We store this because we might need to add more at `signIn`. If the user - // doesn't `silentSignIn`, we expand this list to consult the People API to - // return some basic Authentication information. - final List _initialScopes; + // The identifier of this web client. + final String _clientId; - // The Google Identity Services client for oauth requests. - late TokenClient _tokenClient; - // CodeClient will not be created if `initialScopes` is empty. - CodeClient? _codeClient; + /// The domain to restrict logins to. + final String? _hostedDomain; - // Streams of credential and token responses. + // Stream of credential responses from sign-in events. late StreamController _credentialResponses; - late StreamController _tokenResponses; - late StreamController _codeResponses; - // The last-seen credential and token responses - CredentialResponse? _lastCredentialResponse; - TokenResponse? _lastTokenResponse; - // Expiration timestamp for the lastTokenResponse, which only has an `expires_in` field. - DateTime? _lastTokenResponseExpiration; - CodeResponse? _lastCodeResponse; + // The last client authorization token responses, keyed by the user ID the + // authorization was requested for. A nil key stores the last authorization + // request that was not associated with a known user (i.e., no user ID hint + // was provided with the request). + final Map + _lastClientAuthorizationByUser = + {}; /// The StreamController onto which the GIS Client propagates user authentication events. /// /// This is provided by the implementation of the plugin. - final StreamController _userDataEventsController; - - // If the user *authenticates* (signs in) through oauth2, the SDK doesn't return - // identity information anymore, so we synthesize it by calling the PeopleAPI - // (if needed) - // - // (This is a synthetic _lastCredentialResponse) - // - // TODO(dit): Clean this up. https://github.com/flutter/flutter/issues/137727 - GoogleSignInUserData? _requestedUserData; + final StreamController _authenticationController; } diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart index 05ed6a877d1..0f27f04d195 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'package:google_identity_services_web/id.dart'; -import 'package:google_identity_services_web/oauth2.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; /// A codec that can encode/decode JWT payloads. @@ -65,7 +64,7 @@ Map? getResponsePayload(CredentialResponse? response) { /// /// May return `null`, if the `credentialResponse` is null, or its `credential` /// cannot be decoded. -GoogleSignInUserData? gisResponsesToUserData( +AuthenticationEvent? gisResponsesToAuthenticationEvent( CredentialResponse? credentialResponse) { final Map? payload = getResponsePayload(credentialResponse); if (payload == null) { @@ -75,12 +74,15 @@ GoogleSignInUserData? gisResponsesToUserData( assert(credentialResponse?.credential != null, 'The CredentialResponse cannot be null and have a payload.'); - return GoogleSignInUserData( - email: payload['email']! as String, - id: payload['sub']! as String, - displayName: payload['name'] as String?, - photoUrl: payload['picture'] as String?, - idToken: credentialResponse!.credential, + return AuthenticationEventSignIn( + user: GoogleSignInUserData( + email: payload['email']! as String, + id: payload['sub']! as String, + displayName: payload['name'] as String?, + photoUrl: payload['picture'] as String?, + ), + authenticationTokens: + AuthenticationTokenData(idToken: credentialResponse!.credential), ); } @@ -96,16 +98,3 @@ DateTime? getCredentialResponseExpirationTimestamp( // Return 'exp' (a timestamp in seconds since Epoch) as a DateTime. return (exp != null) ? DateTime.fromMillisecondsSinceEpoch(exp * 1000) : null; } - -/// Converts responses from the GIS library into TokenData for the plugin. -GoogleSignInTokenData gisResponsesToTokenData( - CredentialResponse? credentialResponse, - TokenResponse? tokenResponse, [ - CodeResponse? codeResponse, -]) { - return GoogleSignInTokenData( - idToken: credentialResponse?.credential, - accessToken: tokenResponse?.access_token, - serverAuthCode: codeResponse?.code, - ); -} diff --git a/packages/google_sign_in/google_sign_in_web/lib/web_only.dart b/packages/google_sign_in/google_sign_in_web/lib/web_only.dart index 34f153d0aef..899a88cd45a 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/web_only.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/web_only.dart @@ -39,10 +39,3 @@ GoogleSignInPlugin get _plugin { Widget renderButton({GSIButtonConfiguration? configuration}) { return _plugin.renderButton(configuration: configuration); } - -/// Requests server auth code from the GIS Client. -/// -/// See: https://developers.google.com/identity/oauth2/web/guides/use-code-model -Future requestServerAuthCode() async { - return _plugin.requestServerAuthCode(); -} From eba7a8bcafa69b1679c0b2ef59d6e842b5d003ee Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 2 May 2025 13:27:49 -0400 Subject: [PATCH 05/52] Add nonce support --- .../google_sign_in/lib/google_sign_in.dart | 9 ++++++++- .../plugins/googlesignin/GoogleSignInPlugin.java | 4 ++++ .../kotlin/io/flutter/plugins/googlesignin/Messages.kt | 8 ++++++-- .../lib/google_sign_in_android.dart | 5 ++++- .../google_sign_in_android/lib/src/messages.g.dart | 5 +++++ .../google_sign_in_android/pigeons/messages.dart | 2 ++ .../Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m | 10 +++++++--- .../include/google_sign_in_ios/messages.g.h | 1 + .../Sources/google_sign_in_ios/messages.g.m | 6 ++++-- .../google_sign_in_ios/lib/google_sign_in_ios.dart | 7 +++++-- .../google_sign_in_ios/lib/src/messages.g.dart | 6 +++--- .../google_sign_in_ios/pigeons/messages.dart | 4 ++-- .../lib/src/types.dart | 4 ++++ .../google_sign_in_web/lib/google_sign_in_web.dart | 1 + .../google_sign_in_web/lib/src/gis_client.dart | 4 ++++ 15 files changed, 60 insertions(+), 16 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index e0af08455fc..a7c41a1099f 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -251,16 +251,21 @@ class GoogleSignIn { /// required. See the README for details. If provided, it will take precedence /// over any value in a configuration file. /// + /// If provided, [nonce] will be passed as part of any authentication + /// requests, to allow additional validation of the resulting ID token. + /// /// If provided, [hostedDomain] restricts account selection to accounts in /// that domain. Future initialize({ String? clientId, String? serverClientId, + String? nonce, String? hostedDomain, }) async { await GoogleSignInPlatform.instance.init(InitParameters( clientId: clientId, serverClientId: serverClientId, + nonce: nonce, hostedDomain: hostedDomain, )); @@ -420,7 +425,9 @@ class GoogleSignIn { {List scopeHint = const []}) async { try { final AuthenticationResults result = await GoogleSignInPlatform.instance - .authenticate(AuthenticateParameters(scopeHint: scopeHint)); + .authenticate(AuthenticateParameters( + scopeHint: scopeHint, + )); final GoogleSignInAccount account = GoogleSignInAccount._(result.user, result.authenticationTokens); if (_createAuthenticationStreamEvents) { diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 8d4fcdc6dac..4b4fd4ea251 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -184,6 +184,10 @@ public void getCredential( .setFilterByAuthorizedAccounts(params.getFilterToAuthorized()) .setAutoSelectEnabled(params.getAutoSelectEnabled()) .setServerClientId(serverClientId); + String nonce = params.getNonce(); + if (nonce != null) { + optionBuilder.setNonce(nonce); + } GetGoogleIdOption googleIdOption = optionBuilder.build(); GetCredentialRequest request = new GetCredentialRequest.Builder().addCredentialOption(googleIdOption).build(); diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt index 5edd189f256..8fcaf52eab7 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt @@ -147,14 +147,17 @@ data class PlatformAuthorizationRequest( data class GetCredentialRequestParams( val filterToAuthorized: Boolean, val autoSelectEnabled: Boolean, - val serverClientId: String? = null + val serverClientId: String? = null, + val nonce: String? = null ) { companion object { fun fromList(pigeonVar_list: List): GetCredentialRequestParams { val filterToAuthorized = pigeonVar_list[0] as Boolean val autoSelectEnabled = pigeonVar_list[1] as Boolean val serverClientId = pigeonVar_list[2] as String? - return GetCredentialRequestParams(filterToAuthorized, autoSelectEnabled, serverClientId) + val nonce = pigeonVar_list[3] as String? + return GetCredentialRequestParams( + filterToAuthorized, autoSelectEnabled, serverClientId, nonce) } } @@ -163,6 +166,7 @@ data class GetCredentialRequestParams( filterToAuthorized, autoSelectEnabled, serverClientId, + nonce, ) } } diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index fd7947acda4..5e99d824d3e 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -26,6 +26,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { String? _serverClientId; String? _hostedDomain; + String? _nonce; /// Registers this class as the default instance of [GoogleSignInPlatform]. static void registerWith() { @@ -37,6 +38,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { _hostedDomain = params.hostedDomain; _serverClientId = params.serverClientId ?? await _credentialManaagerApi.getGoogleServicesJsonServerClientId(); + _nonce = params.nonce; // The clientId parameter is not supported on Android. // Android apps are identified by their package name and the SHA-1 of their signing key. } @@ -119,7 +121,8 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { await _credentialManaagerApi.getCredential(GetCredentialRequestParams( filterToAuthorized: filterToAuthorized, autoSelectEnabled: autoSelectEnabled, - serverClientId: _serverClientId)); + serverClientId: _serverClientId, + nonce: _nonce)); switch (authnResult) { case GetCredentialFailure(): String? message = authnResult.message; diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart index 6ffff824565..3c0146b26c1 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -119,6 +119,7 @@ class GetCredentialRequestParams { required this.filterToAuthorized, required this.autoSelectEnabled, this.serverClientId, + this.nonce, }); bool filterToAuthorized; @@ -127,11 +128,14 @@ class GetCredentialRequestParams { String? serverClientId; + String? nonce; + Object encode() { return [ filterToAuthorized, autoSelectEnabled, serverClientId, + nonce, ]; } @@ -141,6 +145,7 @@ class GetCredentialRequestParams { filterToAuthorized: result[0]! as bool, autoSelectEnabled: result[1]! as bool, serverClientId: result[2] as String?, + nonce: result[3] as String?, ); } } diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart index 3b2e4fb2f3c..127f740efe3 100644 --- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -35,10 +35,12 @@ class GetCredentialRequestParams { required this.filterToAuthorized, required this.autoSelectEnabled, this.serverClientId, + this.nonce, }); bool filterToAuthorized; bool autoSelectEnabled; String? serverClientId; + String? nonce; } /// Pigeon equivalent of the native GoogleIdTokenCredential. diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m index e23b5d216d3..05fd859ea83 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m @@ -182,12 +182,14 @@ - (void)restorePreviousSignInWithCompletion:(nonnull void (^)(FSISignInResult *_ } - (void)signInWithScopeHint:(NSArray *)scopeHint + nonce:(nullable NSString *)nonce completion:(nonnull void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion { @try { __weak typeof(self) weakSelf = self; [self signInWithHint:nil additionalScopes:scopeHint + nonce:nonce completion:^(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error) { [weakSelf handleAuthResultWithUser:signInResult.user serverAuthCode:signInResult.serverAuthCode @@ -273,8 +275,12 @@ - (void)disconnectWithCompletion:(nonnull void (^)(FlutterError *_Nullable))comp // Wraps the iOS and macOS sign in display methods. - (void)signInWithHint:(nullable NSString *)hint additionalScopes:(nullable NSArray *)additionalScopes + nonce:(nullable NSString *)nonce completion:(void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion { + // TODO(stuartmorgan): Add the nonce parameter to the calls below once it's available; it was + // added after 8.0, and based on https://github.com/google/GoogleSignIn-iOS/releases appears to + // be slated for an 8.1 release. See https://github.com/flutter/flutter/issues/85439. #if TARGET_OS_OSX [self.signIn signInWithPresentingWindow:self.registrar.view.window hint:hint @@ -296,9 +302,7 @@ - (void)addScopes:(NSArray *)scopes #if TARGET_OS_OSX [user addScopes:scopes presentingWindow:self.registrar.view.window completion:completion]; #else - [user addScopes:scopes - presentingViewController:[self topViewController] - completion:completion]; + [user addScopes:scopes presentingViewController:[self topViewController] completion:completion]; #endif } diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h index b12ff4e3698..7bea4b5f533 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h @@ -138,6 +138,7 @@ NSObject *FSIGetMessagesCodec(void); FlutterError *_Nullable))completion; /// Starts a sign in with user interaction. - (void)signInWithScopeHint:(NSArray *)scopeHint + nonce:(nullable NSString *)nonce completion: (void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion; /// Requests the access token for the current sign in. diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m index cdacda48fcf..7aae8b9968a 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m @@ -376,14 +376,16 @@ void SetUpFSIGoogleSignInApiWithSuffix(id binaryMessenge binaryMessenger:binaryMessenger codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(signInWithScopeHint:completion:)], + NSCAssert([api respondsToSelector:@selector(signInWithScopeHint:nonce:completion:)], @"FSIGoogleSignInApi api (%@) doesn't respond to " - @"@selector(signInWithScopeHint:completion:)", + @"@selector(signInWithScopeHint:nonce:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSArray *arg_scopeHint = GetNullableObjectAtIndex(args, 0); + NSString *arg_nonce = GetNullableObjectAtIndex(args, 1); [api signInWithScopeHint:arg_scopeHint + nonce:arg_nonce completion:^(FSISignInResult *_Nullable output, FlutterError *_Nullable error) { callback(wrapResult(output, error)); diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart index d4a7291aa19..5d61bcdcde3 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -18,6 +18,8 @@ class GoogleSignInIOS extends GoogleSignInPlatform { final GoogleSignInApi _api; + String? _nonce; + /// Registers this class as the default instance of [GoogleSignInPlatform]. static void registerWith() { GoogleSignInPlatform.instance = GoogleSignInIOS(); @@ -25,6 +27,7 @@ class GoogleSignInIOS extends GoogleSignInPlatform { @override Future init(InitParameters params) async { + _nonce = params.nonce; await _api.configure(PlatformConfigurationParams( clientId: params.clientId, serverClientId: params.serverClientId, @@ -59,7 +62,7 @@ class GoogleSignInIOS extends GoogleSignInPlatform { @override Future authenticate( AuthenticateParameters params) async { - final SignInResult result = await _api.signIn(params.scopeHint); + final SignInResult result = await _api.signIn(params.scopeHint, _nonce); // This should never happen; the corresponding native error code is // documented as being specific to restorePreviousSignIn. @@ -129,7 +132,7 @@ class GoogleSignInIOS extends GoogleSignInPlatform { if (success == null) { // There's no existing sign-in to use, so return the results of the // combined authn+authz flow. - result = await _api.signIn(request.scopes); + result = await _api.signIn(request.scopes, _nonce); return _processAuthorizationResult(result); } else { // Discard the authentication information, and extract the user ID to diff --git a/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart index f5607f5d64e..f4e23701c04 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/src/messages.g.dart @@ -358,7 +358,7 @@ class GoogleSignInApi { } /// Starts a sign in with user interaction. - Future signIn(List scopeHint) async { + Future signIn(List scopeHint, String? nonce) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.google_sign_in_ios.GoogleSignInApi.signIn$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = @@ -367,8 +367,8 @@ class GoogleSignInApi { pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final List? pigeonVar_replyList = - await pigeonVar_channel.send([scopeHint]) as List?; + final List? pigeonVar_replyList = await pigeonVar_channel + .send([scopeHint, nonce]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { diff --git a/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart index 7060c0197be..cb4161c96e6 100644 --- a/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart @@ -136,8 +136,8 @@ abstract class GoogleSignInApi { /// Starts a sign in with user interaction. @async - @ObjCSelector('signInWithScopeHint:') - SignInResult signIn(List scopeHint); + @ObjCSelector('signInWithScopeHint:nonce:') + SignInResult signIn(List scopeHint, String? nonce); /// Requests the access token for the current sign in. @async 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 d3e6d50e9b5..76384bac841 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 @@ -80,6 +80,7 @@ class InitParameters { const InitParameters({ this.clientId, this.serverClientId, + this.nonce, this.hostedDomain, }); @@ -107,6 +108,9 @@ class InitParameters { /// where you can find the details about the configuration files. final String? serverClientId; + /// 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. 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 6988cd8a3bb..58ce0b619ee 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 @@ -138,6 +138,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { _gisSdkClient ??= GisSdkClient( clientId: appClientId!, + nonce: params.nonce, hostedDomain: params.hostedDomain, authenticationController: _authenticationController, loggingEnabled: kDebugMode, diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart index c3844642f6d..90bfc7c141e 100644 --- a/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart +++ b/packages/google_sign_in/google_sign_in_web/lib/src/gis_client.dart @@ -23,6 +23,7 @@ class GisSdkClient { required String clientId, required StreamController authenticationController, bool loggingEnabled = false, + String? nonce, String? hostedDomain, }) : _clientId = clientId, _hostedDomain = hostedDomain, @@ -38,6 +39,7 @@ class GisSdkClient { _initializeIdClient( clientId, onResponse: _onCredentialResponse, + nonce: nonce, hostedDomain: hostedDomain, useFedCM: true, ); @@ -82,6 +84,7 @@ class GisSdkClient { void _initializeIdClient( String clientId, { required CallbackFn onResponse, + String? nonce, String? hostedDomain, bool? useFedCM, }) { @@ -91,6 +94,7 @@ class GisSdkClient { callback: onResponse, cancel_on_tap_outside: false, auto_select: true, // Attempt to sign-in silently. + nonce: nonce, hd: hostedDomain, use_fedcm_for_prompt: useFedCM, // Use the native browser prompt, when available. From f3d472fe2d8a3947dc9670a7058e15f43a25b79d Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 2 May 2025 16:19:59 -0400 Subject: [PATCH 06/52] Initial README updates and migration guide --- .../google_sign_in/MIGRATION.md | 62 +++++++ .../google_sign_in/google_sign_in/README.md | 174 ++++++++---------- .../google_sign_in/example/lib/main.dart | 89 +++++---- .../example/lib/src/sign_in_button.dart | 7 - .../lib/src/sign_in_button/mobile.dart | 15 -- .../example/lib/src/sign_in_button/stub.dart | 15 -- .../example/lib/src/sign_in_button/web.dart | 13 -- .../google_sign_in_android/README.md | 25 +++ .../google_sign_in_web/README.md | 104 +++++------ 9 files changed, 263 insertions(+), 241 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/MIGRATION.md delete mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart delete mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart delete mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart delete mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart diff --git a/packages/google_sign_in/google_sign_in/MIGRATION.md b/packages/google_sign_in/google_sign_in/MIGRATION.md new file mode 100644 index 00000000000..31eab83b3c9 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/MIGRATION.md @@ -0,0 +1,62 @@ +# Migrating from `google_sign_in` 6.x to 7.x + +The API of `google_sign_in` 6.x and earlier was designed for the Google Sign-In +SDK, which has been deprecated on both Android and Web, and replaced with new +SDKs that have significantly different structures. As a result, the +`google_sign_in` API surface has changed significantly. Notable differences +include: +* There is now an explicit `initialize` step that must be called excatly once, + before any other methods. On some platforms the future will complete almost + immediately, but on others (for example, web) it may take some time. + * `GoogleSignIn` is also now a singleton, which is obtained via + `GoogleSignIn.instance`. In practice, creating multiple `GoogleSignIn` + instances in 6.x would not work correctly, so this just enforces an existing + restriction. +* The plugin no longer tracks a single "current" signed in user. Instead, + applications that assume a single signed in user should track this at the + application level using the `authenticationEvents` stream. +* Authentication (signing in) and authorization (allowing access to user data + in the form of scopes) are now separate steps. Recommended practice is to + authenticate as soon as it makes sense for a user to potentially be signed in, + but to delay authorization until the point where the data will actually be + used. + * In applications where these steps should happen at the same time, you can + pass a `scopeHint` during the authentication step. On platforms that support + it this allows for a combined authentication and authorization UI flow, but + not all platforms allow combining them, so your application should be + prepared to trigger a separate authorization flow if necessary. + * Authorization is further separated into client and server authorization. + Applications that need a `serverAuthCode` must now call a separate method, + `authorizeServer`, to obtain that code. + * Client authorization is handled via two new methods: + * `authorizationForScopes`, which returns an access token if the requested + scopes are already authorized, or null if not, and + * `authorizeScopes`, which requests that the user authorize the scopes, and + is expected to show UI. + + Clients should generally attempt to get tokens via `authorizationForScopes`, + and if they are unable to do so, show some UI to request authoriaztion that + calls `authorizeScopes`. This is similar to the previously web-only flow + of calling `canAccessScopes` and then calling `addScopes` if necessary. +* `signInSilently` has been replaced with `attemptLightweightAuthentication`. + The intended usage is essentially the same, but the change reflects that it + is no longer guaranteed to be silent. For example, as of the publishing of + 7.0, on web this may show a floating sign-in card, and on Android it may show + an account selection sheet. + * This new method is no longer guaranteed to return a future. This allows + clients to distinguish, at runtime, platforms where a definitive "signed in" + or "not signed in" response can be returned quickly, and thus `await`-ing + completion is reasonable, in which case a `Future` is returned, and those + (such as web) where it could take an arbitrary amount of time, in which case + no `Future` is returned, and clients should assume a non-signed-in state + until/unless a sign-in event is eventually posted to the + `authenticationEvents` stream. +* `authenticate` replaces the authentication portion of `signIn` on platforms + that support it (see below). +* The new `supportsAuthenticate` method allows clients to determine at runtime + whether the `authenticate` method is supported, as some platforms do not allow + custom UI to trigger explicit authentication. These platforms instead provide + some other platform-specific way of triggering authentication. As of + publishing, the only platform that does not support `authenticate` is web, + where `google_sign_in_web`'s `renderButton` is used to create a sign-in + button. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 2bcac7b3768..64e84255e50 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -6,24 +6,17 @@ A Flutter plugin for [Google Sign In](https://developers.google.com/identity/). |-------------|---------|-------|--------|-----| | **Support** | SDK 21+ | 12.0+ | 10.15+ | Any | -## Platform integration +## Setup -### Android integration +### Import the package -To access Google Sign-In, you'll need to make sure to -[register your application](https://firebase.google.com/docs/android/setup). +To use this plugin, follow the +[plugin installation instructions](https://pub.dev/packages/google_sign_in/install), +then follow the platform integration steps below for all platforms you support. -You don't need to include the google-services.json file in your app unless you -are using Google services that require it. You do need to enable the OAuth APIs -that you want, using the -[Google Cloud Platform API manager](https://console.developers.google.com/). For -example, if you want to mimic the behavior of the Google Sign-In sample app, -you'll need to enable the -[Google People API](https://developers.google.com/people/). +### Android integration -Make sure you've filled out all required fields in the console for -[OAuth consent screen](https://console.developers.google.com/apis/credentials/consent). -Otherwise, you may encounter `APIException` errors. +Please see [instructions on integrating Google Sign-In on Android](https://pub.dev/packages/google_sign_in_android#integration). ### iOS integration @@ -61,100 +54,86 @@ specific web integration details, see the ## Usage -### Import the package - -To use this plugin, follow the -[plugin installation instructions](https://pub.dev/packages/google_sign_in/install). - -### Use the plugin +### Initialization and authentication -Initialize `GoogleSignIn` with the scopes you want: +Initialize the `GoogleSignIn` instance, and (optionally) start the lightweight +authentication process: - + ```dart -const List scopes = [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', -]; - -GoogleSignIn _googleSignIn = GoogleSignIn( - // Optional clientId - // clientId: 'your-client_id.apps.googleusercontent.com', - scopes: scopes, -); +final GoogleSignIn signIn = GoogleSignIn.instance; +unawaited(signIn + .initialize(clientId: clientId, serverClientId: serverClientId) + .then((_) { + signIn.authenticationEvents.listen(_handleAuthenticationEvent); + + /// This example always uses the stream-based approach to determining + /// which UI state to show, rather than using the future returned here, + /// if any, to conditionally skip directly to the signed-in state. + signIn.attemptLightweightAuthentication(); +})); ``` -[Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). - -You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. +If the user isn't signed in by the lightweight method, you can show UI to +start a sign-in flow: - + ```dart -Future _handleSignIn() async { - try { - await _googleSignIn.signIn(); - } catch (error) { - print(error); - } -} +if (GoogleSignIn.instance.supportsAuthenticate()) + ElevatedButton( + onPressed: () async { + try { + await GoogleSignIn.instance.authenticate(); + } catch (e) { + // ··· + } + }, + child: const Text('SIGN IN'), + ) +else ...[ + if (kIsWeb) + web.renderButton() + // ··· +] ``` -In the web, you should use the **Google Sign In button** (and not the `signIn` method) -to guarantee that your user authentication contains a valid `idToken`. - -For more details, take a look at the -[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). - -## Working with scopes, and incremental authorization. - -If your app supports both mobile and web, read this section! +## Authorization ### Checking if scopes have been granted -Users may (or may *not*) grant all the scopes that an application requests at -Sign In. In fact, in the web, no scopes are granted by `signIn`, `silentSignIn` -or the `renderButton` widget anymore. +If the user has previously authorized the scopes required by you application, +you can silently request an access token for those scopes: -Applications must be able to: - -* Detect if the authenticated user has authorized the scopes they need. -* Determine if the scopes that were granted a few minutes ago are still valid. - -There's a new method that enables the checks above, `canAccessScopes`: - - + ```dart -// In mobile, being authenticated means being authorized... -bool isAuthorized = account != null; -// However, on web... -if (kIsWeb && account != null) { - isAuthorized = await _googleSignIn.canAccessScopes(scopes); -} +const List scopes = [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', +]; +// ··· + GoogleSignInAccount? user; + // ··· + GoogleSignInClientAuthorization? authorization; + if (user != null) { + authorization = + await user.authorizationClient.authorizationForScopes(scopes); + } ``` -_(Only implemented in the web platform, from version 6.1.0 of this package)_ +[Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). ### Requesting more scopes when needed If an app determines that the user hasn't granted the scopes it requires, it -should initiate an Authorization request. (Remember that in the web platform, -this request **must be initiated from an user interaction**, like a button press). +should initiate an Authorization request. On some platforms, such as web, +this request **must be initiated from an user interaction** like a button press. - + ```dart -Future _handleAuthorizeScopes() async { - final bool isAuthorized = await _googleSignIn.requestScopes(scopes); - if (isAuthorized) { - unawaited(_handleGetContact(_currentUser!)); - } +final GoogleSignInClientAuthorization authorization = + await user.authorizationClient.authorizeScopes(scopes); ``` -The `requestScopes` returns a `boolean` value that is `true` if the user has -granted all the requested scopes or `false` otherwise. - -Once your app determines that the current user `isAuthorized` to access the -services for which you need `scopes`, it can proceed normally. - ### Authorization expiration In the web, **the `accessToken` is no longer refreshed**. It expires after 3600 @@ -167,21 +146,24 @@ the `canAccessScopes` and `requestScopes` methods described above. For more details, take a look at the [`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). -### Does an app always need to check `canAccessScopes`? +### Requesting a server auth code -The new web SDK implicitly grant access to the `email`, `profile` and `openid` -scopes when users complete the sign-in process (either via the One Tap UX or the -Google Sign In button). +If your application needs to access user data from a backend server, you can +request a server auth code to send to the server: -If an app only needs an `idToken`, or only requests permissions to any/all of -the three scopes mentioned above -([OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect)), -it won't need to implement any additional scope handling. + +```dart +final GoogleSignInServerAuthorization? serverAuth = + await user.authorizationClient.authorizeServer(scopes); +``` -If an app needs any scope other than `email`, `profile` and `openid`, it **must** -implement a more complete scope handling, as described above. +Server auth codes are not always available on all platforms. In general, if you +need a server auth code you should request it as soon as possible after initial +sign-in, and manage server tokens for that user entirely on the server side +unless the user signs in as a different user. ## Example -Find the example wiring in the -[Google sign-in example application](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart). +The +[Google Sign-In example application](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart) demonstrates one approach to using this +package to sign a user in and authorize access to specific user data. diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 58fa31da798..b9d1a3e3c51 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -7,12 +7,12 @@ import 'dart:async'; import 'dart:convert' show json; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:google_sign_in_web/web_only.dart' as web; import 'package:http/http.dart' as http; -import 'src/sign_in_button.dart'; - /// To run this example, replace this value with your client ID, and/or /// update the relevant configuration files, as described in the README. String? clientId; @@ -22,10 +22,12 @@ String? clientId; String? serverClientId; /// The scopes required by this application. +// #docregion CheckAuthorization const List scopes = [ 'email', 'https://www.googleapis.com/auth/contacts.readonly', ]; +// #enddocregion CheckAuthorization void main() { runApp( @@ -56,23 +58,26 @@ class _SignInDemoState extends State { void initState() { super.initState(); + // #docregion Setup final GoogleSignIn signIn = GoogleSignIn.instance; unawaited(signIn .initialize(clientId: clientId, serverClientId: serverClientId) .then((_) { - GoogleSignIn.instance.authenticationEvents - .listen(_handleAuthenticationEvent); + signIn.authenticationEvents.listen(_handleAuthenticationEvent); /// This example always uses the stream-based approach to determining /// which UI state to show, rather than using the future returned here, /// if any, to conditionally skip directly to the signed-in state. signIn.attemptLightweightAuthentication(); })); + // #enddocregion Setup } Future _handleAuthenticationEvent( GoogleSignInAuthenticationEvent event) async { + // #docregion CheckAuthorization GoogleSignInAccount? user; + // #enddocregion CheckAuthorization String error = ''; switch (event) { case GoogleSignInAuthenticationEventSignIn(): @@ -85,25 +90,24 @@ class _SignInDemoState extends State { error = 'GoogleSignInException ${e.code}: ${e.description}'; } - // #docregion CanAccessScopes // Check for existing authorization. - bool isAuthorized = false; + // #docregion CheckAuthorization + GoogleSignInClientAuthorization? authorization; if (user != null) { - final GoogleSignInClientAuthorization? authorization = + authorization = await user.authorizationClient.authorizationForScopes(scopes); - isAuthorized = authorization != null; } - // #enddocregion CanAccessScopes + // #enddocregion CheckAuthorization setState(() { _currentUser = user; - _isAuthorized = isAuthorized; + _isAuthorized = authorization != null; _errorMessage = error; }); // Now that we know that the user can access the required scopes, the app // can call the REST API. - if (user != null && isAuthorized) { + if (user != null && authorization != null) { unawaited(_handleGetContact(user)); } } @@ -166,38 +170,26 @@ class _SignInDemoState extends State { return null; } - // This is the on-click handler for the Sign In button that is rendered by Flutter. - // - // On the web, the on-click handler of the Sign In button is owned by the JS - // SDK, so this method can be considered mobile only. - // #docregion SignIn - Future _handleSignIn() async { - try { - await GoogleSignIn.instance.authenticate(); - } catch (e) { - _errorMessage = e.toString(); - } - } - // #enddocregion SignIn - // Prompts the user to authorize `scopes`. // // On the web, this must be called from an user interaction (button click). - // #docregion RequestScopes Future _handleAuthorizeScopes(GoogleSignInAccount user) async { try { - // The returned tokens are ignored here since _handleGetContact uses the - // authorizationHeaders method to re-read the token cached here. - await user.authorizationClient.authorizeScopes(scopes); - + // #docregion RequestScopes + final GoogleSignInClientAuthorization authorization = + await user.authorizationClient.authorizeScopes(scopes); // #enddocregion RequestScopes + + // The returned tokens are ignored since _handleGetContact uses the + // authorizationHeaders method to re-read the token cached by this call. + // ignore: unnecessary_statements + authorization; + setState(() { _isAuthorized = true; _errorMessage = ''; }); - // #docregion RequestScopes unawaited(_handleGetContact(_currentUser!)); - // #enddocregion RequestScopes } on GoogleSignInException catch (e) { _errorMessage = 'GoogleSignInException ${e.code}: ${e.description}'; } @@ -206,13 +198,13 @@ class _SignInDemoState extends State { // Requests a server auth code for the authorized scopes. // // On the web, this must be called from an user interaction (button click). - // #docregion RequestScopes Future _handleGetAuthCode(GoogleSignInAccount user) async { try { + // #docregion RequestServerAuth final GoogleSignInServerAuthorization? serverAuth = await user.authorizationClient.authorizeServer(scopes); + // #enddocregion RequestServerAuth - // #enddocregion RequestScopes setState(() { _serverAuthCode = serverAuth == null ? '' : serverAuth.serverAuthCode; }); @@ -286,11 +278,30 @@ class _SignInDemoState extends State { List _buildUnauthenticatedWidgets() { return [ const Text('You are not currently signed in.'), - // This method is used to separate mobile from web code with conditional exports. - // See: src/sign_in_button.dart - buildSignInButton( - onPressed: _handleSignIn, - ), + // #docregion ExplicitSignIn + if (GoogleSignIn.instance.supportsAuthenticate()) + ElevatedButton( + onPressed: () async { + try { + await GoogleSignIn.instance.authenticate(); + } catch (e) { + // #enddocregion ExplicitSignIn + _errorMessage = e.toString(); + // #docregion ExplicitSignIn + } + }, + child: const Text('SIGN IN'), + ) + else ...[ + if (kIsWeb) + web.renderButton() + // #enddocregion ExplicitSignIn + else + const Text( + 'This platform does not have a known authentication method') + // #docregion ExplicitSignIn + ] + // #enddocregion ExplicitSignIn ]; } diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart deleted file mode 100644 index c0a33966312..00000000000 --- a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button.dart +++ /dev/null @@ -1,7 +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. - -export 'sign_in_button/stub.dart' - if (dart.library.js_util) 'sign_in_button/web.dart' - if (dart.library.io) 'sign_in_button/mobile.dart'; diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart deleted file mode 100644 index 8d929d7ef83..00000000000 --- a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/mobile.dart +++ /dev/null @@ -1,15 +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/material.dart'; - -import 'stub.dart'; - -/// Renders a SIGN IN button that calls `handleSignIn` onclick. -Widget buildSignInButton({HandleSignInFn? onPressed}) { - return ElevatedButton( - onPressed: onPressed, - child: const Text('SIGN IN'), - ); -} diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart deleted file mode 100644 index 85a54f0ac27..00000000000 --- a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/stub.dart +++ /dev/null @@ -1,15 +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/material.dart'; - -/// The type of the onClick callback for the (mobile) Sign In Button. -typedef HandleSignInFn = Future Function(); - -/// Renders a SIGN IN button that (maybe) calls the `handleSignIn` onclick. -Widget buildSignInButton({HandleSignInFn? onPressed}) { - return Container(); -} diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart b/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart deleted file mode 100644 index 5c854fc470b..00000000000 --- a/packages/google_sign_in/google_sign_in/example/lib/src/sign_in_button/web.dart +++ /dev/null @@ -1,13 +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/material.dart'; -import 'package:google_sign_in_web/web_only.dart' as web; - -import 'stub.dart'; - -/// Renders a web-only SIGN IN button. -Widget buildSignInButton({HandleSignInFn? onPressed}) { - return web.renderButton(); -} diff --git a/packages/google_sign_in/google_sign_in_android/README.md b/packages/google_sign_in/google_sign_in_android/README.md index aeaeb9df6e9..82f4a936ba3 100644 --- a/packages/google_sign_in/google_sign_in_android/README.md +++ b/packages/google_sign_in/google_sign_in_android/README.md @@ -13,3 +13,28 @@ should add it to your `pubspec.yaml` as usual. [1]: https://pub.dev/packages/google_sign_in [2]: https://flutter.dev/to/endorsed-federated-plugin + +## Integration + +To use Google Sign-In, you'll need to +[register your application](https://firebase.google.com/docs/android/setup). +If you are using Google Cloud Platform directl, rather than Firebase, you will +need to register both an Android application and a web application in the +[Google Cloud Platform API manager](https://console.developers.google.com/). + +* If you are use the `google-services.json` file and Gradle-based registration + system, no identifiers need to be provided in Dart when initializing the + `GoogleSignIn` instance. +* If you are not using `google-services.json`, you need to pass the client + ID of the *web* application you registered as the `serverClientId` when + initializing the `GoogleSignIn` instance. + +Make sure you've filled out all required fields in the console for +[OAuth consent screen](https://console.developers.google.com/apis/credentials/consent). +Otherwise, you may encounter `APIException` errors. + +You will also need to enable any OAuth APIs that you want, using the +[Google Cloud Platform API manager](https://console.developers.google.com/). For +example, if you want to mimic the behavior of the Google Sign-In sample app, +you'll need to enable the +[Google People API](https://developers.google.com/people/). diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index f51d1bd6359..3aeb3565095 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -2,6 +2,53 @@ The web implementation of [google_sign_in](https://pub.dev/packages/google_sign_in) +## Usage + +This package is [endorsed](https://flutter.dev/to/endorsed-federated-plugin), +which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do, +so you do not need to add it to your `pubspec.yaml`. + +However, if you `import` this package to use any of its APIs directly, you +should add it to your `pubspec.yaml` as usual. + +For example, you need to import this package directly if you plan to use the +web-only `Widget renderButton()` method. + +## Integration + +First, go through the instructions [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) to create your Google Sign-In OAuth client ID. + +On your `web/index.html` file, add the following `meta` tag, somewhere in the +`head` of the document: + +```html + +``` + +For this client to work correctly, the last step is to configure the **Authorized JavaScript origins**, which _identify the domains from which your application can send API requests._ When in local development, this is normally `localhost` and some port. + +You can do this by: + +1. Going to the [Credentials page](https://console.developers.google.com/apis/credentials). +2. Clicking "Edit" in the OAuth 2.0 Web application client that you created above. +3. Adding the URIs you want to the **Authorized JavaScript origins**. + +For local development, you must add two `localhost` entries: + +* `http://localhost` and +* `http://localhost:7357` (or any port that is free in your machine) + +### Starting flutter in http://localhost:7357 + +Normally `flutter run` starts in a random port. In the case where you need to deal with authentication like the above, that's not the most appropriate behavior. + +You can tell `flutter run` to listen for requests in a specific host and port with the following: + +```sh +flutter run -d chrome --web-hostname localhost --web-port 7357 +``` + ## Migrating to v0.11 and v0.12 (Google Identity Services) The `google_sign_in_web` plugin is backed by the new Google Identity Services @@ -83,7 +130,7 @@ their own Sign-In buttons, or an API to start the sign in flow, the current implementation of `signIn` (that does authorization and authentication) is no longer feasible on the web. -The web plugin attempts to simulate the old `signIn` behavior by using the +The web plugin attempts to simulate the old `signIn` behavior by using the [OAuth Implicit pop-up flow](https://developers.google.com/identity/oauth2/web/guides/use-token-model), which authenticates and authorizes users. @@ -141,65 +188,10 @@ In either case, your app needs to prompt the end user to `requestScopes`, to The GIS SDK limits authorization token duration to one hour (3600 seconds). -## Usage - -### Import the package - -This package is [endorsed](https://flutter.dev/to/endorsed-federated-plugin), -which means you can simply use `google_sign_in` -normally. This package will be automatically included in your app when you do, -so you do not need to add it to your `pubspec.yaml`. - -However, if you `import` this package to use any of its APIs directly, you -should add it to your `pubspec.yaml` as usual. - -For example, you need to import this package directly if you plan to use the -web-only `Widget renderButton()` method. - -### Web integration - -First, go through the instructions [here](https://developers.google.com/identity/gsi/web/guides/get-google-api-clientid) to create your Google Sign-In OAuth client ID. - -On your `web/index.html` file, add the following `meta` tag, somewhere in the -`head` of the document: - -```html - -``` - -For this client to work correctly, the last step is to configure the **Authorized JavaScript origins**, which _identify the domains from which your application can send API requests._ When in local development, this is normally `localhost` and some port. - -You can do this by: - -1. Going to the [Credentials page](https://console.developers.google.com/apis/credentials). -2. Clicking "Edit" in the OAuth 2.0 Web application client that you created above. -3. Adding the URIs you want to the **Authorized JavaScript origins**. - -For local development, you must add two `localhost` entries: - -* `http://localhost` and -* `http://localhost:7357` (or any port that is free in your machine) - -#### Starting flutter in http://localhost:7357 - -Normally `flutter run` starts in a random port. In the case where you need to deal with authentication like the above, that's not the most appropriate behavior. - -You can tell `flutter run` to listen for requests in a specific host and port with the following: - -```sh -flutter run -d chrome --web-hostname localhost --web-port 7357 -``` - ### Other APIs Read the rest of the instructions if you need to add extra APIs (like Google People API). -### Using the plugin - -See the [**Usage** instructions of `package:google_sign_in`](https://pub.dev/packages/google_sign_in#usage) - -Note that the **`serverClientId` parameter of the `GoogleSignIn` constructor is not supported on Web.** - ## Example Find the example wiring in the [Google sign-in example application](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart). From 728a4f9bc83b648ed72f48b83cbd67248b059a6e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 5 May 2025 14:09:23 -0400 Subject: [PATCH 07/52] Rework Android calls to match recommended practice --- .../google_sign_in/example/lib/main.dart | 3 +- .../example/lib/src/web_wrapper.dart | 5 +++ .../example/lib/src/web_wrapper_stub.dart | 11 +++++++ .../example/lib/src/web_wrapper_web.dart | 5 +++ .../googlesignin/GoogleSignInPlugin.java | 32 ++++++++++++------- .../flutter/plugins/googlesignin/Messages.kt | 9 ++++-- .../lib/google_sign_in_android.dart | 23 ++++++++----- .../lib/src/messages.g.dart | 9 ++++-- .../pigeons/messages.dart | 8 +++++ 9 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper.dart create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_stub.dart create mode 100644 packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_web.dart diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index b9d1a3e3c51..43a4f59e1fa 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -10,9 +10,10 @@ import 'dart:convert' show json; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; -import 'package:google_sign_in_web/web_only.dart' as web; import 'package:http/http.dart' as http; +import 'src/web_wrapper.dart' as web; + /// To run this example, replace this value with your client ID, and/or /// update the relevant configuration files, as described in the README. String? clientId; diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper.dart b/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper.dart new file mode 100644 index 00000000000..e96b03438c7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper.dart @@ -0,0 +1,5 @@ +// 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. + +export 'web_wrapper_stub.dart' if (dart.library.js_util) 'web_wrapper_web.dart'; diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_stub.dart b/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_stub.dart new file mode 100644 index 00000000000..1e55df4a1fe --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_stub.dart @@ -0,0 +1,11 @@ +// 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/material.dart'; + +/// Stub for the web-only renderButton method, since google_sign_in_web has to +/// be behind a conditional import. +Widget renderButton() { + throw StateError('This should only be called on web'); +} diff --git a/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_web.dart b/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_web.dart new file mode 100644 index 00000000000..e60009b86ed --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/lib/src/web_wrapper_web.dart @@ -0,0 +1,5 @@ +// 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. + +export 'package:google_sign_in_web/web_only.dart'; diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 4b4fd4ea251..94f411f3251 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -35,6 +35,7 @@ import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.Scope; import com.google.android.libraries.identity.googleid.GetGoogleIdOption; +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption; import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential; import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -179,22 +180,31 @@ public void getCredential( return; } - GetGoogleIdOption.Builder optionBuilder = - new GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(params.getFilterToAuthorized()) - .setAutoSelectEnabled(params.getAutoSelectEnabled()) - .setServerClientId(serverClientId); String nonce = params.getNonce(); - if (nonce != null) { - optionBuilder.setNonce(nonce); + GetCredentialRequest.Builder requestBuilder = new GetCredentialRequest.Builder(); + if (params.getUseButtonFlow()) { + GetSignInWithGoogleOption.Builder optionBuilder = + new GetSignInWithGoogleOption.Builder(serverClientId); + if (nonce != null) { + optionBuilder.setNonce(nonce); + } + requestBuilder.addCredentialOption(optionBuilder.build()); + } else { + GetGoogleIdOption.Builder optionBuilder = + new GetGoogleIdOption.Builder() + .setFilterByAuthorizedAccounts(params.getFilterToAuthorized()) + .setAutoSelectEnabled(params.getAutoSelectEnabled()) + .setServerClientId(serverClientId); + if (nonce != null) { + optionBuilder.setNonce(nonce); + } + requestBuilder.addCredentialOption(optionBuilder.build()); } - GetGoogleIdOption googleIdOption = optionBuilder.build(); - GetCredentialRequest request = - new GetCredentialRequest.Builder().addCredentialOption(googleIdOption).build(); + CredentialManager credentialManager = CredentialManager.create(context); credentialManager.getCredentialAsync( context, - request, + requestBuilder.build(), null, Executors.newSingleThreadExecutor(), new CredentialManagerCallback<>() { diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt index 8fcaf52eab7..ef7637cdfa6 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt @@ -147,6 +147,7 @@ data class PlatformAuthorizationRequest( data class GetCredentialRequestParams( val filterToAuthorized: Boolean, val autoSelectEnabled: Boolean, + val useButtonFlow: Boolean, val serverClientId: String? = null, val nonce: String? = null ) { @@ -154,10 +155,11 @@ data class GetCredentialRequestParams( fun fromList(pigeonVar_list: List): GetCredentialRequestParams { val filterToAuthorized = pigeonVar_list[0] as Boolean val autoSelectEnabled = pigeonVar_list[1] as Boolean - val serverClientId = pigeonVar_list[2] as String? - val nonce = pigeonVar_list[3] as String? + val useButtonFlow = pigeonVar_list[2] as Boolean + val serverClientId = pigeonVar_list[3] as String? + val nonce = pigeonVar_list[4] as String? return GetCredentialRequestParams( - filterToAuthorized, autoSelectEnabled, serverClientId, nonce) + filterToAuthorized, autoSelectEnabled, useButtonFlow, serverClientId, nonce) } } @@ -165,6 +167,7 @@ data class GetCredentialRequestParams( return listOf( filterToAuthorized, autoSelectEnabled, + useButtonFlow, serverClientId, nonce, ) diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 5e99d824d3e..c1f6391435e 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -46,9 +46,18 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { @override Future attemptLightweightAuthentication( AttemptLightweightAuthenticationParameters params) async { - final PlatformGoogleIdTokenCredential? credential = await _authenticate( + // Attempt to auto-sign-in, for single-account or returning users. + PlatformGoogleIdTokenCredential? credential = await _authenticate( filterToAuthorized: true, autoSelectEnabled: true, + useButtonFlow: false, + ); + // If no auto-sign-in is available, potentially prompt for an account via + // the bottom sheet flow. + credential ??= await _authenticate( + filterToAuthorized: false, + autoSelectEnabled: false, + useButtonFlow: false, ); return credential == null ? null @@ -58,15 +67,11 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { @override Future authenticate( AuthenticateParameters params) async { - // Attempt to authorize without user interaction. - PlatformGoogleIdTokenCredential? credential = await _authenticate( - filterToAuthorized: true, - autoSelectEnabled: true, - ); - // If no auto-sign-in is available, prompt for an account. - credential ??= credential = await _authenticate( + // Attempt to authorize with minimal interaction. + final PlatformGoogleIdTokenCredential? credential = await _authenticate( filterToAuthorized: false, autoSelectEnabled: false, + useButtonFlow: true, throwForNoAuth: true, ); // It's not clear from the documentation if this can happen; if it does, @@ -115,12 +120,14 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { Future _authenticate({ required bool filterToAuthorized, required bool autoSelectEnabled, + required bool useButtonFlow, bool throwForNoAuth = false, }) async { final GetCredentialResult authnResult = await _credentialManaagerApi.getCredential(GetCredentialRequestParams( filterToAuthorized: filterToAuthorized, autoSelectEnabled: autoSelectEnabled, + useButtonFlow: useButtonFlow, serverClientId: _serverClientId, nonce: _nonce)); switch (authnResult) { diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart index 3c0146b26c1..c72f3f5b0bb 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -118,6 +118,7 @@ class GetCredentialRequestParams { GetCredentialRequestParams({ required this.filterToAuthorized, required this.autoSelectEnabled, + required this.useButtonFlow, this.serverClientId, this.nonce, }); @@ -126,6 +127,8 @@ class GetCredentialRequestParams { bool autoSelectEnabled; + bool useButtonFlow; + String? serverClientId; String? nonce; @@ -134,6 +137,7 @@ class GetCredentialRequestParams { return [ filterToAuthorized, autoSelectEnabled, + useButtonFlow, serverClientId, nonce, ]; @@ -144,8 +148,9 @@ class GetCredentialRequestParams { return GetCredentialRequestParams( filterToAuthorized: result[0]! as bool, autoSelectEnabled: result[1]! as bool, - serverClientId: result[2] as String?, - nonce: result[3] as String?, + useButtonFlow: result[2]! as bool, + serverClientId: result[3] as String?, + nonce: result[4] as String?, ); } } diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart index 127f740efe3..2330cd01a2a 100644 --- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -32,11 +32,19 @@ class PlatformAuthorizationRequest { /// classes that are used for this plugin. class GetCredentialRequestParams { GetCredentialRequestParams({ + required this.useButtonFlow, required this.filterToAuthorized, required this.autoSelectEnabled, this.serverClientId, this.nonce, }); + + /// Whether to use the Sign in with Google button flow + /// (GetSignInWithGoogleOption), corresponding to an explicit sign-in request, + /// or not (GetGoogleIdOption), corresponding to an implicit potential + /// sign-in. + bool useButtonFlow; + bool filterToAuthorized; bool autoSelectEnabled; String? serverClientId; From c1658342757f7e3be53d217e1f37c51f71a7060e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 5 May 2025 14:26:31 -0400 Subject: [PATCH 08/52] Synthesize sign-out event for disconnect --- .../google_sign_in/google_sign_in/lib/google_sign_in.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index a7c41a1099f..2c5e09264e0 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -469,6 +469,11 @@ class GoogleSignIn { /// Disconnects any currently authorized users from the app, revoking previous /// authorization. Future disconnect() async { + // Disconnecting also signs out, so synthesize a sign-out if necessary. + if (_createAuthenticationStreamEvents) { + _authenticationStreamController + .add(GoogleSignInAuthenticationEventSignOut()); + } // TODO(stuartmorgan): Consider making a per-user disconnect option once // the Android implementation is available so that we can see how it is // structured. In practice, currently the plugin only fully supports a From dba0f3ec613ded80d8dd3e1047272aa42c3e6b3b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 5 May 2025 14:55:11 -0400 Subject: [PATCH 09/52] platform interface tests --- ...oogle_sign_in_platform_interface_test.dart | 102 ++++++++++++++---- 1 file changed, 81 insertions(+), 21 deletions(-) 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 6ad48e0c226..0ef1d0495af 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,8 +8,8 @@ import 'package:mockito/mockito.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { - group('$GoogleSignInPlatform', () { - 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 @@ -22,28 +22,20 @@ void main() { }, throwsA(anything)); }); - test('Can be extended', () { + test('can be extended', () { GoogleSignInPlatform.instance = ExtendsGoogleSignInPlatform(); }); - test('Can be mocked with `implements`', () { + test('can be mocked with `implements`', () { GoogleSignInPlatform.instance = MockImplementation(); }); - }); - group('GoogleSignInTokenData', () { - test('can be compared by == operator', () { - const GoogleSignInTokenData firstInstance = GoogleSignInTokenData( - accessToken: 'accessToken', - idToken: 'idToken', - serverAuthCode: 'serverAuthCode', - ); - const 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); }); }); @@ -54,15 +46,51 @@ void main() { id: 'id', displayName: 'displayName', photoUrl: 'photoUrl', - idToken: 'idToken', - serverAuthCode: 'serverAuthCode', ); 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', + ); + expect(firstInstance == secondInstance, isTrue); + }); + }); + + 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); @@ -77,4 +105,36 @@ class MockImplementation extends Mock class ImplementsGoogleSignInPlatform extends Mock implements GoogleSignInPlatform {} -class ExtendsGoogleSignInPlatform extends GoogleSignInPlatform {} +class ExtendsGoogleSignInPlatform extends GoogleSignInPlatform { + @override + Future? attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) async { + return null; + } + + @override + Future authenticate(AuthenticateParameters params) { + throw UnimplementedError(); + } + + @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 {} +} From 83c7b1d6fba4859d4e28e52d348f9539d630030d Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 6 May 2025 12:36:15 -0400 Subject: [PATCH 10/52] Simplify Android Pigeon API --- .../googlesignin/GoogleSignInPlugin.java | 11 +-- .../flutter/plugins/googlesignin/Messages.kt | 73 +++++++------------ .../lib/google_sign_in_android.dart | 21 ++---- .../lib/src/messages.g.dart | 55 ++++++-------- .../pigeons/messages.dart | 12 ++- 5 files changed, 65 insertions(+), 107 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 94f411f3251..36c27b294f8 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -61,15 +61,13 @@ public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware { public void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) { this.messenger = messenger; delegate = new Delegate(context); - CredentialManagerApi.Companion.setUp(messenger, delegate); - AuthorizationClientApi.Companion.setUp(messenger, delegate); + GoogleSignInApi.Companion.setUp(messenger, delegate); } private void dispose() { delegate = null; if (messenger != null) { - CredentialManagerApi.Companion.setUp(messenger, null); - AuthorizationClientApi.Companion.setUp(messenger, null); + GoogleSignInApi.Companion.setUp(messenger, null); messenger = null; } } @@ -126,10 +124,7 @@ public void onDetachedFromActivity() { * completed (either successfully or in error). This class provides no synchronization constructs * to guarantee such behavior; callers are responsible for providing such guarantees. */ - public static class Delegate - implements PluginRegistry.ActivityResultListener, - CredentialManagerApi, - AuthorizationClientApi { + public static class Delegate implements PluginRegistry.ActivityResultListener, GoogleSignInApi { private static final int REQUEST_CODE_AUTHORIZE = 53294; private final @NonNull Context context; diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt index ef7637cdfa6..a90f740d658 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt @@ -145,29 +145,34 @@ data class PlatformAuthorizationRequest( * Generated class from Pigeon that represents data sent in messages. */ data class GetCredentialRequestParams( + /** + * Whether to use the Sign in with Google button flow (GetSignInWithGoogleOption), corresponding + * to an explicit sign-in request, or not (GetGoogleIdOption), corresponding to an implicit + * potential sign-in. + */ + val useButtonFlow: Boolean, val filterToAuthorized: Boolean, val autoSelectEnabled: Boolean, - val useButtonFlow: Boolean, val serverClientId: String? = null, val nonce: String? = null ) { companion object { fun fromList(pigeonVar_list: List): GetCredentialRequestParams { - val filterToAuthorized = pigeonVar_list[0] as Boolean - val autoSelectEnabled = pigeonVar_list[1] as Boolean - val useButtonFlow = pigeonVar_list[2] as Boolean + val useButtonFlow = pigeonVar_list[0] as Boolean + val filterToAuthorized = pigeonVar_list[1] as Boolean + val autoSelectEnabled = pigeonVar_list[2] as Boolean val serverClientId = pigeonVar_list[3] as String? val nonce = pigeonVar_list[4] as String? return GetCredentialRequestParams( - filterToAuthorized, autoSelectEnabled, useButtonFlow, serverClientId, nonce) + useButtonFlow, filterToAuthorized, autoSelectEnabled, serverClientId, nonce) } } fun toList(): List { return listOf( + useButtonFlow, filterToAuthorized, autoSelectEnabled, - useButtonFlow, serverClientId, nonce, ) @@ -423,34 +428,36 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface CredentialManagerApi { +interface GoogleSignInApi { /** * Returns the server client ID parsed from google-services.json by the google-services Gradle * script, if any. - * - * This is not part of CredentialManager, but is included here for convenience since - * CredentialManager requires a server client ID. */ fun getGoogleServicesJsonServerClientId(): String? - + /** Requests an authentication credential (sign in) via CredentialManager's getCredential. */ fun getCredential( params: GetCredentialRequestParams, callback: (Result) -> Unit ) - + /** Clears CredentialManager credential state. */ fun clearCredentialState(callback: (Result) -> Unit) + /** Requests authorization tokens via AuthorizationClient. */ + fun authorize( + params: PlatformAuthorizationRequest, + promptIfUnauthorized: Boolean, + callback: (Result) -> Unit + ) companion object { - /** The codec used by CredentialManagerApi. */ + /** The codec used by GoogleSignInApi. */ val codec: MessageCodec by lazy { MessagesPigeonCodec() } /** - * Sets up an instance of `CredentialManagerApi` to handle messages through the - * `binaryMessenger`. + * Sets up an instance of `GoogleSignInApi` to handle messages through the `binaryMessenger`. */ @JvmOverloads fun setUp( binaryMessenger: BinaryMessenger, - api: CredentialManagerApi?, + api: GoogleSignInApi?, messageChannelSuffix: String = "" ) { val separatedMessageChannelSuffix = @@ -459,7 +466,7 @@ interface CredentialManagerApi { val channel = BasicMessageChannel( binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getGoogleServicesJsonServerClientId$separatedMessageChannelSuffix", + "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.getGoogleServicesJsonServerClientId$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> @@ -479,7 +486,7 @@ interface CredentialManagerApi { val channel = BasicMessageChannel( binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getCredential$separatedMessageChannelSuffix", + "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.getCredential$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> @@ -503,7 +510,7 @@ interface CredentialManagerApi { val channel = BasicMessageChannel( binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.clearCredentialState$separatedMessageChannelSuffix", + "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.clearCredentialState$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { _, reply -> @@ -520,37 +527,11 @@ interface CredentialManagerApi { channel.setMessageHandler(null) } } - } - } -} -/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ -interface AuthorizationClientApi { - fun authorize( - params: PlatformAuthorizationRequest, - promptIfUnauthorized: Boolean, - callback: (Result) -> Unit - ) - - companion object { - /** The codec used by AuthorizationClientApi. */ - val codec: MessageCodec by lazy { MessagesPigeonCodec() } - /** - * Sets up an instance of `AuthorizationClientApi` to handle messages through the - * `binaryMessenger`. - */ - @JvmOverloads - fun setUp( - binaryMessenger: BinaryMessenger, - api: AuthorizationClientApi?, - messageChannelSuffix: String = "" - ) { - val separatedMessageChannelSuffix = - if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" run { val channel = BasicMessageChannel( binaryMessenger, - "dev.flutter.pigeon.google_sign_in_android.AuthorizationClientApi.authorize$separatedMessageChannelSuffix", + "dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.authorize$separatedMessageChannelSuffix", codec) if (api != null) { channel.setMessageHandler { message, reply -> diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index c1f6391435e..ae08cc26b9a 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -14,15 +14,10 @@ import 'src/messages.g.dart'; class GoogleSignInAndroid extends GoogleSignInPlatform { /// Creates a new plugin implementation instance. GoogleSignInAndroid({ - @visibleForTesting CredentialManagerApi? credentialManaagerApi, - @visibleForTesting AuthorizationClientApi? authorizationClientApi, - }) : _credentialManaagerApi = - credentialManaagerApi ?? CredentialManagerApi(), - _authorizationClientApi = - authorizationClientApi ?? AuthorizationClientApi(); + @visibleForTesting GoogleSignInApi? googleSignInApi, + }) : _hostApi = googleSignInApi ?? GoogleSignInApi(); - final CredentialManagerApi _credentialManaagerApi; - final AuthorizationClientApi _authorizationClientApi; + final GoogleSignInApi _hostApi; String? _serverClientId; String? _hostedDomain; @@ -37,7 +32,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { Future init(InitParameters params) async { _hostedDomain = params.hostedDomain; _serverClientId = params.serverClientId ?? - await _credentialManaagerApi.getGoogleServicesJsonServerClientId(); + await _hostApi.getGoogleServicesJsonServerClientId(); _nonce = params.nonce; // The clientId parameter is not supported on Android. // Android apps are identified by their package name and the SHA-1 of their signing key. @@ -86,7 +81,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { @override Future signOut(SignOutParams params) { - return _credentialManaagerApi.clearCredentialState(); + return _hostApi.clearCredentialState(); } @override @@ -123,8 +118,8 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { required bool useButtonFlow, bool throwForNoAuth = false, }) async { - final GetCredentialResult authnResult = - await _credentialManaagerApi.getCredential(GetCredentialRequestParams( + final GetCredentialResult authnResult = await _hostApi.getCredential( + GetCredentialRequestParams( filterToAuthorized: filterToAuthorized, autoSelectEnabled: autoSelectEnabled, useButtonFlow: useButtonFlow, @@ -173,7 +168,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { Future<({String? accessToken, String? serverAuthCode})> _authorize( AuthorizationRequestDetails request, {required bool requestOfflineAccess}) async { - final AuthorizeResult result = await _authorizationClientApi.authorize( + final AuthorizeResult result = await _hostApi.authorize( PlatformAuthorizationRequest( scopes: request.scopes, accountEmail: request.email, diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart index c72f3f5b0bb..8bd8c6431b3 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -116,28 +116,32 @@ class PlatformAuthorizationRequest { /// classes that are used for this plugin. class GetCredentialRequestParams { GetCredentialRequestParams({ + required this.useButtonFlow, required this.filterToAuthorized, required this.autoSelectEnabled, - required this.useButtonFlow, this.serverClientId, this.nonce, }); + /// Whether to use the Sign in with Google button flow + /// (GetSignInWithGoogleOption), corresponding to an explicit sign-in request, + /// or not (GetGoogleIdOption), corresponding to an implicit potential + /// sign-in. + bool useButtonFlow; + bool filterToAuthorized; bool autoSelectEnabled; - bool useButtonFlow; - String? serverClientId; String? nonce; Object encode() { return [ + useButtonFlow, filterToAuthorized, autoSelectEnabled, - useButtonFlow, serverClientId, nonce, ]; @@ -146,9 +150,9 @@ class GetCredentialRequestParams { static GetCredentialRequestParams decode(Object result) { result as List; return GetCredentialRequestParams( - filterToAuthorized: result[0]! as bool, - autoSelectEnabled: result[1]! as bool, - useButtonFlow: result[2]! as bool, + useButtonFlow: result[0]! as bool, + filterToAuthorized: result[1]! as bool, + autoSelectEnabled: result[2]! as bool, serverClientId: result[3] as String?, nonce: result[4] as String?, ); @@ -407,11 +411,11 @@ class _PigeonCodec extends StandardMessageCodec { } } -class CredentialManagerApi { - /// Constructor for [CredentialManagerApi]. The [binaryMessenger] named argument is +class GoogleSignInApi { + /// Constructor for [GoogleSignInApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - CredentialManagerApi( + GoogleSignInApi( {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) : pigeonVar_binaryMessenger = binaryMessenger, pigeonVar_messageChannelSuffix = @@ -424,12 +428,9 @@ class CredentialManagerApi { /// Returns the server client ID parsed from google-services.json by the /// google-services Gradle script, if any. - /// - /// This is not part of CredentialManager, but is included here for - /// convenience since CredentialManager requires a server client ID. Future getGoogleServicesJsonServerClientId() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getGoogleServicesJsonServerClientId$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.getGoogleServicesJsonServerClientId$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -452,10 +453,12 @@ class CredentialManagerApi { } } + /// Requests an authentication credential (sign in) via CredentialManager's + /// getCredential. Future getCredential( GetCredentialRequestParams params) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.getCredential$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.getCredential$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -484,9 +487,10 @@ class CredentialManagerApi { } } + /// Clears CredentialManager credential state. Future clearCredentialState() async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.CredentialManagerApi.clearCredentialState$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.clearCredentialState$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -508,27 +512,12 @@ class CredentialManagerApi { return; } } -} - -class AuthorizationClientApi { - /// Constructor for [AuthorizationClientApi]. The [binaryMessenger] named argument is - /// available for dependency injection. If it is left null, the default - /// BinaryMessenger will be used which routes to the host platform. - AuthorizationClientApi( - {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - final BinaryMessenger? pigeonVar_binaryMessenger; - - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - final String pigeonVar_messageChannelSuffix; + /// Requests authorization tokens via AuthorizationClient. Future authorize(PlatformAuthorizationRequest params, {required bool promptIfUnauthorized}) async { final String pigeonVar_channelName = - 'dev.flutter.pigeon.google_sign_in_android.AuthorizationClientApi.authorize$pigeonVar_messageChannelSuffix'; + 'dev.flutter.pigeon.google_sign_in_android.GoogleSignInApi.authorize$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart index 2330cd01a2a..af39cb3c226 100644 --- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -163,23 +163,21 @@ class PlatformAuthorizationResult extends AuthorizeResult { } @HostApi() -abstract class CredentialManagerApi { +abstract class GoogleSignInApi { /// Returns the server client ID parsed from google-services.json by the /// google-services Gradle script, if any. - /// - /// This is not part of CredentialManager, but is included here for - /// convenience since CredentialManager requires a server client ID. String? getGoogleServicesJsonServerClientId(); + /// Requests an authentication credential (sign in) via CredentialManager's + /// getCredential. @async GetCredentialResult getCredential(GetCredentialRequestParams params); + /// Clears CredentialManager credential state. @async void clearCredentialState(); -} -@HostApi() -abstract class AuthorizationClientApi { + /// Requests authorization tokens via AuthorizationClient. @async AuthorizeResult authorize(PlatformAuthorizationRequest params, {required bool promptIfUnauthorized}); From 82c053095f791054d3235de343bcab699fd0c5e6 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 8 May 2025 08:18:33 -0400 Subject: [PATCH 11/52] Android Dart unit tests --- .../lib/google_sign_in_android.dart | 10 +- .../test/google_sign_in_android_test.dart | 779 ++++++++++++++---- .../google_sign_in_android_test.mocks.dart | 166 ++-- 3 files changed, 702 insertions(+), 253 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index ae08cc26b9a..9b0844bd7cd 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -14,8 +14,8 @@ import 'src/messages.g.dart'; class GoogleSignInAndroid extends GoogleSignInPlatform { /// Creates a new plugin implementation instance. GoogleSignInAndroid({ - @visibleForTesting GoogleSignInApi? googleSignInApi, - }) : _hostApi = googleSignInApi ?? GoogleSignInApi(); + @visibleForTesting GoogleSignInApi? api, + }) : _hostApi = api ?? GoogleSignInApi(); final GoogleSignInApi _hostApi; @@ -178,6 +178,7 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { promptIfUnauthorized: request.promptIfUnauthorized); switch (result) { case AuthorizeFailure(): + String? message = result.message; final GoogleSignInExceptionCode code; switch (result.type) { case AuthorizeFailureType.unauthorized: @@ -187,13 +188,16 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { case AuthorizeFailureType.pendingIntentException: code = GoogleSignInExceptionCode.canceled; case AuthorizeFailureType.authorizeFailure: + message = 'Authorization failed: $message'; + code = GoogleSignInExceptionCode.unknownError; case AuthorizeFailureType.apiException: + message = 'SDK reported an exception: $message'; code = GoogleSignInExceptionCode.unknownError; case AuthorizeFailureType.noActivity: code = GoogleSignInExceptionCode.uiUnavailable; } throw GoogleSignInException( - code: code, description: result.message, details: result.details); + code: code, description: message, details: result.details); case PlatformAuthorizationResult(): final String? accessToken = result.accessToken; if (accessToken == null) { diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart index 20cfb6a746e..f583fa50eff 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -2,7 +2,8 @@ // 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 'dart:convert'; + import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_android/google_sign_in_android.dart'; import 'package:google_sign_in_android/src/messages.g.dart'; @@ -12,28 +13,36 @@ import 'package:mockito/mockito.dart'; import 'google_sign_in_android_test.mocks.dart'; -final GoogleSignInUserData _user = GoogleSignInUserData( +const GoogleSignInUserData _testUser = GoogleSignInUserData( email: 'john.doe@gmail.com', id: '8162538176523816253123', photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', displayName: 'John Doe', - idToken: '123', - serverAuthCode: '789', ); -final GoogleSignInTokenData _token = GoogleSignInTokenData( - accessToken: '456', +final AuthenticationTokenData _testAuthnToken = AuthenticationTokenData( + // This is just real enough to test the id-from-idToken extraction logic, with + // the middle (payload) section having an actual base-64 encoded JSON + // dictionary with only the "sub":"id" entry needed by the plugin code. + idToken: 'header.${base64UrlEncode(JsonUtf8Encoder().convert( + {'sub': _testUser.id}, + ))}.signatune', ); -@GenerateMocks([GoogleSignInApi]) +@GenerateNiceMocks(>[MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); late GoogleSignInAndroid googleSignIn; - late MockGoogleSignInApi api; + late MockGoogleSignInApi mockApi; setUp(() { - api = MockGoogleSignInApi(); - googleSignIn = GoogleSignInAndroid(api: api); + mockApi = MockGoogleSignInApi(); + googleSignIn = GoogleSignInAndroid(api: mockApi); + + provideDummy(GetCredentialSuccess( + credential: PlatformGoogleIdTokenCredential(id: '', idToken: ''))); + provideDummy( + PlatformAuthorizationResult(grantedScopes: [])); }); test('registered instance', () { @@ -41,163 +50,635 @@ void main() { expect(GoogleSignInPlatform.instance, isA()); }); - test('signInSilently transforms platform data to GoogleSignInUserData', - () async { - when(api.signInSilently()).thenAnswer((_) async => UserData( - email: _user.email, - id: _user.id, - photoUrl: _user.photoUrl, - displayName: _user.displayName, - idToken: _user.idToken, - serverAuthCode: _user.serverAuthCode, - )); - - final dynamic response = await googleSignIn.signInSilently(); - - expect(response, _user); - }); - - test('signInSilently Exceptions -> throws', () async { - when(api.signInSilently()) - .thenAnswer((_) async => throw PlatformException(code: 'fail')); - - expect(googleSignIn.signInSilently(), - throwsA(isInstanceOf())); - }); - - test('signIn transforms platform data to GoogleSignInUserData', () async { - when(api.signIn()).thenAnswer((_) async => UserData( - email: _user.email, - id: _user.id, - photoUrl: _user.photoUrl, - displayName: _user.displayName, - idToken: _user.idToken, - serverAuthCode: _user.serverAuthCode, - )); - - final dynamic response = await googleSignIn.signIn(); - - expect(response, _user); - }); - - test('signIn Exceptions -> throws', () async { - when(api.signIn()) - .thenAnswer((_) async => throw PlatformException(code: 'fail')); - - expect(googleSignIn.signIn(), throwsA(isInstanceOf())); - }); - - test('getTokens transforms platform data to GoogleSignInTokenData', () async { - const bool recoverAuth = false; - when(api.getAccessToken(_user.email, recoverAuth)) - .thenAnswer((_) async => _token.accessToken!); - - final GoogleSignInTokenData response = await googleSignIn.getTokens( - email: _user.email, shouldRecoverAuth: recoverAuth); - - expect(response, _token); + group('attemptLightweightAuthentication', () { + test('passes explicit server client ID', () async { + const String serverClientId = 'aServerClient'; + + await googleSignIn + .init(const InitParameters(serverClientId: serverClientId)); + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + verifyNever(mockApi.getGoogleServicesJsonServerClientId()); + final VerificationResult verification = + verify(mockApi.getCredential(captureAny)); + final GetCredentialRequestParams hostParams = + verification.captured[0] as GetCredentialRequestParams; + expect(hostParams.serverClientId, serverClientId); + }); + + test('passes JSON server client ID if not overridden', () async { + const String serverClientId = 'aServerClient'; + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => serverClientId); + + // Passing no server client ID should cause it to be queried via + // getGoogleServicesJsonServerClientId(). + await googleSignIn.init(const InitParameters()); + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + verify(mockApi.getGoogleServicesJsonServerClientId()); + final VerificationResult verification = + verify(mockApi.getCredential(captureAny)); + final GetCredentialRequestParams hostParams = + verification.captured[0] as GetCredentialRequestParams; + expect(hostParams.serverClientId, serverClientId); + }); + + test('passes nonce if provided', () async { + const String nonce = 'nonce'; + + await googleSignIn + .init(const InitParameters(nonce: nonce, serverClientId: 'id')); + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + final VerificationResult verification = + verify(mockApi.getCredential(captureAny)); + final GetCredentialRequestParams hostParams = + verification.captured[0] as GetCredentialRequestParams; + expect(hostParams.nonce, nonce); + }); + + test('passes success data to caller', () async { + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialSuccess( + credential: PlatformGoogleIdTokenCredential( + displayName: _testUser.displayName, + profilePictureUri: _testUser.photoUrl, + id: _testUser.email, + idToken: _testAuthnToken.idToken!))); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + final AuthenticationResults? result = + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + expect(result?.user, _testUser); + expect(result?.authenticationTokens, _testAuthnToken); + }); + + test('returns null for missing auth', () async { + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure(type: GetCredentialFailureType.noCredential)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + final AuthenticationResults? result = + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + expect(result, null); + }); }); - test('getTokens will not pass null for shouldRecoverAuth', () async { - when(api.getAccessToken(_user.email, true)) - .thenAnswer((_) async => _token.accessToken!); - - final GoogleSignInTokenData response = await googleSignIn.getTokens( - email: _user.email, shouldRecoverAuth: null); - - expect(response, _token); + group('authenticate', () { + test('passes explicit server client ID', () async { + const String serverClientId = 'aServerClient'; + + await googleSignIn + .init(const InitParameters(serverClientId: serverClientId)); + await googleSignIn.authenticate(const AuthenticateParameters()); + + verifyNever(mockApi.getGoogleServicesJsonServerClientId()); + final VerificationResult verification = + verify(mockApi.getCredential(captureAny)); + final GetCredentialRequestParams hostParams = + verification.captured[0] as GetCredentialRequestParams; + expect(hostParams.serverClientId, serverClientId); + }); + + test('passes JSON server client ID if not overridden', () async { + const String serverClientId = 'aServerClient'; + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => serverClientId); + + // Passing no server client ID should cause it to be queried via + // getGoogleServicesJsonServerClientId(). + await googleSignIn.init(const InitParameters()); + await googleSignIn.authenticate(const AuthenticateParameters()); + + verify(mockApi.getGoogleServicesJsonServerClientId()); + final VerificationResult verification = + verify(mockApi.getCredential(captureAny)); + final GetCredentialRequestParams hostParams = + verification.captured[0] as GetCredentialRequestParams; + expect(hostParams.serverClientId, serverClientId); + }); + + test('passes nonce if provided', () async { + const String nonce = 'nonce'; + + await googleSignIn.init(const InitParameters(nonce: nonce)); + await googleSignIn.authenticate(const AuthenticateParameters()); + + final VerificationResult verification = + verify(mockApi.getCredential(captureAny)); + final GetCredentialRequestParams hostParams = + verification.captured[0] as GetCredentialRequestParams; + expect(hostParams.nonce, nonce); + }); + + test('passes success data to caller', () async { + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialSuccess( + credential: PlatformGoogleIdTokenCredential( + displayName: _testUser.displayName, + profilePictureUri: _testUser.photoUrl, + id: _testUser.email, + idToken: _testAuthnToken.idToken!))); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + final AuthenticationResults result = + await googleSignIn.authenticate(const AuthenticateParameters()); + + expect(result.user, _testUser); + expect(result.authenticationTokens, _testAuthnToken); + }); + + test('throws unknown for missing auth', () async { + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure(type: GetCredentialFailureType.noCredential)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); + + test('throws client configuration error for missing server client ID', + () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure( + type: GetCredentialFailureType.missingServerClientId)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf() + .having((GoogleSignInException e) => e.code, 'code', + GoogleSignInExceptionCode.clientConfigurationError) + .having((GoogleSignInException e) => e.description, 'description', + contains('serverClientId must be provided')))); + }); + + test('throws provider configuration error for wrong credential type', + () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure( + type: GetCredentialFailureType.unexpectedCredentialType)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf() + .having((GoogleSignInException e) => e.code, 'code', + GoogleSignInExceptionCode.providerConfigurationError) + .having((GoogleSignInException e) => e.description, 'description', + contains('Unexpected credential type')))); + }); + + test( + 'throws provider configuration error if device does not ' + 'support Credential Manager', () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure(type: GetCredentialFailureType.unsupported)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf() + .having((GoogleSignInException e) => e.code, 'code', + GoogleSignInExceptionCode.providerConfigurationError) + .having((GoogleSignInException e) => e.description, 'description', + contains('Credential Manager not supported')))); + }); + + test( + 'throws provider configuration error for SDK-reported ' + 'provider configuration error', () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure( + type: GetCredentialFailureType.providerConfigurationIssue)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.providerConfigurationError))); + }); + + test('throws interrupted from SDK', () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure(type: GetCredentialFailureType.interrupted)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.interrupted))); + }); + + test('throws canceled from SDK', () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure(type: GetCredentialFailureType.canceled)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test('throws unknown from SDK', () async { + when(mockApi.getGoogleServicesJsonServerClientId()) + .thenAnswer((_) async => null); + when(mockApi.getCredential(any)).thenAnswer((_) async => + GetCredentialFailure(type: GetCredentialFailureType.unknown)); + + await googleSignIn.init(const InitParameters()); + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); }); - test('initWithParams passes arguments', () async { - const SignInInitParameters initParams = SignInInitParameters( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId', - ); - - await googleSignIn.init( - hostedDomain: initParams.hostedDomain, - scopes: initParams.scopes, - signInOption: initParams.signInOption, - clientId: initParams.clientId, + group('clientAuthorizationTokensForScopes', () { + // Request details used when the details of the request are not relevant to + // the test. + const AuthorizationRequestDetails defaultAuthRequest = + AuthorizationRequestDetails( + scopes: ['a'], + userId: null, + email: null, + promptIfUnauthorized: false, ); - final VerificationResult result = verify(api.init(captureAny)); - final InitParams passedParams = result.captured[0] as InitParams; - expect(passedParams.hostedDomain, initParams.hostedDomain); - expect(passedParams.scopes, initParams.scopes); - expect(passedParams.signInType, SignInType.games); - expect(passedParams.clientId, initParams.clientId); - // These should use whatever the SignInInitParameters defaults are. - expect(passedParams.serverClientId, initParams.serverClientId); - expect(passedParams.forceCodeForRefreshToken, - initParams.forceCodeForRefreshToken); + test('passes expected values', () async { + const List scopes = ['a', 'b']; + const String userId = '12345'; + const String userEmail = 'user@example.com'; + const bool promptIfUnauthorized = false; + const String hostedDomain = 'example.com'; + + when(mockApi.authorize(any, promptIfUnauthorized: promptIfUnauthorized)) + .thenAnswer((_) async => + PlatformAuthorizationResult(grantedScopes: [])); + + await googleSignIn.init(const InitParameters( + serverClientId: 'id', + hostedDomain: hostedDomain, + )); + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: userId, + email: userEmail, + promptIfUnauthorized: promptIfUnauthorized, + ))); + + final VerificationResult verification = verify(mockApi + .authorize(captureAny, promptIfUnauthorized: promptIfUnauthorized)); + final PlatformAuthorizationRequest hostParams = + verification.captured[0] as PlatformAuthorizationRequest; + expect(hostParams.scopes, scopes); + expect(hostParams.accountEmail, userEmail); + expect(hostParams.hostedDomain, hostedDomain); + expect(hostParams.serverClientIdForForcedRefreshToken, null); + }); + + test('passes true promptIfUnauthorized when requested', () async { + const List scopes = ['a', 'b']; + const bool promptIfUnauthorized = true; + + when(mockApi.authorize(any, promptIfUnauthorized: promptIfUnauthorized)) + .thenAnswer((_) async => + PlatformAuthorizationResult(grantedScopes: [])); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: promptIfUnauthorized, + ))); + + verify( + mockApi.authorize(any, promptIfUnauthorized: promptIfUnauthorized)); + }); + + test('passes success data to caller', () async { + const String accessToken = 'token'; + + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => PlatformAuthorizationResult( + grantedScopes: [], accessToken: accessToken)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + final ClientAuthorizationTokenData? result = + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)); + + expect(result?.accessToken, accessToken); + }); + + test('returns null when unauthorized', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => + AuthorizeFailure(type: AuthorizeFailureType.unauthorized)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + null); + }); + + test('thows canceled if pending intent fails', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => AuthorizeFailure( + type: AuthorizeFailureType.pendingIntentException)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test('throws unknown if authorization fails', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => + AuthorizeFailure(type: AuthorizeFailureType.authorizeFailure)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); + + test('throws unknown for API exception', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => + AuthorizeFailure(type: AuthorizeFailureType.apiException)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf() + .having((GoogleSignInException e) => e.code, 'code', + GoogleSignInExceptionCode.unknownError) + .having((GoogleSignInException e) => e.description, 'description', + contains('SDK reported an exception')))); + }); + + test('throws UI unavailable if there is no activity available', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => AuthorizeFailure(type: AuthorizeFailureType.noActivity)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.uiUnavailable))); + }); }); - test('initWithParams passes arguments', () async { - const SignInInitParameters initParams = SignInInitParameters( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId', - serverClientId: 'fakeServerClientId', - forceCodeForRefreshToken: true, - forceAccountName: 'fakeEmailAddress@example.com', + group('serverAuthorizationTokensForScopes', () { + // Request details used when the details of the request are not relevant to + // the test. + const AuthorizationRequestDetails defaultAuthRequest = + AuthorizationRequestDetails( + scopes: ['a'], + userId: null, + email: null, + promptIfUnauthorized: false, ); - await googleSignIn.initWithParams(initParams); - - final VerificationResult result = verify(api.init(captureAny)); - final InitParams passedParams = result.captured[0] as InitParams; - expect(passedParams.hostedDomain, initParams.hostedDomain); - expect(passedParams.scopes, initParams.scopes); - expect(passedParams.signInType, SignInType.games); - expect(passedParams.clientId, initParams.clientId); - expect(passedParams.serverClientId, initParams.serverClientId); - expect(passedParams.forceCodeForRefreshToken, - initParams.forceCodeForRefreshToken); - expect(passedParams.forceAccountName, initParams.forceAccountName); - }); - - test('clearAuthCache passes arguments', () async { - const String token = 'abc'; - - await googleSignIn.clearAuthCache(token: token); - - verify(api.clearAuthCache(token)); - }); - - test('requestScopens passes arguments', () async { - const List scopes = ['newScope', 'anotherScope']; - when(api.requestScopes(scopes)).thenAnswer((_) async => true); - - final bool response = await googleSignIn.requestScopes(scopes); - - expect(response, true); + test('serverAuthorizationTokensForScopes passes expected values', () async { + const List scopes = ['a', 'b']; + const String userId = '12345'; + const String userEmail = 'user@example.com'; + const bool promptIfUnauthorized = false; + const String hostedDomain = 'example.com'; + const String serverClientId = 'serverClientId'; + + when(mockApi.authorize(any, promptIfUnauthorized: promptIfUnauthorized)) + .thenAnswer((_) async => + PlatformAuthorizationResult(grantedScopes: [])); + + await googleSignIn.init(const InitParameters( + serverClientId: serverClientId, + hostedDomain: hostedDomain, + )); + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: userId, + email: userEmail, + promptIfUnauthorized: promptIfUnauthorized, + ))); + + final VerificationResult verification = verify(mockApi + .authorize(captureAny, promptIfUnauthorized: promptIfUnauthorized)); + final PlatformAuthorizationRequest hostParams = + verification.captured[0] as PlatformAuthorizationRequest; + expect(hostParams.scopes, scopes); + expect(hostParams.accountEmail, userEmail); + expect(hostParams.hostedDomain, hostedDomain); + expect(hostParams.serverClientIdForForcedRefreshToken, serverClientId); + }); + + test( + 'serverAuthorizationTokensForScopes passes true promptIfUnauthorized when requested', + () async { + const List scopes = ['a', 'b']; + const bool promptIfUnauthorized = true; + + when(mockApi.authorize(any, promptIfUnauthorized: promptIfUnauthorized)) + .thenAnswer((_) async => + PlatformAuthorizationResult(grantedScopes: [])); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: promptIfUnauthorized, + ))); + + verify( + mockApi.authorize(any, promptIfUnauthorized: promptIfUnauthorized)); + }); + + test('serverAuthorizationTokensForScopes passes success data to caller', + () async { + const List scopes = ['a', 'b']; + const String authCode = 'code'; + + when(mockApi.authorize(any, promptIfUnauthorized: false)) + .thenAnswer((_) async => PlatformAuthorizationResult( + grantedScopes: [], + accessToken: 'token', + serverAuthCode: authCode, + )); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + final ServerAuthorizationTokenData? result = + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: false, + ))); + + expect(result?.serverAuthCode, authCode); + }); + + test('returns null when unauthorized', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => + AuthorizeFailure(type: AuthorizeFailureType.unauthorized)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + null); + }); + + test('thows canceled if pending intent fails', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => AuthorizeFailure( + type: AuthorizeFailureType.pendingIntentException)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test('throws unknown if authorization fails', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => + AuthorizeFailure(type: AuthorizeFailureType.authorizeFailure)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); + + test('throws unknown for API exception', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => + AuthorizeFailure(type: AuthorizeFailureType.apiException)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf() + .having((GoogleSignInException e) => e.code, 'code', + GoogleSignInExceptionCode.unknownError) + .having((GoogleSignInException e) => e.description, 'description', + contains('SDK reported an exception')))); + }); + + test('throws UI unavailable if there is no activity available', () async { + when(mockApi.authorize(any, promptIfUnauthorized: false)).thenAnswer( + (_) async => AuthorizeFailure(type: AuthorizeFailureType.noActivity)); + + await googleSignIn.init(const InitParameters(serverClientId: 'id')); + expect( + googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.uiUnavailable))); + }); }); test('signOut calls through', () async { - await googleSignIn.signOut(); + await googleSignIn.signOut(const SignOutParams()); - verify(api.signOut()); + verify(mockApi.clearCredentialState()); }); - test('disconnect calls through', () async { - await googleSignIn.disconnect(); + test('disconnect also signs out', () async { + await googleSignIn.disconnect(const DisconnectParams()); - verify(api.disconnect()); + verify(mockApi.clearCredentialState()); }); - test('isSignedIn passes true response', () async { - when(api.isSignedIn()).thenAnswer((_) async => true); - - expect(await googleSignIn.isSignedIn(), true); - }); - - test('isSignedIn passes false response', () async { - when(api.isSignedIn()).thenAnswer((_) async => false); - - expect(await googleSignIn.isSignedIn(), false); + // Returning null triggers the app-facing package to create stream events, + // per GoogleSignInPlatform docs, so it's important that this returns null + // unless the platform implementation is changed to create all necessary + // notifications. + test('authenticationEvents returns null', () async { + expect(googleSignIn.authenticationEvents, null); }); } diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart index 57c49c58a24..46f5cf978a5 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart @@ -1,13 +1,13 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.6 from annotations // in google_sign_in_android/test/google_sign_in_android_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; +import 'dart:async' as _i4; import 'package:google_sign_in_android/src/messages.g.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i4; +import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -17,140 +17,104 @@ import 'package:mockito/src/dummies.dart' as _i4; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeUserData_0 extends _i1.SmartFake implements _i2.UserData { - _FakeUserData_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [GoogleSignInApi]. /// /// See the documentation for Mockito's code generation for more information. class MockGoogleSignInApi extends _i1.Mock implements _i2.GoogleSignInApi { - MockGoogleSignInApi() { - _i1.throwOnMissingStub(this); - } - @override - _i3.Future init(_i2.InitParams? arg_params) => (super.noSuchMethod( - Invocation.method( - #init, - [arg_params], + String get pigeonVar_messageChannelSuffix => (super.noSuchMethod( + Invocation.getter(#pigeonVar_messageChannelSuffix), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValueForMissingStub: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), + ), + ) as String); @override - _i3.Future<_i2.UserData> signInSilently() => (super.noSuchMethod( + _i4.Future getGoogleServicesJsonServerClientId() => + (super.noSuchMethod( Invocation.method( - #signInSilently, + #getGoogleServicesJsonServerClientId, [], ), - returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0( - this, - Invocation.method( - #signInSilently, - [], - ), - )), - ) as _i3.Future<_i2.UserData>); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i2.UserData> signIn() => (super.noSuchMethod( + _i4.Future<_i2.GetCredentialResult> getCredential( + _i2.GetCredentialRequestParams? params) => + (super.noSuchMethod( Invocation.method( - #signIn, - [], + #getCredential, + [params], ), - returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0( + returnValue: _i4.Future<_i2.GetCredentialResult>.value( + _i3.dummyValue<_i2.GetCredentialResult>( this, Invocation.method( - #signIn, - [], + #getCredential, + [params], ), )), - ) as _i3.Future<_i2.UserData>); - - @override - _i3.Future getAccessToken( - String? arg_email, - bool? arg_shouldRecoverAuth, - ) => - (super.noSuchMethod( - Invocation.method( - #getAccessToken, - [ - arg_email, - arg_shouldRecoverAuth, - ], - ), - returnValue: _i3.Future.value(_i4.dummyValue( + returnValueForMissingStub: _i4.Future<_i2.GetCredentialResult>.value( + _i3.dummyValue<_i2.GetCredentialResult>( this, Invocation.method( - #getAccessToken, - [ - arg_email, - arg_shouldRecoverAuth, - ], + #getCredential, + [params], ), )), - ) as _i3.Future); + ) as _i4.Future<_i2.GetCredentialResult>); @override - _i3.Future signOut() => (super.noSuchMethod( + _i4.Future clearCredentialState() => (super.noSuchMethod( Invocation.method( - #signOut, + #clearCredentialState, [], ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future disconnect() => (super.noSuchMethod( - Invocation.method( - #disconnect, - [], - ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); - - @override - _i3.Future isSignedIn() => (super.noSuchMethod( - Invocation.method( - #isSignedIn, - [], - ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); - - @override - _i3.Future clearAuthCache(String? arg_token) => (super.noSuchMethod( - Invocation.method( - #clearAuthCache, - [arg_token], - ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); - - @override - _i3.Future requestScopes(List? arg_scopes) => + _i4.Future<_i2.AuthorizeResult> authorize( + _i2.PlatformAuthorizationRequest? params, { + required bool? promptIfUnauthorized, + }) => (super.noSuchMethod( Invocation.method( - #requestScopes, - [arg_scopes], + #authorize, + [params], + {#promptIfUnauthorized: promptIfUnauthorized}, ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + returnValue: _i4.Future<_i2.AuthorizeResult>.value( + _i3.dummyValue<_i2.AuthorizeResult>( + this, + Invocation.method( + #authorize, + [params], + {#promptIfUnauthorized: promptIfUnauthorized}, + ), + )), + returnValueForMissingStub: _i4.Future<_i2.AuthorizeResult>.value( + _i3.dummyValue<_i2.AuthorizeResult>( + this, + Invocation.method( + #authorize, + [params], + {#promptIfUnauthorized: promptIfUnauthorized}, + ), + )), + ) as _i4.Future<_i2.AuthorizeResult>); } From 4123b18ff907261f5b2c1dbabad1f8b3ac6a5a44 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 9 May 2025 10:34:06 -0400 Subject: [PATCH 12/52] iOS Dart unit tests --- .../integration_test/google_sign_in_test.dart | 12 +- .../lib/google_sign_in_ios.dart | 23 +- .../test/google_sign_in_ios_test.dart | 1107 ++++++++++++++--- .../test/google_sign_in_ios_test.mocks.dart | 162 +-- 4 files changed, 1044 insertions(+), 260 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart index f1388ce86d6..296cca2d0b0 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart @@ -9,16 +9,16 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Can initialize the plugin', (WidgetTester tester) async { + testWidgets('Can instantiate the plugin', (WidgetTester tester) async { final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; expect(signIn, isNotNull); }); - testWidgets('Method channel handler is present', (WidgetTester tester) async { - // isSignedIn can be called without initialization, so use it to validate - // that the native method handler is present (e.g., that the channel name - // is correct). + testWidgets('Can initialize the plugin', (WidgetTester tester) async { + // This is primarily to validate that the native method handler is present + // and correctly set up to receive messages (i.e., that this doesn't + // throw). final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; - await expectLater(signIn.isSignedIn(), completes); + await expectLater(signIn.init(const InitParameters()), completes); }); } diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart index 5d61bcdcde3..fee68a7a3f2 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -131,9 +131,15 @@ class GoogleSignInIOS extends GoogleSignInPlatform { final SignInSuccess? success = result.success; if (success == null) { // There's no existing sign-in to use, so return the results of the - // combined authn+authz flow. - result = await _api.signIn(request.scopes, _nonce); - return _processAuthorizationResult(result); + // combined authn+authz flow, if prompting is allowed. + if (request.promptIfUnauthorized) { + result = await _api.signIn(request.scopes, _nonce); + return _processAuthorizationResult(result); + } else { + // No existing authentication, and no prompting allowed, so return + // no tokens. + return (accessToken: null, serverAuthCode: null); + } } else { // Discard the authentication information, and extract the user ID to // pass back to the authorization step so that it can re-associate @@ -142,10 +148,11 @@ class GoogleSignInIOS extends GoogleSignInPlatform { } } - SignInResult result = request.promptIfUnauthorized - ? await _api.addScopes(request.scopes, userId) - : await _api.getRefreshedAuthorizationTokens(userId); - if (request.promptIfUnauthorized && + final bool useExistingAuthorization = !request.promptIfUnauthorized; + SignInResult result = useExistingAuthorization + ? await _api.getRefreshedAuthorizationTokens(userId) + : await _api.addScopes(request.scopes, userId); + if (!useExistingAuthorization && result.error?.type == GoogleSignInErrorCode.scopesAlreadyGranted) { // The Google Sign In SDK returns an error when requesting scopes that are // already authorized, so in that case request updated tokens instead to @@ -159,7 +166,7 @@ class GoogleSignInIOS extends GoogleSignInPlatform { // If re-using an existing authorization, ensure that it has all of the // requested scopes before returning it, as the list of requested scopes // may have changed since the last authorization. - if (!request.promptIfUnauthorized) { + if (useExistingAuthorization) { final SignInSuccess? success = result.success; // Don't validate the OpenID Connect scopes (see // https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect diff --git a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart index bd9d473a320..d32abd13bbe 100644 --- a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -2,7 +2,6 @@ // 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_ios/google_sign_in_ios.dart'; import 'package:google_sign_in_ios/src/messages.g.dart'; @@ -12,17 +11,11 @@ import 'package:mockito/mockito.dart'; import 'google_sign_in_ios_test.mocks.dart'; -final GoogleSignInUserData _user = GoogleSignInUserData( - email: 'john.doe@gmail.com', - id: '8162538176523816253123', - photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', - displayName: 'John Doe', - serverAuthCode: '789', - idToken: '123'); - -final GoogleSignInTokenData _token = GoogleSignInTokenData( - idToken: '123', - accessToken: '456', +const GoogleSignInUserData _testUser = GoogleSignInUserData( + email: 'john.doe@gmail.com', + id: '8162538176523816253123', + photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', + displayName: 'John Doe', ); @GenerateMocks([GoogleSignInApi]) @@ -30,11 +23,11 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); late GoogleSignInIOS googleSignIn; - late MockGoogleSignInApi api; + late MockGoogleSignInApi mockApi; setUp(() { - api = MockGoogleSignInApi(); - googleSignIn = GoogleSignInIOS(api: api); + mockApi = MockGoogleSignInApi(); + googleSignIn = GoogleSignInIOS(api: mockApi); }); test('registered instance', () { @@ -42,164 +35,972 @@ void main() { expect(GoogleSignInPlatform.instance, isA()); }); - test('init throws for SignInOptions.games', () async { - expect( - () => googleSignIn.init( - hostedDomain: 'example.com', - signInOption: SignInOption.games, - clientId: 'fakeClientId'), - throwsA(isInstanceOf().having( - (PlatformException e) => e.code, 'code', 'unsupported-options'))); + group('init', () { + test('passes expected values', () async { + const String clientId = 'aClient'; + const String serverClientId = 'aServerClient'; + const String hostedDomain = 'example.com'; + + await googleSignIn.init(const InitParameters( + clientId: clientId, + serverClientId: serverClientId, + hostedDomain: hostedDomain, + )); + + final VerificationResult verification = + verify(mockApi.configure(captureAny)); + final PlatformConfigurationParams hostParams = + verification.captured[0] as PlatformConfigurationParams; + expect(hostParams.clientId, clientId); + expect(hostParams.serverClientId, serverClientId); + expect(hostParams.hostedDomain, hostedDomain); + }); }); - test('init throws for forceAccountName', () async { - expect( - () => googleSignIn.initWithParams( - const SignInInitParameters( - hostedDomain: 'example.com', - clientId: 'fakeClientId', - forceAccountName: 'fakeEmailAddress@example.com', - ), - ), - throwsA(isInstanceOf().having( - (ArgumentError e) => e.message, - 'message', - 'Force account name is not supported on iOS'))); - }); - - test('signInSilently transforms platform data to GoogleSignInUserData', - () async { - when(api.signInSilently()).thenAnswer((_) async => UserData( - email: _user.email, - userId: _user.id, - photoUrl: _user.photoUrl, - displayName: _user.displayName, - serverAuthCode: _user.serverAuthCode, - idToken: _user.idToken, - )); - - final dynamic response = await googleSignIn.signInSilently(); - - expect(response, _user); - }); - - test('signInSilently Exceptions -> throws', () async { - when(api.signInSilently()) - .thenAnswer((_) async => throw PlatformException(code: 'fail')); - - expect(googleSignIn.signInSilently(), - throwsA(isInstanceOf())); - }); - - test('signIn transforms platform data to GoogleSignInUserData', () async { - when(api.signIn()).thenAnswer((_) async => UserData( - email: _user.email, - userId: _user.id, - photoUrl: _user.photoUrl, - displayName: _user.displayName, - serverAuthCode: _user.serverAuthCode, - idToken: _user.idToken, - )); - - final dynamic response = await googleSignIn.signIn(); - - expect(response, _user); - }); - - test('signIn Exceptions -> throws', () async { - when(api.signIn()) - .thenAnswer((_) async => throw PlatformException(code: 'fail')); - - expect(googleSignIn.signIn(), throwsA(isInstanceOf())); - }); - - test('getTokens transforms platform data to GoogleSignInTokenData', () async { - const bool recoverAuth = false; - when(api.getAccessToken()).thenAnswer((_) async => - TokenData(idToken: _token.idToken, accessToken: _token.accessToken)); - - final GoogleSignInTokenData response = await googleSignIn.getTokens( - email: _user.email, shouldRecoverAuth: recoverAuth); - - expect(response, _token); + group('attemptLightweightAuthentication', () { + test('passes success data to caller', () async { + const String idToken = 'idToken'; + when(mockApi.restorePreviousSignIn()) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: idToken, + ), + accessToken: '', + grantedScopes: [], + ))); + + final AuthenticationResults? result = + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + expect(result?.user, _testUser); + expect(result?.authenticationTokens.idToken, idToken); + }); + + test('returns null for missing auth', () async { + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + + final AuthenticationResults? result = + await googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + + expect(result, null); + }); + + test('throws for other errors', () async { + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.keychainError))); + + expect( + googleSignIn.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.providerConfigurationError))); + }); }); - test('clearAuthCache silently no-ops', () async { - expect(googleSignIn.clearAuthCache(token: 'abc'), completes); + group('authenticate', () { + test('passes nonce if provided', () async { + const String nonce = 'nonce'; + when(mockApi.signIn(any, nonce)).thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: [], + ))); + + await googleSignIn.init(const InitParameters(nonce: nonce)); + await googleSignIn.authenticate(const AuthenticateParameters()); + + verify(mockApi.signIn(any, nonce)); + }); + + test('passes success data to caller', () async { + const String idToken = 'idToken'; + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: idToken, + ), + accessToken: '', + grantedScopes: [], + ))); + + final AuthenticationResults result = + await googleSignIn.authenticate(const AuthenticateParameters()); + + expect(result.user, _testUser); + expect(result.authenticationTokens.idToken, idToken); + }); + + test('throws unknown for missing auth', () async { + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf() + .having((GoogleSignInException e) => e.code, 'code', + GoogleSignInExceptionCode.unknownError) + .having((GoogleSignInException e) => e.description, 'description', + contains('No auth reported')))); + }); + + test('throws provider configuration error for keychain error', () async { + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.keychainError))); + + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.providerConfigurationError))); + }); + + test('throws provider configuration error for EEM error', () async { + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.eemError))); + + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.providerConfigurationError))); + }); + + test('throws canceled from SDK', () async { + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.canceled))); + + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test('throws user mismatch from SDK', () async { + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.userMismatch))); + + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.userMismatch))); + }); + + test('throws unknown from SDK', () async { + when(mockApi.signIn(any, null)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.unknown))); + + expect( + googleSignIn.authenticate(const AuthenticateParameters()), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); }); - test('initWithParams passes arguments', () async { - const SignInInitParameters initParams = SignInInitParameters( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - clientId: 'fakeClientId', + group('clientAuthorizationTokensForScopes', () { + // Request details used when the details of the request are not relevant to + // the test. + const AuthorizationRequestDetails defaultAuthRequest = + AuthorizationRequestDetails( + scopes: ['a'], + userId: null, + email: null, + promptIfUnauthorized: false, ); - await googleSignIn.init( - hostedDomain: initParams.hostedDomain, - scopes: initParams.scopes, - signInOption: initParams.signInOption, - clientId: initParams.clientId, - ); - - final VerificationResult result = verify(api.init(captureAny)); - final InitParams passedParams = result.captured[0] as InitParams; - expect(passedParams.hostedDomain, initParams.hostedDomain); - expect(passedParams.scopes, initParams.scopes); - expect(passedParams.clientId, initParams.clientId); - // This should use whatever the SignInInitParameters defaults are. - expect(passedParams.serverClientId, initParams.serverClientId); + test('passes expected values to addScopes if interaction is allowed', + () async { + const List scopes = ['a', 'b']; + when(mockApi.addScopes(any, _testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: scopes, + ))); + + await googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + final VerificationResult verification = + verify(mockApi.addScopes(captureAny, _testUser.id)); + final List passedScopes = + verification.captured[0] as List; + expect(passedScopes, scopes); + }); + + test( + 'passes expected values to getRefreshedAuthorizationTokens if ' + 'interaction is not allowed', () async { + const List scopes = ['a', 'b']; + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: scopes, + ))); + + await googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: false, + ))); + + verify(mockApi.getRefreshedAuthorizationTokens(_testUser.id)); + }); + + test('attempts to restore previous sign in if no user is provided', + () async { + const List scopes = ['a', 'b']; + final SignInResult signInResult = SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: [], + )); + when(mockApi.restorePreviousSignIn()) + .thenAnswer((_) async => signInResult); + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => signInResult); + + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: false, + ))); + + // With no user ID provided to clientAuthorizationTokensForScopes, the + // implementation should attempt to restore an existing sign-in, and then + // when that succeeds, get the authorization tokens for that user. + verify(mockApi.restorePreviousSignIn()); + verify(mockApi.getRefreshedAuthorizationTokens(_testUser.id)); + }); + + test('returns null if unauthenticated and interaction is not allowed', + () async { + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + + final ClientAuthorizationTokenData? result = + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: ['a', 'b'], + userId: null, + email: null, + promptIfUnauthorized: false, + ))); + + // With no user ID provided to clientAuthorizationTokensForScopes, the + // implementation should attempt to restore an existing sign-in, and then + // when that fails, return null since without prompting, there is no way + // to authenticate. + verify(mockApi.restorePreviousSignIn()); + expect(result, null); + }); + + test( + 'attempts to authenticate if no user is provided or already signed in ' + 'and interaction is allowed', () async { + const List scopes = ['a', 'b']; + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + when(mockApi.signIn(scopes, null)).thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: [], + ))); + + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: true, + ))); + + // With no user ID provided to clientAuthorizationTokensForScopes, the + // implementation should attempt to restore an existing sign-in, and when + // that fails, prompt for a combined authn+authz. + verify(mockApi.restorePreviousSignIn()); + verify(mockApi.signIn(scopes, null)); + }); + + test('passes success data to caller when refreshing existing auth', + () async { + const List scopes = ['a', 'b']; + const String accessToken = 'token'; + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: accessToken, + grantedScopes: scopes, + ))); + + final ClientAuthorizationTokenData? result = + await googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: false, + ))); + + expect(result?.accessToken, accessToken); + }); + + test('passes success data to caller when calling addScopes', () async { + const List scopes = ['a', 'b']; + const String accessToken = 'token'; + when(mockApi.addScopes(scopes, _testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: accessToken, + grantedScopes: scopes, + ))); + + final ClientAuthorizationTokenData? result = + await googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + expect(result?.accessToken, accessToken); + }); + + test( + 'successfully returns refreshed tokens if addScopes indicates the ' + 'requested scopes are already granted', () async { + const List scopes = ['a', 'b']; + const String accessToken = 'token'; + when(mockApi.addScopes(scopes, _testUser.id)).thenAnswer((_) async => + SignInResult( + error: SignInFailure( + type: GoogleSignInErrorCode.scopesAlreadyGranted))); + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: accessToken, + grantedScopes: scopes, + ))); + + final ClientAuthorizationTokenData? result = + await googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + verify(mockApi.addScopes(scopes, _testUser.id)); + verify(mockApi.getRefreshedAuthorizationTokens(_testUser.id)); + + expect(result?.accessToken, accessToken); + }); + + test('returns null if re-using existing auth and scopes are missing', + () async { + const List requestedScopes = ['a', 'b']; + const List grantedScopes = ['a']; + const String accessToken = 'token'; + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: accessToken, + grantedScopes: grantedScopes, + ))); + + final ClientAuthorizationTokenData? result = + await googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: requestedScopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: false, + ))); + + expect(result, null); + }); + + test('returns null when unauthorized', () async { + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + + expect( + await googleSignIn.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + null); + }); + + test('thows canceled from SDK', () async { + when(mockApi.addScopes(any, any)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.canceled))); + + expect( + googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: const ['a'], + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test('throws unknown from SDK', () async { + when(mockApi.addScopes(any, any)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.unknown))); + + expect( + googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: const ['a'], + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); + + test('throws user mismatch from SDK', () async { + when(mockApi.addScopes(any, any)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.userMismatch))); + + expect( + googleSignIn.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: const ['a'], + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.userMismatch))); + }); }); - test('initWithParams passes arguments', () async { - const SignInInitParameters initParams = SignInInitParameters( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - clientId: 'fakeClientId', - serverClientId: 'fakeServerClientId', - forceCodeForRefreshToken: true, + group('serverAuthorizationTokensForScopes', () { + // Request details used when the details of the request are not relevant to + // the test. + const AuthorizationRequestDetails defaultAuthRequest = + AuthorizationRequestDetails( + scopes: ['a'], + userId: null, + email: null, + promptIfUnauthorized: false, ); - await googleSignIn.initWithParams(initParams); - - final VerificationResult result = verify(api.init(captureAny)); - final InitParams passedParams = result.captured[0] as InitParams; - expect(passedParams.hostedDomain, initParams.hostedDomain); - expect(passedParams.scopes, initParams.scopes); - expect(passedParams.clientId, initParams.clientId); - expect(passedParams.serverClientId, initParams.serverClientId); - }); - - test('requestScopes passes arguments', () async { - const List scopes = ['newScope', 'anotherScope']; - when(api.requestScopes(scopes)).thenAnswer((_) async => true); - - final bool response = await googleSignIn.requestScopes(scopes); - - expect(response, true); + test('passes expected values to addScopes if interaction is allowed', + () async { + const List scopes = ['a', 'b']; + when(mockApi.addScopes(any, _testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: scopes, + ))); + + await googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + final VerificationResult verification = + verify(mockApi.addScopes(captureAny, _testUser.id)); + final List passedScopes = + verification.captured[0] as List; + expect(passedScopes, scopes); + }); + + test( + 'passes expected values to getRefreshedAuthorizationTokens if ' + 'interaction is not allowed', () async { + const List scopes = ['a', 'b']; + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: scopes, + ))); + + await googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: false, + ))); + + verify(mockApi.getRefreshedAuthorizationTokens(_testUser.id)); + }); + + test('attempts to restore previous sign in if no user is provided', + () async { + const List scopes = ['a', 'b']; + final SignInResult signInResult = SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: [], + )); + when(mockApi.restorePreviousSignIn()) + .thenAnswer((_) async => signInResult); + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => signInResult); + + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: false, + ))); + + // With no user ID provided to serverAuthorizationTokensForScopes, the + // implementation should attempt to restore an existing sign-in, and then + // when that succeeds, get the authorization tokens for that user. + verify(mockApi.restorePreviousSignIn()); + verify(mockApi.getRefreshedAuthorizationTokens(_testUser.id)); + }); + + test('returns null if unauthenticated and interaction is not allowed', + () async { + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + + final ServerAuthorizationTokenData? result = + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: ['a', 'b'], + userId: null, + email: null, + promptIfUnauthorized: false, + ))); + + // With no user ID provided to serverAuthorizationTokensForScopes, the + // implementation should attempt to restore an existing sign-in, and then + // when that fails, return null since without prompting, there is no way + // to authenticate. + verify(mockApi.restorePreviousSignIn()); + expect(result, null); + }); + + test( + 'attempts to authenticate if no user is provided or already signed in ' + 'and interaction is allowed', () async { + const List scopes = ['a', 'b']; + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + when(mockApi.signIn(scopes, null)).thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: '', + ), + accessToken: '', + grantedScopes: [], + ))); + + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: true, + ))); + + // With no user ID provided to serverAuthorizationTokensForScopes, the + // implementation should attempt to restore an existing sign-in, and when + // that fails, prompt for a combined authn+authz. + verify(mockApi.restorePreviousSignIn()); + verify(mockApi.signIn(scopes, null)); + }); + + test('passes success data to caller when refreshing existing auth', + () async { + const List scopes = ['a', 'b']; + const String serverAuthCode = 'authCode'; + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: 'token', + serverAuthCode: serverAuthCode, + grantedScopes: scopes, + ))); + + final ServerAuthorizationTokenData? result = + await googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: false, + ))); + + expect(result?.serverAuthCode, serverAuthCode); + }); + + test('passes success data to caller when calling addScopes', () async { + const List scopes = ['a', 'b']; + const String serverAuthCode = 'authCode'; + when(mockApi.addScopes(scopes, _testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: 'token', + serverAuthCode: serverAuthCode, + grantedScopes: scopes, + ))); + + final ServerAuthorizationTokenData? result = + await googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + expect(result?.serverAuthCode, serverAuthCode); + }); + + test( + 'successfully returns refreshed tokens if addScopes indicates the ' + 'requested scopes are already granted', () async { + const List scopes = ['a', 'b']; + const String serverAuthCode = 'authCode'; + when(mockApi.addScopes(scopes, _testUser.id)).thenAnswer((_) async => + SignInResult( + error: SignInFailure( + type: GoogleSignInErrorCode.scopesAlreadyGranted))); + when(mockApi.getRefreshedAuthorizationTokens(_testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: 'token', + serverAuthCode: serverAuthCode, + grantedScopes: scopes, + ))); + + final ServerAuthorizationTokenData? result = + await googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + verify(mockApi.addScopes(scopes, _testUser.id)); + verify(mockApi.getRefreshedAuthorizationTokens(_testUser.id)); + + expect(result?.serverAuthCode, serverAuthCode); + }); + + test('returns null if re-using existing auth and scopes are missing', + () async { + const List requestedScopes = ['a', 'b']; + const List grantedScopes = ['a']; + const String accessToken = 'token'; + when(mockApi.addScopes(requestedScopes, _testUser.id)) + .thenAnswer((_) async => SignInResult( + success: SignInSuccess( + user: UserData( + displayName: _testUser.displayName, + email: _testUser.email, + userId: _testUser.id, + photoUrl: _testUser.photoUrl, + idToken: 'idToken', + ), + accessToken: accessToken, + grantedScopes: grantedScopes, + ))); + + final ServerAuthorizationTokenData? result = + await googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: requestedScopes, + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))); + + expect(result, null); + }); + + test('returns null when unauthorized', () async { + when(mockApi.restorePreviousSignIn()).thenAnswer((_) async => + SignInResult( + error: + SignInFailure(type: GoogleSignInErrorCode.noAuthInKeychain))); + + expect( + await googleSignIn.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: defaultAuthRequest)), + null); + }); + + test('thows canceled from SDK', () async { + when(mockApi.addScopes(any, any)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.canceled))); + + expect( + googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: const ['a'], + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test('throws unknown from SDK', () async { + when(mockApi.addScopes(any, any)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.unknown))); + + expect( + googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: const ['a'], + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); + }); + + test('throws user mismatch from SDK', () async { + when(mockApi.addScopes(any, any)).thenAnswer((_) async => SignInResult( + error: SignInFailure(type: GoogleSignInErrorCode.userMismatch))); + + expect( + googleSignIn.serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: const ['a'], + userId: _testUser.id, + email: _testUser.email, + promptIfUnauthorized: true, + ))), + throwsA(isInstanceOf().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.userMismatch))); + }); }); test('signOut calls through', () async { - await googleSignIn.signOut(); + await googleSignIn.signOut(const SignOutParams()); - verify(api.signOut()); + verify(mockApi.signOut()); }); - test('disconnect calls through', () async { - await googleSignIn.disconnect(); + test('disconnect calls through and also signs out', () async { + await googleSignIn.disconnect(const DisconnectParams()); - verify(api.disconnect()); + verifyInOrder(>[ + mockApi.disconnect(), + mockApi.signOut(), + ]); }); - test('isSignedIn passes true response', () async { - when(api.isSignedIn()).thenAnswer((_) async => true); - - expect(await googleSignIn.isSignedIn(), true); - }); - - test('isSignedIn passes false response', () async { - when(api.isSignedIn()).thenAnswer((_) async => false); - - expect(await googleSignIn.isSignedIn(), false); + // Returning null triggers the app-facing package to create stream events, + // per GoogleSignInPlatform docs, so it's important that this returns null + // unless the platform implementation is changed to create all necessary + // notifications. + test('authenticationEvents returns null', () async { + expect(googleSignIn.authenticationEvents, null); }); } diff --git a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.mocks.dart b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.mocks.dart index b1414eb4760..7c4871506d3 100644 --- a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.mocks.dart @@ -1,12 +1,13 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in google_sign_in_ios/test/google_sign_in_ios_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; +import 'dart:async' as _i4; import 'package:google_sign_in_ios/src/messages.g.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i3; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -16,29 +17,15 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeUserData_0 extends _i1.SmartFake implements _i2.UserData { - _FakeUserData_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - -class _FakeTokenData_1 extends _i1.SmartFake implements _i2.TokenData { - _FakeTokenData_1( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); +class _FakeSignInResult_0 extends _i1.SmartFake implements _i2.SignInResult { + _FakeSignInResult_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } /// A class which mocks [GoogleSignInApi]. @@ -50,96 +37,85 @@ class MockGoogleSignInApi extends _i1.Mock implements _i2.GoogleSignInApi { } @override - _i3.Future init(_i2.InitParams? arg_params) => (super.noSuchMethod( - Invocation.method( - #init, - [arg_params], + String get pigeonVar_messageChannelSuffix => (super.noSuchMethod( + Invocation.getter(#pigeonVar_messageChannelSuffix), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#pigeonVar_messageChannelSuffix), ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + ) as String); @override - _i3.Future<_i2.UserData> signInSilently() => (super.noSuchMethod( - Invocation.method( - #signInSilently, - [], - ), - returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0( - this, - Invocation.method( - #signInSilently, - [], - ), - )), - ) as _i3.Future<_i2.UserData>); + _i4.Future configure(_i2.PlatformConfigurationParams? params) => + (super.noSuchMethod( + Invocation.method(#configure, [params]), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future<_i2.UserData> signIn() => (super.noSuchMethod( - Invocation.method( - #signIn, - [], - ), - returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0( - this, - Invocation.method( - #signIn, - [], + _i4.Future<_i2.SignInResult> restorePreviousSignIn() => (super.noSuchMethod( + Invocation.method(#restorePreviousSignIn, []), + returnValue: _i4.Future<_i2.SignInResult>.value( + _FakeSignInResult_0( + this, + Invocation.method(#restorePreviousSignIn, []), ), - )), - ) as _i3.Future<_i2.UserData>); + ), + ) as _i4.Future<_i2.SignInResult>); @override - _i3.Future<_i2.TokenData> getAccessToken() => (super.noSuchMethod( - Invocation.method( - #getAccessToken, - [], - ), - returnValue: _i3.Future<_i2.TokenData>.value(_FakeTokenData_1( - this, - Invocation.method( - #getAccessToken, - [], + _i4.Future<_i2.SignInResult> signIn(List? scopeHint, String? nonce) => + (super.noSuchMethod( + Invocation.method(#signIn, [scopeHint, nonce]), + returnValue: _i4.Future<_i2.SignInResult>.value( + _FakeSignInResult_0( + this, + Invocation.method(#signIn, [scopeHint, nonce]), ), - )), - ) as _i3.Future<_i2.TokenData>); + ), + ) as _i4.Future<_i2.SignInResult>); @override - _i3.Future signOut() => (super.noSuchMethod( - Invocation.method( - #signOut, - [], + _i4.Future<_i2.SignInResult> getRefreshedAuthorizationTokens( + String? userId, + ) => + (super.noSuchMethod( + Invocation.method(#getRefreshedAuthorizationTokens, [userId]), + returnValue: _i4.Future<_i2.SignInResult>.value( + _FakeSignInResult_0( + this, + Invocation.method(#getRefreshedAuthorizationTokens, [userId]), + ), ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + ) as _i4.Future<_i2.SignInResult>); @override - _i3.Future disconnect() => (super.noSuchMethod( - Invocation.method( - #disconnect, - [], + _i4.Future<_i2.SignInResult> addScopes( + List? scopes, + String? userId, + ) => + (super.noSuchMethod( + Invocation.method(#addScopes, [scopes, userId]), + returnValue: _i4.Future<_i2.SignInResult>.value( + _FakeSignInResult_0( + this, + Invocation.method(#addScopes, [scopes, userId]), + ), ), - returnValue: _i3.Future.value(), - returnValueForMissingStub: _i3.Future.value(), - ) as _i3.Future); + ) as _i4.Future<_i2.SignInResult>); @override - _i3.Future isSignedIn() => (super.noSuchMethod( - Invocation.method( - #isSignedIn, - [], - ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + _i4.Future signOut() => (super.noSuchMethod( + Invocation.method(#signOut, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); @override - _i3.Future requestScopes(List? arg_scopes) => - (super.noSuchMethod( - Invocation.method( - #requestScopes, - [arg_scopes], - ), - returnValue: _i3.Future.value(false), - ) as _i3.Future); + _i4.Future disconnect() => (super.noSuchMethod( + Invocation.method(#disconnect, []), + returnValue: _i4.Future.value(), + returnValueForMissingStub: _i4.Future.value(), + ) as _i4.Future); } From 165fb78d9da00704f61f30e112dd520eb7c4aae4 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 9 May 2025 14:00:35 -0400 Subject: [PATCH 13/52] Web Dart 'unit' tests, and fix lightweight auth return --- .../google_sign_in_web_test.dart | 261 +++++++++--------- .../google_sign_in_web_test.mocks.dart | 184 ++++-------- .../example/integration_test/people_test.dart | 3 - .../example/integration_test/utils_test.dart | 53 +--- .../integration_test/web_only_test.dart | 74 +++-- .../integration_test/web_only_test.mocks.dart | 184 ++++-------- .../example/lib/button_tester.dart | 18 +- .../lib/google_sign_in_web.dart | 12 +- 8 files changed, 311 insertions(+), 478 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart index 88b69f26fb5..9ffff117dee 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -4,7 +4,6 @@ import 'dart:async'; -import 'package:flutter/services.dart' show PlatformException; 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_web/google_sign_in_web.dart'; @@ -54,7 +53,7 @@ void main() { }); }); - group('initWithParams', () { + group('init', () { late GoogleSignInPlugin plugin; late MockGisSdkClient mockGis; @@ -67,10 +66,9 @@ void main() { }); testWidgets('initializes if all is OK', (_) async { - await plugin.initWithParams( - const SignInInitParameters( + await plugin.init( + const InitParameters( clientId: 'some-non-null-client-id', - scopes: ['ok1', 'ok2', 'ok3'], ), ); @@ -79,16 +77,16 @@ void main() { testWidgets('asserts clientId is not null', (_) async { expect(() async { - await plugin.initWithParams( - const SignInInitParameters(), + await plugin.init( + const InitParameters(), ); }, throwsAssertionError); }); testWidgets('asserts serverClientId must be null', (_) async { expect(() async { - await plugin.initWithParams( - const SignInInitParameters( + await plugin.init( + const InitParameters( clientId: 'some-non-null-client-id', serverClientId: 'unexpected-non-null-client-id', ), @@ -96,63 +94,28 @@ void main() { }, throwsAssertionError); }); - testWidgets('asserts no scopes have any spaces', (_) async { - expect(() async { - await plugin.initWithParams( - const SignInInitParameters( - clientId: 'some-non-null-client-id', - scopes: ['ok1', 'ok2', 'not ok', 'ok3'], - ), - ); - }, throwsAssertionError); - }); - - testWidgets('asserts forceAccountName must be null', (_) async { - expect(() async { - await plugin.initWithParams( - const SignInInitParameters( - clientId: 'some-non-null-client-id', - forceAccountName: 'fakeEmailAddress@example.com', - ), - ); - }, throwsAssertionError); - }); - testWidgets('must be called for most of the API to work', (_) async { expect(() async { - await plugin.signInSilently(); - }, throwsStateError); - - expect(() async { - await plugin.signIn(); - }, throwsStateError); - - expect(() async { - await plugin.getTokens(email: ''); - }, throwsStateError); - - expect(() async { - await plugin.signOut(); + await plugin.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); }, throwsStateError); expect(() async { - await plugin.disconnect(); + await plugin.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: [], + userId: null, + email: null, + promptIfUnauthorized: false))); }, throwsStateError); expect(() async { - await plugin.isSignedIn(); + await plugin.signOut(const SignOutParams()); }, throwsStateError); expect(() async { - await plugin.clearAuthCache(token: ''); - }, throwsStateError); - - expect(() async { - await plugin.requestScopes([]); - }, throwsStateError); - - expect(() async { - await plugin.canAccessScopes([]); + await plugin.disconnect(const DisconnectParams()); }, throwsStateError); }); }); @@ -160,9 +123,8 @@ void main() { group('(with mocked GIS)', () { late GoogleSignInPlugin plugin; late MockGisSdkClient mockGis; - const SignInInitParameters options = SignInInitParameters( + const InitParameters options = InitParameters( clientId: 'some-non-null-client-id', - scopes: ['ok1', 'ok2', 'ok3'], ); setUp(() { @@ -173,139 +135,182 @@ void main() { ); }); - group('signInSilently', () { + group('attemptLightweightAuthentication', () { setUp(() { - plugin.initWithParams(options); + plugin.init(options); }); - testWidgets('returns the GIS response', (_) async { - final GoogleSignInUserData someUser = extractUserData(person)!; - + testWidgets('Calls requestOneTap on GIS client', (_) async { mockito - .when(mockGis.signInSilently()) - .thenAnswer((_) => Future.value(someUser)); + .when(mockGis.requestOneTap()) + .thenAnswer((_) => Future.value()); - expect(await plugin.signInSilently(), someUser); + final Future? future = + plugin.attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); - mockito - .when(mockGis.signInSilently()) - .thenAnswer((_) => Future.value()); + expect(future, null); - expect(await plugin.signInSilently(), isNull); - }); - }); + // Since the implementation intentionally doesn't return a future, just + // given the async call a chance to be made. + await pumpEventQueue(); - group('signIn', () { - setUp(() { - plugin.initWithParams(options); + mockito.verify(mockGis.requestOneTap()); }); + }); - testWidgets('returns the signed-in user', (_) async { - final GoogleSignInUserData someUser = extractUserData(person)!; - - mockito - .when(mockGis.signIn()) - .thenAnswer((_) => Future.value(someUser)); + group('clientAuthorizationTokensForScopes', () { + const String someAccessToken = '50m3_4cc35_70k3n'; + const List scopes = ['scope1', 'scope2']; - expect(await plugin.signIn(), someUser); + setUp(() { + plugin.init(options); }); - testWidgets('returns null if no user is signed in', (_) async { + testWidgets('calls requestScopes on GIS client', (_) async { mockito - .when(mockGis.signIn()) - .thenAnswer((_) => Future.value()); - - expect(await plugin.signIn(), isNull); - }); + .when( + mockGis.requestScopes(mockito.any, + promptIfUnauthorized: + mockito.anyNamed('promptIfUnauthorized'), + userHint: mockito.anyNamed('userHint')), + ) + .thenAnswer((_) => Future.value(someAccessToken)); - testWidgets('converts inner errors to PlatformException', (_) async { - mockito.when(mockGis.signIn()).thenThrow('popup_closed'); + final ClientAuthorizationTokenData? token = + await plugin.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: false))); - try { - await plugin.signIn(); - fail('signIn should have thrown an exception'); - } catch (exception) { - expect(exception, isA()); - expect((exception as PlatformException).code, 'popup_closed'); - } - }); - }); + final List arguments = mockito + .verify( + mockGis.requestScopes(mockito.captureAny, + promptIfUnauthorized: + mockito.captureAnyNamed('promptIfUnauthorized'), + userHint: mockito.captureAnyNamed('userHint')), + ) + .captured; - group('canAccessScopes', () { - const String someAccessToken = '50m3_4cc35_70k3n'; - const List scopes = ['scope1', 'scope2']; + expect(token?.accessToken, someAccessToken); - setUp(() { - plugin.initWithParams(options); + expect(arguments.elementAt(0), scopes); + expect(arguments.elementAt(1), false); + expect(arguments.elementAt(2), null); }); - testWidgets('passes-through call to gis client', (_) async { + testWidgets('passes expected values to requestScopes', (_) async { + const String someUserId = 'someUser'; mockito .when( - mockGis.canAccessScopes(mockito.captureAny, mockito.captureAny), + mockGis.requestScopes(mockito.any, + promptIfUnauthorized: + mockito.anyNamed('promptIfUnauthorized'), + userHint: mockito.anyNamed('userHint')), ) - .thenAnswer((_) => Future.value(true)); + .thenAnswer((_) => Future.value(someAccessToken)); - final bool canAccess = - await plugin.canAccessScopes(scopes, accessToken: someAccessToken); + final ClientAuthorizationTokenData? token = + await plugin.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: scopes, + userId: someUserId, + email: 'someone@example.com', + promptIfUnauthorized: true))); final List arguments = mockito .verify( - mockGis.canAccessScopes(mockito.captureAny, mockito.captureAny), + mockGis.requestScopes(mockito.captureAny, + promptIfUnauthorized: + mockito.captureAnyNamed('promptIfUnauthorized'), + userHint: mockito.captureAnyNamed('userHint')), ) .captured; - expect(canAccess, isTrue); + expect(token?.accessToken, someAccessToken); - expect(arguments.first, scopes); - expect(arguments.elementAt(1), someAccessToken); + expect(arguments.elementAt(0), scopes); + expect(arguments.elementAt(1), true); + expect(arguments.elementAt(2), someUserId); }); }); - group('requestServerAuthCode', () { - const String someAuthCode = '50m3_4u7h_c0d3'; + group('serverAuthorizationTokensForScopes', () { + const String someAuthCode = 'abc123'; + const List scopes = ['scope1', 'scope2']; setUp(() { - plugin.initWithParams(options); + plugin.init(options); }); - testWidgets('passes-through call to gis client', (_) async { + testWidgets('calls requestServerAuthCode on GIS client', (_) async { mockito - .when(mockGis.requestServerAuthCode()) + .when( + mockGis.requestServerAuthCode(mockito.any), + ) .thenAnswer((_) => Future.value(someAuthCode)); - final String? serverAuthCode = await plugin.requestServerAuthCode(); + const AuthorizationRequestDetails request = AuthorizationRequestDetails( + scopes: scopes, + userId: null, + email: null, + promptIfUnauthorized: true); + final ServerAuthorizationTokenData? token = + await plugin.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: request)); + + final List arguments = mockito + .verify(mockGis.requestServerAuthCode(mockito.captureAny)) + .captured; + + expect(token?.serverAuthCode, someAuthCode); - expect(serverAuthCode, someAuthCode); + final AuthorizationRequestDetails passedRequest = + arguments.first! as AuthorizationRequestDetails; + expect(passedRequest.scopes, request.scopes); + expect(passedRequest.userId, request.userId); + expect(passedRequest.email, request.email); + expect( + passedRequest.promptIfUnauthorized, request.promptIfUnauthorized); }); }); }); group('userDataEvents', () { - final StreamController controller = - StreamController.broadcast(); + final StreamController controller = + StreamController.broadcast(); late GoogleSignInPlugin plugin; setUp(() { plugin = GoogleSignInPlugin( debugOverrideLoader: true, - debugOverrideUserDataController: controller, + debugAuthenticationController: controller, ); }); testWidgets('accepts async user data events from GIS.', (_) async { - final Future data = plugin.userDataEvents!.first; + final Future event = + plugin.authenticationEvents.first; - final GoogleSignInUserData expected = extractUserData(person)!; + final AuthenticationEvent expected = AuthenticationEventSignIn( + user: extractUserData(person)!, + authenticationTokens: + const AuthenticationTokenData(idToken: 'someToken')); controller.add(expected); - expect(await data, expected, + expect(await event, expected, reason: 'Sign-in events should be propagated'); - final Future more = plugin.userDataEvents!.first; - controller.add(null); + final Future nextEvent = + plugin.authenticationEvents.first; + controller.add(AuthenticationEventSignOut()); - expect(await more, isNull, + expect(await nextEvent, isA(), reason: 'Sign-out events can also be propagated'); }); }); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart index a6b5e9a7183..fd6866e78bb 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.mocks.dart @@ -1,14 +1,14 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in google_sign_in_web_integration_tests/integration_test/google_sign_in_web_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' - as _i2; -import 'package:google_sign_in_web/src/button_configuration.dart' as _i5; -import 'package:google_sign_in_web/src/gis_client.dart' as _i3; + as _i5; +import 'package:google_sign_in_web/src/button_configuration.dart' as _i4; +import 'package:google_sign_in_web/src/gis_client.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: type=lint @@ -19,161 +19,73 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake - implements _i2.GoogleSignInTokenData { - _FakeGoogleSignInTokenData_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [GisSdkClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient { +class MockGisSdkClient extends _i1.Mock implements _i2.GisSdkClient { @override - _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod( - Invocation.method( - #signInSilently, - [], - ), - returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), - returnValueForMissingStub: - _i4.Future<_i2.GoogleSignInUserData?>.value(), - ) as _i4.Future<_i2.GoogleSignInUserData?>); + void requestOneTap() => super.noSuchMethod( + Invocation.method(#requestOneTap, []), + returnValueForMissingStub: null, + ); @override - _i4.Future renderButton( + _i3.Future renderButton( Object? parent, - _i5.GSIButtonConfiguration? options, + _i4.GSIButtonConfiguration? options, ) => (super.noSuchMethod( - Invocation.method( - #renderButton, - [ - parent, - options, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future requestServerAuthCode() => (super.noSuchMethod( - Invocation.method( - #requestServerAuthCode, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod( - Invocation.method( - #signIn, - [], - ), - returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), - returnValueForMissingStub: - _i4.Future<_i2.GoogleSignInUserData?>.value(), - ) as _i4.Future<_i2.GoogleSignInUserData?>); - - @override - _i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod( - Invocation.method( - #getTokens, - [], - ), - returnValue: _FakeGoogleSignInTokenData_0( - this, - Invocation.method( - #getTokens, - [], - ), - ), - returnValueForMissingStub: _FakeGoogleSignInTokenData_0( - this, - Invocation.method( - #getTokens, - [], - ), - ), - ) as _i2.GoogleSignInTokenData); + Invocation.method(#renderButton, [parent, options]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future signOut() => (super.noSuchMethod( - Invocation.method( - #signOut, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + _i3.Future requestServerAuthCode( + _i5.AuthorizationRequestDetails? request, + ) => + (super.noSuchMethod( + Invocation.method(#requestServerAuthCode, [request]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future disconnect() => (super.noSuchMethod( - Invocation.method( - #disconnect, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + _i3.Future signOut() => (super.noSuchMethod( + Invocation.method(#signOut, []), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future isSignedIn() => (super.noSuchMethod( - Invocation.method( - #isSignedIn, - [], - ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); + _i3.Future disconnect() => (super.noSuchMethod( + Invocation.method(#disconnect, []), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future clearAuthCache() => (super.noSuchMethod( - Invocation.method( - #clearAuthCache, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future requestScopes(List? scopes) => (super.noSuchMethod( + _i3.Future requestScopes( + List? scopes, { + required bool? promptIfUnauthorized, + String? userHint, + }) => + (super.noSuchMethod( Invocation.method( #requestScopes, [scopes], + { + #promptIfUnauthorized: promptIfUnauthorized, + #userHint: userHint, + }, ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); - - @override - _i4.Future canAccessScopes( - List? scopes, - String? accessToken, - ) => - (super.noSuchMethod( - Invocation.method( - #canAccessScopes, - [ - scopes, - accessToken, - ], - ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); } diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart index e81ccb6e95b..5ed1244e8d8 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart @@ -52,7 +52,6 @@ void main() { expect(user.id, expectedPersonId); expect(user.displayName, expectedPersonName); expect(user.photoUrl, expectedPersonPhoto); - expect(user.idToken, isNull); expect( accessTokenCompleter.future, completion('Bearer $expectedAccessToken'), @@ -87,7 +86,6 @@ void main() { expect(user.id, expectedPersonId); expect(user.displayName, expectedPersonName); expect(user.photoUrl, expectedPersonPhoto); - expect(user.idToken, isNull); }); testWidgets('no name/photo - keeps going', (_) async { @@ -104,7 +102,6 @@ void main() { expect(user.id, expectedPersonId); expect(user.displayName, isNull); expect(user.photoUrl, isNull); - expect(user.idToken, isNull); }); testWidgets('no userId - throws assertion error', (_) async { diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart index 9dec77b81bd..0914f762cd6 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/utils_test.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_identity_services_web/id.dart'; -import 'package:google_identity_services_web/oauth2.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in_web/src/utils.dart'; import 'package:integration_test/integration_test.dart'; @@ -17,63 +16,39 @@ import 'src/jwt_examples.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('gisResponsesToTokenData', () { - testWidgets('null objects -> no problem', (_) async { - final GoogleSignInTokenData tokens = gisResponsesToTokenData(null, null); - - expect(tokens.accessToken, isNull); - expect(tokens.idToken, isNull); - expect(tokens.serverAuthCode, isNull); - }); - - testWidgets('non-null objects are correctly used', (_) async { - const String expectedIdToken = 'some-value-for-testing'; - const String expectedAccessToken = 'another-value-for-testing'; - - final CredentialResponse credential = - jsifyAs({ - 'credential': expectedIdToken, - }); - final TokenResponse token = jsifyAs({ - 'access_token': expectedAccessToken, - }); - final GoogleSignInTokenData tokens = - gisResponsesToTokenData(credential, token); - - expect(tokens.accessToken, expectedAccessToken); - expect(tokens.idToken, expectedIdToken); - expect(tokens.serverAuthCode, isNull); - }); - }); - - group('gisResponsesToUserData', () { + group('gisResponsesToAuthenticationEvent', () { testWidgets('happy case', (_) async { - final GoogleSignInUserData data = gisResponsesToUserData(goodCredential)!; + final AuthenticationEventSignIn signIn = + gisResponsesToAuthenticationEvent(goodCredential)! + as AuthenticationEventSignIn; + final GoogleSignInUserData data = signIn.user; expect(data.displayName, 'Vincent Adultman'); expect(data.id, '123456'); expect(data.email, 'adultman@example.com'); expect(data.photoUrl, 'https://thispersondoesnotexist.com/image?x=.jpg'); - expect(data.idToken, goodJwtToken); + expect(signIn.authenticationTokens.idToken, goodJwtToken); }); testWidgets('happy case (minimal)', (_) async { - final GoogleSignInUserData data = - gisResponsesToUserData(minimalCredential)!; + final AuthenticationEventSignIn signIn = + gisResponsesToAuthenticationEvent(minimalCredential)! + as AuthenticationEventSignIn; + final GoogleSignInUserData data = signIn.user; expect(data.displayName, isNull); expect(data.id, '123456'); expect(data.email, 'adultman@example.com'); expect(data.photoUrl, isNull); - expect(data.idToken, minimalJwtToken); + expect(signIn.authenticationTokens.idToken, minimalJwtToken); }); testWidgets('null response -> null', (_) async { - expect(gisResponsesToUserData(null), isNull); + expect(gisResponsesToAuthenticationEvent(null), isNull); }); testWidgets('null response.credential -> null', (_) async { - expect(gisResponsesToUserData(nullCredential), isNull); + expect(gisResponsesToAuthenticationEvent(nullCredential), isNull); }); testWidgets('invalid payload -> null', (_) async { @@ -81,7 +56,7 @@ void main() { jsifyAs({ 'credential': 'some-bogus.thing-that-is-not.valid-jwt', }); - expect(gisResponsesToUserData(response), isNull); + expect(gisResponsesToAuthenticationEvent(response), isNull); }); }); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart index 508e291bf2a..79f8cd35cd8 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart @@ -2,6 +2,7 @@ // 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_test/flutter_test.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in_web/google_sign_in_web.dart' @@ -10,7 +11,7 @@ import 'package:google_sign_in_web/src/gis_client.dart'; import 'package:google_sign_in_web/web_only.dart' as web; import 'package:integration_test/integration_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart' as mockito; +import 'package:mockito/mockito.dart'; import 'web_only_test.mocks.dart'; @@ -31,42 +32,73 @@ void main() { web.renderButton(); }, throwsAssertionError); }); - - testWidgets('requestServerAuthCode throws', (WidgetTester _) async { - expect(() async { - await web.requestServerAuthCode(); - }, throwsAssertionError); - }); }); group('web plugin instance', () { - const String someAuthCode = '50m3_4u7h_c0d3'; late MockGisSdkClient mockGis; - setUp(() { + setUp(() async { mockGis = MockGisSdkClient(); GoogleSignInPlatform.instance = GoogleSignInPlugin( debugOverrideLoader: true, debugOverrideGisSdkClient: mockGis, - )..initWithParams( - const SignInInitParameters( - clientId: 'does-not-matter', - ), - ); + ); + await GoogleSignInPlatform.instance.init( + const InitParameters( + clientId: 'does-not-matter', + ), + ); }); - testWidgets('call reaches GIS API', (WidgetTester _) async { - mockito - .when(mockGis.requestServerAuthCode()) - .thenAnswer((_) => Future.value(someAuthCode)); + testWidgets('renderButton returns successfully', (WidgetTester _) async { + when(mockGis.renderButton(any, any)) + .thenAnswer((_) => Future.value()); - final String? serverAuthCode = await web.requestServerAuthCode(); + final Widget button = web.renderButton(); - expect(serverAuthCode, someAuthCode); + expect(button, isNotNull); }); }); } /// Fake non-web implementation used to verify that the web_only methods /// throw when the wrong type of instance is configured. -class NonWebImplementation extends GoogleSignInPlatform {} +class NonWebImplementation extends GoogleSignInPlatform { + @override + Future? attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) { + throw UnimplementedError(); + } + + @override + Future authenticate(AuthenticateParameters params) { + throw UnimplementedError(); + } + + @override + Future clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters params) { + throw UnimplementedError(); + } + + @override + Future disconnect(DisconnectParams params) { + throw UnimplementedError(); + } + + @override + Future init(InitParameters params) { + throw UnimplementedError(); + } + + @override + Future serverAuthorizationTokensForScopes( + ServerAuthorizationTokensForScopesParameters params) { + throw UnimplementedError(); + } + + @override + Future signOut(SignOutParams params) { + throw UnimplementedError(); + } +} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.mocks.dart index e7505d6c8ef..b2b181c0cd8 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.mocks.dart @@ -1,14 +1,14 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in google_sign_in_web_integration_tests/integration_test/web_only_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i4; +import 'dart:async' as _i3; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart' - as _i2; -import 'package:google_sign_in_web/src/button_configuration.dart' as _i5; -import 'package:google_sign_in_web/src/gis_client.dart' as _i3; + as _i5; +import 'package:google_sign_in_web/src/button_configuration.dart' as _i4; +import 'package:google_sign_in_web/src/gis_client.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: type=lint @@ -19,161 +19,73 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake - implements _i2.GoogleSignInTokenData { - _FakeGoogleSignInTokenData_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); -} - /// A class which mocks [GisSdkClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockGisSdkClient extends _i1.Mock implements _i3.GisSdkClient { +class MockGisSdkClient extends _i1.Mock implements _i2.GisSdkClient { @override - _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod( - Invocation.method( - #signInSilently, - [], - ), - returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), - returnValueForMissingStub: - _i4.Future<_i2.GoogleSignInUserData?>.value(), - ) as _i4.Future<_i2.GoogleSignInUserData?>); + void requestOneTap() => super.noSuchMethod( + Invocation.method(#requestOneTap, []), + returnValueForMissingStub: null, + ); @override - _i4.Future renderButton( + _i3.Future renderButton( Object? parent, - _i5.GSIButtonConfiguration? options, + _i4.GSIButtonConfiguration? options, ) => (super.noSuchMethod( - Invocation.method( - #renderButton, - [ - parent, - options, - ], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future requestServerAuthCode() => (super.noSuchMethod( - Invocation.method( - #requestServerAuthCode, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod( - Invocation.method( - #signIn, - [], - ), - returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), - returnValueForMissingStub: - _i4.Future<_i2.GoogleSignInUserData?>.value(), - ) as _i4.Future<_i2.GoogleSignInUserData?>); - - @override - _i2.GoogleSignInTokenData getTokens() => (super.noSuchMethod( - Invocation.method( - #getTokens, - [], - ), - returnValue: _FakeGoogleSignInTokenData_0( - this, - Invocation.method( - #getTokens, - [], - ), - ), - returnValueForMissingStub: _FakeGoogleSignInTokenData_0( - this, - Invocation.method( - #getTokens, - [], - ), - ), - ) as _i2.GoogleSignInTokenData); + Invocation.method(#renderButton, [parent, options]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future signOut() => (super.noSuchMethod( - Invocation.method( - #signOut, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + _i3.Future requestServerAuthCode( + _i5.AuthorizationRequestDetails? request, + ) => + (super.noSuchMethod( + Invocation.method(#requestServerAuthCode, [request]), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future disconnect() => (super.noSuchMethod( - Invocation.method( - #disconnect, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + _i3.Future signOut() => (super.noSuchMethod( + Invocation.method(#signOut, []), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future isSignedIn() => (super.noSuchMethod( - Invocation.method( - #isSignedIn, - [], - ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); + _i3.Future disconnect() => (super.noSuchMethod( + Invocation.method(#disconnect, []), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); @override - _i4.Future clearAuthCache() => (super.noSuchMethod( - Invocation.method( - #clearAuthCache, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); - - @override - _i4.Future requestScopes(List? scopes) => (super.noSuchMethod( + _i3.Future requestScopes( + List? scopes, { + required bool? promptIfUnauthorized, + String? userHint, + }) => + (super.noSuchMethod( Invocation.method( #requestScopes, [scopes], + { + #promptIfUnauthorized: promptIfUnauthorized, + #userHint: userHint, + }, ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); - - @override - _i4.Future canAccessScopes( - List? scopes, - String? accessToken, - ) => - (super.noSuchMethod( - Invocation.method( - #canAccessScopes, - [ - scopes, - accessToken, - ], - ), - returnValue: _i4.Future.value(false), - returnValueForMissingStub: _i4.Future.value(false), - ) as _i4.Future); + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); } diff --git a/packages/google_sign_in/google_sign_in_web/example/lib/button_tester.dart b/packages/google_sign_in/google_sign_in_web/example/lib/button_tester.dart index 02b4346e9b8..862afe64e91 100644 --- a/packages/google_sign_in/google_sign_in_web/example/lib/button_tester.dart +++ b/packages/google_sign_in/google_sign_in_web/example/lib/button_tester.dart @@ -14,7 +14,7 @@ import 'src/button_configuration_column.dart'; final GoogleSignInPlatform _platform = GoogleSignInPlatform.instance; Future main() async { - await _platform.initWithParams(const SignInInitParameters( + await _platform.init(const InitParameters( clientId: 'your-client_id.apps.googleusercontent.com', )); runApp( @@ -41,19 +41,21 @@ class _ButtonConfiguratorState extends State { @override void initState() { super.initState(); - _platform.userDataEvents?.listen((GoogleSignInUserData? userData) { + _platform.authenticationEvents?.listen((AuthenticationEvent authEvent) { setState(() { - _userData = userData; + switch (authEvent) { + case AuthenticationEventSignIn(): + _userData = authEvent.user; + case AuthenticationEventSignOut(): + case AuthenticationEventException(): + _userData = null; + } }); }); } void _handleSignOut() { - _platform.signOut(); - setState(() { - // signOut does not broadcast through the userDataEvents, so we fake it. - _userData = null; - }); + _platform.signOut(const SignOutParams()); } void _handleNewWebButtonConfiguration(GSIButtonConfiguration newConfig) { 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 58ce0b619ee..e897f13e9c8 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 @@ -148,13 +148,11 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future attemptLightweightAuthentication( - AttemptLightweightAuthenticationParameters params) async { - await initialized; - - // The new user is being injected from the `userDataEvents` Stream. - _gisClient.requestOneTap(); - + Future? attemptLightweightAuthentication( + AttemptLightweightAuthenticationParameters params) { + initialized.then((void value) { + _gisClient.requestOneTap(); + }); // One tap does not necessarily return immediately, and may never return, // so clients should not await it. Return null to signal that. return null; From eda66bbf01e4430da215f5d38609a88da8131a7e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 9 May 2025 16:22:46 -0400 Subject: [PATCH 14/52] Update example apps --- .../integration_test/google_sign_in_test.dart | 8 +- .../example/lib/main.dart | 156 ++++++++++++------ .../google_sign_in_ios/example/lib/main.dart | 155 +++++++++++------ 3 files changed, 212 insertions(+), 107 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart index f1388ce86d6..e6cca19c509 100644 --- a/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart @@ -9,16 +9,14 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Can initialize the plugin', (WidgetTester tester) async { + testWidgets('Can instantiate the plugin', (WidgetTester tester) async { final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; expect(signIn, isNotNull); }); testWidgets('Method channel handler is present', (WidgetTester tester) async { - // isSignedIn can be called without initialization, so use it to validate - // that the native method handler is present (e.g., that the channel name - // is correct). + // Validate that the native method handler is present and configured. final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; - await expectLater(signIn.isSignedIn(), completes); + await expectLater(signIn.signOut(const SignOutParams()), completes); }); } diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart index d03019da185..52b1c6b7ca2 100644 --- a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -8,10 +8,14 @@ import 'dart:async'; import 'dart:convert' show json; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:http/http.dart' as http; +const List _scopes = [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', +]; + void main() { runApp( const MaterialApp( @@ -30,7 +34,9 @@ class SignInDemo extends StatefulWidget { class SignInDemoState extends State { GoogleSignInUserData? _currentUser; + bool _isAuthorized = false; String _contactText = ''; + String _errorMessage = ''; // Future that completes when `init` has completed on the sign in instance. Future? _initialization; @@ -41,15 +47,10 @@ class SignInDemoState extends State { } Future _ensureInitialized() { + // The example app uses the parsing of values from google-services.json + // to provide the serverClientId, otherwise it would be required here. return _initialization ??= - GoogleSignInPlatform.instance.initWithParams(const SignInInitParameters( - scopes: [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', - ], - // The example app uses the parsing of values from google-services.json - // to provide the serverClientId, otherwise it would be required here. - )) + GoogleSignInPlatform.instance.init(const InitParameters()) ..catchError((dynamic _) { _initialization = null; }); @@ -58,33 +59,71 @@ class SignInDemoState extends State { void _setUser(GoogleSignInUserData? user) { setState(() { _currentUser = user; - if (user != null) { - _handleGetContact(user); - } }); + if (user != null) { + // Try getting contacts, in case authorization is already granted. + _handleGetContact(user); + } } Future _signIn() async { await _ensureInitialized(); - final GoogleSignInUserData? newUser = - await GoogleSignInPlatform.instance.signInSilently(); - _setUser(newUser); + try { + final AuthenticationResults? result = await GoogleSignInPlatform.instance + .attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + _setUser(result?.user); + } on GoogleSignInException catch (e) { + setState(() { + _errorMessage = e.code == GoogleSignInExceptionCode.canceled + ? '' + : 'GoogleSignInException ${e.code}: ${e.description}'; + }); + } } - Future> _getAuthHeaders() async { - final GoogleSignInUserData? user = _currentUser; - if (user == null) { - throw StateError('No user signed in'); + Future _handleAuthorizeScopes(GoogleSignInUserData user) async { + try { + final ClientAuthorizationTokenData? tokens = await GoogleSignInPlatform + .instance + .clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: _scopes, + userId: user.id, + email: user.email, + promptIfUnauthorized: true))); + + setState(() { + _isAuthorized = tokens != null; + _errorMessage = ''; + }); + if (_isAuthorized) { + unawaited(_handleGetContact(user)); + } + } on GoogleSignInException catch (e) { + setState(() { + _errorMessage = 'GoogleSignInException ${e.code}: ${e.description}'; + }); } + } - final GoogleSignInTokenData response = - await GoogleSignInPlatform.instance.getTokens( - email: user.email, - shouldRecoverAuth: true, - ); + Future?> _getAuthHeaders( + GoogleSignInUserData user) async { + final ClientAuthorizationTokenData? tokens = + await GoogleSignInPlatform.instance.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: _scopes, + userId: user.id, + email: user.email, + promptIfUnauthorized: false))); + if (tokens == null) { + return null; + } return { - 'Authorization': 'Bearer ${response.accessToken}', + 'Authorization': 'Bearer ${tokens.accessToken}', // TODO(kevmoo): Use the correct value once it's available. // See https://github.com/flutter/flutter/issues/80905 'X-Goog-AuthUser': '0', @@ -95,10 +134,17 @@ class SignInDemoState extends State { setState(() { _contactText = 'Loading contact info...'; }); + final Map? headers = await _getAuthHeaders(user); + setState(() { + _isAuthorized = headers != null; + }); + if (headers == null) { + return; + } final http.Response response = await http.get( Uri.parse('https://people.googleapis.com/v1/people/me/connections' '?requestMask.includeField=person.names'), - headers: await _getAuthHeaders(), + headers: headers, ); if (response.statusCode != 200) { setState(() { @@ -120,55 +166,63 @@ class SignInDemoState extends State { Future _handleSignIn() async { try { await _ensureInitialized(); - _setUser(await GoogleSignInPlatform.instance.signIn()); - } catch (error) { - final bool canceled = - error is PlatformException && error.code == 'sign_in_canceled'; - if (!canceled) { - print(error); - } + final AuthenticationResults result = await GoogleSignInPlatform.instance + .authenticate(const AuthenticateParameters()); + _setUser(result.user); + } on GoogleSignInException catch (e) { + setState(() { + _errorMessage = e.code == GoogleSignInExceptionCode.canceled + ? '' + : 'GoogleSignInException ${e.code}: ${e.description}'; + }); } } Future _handleSignOut() async { await _ensureInitialized(); - await GoogleSignInPlatform.instance.disconnect(); + await GoogleSignInPlatform.instance.disconnect(const DisconnectParams()); } Widget _buildBody() { final GoogleSignInUserData? user = _currentUser; - if (user != null) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (user != null) ...[ ListTile( title: Text(user.displayName ?? ''), subtitle: Text(user.email), ), const Text('Signed in successfully.'), - Text(_contactText), ElevatedButton( onPressed: _handleSignOut, child: const Text('SIGN OUT'), ), - ElevatedButton( - child: const Text('REFRESH'), - onPressed: () => _handleGetContact(user), - ), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + if (_isAuthorized) ...[ + // The user has Authorized all required scopes. + if (_contactText.isNotEmpty) Text(_contactText), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ] else ...[ + // The user has NOT Authorized all required scopes. + const Text('Authorization needed to read your contacts.'), + ElevatedButton( + onPressed: () => _handleAuthorizeScopes(user), + child: const Text('REQUEST PERMISSIONS'), + ), + ], + ] else ...[ const Text('You are not currently signed in.'), ElevatedButton( onPressed: _handleSignIn, child: const Text('SIGN IN'), ), ], - ); - } + if (_errorMessage.isNotEmpty) Text(_errorMessage), + ], + ); } @override diff --git a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart index 81790d193c7..a821469792d 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_ios/example/lib/main.dart @@ -8,10 +8,14 @@ import 'dart:async'; import 'dart:convert' show json; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:http/http.dart' as http; +const List _scopes = [ + 'email', + 'https://www.googleapis.com/auth/contacts.readonly', +]; + void main() { runApp( const MaterialApp( @@ -30,9 +34,10 @@ class SignInDemo extends StatefulWidget { class SignInDemoState extends State { GoogleSignInUserData? _currentUser; + bool _isAuthorized = false; String _contactText = ''; - // Future that completes when `initWithParams` has completed on the sign in - // instance. + String _errorMessage = ''; + // Future that completes when `init` has completed on the sign in instance. Future? _initialization; @override @@ -43,12 +48,7 @@ class SignInDemoState extends State { Future _ensureInitialized() { return _initialization ??= - GoogleSignInPlatform.instance.initWithParams(const SignInInitParameters( - scopes: [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', - ], - )) + GoogleSignInPlatform.instance.init(const InitParameters()) ..catchError((dynamic _) { _initialization = null; }); @@ -57,33 +57,71 @@ class SignInDemoState extends State { void _setUser(GoogleSignInUserData? user) { setState(() { _currentUser = user; - if (user != null) { - _handleGetContact(user); - } }); + if (user != null) { + // Try getting contacts, in case authorization is already granted. + _handleGetContact(user); + } } Future _signIn() async { await _ensureInitialized(); - final GoogleSignInUserData? newUser = - await GoogleSignInPlatform.instance.signInSilently(); - _setUser(newUser); + try { + final AuthenticationResults? result = await GoogleSignInPlatform.instance + .attemptLightweightAuthentication( + const AttemptLightweightAuthenticationParameters()); + _setUser(result?.user); + } on GoogleSignInException catch (e) { + setState(() { + _errorMessage = e.code == GoogleSignInExceptionCode.canceled + ? '' + : 'GoogleSignInException ${e.code}: ${e.description}'; + }); + } } - Future> _getAuthHeaders() async { - final GoogleSignInUserData? user = _currentUser; - if (user == null) { - throw StateError('No user signed in'); + Future _handleAuthorizeScopes(GoogleSignInUserData user) async { + try { + final ClientAuthorizationTokenData? tokens = await GoogleSignInPlatform + .instance + .clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: _scopes, + userId: user.id, + email: user.email, + promptIfUnauthorized: true))); + + setState(() { + _isAuthorized = tokens != null; + _errorMessage = ''; + }); + if (_isAuthorized) { + unawaited(_handleGetContact(user)); + } + } on GoogleSignInException catch (e) { + setState(() { + _errorMessage = 'GoogleSignInException ${e.code}: ${e.description}'; + }); } + } - final GoogleSignInTokenData response = - await GoogleSignInPlatform.instance.getTokens( - email: user.email, - shouldRecoverAuth: true, - ); + Future?> _getAuthHeaders( + GoogleSignInUserData user) async { + final ClientAuthorizationTokenData? tokens = + await GoogleSignInPlatform.instance.clientAuthorizationTokensForScopes( + ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: _scopes, + userId: user.id, + email: user.email, + promptIfUnauthorized: false))); + if (tokens == null) { + return null; + } return { - 'Authorization': 'Bearer ${response.accessToken}', + 'Authorization': 'Bearer ${tokens.accessToken}', // TODO(kevmoo): Use the correct value once it's available. // See https://github.com/flutter/flutter/issues/80905 'X-Goog-AuthUser': '0', @@ -94,10 +132,17 @@ class SignInDemoState extends State { setState(() { _contactText = 'Loading contact info...'; }); + final Map? headers = await _getAuthHeaders(user); + setState(() { + _isAuthorized = headers != null; + }); + if (headers == null) { + return; + } final http.Response response = await http.get( Uri.parse('https://people.googleapis.com/v1/people/me/connections' '?requestMask.includeField=person.names'), - headers: await _getAuthHeaders(), + headers: headers, ); if (response.statusCode != 200) { setState(() { @@ -119,55 +164,63 @@ class SignInDemoState extends State { Future _handleSignIn() async { try { await _ensureInitialized(); - _setUser(await GoogleSignInPlatform.instance.signIn()); - } catch (error) { - final bool canceled = - error is PlatformException && error.code == 'sign_in_canceled'; - if (!canceled) { - print(error); - } + final AuthenticationResults result = await GoogleSignInPlatform.instance + .authenticate(const AuthenticateParameters()); + _setUser(result.user); + } on GoogleSignInException catch (e) { + setState(() { + _errorMessage = e.code == GoogleSignInExceptionCode.canceled + ? '' + : 'GoogleSignInException ${e.code}: ${e.description}'; + }); } } Future _handleSignOut() async { await _ensureInitialized(); - await GoogleSignInPlatform.instance.disconnect(); + await GoogleSignInPlatform.instance.disconnect(const DisconnectParams()); } Widget _buildBody() { final GoogleSignInUserData? user = _currentUser; - if (user != null) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + if (user != null) ...[ ListTile( title: Text(user.displayName ?? ''), subtitle: Text(user.email), ), const Text('Signed in successfully.'), - Text(_contactText), ElevatedButton( onPressed: _handleSignOut, child: const Text('SIGN OUT'), ), - ElevatedButton( - child: const Text('REFRESH'), - onPressed: () => _handleGetContact(user), - ), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + if (_isAuthorized) ...[ + // The user has Authorized all required scopes. + if (_contactText.isNotEmpty) Text(_contactText), + ElevatedButton( + child: const Text('REFRESH'), + onPressed: () => _handleGetContact(user), + ), + ] else ...[ + // The user has NOT Authorized all required scopes. + const Text('Authorization needed to read your contacts.'), + ElevatedButton( + onPressed: () => _handleAuthorizeScopes(user), + child: const Text('REQUEST PERMISSIONS'), + ), + ], + ] else ...[ const Text('You are not currently signed in.'), ElevatedButton( onPressed: _handleSignIn, child: const Text('SIGN IN'), ), ], - ); - } + if (_errorMessage.isNotEmpty) Text(_errorMessage), + ], + ); } @override From 21635a05d4984df05bb87711af67e3d1ade1fc1b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 13 May 2025 09:42:10 -0400 Subject: [PATCH 15/52] Update native unit tests, fix an Obj-C selector name --- .../darwin/Tests/GoogleSignInTests.m | 589 ++++++++---------- .../FLTGoogleSignInPlugin.m | 10 +- .../FLTGoogleSignInPlugin_Test.h | 3 + .../include/google_sign_in_ios/messages.g.h | 6 +- .../Sources/google_sign_in_ios/messages.g.m | 15 +- .../xcshareddata/swiftpm/Package.resolved | 31 +- .../google_sign_in_ios/pigeons/messages.dart | 2 +- 7 files changed, 310 insertions(+), 346 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/Tests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in_ios/darwin/Tests/GoogleSignInTests.m index 6ec7f908862..fa33c254ca6 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/Tests/GoogleSignInTests.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/Tests/GoogleSignInTests.m @@ -71,22 +71,7 @@ - (void)testDisconnect { [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testDisconnectIgnoresError { - NSError *sdkError = [NSError errorWithDomain:kGIDSignInErrorDomain - code:kGIDSignInErrorCodeHasNoAuthInKeychain - userInfo:nil]; - [(GIDSignIn *)[self.mockSignIn stub] - disconnectWithCompletion:[OCMArg invokeBlockWithArgs:sdkError, nil]]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - [self.plugin disconnectWithCompletion:^(FlutterError *error) { - XCTAssertNil(error); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; -} - -#pragma mark - Init +#pragma mark - Configure - (void)testInitNoClientIdNoError { // Init plugin without GoogleService-Info.plist. @@ -95,13 +80,12 @@ - (void)testInitNoClientIdNoError { googleServiceProperties:nil]; // init call does not provide a clientId. - FSIInitParams *params = [FSIInitParams makeWithScopes:@[] - hostedDomain:nil - clientId:nil - serverClientId:nil]; + FSIPlatformConfigurationParams *params = [FSIPlatformConfigurationParams makeWithClientId:nil + serverClientId:nil + hostedDomain:nil]; FlutterError *error; - [self.plugin initializeSignInWithParameters:params error:&error]; + [self.plugin configureWithParameters:params error:&error]; XCTAssertNil(error); } @@ -109,29 +93,25 @@ - (void)testInitGoogleServiceInfoPlist { self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn registrar:self.mockPluginRegistrar googleServiceProperties:self.googleServiceInfo]; - FSIInitParams *params = [FSIInitParams makeWithScopes:@[] - hostedDomain:@"example.com" - clientId:nil - serverClientId:nil]; + FSIPlatformConfigurationParams *params = + [FSIPlatformConfigurationParams makeWithClientId:nil + serverClientId:nil + hostedDomain:@"example.com"]; + + OCMExpect([self.mockSignIn + setConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *config) { + XCTAssertEqualObjects(config.hostedDomain, @"example.com"); + // Set in example app GoogleService-Info.plist. + XCTAssertEqualObjects( + config.clientID, + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); + XCTAssertEqualObjects(config.serverClientID, @"YOUR_SERVER_CLIENT_ID"); + return YES; + }]]); - FlutterError *initializationError; - [self.plugin initializeSignInWithParameters:params error:&initializationError]; - XCTAssertNil(initializationError); - - // Initialization values used in the next sign in request. - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error){ - }]; - OCMVerify([self configureMock:self.mockSignIn - forSignInWithHint:nil - additionalScopes:OCMOCK_ANY - completion:OCMOCK_ANY]); - - XCTAssertEqualObjects(self.plugin.configuration.hostedDomain, @"example.com"); - // Set in example app GoogleService-Info.plist. - XCTAssertEqualObjects( - self.plugin.configuration.clientID, - @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); - XCTAssertEqualObjects(self.plugin.configuration.serverClientID, @"YOUR_SERVER_CLIENT_ID"); + FlutterError *error; + [self.plugin configureWithParameters:params error:&error]; + XCTAssertNil(error); } - (void)testInitDynamicClientIdNullDomain { @@ -140,96 +120,75 @@ - (void)testInitDynamicClientIdNullDomain { registrar:self.mockPluginRegistrar googleServiceProperties:nil]; - FSIInitParams *params = [FSIInitParams makeWithScopes:@[] - hostedDomain:nil - clientId:@"mockClientId" - serverClientId:nil]; + OCMExpect( + [self.mockSignIn setConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *config) { + XCTAssertEqualObjects(config.hostedDomain, nil); + XCTAssertEqualObjects(config.clientID, @"mockClientId"); + XCTAssertEqualObjects(config.serverClientID, nil); + return YES; + }]]); + + FSIPlatformConfigurationParams *params = + [FSIPlatformConfigurationParams makeWithClientId:@"mockClientId" + serverClientId:nil + hostedDomain:nil]; FlutterError *initializationError; - [self.plugin initializeSignInWithParameters:params error:&initializationError]; + [self.plugin configureWithParameters:params error:&initializationError]; XCTAssertNil(initializationError); - // Initialization values used in the next sign in request. - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error){ - }]; - OCMVerify([self configureMock:self.mockSignIn - forSignInWithHint:nil - additionalScopes:OCMOCK_ANY - completion:OCMOCK_ANY]); - - XCTAssertEqualObjects(self.plugin.configuration.hostedDomain, nil); - XCTAssertEqualObjects(self.plugin.configuration.clientID, @"mockClientId"); - XCTAssertEqualObjects(self.plugin.configuration.serverClientID, nil); + OCMVerifyAll(self.mockSignIn); } - (void)testInitDynamicServerClientIdNullDomain { self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn registrar:self.mockPluginRegistrar googleServiceProperties:self.googleServiceInfo]; - FSIInitParams *params = [FSIInitParams makeWithScopes:@[] - hostedDomain:nil - clientId:nil - serverClientId:@"mockServerClientId"]; + FSIPlatformConfigurationParams *params = + [FSIPlatformConfigurationParams makeWithClientId:nil + serverClientId:@"mockServerClientId" + hostedDomain:nil]; + + OCMExpect([self.mockSignIn + setConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *config) { + XCTAssertEqualObjects(config.hostedDomain, nil); + // Set in example app GoogleService-Info.plist. + XCTAssertEqualObjects( + config.clientID, + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); + XCTAssertEqualObjects(config.serverClientID, @"mockServerClientId"); + return YES; + }]]); + FlutterError *initializationError; - [self.plugin initializeSignInWithParameters:params error:&initializationError]; + [self.plugin configureWithParameters:params error:&initializationError]; XCTAssertNil(initializationError); - - // Initialization values used in the next sign in request. - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error){ - }]; - OCMVerify([self configureMock:self.mockSignIn - forSignInWithHint:nil - additionalScopes:OCMOCK_ANY - completion:OCMOCK_ANY]); - - XCTAssertEqualObjects(self.plugin.configuration.hostedDomain, nil); - // Set in example app GoogleService-Info.plist. - XCTAssertEqualObjects( - self.plugin.configuration.clientID, - @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); - XCTAssertEqualObjects(self.plugin.configuration.serverClientID, @"mockServerClientId"); } - (void)testInitInfoPlist { - FSIInitParams *params = [FSIInitParams makeWithScopes:@[ @"scope1" ] - hostedDomain:@"example.com" - clientId:nil - serverClientId:nil]; + FSIPlatformConfigurationParams *params = + [FSIPlatformConfigurationParams makeWithClientId:nil + serverClientId:nil + hostedDomain:@"example.com"]; + + OCMExpect([self.mockSignIn + setConfiguration:[OCMArg checkWithBlock:^BOOL(GIDConfiguration *config) { + XCTAssertEqualObjects(config.hostedDomain, nil); + // Set in example app Info.plist. + XCTAssertEqualObjects( + config.clientID, + @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); + XCTAssertEqualObjects(config.serverClientID, @"YOUR_SERVER_CLIENT_ID"); + return YES; + }]]); FlutterError *error; self.plugin = [[FLTGoogleSignInPlugin alloc] initWithRegistrar:self.mockPluginRegistrar]; - [self.plugin initializeSignInWithParameters:params error:&error]; - XCTAssertNil(error); - XCTAssertNil(self.plugin.configuration); - XCTAssertNotNil(self.plugin.requestedScopes); - // Set in example app Info.plist. - XCTAssertEqualObjects( - self.plugin.signIn.configuration.clientID, - @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); - XCTAssertEqualObjects(self.plugin.signIn.configuration.serverClientID, @"YOUR_SERVER_CLIENT_ID"); -} - -#pragma mark - Is signed in - -- (void)testIsNotSignedIn { - OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(NO); - - FlutterError *error; - NSNumber *result = [self.plugin isSignedInWithError:&error]; - XCTAssertNil(error); - XCTAssertFalse(result.boolValue); -} - -- (void)testIsSignedIn { - OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(YES); - - FlutterError *error; - NSNumber *result = [self.plugin isSignedInWithError:&error]; + [self.plugin configureWithParameters:params error:&error]; XCTAssertNil(error); - XCTAssertTrue(result.boolValue); } -#pragma mark - Sign in silently +#pragma mark - restorePreviousSignIn - (void)testSignInSilently { id mockUser = OCMClassMock([GIDGoogleUser class]); @@ -240,20 +199,23 @@ - (void)testSignInSilently { invokeBlockWithArgs:mockUser, [NSNull null], nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin signInSilentlyWithCompletion:^(FSIUserData *user, FlutterError *error) { + [self.plugin restorePreviousSignInWithCompletion:^(FSISignInResult *result, FlutterError *error) { XCTAssertNil(error); - XCTAssertNotNil(user); + XCTAssertNil(result.error); + XCTAssertNotNil(result.success); + FSIUserData *user = result.success.user; XCTAssertNil(user.displayName); XCTAssertNil(user.email); XCTAssertEqualObjects(user.userId, @"mockID"); XCTAssertNil(user.photoUrl); - XCTAssertNil(user.serverAuthCode); + XCTAssertNil(result.success.accessToken); + XCTAssertNil(result.success.serverAuthCode); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testSignInSilentlyWithError { +- (void)testRestorePreviousSignInWithError { NSError *sdkError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeHasNoAuthInKeychain userInfo:nil]; @@ -263,15 +225,16 @@ - (void)testSignInSilentlyWithError { invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin signInSilentlyWithCompletion:^(FSIUserData *user, FlutterError *error) { - XCTAssertNil(user); - XCTAssertEqualObjects(error.code, @"sign_in_required"); + [self.plugin restorePreviousSignInWithCompletion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, FSIGoogleSignInErrorCodeNoAuthInKeychain); [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -#pragma mark - Sign in +#pragma mark - signIn - (void)testSignIn { self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:self.mockSignIn @@ -285,12 +248,17 @@ - (void)testSignIn { OCMStub([mockUserProfile imageURLWithDimension:1337]) .andReturn([NSURL URLWithString:@"https://example.com/profile.png"]); + NSString *accessToken = @"mockAccessToken"; + NSString *serverAuthCode = @"mockAuthCode"; OCMStub([mockUser profile]).andReturn(mockUserProfile); OCMStub([mockUser userID]).andReturn(@"mockID"); + id mockAccessToken = OCMClassMock([GIDToken class]); + OCMStub([mockAccessToken tokenString]).andReturn(accessToken); + OCMStub([mockUser accessToken]).andReturn(mockAccessToken); id mockSignInResult = OCMClassMock([GIDSignInResult class]); OCMStub([mockSignInResult user]).andReturn(mockUser); - OCMStub([mockSignInResult serverAuthCode]).andReturn(@"mockAuthCode"); + OCMStub([mockSignInResult serverAuthCode]).andReturn(serverAuthCode); [self configureMock:[self.mockSignIn expect] forSignInWithHint:nil @@ -298,53 +266,53 @@ - (void)testSignIn { completion:[OCMArg invokeBlockWithArgs:mockSignInResult, [NSNull null], nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(user.displayName, @"mockDisplay"); - XCTAssertEqualObjects(user.email, @"mock@example.com"); - XCTAssertEqualObjects(user.userId, @"mockID"); - XCTAssertEqualObjects(user.photoUrl, @"https://example.com/profile.png"); - XCTAssertEqualObjects(user.serverAuthCode, @"mockAuthCode"); - [expectation fulfill]; - }]; + [self.plugin signInWithScopeHint:@[] + nonce:nil + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + FSIUserData *user = result.success.user; + XCTAssertEqualObjects(user.displayName, @"mockDisplay"); + XCTAssertEqualObjects(user.email, @"mock@example.com"); + XCTAssertEqualObjects(user.userId, @"mockID"); + XCTAssertEqualObjects(user.photoUrl, @"https://example.com/profile.png"); + XCTAssertEqualObjects(result.success.accessToken, accessToken); + XCTAssertEqualObjects(result.success.serverAuthCode, serverAuthCode); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; - // Set in example app GoogleService-Info.plist. - XCTAssertEqualObjects( - self.plugin.configuration.clientID, - @"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"); - OCMVerifyAll(self.mockSignIn); } -- (void)testSignInWithInitializedScopes { +- (void)testSignInWithScopeHint { FlutterError *initializationError; - [self.plugin - initializeSignInWithParameters:[FSIInitParams makeWithScopes:@[ @"initial1", @"initial2" ] - hostedDomain:nil - clientId:nil - serverClientId:nil] - error:&initializationError]; + [self.plugin configureWithParameters:[FSIPlatformConfigurationParams makeWithClientId:nil + serverClientId:nil + hostedDomain:nil] + error:&initializationError]; id mockUser = OCMClassMock([GIDGoogleUser class]); OCMStub([mockUser userID]).andReturn(@"mockID"); id mockSignInResult = OCMClassMock([GIDSignInResult class]); OCMStub([mockSignInResult user]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"scope1", @"scope2" ]; [self configureMock:[self.mockSignIn expect] forSignInWithHint:nil additionalScopes:[OCMArg checkWithBlock:^BOOL(NSArray *scopes) { - return [[NSSet setWithArray:scopes] - isEqualToSet:[NSSet setWithObjects:@"initial1", @"initial2", nil]]; + return [[NSSet setWithArray:scopes] isEqualToSet:[NSSet setWithArray:requestedScopes]]; }] completion:[OCMArg invokeBlockWithArgs:mockSignInResult, [NSNull null], nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(user.userId, @"mockID"); - [expectation fulfill]; - }]; + [self.plugin signInWithScopeHint:requestedScopes + nonce:nil + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.error); + XCTAssertEqualObjects(result.success.user.userId, @"mockID"); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; OCMVerifyAll(self.mockSignIn); @@ -369,11 +337,14 @@ - (void)testSignInAlreadyGranted { completion:[OCMArg invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(user.userId, @"mockID"); - [expectation fulfill]; - }]; + [self.plugin signInWithScopeHint:@[] + nonce:nil + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.error); + XCTAssertEqualObjects(result.success.user.userId, @"mockID"); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } @@ -387,11 +358,16 @@ - (void)testSignInError { completion:[OCMArg invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *error) { - XCTAssertNil(user); - XCTAssertEqualObjects(error.code, @"sign_in_canceled"); - [expectation fulfill]; - }]; + [self.plugin signInWithScopeHint:@[] + nonce:nil + completion:^(FSISignInResult *result, FlutterError *error) { + // Known errors from the SDK are returned as structured data, not + // FlutterError. + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, FSIGoogleSignInErrorCodeCanceled); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } @@ -403,22 +379,27 @@ - (void)testSignInException { .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); __block FlutterError *error; - XCTAssertThrows( - [self.plugin signInWithCompletion:^(FSIUserData *user, FlutterError *signInError) { - XCTAssertNil(user); - error = signInError; - }]); + XCTAssertThrows([self.plugin + signInWithScopeHint:@[] + nonce:nil + completion:^(FSISignInResult *result, FlutterError *signInError) { + // Unexpected errors, such as runtime exceptions, are returned as FlutterError. + XCTAssertNil(result); + error = signInError; + }]); XCTAssertEqualObjects(error.code, @"google_sign_in"); XCTAssertEqualObjects(error.message, @"MockReason"); XCTAssertEqualObjects(error.details, @"MockName"); } -#pragma mark - Get tokens +#pragma mark - refreshedAuthorizationTokens -- (void)testGetTokens { - id mockUser = OCMClassMock([GIDGoogleUser class]); +- (void)testRefreshTokens { + id mockUser = [self signedInMockUser]; + NSString *userIdentifier = ((GIDGoogleUser *)mockUser).userID; id mockUserResponse = OCMClassMock([GIDGoogleUser class]); + OCMStub([mockUserResponse userID]).andReturn(userIdentifier); id mockIdToken = OCMClassMock([GIDToken class]); OCMStub([mockIdToken tokenString]).andReturn(@"mockIdToken"); @@ -431,21 +412,39 @@ - (void)testGetTokens { [[mockUser stub] refreshTokensIfNeededWithCompletion:[OCMArg invokeBlockWithArgs:mockUserResponse, [NSNull null], nil]]; - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin getAccessTokenWithCompletion:^(FSITokenData *token, FlutterError *error) { - XCTAssertNil(error); - XCTAssertEqualObjects(token.idToken, @"mockIdToken"); - XCTAssertEqualObjects(token.accessToken, @"mockAccessToken"); - [expectation fulfill]; - }]; + [self.plugin + refreshedAuthorizationTokensForUser:userIdentifier + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.error); + XCTAssertEqualObjects(result.success.user.idToken, @"mockIdToken"); + XCTAssertEqualObjects(result.success.accessToken, + @"mockAccessToken"); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testGetTokensNoAuthKeychainError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); +- (void)testRefreshTokensUnkownUser { + XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; + [self.plugin + refreshedAuthorizationTokensForUser:@"unknownUser" + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, + FSIGoogleSignInErrorCodeUserMismatch); + XCTAssertEqualObjects(result.error.message, + @"The user is no longer signed in."); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRefreshTokensNoAuthKeychainError { + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeHasNoAuthInKeychain @@ -454,18 +453,19 @@ - (void)testGetTokensNoAuthKeychainError { sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin getAccessTokenWithCompletion:^(FSITokenData *token, FlutterError *error) { - XCTAssertNil(token); - XCTAssertEqualObjects(error.code, @"sign_in_required"); - XCTAssertEqualObjects(error.message, kGIDSignInErrorDomain); - [expectation fulfill]; - }]; + [self.plugin refreshedAuthorizationTokensForUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, + FSIGoogleSignInErrorCodeNoAuthInKeychain); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testGetTokensCancelledError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); +- (void)testRefreshTokensCancelledError { + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeCanceled @@ -474,18 +474,19 @@ - (void)testGetTokensCancelledError { sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin getAccessTokenWithCompletion:^(FSITokenData *token, FlutterError *error) { - XCTAssertNil(token); - XCTAssertEqualObjects(error.code, @"sign_in_canceled"); - XCTAssertEqualObjects(error.message, kGIDSignInErrorDomain); - [expectation fulfill]; - }]; + [self.plugin refreshedAuthorizationTokensForUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, + FSIGoogleSignInErrorCodeCanceled); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testGetTokensURLError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); +- (void)testRefreshTokensURLError { + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut @@ -494,49 +495,53 @@ - (void)testGetTokensURLError { sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin getAccessTokenWithCompletion:^(FSITokenData *token, FlutterError *error) { - XCTAssertNil(token); - XCTAssertEqualObjects(error.code, @"network_error"); - XCTAssertEqualObjects(error.message, NSURLErrorDomain); - [expectation fulfill]; - }]; + [self.plugin refreshedAuthorizationTokensForUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(result.error); + XCTAssertNil(result.success); + NSString *expectedCode = [NSString + stringWithFormat:@"%@: %ld", NSURLErrorDomain, + NSURLErrorTimedOut]; + XCTAssertEqualObjects(error.code, expectedCode); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -- (void)testGetTokensUnknownError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); +- (void)testRefreshTokensUnknownError { + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; [[mockUser stub] refreshTokensIfNeededWithCompletion:[OCMArg invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin getAccessTokenWithCompletion:^(FSITokenData *token, FlutterError *error) { - XCTAssertNil(token); - XCTAssertEqualObjects(error.code, @"sign_in_failed"); - XCTAssertEqualObjects(error.message, @"BogusDomain"); - [expectation fulfill]; - }]; + [self.plugin refreshedAuthorizationTokensForUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(result.success); + XCTAssertEqualObjects(error.code, @"BogusDomain: 42"); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } -#pragma mark - Request scopes +#pragma mark - addScopes - (void)testRequestScopesResultErrorIfNotSignedIn { XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin requestScopes:@[ @"mockScope1" ] - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(success); - XCTAssertEqualObjects(error.code, @"sign_in_required"); - [expectation fulfill]; - }]; + [self.plugin addScopes:@[ @"mockScope1" ] + forUser:@"unknownUser" + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, FSIGoogleSignInErrorCodeUserMismatch); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesIfNoMissingScope { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeScopesAlreadyGranted @@ -546,18 +551,19 @@ - (void)testRequestScopesIfNoMissingScope { completion:[OCMArg invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin requestScopes:@[ @"mockScope1" ] - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(error); - XCTAssertTrue(success.boolValue); - [expectation fulfill]; - }]; + [self.plugin addScopes:@[ @"mockScope1" ] + forUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, FSIGoogleSignInErrorCodeScopesAlreadyGranted); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesResultErrorIfMismatchingUser { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:kGIDSignInErrorDomain code:kGIDSignInErrorCodeMismatchWithCurrentUser @@ -567,18 +573,19 @@ - (void)testRequestScopesResultErrorIfMismatchingUser { completion:[OCMArg invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin requestScopes:@[ @"mockScope1" ] - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(success); - XCTAssertEqualObjects(error.code, @"mismatch_user"); - [expectation fulfill]; - }]; + [self.plugin addScopes:@[ @"mockScope1" ] + forUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(error); + XCTAssertNil(result.success); + XCTAssertEqual(result.error.type, FSIGoogleSignInErrorCodeUserMismatch); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesWithUnknownError { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + id mockUser = [self signedInMockUser]; NSError *sdkError = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; [self configureMock:[mockUser stub] @@ -586,110 +593,42 @@ - (void)testRequestScopesWithUnknownError { completion:[OCMArg invokeBlockWithArgs:[NSNull null], sdkError, nil]]; XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin requestScopes:@[ @"mockScope1" ] - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(error); - XCTAssertFalse(success.boolValue); - [expectation fulfill]; - }]; + [self.plugin addScopes:@[ @"mockScope1" ] + forUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(result); + XCTAssertEqualObjects(error.code, @"BogusDomain: 42"); + [expectation fulfill]; + }]; [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testRequestScopesException { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + id mockUser = [self signedInMockUser]; OCMExpect([self configureMock:mockUser forAddScopes:@[] completion:OCMOCK_ANY]) .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); - [self.plugin requestScopes:@[] - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(success); - XCTAssertEqualObjects(error.code, @"request_scopes"); - XCTAssertEqualObjects(error.message, @"MockReason"); - XCTAssertEqualObjects(error.details, @"MockName"); - }]; -} - -- (void)testRequestScopesReturnsFalseIfOnlySubsetGranted { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1", @"mockScope2" ]; - - // Only grant one of the two requested scopes. - id mockSignInResult = OCMClassMock([GIDSignInResult class]); - OCMStub([mockUser grantedScopes]).andReturn(@[ @"mockScope1" ]); - OCMStub([mockSignInResult user]).andReturn(mockUser); - - [self configureMock:[mockUser stub] - forAddScopes:requestedScopes - completion:[OCMArg invokeBlockWithArgs:mockSignInResult, [NSNull null], nil]]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin requestScopes:requestedScopes - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(error); - XCTAssertFalse(success.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; + [self.plugin addScopes:@[] + forUser:((GIDGoogleUser *)mockUser).userID + completion:^(FSISignInResult *result, FlutterError *error) { + XCTAssertNil(result); + XCTAssertEqualObjects(error.code, @"request_scopes"); + XCTAssertEqualObjects(error.message, @"MockReason"); + XCTAssertEqualObjects(error.details, @"MockName"); + }]; } -- (void)testRequestsInitializedScopes { - id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - - FSIInitParams *params = [FSIInitParams makeWithScopes:@[ @"initial1", @"initial2" ] - hostedDomain:nil - clientId:nil - serverClientId:nil]; - FlutterError *initializationError; - [self.plugin initializeSignInWithParameters:params error:&initializationError]; - XCTAssertNil(initializationError); - - // Include one of the initially requested scopes. - NSArray *addedScopes = @[ @"initial1", @"addScope1", @"addScope2" ]; - - [self.plugin requestScopes:addedScopes - completion:^(NSNumber *success, FlutterError *error){ - }]; - - // All four scopes are requested. - [self configureMock:[mockUser verify] - forAddScopes:[OCMArg checkWithBlock:^BOOL(NSArray *scopes) { - return [[NSSet setWithArray:scopes] - isEqualToSet:[NSSet setWithObjects:@"initial1", @"initial2", @"addScope1", - @"addScope2", nil]]; - }] - completion:OCMOCK_ANY]; -} +#pragma mark - Utils -- (void)testRequestScopesReturnsTrueIfGranted { +- (id)signedInMockUser { + NSString *identifier = @"mockID"; id mockUser = OCMClassMock([GIDGoogleUser class]); - OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1", @"mockScope2" ]; - - // Grant both of the requested scopes. - id mockSignInResult = OCMClassMock([GIDSignInResult class]); - OCMStub([mockUser grantedScopes]).andReturn(requestedScopes); - OCMStub([mockSignInResult user]).andReturn(mockUser); - - [self configureMock:[mockUser stub] - forAddScopes:requestedScopes - completion:[OCMArg invokeBlockWithArgs:mockSignInResult, [NSNull null], nil]]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"completion called"]; - [self.plugin requestScopes:requestedScopes - completion:^(NSNumber *success, FlutterError *error) { - XCTAssertNil(error); - XCTAssertTrue(success.boolValue); - [expectation fulfill]; - }]; - [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMStub([mockUser userID]).andReturn(identifier); + self.plugin.usersByIdentifier[identifier] = mockUser; + return mockUser; } -#pragma mark - Utils - - (void)configureMock:(id)mock forAddScopes:(NSArray *)scopes completion:(nullable void (^)(GIDSignInResult *_Nullable signInResult, diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m index 05fd859ea83..ecbdc3366cf 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m @@ -100,9 +100,6 @@ @interface FLTGoogleSignInPlugin () // The plugin registrar, for querying views. @property(nonatomic) id registrar; -// A mapping of user IDs to GIDGoogleUser instances to use for follow-up calls. -@property(nonatomic) NSMutableDictionary *usersByIdentifier; - @end @implementation FLTGoogleSignInPlugin @@ -202,10 +199,9 @@ - (void)signInWithScopeHint:(NSArray *)scopeHint } } -- (void)getRefreshedAuthorizationTokensForUser:(NSString *)userId - completion: - (nonnull void (^)(FSISignInResult *_Nullable, - FlutterError *_Nullable))completion { +- (void)refreshedAuthorizationTokensForUser:(NSString *)userId + completion:(nonnull void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion { GIDGoogleUser *user = self.usersByIdentifier[userId]; if (user == nil) { completion( diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h index 48918e552fb..430863556d5 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/FLTGoogleSignInPlugin_Test.h @@ -19,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN // sign in, sign out, and requesting additional scopes. @property(strong, readonly) GIDSignIn *signIn; +// A mapping of user IDs to GIDGoogleUser instances to use for follow-up calls. +@property(nonatomic) NSMutableDictionary *usersByIdentifier; + /// Inject @c FlutterPluginRegistrar for testing. - (instancetype)initWithRegistrar:(NSObject *)registrar; diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h index 7bea4b5f533..3f99f2785d0 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/include/google_sign_in_ios/messages.g.h @@ -142,9 +142,9 @@ NSObject *FSIGetMessagesCodec(void); completion: (void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion; /// Requests the access token for the current sign in. -- (void)getRefreshedAuthorizationTokensForUser:(NSString *)userId - completion:(void (^)(FSISignInResult *_Nullable, - FlutterError *_Nullable))completion; +- (void)refreshedAuthorizationTokensForUser:(NSString *)userId + completion:(void (^)(FSISignInResult *_Nullable, + FlutterError *_Nullable))completion; /// Requests authorization of the given additional scopes. - (void)addScopes:(NSArray *)scopes forUser:(NSString *)userId diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m index 7aae8b9968a..522e8cd26ee 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/messages.g.m @@ -406,19 +406,18 @@ void SetUpFSIGoogleSignInApiWithSuffix(id binaryMessenge binaryMessenger:binaryMessenger codec:FSIGetMessagesCodec()]; if (api) { - NSCAssert([api respondsToSelector:@selector(getRefreshedAuthorizationTokensForUser: - completion:)], + NSCAssert([api respondsToSelector:@selector(refreshedAuthorizationTokensForUser:completion:)], @"FSIGoogleSignInApi api (%@) doesn't respond to " - @"@selector(getRefreshedAuthorizationTokensForUser:completion:)", + @"@selector(refreshedAuthorizationTokensForUser:completion:)", api); [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { NSArray *args = message; NSString *arg_userId = GetNullableObjectAtIndex(args, 0); - [api getRefreshedAuthorizationTokensForUser:arg_userId - completion:^(FSISignInResult *_Nullable output, - FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; + [api refreshedAuthorizationTokensForUser:arg_userId + completion:^(FSISignInResult *_Nullable output, + FlutterError *_Nullable error) { + callback(wrapResult(output, error)); + }]; }]; } else { [channel setMessageHandler:nil]; diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3dc8b8235fe..c80b4c1417b 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { "originHash" : "4b36b64d9a071f871b5e5ecf435f3e097843030f79026880b7d9ef1f081d563d", "pins" : [ + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", + "version" : "11.2.0" + } + }, { "identity" : "appauth-ios", "kind" : "remoteSourceControl", @@ -15,8 +24,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleSignIn-iOS.git", "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" + "revision" : "65fb3f1aa6ffbfdc79c4e22178a55cd91561f5e9", + "version" : "8.0.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", + "version" : "8.1.0" } }, { @@ -44,6 +62,15 @@ "state" : { "revision" : "fe1661a3efed11831a6452f4b1a0c5e6ddc08c3d" } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } } ], "version" : 3 diff --git a/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart index cb4161c96e6..aae37cdf728 100644 --- a/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_ios/pigeons/messages.dart @@ -141,7 +141,7 @@ abstract class GoogleSignInApi { /// Requests the access token for the current sign in. @async - @ObjCSelector('getRefreshedAuthorizationTokensForUser:') + @ObjCSelector('refreshedAuthorizationTokensForUser:') SignInResult getRefreshedAuthorizationTokens(String userId); /// Requests authorization of the given additional scopes. From 39b75055c612afbf314d7fe67899cb086a644cae Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 13 May 2025 09:47:46 -0400 Subject: [PATCH 16/52] Update to recent 1.5 release --- .../google_sign_in_android/android/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/android/build.gradle b/packages/google_sign_in/google_sign_in_android/android/build.gradle index fe930e9faf7..829160c5e0e 100644 --- a/packages/google_sign_in/google_sign_in_android/android/build.gradle +++ b/packages/google_sign_in/google_sign_in_android/android/build.gradle @@ -67,8 +67,8 @@ android { } dependencies { - implementation 'androidx.credentials:credentials:1.3.0' - implementation 'androidx.credentials:credentials-play-services-auth:1.3.0' + implementation 'androidx.credentials:credentials:1.5.0' + implementation 'androidx.credentials:credentials-play-services-auth:1.5.0' implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1' implementation 'com.google.android.gms:play-services-auth:21.3.0' testImplementation 'junit:junit:4.13.2' From 07fe54322a9133853019e3c4d1e748b0e557764b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 14 May 2025 16:23:17 -0400 Subject: [PATCH 17/52] Android native unit tests --- .../googlesignin/GoogleSignInPlugin.java | 59 +- .../googlesignin/GoogleSignInTest.java | 1066 ++++++++++++----- .../plugins/googlesignin/TestResultUtils.kt | 35 + 3 files changed, 872 insertions(+), 288 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/TestResultUtils.kt diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 36c27b294f8..53a1e48cc78 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -29,6 +29,7 @@ import androidx.credentials.exceptions.GetCredentialProviderConfigurationException; import androidx.credentials.exceptions.GetCredentialUnsupportedException; import androidx.credentials.exceptions.NoCredentialException; +import com.google.android.gms.auth.api.identity.AuthorizationClient; import com.google.android.gms.auth.api.identity.AuthorizationRequest; import com.google.android.gms.auth.api.identity.AuthorizationResult; import com.google.android.gms.auth.api.identity.Identity; @@ -60,7 +61,13 @@ public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware { @VisibleForTesting public void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) { this.messenger = messenger; - delegate = new Delegate(context); + delegate = + new Delegate( + context, + (@NonNull Context c) -> CredentialManager.create(c), + (@NonNull Context c) -> Identity.getAuthorizationClient(c), + (@Nullable Credential credential) -> + GoogleIdTokenCredential.createFrom(credential.getData())); GoogleSignInApi.Companion.setUp(messenger, delegate); } @@ -115,6 +122,28 @@ public void onDetachedFromActivity() { disposeActivity(); } + // Creates CredentialManager instances. This is provided to be overridden for tests. + @VisibleForTesting + public interface CredentialManagerFactory { + @NonNull + CredentialManager create(@NonNull Context context); + } + + // Creates AuthorizationClient instances. This is provided to be overridden for tests. + @VisibleForTesting + public interface AuthorizationClientFactory { + @NonNull + AuthorizationClient create(@NonNull Context context); + } + + // Creates GoogleIdTokenCredential instances from Credential instances. This is provided + // to be overridden for tests. + @VisibleForTesting + public interface GoogleIdCredentialConverter { + @NonNull + GoogleIdTokenCredential createFrom(@NonNull Credential credential); + } + /** * Delegate class that does the work for the Google sign-in plugin. This is exposed as a dedicated * class for use in other plugins that wrap basic sign-in functionality. @@ -125,16 +154,26 @@ public void onDetachedFromActivity() { * to guarantee such behavior; callers are responsible for providing such guarantees. */ public static class Delegate implements PluginRegistry.ActivityResultListener, GoogleSignInApi { - private static final int REQUEST_CODE_AUTHORIZE = 53294; + @VisibleForTesting static final int REQUEST_CODE_AUTHORIZE = 53294; private final @NonNull Context context; - // Only set activity for v2 embedder. Always access activity from getActivity() method. + private final @NonNull CredentialManagerFactory credentialManagerFactory; + private final @NonNull AuthorizationClientFactory authorizationClientFactory; + final @NonNull GoogleIdCredentialConverter credentialConverter; + // Always access activity from getActivity() method. private @Nullable Activity activity; private Function1, Unit> pendingAuthorizationCallback; - public Delegate(@NonNull Context context) { + public Delegate( + @NonNull Context context, + @NonNull CredentialManagerFactory credentialManagerFactory, + @NonNull AuthorizationClientFactory authorizationClientFactory, + @NonNull GoogleIdCredentialConverter credentialConverter) { this.context = context; + this.credentialManagerFactory = credentialManagerFactory; + this.authorizationClientFactory = authorizationClientFactory; + this.credentialConverter = credentialConverter; } public void setActivity(@Nullable Activity activity) { @@ -196,7 +235,7 @@ public void getCredential( requestBuilder.addCredentialOption(optionBuilder.build()); } - CredentialManager credentialManager = CredentialManager.create(context); + CredentialManager credentialManager = credentialManagerFactory.create(context); credentialManager.getCredentialAsync( context, requestBuilder.build(), @@ -211,7 +250,7 @@ public void onResult(GetCredentialResponse response) { .getType() .equals(GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL)) { GoogleIdTokenCredential googleIdTokenCredential = - GoogleIdTokenCredential.createFrom(credential.getData()); + credentialConverter.createFrom(credential); Uri profilePictureUri = googleIdTokenCredential.getProfilePictureUri(); ResultUtilsKt.completeWithGetGetCredentialResult( callback, @@ -267,7 +306,7 @@ public void onError(@NonNull GetCredentialException e) { @Override public void clearCredentialState(@NonNull Function1, Unit> callback) { - CredentialManager credentialManager = CredentialManager.create(context); + CredentialManager credentialManager = credentialManagerFactory.create(context); credentialManager.clearCredentialStateAsync( new ClearCredentialStateRequest(), null, @@ -280,7 +319,6 @@ public void onResult(Void result) { @Override public void onError(@NonNull ClearCredentialException e) { - // TODO(stuartmorgan): Consider a non-exception callback. ResultUtilsKt.completeWithClearCredentialStateError( callback, new FlutterError("Clear Failed", e.getMessage(), null)); } @@ -311,7 +349,8 @@ public void authorize( new Account(params.getAccountEmail(), "com.google")); } AuthorizationRequest authorizationRequest = authorizationRequestBuilder.build(); - Identity.getAuthorizationClient(context) + authorizationClientFactory + .create(context) .authorize(authorizationRequest) .addOnSuccessListener( authorizationResult -> { @@ -384,7 +423,7 @@ public boolean onActivityResult(int requestCode, int resultCode, @Nullable Inten if (pendingAuthorizationCallback != null) { try { AuthorizationResult authorizationResult = - Identity.getAuthorizationClient(context).getAuthorizationResultFromIntent(data); + authorizationClientFactory.create(context).getAuthorizationResultFromIntent(data); ResultUtilsKt.completeWithAuthorizationResult( pendingAuthorizationCallback, new PlatformAuthorizationResult( diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 32ade75d0cb..92868a89a36 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -4,51 +4,69 @@ package io.flutter.plugins.googlesignin; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.accounts.Account; import android.app.Activity; +import android.app.PendingIntent; import android.content.Context; -import android.content.Intent; +import android.content.IntentSender; import android.content.res.Resources; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.auth.api.signin.GoogleSignInClient; -import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import androidx.credentials.ClearCredentialStateRequest; +import androidx.credentials.Credential; +import androidx.credentials.CredentialManager; +import androidx.credentials.CredentialManagerCallback; +import androidx.credentials.CustomCredential; +import androidx.credentials.GetCredentialRequest; +import androidx.credentials.GetCredentialResponse; +import androidx.credentials.PasswordCredential; +import androidx.credentials.exceptions.ClearCredentialException; +import androidx.credentials.exceptions.GetCredentialCancellationException; +import androidx.credentials.exceptions.GetCredentialException; +import androidx.credentials.exceptions.GetCredentialInterruptedException; +import androidx.credentials.exceptions.GetCredentialProviderConfigurationException; +import androidx.credentials.exceptions.GetCredentialUnknownException; +import androidx.credentials.exceptions.GetCredentialUnsupportedException; +import androidx.credentials.exceptions.NoCredentialException; +import com.google.android.gms.auth.api.identity.AuthorizationClient; +import com.google.android.gms.auth.api.identity.AuthorizationResult; import com.google.android.gms.common.api.ApiException; -import com.google.android.gms.common.api.CommonStatusCodes; -import com.google.android.gms.common.api.Scope; import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.OnSuccessListener; import com.google.android.gms.tasks.Task; -import io.flutter.plugins.googlesignin.Messages.FlutterError; -import io.flutter.plugins.googlesignin.Messages.InitParams; -import java.util.Collections; +import com.google.android.libraries.identity.googleid.GetGoogleIdOption; +import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption; +import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.MockedConstruction; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.mockito.Spy; public class GoogleSignInTest { @Mock Context mockContext; @Mock Resources mockResources; @Mock Activity mockActivity; - @Spy Messages.VoidResult voidResult; - @Spy Messages.Result boolResult; - @Spy Messages.Result userDataResult; - @Mock GoogleSignInWrapper mockGoogleSignIn; - @Mock GoogleSignInAccount account; - @Mock GoogleSignInClient mockClient; - @Mock Task mockSignInTask; + @Mock PendingIntent mockAuthorizationIntent; + @Mock IntentSender mockAuthorizationIntentSender; + @Mock AuthorizeResult mockAuthorizeResult; + @Mock CredentialManager mockCredentialManager; + @Mock AuthorizationClient mockAuthorizationClient; + @Mock CustomCredential mockGenericCredential; + @Mock GoogleIdTokenCredential mockGoogleCredential; + @Mock Task mockAuthorizationTask; private GoogleSignInPlugin.Delegate plugin; private AutoCloseable mockCloseable; @@ -56,8 +74,21 @@ public class GoogleSignInTest { @Before public void setUp() { mockCloseable = MockitoAnnotations.openMocks(this); + + // Wire up basic mock functionality that is not test-specific. when(mockContext.getResources()).thenReturn(mockResources); - plugin = new GoogleSignInPlugin.Delegate(mockContext, mockGoogleSignIn); + when(mockGenericCredential.getType()) + .thenReturn(GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL); + when(mockAuthorizationTask.addOnSuccessListener(any())).thenReturn(mockAuthorizationTask); + when(mockAuthorizationTask.addOnFailureListener(any())).thenReturn(mockAuthorizationTask); + when(mockAuthorizationIntent.getIntentSender()).thenReturn(mockAuthorizationIntentSender); + + plugin = + new GoogleSignInPlugin.Delegate( + mockContext, + (Context c) -> mockCredentialManager, + (Context c) -> mockAuthorizationClient, + (Credential cred) -> mockGoogleCredential); } @After @@ -66,327 +97,806 @@ public void tearDown() throws Exception { } @Test - public void requestScopes_ResultErrorIfAccountIsNull() { - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - - plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); + public void getGoogleServicesJsonServerClientId_loadsServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); - ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(boolResult).error(resultCaptor.capture()); - FlutterError error = (FlutterError) resultCaptor.getValue(); - Assert.assertEquals("sign_in_required", error.code); - Assert.assertEquals("No account to grant scopes.", error.getMessage()); + final String returnedId = plugin.getGoogleServicesJsonServerClientId(); + assertEquals(serverClientId, returnedId); } @Test - public void requestScopes_ResultTrueIfAlreadyGranted() { - Scope requestedScope = new Scope("requestedScope"); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); - - plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); + public void getGoogleServicesJsonServerClientId_returnsNullIfNotFound() { + final String packageName = "fakePackageName"; + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)).thenReturn(0); - verify(boolResult).success(true); + final String returnedId = plugin.getGoogleServicesJsonServerClientId(); + assertNull(returnedId); } @Test - public void requestScopes_RequestsPermissionIfNotGranted() { - Scope requestedScope = new Scope("requestedScope"); - plugin.setActivity(mockActivity); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); - - verify(mockGoogleSignIn) - .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); + public void getCredential_returnsAuthenticationInfo() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final String displayName = "Jane User"; + final String givenName = "Jane"; + final String familyName = "User"; + final String id = "someId"; + final String idToken = "idToken"; + when(mockGoogleCredential.getDisplayName()).thenReturn(displayName); + when(mockGoogleCredential.getGivenName()).thenReturn(givenName); + when(mockGoogleCredential.getFamilyName()).thenReturn(familyName); + when(mockGoogleCredential.getId()).thenReturn(id); + when(mockGoogleCredential.getIdToken()).thenReturn(idToken); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialSuccess); + PlatformGoogleIdTokenCredential credential = + ((GetCredentialSuccess) result).getCredential(); + assertEquals(displayName, credential.getDisplayName()); + assertEquals(givenName, credential.getGivenName()); + assertEquals(familyName, credential.getFamilyName()); + assertEquals(id, credential.getId()); + assertEquals(idToken, credential.getIdToken()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onResult(new GetCredentialResponse(mockGenericCredential)); + assertTrue(callbackCalled[0]); } @Test - public void requestScopes_ReturnsFalseIfPermissionDenied() { - Scope requestedScope = new Scope("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); - plugin.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, - Activity.RESULT_CANCELED, - new Intent()); - - verify(boolResult).success(false); + public void getCredential_usesGetSignInWithGoogleOptionForButtonFlow() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(true, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + return null; + })); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(GetCredentialRequest.class); + verify(mockCredentialManager) + .getCredentialAsync(eq(mockContext), captor.capture(), any(), any(), any()); + + assertEquals(1, captor.getValue().getCredentialOptions().size()); + assertTrue( + captor.getValue().getCredentialOptions().get(0) instanceof GetSignInWithGoogleOption); } @Test - public void requestScopes_ReturnsTrueIfPermissionGranted() { - Scope requestedScope = new Scope("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); - plugin.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(boolResult).success(true); + public void getCredential_usesGetGoogleIdOptionForNonButtonFlow() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + return null; + })); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(GetCredentialRequest.class); + verify(mockCredentialManager) + .getCredentialAsync(eq(mockContext), captor.capture(), any(), any(), any()); + + assertEquals(1, captor.getValue().getCredentialOptions().size()); + assertTrue(captor.getValue().getCredentialOptions().get(0) instanceof GetGoogleIdOption); } @Test - public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { - List requestedScopes = Collections.singletonList("requestedScope"); - Scope requestedScope = new Scope("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.requestScopes(requestedScopes, boolResult); - plugin.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.requestScopes(requestedScopes, boolResult); - plugin.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(boolResult, times(2)).success(true); + public void getCredential_passesNonceInButtonFlow() { + final String nonce = "nonce"; + GetCredentialRequestParams params = + new GetCredentialRequestParams(true, false, false, "serverClientId", nonce); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + return null; + })); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(GetCredentialRequest.class); + verify(mockCredentialManager) + .getCredentialAsync(eq(mockContext), captor.capture(), any(), any(), any()); + + assertEquals(1, captor.getValue().getCredentialOptions().size()); + assertEquals( + nonce, + ((GetSignInWithGoogleOption) captor.getValue().getCredentialOptions().get(0)).getNonce()); } @Test - public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { - List requestedScopes = Collections.singletonList("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - - plugin.requestScopes(requestedScopes, boolResult); - plugin.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.requestScopes(requestedScopes, boolResult); - plugin.onActivityResult( - GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(boolResult, times(2)).error(resultCaptor.capture()); - List errors = resultCaptor.getAllValues(); - Assert.assertEquals(2, errors.size()); - FlutterError error = (FlutterError) errors.get(0); - Assert.assertEquals("sign_in_required", error.code); - Assert.assertEquals("No account to grant scopes.", error.getMessage()); - error = (FlutterError) errors.get(1); - Assert.assertEquals("sign_in_required", error.code); - Assert.assertEquals("No account to grant scopes.", error.getMessage()); + public void getCredential_passesNonceInNonButtonFlow() { + final String nonce = "nonce"; + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", nonce); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + return null; + })); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(GetCredentialRequest.class); + verify(mockCredentialManager) + .getCredentialAsync(eq(mockContext), captor.capture(), any(), any(), any()); + + assertEquals(1, captor.getValue().getCredentialOptions().size()); + assertEquals( + nonce, ((GetGoogleIdOption) captor.getValue().getCredentialOptions().get(0)).getNonce()); } - @Test(expected = IllegalStateException.class) - public void signInThrowsWithoutActivity() { - final GoogleSignInPlugin.Delegate plugin = - new GoogleSignInPlugin.Delegate(mock(Context.class), mock(GoogleSignInWrapper.class)); - - plugin.signIn(userDataResult); + @Test + public void getCredential_reportsMissingServerClientId() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, null, null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.MISSING_SERVER_CLIENT_ID, failure.getType()); + return null; + })); + assertTrue(callbackCalled[0]); } @Test - public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError() - throws ApiException { - final String clientId = "fakeClientId"; - InitParams params = buildInitParams(clientId, null); - initAndAssertServerClientId(params, clientId); - - ApiException exception = - new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text")); - when(mockClient.silentSignIn()).thenReturn(mockSignInTask); - when(mockSignInTask.isComplete()).thenReturn(true); - when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception); - - plugin.signInSilently(userDataResult); - ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Throwable.class); - verify(userDataResult).error(resultCaptor.capture()); - FlutterError error = (FlutterError) resultCaptor.getValue(); - Assert.assertEquals("sign_in_required", error.code); - Assert.assertEquals( - "com.google.android.gms.common.api.ApiException: 4: Error text", error.getMessage()); + public void getCredential_reportsWrongCredentialType() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.UNEXPECTED_CREDENTIAL_TYPE, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + // PasswordCredential is used because it's easy to create without mocking; all that matters is + // that it's not a CustomCredential of type TYPE_GOOGLE_ID_TOKEN_CREDENTIAL. + callbackCaptor + .getValue() + .onResult(new GetCredentialResponse(new PasswordCredential("wrong", "type"))); + assertTrue(callbackCalled[0]); } @Test - public void init_LoadsServerClientIdFromResources() { - final String packageName = "fakePackageName"; - final String serverClientId = "fakeServerClientId"; - final int resourceId = 1; - InitParams params = buildInitParams(null, null); - when(mockContext.getPackageName()).thenReturn(packageName); - when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) - .thenReturn(resourceId); - when(mockContext.getString(resourceId)).thenReturn(serverClientId); - initAndAssertServerClientId(params, serverClientId); + public void getCredential_reportsCancellation() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.CANCELED, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onError(new GetCredentialCancellationException()); + assertTrue(callbackCalled[0]); } @Test - public void init_InterpretsClientIdAsServerClientId() { - final String clientId = "fakeClientId"; - InitParams params = buildInitParams(clientId, null); - initAndAssertServerClientId(params, clientId); + public void getCredential_reportsInterrupted() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.INTERRUPTED, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onError(new GetCredentialInterruptedException()); + assertTrue(callbackCalled[0]); } @Test - public void init_ForwardsServerClientId() { - final String serverClientId = "fakeServerClientId"; - InitParams params = buildInitParams(null, serverClientId); - initAndAssertServerClientId(params, serverClientId); + public void getCredential_reportsProviderConfigurationIssue() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals( + GetCredentialFailureType.PROVIDER_CONFIGURATION_ISSUE, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onError(new GetCredentialProviderConfigurationException()); + assertTrue(callbackCalled[0]); } @Test - public void init_IgnoresClientIdIfServerClientIdIsProvided() { - final String clientId = "fakeClientId"; - final String serverClientId = "fakeServerClientId"; - InitParams params = buildInitParams(clientId, serverClientId); - initAndAssertServerClientId(params, serverClientId); + public void getCredential_reportsUnsupported() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.UNSUPPORTED, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onError(new GetCredentialUnsupportedException()); + assertTrue(callbackCalled[0]); } @Test - public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdParameter() { - InitParams params = buildInitParams("fakeClientId", "fakeServerClientId", false); - - initAndAssertForceCodeForRefreshToken(params, false); + public void getCredential_reportsNoCredential() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.NO_CREDENTIAL, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onError(new NoCredentialException()); + assertTrue(callbackCalled[0]); } @Test - public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdParameter() { - InitParams params = buildInitParams("fakeClientId", "fakeServerClientId", true); - - initAndAssertForceCodeForRefreshToken(params, true); + public void getCredential_reportsUnknown() { + GetCredentialRequestParams params = + new GetCredentialRequestParams(false, false, false, "serverClientId", null); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.getCredential( + params, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + GetCredentialResult result = reply.getOrNull(); + assertTrue(result instanceof GetCredentialFailure); + GetCredentialFailure failure = (GetCredentialFailure) result; + assertEquals(GetCredentialFailureType.UNKNOWN, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> + callbackCaptor = ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .getCredentialAsync( + eq(mockContext), + any(GetCredentialRequest.class), + any(), + any(), + callbackCaptor.capture()); + + callbackCaptor.getValue().onError(new GetCredentialUnknownException()); + assertTrue(callbackCalled[0]); } @Test - public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdFromResources() { - final String packageName = "fakePackageName"; - final String serverClientId = "fakeServerClientId"; - final int resourceId = 1; - InitParams params = buildInitParams(null, null, false); - when(mockContext.getPackageName()).thenReturn(packageName); - when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) - .thenReturn(resourceId); - when(mockContext.getString(resourceId)).thenReturn(serverClientId); - - initAndAssertForceCodeForRefreshToken(params, false); + public void authorize_returnsImmediateResult() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + false, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof PlatformAuthorizationResult); + PlatformAuthorizationResult auth = (PlatformAuthorizationResult) result; + assertEquals(accessToken, auth.getAccessToken()); + assertEquals(serverAuthCode, auth.getServerAuthCode()); + assertEquals(scopes, auth.getGrantedScopes()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(OnSuccessListener.class); + verify(mockAuthorizationTask).addOnSuccessListener(callbackCaptor.capture()); + + callbackCaptor + .getValue() + .onSuccess( + new AuthorizationResult(serverAuthCode, accessToken, "idToken", scopes, null, null)); + assertTrue(callbackCalled[0]); } @Test - public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdFromResources() { - final String packageName = "fakePackageName"; - final String serverClientId = "fakeServerClientId"; - final int resourceId = 1; - InitParams params = buildInitParams(null, null, true); - when(mockContext.getPackageName()).thenReturn(packageName); - when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) - .thenReturn(resourceId); - when(mockContext.getString(resourceId)).thenReturn(serverClientId); - - initAndAssertForceCodeForRefreshToken(params, true); + public void authorize_reportsImmediateException() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenThrow(new RuntimeException()); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + false, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof AuthorizeFailure); + AuthorizeFailure failure = (AuthorizeFailure) result; + assertEquals(AuthorizeFailureType.API_EXCEPTION, failure.getType()); + return null; + })); + + assertTrue(callbackCalled[0]); } @Test - public void init_PassesForceAccountName() { - String fakeAccountName = "fakeEmailAddress@example.com"; + public void authorize_reportsFailureIfUnauthorizedAndNoPromptAllowed() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + false, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof AuthorizeFailure); + AuthorizeFailure failure = (AuthorizeFailure) result; + assertEquals(AuthorizeFailureType.UNAUTHORIZED, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(OnSuccessListener.class); + verify(mockAuthorizationTask).addOnSuccessListener(callbackCaptor.capture()); + + callbackCaptor + .getValue() + .onSuccess( + new AuthorizationResult(null, null, null, scopes, null, mockAuthorizationIntent)); + assertTrue(callbackCalled[0]); + } - try (MockedConstruction mocked = - Mockito.mockConstruction( - Account.class, - (mock, context) -> { - when(mock.toString()).thenReturn(fakeAccountName); - })) { - InitParams params = buildInitParams("fakeClientId", "fakeServerClientId2", fakeAccountName); + @Test + public void authorize_reportsFailureIfUnauthorizedAndNoActivity() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + + plugin.setActivity(null); + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + true, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof AuthorizeFailure); + AuthorizeFailure failure = (AuthorizeFailure) result; + assertEquals(AuthorizeFailureType.NO_ACTIVITY, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(OnSuccessListener.class); + verify(mockAuthorizationTask).addOnSuccessListener(callbackCaptor.capture()); + + callbackCaptor + .getValue() + .onSuccess( + new AuthorizationResult(null, null, null, scopes, null, mockAuthorizationIntent)); + assertTrue(callbackCalled[0]); + } - initAndAssertForceAccountName(params, fakeAccountName); + @Test + public void authorize_returnsPostIntentResult() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + try { + when(mockAuthorizationClient.getAuthorizationResultFromIntent(any())) + .thenReturn( + new AuthorizationResult(serverAuthCode, accessToken, "idToken", scopes, null, null)); + } catch (ApiException e) { + fail(); + } - List constructed = mocked.constructed(); - Assert.assertEquals(1, constructed.size()); + plugin.setActivity(mockActivity); + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + true, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof PlatformAuthorizationResult); + PlatformAuthorizationResult auth = (PlatformAuthorizationResult) result; + assertEquals(accessToken, auth.getAccessToken()); + assertEquals(serverAuthCode, auth.getServerAuthCode()); + assertEquals(scopes, auth.getGrantedScopes()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(OnSuccessListener.class); + verify(mockAuthorizationTask).addOnSuccessListener(callbackCaptor.capture()); + callbackCaptor + .getValue() + .onSuccess( + new AuthorizationResult(null, null, null, scopes, null, mockAuthorizationIntent)); + try { + verify(mockActivity) + .startIntentSenderForResult( + mockAuthorizationIntent.getIntentSender(), + GoogleSignInPlugin.Delegate.REQUEST_CODE_AUTHORIZE, + null, + 0, + 0, + 0, + null); + } catch (IntentSender.SendIntentException e) { + fail(); } - } + // Simulate the UI flow completing. The intent data can be null here because the mock of + // mockAuthorizationClient.getAuthorizationResultFromIntent above ignores the parameter. + plugin.onActivityResult(GoogleSignInPlugin.Delegate.REQUEST_CODE_AUTHORIZE, 0, null); - public void initAndAssertServerClientId(InitParams params, String serverClientId) { - ArgumentCaptor optionsCaptor = - ArgumentCaptor.forClass(GoogleSignInOptions.class); - when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) - .thenReturn(mockClient); - plugin.init(params); - Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId()); + assertTrue(callbackCalled[0]); } - public void initAndAssertForceCodeForRefreshToken( - InitParams params, boolean forceCodeForRefreshToken) { - ArgumentCaptor optionsCaptor = - ArgumentCaptor.forClass(GoogleSignInOptions.class); - when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) - .thenReturn(mockClient); - plugin.init(params); - Assert.assertEquals( - forceCodeForRefreshToken, optionsCaptor.getValue().isForceCodeForRefreshToken()); - } + @Test + public void authorize_reportsPendingIntentException() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + try { + doThrow(new IntentSender.SendIntentException()) + .when(mockActivity) + .startIntentSenderForResult( + mockAuthorizationIntentSender, + GoogleSignInPlugin.Delegate.REQUEST_CODE_AUTHORIZE, + null, + 0, + 0, + 0, + null); + } catch (IntentSender.SendIntentException e) { + fail(); + } - public void initAndAssertForceAccountName(InitParams params, String forceAccountName) { - ArgumentCaptor optionsCaptor = - ArgumentCaptor.forClass(GoogleSignInOptions.class); - when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) - .thenReturn(mockClient); - plugin.init(params); - Assert.assertEquals(forceAccountName, optionsCaptor.getValue().getAccount().toString()); + plugin.setActivity(mockActivity); + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + true, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof AuthorizeFailure); + AuthorizeFailure failure = (AuthorizeFailure) result; + assertEquals(AuthorizeFailureType.PENDING_INTENT_EXCEPTION, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(OnSuccessListener.class); + verify(mockAuthorizationTask).addOnSuccessListener(callbackCaptor.capture()); + callbackCaptor + .getValue() + .onSuccess( + new AuthorizationResult(null, null, null, scopes, null, mockAuthorizationIntent)); + + assertTrue(callbackCalled[0]); } - private static InitParams buildInitParams(String clientId, String serverClientId) { - return buildInitParams( - Messages.SignInType.STANDARD, - Collections.emptyList(), - clientId, - serverClientId, - false, - null); - } + @Test + public void authorize_reportsPostIntentException() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + try { + when(mockAuthorizationClient.getAuthorizationResultFromIntent(any())) + .thenThrow(new ApiException(Status.RESULT_INTERNAL_ERROR)); + } catch (ApiException e) { + fail(); + } - private static InitParams buildInitParams( - String clientId, String serverClientId, boolean forceCodeForRefreshToken) { - return buildInitParams( - Messages.SignInType.STANDARD, - Collections.emptyList(), - clientId, - serverClientId, - forceCodeForRefreshToken, - null); + plugin.setActivity(mockActivity); + final Boolean[] callbackCalled = new Boolean[1]; + plugin.authorize( + params, + true, + ResultCompat.asCompatCallback( + reply -> { + callbackCalled[0] = true; + // This failure is a structured return value, not an exception. + assertTrue(reply.isSuccess()); + AuthorizeResult result = reply.getOrNull(); + assertTrue(result instanceof AuthorizeFailure); + AuthorizeFailure failure = (AuthorizeFailure) result; + assertEquals(AuthorizeFailureType.API_EXCEPTION, failure.getType()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(OnSuccessListener.class); + verify(mockAuthorizationTask).addOnSuccessListener(callbackCaptor.capture()); + callbackCaptor + .getValue() + .onSuccess( + new AuthorizationResult(null, null, null, scopes, null, mockAuthorizationIntent)); + try { + verify(mockActivity) + .startIntentSenderForResult( + mockAuthorizationIntent.getIntentSender(), + GoogleSignInPlugin.Delegate.REQUEST_CODE_AUTHORIZE, + null, + 0, + 0, + 0, + null); + } catch (IntentSender.SendIntentException e) { + fail(); + } + // Simulate the UI flow completing. The intent data can be null here because the mock of + // mockAuthorizationClient.getAuthorizationResultFromIntent above ignores the parameter. + plugin.onActivityResult(GoogleSignInPlugin.Delegate.REQUEST_CODE_AUTHORIZE, 0, null); + + assertTrue(callbackCalled[0]); } - private static InitParams buildInitParams( - String clientId, String serverClientId, String forceAccountName) { - return buildInitParams( - Messages.SignInType.STANDARD, - Collections.emptyList(), - clientId, - serverClientId, - false, - forceAccountName); + @Test + public void clearCredentialState_reportsSuccess() { + plugin.clearCredentialState( + ResultCompat.asCompatCallback( + reply -> { + assertTrue(reply.isSuccess()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .clearCredentialStateAsync( + any(ClearCredentialStateRequest.class), any(), any(), callbackCaptor.capture()); + + callbackCaptor.getValue().onResult(null); } - private static InitParams buildInitParams( - Messages.SignInType signInType, - List scopes, - String clientId, - String serverClientId, - boolean forceCodeForRefreshToken, - String forceAccountName) { - InitParams.Builder builder = new InitParams.Builder(); - builder.setSignInType(signInType); - builder.setScopes(scopes); - if (clientId != null) { - builder.setClientId(clientId); - } - if (serverClientId != null) { - builder.setServerClientId(serverClientId); - } - builder.setForceCodeForRefreshToken(forceCodeForRefreshToken); - if (forceAccountName != null) { - builder.setForceAccountName(forceAccountName); - } - return builder.build(); + @Test + public void clearCredentialState_reportsFailure() { + plugin.clearCredentialState( + ResultCompat.asCompatCallback( + reply -> { + assertTrue(reply.isFailure()); + return null; + })); + + @SuppressWarnings("unchecked") + ArgumentCaptor> callbackCaptor = + ArgumentCaptor.forClass(CredentialManagerCallback.class); + verify(mockCredentialManager) + .clearCredentialStateAsync( + any(ClearCredentialStateRequest.class), any(), any(), callbackCaptor.capture()); + + callbackCaptor.getValue().onError(mock(ClearCredentialException.class)); } } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/TestResultUtils.kt b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/TestResultUtils.kt new file mode 100644 index 00000000000..e6b8e965b97 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/TestResultUtils.kt @@ -0,0 +1,35 @@ +// 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. + +package io.flutter.plugins.googlesignin + +/** Wraps Kotlin Result for use in Java unit tests. */ +@Suppress("UNCHECKED_CAST") +class ResultCompat(private val result: Result) { + private val value: T? = result.getOrNull() + private val exception = result.exceptionOrNull() + val isSuccess = result.isSuccess + val isFailure = result.isFailure + + companion object { + @JvmStatic + fun success(value: T, callback: Any) { + val castedCallback: (Result) -> Unit = callback as (Result) -> Unit + castedCallback(Result.success(value)) + } + + @JvmStatic + fun asCompatCallback(result: (ResultCompat) -> Unit): (Result) -> Unit { + return { result(ResultCompat(it)) } + } + } + + fun getOrNull(): T? { + return value + } + + fun exceptionOrNull(): Throwable? { + return exception + } +} From e6c9f62fe8190ab386c666eba9df19a52b979a36 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 16 May 2025 09:22:28 -0400 Subject: [PATCH 18/52] README, CHANGELOG, and version updates --- .../google_sign_in/CHANGELOG.md | 13 +- .../google_sign_in/MIGRATION.md | 31 +-- .../google_sign_in/google_sign_in/README.md | 63 +++---- .../google_sign_in/pubspec.yaml | 2 +- .../google_sign_in_android/CHANGELOG.md | 7 + .../google_sign_in_android/README.md | 4 +- .../google_sign_in_android/pubspec.yaml | 2 +- .../google_sign_in_ios/CHANGELOG.md | 5 + .../google_sign_in_ios/README.md | 47 +++-- .../google_sign_in_ios/pubspec.yaml | 2 +- .../CHANGELOG.md | 7 + .../pubspec.yaml | 2 +- .../google_sign_in_web/CHANGELOG.md | 5 + .../google_sign_in_web/README.md | 177 +++--------------- .../google_sign_in_web/pubspec.yaml | 2 +- 15 files changed, 134 insertions(+), 235 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 124b689e716..fb703941712 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,6 +1,13 @@ -## NEXT - -* Updates README to indicate that Andoid SDK <21 is no longer supported. +## 7.0.0 + +* **BREAKING CHANGE**: Many APIs have changed or been replaced to reflect the + current APIs and best practices of the underlying platform SDKs. For full + details, see the README and migration guide, but notable highlights include: + * The `GoogleSignIn` instance is now a singleton. + * Clients must call and await the new `initialize` method before calling any + other methods on the instance. + * Authentication and authorization are now separate steps. + * Access tokens and server auth codes are obtained via separate calls. ## 6.3.0 diff --git a/packages/google_sign_in/google_sign_in/MIGRATION.md b/packages/google_sign_in/google_sign_in/MIGRATION.md index 31eab83b3c9..1369767d102 100644 --- a/packages/google_sign_in/google_sign_in/MIGRATION.md +++ b/packages/google_sign_in/google_sign_in/MIGRATION.md @@ -5,13 +5,13 @@ SDK, which has been deprecated on both Android and Web, and replaced with new SDKs that have significantly different structures. As a result, the `google_sign_in` API surface has changed significantly. Notable differences include: -* There is now an explicit `initialize` step that must be called excatly once, +* `GoogleSignIn` is now a singleton, which is obtained via + `GoogleSignIn.instance`. In practice, creating multiple `GoogleSignIn` + instances in 6.x would not work correctly, so this just enforces an existing + restriction. +* There is now an explicit `initialize` step that must be called exactly once, before any other methods. On some platforms the future will complete almost immediately, but on others (for example, web) it may take some time. - * `GoogleSignIn` is also now a singleton, which is obtained via - `GoogleSignIn.instance`. In practice, creating multiple `GoogleSignIn` - instances in 6.x would not work correctly, so this just enforces an existing - restriction. * The plugin no longer tracks a single "current" signed in user. Instead, applications that assume a single signed in user should track this at the application level using the `authenticationEvents` stream. @@ -22,9 +22,9 @@ include: used. * In applications where these steps should happen at the same time, you can pass a `scopeHint` during the authentication step. On platforms that support - it this allows for a combined authentication and authorization UI flow, but - not all platforms allow combining them, so your application should be - prepared to trigger a separate authorization flow if necessary. + it this allows for a combined authentication and authorization UI flow. + Not all platforms allow combining these flows, so your application should be + prepared to trigger a separate authorization prompt if necessary. * Authorization is further separated into client and server authorization. Applications that need a `serverAuthCode` must now call a separate method, `authorizeServer`, to obtain that code. @@ -44,13 +44,14 @@ include: 7.0, on web this may show a floating sign-in card, and on Android it may show an account selection sheet. * This new method is no longer guaranteed to return a future. This allows - clients to distinguish, at runtime, platforms where a definitive "signed in" - or "not signed in" response can be returned quickly, and thus `await`-ing - completion is reasonable, in which case a `Future` is returned, and those - (such as web) where it could take an arbitrary amount of time, in which case - no `Future` is returned, and clients should assume a non-signed-in state - until/unless a sign-in event is eventually posted to the - `authenticationEvents` stream. + clients to distinguish, at runtime: + * platforms where a definitive "signed in" or "not signed in" response + can be returned quickly, and thus `await`-ing completion is reasonable, + in which case a `Future` is returned, and + * platforms (such as web) where it could take an arbitrary amount of time, + in which case no `Future` is returned, and clients should assume a + non-signed-in state until/unless a sign-in event is eventually posted to + the `authenticationEvents` stream. * `authenticate` replaces the authentication portion of `signIn` on platforms that support it (see below). * The new `supportsAuthenticate` method allows clients to determine at runtime diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 64e84255e50..bde0918557e 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -14,43 +14,12 @@ To use this plugin, follow the [plugin installation instructions](https://pub.dev/packages/google_sign_in/install), then follow the platform integration steps below for all platforms you support. -### Android integration +### Platform integration -Please see [instructions on integrating Google Sign-In on Android](https://pub.dev/packages/google_sign_in_android#integration). - -### iOS integration - -Please see [instructions on integrating Google Sign-In for iOS](https://pub.dev/packages/google_sign_in_ios#ios-integration). - -#### iOS additional requirement - -Note that according to -https://developer.apple.com/sign-in-with-apple/get-started, starting June 30, -2020, apps that use login services must also offer a "Sign in with Apple" option -when submitting to the Apple App Store. - -Consider also using an Apple sign in plugin from pub.dev. - -The Flutter Favorite -[sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) plugin could -be an option. - -### macOS integration - -Please see [instructions on integrating Google Sign-In for macOS](https://pub.dev/packages/google_sign_in_ios#macos-setup). - -### Web integration - -The new SDK used by the web has fully separated Authentication from Authorization, -so `signIn` and `signInSilently` no longer authorize OAuth `scopes`. - -Flutter apps must be able to detect what scopes have been granted by their users, -and if the grants are still valid. - -Read below about **Working with scopes, and incremental authorization** for -general information about changes that may be needed on an app, and for more -specific web integration details, see the -[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). +* **Android**: Please see [the `google_sign_in_android` README](https://pub.dev/packages/google_sign_in_android#integration). +* **iOS**: Please see [the `google_sign_in_ios` README](https://pub.dev/packages/google_sign_in_ios#ios-integration). +* **macOS**: Please see [the `google_sign_in_ios` README](https://pub.dev/packages/google_sign_in_ios#macos-integration) (which also supports macOS). +* **Web**: Please see [the `google_sign_in_web` README](https://pub.dev/packages/google_sign_in_web#integration). ## Usage @@ -75,7 +44,11 @@ unawaited(signIn ``` If the user isn't signed in by the lightweight method, you can show UI to -start a sign-in flow: +start a sign-in flow. This uses `authenticate` on platforms that return true +for `supportsAuthenticate`, otherwise applications should fall back to a +platform-specific approach. For instance, user-initiated sign in on web must +use a button rendered by the sign in SDK, rather than application-provided +UI: ```dart @@ -101,7 +74,7 @@ else ...[ ### Checking if scopes have been granted -If the user has previously authorized the scopes required by you application, +If the user has previously authorized the scopes required by your application, you can silently request an access token for those scopes: @@ -141,7 +114,7 @@ seconds (one hour), so your app needs to be able to handle failed REST requests, and update its UI to prompt the user for a new Authorization round. This can be done by combining the error responses from your REST requests with -the `canAccessScopes` and `requestScopes` methods described above. +the authorization methods described above. For more details, take a look at the [`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). @@ -157,13 +130,21 @@ final GoogleSignInServerAuthorization? serverAuth = await user.authorizationClient.authorizeServer(scopes); ``` -Server auth codes are not always available on all platforms. In general, if you +Server auth codes are not always available on all platforms. For instance, on +some platforms they may only be returned when a user initially signs in, and +not for subsequent authentications via the lightweight process. If you need a server auth code you should request it as soon as possible after initial sign-in, and manage server tokens for that user entirely on the server side -unless the user signs in as a different user. +unless the signed in user changes. ## Example The [Google Sign-In example application](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart) demonstrates one approach to using this package to sign a user in and authorize access to specific user data. + +## Migration from pre-7.0 versions + +If you used version 6.x or earlier of `google_sign_in`, see +[the migration guide](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/MIGRATION.md) +for more information about the changes. diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 3e5d7b652d2..c939e5c0ba8 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.3.0 +version: 7.0.0 environment: sdk: ^3.6.0 diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index 3d66c7b33fc..5cc5b67e718 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,10 @@ +## 7.0.0 + +* **BREAKING CHANGE**: Switches to implementing version 3.0 of the platform + interface package, rather than 2.x, significantly changing the API surface. +* Switches to Sign in with Google (`CredentialManager`) as the underlying + SDK, removing usage of the deprecated Google Sign In for Android SDK. + ## 6.2.1 * Removes obsolete code related to supporting SDK <21. diff --git a/packages/google_sign_in/google_sign_in_android/README.md b/packages/google_sign_in/google_sign_in_android/README.md index 82f4a936ba3..bd91f042682 100644 --- a/packages/google_sign_in/google_sign_in_android/README.md +++ b/packages/google_sign_in/google_sign_in_android/README.md @@ -18,7 +18,7 @@ should add it to your `pubspec.yaml` as usual. To use Google Sign-In, you'll need to [register your application](https://firebase.google.com/docs/android/setup). -If you are using Google Cloud Platform directl, rather than Firebase, you will +If you are using Google Cloud Platform directly, rather than Firebase, you will need to register both an Android application and a web application in the [Google Cloud Platform API manager](https://console.developers.google.com/). @@ -35,6 +35,6 @@ Otherwise, you may encounter `APIException` errors. You will also need to enable any OAuth APIs that you want, using the [Google Cloud Platform API manager](https://console.developers.google.com/). For -example, if you want to mimic the behavior of the Google Sign-In sample app, +example, if you want to mimic the behavior of the Google Sign-In example app, you'll need to enable the [Google People API](https://developers.google.com/people/). diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index f4444cf82a3..cfe6520b3dd 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.2.1 +version: 7.0.0 environment: sdk: ^3.6.0 diff --git a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md index ddcbd4044e0..0bd1341f537 100644 --- a/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 6.0.0 + +* **BREAKING CHANGE**: Switches to implementing version 3.0 of the platform + interface package, rather than 2.x, significantly changing the API surface. + ## 5.9.0 * Updates Google Sign-In SDK to 8.0+. diff --git a/packages/google_sign_in/google_sign_in_ios/README.md b/packages/google_sign_in/google_sign_in_ios/README.md index 3fe0f7a1307..e3027fac31d 100644 --- a/packages/google_sign_in/google_sign_in_ios/README.md +++ b/packages/google_sign_in/google_sign_in_ios/README.md @@ -11,24 +11,6 @@ so you do not need to add it to your `pubspec.yaml`. However, if you `import` this package to use any of its APIs directly, you should add it to your `pubspec.yaml` as usual. -### macOS setup - -The GoogleSignIn SDK requires keychain sharing to be enabled, by [adding the -following entitlements](https://flutter.dev/to/macos-entitlements): - -```xml - keychain-access-groups - - $(AppIdentifierPrefix)com.google.GIDSignIn - -``` - -Without this step, the plugin will throw a `keychain error` `PlatformException` -when trying to sign in. - -[1]: https://pub.dev/packages/google_sign_in -[2]: https://flutter.dev/to/endorsed-federated-plugin - ### iOS integration 1. [Create a Firebase project](https://firebase.google.com/docs/ios/setup#create-firebase-project) @@ -87,3 +69,32 @@ final GoogleSignIn googleSignIn = GoogleSignIn( ``` Note that step 6 is still required. + +#### App Store requirements + +Apple's App Review Guidelines impose +[extra login option requirements](https://developer.apple.com/app-store/review/guidelines/#login-services) +on apps that include Google Sign-In. Other packages, such as the Flutter Favorite +[`sign_in_with_apple`](https://pub.dev/packages/sign_in_with_apple), may +be useful in satisfying the review requirements. + +### macOS integration + +Follow the steps above for iOS integration, but using the `Info.plist` in the +`macos` directory. + +In addition, the GoogleSignIn SDK requires keychain sharing to be enabled, by +[adding the following entitlements](https://flutter.dev/to/macos-entitlements): + +```xml + keychain-access-groups + + $(AppIdentifierPrefix)com.google.GIDSignIn + +``` + +Without this step, the plugin will throw a `keychain error` `PlatformException` +when trying to sign in. + +[1]: https://pub.dev/packages/google_sign_in +[2]: https://flutter.dev/to/endorsed-federated-plugin diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index f73ec169ac2..337842d1bdb 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_ios description: iOS implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 5.9.0 +version: 6.0.0 environment: sdk: ^3.4.0 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 d12f577b7d3..68c44da1f25 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,3 +1,10 @@ +## 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. + ## 2.5.0 * Adds a sign-in field to allow Android clients to explicitly specify an account name. 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 d0156993eb3..b8fe4db6836 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.4.0 diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index 0dd0d245abf..337d93b8136 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.0.0 + +* **BREAKING CHANGE**: Switches to implementing version 3.0 of the platform + interface package, rather than 2.x, significantly changing the API surface. + ## 0.12.4+4 * Asserts that new `forceAccountName` parameter is null (not used in web). diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 3aeb3565095..f9222b53b45 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -49,168 +49,43 @@ You can tell `flutter run` to listen for requests in a specific host and port wi flutter run -d chrome --web-hostname localhost --web-port 7357 ``` -## Migrating to v0.11 and v0.12 (Google Identity Services) +## Authentication -The `google_sign_in_web` plugin is backed by the new Google Identity Services -(GIS) JS SDK since version 0.11.0. +This implementation returns false for `supportsAuthentication`, and will throw +if `authenticate` is called. This is because the +[Google Identity Services (GIS) SDK](https://developers.google.com/identity/gsi/web/guides/overview) +only allows signing in using UI provided by the SDK. -The GIS SDK is used both for [Authentication](https://developers.google.com/identity/gsi/web/guides/overview) -and [Authorization](https://developers.google.com/identity/oauth2/web/guides/overview) flows. +On the web, instead of providing custom UI that calls `authenticate`, you should +display the Widget returned by `renderButton` (from `web_only.dart`), and listen +to `authenticationEvents` to know when the user has signed in. -The GIS SDK, however, doesn't behave exactly like the one being deprecated. -Some concepts have experienced pretty drastic changes, and that's why this -plugin required a major version update. +### Migration from versions before 0.12 -### Differences between Google Identity Services SDK and Google Sign-In for Web SDK. - -The **Google Sign-In JavaScript for Web JS SDK** is set to be deprecated after -March 31, 2023. **Google Identity Services (GIS) SDK** is the new solution to -quickly and easily sign users into your app using their Google accounts. - -* In the GIS SDK, Authentication and Authorization are now two separate concerns. - * Authentication (information about the current user) flows will not - authorize `scopes` anymore. - * Authorization (permissions for the app to access certain user information) - flows will not return authentication information. -* The GIS SDK no longer has direct access to previously-seen users upon initialization. - * `signInSilently` now displays the One Tap UX for web. -* **Since 0.12** The plugin provides an `idToken` (JWT-encoded info) when the - user successfully completes an authentication flow: - * In the plugin: `signInSilently` and through the web-only `renderButton` widget. -* The plugin `signIn` method uses the OAuth "Implicit Flow" to Authorize the requested `scopes`. - * This method only provides an `accessToken`, and not an `idToken`, so if your - app needs an `idToken`, this method **should be avoided on the web**. -* The GIS SDK no longer handles sign-in state and user sessions, it only provides - Authentication credentials for the moment the user did authenticate. -* The GIS SDK no longer is able to renew Authorization sessions on the web. - Once the token expires, API requests will begin to fail with unauthorized, - and user Authorization is required again. - -See more differences in the following migration guides: - -* Authentication > [Migrating from Google Sign-In](https://developers.google.com/identity/gsi/web/guides/migration) -* Authorization > [Migrate to Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/migration-to-gis) - -### New use cases to take into account in your app - -#### Authentication != Authorization - -In the GIS SDK, the concepts of Authentication and Authorization have been separated. - -It is possible now to have an Authenticated user that hasn't Authorized any `scopes`. - -Flutter apps that need to run in the web must now handle the fact that an Authenticated -user may not have permissions to access the `scopes` it requires to function. - -The Google Sign In plugin has a new `canAccessScopes` method that can be used to -check if a user is Authorized or not. - -It is also possible that Authorizations expire while users are using an app -(after 3600 seconds), so apps should monitor response failures from the APIs, and -prompt users (interactively) to grant permissions again. - -Check the "Integration considerations > [UX separation for authentication and authorization](https://developers.google.com/identity/gsi/web/guides/integrate#ux_separation_for_authentication_and_authorization) -guide" in the official GIS SDK documentation for more information about this. - -_(See also the [package:google_sign_in example app](https://pub.dev/packages/google_sign_in/example) -for a simple implementation of this (look at the `isAuthorized` variable).)_ - -#### Is this separation *always required*? - -Only if the scopes required by an app are different from the -[OpenID Connect scopes](https://developers.google.com/identity/protocols/oauth2/scopes#openid-connect). - -If an app only needs an `idToken`, or the OpenID Connect scopes, the Authentication -bits of the plugin should be enough for your app (`signInSilently` and `renderButton`). - -### What happened to the `signIn` method on the web? - -Because the GIS SDK for web no longer provides users with the ability to create -their own Sign-In buttons, or an API to start the sign in flow, the current -implementation of `signIn` (that does authorization and authentication) is no -longer feasible on the web. - -The web plugin attempts to simulate the old `signIn` behavior by using the -[OAuth Implicit pop-up flow](https://developers.google.com/identity/oauth2/web/guides/use-token-model), -which authenticates and authorizes users. - -The drawback of this approach is that the OAuth flow **only returns an `accessToken`**, -and a synthetic version of the User Data, that does **not include an `idToken`**. - -The solution to this is to **migrate your custom "Sign In" buttons in the web to -the Button Widget provided by this package: `Widget renderButton()`.** - -_(Check the [package:google_sign_in example app](https://pub.dev/packages/google_sign_in/example) -for an example on how to mix the `renderButton` widget on the web, with a custom -button for the mobile.)_ - -#### Enable access to the People API for your GCP project - -If you want to use the `signIn` method on the web, the plugin will do an additional -request to the PeopleAPI to retrieve the logged-in user information (minus the `idToken`). - -For this to work, you must enable access to the People API on your Client ID in -the GCP console. - -This is **not recommended**. Ideally, your web application should use a mix of -`signInSilently` and the Google Sign In web `renderButton` to authenticate your -users, and then `canAccessScopes` and `requestScopes` to authorize the `scopes` -that are needed. - -#### Why is the `idToken` missing after `signIn`? - -The `idToken` is cryptographically signed by Google Identity Services, and -this plugin can't spoof that signature. - -#### User Sessions +See [Migrating from Google Sign-In](https://developers.google.com/identity/gsi/web/guides/migration) +for information about the differences between authentication in the GIS SDK and +the SDK used in older versions of this plugin. Since the GIS SDK does _not_ manage user sessions anymore, apps that relied on -this feature might break. - -If long-lived sessions are required, consider using some user authentication -system that supports Google Sign In as a federated Authentication provider, -like [Firebase Auth](https://firebase.google.com/docs/auth/flutter/federated-auth#google), -or similar. - -#### Expired / Invalid Authorization Tokens +this feature might break. If long-lived sessions are required, consider using +some user authentication system that supports Google Sign In as a federated +Authentication provider, like +[Firebase Auth](https://firebase.google.com/docs/auth/flutter/federated-auth#google). -Since the GIS SDK does _not_ auto-renew authorization tokens anymore, it's now -the responsibility of your app to do so. +## Authorization -Apps now need to monitor the status code of their REST API requests for response -codes different to `200`. For example: +The GIS SDK does not renew authorization sessions. Once the token expires +(after 3600 seconds), API requests will begin to fail, and you must re-request +user authorization. For example: * `401`: Missing or invalid access token. * `403`: Expired access token. -In either case, your app needs to prompt the end user to `requestScopes`, to -**interactively** renew the token. - -The GIS SDK limits authorization token duration to one hour (3600 seconds). - -### Other APIs - -Read the rest of the instructions if you need to add extra APIs (like Google People API). - -## Example - -Find the example wiring in the [Google sign-in example application](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/example/lib/main.dart). - -## API details - -See [google_sign_in.dart](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart) for more API details. - -## Contributions and Testing - -Tests are crucial for contributions to this package. All new contributions should be reasonably tested. - -**Check the [`test/README.md` file](https://github.com/flutter/packages/blob/main/packages/google_sign_in/google_sign_in_web/test/README.md)** for more information on how to run tests on this package. - -Contributions to this package are welcome. Read the [Contributing to Flutter Plugins](https://github.com/flutter/packages/blob/main/CONTRIBUTING.md) guide to get started. - -## Issues and feedback +See the "Integration considerations > [UX separation for authentication and authorization](https://developers.google.com/identity/gsi/web/guides/integrate#ux_separation_for_authentication_and_authorization) +guide" in the official GIS SDK documentation for more information about this. -Please file [issues](https://github.com/flutter/flutter/issues/new) -to send feedback or report a bug. +### Migration from versions before 0.12 -**Thank you!** +See [Migrate to Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/migration-to-gis) +for information about the differences between authentication in the GIS SDK and +the SDK used in older versions of this plugin. 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 bba9aa07f15..830b947fb0d 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -3,7 +3,7 @@ description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_web issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 0.12.4+4 +version: 1.0.0 environment: sdk: ^3.4.0 From 90a63aa292f375b1b3ac4d45b7e8b082af166a96 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 16 May 2025 13:35:18 -0400 Subject: [PATCH 19/52] App-facing Dart tests --- .../integration_test/google_sign_in_test.dart | 4 +- .../google_sign_in/pubspec.yaml | 1 + .../test/google_sign_in_test.dart | 732 ++++++++---------- .../test/google_sign_in_test.mocks.dart | 179 ++--- .../google_sign_in/test/widgets_test.dart | 3 - .../lib/src/types.dart | 2 +- 6 files changed, 382 insertions(+), 539 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart index 54e454c28f4..303d17d3d2c 100644 --- a/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart @@ -10,7 +10,9 @@ void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('Can initialize the plugin', (WidgetTester tester) async { - final GoogleSignIn signIn = GoogleSignIn(); + final GoogleSignIn signIn = GoogleSignIn.instance; expect(signIn, isNotNull); + + await signIn.initialize(); }); } diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index c939e5c0ba8..a24eded31d3 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -35,6 +35,7 @@ dev_dependencies: sdk: flutter http: ">=0.13.0 <2.0.0" mockito: ^5.4.4 + plugin_platform_interface: ^2.1.8 topics: - authentication diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 0853691f6ba..aff84193b94 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -9,6 +9,7 @@ import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'google_sign_in_test.mocks.dart'; @@ -16,486 +17,411 @@ import 'google_sign_in_test.mocks.dart'; // ignore: avoid_implementing_value_types, must_be_immutable, unreachable_from_main class MockGoogleSignInAccount extends Mock implements GoogleSignInAccount {} +// Add the mixin to make the platform interface accept the mock. +class TestMockGoogleSignInPlatform extends MockGoogleSignInPlatform + with MockPlatformInterfaceMixin {} + @GenerateMocks([GoogleSignInPlatform]) void main() { - late MockGoogleSignInPlatform mockPlatform; - - group('GoogleSignIn', () { - final GoogleSignInUserData kDefaultUser = GoogleSignInUserData( - email: 'john.doe@gmail.com', - id: '8162538176523816253123', - photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', - displayName: 'John Doe', - serverAuthCode: '789'); - - setUp(() { - mockPlatform = MockGoogleSignInPlatform(); - when(mockPlatform.isMock).thenReturn(true); - when(mockPlatform.userDataEvents).thenReturn(null); - when(mockPlatform.signInSilently()) - .thenAnswer((Invocation _) async => kDefaultUser); - when(mockPlatform.signIn()) - .thenAnswer((Invocation _) async => kDefaultUser); - - GoogleSignInPlatform.instance = mockPlatform; - }); + const GoogleSignInUserData defaultUser = GoogleSignInUserData( + email: 'john.doe@gmail.com', + id: '8162538176523816253123', + photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', + displayName: 'John Doe'); - test('signInSilently', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); + late MockGoogleSignInPlatform mockPlatform; - await googleSignIn.signInSilently(); + setUp(() { + mockPlatform = TestMockGoogleSignInPlatform(); + when(mockPlatform.authenticationEvents).thenReturn(null); - expect(googleSignIn.currentUser, isNotNull); - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()); - }); + GoogleSignInPlatform.instance = mockPlatform; + }); - test('signIn', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); + group('initialize', () { + test('passes nulls by default', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - await googleSignIn.signIn(); + await googleSignIn.initialize(); - expect(googleSignIn.currentUser, isNotNull); - _verifyInit(mockPlatform); - verify(mockPlatform.signIn()); + final VerificationResult verification = + verify(mockPlatform.init(captureAny)); + final InitParameters params = verification.captured[0] as InitParameters; + expect(params.clientId, null); + expect(params.serverClientId, null); + expect(params.nonce, null); + expect(params.hostedDomain, null); }); - test('clientId parameter is forwarded to implementation', () async { - const String fakeClientId = 'fakeClientId'; - final GoogleSignIn googleSignIn = GoogleSignIn(clientId: fakeClientId); - - await googleSignIn.signIn(); - - _verifyInit(mockPlatform, clientId: fakeClientId); - verify(mockPlatform.signIn()); + test('passes all paramaters', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const String clientId = 'clientId'; + const String serverClientId = 'serverClientId'; + const String nonce = 'nonce'; + const String hostedDomain = 'example.com'; + await googleSignIn.initialize( + clientId: clientId, + serverClientId: serverClientId, + nonce: nonce, + hostedDomain: hostedDomain); + + final VerificationResult verification = + verify(mockPlatform.init(captureAny)); + final InitParameters params = verification.captured[0] as InitParameters; + expect(params.clientId, clientId); + expect(params.serverClientId, serverClientId); + expect(params.nonce, nonce); + expect(params.hostedDomain, hostedDomain); }); + }); - test('serverClientId parameter is forwarded to implementation', () async { - const String fakeServerClientId = 'fakeServerClientId'; - final GoogleSignIn googleSignIn = - GoogleSignIn(serverClientId: fakeServerClientId); - - await googleSignIn.signIn(); - - _verifyInit(mockPlatform, serverClientId: fakeServerClientId); - verify(mockPlatform.signIn()); + group('authenticationEvents', () { + test('reports success from attemptLightweightAuthentication', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const String idToken = 'idToken'; + when(mockPlatform.attemptLightweightAuthentication(any)).thenAnswer( + (_) async => const AuthenticationResults( + user: defaultUser, + authenticationTokens: AuthenticationTokenData(idToken: idToken))); + + final Future eventFuture = + googleSignIn.authenticationEvents.first; + await googleSignIn.initialize(); + await googleSignIn.attemptLightweightAuthentication(); + final GoogleSignInAuthenticationEvent event = await eventFuture; + + expect(event, isA()); + final GoogleSignInAuthenticationEventSignIn signIn = + event as GoogleSignInAuthenticationEventSignIn; + expect(signIn.user.id, defaultUser.id); + expect(signIn.user.authentication.idToken, idToken); }); - test( - 'clientId and serverClientId parameters is forwarded to implementation', - () async { - // #docregion GoogleSignIn - final GoogleSignIn googleSignIn = GoogleSignIn( - // The OAuth client id of your app. This is required. - clientId: 'Your Client ID', - // If you need to authenticate to a backend server, specify its OAuth client. This is optional. - serverClientId: 'Your Server ID', - ); - // #enddocregion GoogleSignIn - - await googleSignIn.signIn(); - - _verifyInit( - mockPlatform, - clientId: 'Your Client ID', - serverClientId: 'Your Server ID', - ); - verify(mockPlatform.signIn()); + test('reports exceptions from attemptLightweightAuthentication', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenThrow(exception); + + final Future eventFuture = + googleSignIn.authenticationEvents.first; + await googleSignIn.initialize(); + // This doesn't throw, since reportAllExceptions is false. + await googleSignIn.attemptLightweightAuthentication(); + final GoogleSignInAuthenticationEvent event = await eventFuture; + + expect(event, isA()); + final GoogleSignInAuthenticationEventException exceptionEvent = + event as GoogleSignInAuthenticationEventException; + expect(exceptionEvent.exception, exception); }); - test('forceCodeForRefreshToken sent with init method call', () async { - final GoogleSignIn googleSignIn = - GoogleSignIn(forceCodeForRefreshToken: true); - - await googleSignIn.signIn(); - - _verifyInit(mockPlatform, forceCodeForRefreshToken: true); - verify(mockPlatform.signIn()); + test('reports success from authenticate', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const String idToken = 'idToken'; + when(mockPlatform.authenticate(any)).thenAnswer((_) async => + const AuthenticationResults( + user: defaultUser, + authenticationTokens: AuthenticationTokenData(idToken: idToken))); + + final Future eventFuture = + googleSignIn.authenticationEvents.first; + await googleSignIn.initialize(); + await googleSignIn.authenticate(); + final GoogleSignInAuthenticationEvent event = await eventFuture; + + expect(event, isA()); + final GoogleSignInAuthenticationEventSignIn signIn = + event as GoogleSignInAuthenticationEventSignIn; + expect(signIn.user.id, defaultUser.id); + expect(signIn.user.authentication.idToken, idToken); }); - test('forceAccountName sent with init method call', () async { - final GoogleSignIn googleSignIn = - GoogleSignIn(forceAccountName: 'fakeEmailAddress@example.com'); + test('reports exceptions from authenticate', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); + when(mockPlatform.authenticate(any)).thenThrow(exception); - await googleSignIn.signIn(); + final Future eventFuture = + googleSignIn.authenticationEvents.first; + await googleSignIn.initialize(); + await expectLater( + googleSignIn.authenticate(), throwsA(isA())); + final GoogleSignInAuthenticationEvent event = await eventFuture; - _verifyInit(mockPlatform, - forceAccountName: 'fakeEmailAddress@example.com'); - verify(mockPlatform.signIn()); + expect(event, isA()); + final GoogleSignInAuthenticationEventException exceptionEvent = + event as GoogleSignInAuthenticationEventException; + expect(exceptionEvent.exception, exception); }); - test('signOut', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); + test('reports sign out from signOut', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + final Future eventFuture = + googleSignIn.authenticationEvents.first; + await googleSignIn.initialize(); await googleSignIn.signOut(); + final GoogleSignInAuthenticationEvent event = await eventFuture; - _verifyInit(mockPlatform); - verify(mockPlatform.signOut()); + expect(event, isA()); }); - test('disconnect; null response', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); + test('reports sign out from disconnect', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + final Future eventFuture = + googleSignIn.authenticationEvents.first; + await googleSignIn.initialize(); await googleSignIn.disconnect(); + final GoogleSignInAuthenticationEvent event = await eventFuture; - expect(googleSignIn.currentUser, isNull); - _verifyInit(mockPlatform); - verify(mockPlatform.disconnect()); + expect(event, isA()); }); + }); - test('isSignedIn', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.isSignedIn()).thenAnswer((Invocation _) async => true); - - final bool result = await googleSignIn.isSignedIn(); + group('supportsAuthenticate', () { + for (final bool support in [true, false]) { + test('reports $support from platform', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - expect(result, isTrue); - _verifyInit(mockPlatform); - verify(mockPlatform.isSignedIn()); - }); + when(mockPlatform.supportsAuthenticate()).thenReturn(support); - test('signIn works even if a previous call throws error in other zone', - () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - - when(mockPlatform.signInSilently()).thenThrow(Exception('Not a user')); - await runZonedGuarded(() async { - expect(await googleSignIn.signInSilently(), isNull); - }, (Object e, StackTrace st) {}); - expect(await googleSignIn.signIn(), isNotNull); - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()); - verify(mockPlatform.signIn()); - }); + expect(googleSignIn.supportsAuthenticate(), support); + }); + } + }); - test('concurrent calls of the same method trigger sign in once', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - final List> futures = - >[ - googleSignIn.signInSilently(), - googleSignIn.signInSilently(), - ]; - - expect(futures.first, isNot(futures.last), - reason: 'Must return new Future'); - - final List users = await Future.wait(futures); - - expect(googleSignIn.currentUser, isNotNull); - expect(users, [ - googleSignIn.currentUser, - googleSignIn.currentUser - ]); - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()).called(1); + group('authorizationForScopes', () { + test('passes expected paramaters when called for a user', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + when(mockPlatform.authenticate(any)).thenAnswer((_) async => + const AuthenticationResults( + user: defaultUser, + authenticationTokens: + AuthenticationTokenData(idToken: 'idToken'))); + when(mockPlatform.clientAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); + + await googleSignIn.initialize(); + final GoogleSignInAccount authentication = + await googleSignIn.authenticate(); + const List scopes = ['scope1', 'scope2']; + await authentication.authorizationClient.authorizationForScopes(scopes); + + final VerificationResult verification = + verify(mockPlatform.clientAuthorizationTokensForScopes(captureAny)); + final ClientAuthorizationTokensForScopesParameters params = verification + .captured[0] as ClientAuthorizationTokensForScopesParameters; + expect(params.request.scopes, scopes); + expect(params.request.userId, defaultUser.id); + expect(params.request.email, defaultUser.email); + expect(params.request.promptIfUnauthorized, false); }); - test('can sign in after previously failed attempt', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.signInSilently()).thenThrow(Exception('Not a user')); + test('passes expected paramaters when called without a user', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - expect(await googleSignIn.signInSilently(), isNull); - expect(await googleSignIn.signIn(), isNotNull); + when(mockPlatform.clientAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()); - verify(mockPlatform.signIn()); - }); + const List scopes = ['scope1', 'scope2']; + await googleSignIn.authorizationClient.authorizationForScopes(scopes); - test('concurrent calls of different signIn methods', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - final List> futures = - >[ - googleSignIn.signInSilently(), - googleSignIn.signIn(), - ]; - expect(futures.first, isNot(futures.last)); - - final List users = await Future.wait(futures); - - expect(users.first, users.last, reason: 'Must return the same user'); - expect(googleSignIn.currentUser, users.last); - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()); - verifyNever(mockPlatform.signIn()); + final VerificationResult verification = + verify(mockPlatform.clientAuthorizationTokensForScopes(captureAny)); + final ClientAuthorizationTokensForScopesParameters params = verification + .captured[0] as ClientAuthorizationTokensForScopesParameters; + expect(params.request.scopes, scopes); + expect(params.request.userId, null); + expect(params.request.email, null); + expect(params.request.promptIfUnauthorized, false); }); - test('can sign in after aborted flow', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); + test('reports tokens', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - when(mockPlatform.signIn()).thenAnswer((Invocation _) async => null); - expect(await googleSignIn.signIn(), isNull); - - when(mockPlatform.signIn()) - .thenAnswer((Invocation _) async => kDefaultUser); - expect(await googleSignIn.signIn(), isNotNull); - }); + const String accessToken = 'accessToken'; + when(mockPlatform.clientAuthorizationTokensForScopes(any)).thenAnswer( + (_) async => + const ClientAuthorizationTokenData(accessToken: accessToken)); - test('signOut/disconnect methods always trigger native calls', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - final List> futures = - >[ - googleSignIn.signOut(), - googleSignIn.signOut(), - googleSignIn.disconnect(), - googleSignIn.disconnect(), - ]; - - await Future.wait(futures); - - _verifyInit(mockPlatform); - verify(mockPlatform.signOut()).called(2); - verify(mockPlatform.disconnect()).called(2); + const List scopes = ['scope1', 'scope2']; + final GoogleSignInClientAuthorization? auth = + await googleSignIn.authorizationClient.authorizationForScopes(scopes); + expect(auth?.accessToken, accessToken); }); - test('queue of many concurrent calls', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - final List> futures = - >[ - googleSignIn.signInSilently(), - googleSignIn.signOut(), - googleSignIn.signIn(), - googleSignIn.disconnect(), - ]; - - await Future.wait(futures); - - _verifyInit(mockPlatform); - verifyInOrder([ - mockPlatform.signInSilently(), - mockPlatform.signOut(), - mockPlatform.signIn(), - mockPlatform.disconnect(), - ]); - }); + test('reports null', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - test('signInSilently suppresses errors by default', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.signInSilently()).thenThrow(Exception('I am an error')); - expect(await googleSignIn.signInSilently(), isNull); // should not throw - }); + when(mockPlatform.clientAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); - test('signInSilently forwards exceptions', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.signInSilently()).thenThrow(Exception('I am an error')); - expect(googleSignIn.signInSilently(suppressErrors: false), - throwsA(isInstanceOf())); + const List scopes = ['scope1', 'scope2']; + expect( + await googleSignIn.authorizationClient.authorizationForScopes(scopes), + null); }); + }); - test('signInSilently allows re-authentication to be requested', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - await googleSignIn.signInSilently(); - expect(googleSignIn.currentUser, isNotNull); - - await googleSignIn.signInSilently(reAuthenticate: true); - - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()).called(2); + group('authorizeScopes', () { + test('passes expected paramaters when called for a user', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + when(mockPlatform.authenticate(any)).thenAnswer((_) async => + const AuthenticationResults( + user: defaultUser, + authenticationTokens: + AuthenticationTokenData(idToken: 'idToken'))); + when(mockPlatform.clientAuthorizationTokensForScopes(any)).thenAnswer( + (_) async => + const ClientAuthorizationTokenData(accessToken: 'accessToken')); + + await googleSignIn.initialize(); + final GoogleSignInAccount authentication = + await googleSignIn.authenticate(); + const List scopes = ['scope1', 'scope2']; + await authentication.authorizationClient.authorizeScopes(scopes); + + final VerificationResult verification = + verify(mockPlatform.clientAuthorizationTokensForScopes(captureAny)); + final ClientAuthorizationTokensForScopesParameters params = verification + .captured[0] as ClientAuthorizationTokensForScopesParameters; + expect(params.request.scopes, scopes); + expect(params.request.userId, defaultUser.id); + expect(params.request.email, defaultUser.email); + expect(params.request.promptIfUnauthorized, true); }); - test('can sign in after init failed before', () async { - // Web eagerly `initWithParams` when GoogleSignIn is created, so make sure - // the initWithParams is throwy ASAP. - when(mockPlatform.initWithParams(any)) - .thenThrow(Exception('First init fails')); - - final GoogleSignIn googleSignIn = GoogleSignIn(); + test('passes expected paramaters when called without a user', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - expect(googleSignIn.signIn(), throwsA(isInstanceOf())); + when(mockPlatform.clientAuthorizationTokensForScopes(any)).thenAnswer( + (_) async => + const ClientAuthorizationTokenData(accessToken: 'accessToken')); - when(mockPlatform.initWithParams(any)) - .thenAnswer((Invocation _) async {}); - expect(await googleSignIn.signIn(), isNotNull); - }); - - test('created with standard factory uses correct options', () async { - final GoogleSignIn googleSignIn = GoogleSignIn.standard(); + const List scopes = ['scope1', 'scope2']; + await googleSignIn.authorizationClient.authorizeScopes(scopes); - await googleSignIn.signInSilently(); - expect(googleSignIn.currentUser, isNotNull); - _verifyInit(mockPlatform); - verify(mockPlatform.signInSilently()); + final VerificationResult verification = + verify(mockPlatform.clientAuthorizationTokensForScopes(captureAny)); + final ClientAuthorizationTokensForScopesParameters params = verification + .captured[0] as ClientAuthorizationTokensForScopesParameters; + expect(params.request.scopes, scopes); + expect(params.request.userId, null); + expect(params.request.email, null); + expect(params.request.promptIfUnauthorized, true); }); - test('created with defaultGamesSignIn factory uses correct options', - () async { - final GoogleSignIn googleSignIn = GoogleSignIn.games(); + test('reports tokens', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - await googleSignIn.signInSilently(); - expect(googleSignIn.currentUser, isNotNull); - _verifyInit(mockPlatform, signInOption: SignInOption.games); - verify(mockPlatform.signInSilently()); - }); + const String accessToken = 'accessToken'; + when(mockPlatform.clientAuthorizationTokensForScopes(any)).thenAnswer( + (_) async => + const ClientAuthorizationTokenData(accessToken: accessToken)); - test('authentication', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.getTokens( - email: anyNamed('email'), - shouldRecoverAuth: anyNamed('shouldRecoverAuth'))) - .thenAnswer((Invocation _) async => GoogleSignInTokenData( - idToken: '123', - accessToken: '456', - serverAuthCode: '789', - )); - - await googleSignIn.signIn(); - - final GoogleSignInAccount user = googleSignIn.currentUser!; - final GoogleSignInAuthentication auth = await user.authentication; - - expect(auth.accessToken, '456'); - expect(auth.idToken, '123'); - verify(mockPlatform.getTokens( - email: 'john.doe@gmail.com', shouldRecoverAuth: true)); + const List scopes = ['scope1', 'scope2']; + final GoogleSignInClientAuthorization auth = + await googleSignIn.authorizationClient.authorizeScopes(scopes); + expect(auth.accessToken, accessToken); }); - test('requestScopes returns true once new scope is granted', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.requestScopes(any)) - .thenAnswer((Invocation _) async => true); + test('throws for unexpected null', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - await googleSignIn.signIn(); - final bool result = - await googleSignIn.requestScopes(['testScope']); + when(mockPlatform.clientAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); - expect(result, isTrue); - _verifyInit(mockPlatform); - verify(mockPlatform.signIn()); - verify(mockPlatform.requestScopes(['testScope'])); + const List scopes = ['scope1', 'scope2']; + await expectLater( + googleSignIn.authorizationClient.authorizeScopes(scopes), + throwsA(isA().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.unknownError))); }); + }); - test('canAccessScopes forwards calls to platform', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - when(mockPlatform.canAccessScopes( - any, - accessToken: anyNamed('accessToken'), - )).thenAnswer((Invocation _) async => true); - - await googleSignIn.signIn(); - final bool result = await googleSignIn.canAccessScopes( - ['testScope'], - accessToken: 'xyz', - ); - - expect(result, isTrue); - _verifyInit(mockPlatform); - verify(mockPlatform.canAccessScopes( - ['testScope'], - accessToken: 'xyz', - )); + group('authorizeServer', () { + test('passes expected paramaters when called for a user', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + when(mockPlatform.authenticate(any)).thenAnswer((_) async => + const AuthenticationResults( + user: defaultUser, + authenticationTokens: + AuthenticationTokenData(idToken: 'idToken'))); + when(mockPlatform.serverAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); + + await googleSignIn.initialize(); + final GoogleSignInAccount authentication = + await googleSignIn.authenticate(); + const List scopes = ['scope1', 'scope2']; + await authentication.authorizationClient.authorizeServer(scopes); + + final VerificationResult verification = + verify(mockPlatform.serverAuthorizationTokensForScopes(captureAny)); + final ServerAuthorizationTokensForScopesParameters params = verification + .captured[0] as ServerAuthorizationTokensForScopesParameters; + expect(params.request.scopes, scopes); + expect(params.request.userId, defaultUser.id); + expect(params.request.email, defaultUser.email); + expect(params.request.promptIfUnauthorized, true); }); - test('userDataEvents are forwarded through the onUserChanged stream', - () async { - final StreamController userDataController = - StreamController(); - - when(mockPlatform.userDataEvents) - .thenAnswer((Invocation _) => userDataController.stream); - when(mockPlatform.isSignedIn()).thenAnswer((Invocation _) async => false); - - final GoogleSignIn googleSignIn = GoogleSignIn(); - await googleSignIn.isSignedIn(); + test('passes expected paramaters when called without a user', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - // This is needed to ensure `_ensureInitialized` is called! - final Future> nextTwoEvents = - googleSignIn.onCurrentUserChanged.take(2).toList(); + when(mockPlatform.serverAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); - // Dispatch two events - userDataController.add(kDefaultUser); - userDataController.add(null); + const List scopes = ['scope1', 'scope2']; + await googleSignIn.authorizationClient.authorizeServer(scopes); - final List events = await nextTwoEvents; - - expect(events.first, isNotNull); - - final GoogleSignInAccount user = events.first!; + final VerificationResult verification = + verify(mockPlatform.serverAuthorizationTokensForScopes(captureAny)); + final ServerAuthorizationTokensForScopesParameters params = verification + .captured[0] as ServerAuthorizationTokensForScopesParameters; + expect(params.request.scopes, scopes); + expect(params.request.userId, null); + expect(params.request.email, null); + expect(params.request.promptIfUnauthorized, true); + }); - expect(user.displayName, equals(kDefaultUser.displayName)); - expect(user.email, equals(kDefaultUser.email)); - expect(user.id, equals(kDefaultUser.id)); - expect(user.photoUrl, equals(kDefaultUser.photoUrl)); - expect(user.serverAuthCode, equals(kDefaultUser.serverAuthCode)); + test('reports tokens', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - // The second event was a null... - expect(events.last, isNull); - }); + const String authCode = 'authCode'; + when(mockPlatform.serverAuthorizationTokensForScopes(any)).thenAnswer( + (_) async => + const ServerAuthorizationTokenData(serverAuthCode: authCode)); - test('user starts as null', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - expect(googleSignIn.currentUser, isNull); + const List scopes = ['scope1', 'scope2']; + final GoogleSignInServerAuthorization? auth = + await googleSignIn.authorizationClient.authorizeServer(scopes); + expect(auth?.serverAuthCode, authCode); }); - test('can sign in and sign out', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - await googleSignIn.signIn(); - - final GoogleSignInAccount user = googleSignIn.currentUser!; + test('reports null', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; - expect(user.displayName, equals(kDefaultUser.displayName)); - expect(user.email, equals(kDefaultUser.email)); - expect(user.id, equals(kDefaultUser.id)); - expect(user.photoUrl, equals(kDefaultUser.photoUrl)); - expect(user.serverAuthCode, equals(kDefaultUser.serverAuthCode)); + when(mockPlatform.serverAuthorizationTokensForScopes(any)) + .thenAnswer((_) async => null); - await googleSignIn.disconnect(); - expect(googleSignIn.currentUser, isNull); - }); - - test('disconnect when signout already succeeds', () async { - final GoogleSignIn googleSignIn = GoogleSignIn(); - await googleSignIn.disconnect(); - expect(googleSignIn.currentUser, isNull); + const List scopes = ['scope1', 'scope2']; + expect( + await googleSignIn.authorizationClient.authorizeServer(scopes), null); }); }); } - -void _verifyInit( - MockGoogleSignInPlatform mockSignIn, { - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String? hostedDomain, - String? clientId, - String? serverClientId, - bool forceCodeForRefreshToken = false, - String? forceAccountName, -}) { - verify(mockSignIn.initWithParams(argThat( - isA() - .having( - (SignInInitParameters p) => p.scopes, - 'scopes', - scopes, - ) - .having( - (SignInInitParameters p) => p.signInOption, - 'signInOption', - signInOption, - ) - .having( - (SignInInitParameters p) => p.hostedDomain, - 'hostedDomain', - hostedDomain, - ) - .having( - (SignInInitParameters p) => p.clientId, - 'clientId', - clientId, - ) - .having( - (SignInInitParameters p) => p.serverClientId, - 'serverClientId', - serverClientId, - ) - .having( - (SignInInitParameters p) => p.forceCodeForRefreshToken, - 'forceCodeForRefreshToken', - forceCodeForRefreshToken, - ) - .having( - (SignInInitParameters p) => p.forceAccountName, - 'forceAccountName', - forceAccountName, - ), - ))); -} diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart index e81094692b8..f3a891d8796 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.4 from annotations +// Mocks generated by Mockito 5.4.5 from annotations // in google_sign_in/test/google_sign_in_test.dart. // Do not manually edit this file. @@ -18,20 +18,16 @@ import 'package:mockito/mockito.dart' as _i1; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types // ignore_for_file: subtype_of_sealed_class -class _FakeGoogleSignInTokenData_0 extends _i1.SmartFake - implements _i2.GoogleSignInTokenData { - _FakeGoogleSignInTokenData_0( - Object parent, - Invocation parentInvocation, - ) : super( - parent, - parentInvocation, - ); +class _FakeAuthenticationResults_0 extends _i1.SmartFake + implements _i2.AuthenticationResults { + _FakeAuthenticationResults_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); } /// A class which mocks [GoogleSignInPlatform]. @@ -44,151 +40,72 @@ class MockGoogleSignInPlatform extends _i1.Mock } @override - bool get isMock => (super.noSuchMethod( - Invocation.getter(#isMock), - returnValue: false, - ) as bool); - - @override - _i4.Future init({ - List? scopes = const [], - _i2.SignInOption? signInOption = _i2.SignInOption.standard, - String? hostedDomain, - String? clientId, - }) => - (super.noSuchMethod( - Invocation.method( - #init, - [], - { - #scopes: scopes, - #signInOption: signInOption, - #hostedDomain: hostedDomain, - #clientId: clientId, - }, - ), + _i4.Future init(_i2.InitParameters? params) => (super.noSuchMethod( + Invocation.method(#init, [params]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future initWithParams(_i2.SignInInitParameters? params) => + _i4.Future<_i2.AuthenticationResults?>? attemptLightweightAuthentication( + _i2.AttemptLightweightAuthenticationParameters? params, + ) => (super.noSuchMethod( - Invocation.method( - #initWithParams, - [params], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + Invocation.method(#attemptLightweightAuthentication, [params]), + ) as _i4.Future<_i2.AuthenticationResults?>?); @override - _i4.Future<_i2.GoogleSignInUserData?> signInSilently() => (super.noSuchMethod( - Invocation.method( - #signInSilently, - [], + _i4.Future<_i2.AuthenticationResults> authenticate( + _i2.AuthenticateParameters? params, + ) => + (super.noSuchMethod( + Invocation.method(#authenticate, [params]), + returnValue: _i4.Future<_i2.AuthenticationResults>.value( + _FakeAuthenticationResults_0( + this, + Invocation.method(#authenticate, [params]), + ), ), - returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), - ) as _i4.Future<_i2.GoogleSignInUserData?>); + ) as _i4.Future<_i2.AuthenticationResults>); @override - _i4.Future<_i2.GoogleSignInUserData?> signIn() => (super.noSuchMethod( - Invocation.method( - #signIn, - [], - ), - returnValue: _i4.Future<_i2.GoogleSignInUserData?>.value(), - ) as _i4.Future<_i2.GoogleSignInUserData?>); + bool supportsAuthenticate() => (super.noSuchMethod( + Invocation.method(#supportsAuthenticate, []), + returnValue: false, + ) as bool); @override - _i4.Future<_i2.GoogleSignInTokenData> getTokens({ - required String? email, - bool? shouldRecoverAuth, - }) => - (super.noSuchMethod( - Invocation.method( - #getTokens, - [], - { - #email: email, - #shouldRecoverAuth: shouldRecoverAuth, - }, - ), - returnValue: _i4.Future<_i2.GoogleSignInTokenData>.value( - _FakeGoogleSignInTokenData_0( - this, - Invocation.method( - #getTokens, - [], - { - #email: email, - #shouldRecoverAuth: shouldRecoverAuth, - }, - ), - )), - ) as _i4.Future<_i2.GoogleSignInTokenData>); + _i4.Future<_i2.ClientAuthorizationTokenData?> + clientAuthorizationTokensForScopes( + _i2.ClientAuthorizationTokensForScopesParameters? params, + ) => + (super.noSuchMethod( + Invocation.method(#clientAuthorizationTokensForScopes, [params]), + returnValue: _i4.Future<_i2.ClientAuthorizationTokenData?>.value(), + ) as _i4.Future<_i2.ClientAuthorizationTokenData?>); @override - _i4.Future signOut() => (super.noSuchMethod( - Invocation.method( - #signOut, - [], - ), - returnValue: _i4.Future.value(), - returnValueForMissingStub: _i4.Future.value(), - ) as _i4.Future); + _i4.Future<_i2.ServerAuthorizationTokenData?> + serverAuthorizationTokensForScopes( + _i2.ServerAuthorizationTokensForScopesParameters? params, + ) => + (super.noSuchMethod( + Invocation.method(#serverAuthorizationTokensForScopes, [params]), + returnValue: _i4.Future<_i2.ServerAuthorizationTokenData?>.value(), + ) as _i4.Future<_i2.ServerAuthorizationTokenData?>); @override - _i4.Future disconnect() => (super.noSuchMethod( - Invocation.method( - #disconnect, - [], - ), + _i4.Future signOut(_i2.SignOutParams? params) => (super.noSuchMethod( + Invocation.method(#signOut, [params]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); @override - _i4.Future isSignedIn() => (super.noSuchMethod( - Invocation.method( - #isSignedIn, - [], - ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); - - @override - _i4.Future clearAuthCache({required String? token}) => + _i4.Future disconnect(_i2.DisconnectParams? params) => (super.noSuchMethod( - Invocation.method( - #clearAuthCache, - [], - {#token: token}, - ), + Invocation.method(#disconnect, [params]), returnValue: _i4.Future.value(), returnValueForMissingStub: _i4.Future.value(), ) as _i4.Future); - - @override - _i4.Future requestScopes(List? scopes) => (super.noSuchMethod( - Invocation.method( - #requestScopes, - [scopes], - ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); - - @override - _i4.Future canAccessScopes( - List? scopes, { - String? accessToken, - }) => - (super.noSuchMethod( - Invocation.method( - #canAccessScopes, - [scopes], - {#accessToken: accessToken}, - ), - returnValue: _i4.Future.value(false), - ) as _i4.Future); } diff --git a/packages/google_sign_in/google_sign_in/test/widgets_test.dart b/packages/google_sign_in/google_sign_in/test/widgets_test.dart index 717edc3699e..ad34111b377 100644 --- a/packages/google_sign_in/google_sign_in/test/widgets_test.dart +++ b/packages/google_sign_in/google_sign_in/test/widgets_test.dart @@ -28,9 +28,6 @@ class _TestGoogleIdentity extends GoogleIdentity { @override String? get displayName => null; - - @override - String? get serverAuthCode => null; } /// A mocked [HttpClient] which always returns a [_MockHttpRequest]. 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 76384bac841..047462fe22e 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 @@ -23,7 +23,7 @@ class GoogleSignInException implements Exception { @override String toString() => - 'CredentialException(code $code, $description, $details)'; + 'GoogleSignInException(code $code, $description, $details)'; } /// Types of [GoogleSignInException]s, as indicated by From 816f614bf4cd1792f6b19eb33da3f28db056dce3 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 16 May 2025 13:52:23 -0400 Subject: [PATCH 20/52] Revert unrelated Xcode project changes --- .../xcshareddata/swiftpm/Package.resolved | 69 ----------------- .../xcshareddata/swiftpm/Package.resolved | 42 ---------- .../xcshareddata/swiftpm/Package.resolved | 42 ---------- .../xcshareddata/swiftpm/Package.resolved | 50 ------------ .../xcshareddata/xcschemes/Runner.xcscheme | 2 - .../xcshareddata/swiftpm/Package.resolved | 77 ------------------- 6 files changed, 282 deletions(-) delete mode 100644 packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved delete mode 100644 packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 160aa48fc26..00000000000 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,69 +0,0 @@ -{ - "originHash" : "fd06f65309a465a6cb6f442cd439d2481f4f7bb167b133bd83bf27b3b6b211e8", - "pins" : [ - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "61b85103a1aeed8218f17c794687781505fbbef5", - "version" : "11.2.0" - } - }, - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", - "version" : "1.7.6" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS.git", - "state" : { - "revision" : "65fb3f1aa6ffbfdc79c4e22178a55cd91561f5e9", - "version" : "8.0.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "60da361632d0de02786f709bdc0c4df340f7613e", - "version" : "8.1.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", - "version" : "2.4.0" - } - } - ], - "version" : 3 -} diff --git a/packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 2ae8b21eb12..00000000000 --- a/packages/google_sign_in/google_sign_in/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,42 +0,0 @@ -{ - "originHash" : "7220edc89d961f8413c785deacf1702210b58380c136ba1241e754a6e1eab3fb", - "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", - "version" : "1.7.6" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS.git", - "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - } - ], - "version" : 3 -} diff --git a/packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 2ae8b21eb12..00000000000 --- a/packages/google_sign_in/google_sign_in/example/macos/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,42 +0,0 @@ -{ - "originHash" : "7220edc89d961f8413c785deacf1702210b58380c136ba1241e754a6e1eab3fb", - "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", - "version" : "1.7.6" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS.git", - "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - } - ], - "version" : 3 -} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 3dc8b8235fe..00000000000 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,50 +0,0 @@ -{ - "originHash" : "4b36b64d9a071f871b5e5ecf435f3e097843030f79026880b7d9ef1f081d563d", - "pins" : [ - { - "identity" : "appauth-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/openid/AppAuth-iOS.git", - "state" : { - "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", - "version" : "1.7.6" - } - }, - { - "identity" : "googlesignin-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleSignIn-iOS.git", - "state" : { - "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d", - "version" : "7.1.0" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b", - "version" : "3.5.0" - } - }, - { - "identity" : "gtmappauth", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GTMAppAuth.git", - "state" : { - "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a", - "version" : "4.1.1" - } - }, - { - "identity" : "ocmock", - "kind" : "remoteSourceControl", - "location" : "https://github.com/erikdoe/ocmock", - "state" : { - "revision" : "fe1661a3efed11831a6452f4b1a0c5e6ddc08c3d" - } - } - ], - "version" : 3 -} diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 6bf8b2f1ace..330fa765f0d 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,7 +44,6 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Date: Fri, 16 May 2025 14:26:03 -0400 Subject: [PATCH 21/52] Analysis fix --- .../google_sign_in/google_sign_in/test/google_sign_in_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index aff84193b94..ac4a8b887b6 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -2,8 +2,6 @@ // 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_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; From a0cf078f936479e3d28c6920ad3415594d45ac65 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 16 May 2025 14:27:13 -0400 Subject: [PATCH 22/52] Revert extension package changes --- .../example/pubspec.yaml | 4 ---- .../extension_google_sign_in_as_googleapis_auth/pubspec.yaml | 4 ---- 2 files changed, 8 deletions(-) diff --git a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml index 4f089757bb6..2cae99f0f90 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml @@ -26,7 +26,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in: {path: ../../../packages/google_sign_in/google_sign_in} diff --git a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml index f76c5e6c964..d3a29c234fe 100644 --- a/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml +++ b/packages/extension_google_sign_in_as_googleapis_auth/pubspec.yaml @@ -33,7 +33,3 @@ topics: false_secrets: - example/android/app/google-services.json - example/ios/Runner/GoogleService-Info.plist -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in: {path: ../../packages/google_sign_in/google_sign_in} From 0a134ec866a3b5126cbb30582a770f7cbd85e413 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 16 May 2025 14:34:04 -0400 Subject: [PATCH 23/52] Fix Obj-C warnings --- .../Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m index ecbdc3366cf..4f058fa8b09 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m @@ -32,7 +32,7 @@ static id FSISanitizedUserInfo(id value) { NSError *error = value; return @{ @"domain" : error.domain, - @"code" : [NSString stringWithFormat:@"%d", error.code], + @"code" : [NSString stringWithFormat:@"%ld", (long)error.code], @"localizedDescription" : error.localizedDescription, @"userInfo" : FSISanitizedUserInfo(error.userInfo), }; @@ -66,9 +66,10 @@ static id FSISanitizedUserInfo(id value) { /// This should only be used when an error can't be recognized and mapped to a /// GoogleSignInErrorCode. static FlutterError *FSIFlutterErrorForNSError(NSError *error) { - return [FlutterError errorWithCode:[NSString stringWithFormat:@"%@: %d", error.domain, error.code] - message:error.localizedDescription - details:FSISanitizedUserInfo(error.userInfo)]; + return [FlutterError + errorWithCode:[NSString stringWithFormat:@"%@: %ld", error.domain, (long)error.code] + message:error.localizedDescription + details:FSISanitizedUserInfo(error.userInfo)]; } /// Maps a GIDSignInErrorCode to the corresponding Pigeon GoogleSignInErrorCode From 1b0d4d11b58a5a9d36dd4159b13b775b3724176c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 16 May 2025 15:01:54 -0400 Subject: [PATCH 24/52] Missing license --- .../kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt index 5e7b8f50098..4a9f09795f7 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/ResultUtils.kt @@ -1,3 +1,7 @@ +// 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. + package io.flutter.plugins.googlesignin fun completeWithGetGetCredentialResult( From 8f57604a334ef22846171046c85011e0584c015e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 19 May 2025 12:45:25 -0400 Subject: [PATCH 25/52] Analyze fix --- .../google_sign_in_ios/lib/google_sign_in_ios.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart index fee68a7a3f2..770a8b72e76 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -102,8 +102,8 @@ class GoogleSignInIOS extends GoogleSignInPlatform { @override Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params) async { - final (:String? accessToken, :String? serverAuthCode) = - await _getAuthorizationTokens(params.request); + final String? accessToken = + (await _getAuthorizationTokens(params.request)).accessToken; return accessToken == null ? null : ClientAuthorizationTokenData(accessToken: accessToken); @@ -112,8 +112,8 @@ class GoogleSignInIOS extends GoogleSignInPlatform { @override Future serverAuthorizationTokensForScopes( ServerAuthorizationTokensForScopesParameters params) async { - final (:String? accessToken, :String? serverAuthCode) = - await _getAuthorizationTokens(params.request); + final String? serverAuthCode = + (await _getAuthorizationTokens(params.request)).serverAuthCode; return serverAuthCode == null ? null : ServerAuthorizationTokenData(serverAuthCode: serverAuthCode); From e78699850f4a8df16c4c80a3fe785d4502a31a35 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 19 May 2025 12:58:21 -0400 Subject: [PATCH 26/52] Update excerpting --- .../google_sign_in/google_sign_in_ios/README.md | 16 +++++++++------- .../integration_test/google_sign_in_test.dart | 16 ++++++++++++++-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 ++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_ios/README.md b/packages/google_sign_in/google_sign_in_ios/README.md index e3027fac31d..9d70264a7de 100644 --- a/packages/google_sign_in/google_sign_in_ios/README.md +++ b/packages/google_sign_in/google_sign_in_ios/README.md @@ -55,17 +55,19 @@ should add it to your `pubspec.yaml` as usual. ``` As an alternative to editing the `Info.plist` in your Xcode project, -you can instead configure your app in Dart code. In this case, skip steps 4 to 5 - and pass `clientId` and `serverClientId` to the `GoogleSignIn` constructor: +you can instead configure your app in Dart code. In this case, skip steps 4 and +5 and pass `clientId` and `serverClientId` to the `GoogleSignIn` initialization: - + ```dart -final GoogleSignIn googleSignIn = GoogleSignIn( - // The OAuth client id of your app. This is required. +final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; +await signIn.init(const InitParameters( + // The OAuth client ID of your app. This is required. clientId: 'Your Client ID', - // If you need to authenticate to a backend server, specify its OAuth client. This is optional. + // If you need to authenticate to a backend server, specify the server's + // OAuth client ID. This is optional. serverClientId: 'Your Server ID', -); +)); ``` Note that step 6 is still required. diff --git a/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart index 296cca2d0b0..11a312c90f0 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_ios/example/integration_test/google_sign_in_test.dart @@ -18,7 +18,19 @@ void main() { // This is primarily to validate that the native method handler is present // and correctly set up to receive messages (i.e., that this doesn't // throw). - final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; - await expectLater(signIn.init(const InitParameters()), completes); + try { + // #docregion IDsInCode + final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; + await signIn.init(const InitParameters( + // The OAuth client ID of your app. This is required. + clientId: 'Your Client ID', + // If you need to authenticate to a backend server, specify the server's + // OAuth client ID. This is optional. + serverClientId: 'Your Server ID', + )); + // #enddocregion IDsInCode + } catch (e) { + fail('Initialization should succeed'); + } }); } diff --git a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 330fa765f0d..6bf8b2f1ace 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_sign_in/google_sign_in_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -44,6 +44,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Date: Tue, 27 May 2025 12:40:28 -0400 Subject: [PATCH 27/52] Remove email scope --- packages/google_sign_in/google_sign_in/README.md | 1 - packages/google_sign_in/google_sign_in/example/lib/main.dart | 1 - .../google_sign_in/google_sign_in_android/example/lib/main.dart | 1 - 3 files changed, 3 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index bde0918557e..199010ef4e1 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -80,7 +80,6 @@ you can silently request an access token for those scopes: ```dart const List scopes = [ - 'email', 'https://www.googleapis.com/auth/contacts.readonly', ]; // ··· diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 43a4f59e1fa..3de6419af93 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -25,7 +25,6 @@ String? serverClientId; /// The scopes required by this application. // #docregion CheckAuthorization const List scopes = [ - 'email', 'https://www.googleapis.com/auth/contacts.readonly', ]; // #enddocregion CheckAuthorization diff --git a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart index 52b1c6b7ca2..3a177c19ff3 100644 --- a/packages/google_sign_in/google_sign_in_android/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in_android/example/lib/main.dart @@ -12,7 +12,6 @@ import 'package:google_sign_in_platform_interface/google_sign_in_platform_interf import 'package:http/http.dart' as http; const List _scopes = [ - 'email', 'https://www.googleapis.com/auth/contacts.readonly', ]; From 5a587b4db41da1596d5a25ad691141282010a69e Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 27 May 2025 16:07:16 -0400 Subject: [PATCH 28/52] Web README additions --- packages/google_sign_in/google_sign_in_web/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index f9222b53b45..2c226791c92 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -60,6 +60,12 @@ On the web, instead of providing custom UI that calls `authenticate`, you should display the Widget returned by `renderButton` (from `web_only.dart`), and listen to `authenticationEvents` to know when the user has signed in. +The GIS SDK does not renew authentication sessions. Once the token expires +(after 3600 seconds), if you need to use the `idToken` again you must trigger +a new authentication flow. In most cases, you should use the `idToken` +immediately after authentication, and track sign-in state at the application +level, or via a separate server backend. + ### Migration from versions before 0.12 See [Migrating from Google Sign-In](https://developers.google.com/identity/gsi/web/guides/migration) From 5f78e10f44549708b57962371b2fb7eb546c4dfe Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Tue, 27 May 2025 16:07:47 -0400 Subject: [PATCH 29/52] Update stream error handling, update main example --- .../google_sign_in/example/lib/main.dart | 53 +++++++++++++------ .../google_sign_in/lib/google_sign_in.dart | 9 ++-- .../google_sign_in/lib/src/event_types.dart | 11 ---- .../test/google_sign_in_test.dart | 22 ++++---- .../google_sign_in_platform_interface.dart | 5 ++ 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 3de6419af93..1580cc0758b 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -63,7 +63,9 @@ class _SignInDemoState extends State { unawaited(signIn .initialize(clientId: clientId, serverClientId: serverClientId) .then((_) { - signIn.authenticationEvents.listen(_handleAuthenticationEvent); + signIn.authenticationEvents + .listen(_handleAuthenticationEvent) + .onError(_handleAuthenticationError); /// This example always uses the stream-based approach to determining /// which UI state to show, rather than using the future returned here, @@ -78,16 +80,11 @@ class _SignInDemoState extends State { // #docregion CheckAuthorization GoogleSignInAccount? user; // #enddocregion CheckAuthorization - String error = ''; switch (event) { case GoogleSignInAuthenticationEventSignIn(): user = event.user; case GoogleSignInAuthenticationEventSignOut(): user = null; - case GoogleSignInAuthenticationEventException(): - user = null; - final GoogleSignInException e = event.exception; - error = 'GoogleSignInException ${e.code}: ${e.description}'; } // Check for existing authorization. @@ -102,16 +99,26 @@ class _SignInDemoState extends State { setState(() { _currentUser = user; _isAuthorized = authorization != null; - _errorMessage = error; + _errorMessage = ''; }); - // Now that we know that the user can access the required scopes, the app - // can call the REST API. + // If the user has already granted access to the required scopes, call the + // REST API. if (user != null && authorization != null) { unawaited(_handleGetContact(user)); } } + Future _handleAuthenticationError(Object e) async { + setState(() { + _currentUser = null; + _isAuthorized = false; + _errorMessage = e is GoogleSignInException + ? 'GoogleSignInException ${e.code}: ${e.description}' + : 'Unknown error: $e'; + }); + } + // Calls the People API REST endpoint for the signed-in user to retrieve information. Future _handleGetContact(GoogleSignInAccount user) async { setState(() { @@ -121,7 +128,8 @@ class _SignInDemoState extends State { await user.authorizationClient.authorizationHeaders(scopes); if (headers == null) { setState(() { - _contactText = 'Failed to construct authorization headers.'; + _contactText = ''; + _errorMessage = 'Failed to construct authorization headers.'; }); return; } @@ -131,11 +139,19 @@ class _SignInDemoState extends State { headers: headers, ); if (response.statusCode != 200) { - setState(() { - _contactText = 'People API gave a ${response.statusCode} ' - 'response. Check logs for details.'; - }); - print('People API ${response.statusCode} response: ${response.body}'); + if (response.statusCode == 401 || response.statusCode == 403) { + setState(() { + _isAuthorized = false; + _errorMessage = 'People API gave a ${response.statusCode} response. ' + 'Please re-authorize access.'; + }); + } else { + print('People API ${response.statusCode} response: ${response.body}'); + setState(() { + _contactText = 'People API gave a ${response.statusCode} ' + 'response. Check logs for details.'; + }); + } return; } final Map data = @@ -181,7 +197,12 @@ class _SignInDemoState extends State { // #enddocregion RequestScopes // The returned tokens are ignored since _handleGetContact uses the - // authorizationHeaders method to re-read the token cached by this call. + // authorizationHeaders method to re-read the token cached by + // authorizeScopes. The code above is used as a README excerpt, so shows + // the much more common pattern of getting the authorization and + // immediately using it. That results in an unused variable, which this + // statement suppresses (without adding an ignore: directive to the README + // excerpt). // ignore: unnecessary_statements authorization; diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 2c5e09264e0..07505fe8968 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -298,8 +298,7 @@ class GoogleSignIn { _authenticationStreamController .add(GoogleSignInAuthenticationEventSignOut()); case AuthenticationEventException(): - _authenticationStreamController - .add(GoogleSignInAuthenticationEventException(event.exception)); + _authenticationStreamController.addError(event.exception); } } @@ -377,8 +376,7 @@ class GoogleSignIn { return account; } on GoogleSignInException catch (e) { if (_createAuthenticationStreamEvents) { - _authenticationStreamController - .add(GoogleSignInAuthenticationEventException(e)); + _authenticationStreamController.addError(e); } if (!reportAllExceptions) { @@ -437,8 +435,7 @@ class GoogleSignIn { return account; } on GoogleSignInException catch (e) { if (_createAuthenticationStreamEvents) { - _authenticationStreamController - .add(GoogleSignInAuthenticationEventException(e)); + _authenticationStreamController.addError(e); } rethrow; } diff --git a/packages/google_sign_in/google_sign_in/lib/src/event_types.dart b/packages/google_sign_in/google_sign_in/lib/src/event_types.dart index 3c24d2f82df..b29a1f64a18 100644 --- a/packages/google_sign_in/google_sign_in/lib/src/event_types.dart +++ b/packages/google_sign_in/google_sign_in/lib/src/event_types.dart @@ -34,14 +34,3 @@ class GoogleSignInAuthenticationEventSignIn @immutable class GoogleSignInAuthenticationEventSignOut extends GoogleSignInAuthenticationEvent {} - -/// An authentication failure that resulted in an exception. -@immutable -class GoogleSignInAuthenticationEventException - extends GoogleSignInAuthenticationEvent { - /// Creates an exception event. - const GoogleSignInAuthenticationEventException(this.exception); - - /// The exception thrown during authentication. - final GoogleSignInException exception; -} diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index ac4a8b887b6..0c8187a47f0 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -110,12 +110,13 @@ void main() { await googleSignIn.initialize(); // This doesn't throw, since reportAllExceptions is false. await googleSignIn.attemptLightweightAuthentication(); - final GoogleSignInAuthenticationEvent event = await eventFuture; - expect(event, isA()); - final GoogleSignInAuthenticationEventException exceptionEvent = - event as GoogleSignInAuthenticationEventException; - expect(exceptionEvent.exception, exception); + try { + await eventFuture; + fail('The stream should throw before returning an event.'); + } on GoogleSignInException catch (e) { + expect(e, exception); + } }); test('reports success from authenticate', () async { @@ -152,12 +153,13 @@ void main() { await googleSignIn.initialize(); await expectLater( googleSignIn.authenticate(), throwsA(isA())); - final GoogleSignInAuthenticationEvent event = await eventFuture; - expect(event, isA()); - final GoogleSignInAuthenticationEventException exceptionEvent = - event as GoogleSignInAuthenticationEventException; - expect(exceptionEvent.exception, exception); + try { + await eventFuture; + fail('The stream should throw before returning an event.'); + } on GoogleSignInException catch (e) { + expect(e, exception); + } }); test('reports sign out from signOut', () async { 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 80a46f3e837..af1e26852e6 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 @@ -105,6 +105,11 @@ abstract class GoogleSignInPlatform extends PlatformInterface { /// 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; } From a6036b8d8a9d42ce80d38e03e6453c4d0e966ea8 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 12:57:02 -0400 Subject: [PATCH 30/52] Fix lightweight auth null return, and add missing tests --- .../google_sign_in/example/lib/main.dart | 7 +- .../google_sign_in/lib/google_sign_in.dart | 68 +++++-- .../test/google_sign_in_test.dart | 181 +++++++++++++++++- 3 files changed, 236 insertions(+), 20 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 1580cc0758b..9f322034976 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -199,10 +199,9 @@ class _SignInDemoState extends State { // The returned tokens are ignored since _handleGetContact uses the // authorizationHeaders method to re-read the token cached by // authorizeScopes. The code above is used as a README excerpt, so shows - // the much more common pattern of getting the authorization and - // immediately using it. That results in an unused variable, which this - // statement suppresses (without adding an ignore: directive to the README - // excerpt). + // the simpler pattern of getting the authorization for immediate use. + // That results in an unused variable, which this statement suppresses + // (without adding an ignore: directive to the README excerpt). // ignore: unnecessary_statements authorization; diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 07505fe8968..aa1bdb55435 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -354,7 +354,7 @@ class GoogleSignIn { /// for those cases instead, set [reportAllExceptions] to true. Future? attemptLightweightAuthentication({ bool reportAllExceptions = false, - }) async { + }) { try { final Future? future = GoogleSignInPlatform.instance.attemptLightweightAuthentication( @@ -362,9 +362,38 @@ class GoogleSignIn { if (future == null) { return null; } + return _resolveLightweightAuthenticationAttempt(future, + reportAllExceptions: reportAllExceptions); + } catch (e) { + if (e is GoogleSignInException) { + if (_createAuthenticationStreamEvents) { + _authenticationStreamController.addError(e); + } + + // For exceptions that should not be reported out, just return null. + if (!_shouldRethrowLightweightAuthenticationException(e, + reportAllExceptions: reportAllExceptions)) { + return Future.value(); + } + } + return Future.error(e); + } + } + + /// Resolves a future from the platform implementation's + /// attemptLightweightAuthentication. + /// + /// This is a separate method from [attemptLightweightAuthentication] to allow + /// using async/await, since [attemptLightweightAuthentication] can't use + /// async without losing the ability to return a null future. + Future _resolveLightweightAuthenticationAttempt( + Future future, { + required bool reportAllExceptions, + }) async { + try { final AuthenticationResults? result = await future; if (result == null) { - return Future.value(); + return null; } final GoogleSignInAccount account = @@ -379,19 +408,30 @@ class GoogleSignIn { _authenticationStreamController.addError(e); } - if (!reportAllExceptions) { - switch (e.code) { - case GoogleSignInExceptionCode.canceled: - case GoogleSignInExceptionCode.interrupted: - case GoogleSignInExceptionCode.uiUnavailable: - return null; - // Only specific types are ignored, everything else should rethrow. - // ignore: no_default_cases - default: - rethrow; - } + if (_shouldRethrowLightweightAuthenticationException(e, + reportAllExceptions: reportAllExceptions)) { + rethrow; } - rethrow; + return null; + } + } + + bool _shouldRethrowLightweightAuthenticationException( + GoogleSignInException e, { + required bool reportAllExceptions, + }) { + if (reportAllExceptions) { + return true; + } + switch (e.code) { + case GoogleSignInExceptionCode.canceled: + case GoogleSignInExceptionCode.interrupted: + case GoogleSignInExceptionCode.uiUnavailable: + return false; + // Only specific types are ignored, everything else should rethrow. + // ignore: no_default_cases + default: + return true; } } diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 0c8187a47f0..7104c9040cb 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -103,7 +103,7 @@ void main() { const GoogleSignInException exception = GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); when(mockPlatform.attemptLightweightAuthentication(any)) - .thenThrow(exception); + .thenAnswer((_) async => throw exception); final Future eventFuture = googleSignIn.authenticationEvents.first; @@ -115,6 +115,8 @@ void main() { await eventFuture; fail('The stream should throw before returning an event.'); } on GoogleSignInException catch (e) { + print(e); + print(exception); expect(e, exception); } }); @@ -146,7 +148,8 @@ void main() { const GoogleSignInException exception = GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); - when(mockPlatform.authenticate(any)).thenThrow(exception); + when(mockPlatform.authenticate(any)) + .thenAnswer((_) async => throw exception); final Future eventFuture = googleSignIn.authenticationEvents.first; @@ -199,6 +202,180 @@ void main() { } }); + group('attemptLightweightAuthentication', () { + test('returns successful authentication', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const String idToken = 'idToken'; + when(mockPlatform.attemptLightweightAuthentication(any)).thenAnswer( + (_) async => const AuthenticationResults( + user: defaultUser, + authenticationTokens: AuthenticationTokenData(idToken: idToken))); + + final Future? signInFuture = + googleSignIn.attemptLightweightAuthentication(); + expect(signInFuture, isNotNull); + final GoogleSignInAccount? signIn = await signInFuture; + expect(signIn?.displayName, defaultUser.displayName); + expect(signIn?.email, defaultUser.email); + expect(signIn?.id, defaultUser.id); + expect(signIn?.photoUrl, defaultUser.photoUrl); + expect(signIn?.authentication.idToken, idToken); + }); + + test('reports all exceptions when requested - sync', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.canceled); + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenThrow(exception); + + await googleSignIn.initialize(); + expect( + googleSignIn.attemptLightweightAuthentication( + reportAllExceptions: true), + throwsA(isA().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test( + 'reports serious exceptions even when all exceptions are not requested - sync', + () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = GoogleSignInException( + code: GoogleSignInExceptionCode.clientConfigurationError); + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenThrow(exception); + + await googleSignIn.initialize(); + expect( + googleSignIn.attemptLightweightAuthentication(), + throwsA(isA().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.clientConfigurationError))); + }); + + test('reports all exceptions when requested - async', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.canceled); + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenAnswer((_) async => throw exception); + + await googleSignIn.initialize(); + expect( + googleSignIn.attemptLightweightAuthentication( + reportAllExceptions: true), + throwsA(isA().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.canceled))); + }); + + test( + 'reports serious exceptions even when all exceptions are not requested - async', + () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = GoogleSignInException( + code: GoogleSignInExceptionCode.clientConfigurationError); + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenAnswer((_) async => throw exception); + + await googleSignIn.initialize(); + expect( + googleSignIn.attemptLightweightAuthentication(), + throwsA(isA().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.clientConfigurationError))); + }); + + test('returns a null future from the platform', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + when(mockPlatform.attemptLightweightAuthentication(any)).thenReturn(null); + + final Future? signInFuture = + googleSignIn.attemptLightweightAuthentication(); + expect(signInFuture, isNull); + }); + + test('returns a future that resolves to null from the platform', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenAnswer((_) async => null); + + final Future? signInFuture = + googleSignIn.attemptLightweightAuthentication(); + expect(signInFuture, isNotNull); + final GoogleSignInAccount? signIn = await signInFuture; + expect(signIn, isNull); + }); + }); + + group('authenticate', () { + test('passes expected paramaters', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const List scopes = ['scope1', 'scope2']; + when(mockPlatform.authenticate(any)).thenAnswer((_) async => + const AuthenticationResults( + user: defaultUser, + authenticationTokens: + AuthenticationTokenData(idToken: 'idToken'))); + + await googleSignIn.initialize(); + await googleSignIn.authenticate(scopeHint: scopes); + + final VerificationResult verification = + verify(mockPlatform.authenticate(captureAny)); + final AuthenticateParameters params = + verification.captured[0] as AuthenticateParameters; + expect(params.scopeHint, scopes); + }); + + test('returns successful authentication', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const String idToken = 'idToken'; + when(mockPlatform.authenticate(any)).thenAnswer((_) async => + const AuthenticationResults( + user: defaultUser, + authenticationTokens: AuthenticationTokenData(idToken: idToken))); + + final GoogleSignInAccount signIn = await googleSignIn.authenticate(); + expect(signIn.displayName, defaultUser.displayName); + expect(signIn.email, defaultUser.email); + expect(signIn.id, defaultUser.id); + expect(signIn.photoUrl, defaultUser.photoUrl); + expect(signIn.authentication.idToken, idToken); + }); + + test('reports exceptions', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); + when(mockPlatform.authenticate(any)).thenThrow(exception); + + await googleSignIn.initialize(); + expect( + googleSignIn.authenticate(), + throwsA(isA().having( + (GoogleSignInException e) => e.code, + 'code', + GoogleSignInExceptionCode.interrupted))); + }); + }); + group('authorizationForScopes', () { test('passes expected paramaters when called for a user', () async { final GoogleSignIn googleSignIn = GoogleSignIn.instance; From 9c9923ca82bcd898a3ee38e444664955a4904670 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 13:31:32 -0400 Subject: [PATCH 31/52] Re-add web asserts for scope spaces --- .../google_sign_in_web_test.dart | 24 +++++++++++++++++++ .../lib/google_sign_in_web.dart | 13 ++++++++++ 2 files changed, 37 insertions(+) diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart index 9ffff117dee..4c7a4f69506 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -237,6 +237,18 @@ void main() { expect(arguments.elementAt(1), true); expect(arguments.elementAt(2), someUserId); }); + + testWidgets('asserts no scopes have any spaces', (_) async { + expect( + plugin.clientAuthorizationTokensForScopes( + const ClientAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: ['bad scope', ...scopes], + userId: 'user', + email: 'someone@example.com', + promptIfUnauthorized: true))), + throwsAssertionError); + }); }); group('serverAuthorizationTokensForScopes', () { @@ -278,6 +290,18 @@ void main() { expect( passedRequest.promptIfUnauthorized, request.promptIfUnauthorized); }); + + testWidgets('asserts no scopes have any spaces', (_) async { + expect( + plugin.serverAuthorizationTokensForScopes( + const ServerAuthorizationTokensForScopesParameters( + request: AuthorizationRequestDetails( + scopes: ['bad scope', ...scopes], + userId: 'user', + email: 'someone@example.com', + promptIfUnauthorized: true))), + throwsAssertionError); + }); }); }); 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 e897f13e9c8..5e24c9f7bc2 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 @@ -186,6 +186,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params) async { await initialized; + _validateScopes(params.request.scopes); final String? token = await _gisClient.requestScopes(params.request.scopes, promptIfUnauthorized: params.request.promptIfUnauthorized, @@ -199,6 +200,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future serverAuthorizationTokensForScopes( ServerAuthorizationTokensForScopesParameters params) async { await initialized; + _validateScopes(params.request.scopes); // There is no way to know whether the flow will prompt in advance, so // always return null if prompting isn't allowed. @@ -212,6 +214,17 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { : ServerAuthorizationTokenData(serverAuthCode: code); } + void _validateScopes(List scopes) { + // Scope lists are space-delimited in the underlying implementation, so + // scopes must not contain any spaces. + // https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#redirecting + 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.'); + } + @override Stream get authenticationEvents => _authenticationController.stream; From b44ff39b694610ef7b173a89fa8b1a98aba309af Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 13:35:19 -0400 Subject: [PATCH 32/52] Remove unused People code --- .../google_sign_in_web_test.dart | 7 +- .../example/integration_test/people_test.dart | 129 --------------- .../example/integration_test/src/person.dart | 66 -------- .../google_sign_in_web/lib/src/people.dart | 152 ------------------ 4 files changed, 3 insertions(+), 351 deletions(-) delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart delete mode 100644 packages/google_sign_in/google_sign_in_web/lib/src/people.dart diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart index 4c7a4f69506..e801f5eb671 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -8,14 +8,12 @@ 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_web/google_sign_in_web.dart'; import 'package:google_sign_in_web/src/gis_client.dart'; -import 'package:google_sign_in_web/src/people.dart'; import 'package:integration_test/integration_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart' as mockito; import 'package:web/web.dart' as web; import 'google_sign_in_web_test.mocks.dart'; -import 'src/person.dart'; // Mock GisSdkClient so we can simulate any response from the JS side. @GenerateMocks([], customMocks: >[ @@ -321,8 +319,9 @@ void main() { final Future event = plugin.authenticationEvents.first; - final AuthenticationEvent expected = AuthenticationEventSignIn( - user: extractUserData(person)!, + const AuthenticationEvent expected = AuthenticationEventSignIn( + user: + GoogleSignInUserData(email: 'someone@example.com', id: 'user_id'), authenticationTokens: const AuthenticationTokenData(idToken: 'someToken')); controller.add(expected); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart deleted file mode 100644 index 5ed1244e8d8..00000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/people_test.dart +++ /dev/null @@ -1,129 +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 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_identity_services_web/oauth2.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/src/people.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart' as http_test; -import 'package:integration_test/integration_test.dart'; - -import 'src/jsify_as.dart'; -import 'src/person.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('requestUserData', () { - const String expectedAccessToken = '3xp3c73d_4cc355_70k3n'; - - final TokenResponse fakeToken = jsifyAs({ - 'token_type': 'Bearer', - 'access_token': expectedAccessToken, - }); - - testWidgets('happy case', (_) async { - final Completer accessTokenCompleter = Completer(); - - final http.Client mockClient = http_test.MockClient( - (http.Request request) async { - accessTokenCompleter.complete(request.headers['Authorization']); - - return http.Response( - jsonEncode(person), - 200, - headers: {'content-type': 'application/json'}, - ); - }, - ); - - final GoogleSignInUserData? user = await requestUserData( - fakeToken, - overrideClient: mockClient, - ); - - expect(user, isNotNull); - expect(user!.email, expectedPersonEmail); - expect(user.id, expectedPersonId); - expect(user.displayName, expectedPersonName); - expect(user.photoUrl, expectedPersonPhoto); - expect( - accessTokenCompleter.future, - completion('Bearer $expectedAccessToken'), - ); - }); - - testWidgets('Unauthorized request - throws exception', (_) async { - final http.Client mockClient = http_test.MockClient( - (http.Request request) async { - return http.Response( - 'Unauthorized', - 403, - ); - }, - ); - - expect(() async { - await requestUserData( - fakeToken, - overrideClient: mockClient, - ); - }, throwsA(isA())); - }); - }); - - group('extractUserData', () { - testWidgets('happy case', (_) async { - final GoogleSignInUserData? user = extractUserData(person); - - expect(user, isNotNull); - expect(user!.email, expectedPersonEmail); - expect(user.id, expectedPersonId); - expect(user.displayName, expectedPersonName); - expect(user.photoUrl, expectedPersonPhoto); - }); - - testWidgets('no name/photo - keeps going', (_) async { - final Map personWithoutSomeData = - mapWithoutKeys(person, { - 'names', - 'photos', - }); - - final GoogleSignInUserData? user = extractUserData(personWithoutSomeData); - - expect(user, isNotNull); - expect(user!.email, expectedPersonEmail); - expect(user.id, expectedPersonId); - expect(user.displayName, isNull); - expect(user.photoUrl, isNull); - }); - - testWidgets('no userId - throws assertion error', (_) async { - final Map personWithoutId = - mapWithoutKeys(person, { - 'resourceName', - }); - - expect(() { - extractUserData(personWithoutId); - }, throwsAssertionError); - }); - - testWidgets('no email - throws assertion error', (_) async { - final Map personWithoutEmail = - mapWithoutKeys(person, { - 'emailAddresses', - }); - - expect(() { - extractUserData(personWithoutEmail); - }, throwsAssertionError); - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart deleted file mode 100644 index 2525596eabe..00000000000 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/src/person.dart +++ /dev/null @@ -1,66 +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. - -const String expectedPersonId = '1234567890'; -const String expectedPersonName = 'Vincent Adultman'; -const String expectedPersonEmail = 'adultman@example.com'; -const String expectedPersonPhoto = - 'https://thispersondoesnotexist.com/image?x=.jpg'; - -/// A subset of https://developers.google.com/people/api/rest/v1/people#Person. -final Map person = { - 'resourceName': 'people/$expectedPersonId', - 'emailAddresses': [ - { - 'metadata': { - 'primary': false, - }, - 'value': 'bad@example.com', - }, - { - 'metadata': {}, - 'value': 'nope@example.com', - }, - { - 'metadata': { - 'primary': true, - }, - 'value': expectedPersonEmail, - }, - ], - 'names': [ - { - 'metadata': { - 'primary': true, - }, - 'displayName': expectedPersonName, - }, - { - 'metadata': { - 'primary': false, - }, - 'displayName': 'Fakey McFakeface', - }, - ], - 'photos': [ - { - 'metadata': { - 'primary': true, - }, - 'url': expectedPersonPhoto, - }, - ], -}; - -/// Returns a copy of [map] without the [keysToRemove]. -T mapWithoutKeys>( - T map, - Set keysToRemove, -) { - return Map.fromEntries( - map.entries.where((MapEntry entry) { - return !keysToRemove.contains(entry.key); - }), - ) as T; -} diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/people.dart b/packages/google_sign_in/google_sign_in_web/lib/src/people.dart deleted file mode 100644 index 528dc89b1a7..00000000000 --- a/packages/google_sign_in/google_sign_in_web/lib/src/people.dart +++ /dev/null @@ -1,152 +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:convert'; - -import 'package:flutter/foundation.dart'; -import 'package:google_identity_services_web/oauth2.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:http/http.dart' as http; - -/// Basic scopes for self-id -const List scopes = [ - 'https://www.googleapis.com/auth/userinfo.profile', - 'https://www.googleapis.com/auth/userinfo.email', -]; - -/// People API to return my profile info... -const String MY_PROFILE = 'https://content-people.googleapis.com/v1/people/me' - '?sources=READ_SOURCE_TYPE_PROFILE' - '&personFields=photos%2Cnames%2CemailAddresses'; - -/// Requests user data from the People API using the given [tokenResponse]. -Future requestUserData( - TokenResponse tokenResponse, { - @visibleForTesting http.Client? overrideClient, -}) async { - // Request my profile from the People API. - final Map person = await _doRequest( - MY_PROFILE, - tokenResponse, - overrideClient: overrideClient, - ); - - // Now transform the Person response into a GoogleSignInUserData. - return extractUserData(person); -} - -/// Extracts user data from a Person resource. -/// -/// See: https://developers.google.com/people/api/rest/v1/people#Person -GoogleSignInUserData? extractUserData(Map json) { - final String? userId = _extractUserId(json); - final String? email = _extractPrimaryField( - json['emailAddresses'] as List?, - 'value', - ); - - assert(userId != null); - assert(email != null); - - return GoogleSignInUserData( - id: userId!, - email: email!, - displayName: _extractPrimaryField( - json['names'] as List?, - 'displayName', - ), - photoUrl: _extractPrimaryField( - json['photos'] as List?, - 'url', - ), - // Synthetic user data doesn't contain an idToken! - ); -} - -/// Extracts the ID from a Person resource. -/// -/// The User ID looks like this: -/// { -/// 'resourceName': 'people/PERSON_ID', -/// ... -/// } -String? _extractUserId(Map profile) { - final String? resourceName = profile['resourceName'] as String?; - return resourceName?.split('/').last; -} - -/// Extracts the [fieldName] marked as 'primary' from a list of [values]. -/// -/// Values can be one of: -/// * `emailAddresses` -/// * `names` -/// * `photos` -/// -/// From a Person object. -T? _extractPrimaryField(List? values, String fieldName) { - if (values != null) { - for (final Object? value in values) { - if (value != null && value is Map) { - final bool isPrimary = _extractPath( - value, - path: ['metadata', 'primary'], - defaultValue: false, - ); - if (isPrimary) { - return value[fieldName] as T?; - } - } - } - } - - return null; -} - -/// Attempts to get the property in [path] of type `T` from a deeply nested [source]. -/// -/// Returns [default] if the property is not found. -T _extractPath( - Map source, { - required List path, - required T defaultValue, -}) { - final String valueKey = path.removeLast(); - Object? data = source; - for (final String key in path) { - if (data != null && data is Map) { - data = data[key]; - } else { - break; - } - } - if (data != null && data is Map) { - return (data[valueKey] ?? defaultValue) as T; - } else { - return defaultValue; - } -} - -/// Gets from [url] with an authorization header defined by [token]. -/// -/// Attempts to [jsonDecode] the result. -Future> _doRequest( - String url, - TokenResponse token, { - http.Client? overrideClient, -}) async { - final Uri uri = Uri.parse(url); - final http.Client client = overrideClient ?? http.Client(); - try { - final http.Response response = - await client.get(uri, headers: { - 'Authorization': '${token.token_type} ${token.access_token}', - }); - if (response.statusCode != 200) { - throw http.ClientException(response.body, uri); - } - return jsonDecode(response.body) as Map; - } finally { - client.close(); - } -} From 8640443748443de890d13add1b9ea6bff120b9d5 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 14:10:56 -0400 Subject: [PATCH 33/52] Restructure options slightly --- .../googlesignin/GoogleSignInPlugin.java | 5 +- .../flutter/plugins/googlesignin/Messages.kt | 69 ++++++++++---- .../googlesignin/GoogleSignInTest.java | 90 +++++++++++++------ .../lib/google_sign_in_android.dart | 47 +++++++--- .../lib/src/messages.g.dart | 71 ++++++++++----- .../pigeons/messages.dart | 20 ++++- 6 files changed, 220 insertions(+), 82 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 53a1e48cc78..53b9f64deb0 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -224,10 +224,11 @@ public void getCredential( } requestBuilder.addCredentialOption(optionBuilder.build()); } else { + GetCredentialRequestGoogleIdOptionParams optionParams = params.getGoogleIdOptionParams(); GetGoogleIdOption.Builder optionBuilder = new GetGoogleIdOption.Builder() - .setFilterByAuthorizedAccounts(params.getFilterToAuthorized()) - .setAutoSelectEnabled(params.getAutoSelectEnabled()) + .setFilterByAuthorizedAccounts(optionParams.getFilterToAuthorized()) + .setAutoSelectEnabled(optionParams.getAutoSelectEnabled()) .setServerClientId(serverClientId); if (nonce != null) { optionBuilder.setNonce(nonce); diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt index a90f740d658..783063d22f7 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/kotlin/io/flutter/plugins/googlesignin/Messages.kt @@ -151,34 +151,56 @@ data class GetCredentialRequestParams( * potential sign-in. */ val useButtonFlow: Boolean, - val filterToAuthorized: Boolean, - val autoSelectEnabled: Boolean, + /** + * Parameters specific to GetGoogleIdOption. + * + * Ignored if useButtonFlow is true. + */ + val googleIdOptionParams: GetCredentialRequestGoogleIdOptionParams, val serverClientId: String? = null, val nonce: String? = null ) { companion object { fun fromList(pigeonVar_list: List): GetCredentialRequestParams { val useButtonFlow = pigeonVar_list[0] as Boolean - val filterToAuthorized = pigeonVar_list[1] as Boolean - val autoSelectEnabled = pigeonVar_list[2] as Boolean - val serverClientId = pigeonVar_list[3] as String? - val nonce = pigeonVar_list[4] as String? - return GetCredentialRequestParams( - useButtonFlow, filterToAuthorized, autoSelectEnabled, serverClientId, nonce) + val googleIdOptionParams = pigeonVar_list[1] as GetCredentialRequestGoogleIdOptionParams + val serverClientId = pigeonVar_list[2] as String? + val nonce = pigeonVar_list[3] as String? + return GetCredentialRequestParams(useButtonFlow, googleIdOptionParams, serverClientId, nonce) } } fun toList(): List { return listOf( useButtonFlow, - filterToAuthorized, - autoSelectEnabled, + googleIdOptionParams, serverClientId, nonce, ) } } +/** Generated class from Pigeon that represents data sent in messages. */ +data class GetCredentialRequestGoogleIdOptionParams( + val filterToAuthorized: Boolean, + val autoSelectEnabled: Boolean +) { + companion object { + fun fromList(pigeonVar_list: List): GetCredentialRequestGoogleIdOptionParams { + val filterToAuthorized = pigeonVar_list[0] as Boolean + val autoSelectEnabled = pigeonVar_list[1] as Boolean + return GetCredentialRequestGoogleIdOptionParams(filterToAuthorized, autoSelectEnabled) + } + } + + fun toList(): List { + return listOf( + filterToAuthorized, + autoSelectEnabled, + ) + } +} + /** * Pigeon equivalent of the native GoogleIdTokenCredential. * @@ -365,19 +387,24 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { } 133.toByte() -> { return (readValue(buffer) as? List)?.let { - PlatformGoogleIdTokenCredential.fromList(it) + GetCredentialRequestGoogleIdOptionParams.fromList(it) } } 134.toByte() -> { - return (readValue(buffer) as? List)?.let { GetCredentialFailure.fromList(it) } + return (readValue(buffer) as? List)?.let { + PlatformGoogleIdTokenCredential.fromList(it) + } } 135.toByte() -> { - return (readValue(buffer) as? List)?.let { GetCredentialSuccess.fromList(it) } + return (readValue(buffer) as? List)?.let { GetCredentialFailure.fromList(it) } } 136.toByte() -> { - return (readValue(buffer) as? List)?.let { AuthorizeFailure.fromList(it) } + return (readValue(buffer) as? List)?.let { GetCredentialSuccess.fromList(it) } } 137.toByte() -> { + return (readValue(buffer) as? List)?.let { AuthorizeFailure.fromList(it) } + } + 138.toByte() -> { return (readValue(buffer) as? List)?.let { PlatformAuthorizationResult.fromList(it) } } else -> super.readValueOfType(type, buffer) @@ -402,26 +429,30 @@ private open class MessagesPigeonCodec : StandardMessageCodec() { stream.write(132) writeValue(stream, value.toList()) } - is PlatformGoogleIdTokenCredential -> { + is GetCredentialRequestGoogleIdOptionParams -> { stream.write(133) writeValue(stream, value.toList()) } - is GetCredentialFailure -> { + is PlatformGoogleIdTokenCredential -> { stream.write(134) writeValue(stream, value.toList()) } - is GetCredentialSuccess -> { + is GetCredentialFailure -> { stream.write(135) writeValue(stream, value.toList()) } - is AuthorizeFailure -> { + is GetCredentialSuccess -> { stream.write(136) writeValue(stream, value.toList()) } - is PlatformAuthorizationResult -> { + is AuthorizeFailure -> { stream.write(137) writeValue(stream, value.toList()) } + is PlatformAuthorizationResult -> { + stream.write(138) + writeValue(stream, value.toList()) + } else -> super.writeValue(stream, value) } } diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 92868a89a36..69e1ec7582a 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -122,8 +122,13 @@ public void getGoogleServicesJsonServerClientId_returnsNullIfNotFound() { @Test public void getCredential_returnsAuthenticationInfo() { + GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final String displayName = "Jane User"; final String givenName = "Jane"; @@ -173,9 +178,12 @@ public void getCredential_returnsAuthenticationInfo() { @Test public void getCredential_usesGetSignInWithGoogleOptionForButtonFlow() { GetCredentialRequestParams params = - new GetCredentialRequestParams(true, false, false, "serverClientId", null); + new GetCredentialRequestParams( + true, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); - final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( params, ResultCompat.asCompatCallback( @@ -197,9 +205,12 @@ public void getCredential_usesGetSignInWithGoogleOptionForButtonFlow() { @Test public void getCredential_usesGetGoogleIdOptionForNonButtonFlow() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); - final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( params, ResultCompat.asCompatCallback( @@ -221,9 +232,12 @@ public void getCredential_usesGetGoogleIdOptionForNonButtonFlow() { public void getCredential_passesNonceInButtonFlow() { final String nonce = "nonce"; GetCredentialRequestParams params = - new GetCredentialRequestParams(true, false, false, "serverClientId", nonce); + new GetCredentialRequestParams( + true, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + nonce); - final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( params, ResultCompat.asCompatCallback( @@ -247,9 +261,12 @@ public void getCredential_passesNonceInButtonFlow() { public void getCredential_passesNonceInNonButtonFlow() { final String nonce = "nonce"; GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", nonce); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + nonce); - final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( params, ResultCompat.asCompatCallback( @@ -271,7 +288,8 @@ public void getCredential_passesNonceInNonButtonFlow() { @Test public void getCredential_reportsMissingServerClientId() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, null, null); + new GetCredentialRequestParams( + false, new GetCredentialRequestGoogleIdOptionParams(false, false), null, null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -293,7 +311,11 @@ public void getCredential_reportsMissingServerClientId() { @Test public void getCredential_reportsWrongCredentialType() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -332,7 +354,11 @@ public void getCredential_reportsWrongCredentialType() { @Test public void getCredential_reportsCancellation() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -367,7 +393,11 @@ public void getCredential_reportsCancellation() { @Test public void getCredential_reportsInterrupted() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -402,7 +432,11 @@ public void getCredential_reportsInterrupted() { @Test public void getCredential_reportsProviderConfigurationIssue() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -438,7 +472,11 @@ public void getCredential_reportsProviderConfigurationIssue() { @Test public void getCredential_reportsUnsupported() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -473,7 +511,11 @@ public void getCredential_reportsUnsupported() { @Test public void getCredential_reportsNoCredential() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -508,7 +550,11 @@ public void getCredential_reportsNoCredential() { @Test public void getCredential_reportsUnknown() { GetCredentialRequestParams params = - new GetCredentialRequestParams(false, false, false, "serverClientId", null); + new GetCredentialRequestParams( + false, + new GetCredentialRequestGoogleIdOptionParams(false, false), + "serverClientId", + null); final Boolean[] callbackCalled = new Boolean[1]; plugin.getCredential( @@ -585,8 +631,6 @@ public void authorize_reportsImmediateException() { PlatformAuthorizationRequest params = new PlatformAuthorizationRequest(scopes, null, null, null); - final String accessToken = "accessToken"; - final String serverAuthCode = "serverAuthCode"; when(mockAuthorizationClient.authorize(any())).thenThrow(new RuntimeException()); final Boolean[] callbackCalled = new Boolean[1]; @@ -614,8 +658,6 @@ public void authorize_reportsFailureIfUnauthorizedAndNoPromptAllowed() { PlatformAuthorizationRequest params = new PlatformAuthorizationRequest(scopes, null, null, null); - final String accessToken = "accessToken"; - final String serverAuthCode = "serverAuthCode"; when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); final Boolean[] callbackCalled = new Boolean[1]; @@ -652,8 +694,6 @@ public void authorize_reportsFailureIfUnauthorizedAndNoActivity() { PlatformAuthorizationRequest params = new PlatformAuthorizationRequest(scopes, null, null, null); - final String accessToken = "accessToken"; - final String serverAuthCode = "serverAuthCode"; when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); plugin.setActivity(null); @@ -754,8 +794,6 @@ public void authorize_reportsPendingIntentException() { PlatformAuthorizationRequest params = new PlatformAuthorizationRequest(scopes, null, null, null); - final String accessToken = "accessToken"; - final String serverAuthCode = "serverAuthCode"; when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); try { doThrow(new IntentSender.SendIntentException()) @@ -807,8 +845,6 @@ public void authorize_reportsPostIntentException() { PlatformAuthorizationRequest params = new PlatformAuthorizationRequest(scopes, null, null, null); - final String accessToken = "accessToken"; - final String serverAuthCode = "serverAuthCode"; when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); try { when(mockAuthorizationClient.getAuthorizationResultFromIntent(any())) diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 9b0844bd7cd..4ccd29d1f7d 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -43,16 +43,20 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { AttemptLightweightAuthenticationParameters params) async { // Attempt to auto-sign-in, for single-account or returning users. PlatformGoogleIdTokenCredential? credential = await _authenticate( - filterToAuthorized: true, - autoSelectEnabled: true, useButtonFlow: false, + nonButtonFlowOptions: _LightweightAuthenticationOptions( + filterToAuthorized: true, + autoSelectEnabled: true, + ), ); // If no auto-sign-in is available, potentially prompt for an account via // the bottom sheet flow. credential ??= await _authenticate( - filterToAuthorized: false, - autoSelectEnabled: false, useButtonFlow: false, + nonButtonFlowOptions: _LightweightAuthenticationOptions( + filterToAuthorized: false, + autoSelectEnabled: false, + ), ); return credential == null ? null @@ -64,10 +68,13 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { AuthenticateParameters params) async { // Attempt to authorize with minimal interaction. final PlatformGoogleIdTokenCredential? credential = await _authenticate( - filterToAuthorized: false, - autoSelectEnabled: false, useButtonFlow: true, throwForNoAuth: true, + // Ignored, since useButtonFlow is true. + nonButtonFlowOptions: _LightweightAuthenticationOptions( + filterToAuthorized: false, + autoSelectEnabled: false, + ), ); // It's not clear from the documentation if this can happen; if it does, // no information is available @@ -113,16 +120,16 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { } Future _authenticate({ - required bool filterToAuthorized, - required bool autoSelectEnabled, required bool useButtonFlow, + required _LightweightAuthenticationOptions nonButtonFlowOptions, bool throwForNoAuth = false, }) async { final GetCredentialResult authnResult = await _hostApi.getCredential( GetCredentialRequestParams( - filterToAuthorized: filterToAuthorized, - autoSelectEnabled: autoSelectEnabled, useButtonFlow: useButtonFlow, + googleIdOptionParams: GetCredentialRequestGoogleIdOptionParams( + filterToAuthorized: nonButtonFlowOptions.filterToAuthorized, + autoSelectEnabled: nonButtonFlowOptions.autoSelectEnabled), serverClientId: _serverClientId, nonce: _nonce)); switch (authnResult) { @@ -262,3 +269,23 @@ String? _idFromIdToken(String idToken) { } return null; } + +/// Options specific to authentication with the lightweight authentication +/// flow, rather than the explict user-requested login flow. +/// +/// These correspond to builder options specific to GetGoogleIdOption on the +/// platform side. +class _LightweightAuthenticationOptions { + _LightweightAuthenticationOptions({ + required this.filterToAuthorized, + required this.autoSelectEnabled, + }); + + /// If true, only allows selection of accounts that have already authorized + /// the app. + bool filterToAuthorized; + + /// If true, automatically selects an account if there is only one + /// authorized account and no additional user action is required. + bool autoSelectEnabled; +} diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart index 8bd8c6431b3..240e2a56b11 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -117,8 +117,7 @@ class PlatformAuthorizationRequest { class GetCredentialRequestParams { GetCredentialRequestParams({ required this.useButtonFlow, - required this.filterToAuthorized, - required this.autoSelectEnabled, + required this.googleIdOptionParams, this.serverClientId, this.nonce, }); @@ -129,9 +128,10 @@ class GetCredentialRequestParams { /// sign-in. bool useButtonFlow; - bool filterToAuthorized; - - bool autoSelectEnabled; + /// Parameters specific to GetGoogleIdOption. + /// + /// Ignored if useButtonFlow is true. + GetCredentialRequestGoogleIdOptionParams googleIdOptionParams; String? serverClientId; @@ -140,8 +140,7 @@ class GetCredentialRequestParams { Object encode() { return [ useButtonFlow, - filterToAuthorized, - autoSelectEnabled, + googleIdOptionParams, serverClientId, nonce, ]; @@ -151,10 +150,36 @@ class GetCredentialRequestParams { result as List; return GetCredentialRequestParams( useButtonFlow: result[0]! as bool, - filterToAuthorized: result[1]! as bool, - autoSelectEnabled: result[2]! as bool, - serverClientId: result[3] as String?, - nonce: result[4] as String?, + googleIdOptionParams: + result[1]! as GetCredentialRequestGoogleIdOptionParams, + serverClientId: result[2] as String?, + nonce: result[3] as String?, + ); + } +} + +class GetCredentialRequestGoogleIdOptionParams { + GetCredentialRequestGoogleIdOptionParams({ + required this.filterToAuthorized, + required this.autoSelectEnabled, + }); + + bool filterToAuthorized; + + bool autoSelectEnabled; + + Object encode() { + return [ + filterToAuthorized, + autoSelectEnabled, + ]; + } + + static GetCredentialRequestGoogleIdOptionParams decode(Object result) { + result as List; + return GetCredentialRequestGoogleIdOptionParams( + filterToAuthorized: result[0]! as bool, + autoSelectEnabled: result[1]! as bool, ); } } @@ -362,21 +387,24 @@ class _PigeonCodec extends StandardMessageCodec { } else if (value is GetCredentialRequestParams) { buffer.putUint8(132); writeValue(buffer, value.encode()); - } else if (value is PlatformGoogleIdTokenCredential) { + } else if (value is GetCredentialRequestGoogleIdOptionParams) { buffer.putUint8(133); writeValue(buffer, value.encode()); - } else if (value is GetCredentialFailure) { + } else if (value is PlatformGoogleIdTokenCredential) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is GetCredentialSuccess) { + } else if (value is GetCredentialFailure) { buffer.putUint8(135); writeValue(buffer, value.encode()); - } else if (value is AuthorizeFailure) { + } else if (value is GetCredentialSuccess) { buffer.putUint8(136); writeValue(buffer, value.encode()); - } else if (value is PlatformAuthorizationResult) { + } else if (value is AuthorizeFailure) { buffer.putUint8(137); writeValue(buffer, value.encode()); + } else if (value is PlatformAuthorizationResult) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -396,14 +424,17 @@ class _PigeonCodec extends StandardMessageCodec { case 132: return GetCredentialRequestParams.decode(readValue(buffer)!); case 133: - return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!); + return GetCredentialRequestGoogleIdOptionParams.decode( + readValue(buffer)!); case 134: - return GetCredentialFailure.decode(readValue(buffer)!); + return PlatformGoogleIdTokenCredential.decode(readValue(buffer)!); case 135: - return GetCredentialSuccess.decode(readValue(buffer)!); + return GetCredentialFailure.decode(readValue(buffer)!); case 136: - return AuthorizeFailure.decode(readValue(buffer)!); + return GetCredentialSuccess.decode(readValue(buffer)!); case 137: + return AuthorizeFailure.decode(readValue(buffer)!); + case 138: return PlatformAuthorizationResult.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart index af39cb3c226..b7cdd267a7e 100644 --- a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -33,8 +33,7 @@ class PlatformAuthorizationRequest { class GetCredentialRequestParams { GetCredentialRequestParams({ required this.useButtonFlow, - required this.filterToAuthorized, - required this.autoSelectEnabled, + required this.googleIdOptionParams, this.serverClientId, this.nonce, }); @@ -45,12 +44,25 @@ class GetCredentialRequestParams { /// sign-in. bool useButtonFlow; - bool filterToAuthorized; - bool autoSelectEnabled; + /// Parameters specific to GetGoogleIdOption. + /// + /// Ignored if useButtonFlow is true. + GetCredentialRequestGoogleIdOptionParams googleIdOptionParams; + String? serverClientId; String? nonce; } +class GetCredentialRequestGoogleIdOptionParams { + GetCredentialRequestGoogleIdOptionParams({ + required this.filterToAuthorized, + required this.autoSelectEnabled, + }); + + bool filterToAuthorized; + bool autoSelectEnabled; +} + /// Pigeon equivalent of the native GoogleIdTokenCredential. class PlatformGoogleIdTokenCredential { String? displayName; From 86a7a1d20ba240bfbe3149fe2258c904db0211f6 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 16:08:27 -0400 Subject: [PATCH 34/52] Add TODO issue link --- .../lib/google_sign_in_android.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 4ccd29d1f7d..908000fa8a7 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -94,8 +94,9 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { @override Future disconnect(DisconnectParams params) async { // TODO(stuartmorgan): Implement this once Credential Manager adds the - // necessary API (or temporarily implement it with the deprecated SDK). - + // necessary API (or temporarily implement it with the deprecated SDK if + // it becomes a significant issue before the API is added). + // https://github.com/flutter/flutter/issues/169612 await signOut(const SignOutParams()); } @@ -119,6 +120,13 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { : ServerAuthorizationTokenData(serverAuthCode: serverAuthCode); } + /// Authenticates with the platform credential manager using either the + /// button-initiated flow (useButtonFlow = true, nonButtonFlowOptions are + /// ignored), or the lightweight flow (useButtonFlow = false, + /// nonButtonFlowOptions are used to configure the request). + /// + /// See https://developer.android.com/identity/sign-in/credential-manager-siwg + /// for discussion of the two different flows Future _authenticate({ required bool useButtonFlow, required _LightweightAuthenticationOptions nonButtonFlowOptions, From 4fb7feaa147f63c6f7f0dfb3be6357ed3bf5b320 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 16:09:22 -0400 Subject: [PATCH 35/52] Add activity lifecycle tests --- .../googlesignin/GoogleSignInPlugin.java | 27 ++++++----- .../googlesignin/GoogleSignInTest.java | 48 +++++++++++++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index 53b9f64deb0..edab543e932 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -58,16 +58,21 @@ public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware { private @Nullable BinaryMessenger messenger; private ActivityPluginBinding activityPluginBinding; - @VisibleForTesting - public void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) { - this.messenger = messenger; - delegate = + private void initInstance(@NonNull BinaryMessenger messenger, @NonNull Context context) { + initWithDelegate( + messenger, new Delegate( context, (@NonNull Context c) -> CredentialManager.create(c), (@NonNull Context c) -> Identity.getAuthorizationClient(c), - (@Nullable Credential credential) -> - GoogleIdTokenCredential.createFrom(credential.getData())); + (@NonNull Credential credential) -> + GoogleIdTokenCredential.createFrom(credential.getData()))); + } + + @VisibleForTesting + void initWithDelegate(@NonNull BinaryMessenger messenger, @NonNull Delegate delegate) { + this.messenger = messenger; + this.delegate = delegate; GoogleSignInApi.Companion.setUp(messenger, delegate); } @@ -374,11 +379,11 @@ public void authorize( activity.startIntentSenderForResult( pendingIntent.getIntentSender(), REQUEST_CODE_AUTHORIZE, - null, - 0, - 0, - 0, - null); + /* fillInIntent */ null, + /* flagsMask */ 0, + /* flagsValue */ 0, + /* extraFlags */ 0, + /* options */ null); } catch (IntentSender.SendIntentException e) { pendingAuthorizationCallback = null; ResultUtilsKt.completeWithAuthorizeFailure( diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 69e1ec7582a..5871f41efa9 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -45,6 +45,7 @@ import com.google.android.libraries.identity.googleid.GetGoogleIdOption; import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption; import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -59,6 +60,7 @@ public class GoogleSignInTest { @Mock Context mockContext; @Mock Resources mockResources; @Mock Activity mockActivity; + @Mock ActivityPluginBinding mockActivityPluginBinding; @Mock PendingIntent mockAuthorizationIntent; @Mock IntentSender mockAuthorizationIntentSender; @Mock AuthorizeResult mockAuthorizeResult; @@ -68,6 +70,9 @@ public class GoogleSignInTest { @Mock GoogleIdTokenCredential mockGoogleCredential; @Mock Task mockAuthorizationTask; + private GoogleSignInPlugin flutterPlugin; + // Technically this is not the plugin, but in practice almost all of the functionality is in this + // class so it is given the simpler name. private GoogleSignInPlugin.Delegate plugin; private AutoCloseable mockCloseable; @@ -82,6 +87,7 @@ public void setUp() { when(mockAuthorizationTask.addOnSuccessListener(any())).thenReturn(mockAuthorizationTask); when(mockAuthorizationTask.addOnFailureListener(any())).thenReturn(mockAuthorizationTask); when(mockAuthorizationIntent.getIntentSender()).thenReturn(mockAuthorizationIntentSender); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockActivity); plugin = new GoogleSignInPlugin.Delegate( @@ -96,6 +102,48 @@ public void tearDown() throws Exception { mockCloseable.close(); } + @Test + public void onAttachedToActivity_updatesDelegate() { + flutterPlugin = new GoogleSignInPlugin(); + flutterPlugin.initWithDelegate(mock(io.flutter.plugin.common.BinaryMessenger.class), plugin); + flutterPlugin.onAttachedToActivity(mockActivityPluginBinding); + + verify(mockActivityPluginBinding).addActivityResultListener(plugin); + assertEquals(mockActivity, plugin.getActivity()); + } + + @Test + public void onDetachedFromActivity_updatesDelegate() { + flutterPlugin = new GoogleSignInPlugin(); + flutterPlugin.initWithDelegate(mock(io.flutter.plugin.common.BinaryMessenger.class), plugin); + flutterPlugin.onAttachedToActivity(mockActivityPluginBinding); + flutterPlugin.onDetachedFromActivity(); + + verify(mockActivityPluginBinding).removeActivityResultListener(plugin); + assertNull(plugin.getActivity()); + } + + @Test + public void onReattachedToActivityForConfigChanges_updatesDelegate() { + flutterPlugin = new GoogleSignInPlugin(); + flutterPlugin.initWithDelegate(mock(io.flutter.plugin.common.BinaryMessenger.class), plugin); + flutterPlugin.onReattachedToActivityForConfigChanges(mockActivityPluginBinding); + + verify(mockActivityPluginBinding).addActivityResultListener(plugin); + assertEquals(mockActivity, plugin.getActivity()); + } + + @Test + public void onDetachedFromActivityForConfigChanges_updatesDelegate() { + flutterPlugin = new GoogleSignInPlugin(); + flutterPlugin.initWithDelegate(mock(io.flutter.plugin.common.BinaryMessenger.class), plugin); + flutterPlugin.onAttachedToActivity(mockActivityPluginBinding); + flutterPlugin.onDetachedFromActivityForConfigChanges(); + + verify(mockActivityPluginBinding).removeActivityResultListener(plugin); + assertNull(plugin.getActivity()); + } + @Test public void getGoogleServicesJsonServerClientId_loadsServerClientIdFromResources() { final String packageName = "fakePackageName"; From 563680cc29c374b06d8f781553c5326dc7a451f3 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 16:30:21 -0400 Subject: [PATCH 36/52] Add authorize param tests --- .../googlesignin/GoogleSignInTest.java | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 5871f41efa9..2f265590246 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -5,6 +5,7 @@ package io.flutter.plugins.googlesignin; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -37,6 +38,7 @@ import androidx.credentials.exceptions.GetCredentialUnsupportedException; import androidx.credentials.exceptions.NoCredentialException; import com.google.android.gms.auth.api.identity.AuthorizationClient; +import com.google.android.gms.auth.api.identity.AuthorizationRequest; import com.google.android.gms.auth.api.identity.AuthorizationResult; import com.google.android.gms.common.api.ApiException; import com.google.android.gms.common.api.Status; @@ -170,7 +172,6 @@ public void getGoogleServicesJsonServerClientId_returnsNullIfNotFound() { @Test public void getCredential_returnsAuthenticationInfo() { - GetCredentialRequestParams params = new GetCredentialRequestParams( false, @@ -634,6 +635,66 @@ public void getCredential_reportsUnknown() { assertTrue(callbackCalled[0]); } + @Test + public void authorize_passesNullParamaters() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, null, null, null); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + + plugin.authorize( + params, + false, + ResultCompat.asCompatCallback( + reply -> { + return null; + })); + + ArgumentCaptor authRequestCaptor = + ArgumentCaptor.forClass(AuthorizationRequest.class); + verify(mockAuthorizationClient).authorize(authRequestCaptor.capture()); + + AuthorizationRequest request = authRequestCaptor.getValue(); + assertNull(request.getHostedDomain()); + assertNull(request.getServerClientId()); + assertNull(request.getAccount()); + } + + @Test + public void authorize_passesOptionalParameters() { + final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); + final String hostedDomain = "example.com"; + final String accountEmail = "someone@example.com"; + final String serverClientId = "serverClientId"; + PlatformAuthorizationRequest params = + new PlatformAuthorizationRequest(scopes, hostedDomain, accountEmail, serverClientId); + + final String accessToken = "accessToken"; + final String serverAuthCode = "serverAuthCode"; + when(mockAuthorizationClient.authorize(any())).thenReturn(mockAuthorizationTask); + + plugin.authorize( + params, + false, + ResultCompat.asCompatCallback( + reply -> { + return null; + })); + + ArgumentCaptor authRequestCaptor = + ArgumentCaptor.forClass(AuthorizationRequest.class); + verify(mockAuthorizationClient).authorize(authRequestCaptor.capture()); + + AuthorizationRequest request = authRequestCaptor.getValue(); + assertEquals(hostedDomain, request.getHostedDomain()); + assertEquals(serverClientId, request.getServerClientId()); + // Account is mostly opaque, so just verify that one was set if an email was provided. + assertNotNull(request.getAccount()); + } + @Test public void authorize_returnsImmediateResult() { final List scopes = new ArrayList<>(Arrays.asList("scope1", "scope1")); From dab117fd47e0cdca2314f0bd3a495e840822c493 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 28 May 2025 22:03:53 -0400 Subject: [PATCH 37/52] Test safety check, better README --- .../google_sign_in_android/README.md | 15 ++++++------- .../googlesignin/GoogleSignInTest.java | 21 +++++++++++++++---- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/README.md b/packages/google_sign_in/google_sign_in_android/README.md index bd91f042682..17aa417740d 100644 --- a/packages/google_sign_in/google_sign_in_android/README.md +++ b/packages/google_sign_in/google_sign_in_android/README.md @@ -16,22 +16,19 @@ should add it to your `pubspec.yaml` as usual. ## Integration -To use Google Sign-In, you'll need to -[register your application](https://firebase.google.com/docs/android/setup). -If you are using Google Cloud Platform directly, rather than Firebase, you will -need to register both an Android application and a web application in the -[Google Cloud Platform API manager](https://console.developers.google.com/). +To use Google Sign-In, you'll need to register your application, either +[using Firebase](https://firebase.google.com/docs/android/setup), or +[directly with Google Cloud Platform](https://developer.android.com/identity/sign-in/credential-manager-siwg#set-google). * If you are use the `google-services.json` file and Gradle-based registration system, no identifiers need to be provided in Dart when initializing the - `GoogleSignIn` instance. + `GoogleSignIn` instance when running on Android. * If you are not using `google-services.json`, you need to pass the client ID of the *web* application you registered as the `serverClientId` when initializing the `GoogleSignIn` instance. -Make sure you've filled out all required fields in the console for -[OAuth consent screen](https://console.developers.google.com/apis/credentials/consent). -Otherwise, you may encounter `APIException` errors. +If you encounter `APIException` errors, double-check that you have followed all +of the registration steps in the instructions above. You will also need to enable any OAuth APIs that you want, using the [Google Cloud Platform API manager](https://console.developers.google.com/). For diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 2f265590246..dbfd32e6d61 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -237,7 +237,8 @@ public void getCredential_usesGetSignInWithGoogleOptionForButtonFlow() { params, ResultCompat.asCompatCallback( reply -> { - // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + // No-op, since this test doesn't trigger the getCredentialsAsync callback that would + // call this. return null; })); @@ -264,7 +265,9 @@ public void getCredential_usesGetGoogleIdOptionForNonButtonFlow() { params, ResultCompat.asCompatCallback( reply -> { - // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -291,7 +294,9 @@ public void getCredential_passesNonceInButtonFlow() { params, ResultCompat.asCompatCallback( reply -> { - // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -320,7 +325,9 @@ public void getCredential_passesNonceInNonButtonFlow() { params, ResultCompat.asCompatCallback( reply -> { - // This is never called, since this test doesn't trigger the getCredentialsAsync callback. + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -650,6 +657,9 @@ public void authorize_passesNullParamaters() { false, ResultCompat.asCompatCallback( reply -> { + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -681,6 +691,9 @@ public void authorize_passesOptionalParameters() { false, ResultCompat.asCompatCallback( reply -> { + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); From 81f59f7c7d9be19ebccd071b704c7f1adabda2a5 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 08:25:12 -0400 Subject: [PATCH 38/52] Analyzer --- .../google_sign_in/test/google_sign_in_test.dart | 2 -- .../example/integration_test/google_sign_in_web_test.dart | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index 7104c9040cb..be5f0ccb705 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -115,8 +115,6 @@ void main() { await eventFuture; fail('The stream should throw before returning an event.'); } on GoogleSignInException catch (e) { - print(e); - print(exception); expect(e, exception); } }); diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart index e801f5eb671..adbbb6babf4 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -322,8 +322,7 @@ void main() { const AuthenticationEvent expected = AuthenticationEventSignIn( user: GoogleSignInUserData(email: 'someone@example.com', id: 'user_id'), - authenticationTokens: - const AuthenticationTokenData(idToken: 'someToken')); + authenticationTokens: AuthenticationTokenData(idToken: 'someToken')); controller.add(expected); expect(await event, expected, From 6b76be003bc623bc907754bbdac6ff07038459cc Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 08:26:09 -0400 Subject: [PATCH 39/52] Java autoformat --- .../googlesignin/GoogleSignInTest.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index dbfd32e6d61..88ac2f3c05e 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -266,8 +266,8 @@ public void getCredential_usesGetGoogleIdOptionForNonButtonFlow() { ResultCompat.asCompatCallback( reply -> { // This test doesn't trigger the getCredentialsAsync callback that would call this, - // so if this is reached something has gone wrong. - fail(); + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -294,9 +294,9 @@ public void getCredential_passesNonceInButtonFlow() { params, ResultCompat.asCompatCallback( reply -> { - // This test doesn't trigger the getCredentialsAsync callback that would call this, - // so if this is reached something has gone wrong. - fail(); + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -325,9 +325,9 @@ public void getCredential_passesNonceInNonButtonFlow() { params, ResultCompat.asCompatCallback( reply -> { - // This test doesn't trigger the getCredentialsAsync callback that would call this, - // so if this is reached something has gone wrong. - fail(); + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -657,9 +657,9 @@ public void authorize_passesNullParamaters() { false, ResultCompat.asCompatCallback( reply -> { - // This test doesn't trigger the getCredentialsAsync callback that would call this, - // so if this is reached something has gone wrong. - fail(); + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); @@ -691,9 +691,9 @@ public void authorize_passesOptionalParameters() { false, ResultCompat.asCompatCallback( reply -> { - // This test doesn't trigger the getCredentialsAsync callback that would call this, - // so if this is reached something has gone wrong. - fail(); + // This test doesn't trigger the getCredentialsAsync callback that would call this, + // so if this is reached something has gone wrong. + fail(); return null; })); From 98173253704dbc06b1bfac4953cbb31d7277f8cc Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 08:27:34 -0400 Subject: [PATCH 40/52] excerpt update --- packages/google_sign_in/google_sign_in/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 199010ef4e1..3543a20c285 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -34,7 +34,9 @@ final GoogleSignIn signIn = GoogleSignIn.instance; unawaited(signIn .initialize(clientId: clientId, serverClientId: serverClientId) .then((_) { - signIn.authenticationEvents.listen(_handleAuthenticationEvent); + signIn.authenticationEvents + .listen(_handleAuthenticationEvent) + .onError(_handleAuthenticationError); /// This example always uses the stream-based approach to determining /// which UI state to show, rather than using the future returned here, From 20a8fc56355f2c5a8f308655761e4c6bed3d2d1d Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 10:26:10 -0400 Subject: [PATCH 41/52] Add stack traces to stream errors --- .../google_sign_in/lib/google_sign_in.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index aa1bdb55435..7cd80fe827a 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -364,10 +364,10 @@ class GoogleSignIn { } return _resolveLightweightAuthenticationAttempt(future, reportAllExceptions: reportAllExceptions); - } catch (e) { + } catch (e, stack) { if (e is GoogleSignInException) { if (_createAuthenticationStreamEvents) { - _authenticationStreamController.addError(e); + _authenticationStreamController.addError(e, stack); } // For exceptions that should not be reported out, just return null. @@ -376,7 +376,7 @@ class GoogleSignIn { return Future.value(); } } - return Future.error(e); + return Future.error(e, stack); } } @@ -403,9 +403,9 @@ class GoogleSignIn { .add(GoogleSignInAuthenticationEventSignIn(user: account)); } return account; - } on GoogleSignInException catch (e) { + } on GoogleSignInException catch (e, stack) { if (_createAuthenticationStreamEvents) { - _authenticationStreamController.addError(e); + _authenticationStreamController.addError(e, stack); } if (_shouldRethrowLightweightAuthenticationException(e, From d6bd8bc955a1cbeb67de27f95da0cd0d18eb021a Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 11:24:42 -0400 Subject: [PATCH 42/52] Handle emulators without sign-in support --- .../integration_test/google_sign_in_test.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart index e6cca19c509..134c6ab5811 100644 --- a/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in_android/example/integration_test/google_sign_in_test.dart @@ -2,6 +2,7 @@ // 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:integration_test/integration_test.dart'; @@ -17,6 +18,16 @@ void main() { testWidgets('Method channel handler is present', (WidgetTester tester) async { // Validate that the native method handler is present and configured. final GoogleSignInPlatform signIn = GoogleSignInPlatform.instance; - await expectLater(signIn.signOut(const SignOutParams()), completes); + try { + await signIn.signOut(const SignOutParams()); + } on PlatformException catch (e) { + if (e.code == 'Clear Failed') { + // This is thrown when running on a device or emulator without Google + // Sign in support. It comes from a call into the SDK, so is fine for + // the purposes of this test since it means the native handler ran. + } else { + fail('Unexpected exception: e'); + } + } }); } From 67668604ce28d8783fcd3e7c5a6182cf94f61228 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 11:48:33 -0400 Subject: [PATCH 43/52] Add another missing stack --- .../google_sign_in/google_sign_in/lib/google_sign_in.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 7cd80fe827a..8244b4d9298 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -473,9 +473,9 @@ class GoogleSignIn { .add(GoogleSignInAuthenticationEventSignIn(user: account)); } return account; - } on GoogleSignInException catch (e) { + } on GoogleSignInException catch (e, stack) { if (_createAuthenticationStreamEvents) { - _authenticationStreamController.addError(e); + _authenticationStreamController.addError(e, stack); } rethrow; } From 8ad0051fa2fac603ba9e523e47cbf4a657157146 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 29 May 2025 13:53:44 -0400 Subject: [PATCH 44/52] Rework the stream tests to make wasm happy --- .../test/google_sign_in_test.dart | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index be5f0ccb705..e635e2c39ef 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -2,6 +2,8 @@ // 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_test/flutter_test.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; @@ -97,7 +99,31 @@ void main() { expect(signIn.user.authentication.idToken, idToken); }); - test('reports exceptions from attemptLightweightAuthentication', () async { + test('reports sync exceptions from attemptLightweightAuthentication', + () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); + when(mockPlatform.attemptLightweightAuthentication(any)) + .thenThrow(exception); + + final Completer errorCompleter = Completer(); + final StreamSubscription subscription = + googleSignIn.authenticationEvents + .handleError((Object e) => errorCompleter.complete(e)) + .listen((_) => fail('The only event should be an error')); + await googleSignIn.initialize(); + // This doesn't throw, since reportAllExceptions is false. + await googleSignIn.attemptLightweightAuthentication(); + + final Object e = await errorCompleter.future; + expect(e, exception); + await subscription.cancel(); + }); + + test('reports async exceptions from attemptLightweightAuthentication', + () async { final GoogleSignIn googleSignIn = GoogleSignIn.instance; const GoogleSignInException exception = @@ -105,18 +131,18 @@ void main() { when(mockPlatform.attemptLightweightAuthentication(any)) .thenAnswer((_) async => throw exception); - final Future eventFuture = - googleSignIn.authenticationEvents.first; + final Completer errorCompleter = Completer(); + final StreamSubscription subscription = + googleSignIn.authenticationEvents + .handleError((Object e) => errorCompleter.complete(e)) + .listen((_) => fail('The only event should be an error')); await googleSignIn.initialize(); // This doesn't throw, since reportAllExceptions is false. await googleSignIn.attemptLightweightAuthentication(); - try { - await eventFuture; - fail('The stream should throw before returning an event.'); - } on GoogleSignInException catch (e) { - expect(e, exception); - } + final Object e = await errorCompleter.future; + expect(e, exception); + await subscription.cancel(); }); test('reports success from authenticate', () async { @@ -141,7 +167,28 @@ void main() { expect(signIn.user.authentication.idToken, idToken); }); - test('reports exceptions from authenticate', () async { + test('reports sync exceptions from authenticate', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + const GoogleSignInException exception = + GoogleSignInException(code: GoogleSignInExceptionCode.interrupted); + when(mockPlatform.authenticate(any)).thenThrow(exception); + + final Completer errorCompleter = Completer(); + final StreamSubscription subscription = + googleSignIn.authenticationEvents + .handleError((Object e) => errorCompleter.complete(e)) + .listen((_) => fail('The only event should be an error')); + await googleSignIn.initialize(); + await expectLater( + googleSignIn.authenticate(), throwsA(isA())); + + final Object e = await errorCompleter.future; + expect(e, exception); + await subscription.cancel(); + }); + + test('reports async exceptions from authenticate', () async { final GoogleSignIn googleSignIn = GoogleSignIn.instance; const GoogleSignInException exception = @@ -149,18 +196,18 @@ void main() { when(mockPlatform.authenticate(any)) .thenAnswer((_) async => throw exception); - final Future eventFuture = - googleSignIn.authenticationEvents.first; + final Completer errorCompleter = Completer(); + final StreamSubscription subscription = + googleSignIn.authenticationEvents + .handleError((Object e) => errorCompleter.complete(e)) + .listen((_) => fail('The only event should be an error')); await googleSignIn.initialize(); await expectLater( googleSignIn.authenticate(), throwsA(isA())); - try { - await eventFuture; - fail('The stream should throw before returning an event.'); - } on GoogleSignInException catch (e) { - expect(e, exception); - } + final Object e = await errorCompleter.future; + expect(e, exception); + await subscription.cancel(); }); test('reports sign out from signOut', () async { From f36a6c8fd97f7c72231e3bf87288d0fc4402e030 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 4 Jun 2025 16:11:49 -0400 Subject: [PATCH 45/52] Minor iOS review fixes --- .../google_sign_in/example/lib/main.dart | 2 +- .../google_sign_in/lib/google_sign_in.dart | 4 ++-- .../FLTGoogleSignInPlugin.m | 21 +++++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index 9f322034976..c194c17d89f 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -78,7 +78,7 @@ class _SignInDemoState extends State { Future _handleAuthenticationEvent( GoogleSignInAuthenticationEvent event) async { // #docregion CheckAuthorization - GoogleSignInAccount? user; + final GoogleSignInAccount? user; // #enddocregion CheckAuthorization switch (event) { case GoogleSignInAuthenticationEventSignIn(): diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 8244b4d9298..1c3fef8b13a 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -437,9 +437,9 @@ class GoogleSignIn { /// Whether or not the current platform supports the [authenticate] method. /// - /// If this returns false, [authenticate] will throw an UnsupportedError if + /// If this returns false, [authenticate] will throw an [UnsupportedError] if /// called. See the platform-specific documentation for the package to - /// determine how authentication his handled. For instance, the platform may + /// determine how authentication is handled. For instance, the platform may /// provide platform-controlled sign-in UI elements that must be used instead /// of application-specific UI. bool supportsAuthenticate() => diff --git a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m index 4f058fa8b09..5412429662c 100644 --- a/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in_ios/darwin/google_sign_in_ios/Sources/google_sign_in_ios/FLTGoogleSignInPlugin.m @@ -99,7 +99,7 @@ @interface FLTGoogleSignInPlugin () @property(nonatomic, nullable) NSDictionary *googleServiceProperties; // The plugin registrar, for querying views. -@property(nonatomic) id registrar; +@property(nonatomic, nonnull) id registrar; @end @@ -226,7 +226,7 @@ - (void)refreshedAuthorizationTokensForUser:(NSString *)userId } - (void)addScopes:(nonnull NSArray *)scopes - forUser:(NSString *)userId + forUser:(nonnull NSString *)userId completion: (nonnull void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion { GIDGoogleUser *user = self.usersByIdentifier[userId]; @@ -258,7 +258,9 @@ - (void)addScopes:(nonnull NSArray *)scopes - (void)signOutWithError:(FlutterError *_Nullable *_Nonnull)error { [self.signIn signOut]; - [self.usersByIdentifier removeAllObjects]; + // usersByIdentifier is left populated, because the SDK may still support some operations on the + // GIDGoogleUser object (e.g., returning existing, non-expired tokens). Operations that the SDK + // doesn't support will return SDK errors that we can handle as normal. } - (void)disconnectWithCompletion:(nonnull void (^)(FlutterError *_Nullable))completion { @@ -292,8 +294,8 @@ - (void)signInWithHint:(nullable NSString *)hint } // Wraps the iOS and macOS scope addition methods. -- (void)addScopes:(NSArray *)scopes - forGoogleSignInUser:(GIDGoogleUser *)user +- (void)addScopes:(nonnull NSArray *)scopes + forGoogleSignInUser:(nonnull GIDGoogleUser *)user completion:(void (^)(GIDSignInResult *_Nullable signInResult, NSError *_Nullable error))completion { #if TARGET_OS_OSX @@ -305,9 +307,10 @@ - (void)addScopes:(NSArray *)scopes /// @return @c nil if GoogleService-Info.plist not found and runtimeClientIdentifier is not /// provided. -- (GIDConfiguration *)configurationWithClientIdentifier:(NSString *)runtimeClientIdentifier - serverClientIdentifier:(NSString *)runtimeServerClientIdentifier - hostedDomain:(NSString *)hostedDomain { +- (GIDConfiguration *)configurationWithClientIdentifier:(nullable NSString *)runtimeClientIdentifier + serverClientIdentifier: + (nullable NSString *)runtimeServerClientIdentifier + hostedDomain:(nullable NSString *)hostedDomain { NSString *clientID = runtimeClientIdentifier ?: self.googleServiceProperties[kClientIdKey]; if (!clientID) { // Creating a GIDConfiguration requires a client identifier. @@ -347,7 +350,7 @@ - (void)handleAuthResultWithUser:(nullable GIDGoogleUser *)user } } -- (void)didSignInForUser:(GIDGoogleUser *)user +- (void)didSignInForUser:(nonnull GIDGoogleUser *)user withServerAuthCode:(nullable NSString *)serverAuthCode completion:(void (^)(FSISignInResult *_Nullable, FlutterError *_Nullable))completion { self.usersByIdentifier[user.userID] = user; From 2823a54a08a45b9396dbf7673a8631091d8c626c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 4 Jun 2025 16:46:55 -0400 Subject: [PATCH 46/52] Add authorizationRequiresUserInteraction --- .../google_sign_in/google_sign_in/README.md | 6 +++++- .../google_sign_in/example/lib/main.dart | 8 ++++++-- .../google_sign_in/lib/google_sign_in.dart | 19 +++++++++++++++---- .../test/google_sign_in_test.dart | 13 +++++++++++++ .../test/google_sign_in_test.mocks.dart | 6 ++++++ .../google_sign_in_platform_interface.dart | 8 ++++++++ .../google_sign_in_web_test.dart | 19 +++++++++++++++++++ .../lib/google_sign_in_web.dart | 3 +++ 8 files changed, 75 insertions(+), 7 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 3543a20c285..131f0b97b1d 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -99,7 +99,8 @@ const List scopes = [ ### Requesting more scopes when needed If an app determines that the user hasn't granted the scopes it requires, it -should initiate an Authorization request. On some platforms, such as web, +should initiate an Authorization request. On platforms where +`authorizationRequiresUserInteraction()` returns true, this request **must be initiated from an user interaction** like a button press. @@ -138,6 +139,9 @@ need a server auth code you should request it as soon as possible after initial sign-in, and manage server tokens for that user entirely on the server side unless the signed in user changes. +On platforms where `authorizationRequiresUserInteraction()` returns true, +this request **must be initiated from an user interaction** like a button press. + ## Example The diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index c194c17d89f..e0627bbb7c9 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -188,7 +188,9 @@ class _SignInDemoState extends State { // Prompts the user to authorize `scopes`. // - // On the web, this must be called from an user interaction (button click). + // If authorizationRequiresUserInteraction() is true, this must be called from + // a user interaction (button click). In this example app, a button is used + // regardless, so authorizationRequiresUserInteraction() is not checked. Future _handleAuthorizeScopes(GoogleSignInAccount user) async { try { // #docregion RequestScopes @@ -217,7 +219,9 @@ class _SignInDemoState extends State { // Requests a server auth code for the authorized scopes. // - // On the web, this must be called from an user interaction (button click). + // If authorizationRequiresUserInteraction() is true, this must be called from + // a user interaction (button click). In this example app, a button is used + // regardless, so authorizationRequiresUserInteraction() is not checked. Future _handleGetAuthCode(GoogleSignInAccount user) async { try { // #docregion RequestServerAuth diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 1c3fef8b13a..ab05f04a26e 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -139,8 +139,9 @@ class GoogleSignInAuthorizationClient { /// details. /// /// This should only be called from a context where user interaction is - /// allowed (for example, during a user event handler on web, or while the - /// app is foregrounded on mobile). + /// allowed (for example, while the app is foregrounded on mobile), and if + /// [GoogleSignIn.authorizationRequiresUserInteraction] returns true this + /// should only be called from an user interaction handler. Future authorizeScopes( List scopes) async { final GoogleSignInClientAuthorization? authz = @@ -189,8 +190,9 @@ class GoogleSignInAuthorizationClient { /// the server side using that token. /// /// This should only be called from a context where user interaction is - /// allowed (for example, during a user event handler on web, or while the - /// app is foregrounded on mobile). + /// allowed (for example, while the app is foregrounded on mobile), and if + /// [GoogleSignIn.authorizationRequiresUserInteraction] returns true this + /// should only be called from an user interaction handler. Future authorizeServer( List scopes) async { final ServerAuthorizationTokenData? tokens = @@ -445,6 +447,15 @@ class GoogleSignIn { bool supportsAuthenticate() => GoogleSignInPlatform.instance.supportsAuthenticate(); + /// 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. + /// + /// For instance, this would return true on web if the sign in SDK uses popups + /// in its flow, since browsers may block popups that are not triggered + /// within the context of a user interaction. + bool authorizationRequiresUserInteraction() => + GoogleSignInPlatform.instance.authorizationRequiresUserInteraction(); + /// Starts an interactive sign-in process. /// /// Returns a [GoogleSignInAccount] with valid authentication tokens for a diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart index e635e2c39ef..7e13bd71203 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.dart @@ -247,6 +247,19 @@ void main() { } }); + group('authorizationRequiresUserInteraction', () { + for (final bool support in [true, false]) { + test('reports $support from platform', () async { + final GoogleSignIn googleSignIn = GoogleSignIn.instance; + + when(mockPlatform.authorizationRequiresUserInteraction()) + .thenReturn(support); + + expect(googleSignIn.authorizationRequiresUserInteraction(), support); + }); + } + }); + group('attemptLightweightAuthentication', () { test('returns successful authentication', () async { final GoogleSignIn googleSignIn = GoogleSignIn.instance; diff --git a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart index f3a891d8796..4a3b29c57e8 100644 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart +++ b/packages/google_sign_in/google_sign_in/test/google_sign_in_test.mocks.dart @@ -74,6 +74,12 @@ class MockGoogleSignInPlatform extends _i1.Mock returnValue: false, ) as bool); + @override + bool authorizationRequiresUserInteraction() => (super.noSuchMethod( + Invocation.method(#authorizationRequiresUserInteraction, []), + returnValue: false, + ) as bool); + @override _i4.Future<_i2.ClientAuthorizationTokenData?> clientAuthorizationTokensForScopes( 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 af1e26852e6..d6c4c8e5f57 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 @@ -74,6 +74,14 @@ abstract class GoogleSignInPlatform extends PlatformInterface { /// flow. bool supportsAuthenticate() => true; + /// 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. + /// + /// The default is true, but platforms that can fail to show UI without an + /// active user interaction should override this to return true. For instance, + /// this should return true for a web implementations that uses popups. + bool authorizationRequiresUserInteraction() => false; + /// Returns the tokens used to authenticate other API calls from a client. /// /// This should only return null if prompting would be necessary but [params] diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart index adbbb6babf4..911c73b3ebf 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/google_sign_in_web_test.dart @@ -118,6 +118,25 @@ void main() { }); }); + group('support queries', () { + testWidgets('reports lack of support for authenticate', (_) async { + final GoogleSignInPlugin plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + + expect(plugin.supportsAuthenticate(), false); + }); + + testWidgets('reports requirement for user interaction to authorize', + (_) async { + final GoogleSignInPlugin plugin = GoogleSignInPlugin( + debugOverrideLoader: true, + ); + + expect(plugin.authorizationRequiresUserInteraction(), true); + }); + }); + group('(with mocked GIS)', () { late GoogleSignInPlugin plugin; late MockGisSdkClient mockGis; 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 5e24c9f7bc2..1285e60be97 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 @@ -161,6 +161,9 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @override bool supportsAuthenticate() => false; + @override + bool authorizationRequiresUserInteraction() => true; + @override Future authenticate( AuthenticateParameters params) async { From 012a6d7601354bc2ae5133d7cd33cabb8bb4481b Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Thu, 5 Jun 2025 10:14:03 -0400 Subject: [PATCH 47/52] final variable in example --- .../google_sign_in/google_sign_in/README.md | 13 ++++-------- .../google_sign_in/example/lib/main.dart | 21 +++++++------------ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index 131f0b97b1d..a844bb7ffd7 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -79,19 +79,14 @@ else ...[ If the user has previously authorized the scopes required by your application, you can silently request an access token for those scopes: - + ```dart const List scopes = [ 'https://www.googleapis.com/auth/contacts.readonly', ]; -// ··· - GoogleSignInAccount? user; - // ··· - GoogleSignInClientAuthorization? authorization; - if (user != null) { - authorization = - await user.authorizationClient.authorizationForScopes(scopes); - } + final GoogleSignInAccount? user = // ... + final GoogleSignInClientAuthorization? authorization = + await user?.authorizationClient.authorizationForScopes(scopes); ``` [Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). diff --git a/packages/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart index e0627bbb7c9..aeed2430dc6 100644 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -78,22 +78,17 @@ class _SignInDemoState extends State { Future _handleAuthenticationEvent( GoogleSignInAuthenticationEvent event) async { // #docregion CheckAuthorization - final GoogleSignInAccount? user; - // #enddocregion CheckAuthorization - switch (event) { - case GoogleSignInAuthenticationEventSignIn(): - user = event.user; - case GoogleSignInAuthenticationEventSignOut(): - user = null; - } + final GoogleSignInAccount? user = // ... + // #enddocregion CheckAuthorization + switch (event) { + GoogleSignInAuthenticationEventSignIn() => event.user, + GoogleSignInAuthenticationEventSignOut() => null, + }; // Check for existing authorization. // #docregion CheckAuthorization - GoogleSignInClientAuthorization? authorization; - if (user != null) { - authorization = - await user.authorizationClient.authorizationForScopes(scopes); - } + final GoogleSignInClientAuthorization? authorization = + await user?.authorizationClient.authorizationForScopes(scopes); // #enddocregion CheckAuthorization setState(() { From 86e9d00ec3b29020b2d6930641a49fa1d8807d60 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 11 Jun 2025 16:33:41 -0400 Subject: [PATCH 48/52] Review feedback --- .../google_sign_in/lib/google_sign_in.dart | 8 +++-- .../lib/google_sign_in_android.dart | 6 ++++ .../test/google_sign_in_android_test.dart | 10 ++++++ .../lib/google_sign_in_ios.dart | 6 ++++ .../test/google_sign_in_ios_test.dart | 10 ++++++ .../google_sign_in_platform_interface.dart | 35 ++++++++++++------- ...oogle_sign_in_platform_interface_test.dart | 6 ++++ .../integration_test/web_only_test.dart | 10 ++++++ 8 files changed, 75 insertions(+), 16 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index ab05f04a26e..d272511bcf2 100644 --- a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart +++ b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart @@ -54,9 +54,11 @@ class GoogleSignInAccount implements GoogleIdentity { /// Returns authentication tokens for this account. /// /// This returns the authentication information that was returned at the time - /// of the initial authentication. Clients are strongly encouraged to use this - /// information immediately after authentication, as tokens are subject to - /// expiration, and obtaining new tokens requires re-authenticating. + /// of the initial authentication. + /// + /// Clients are strongly encouraged to use this information immediately after + /// authentication, as tokens are subject to expiration, and obtaining new + /// tokens requires re-authenticating. GoogleSignInAuthentication get authentication { return GoogleSignInAuthentication(idToken: _authenticationTokens.idToken); } diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index 908000fa8a7..f22e58dac86 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -63,6 +63,9 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { : _authenticationResultFromPlatformCredential(credential); } + @override + bool supportsAuthenticate() => true; + @override Future authenticate( AuthenticateParameters params) async { @@ -100,6 +103,9 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { await signOut(const SignOutParams()); } + @override + bool authorizationRequiresUserInteraction() => false; + @override Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params) async { diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart index f583fa50eff..44850e69c3e 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -50,6 +50,16 @@ void main() { expect(GoogleSignInPlatform.instance, isA()); }); + group('support queries', () { + test('reports support for authenticate', () { + expect(googleSignIn.supportsAuthenticate(), true); + }); + + test('reports no requirement for user interaction to authorize', () { + expect(googleSignIn.authorizationRequiresUserInteraction(), false); + }); + }); + group('attemptLightweightAuthentication', () { test('passes explicit server client ID', () async { const String serverClientId = 'aServerClient'; diff --git a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart index 770a8b72e76..b5970ee0a17 100644 --- a/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart +++ b/packages/google_sign_in/google_sign_in_ios/lib/google_sign_in_ios.dart @@ -59,6 +59,9 @@ class GoogleSignInIOS extends GoogleSignInPlatform { return _authenticationResultsFromSignInSuccess(success); } + @override + bool supportsAuthenticate() => true; + @override Future authenticate( AuthenticateParameters params) async { @@ -99,6 +102,9 @@ class GoogleSignInIOS extends GoogleSignInPlatform { await signOut(const SignOutParams()); } + @override + bool authorizationRequiresUserInteraction() => false; + @override Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params) async { diff --git a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart index d32abd13bbe..153a6c239aa 100644 --- a/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart +++ b/packages/google_sign_in/google_sign_in_ios/test/google_sign_in_ios_test.dart @@ -35,6 +35,16 @@ void main() { expect(GoogleSignInPlatform.instance, isA()); }); + group('support queries', () { + test('reports support for authenticate', () { + expect(googleSignIn.supportsAuthenticate(), true); + }); + + test('reports no requirement for user interaction to authorize', () { + expect(googleSignIn.authorizationRequiresUserInteraction(), false); + }); + }); + group('init', () { test('passes expected values', () async { const String clientId = 'aClient'; 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 d6c4c8e5f57..a68e6666537 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 @@ -59,12 +59,6 @@ abstract class GoogleSignInPlatform extends PlatformInterface { Future? attemptLightweightAuthentication( AttemptLightweightAuthenticationParameters params); - /// Signs in with explicit user intent. - /// - /// This is intended to support the use case where the user has expressed - /// an explicit intent to sign in. - Future authenticate(AuthenticateParameters params); - /// Returns true if the platform implementation supports the [authenticate] /// method. /// @@ -72,15 +66,20 @@ abstract class GoogleSignInPlatform extends PlatformInterface { /// override this to return false, throw [UnsupportedError] from /// [authenticate], and provide a different, platform-specific authentication /// flow. - bool supportsAuthenticate() => true; + bool supportsAuthenticate(); + + /// Signs in with explicit user intent. + /// + /// This is intended to support the use case where the user has expressed + /// an explicit intent to sign in. + Future authenticate(AuthenticateParameters params); /// 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. /// - /// The default is true, but platforms that can fail to show UI without an - /// active user interaction should override this to return true. For instance, - /// this should return true for a web implementations that uses popups. - bool authorizationRequiresUserInteraction() => false; + /// 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(); /// Returns the tokens used to authenticate other API calls from a client. /// @@ -99,8 +98,8 @@ abstract class GoogleSignInPlatform extends PlatformInterface { /// Signs out previously signed in accounts. Future signOut(SignOutParams params); - /// Revokes all of the scopes that all signed in users granted, and then them - /// out. + /// 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. @@ -136,11 +135,21 @@ class _PlaceholderImplementation extends GoogleSignInPlatform { throw UnimplementedError(); } + @override + bool supportsAuthenticate() { + throw UnimplementedError(); + } + @override Future authenticate(AuthenticateParameters params) { throw UnimplementedError(); } + @override + bool authorizationRequiresUserInteraction() { + throw UnimplementedError(); + } + @override Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params) { 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 0ef1d0495af..f536bd2e6bb 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 @@ -112,11 +112,17 @@ class ExtendsGoogleSignInPlatform extends GoogleSignInPlatform { return null; } + @override + bool supportsAuthenticate() => false; + @override Future authenticate(AuthenticateParameters params) { throw UnimplementedError(); } + @override + bool authorizationRequiresUserInteraction() => false; + @override Future clientAuthorizationTokensForScopes( ClientAuthorizationTokensForScopesParameters params) async { diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart index 79f8cd35cd8..f2afcec8f2d 100644 --- a/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/web_only_test.dart @@ -101,4 +101,14 @@ class NonWebImplementation extends GoogleSignInPlatform { Future signOut(SignOutParams params) { throw UnimplementedError(); } + + @override + bool authorizationRequiresUserInteraction() { + throw UnimplementedError(); + } + + @override + bool supportsAuthenticate() { + throw UnimplementedError(); + } } From 9c96d9d59eeca063df2c9fa2ed3ba8ac1f03f8a5 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 23 Jun 2025 16:42:45 -0400 Subject: [PATCH 49/52] Unwind platform interface overrides --- packages/google_sign_in/google_sign_in/example/pubspec.yaml | 1 - packages/google_sign_in/google_sign_in/pubspec.yaml | 3 +-- .../google_sign_in_android/example/pubspec.yaml | 6 +----- packages/google_sign_in/google_sign_in_android/pubspec.yaml | 6 +----- .../google_sign_in/google_sign_in_ios/example/pubspec.yaml | 6 +----- packages/google_sign_in/google_sign_in_ios/pubspec.yaml | 6 +----- .../google_sign_in/google_sign_in_web/example/pubspec.yaml | 6 +----- packages/google_sign_in/google_sign_in_web/pubspec.yaml | 6 +----- 8 files changed, 7 insertions(+), 33 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index 412d71ee96b..9212fd1c4ad 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -33,5 +33,4 @@ flutter: dependency_overrides: google_sign_in_android: {path: ../../../../packages/google_sign_in/google_sign_in_android} google_sign_in_ios: {path: ../../../../packages/google_sign_in/google_sign_in_ios} - google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} google_sign_in_web: {path: ../../../../packages/google_sign_in/google_sign_in_web} diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index a24eded31d3..ae0608e78d5 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -26,7 +26,7 @@ dependencies: sdk: flutter google_sign_in_android: ^6.2.0 google_sign_in_ios: ^5.8.1 - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 google_sign_in_web: ^0.12.4+4 dev_dependencies: @@ -53,5 +53,4 @@ false_secrets: dependency_overrides: google_sign_in_android: {path: ../../../packages/google_sign_in/google_sign_in_android} google_sign_in_ios: {path: ../../../packages/google_sign_in/google_sign_in_ios} - google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} google_sign_in_web: {path: ../../../packages/google_sign_in/google_sign_in_web} diff --git a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml index 0c83910dca5..a8c19eb232a 100644 --- a/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 http: ">=0.13.0 <2.0.0" dev_dependencies: @@ -28,7 +28,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index cfe6520b3dd..2ddf590bbcb 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -20,7 +20,7 @@ flutter: dependencies: flutter: sdk: flutter - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 dev_dependencies: build_runner: ^2.3.0 @@ -37,7 +37,3 @@ topics: false_secrets: - /example/android/app/google-services.json - /example/lib/main.dart -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml index 2e87992e304..4b53bc236a8 100644 --- a/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 http: ">=0.13.0 <2.0.0" dev_dependencies: @@ -27,7 +27,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml index a16f9281a49..6e5cb025024 100644 --- a/packages/google_sign_in/google_sign_in_ios/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_ios/pubspec.yaml @@ -24,7 +24,7 @@ flutter: dependencies: flutter: sdk: flutter - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 dev_dependencies: build_runner: ^2.4.6 @@ -44,7 +44,3 @@ false_secrets: - /example/ios/Runner/Info.plist - /example/lib/main.dart - /example/macos/Runner/Info.plist -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} diff --git a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml index 84bf18756db..a471462ed06 100644 --- a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: flutter: sdk: flutter google_identity_services_web: ^0.3.1 - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 google_sign_in_web: path: ../ @@ -26,7 +26,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_platform_interface: {path: ../../../../packages/google_sign_in/google_sign_in_platform_interface} 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 4231dbdda4c..faffd0143d1 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: flutter_web_plugins: sdk: flutter google_identity_services_web: ^0.3.1 - google_sign_in_platform_interface: ^2.5.0 + google_sign_in_platform_interface: ^3.0.0 http: ">=0.13.0 <2.0.0" web: ">=0.5.1 <2.0.0" @@ -34,7 +34,3 @@ dev_dependencies: topics: - authentication - google-sign-in -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_platform_interface: {path: ../../../packages/google_sign_in/google_sign_in_platform_interface} From 82e3503310cd9ecde1612902dc2b155de998d0de Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 23 Jun 2025 20:03:46 -0400 Subject: [PATCH 50/52] Dependency version bumps --- .../google_sign_in/example/pubspec.yaml | 8 +------- packages/google_sign_in/google_sign_in/pubspec.yaml | 12 +++--------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml index 9212fd1c4ad..12198ea2808 100644 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ - google_sign_in_web: ^0.12.3 + google_sign_in_web: ^1.0.0 http: ">=0.13.0 <2.0.0" dev_dependencies: @@ -28,9 +28,3 @@ dev_dependencies: flutter: uses-material-design: true -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_android: {path: ../../../../packages/google_sign_in/google_sign_in_android} - google_sign_in_ios: {path: ../../../../packages/google_sign_in/google_sign_in_ios} - google_sign_in_web: {path: ../../../../packages/google_sign_in/google_sign_in_web} diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index ae0608e78d5..6c765213f09 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -24,10 +24,10 @@ flutter: dependencies: flutter: sdk: flutter - google_sign_in_android: ^6.2.0 - google_sign_in_ios: ^5.8.1 + google_sign_in_android: ^7.0.0 + google_sign_in_ios: ^6.0.0 google_sign_in_platform_interface: ^3.0.0 - google_sign_in_web: ^0.12.4+4 + google_sign_in_web: ^1.0.0 dev_dependencies: build_runner: ^2.1.10 @@ -48,9 +48,3 @@ false_secrets: - /example/ios/RunnerTests/GoogleService-Info.plist - /example/ios/RunnerTests/GoogleSignInTests.m - /example/macos/Runner/Info.plist -# FOR TESTING AND INITIAL REVIEW ONLY. DO NOT MERGE. -# See https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changing-federated-plugins -dependency_overrides: - google_sign_in_android: {path: ../../../packages/google_sign_in/google_sign_in_android} - google_sign_in_ios: {path: ../../../packages/google_sign_in/google_sign_in_ios} - google_sign_in_web: {path: ../../../packages/google_sign_in/google_sign_in_web} From 47b86c8b544e7d9b4a68c785d714046e92e456e5 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Mon, 23 Jun 2025 20:07:13 -0400 Subject: [PATCH 51/52] Update exclusion list --- script/configs/exclude_all_packages_app.yaml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/script/configs/exclude_all_packages_app.yaml b/script/configs/exclude_all_packages_app.yaml index cd350b3878c..f9ac561b8d6 100644 --- a/script/configs/exclude_all_packages_app.yaml +++ b/script/configs/exclude_all_packages_app.yaml @@ -11,9 +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 -- google_sign_in_android -- google_sign_in_ios -- google_sign_in_web +# Temporarily excluded since it hasn't been updated for the major version +# change in google_sign_in. https://github.com/flutter/flutter/issues/171048 +- extension_google_sign_in_as_googleapis_auth From 276e1c6e39c6c498d35e11a1928f82c90ef6e431 Mon Sep 17 00:00:00 2001 From: stuartmorgan-g Date: Tue, 24 Jun 2025 06:00:40 -0400 Subject: [PATCH 52/52] Minor grammar fix --- packages/google_sign_in/google_sign_in/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md index a844bb7ffd7..772977ca322 100644 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -96,7 +96,7 @@ const List scopes = [ If an app determines that the user hasn't granted the scopes it requires, it should initiate an Authorization request. On platforms where `authorizationRequiresUserInteraction()` returns true, -this request **must be initiated from an user interaction** like a button press. +this request **must be initiated from a user interaction** like a button press. ```dart @@ -135,7 +135,7 @@ sign-in, and manage server tokens for that user entirely on the server side unless the signed in user changes. On platforms where `authorizationRequiresUserInteraction()` returns true, -this request **must be initiated from an user interaction** like a button press. +this request **must be initiated from a user interaction** like a button press. ## Example