Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/firebase_ui_auth/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
google_sign_in: ^6.2.1
google_sign_in: ^7.1.0
http: ^1.1.2
integration_test:
sdk: flutter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class GoogleSignInButton extends _GoogleSignInButton {
super.key,
required super.loadingIndicator,
required super.clientId,
super.serverClientId,
super.redirectUri,
super.scopes,
super.action = null,
Expand All @@ -36,6 +37,7 @@ class GoogleSignInIconButton extends _GoogleSignInButton {
const GoogleSignInIconButton({
super.key,
required super.clientId,
super.serverClientId,
required super.loadingIndicator,
super.scopes,
super.action = null,
Expand Down Expand Up @@ -75,6 +77,7 @@ class _GoogleSignInButton extends StatelessWidget {
final SignedInCallback? onSignedIn;
final double size;
final String clientId;
final String? serverClientId;
final String? redirectUri;
final List<String>? scopes;
final void Function(Exception exception)? onError;
Expand All @@ -83,6 +86,7 @@ class _GoogleSignInButton extends StatelessWidget {
const _GoogleSignInButton({
super.key,
required this.clientId,
this.serverClientId,
required this.loadingIndicator,
this.scopes,
String? label,
Expand All @@ -106,6 +110,7 @@ class _GoogleSignInButton extends StatelessWidget {

return GoogleProvider(
clientId: clientId,
serverClientId: serverClientId,
redirectUri: redirectUri,
scopes: scopes ?? [],
);
Expand Down
98 changes: 74 additions & 24 deletions packages/firebase_ui_oauth_google/lib/src/provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ class GoogleProvider extends OAuthProvider {
/// Ignored on Android and iOS (if `iOSPreferPlist` is true).
final String clientId;

/// The client ID of the web OAuth client associated with the app's
/// server-side component, if any.
///
/// On Android this is required to receive an ID token, unless the app uses
/// `google-services.json` and it contains a web OAuth client entry, in
/// which case the plugin reads the value from there.
final String? serverClientId;

/// When true, the Google Sign In plugin will use the GoogleService-Info.plist
/// for configuration instead of the `clientId` parameter.
final bool iOSPreferPlist;
Expand All @@ -28,7 +36,23 @@ class GoogleProvider extends OAuthProvider {
/// The list of requested authorization scopes requested when signing in.
final List<String>? scopes;

late GoogleSignIn provider;
/// The plugin instance. google_sign_in 7 exposes a single shared instance.
/// Assignable so that tests can inject a mock.
GoogleSignIn provider = GoogleSignIn.instance;

// google_sign_in 7 requires initialize to be called exactly once, while
// multiple GoogleProvider instances may be created (for example one per
// GoogleSignInButton). The first instance to sign in configures the plugin.
static Future<void>? _initialization;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Because _initialization is a static variable, its state will persist across different tests in the same test suite. Since each test in google_sign_in_test.dart injects a new MockGoogleSignIn instance in setUp, subsequent tests will reuse the cached _initialization future from the first test and will not call initialize on their respective mock providers. To ensure proper test isolation and prevent potential flakiness, consider exposing a @visibleForTesting static method to reset _initialization and calling it in the test's setUp block.

Suggested change
static Future<void>? _initialization;
@visibleForTesting
static void debugReset() {
_initialization = null;
}
static Future<void>? _initialization;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 160260b — added a @visibleForTesting debugReset() that clears _initialization, and call it at the top of the test setUp so each test re-initializes on its freshly injected mock.


/// Resets the one-time initialization state.
///
/// The plugin is initialized exactly once per process, so tests that inject
/// a fresh mock in `setUp` must clear the cached future to stay isolated.
@visibleForTesting
static void debugReset() {
_initialization = null;
}

@override
final fba.GoogleAuthProvider firebaseAuthProvider = fba.GoogleAuthProvider();
Expand All @@ -44,19 +68,14 @@ class GoogleProvider extends OAuthProvider {

GoogleProvider({
required this.clientId,
this.serverClientId,
this.redirectUri,
this.scopes,
this.iOSPreferPlist = false,
}) {
firebaseAuthProvider.setCustomParameters(const {
'prompt': 'select_account',
});

if (_ignoreClientId()) {
provider = GoogleSignIn(scopes: scopes ?? []);
} else {
provider = GoogleSignIn(clientId: clientId, scopes: scopes ?? []);
}
}

bool _ignoreClientId() {
Expand All @@ -68,25 +87,55 @@ class GoogleProvider extends OAuthProvider {
return false;
}

Future<void> _ensureInitialized() {
return _initialization ??= provider.initialize(
clientId: _ignoreClientId() ? null : clientId,
serverClientId: serverClientId,
).catchError((Object err) {
// Allow a later sign-in attempt to retry initialization instead of
// rethrowing the same stale error forever.
_initialization = null;
throw err;
});
}
Comment on lines +90 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _ensureInitialized method can be simplified by chaining the .catchError call directly onto the _initialization ??= assignment. This eliminates the need for the intermediate local variable initialization and makes the code more concise and idiomatic.

  Future<void> _ensureInitialized() {
    return _initialization ??= provider.initialize(
      clientId: _ignoreClientId() ? null : clientId,
      serverClientId: serverClientId,
    ).catchError((Object err) {
      // Allow a later sign-in attempt to retry initialization instead of
      // rethrowing the same stale error forever.
      _initialization = null;
      throw err;
    });
  }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 160260b — chained .catchError directly onto initialize() and dropped the intermediate local.


@override
void mobileSignIn(AuthAction action) async {
provider
.signIn()
.then((user) {
if (user == null) throw AuthCancelledException();
return user.authentication;
})
.then((auth) {
final credential = fba.GoogleAuthProvider.credential(
accessToken: auth.accessToken,
idToken: auth.idToken,
);

onCredentialReceived(credential, action);
})
.catchError((err) {
authListener.onError(err);
});
final requestedScopes = scopes ?? const <String>[];

try {
await _ensureInitialized();

final account = await provider.authenticate(scopeHint: requestedScopes);

// Authentication and authorization are separate steps in
// google_sign_in 7. Reuse an existing authorization when one is
// available, otherwise prompt for the requested scopes so that the
// credential carries an access token, matching the previous behavior.
String? accessToken;
if (requestedScopes.isNotEmpty) {
final client = account.authorizationClient;
final authorization =
await client.authorizationForScopes(requestedScopes) ??
await client.authorizeScopes(requestedScopes);
accessToken = authorization.accessToken;
}

final credential = fba.GoogleAuthProvider.credential(
accessToken: accessToken,
idToken: account.authentication.idToken,
);

onCredentialReceived(credential, action);
} on GoogleSignInException catch (err) {
if (err.code == GoogleSignInExceptionCode.canceled) {
authListener.onError(AuthCancelledException());
} else {
authListener.onError(err);
}
} catch (err) {
authListener.onError(err);
}
}

@override
Expand All @@ -112,6 +161,7 @@ class GoogleProvider extends OAuthProvider {
if (defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.macOS) {
await _ensureInitialized();
await provider.signOut();
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/firebase_ui_oauth_google/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ dependencies:
firebase_ui_oauth: ^2.1.0
flutter:
sdk: flutter
google_sign_in: ^6.2.1
google_sign_in: ^7.1.0

dev_dependencies:
flutter_test:
Expand Down
Loading
Loading