Using flutterfire_ui's SignInScreen with unit tests

On the blog post describing the release of Flutter 2.8 (see https://medium.com/flutter/whats-new-in-flutter-2-8-d085b763d181), they introduced flutterfire_ui. This library provides standard UI for signing in with Firebase Auth. The post is a bit long cause I ran into a few issues. If you are not interested in the process of figuring it out, feel free to jump straight to the TDLR at the bottom of the post.

Basic usage

This is great since the code to handle sign-in is quite generic. A reusable Widget would remove lots of boilerplate code. I already had unit tests using firebase_auth_mocks that tested the sign in flow. Let's see how I can replace my sign-in boilerplate code with flutterfire_ui's SignIn widget while still keeping my unit tests.

On https://firebase.flutter.dev/docs/ui/auth/integrating-your-first-screen/, they show a basic snippet to build the UI. In my case I use both GoogleSignIn and anonymous sign-in. So it should look like this:

return SignInScreen(
  providerConfigs: [
    GoogleProviderConfiguration(
      clientId: '...',
    ),
  ],
);

To handle anonymous sign-in, they don't provide a ProviderConfiguration, but they do provide a field to customize the UI. So I'll add my custom button:

  footerBuilder: (context, _) {
    return OutlinedButton(
      child: Text('Start without an account'),
      onPressed: () => auth.signInAnonymously()
    );
  },

Another way would be to write a custom ConfigurationConfiguration. It might be a good exercise for later. But looking at their roadmap, it seems like the feature is coming soon. Until then, my custom footer will do.

Unit tests

Now comes the interesting part. How do I make it so that this UI can still run in unit tests and use a fake FirebaseAuth?

It turns out SignInScreen takes an optional auth parameter. Perfect!

So when running the app for real, I used FirebaseAuth.instance, and when running it in my unit tests, I passed an instance of MockFirebaseAuth. Yet my test still failed with this error:

══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following FirebaseException was thrown building OAuthProviderButton(dependencies:
[_LocalizationsScope-[GlobalKey#a313a], _InheritedCupertinoTheme]):
[core/no-app] No Firebase App '[DEFAULT]' has been created - call Firebase.initializeApp()

The relevant error-causing widget was:
  OAuthProviderButton
  OAuthProviderButton:file:///Users/[username]/flutter/.pub-cache/hosted/pub.dartlang.org/flutterfire_ui-0.3.0/lib/src/auth/views/login_view.dart:68:16

This is a clear sign that it tried to instantiate the real Firebase Auth. What happened?

After a bit of debugging, I found out that during the initialization, LoginScreen, although it does have a fake Auth instance, initializes LoginView without passing the value.

LoginScreen has the fake Auth
auth is not being passed
now auth in null

I was going to patch it locally and possibly file a ticket or PR, when I saw that it's already fixed on master: https://github.com/FirebaseExtended/flutterfire/pull/7645. Someone fixed in 10 days ago, but the fix hasn't been released yet.

So let's replace the dependency in pubspec.yaml to use master on GitHub temporarily until the next version of flutterfire_ui. Using the doc on how to use dependencies at https://docs.flutter.dev/development/packages-and-plugins/using-packages#dependencies-on-unpublished-packages, I changed the dependency to:

  flutterfire_ui:
    git:
      url: git://github.com/FirebaseExtended/flutterfire.git
      path: packages/flutterfire_ui

When I ran the test, I got only this one expected error:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown while running async test code:
Golden "goldens/home_signed_out.png": Pixel test failed, 27.46% diff detected.

The screenshot is different because SignInScreen's UI is different from what I had written previously. Great!

Register vs Sign in

Now the sign in screen looks like this:

Notice the "Register" link. When you click on it, the UI turns into this:

Switches to the Register screen when the user clicks Register.

It would make sense if the UI was different, such as if the user wanted to register by email/password or sign in using email/password. But for Google sign-in, in both cases, you just sign-in. So it is a bit confusing in my case, and as the flutter_ui documentation explains, you can turn it off with this flag:

showAuthActionSwitch:false,

And now, after disabling the Register, and adding a custom subtitle it looks like this. Much cleaner!

So if I still brought over most of my custom UI, what did I gain, you might ask. What it allowed me to do was remove this kind of sign-in code:

import 'package:google_sign_in/google_sign_in.dart';

// initialize GoogleSignIn
final _googleSignIn = GoogleSignIn(
  scopes: [
    'email',
    'https://www.googleapis.com/auth/contacts.readonly',
  ],
);

// Render a button that signs-in with GoogleSignIn
ElevatedButton(
    child: Text('Sign in with Google'),
    onPressed: () => {
      final signinAccount = await _googleSignIn.signIn();
      final googleAuth = await signinAccount.authentication;
      final AuthCredential credential = GoogleAuthProvider.getCredential(
        accessToken: googleAuth.accessToken,
        idToken: googleAuth.idToken,
      );
    }
);

It's not a lot of code, but it's still boilerplate code that takes a while to figure out the first time, and that we'd copy paste from project to project if we didn't use flutterfire_ui. And as you enable more sign-in methods, the benefit increases as that's even more boilerplate that you can replace with a simple ConfigurationProvider.

TLDR

Until a more recent version of flutterfire_ui comes out, add the dependency to your pubspec.yaml like so:

  flutterfire_ui:
    git:
      url: git://github.com/FirebaseExtended/flutterfire.git
      path: packages/flutterfire_ui

When the next version is out (the current version is 0.3.0), you can use:

  flutterfire_ui: ^0.4.0 # or whatever the next version might be

Then in your code, use SignInScreen and pass the desired FirebaseAuth instance:

return SignInScreen(
  auth: auth, // either an instance of FirebaseAuthMock or FirebaseAuth.instance.
  providerConfigs: [...],
);