[google_sign_in] Support Dart-based configuration and `serverClientId` (#5250)

diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md
index d3f0bda..7e433a7 100644
--- a/packages/google_sign_in/google_sign_in/CHANGELOG.md
+++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 5.4.0
+
+* Adds support for configuring `serverClientId` through `GoogleSignIn` constructor.
+* Adds support for Dart-based configuration as alternative to `GoogleService-Info.plist` for iOS.
+
 ## 5.3.3
 
 * Updates references to the obsolete master branch.
diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md
index 5ede3be..e467ca8 100644
--- a/packages/google_sign_in/google_sign_in/README.md
+++ b/packages/google_sign_in/google_sign_in/README.md
@@ -65,6 +65,22 @@
 <!-- End of the Google Sign-in Section -->
 ```
 
+As an alternative to adding `GoogleService-Info.plist` to your Xcode project, you can instead
+configure your app in Dart code. In this case, skip steps 3-6 and pass `clientId` and
+`serverClientId` to the `GoogleSignIn` constructor:
+
+```dart
+GoogleSignIn _googleSignIn = GoogleSignIn(
+  ...
+  // The OAuth client id of your app. This is required.
+  clientId: ...,
+  // If you need to authenticate to a backend server, specify its OAuth client. This is optional.
+  serverClientId: ...,
+);
+```
+
+Note that step 7 is still required.
+
 #### iOS additional requirement
 
 Note that according to
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 3c62e0e..a1f8f7b 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
@@ -12,6 +12,7 @@
 
 export 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'
     show SignInOption;
+
 export 'src/common.dart';
 export 'widgets.dart';
 
@@ -183,6 +184,7 @@
     this.scopes = const <String>[],
     this.hostedDomain,
     this.clientId,
+    this.serverClientId,
   });
 
   /// Factory for creating default sign in user experience.
@@ -228,9 +230,29 @@
   /// Domain to restrict sign-in to.
   final String? hostedDomain;
 
-  /// Client ID being used to connect to google sign-in. Only supported on web.
+  /// Client ID being used to connect to google sign-in.
+  ///
+  /// 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;
 
+  /// Client ID of the backend server to which the app needs to authenticate
+  /// itself.
+  ///
+  /// Optional and not supported on all platforms (e.g. web). By default, it
+  /// is initialized from a configuration file if available.
+  ///
+  /// The value specified here has precedence over a value from a configuration
+  /// file.
+  ///
+  /// [GoogleSignInAuthentication.idToken] and
+  /// [GoogleSignInAccount.serverAuthCode] will be specific to the backend
+  /// server.
+  final String? serverClientId;
+
   final StreamController<GoogleSignInAccount?> _currentUserController =
       StreamController<GoogleSignInAccount?>.broadcast();
 
@@ -260,15 +282,18 @@
   }
 
   Future<void> _ensureInitialized() {
-    return _initialization ??= GoogleSignInPlatform.instance.init(
+    return _initialization ??=
+        GoogleSignInPlatform.instance.initWithParams(SignInInitParameters(
       signInOption: signInOption,
       scopes: scopes,
       hostedDomain: hostedDomain,
       clientId: clientId,
-    )..catchError((dynamic _) {
-        // Invalidate initialization if it errors out.
-        _initialization = null;
-      });
+      serverClientId: serverClientId,
+    ))
+          ..catchError((dynamic _) {
+            // Invalidate initialization if it errors out.
+            _initialization = null;
+          });
   }
 
   /// The most recently scheduled method call.
diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml
index 9ea09dd..c7724ad 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 @@
   for signing in with a Google account on Android and iOS.
 repository: https://github.com/flutter/plugins/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: 5.3.3
+version: 5.4.0
 
 
 environment:
@@ -23,9 +23,9 @@
 dependencies:
   flutter:
     sdk: flutter
-  google_sign_in_android: ^5.2.5
-  google_sign_in_ios: ^5.2.5
-  google_sign_in_platform_interface: ^2.1.0
+  google_sign_in_android: ^6.0.0
+  google_sign_in_ios: ^5.4.0
+  google_sign_in_platform_interface: ^2.2.0
   google_sign_in_web: ^0.10.0
 
 dev_dependencies:
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 2bc51b6..b8676bd 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_platform_interface/google_sign_in_platform_interface.dart';
 import 'package:mockito/annotations.dart';
 import 'package:mockito/mockito.dart';
+
 import 'google_sign_in_test.mocks.dart';
 
 /// Verify that [GoogleSignInAccount] can be mocked even though it's unused
@@ -58,7 +59,7 @@
       verify(mockPlatform.signIn());
     });
 
-    test('signIn prioritize clientId parameter when available', () async {
+    test('clientId parameter is forwarded to implementation', () async {
       const String fakeClientId = 'fakeClientId';
       final GoogleSignIn googleSignIn = GoogleSignIn(clientId: fakeClientId);
 
@@ -68,6 +69,17 @@
       verify(mockPlatform.signIn());
     });
 
+    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());
+    });
+
     test('signOut', () async {
       final GoogleSignIn googleSignIn = GoogleSignIn();
 
@@ -240,10 +252,12 @@
     test('can sign in after init failed before', () async {
       final GoogleSignIn googleSignIn = GoogleSignIn();
 
-      when(mockPlatform.init()).thenThrow(Exception('First init fails'));
+      when(mockPlatform.initWithParams(any))
+          .thenThrow(Exception('First init fails'));
       expect(googleSignIn.signIn(), throwsA(isInstanceOf<Exception>()));
 
-      when(mockPlatform.init()).thenAnswer((Invocation _) async {});
+      when(mockPlatform.initWithParams(any))
+          .thenAnswer((Invocation _) async {});
       expect(await googleSignIn.signIn(), isNotNull);
     });
 
@@ -334,13 +348,44 @@
 
 void _verifyInit(
   MockGoogleSignInPlatform mockSignIn, {
+  List<String> scopes = const <String>[],
   SignInOption signInOption = SignInOption.standard,
+  String? hostedDomain,
   String? clientId,
+  String? serverClientId,
+  bool forceCodeForRefreshToken = false,
 }) {
-  verify(mockSignIn.init(
-    signInOption: signInOption,
-    scopes: <String>[],
-    hostedDomain: null,
-    clientId: clientId,
-  ));
+  verify(mockSignIn.initWithParams(argThat(
+    isA<SignInInitParameters>()
+        .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,
+        ),
+  )));
 }