Google Sign-in for Flutter Web

My Flutter app was running fine for Web until I migrated google_sign_in from 6 to 7. Now signing-in fails with this error: Sign in failed: UnimplementedError: authenticate is not supported on the web. Instead, use renderButton to create a sign-in widget. Let's fix it.

Up until google_sign_in 6, it seemed like I could use the same code to sign in on iOS and Web. But from version 7, authentication on Web is different.

google_sign_in | Flutter package
Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account.
google_sign_in_web | Flutter package
Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android, iOS and Web.
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.

Adding the button

renderButton function - web_only library - Dart API
API docs for the renderButton function from the web_only library, for the Dart programming language.

I changed my code to render that special button on Web:

Before:

ElevatedButton(
    child: Text('Sign in'),
    onPressed: () {
      _handleSignIn(widget.auth, widget.googleSignIn);
    })

After:

!kIsWeb
    ? ElevatedButton(
        child: Text('Sign in'),
        onPressed: () {
          _handleSignIn(widget.auth, widget.googleSignIn);
        })
    : renderButton()

Adding the dependency

However, VSC couldn't find the renderButton. https://pub.dev/packages/google_sign_in_web has the explanation why:

This package is endorsed, 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.

So I do need to manually import this package.

init()

After I add the renderButton, I am getting the following error:

Bad state: GoogleSignInPlugin::init() or GoogleSignInPlugin::initWithParams() must be called before any other method in this plugin.

Looking back at https://pub.dev/packages/google_sign_in#usage, I have all the information I need to initialize it in code:

unawaited(_googleSignIn.initialize(
        clientId:
            '....apps.googleusercontent.com'));

Back in version 6, I had two choices to initialize GoogleSignIn. I could either pass the client id in code, or add a <meta> tag. I guess the meta tag way does not work anymore.

Running with a fixed port

If I attempt to run in Chrome using VSC's built-in Play button, the web app runs on a random port every time. Then Google Sign-In will throw an Access blocked: Authorization Error, Error 400: origin_mismatch.

So I need to run the exact port that I whitelisted on the Cloud Console:

flutter run -d chrome --web-port 5000

After setting it all up, Google Sign-In works on Web again! But wait a second...

Restoring compilation for iOS

Adding a dependency to google_sign_in_web actually broke compilation on other platforms like iOS. When I try to run the app on iOS, I get those errors:

 ../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/webrtc.dart:1067:40: Error: 'JSObject' isn't a type.
    extension type RTCRtpTransceiverInit._(JSObject _) implements JSObject {
                                           ^^^^^^^^
    ../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/webrtc.dart:1070:5: Error: 'JSArray' isn't a type.
        JSArray<MediaStream> streams,
        ^^^^^^^
    ../../.pub-cache/hosted/pub.dev/web-1.1.1/lib/src/dom/webrtc.dart:1071:5: Error: 'JSArray' isn't a type.
        JSArray<RTCRtpEncodingParameters> sendEncodings,
        ^^^^^^^
...

    ../../.pub-cache/hosted/pub.dev/google_identity_services_web-0.3.3+1/lib/src/js_interop/google_accounts_oauth2.dart:79:51: Error: The getter 'toJS' isn't defined for the
    type 'void Function(TokenRevocationResponse)'.
    Try correcting the name to the name of an existing getter, or defining a getter or field named 'toJS'.
        return _revokeWithDone(accessToken.toJS, done.toJS);
                                                      ^^^^
    ../../.pub-cache/hosted/pub.dev/google_identity_services_web-0.3.3+1/lib/src/js_interop/google_accounts_oauth2.dart:83:25: Error: 'JSString' isn't a type.
      external void _revoke(JSString accessToken);
                            ^^^^^^^^
    ../../.pub-cache/hosted/pub.dev/google_identity_services_web-0.3.3+1/lib/src/js_interop/google_accounts_oauth2.dart:85:33: Error: 'JSString' isn't a type.
      external void _revokeWithDone(JSString accessToken, JSFunction done);
                                    ^^^^^^^^

Luckily Gemini has a good way to isolate the code.

import 'package:flutter/material.dart';
import 'package:google_sign_in_web/web_only.dart';

class SignInButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return renderButton();
  }
}
sign_in_button_web.dart
import 'package:flutter/material.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:provider/provider.dart';

class SignInButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final googleSignIn = context.read<GoogleSignIn>();

    return ElevatedButton(
        child: Text('Sign in'),
        onPressed: () {
          googleSignIn.authenticate();
        });
  }
}
sign_in_button_not_web.dart
// Default export for mobile/desktop
export 'sign_in_button_not_web.dart'
    // Conditional export for web
    if (dart.library.js_interop) 'sign_in_button_web.dart';
sign_in_button.dart
import 'package:elysium/sign_in_button.dart';

Provider(
    create: (context) => widget.googleSignIn,
    child: SignInButton())
Usage

And indeed, it compiled fine:

Running Xcode build...                                                  
Xcode build done.                                           40.6s
✓ Built build/ios/iphoneos/Runner.app (50.1MB)

Conditional initialize

When I click on Sign in, I now get an error about not supporting the right URL. That's because I initialized GoogleSignIn with the key used for Web, whereas I configured my iOS app to use other keys.

Before:

unawaited(_googleSignIn.initialize(
        clientId:
            '[...].apps.googleusercontent.com'));
Uses the Web key by default

After:

unawaited(_googleSignIn.initialize(
    clientId: kIsWeb
        ? '[...].apps.googleusercontent.com'
        : null));
Only uses the Web key on Web