Implementing Login in Flutter Web (Hummingbird)

Posted on September 10, 2019 in Flutter

Background

In this article, we'll make use of FirebaseAuth to implement login functionality in Flutter-to-Fly WebApp built using Flutter Web - Hummingbird. Design has been evolved since I wrote my first article about Designing Cross platform Flutter prototype for Landing Page (Web-Hummingbird, Android, iOS). This article focuses on implementing Login functionality in Hummingbird only. Please refer to this youtube video for implementing same login functionality in Android & iOS.

We'll implement LogIn button shown below:

FirebaseAuth-Login

Please refer to previous related articles below:

  1. Designing Cross platform Flutter prototype for Landing Page
  2. Making Cross-platform Flutter Landing Page Responsive
  3. Using Flutter Themes for Cross-platform Landing Page (Web-Hummingbird, Android, iOS)
  4. Implementing Flutter FactsBot using DialogFlow

Checkout the companion video:

Introduction

In this article, we'll make our login button work. I'll use Firebase authentication to implement email and password authentication. First, setup Firebase Project as mentioned here. We need to add configuration details in Flutter app to be able to communicate with Firebase.

We'll add two more pages to WebApp.

  1. LogIn Page: Clicking on "LogIn" button will take user to LogIn Page where either they can login using their credentials or register. Registering a user creates a user record in FireStore.
  2. User Profile Page: Logged in users can see their display name, profile picture placeholder, and SignOut button. This is only for demonstration purposes, and doesn't do much at this point.

pubspec.yaml

Following dependencies need to be added to pubspec.yaml to interact with Firebase. Provider package is used for dependency injection and state management.

dependencies:
  firebase: any
  service_worker: ^0.2.0
  googleapis_auth: ^0.2.3+5
  provider: any

dependency_overrides:
  provider:
    git:
      url: https://github.com/kevmoo/provider
      ref: flutter_web
  firebase:
    git:
      url: https://github.com/FirebaseExtended/firebase-dart

Web entry point: As we know that Flutter Web apps' entry point is web/main.dart which is compiled to javascript, and referred from web/index.html. Let's checkout code in both files:

web/main.dart:

main() async {
  try {
    await config();

    fb.initializeApp(
      apiKey: apiKey,
      authDomain: authDomain,
      databaseURL: databaseUrl,
      storageBucket: storageBucket,
      projectId: projectId,
    );

    await ui.webOnlyInitializePlatform();
    app.main();
  } on fb.FirebaseJsNotLoadedException catch (e) {
    print(e);
  }
}

web/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-firestore.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-auth.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.4.0/firebase-storage.js"></script>
    <script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body>
</body>
</html>

Firebase Configuration:

fb.initializeApp needs Firebase App's configuration parameters. We need to add this information in package:firebase/src/assets/config.json file. There's a sample config.json.sample file available for you for reference:

{
  "_FYI": "https://firebase.google.com/docs/web/setup",
  "_COPY_TO": "config.json",
  "API_KEY": "TODO",
  "AUTH_DOMAIN" : "TODO",
  "DATABASE_URL": "TODO",
  "STORAGE_BUCKET": "TODO",
  "PROJECT_ID": "TODO",
  "MESSAGING_SENDER_ID": "TODO",
  "SERVER_KEY": "TODO",
  "VAPID_KEY": "TODO",
}

Get parameters from Firebase console for your project, and update values in config.json.

LogIn Page

I'll not be explaining the UI code in this tutorial. However, please take a look at source code. Feel free to reach out to me if you need explanation with any part.

LogIn Form:

LogIn-form

Register Form:

Register-form

Authenticating using FirebaseAuthService:

FirebaseAuthService is a ChangeNotifier, which means if any sign-in or sign-out happens in this class, all other subscribed classes are notified. I've abstracted all APIs using BaseAuthService.

abstract class BaseAuthService with ChangeNotifier {
  Future<User> currentUser();
  Future<User> signIn(String email, String password);
  Future<User> googleSignIn();
  Future<User> updateUser(User user);
  Future<User> createUser(
      String firstName, String lastName, String email, String password);
  Future<void> signOut();
}

FirebaseAuthService extends BaseAuthService:

class FireAuthService extends BaseAuthService {
  final Auth _firebaseAuth = fb.auth();

  //Get currently logged-in user
  @override
  Future<User> currentUser() async {
    return await _firebaseAuth.currentUser;
  }

  //Sign-in using email and password, notifies all subscribers.
  @override
  Future<User> signIn(String email, String password) async {
    try {
      var auth =
          await _firebaseAuth.signInWithEmailAndPassword(email, password);

      notifyListeners();
      return auth.user;
    } catch (e) {
      throw Exception(e);
    }
  }

  //This method is called from register form. A user account is created in FirebaseAuth
  @override
  Future<User> createUser(
      String firstName, String lastName, String email, String password) async {
    var auth =
        await _firebaseAuth.createUserWithEmailAndPassword(email, password);

    var info = fb.UserProfile();
    info.displayName = '$firstName $lastName';
    await auth.user.updateProfile(info);

    updateUser(auth.user);

    return auth.user;
  }

  //A record is created at Firestore to keep track of all personalized data for each user.
  @override
  Future<User> updateUser(User user) async {
    final CollectionReference ref = fb.firestore().collection('users');

    String displayName = user.displayName;
    String photoUrl = user.photoURL;

    if (displayName == null) {
      displayName = "No Name yet";
    }

    if (photoUrl == null) {
      photoUrl = "";
    }

    var newData = {
      'uid': user.uid,
      'displayName': displayName,
      'photoUrl': photoUrl,
      'email': user.email,
      'lastActive': DateTime.now()
    };

    await ref.doc(user.uid).set(newData, SetOptions(merge: true));

    return user;
  }

  //Sign-out
  @override
  Future<void> signOut() async {
    _firebaseAuth.signOut();
    notifyListeners();
  }

  @override
  Future<User> googleSignIn() async {
    //TODO
  }
}

Checking for a logged-in user: Following code will check-in whether a user is already signed-in. If so, then UserProfilePage is displayed. Otherwise LogInPage is rendered.

return FutureBuilder<User>(
      future: Provider.of<FireAuthService>(context).currentUser(),
      builder: (context, AsyncSnapshot<User> snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.error != null) {
            return Text(snapshot.error.toString());
          }

          if (snapshot.hasData) {
            return UserProfilePage(context, snapshot.data);
          }

          return LogInPage(title: 'Login');
        } else {
          return Container(
            child: CircularProgressIndicator(),
          );
        }
      },
    );

User Profile Page

A UserProfilePage displays very basic information about the logged-in user. Right now, it shows: welcoming user with their email used as display name, placeholder for profile picture and sign-out button. Please refer to source code for details of implementing user interface.

UserProfile-page

A note on iOS Firebase integration

You may run into trouble building your code for iOS complaining about Firebase imports as shown in screenshot below:

ios-firebase

Solution: I found this StackOverflow post useful to debug and fix this issue:

ios-firebase-fix

Conclusion

We learned how to implement Firebase authentication in Flutter Web / Hummingbird for 'Login' button in our demo web app. We overviewed dependencies, and ChangeNotifier responsible for authentication, registering and creating user, and creating a user record in FireStore. Please refer to code below for Web and Native (Android & iOS) implementations.

Keep Fluttering !

Source code

References/Credits:

Image Credits

Happy cooking with Flutter :)

Liked the article ? Couldn't find a topic of your interest ? Please leave comments or email me about topics you would like me to write ! BTW I love cupcakes and coffee both :)