What You'll Build in this Workshop:

You will also learn about the following:

This is what we'll be accomplishing in this codelab: users will be able to pull account balance information and increment the balance by performing a deposit. Here's the widget page we'll be working on:

Deposit Screen

Here's the schematics on what we'll be building regarding the deposit page:

Deposit Screen

Let's start from the services side of things. We will create a new service that will hold on to the information on the amount be deposited.

Let's call this new service DepositService, and will hold on to on the amount value to be deposited.

Create a class for the DepositService service with an internal property called amountToDeposit, type double. Make this service extend ChangeNotifier as this service will take care of notifying listeners accordingly:

Deposit Screen

Let's add three methods that will perform the following:

Your code inside the DepositService should look like this:

Deposit Screen

Don't forget to inject the newly created DepositService at the root of our application, inside the list of providers of our MultiProvider widget, as such:

Deposit Screen

Now, let's get back to the FlutterBankService and add some needed functionality to support this feature.

In the FlutterBankService, add a new property called selectedAccount, type Account, which will hold on to the account selected by the user on which to perform the desired action (a deposit or a withdrawal):

Deposit Screen

Let's add some getters and setters around the selectedAccount property. In the setSelectedAccount, upon assigining a provided account, make sure to also trigger the notifyListeners method provided by extending ChangeNotifier. For the getSelectedAccount, just return it.

Your code should look like this:

Deposit Screen

Now, let's add a new method called performDeposit, which returns a Future<bool> and takes a BuildContext reference. We'll also be using a Completer object to generate a future through which we'll complete this operation. Go ahead and add a Completer of type bool called depositComplete and return a Future out of it at the end of this method, as such:

Deposit Screen

The reason why we're passing the BuildContext context as a parameter to this method is because we'll also be fetching the data available on other provided services, and the only way to pull them is through the Provider.of mechanism, which requires a context passed to it.

We'll need to fetch both the LoginService to get the unique UID from the user, as well as the DepositService created earlier to get the amountToDeposit value, so let's do just that; we'll grab the required values and store them in local variables:

Deposit Screen

Now, after fetching these values, we'll perform the storing of the data to Firebase. We have the unique user id, which we'll use to fetch the corresponding document associated with the user, as well as the amount we want to change the specific account (the one that will be selected by the user and stored on selectedAccount), therefore we'll grab the document associated with that selecteAccount's id.

Let's inspect the query:

Deposit Screen

We are starting at the root collection (accounts), then searching for the document in the root collection by user id, then search inside that document found inside its nested collection (user_accounts), and in turn, find a document associated with the selected account's id. At the end, the returning document is stored in a variable called doc, type DocumentReference.

Then, on this document reference, we can perform the update to its balance field, where we will grab the existing balance and add the value in amountToDeposit that we fetched from the DepositService. The update method takes a dictionary object with the name of the field to update as key (‘balance') and the value the actual value to be assigned / updated to:

Deposit Screen

We could leave it up to here and it should work, but we kind of want to be notified when the update was performed and if it was done successfully or with an error, and of course we want to notify listeners that the update happed, so let's instead hook up to the .then method since the update call on the DocumentReference itself returns a Future, so we can chain success and error events, as such:

Deposit Screen

Notice other things we did on the event chaining we did on the .then(): upon a successful update, we invoke the depositService.resetDepositService, that way we can reset the amountToDeposit property back to zero, ready to take on a new transaction, and then completing the Future (depositComplete.complete(true)) so whoever is listening can be notified that the Future completed.

On the onError handler, we are completing the Future, but with an error (depositComplete.completeError) and passing the error received in a dictionary with key error.

With this, we've completed all updates we needed to do as far as services is concerned. Now, on to implementing the UI that will consume these services!

Let's start by creating the class that represents the page widget that will perform the deposits. Let's call it FlutterBankDeposit that extends StatelessWidget. Return a Scaffold with white as its backgroundColor:

Deposit Screen

Add an AppBar like in the other pages with the same properties:

Deposit Screen

As the body of the Scaffold, set it as a Container, with 20px padding all around, and as its direct child, add a Column with its items left aligned, as such:

Deposit Screen

Great! We have the foundation for our page.

This what we'll be tackling for the AccountActionHeader widget:

Deposit Screen

We'll be using this header on the rest of our pages, so we'll make it highly reusable.

Start by creating a custom class called AccountActionHeader that extends StatelessWidget. Add two properties for holding both the icon and the header title and a corresponding constructor that populates them:

Add a build method that returns a Container widget, since that will be the root of our structure. Your code should look like this:

Deposit Screen

To this Container, add some bottom margin of 20px, and as its direct child, add a Row widget, as we'll be packing all our widgets horizontally. As the children of this Row, then let's add an Icon widget, feeding to it the icon property, some space after it via a SizedBox 10px wide, and a Text widget feeding to it the headerTitle String with the proper styling.

Confirm that your code looks like this inside the build method:

Deposit Screen

With that, let's go back to the FlutterBankDeposit widget page and install the widget in place. Add the newly created AccountActionHeader widget as the Column's first child, passing its required parameters for headerTitle and icon, as such:

Deposit Screen

See it on DartPad when you run it:

Deposit Screen

Let's move on.

Now let's work on the body of this page, the AccountActionSelection widget, starting by the top portion which displays the accounts available.

Deposit Screen

Back to the FlutterBankDeposit, under the AccountActionHeader, let's add an Expanded widget, since we want the widget here to occupy most of the real estate in the Column, and as its only child, add a placeholder widget called AccountActionSelection:

Here's a schematic of what we'll be building:

Deposit Screen

Let's start by creating the wrapper widget called AccountActionSelection as a StatelessWidget:

Deposit Screen

Since we want this widget to rebuild upon changes to the FlutterBankService, we'll ad as the root of the structure a Consumer widget, and make it a Consumer<FlutterBankService>, so replace the existing Container widget in the build method's body:

Deposit Screen

We'll be fetching the accounts in the same way we did in the FlutterBankMain widget via the FlutterBankService's getAccounts method, so we'll need a FutureBuilder widget, so return a FutureBuilder widget out of the Consumer, and use the injected service in the Consumer, as such:

Deposit Screen

Display Loading When No Data or Waiting

Check whether there's data, so you could show the reusable loading we created earlier called FlutterBankLoading:

Deposit Screen

Display an error widget if there was an issue

Also display a custom widget if there was an error during fetching the data. Let's create it real quick. Create a custom widget called FlutterBankError which just displays a simple generic error message.#

Deposit Screen

Back on the AccountActionSelection, apply this newly created widget after checking for the snapshow.hasError:

Deposit Screen

To support this logic, let's go to the FlutterBankService's getAccounts and add the needed error logic by setting the onError parameter in the .then() from fetching the list of documents to a callback that captures the error and completes the accountsCompleter with an error (accountsCompleter.completeError):

Deposit Screen

After all the error conditions and lack of data conditions are handled, then we can proceed and start building the "happy path" of this widget - displaying the accounts.

Displaying action type label based on transaction

Here's what we'll be building:

Deposit Screen

Let's start by grabbing the data from the Future's snapshot.data property and store it on a local variable called accounts, a List of Account objects. From the injected service in the parent Consumer, get the selected account by calling the service's getSelectedAccount() and storing it on another local variable called selectedAccount:

Deposit Screen

We'll build the returning structure starting with a Column with its children aligned at the top.

Deposit Screen

Let's start with that Text widget that says "To". Remember we want to make this widget reusable; if we are in the "Deposits" page, yes, it should say "To", but not when it is in the "Withdrawals" page - it should say "From". Let's make the content of this Text widget configurable by passing the string to this widget, so let's make a property in the AccountActionSelection widget called actionTypeLabel, type String. Pass it via the constructor as a parameter. Your code should look like this:

Deposit Screen

Ok, let's proceed now. Back inside the FutureBuilder builder method where we're building the structure. Inside the returning Column widget, add the first Text widget, passing the newly added value of the actionTypeLabel to it, with some space below it using a SizedBox 10px high:

Deposit Screen

We'll add the actual value later, but at least we have things hooked up already.

Displaying the accounts: AccountActionCard widget

Let's proceed now to add the AccountActionCard widget, as described below:

Deposit Screen

Let's create a class called AccountActionCard which will allows to render the accounts and do the selection of the card on which we want to perform the selection. Return a Row from its build method since we'll lay out our cards horizontally:

Deposit Screen

To this widget, pass both the accounts list and the selectedAccount as parameters via the constructor:

Deposit Screen

Now, let's build the structure inside the Row:

Use the accounts collection to generate a list of widgets to feed into the Row widget, using the List.generate. Use the accounts.length and the required callback to pull each Account object out of the current iteration. Your code should look like this:

Deposit Screen

Inside this List.generate method, is where we'll build the structure of each of the account cards. Let's start with a Container widget, with padding of 15px all around, as well as 5px of margin. Let's add some border radius of 10px as well as some shadow, and white as the background, as follows:

Deposit Screen

We'll need a reference to the FlutterBankService so let's pull it via the Provider at the top of our build method:

Deposit Screen

We want to apply a border to this Container when we determine that the selected account from the FlutterBankService matches the currentAccount in the iteration, so let's apply it by checking whether the currentAcount is equals to the selectedAccount property being passed via the constructor; apply the border using Border.all with a 5px width, and applying either a transparent color or the mainThemeColor when the selectedAccount != null and selectedAccount.id equals currentAccount.id:

Deposit Screen

Once we've applied all the styles we want to the core Container widget that represents each account, we need to display some content in them. We'll display the account type (in uppercase) and the account number, pulling this data from the currentAccount object. Let's display this information vertically as a Column and show each piece of information as a Text widget:

Deposit Screen

Hey, We've typed a lot so far, and not seeing anything! Don't freak out! I think it's time to try it and see how's looking.

Go back to the AccountActionSelection widget, and go to the Column where the actionTypeLabel Text widget is, so let's start adding our "in-progress" AccountActionCard widget, and pass both the selectedAccount and accounts values to it:

Deposit Screen

Also don't forget to pass the actionTypeLabel all the way from the FlutterBankDeposit widget into the AccountActionSelection widget, like this:

Deposit Screen

If you run it on DartPad now, you will briefly see the spinning wheel / loading component (FlutterBankLoading), and then you'll be seeing the output as below:

Deposit Screen

Glad we're able to see something! Let's keep rolling and build the rest of the interaction.

If you noticed, nothing happens when you tap on them. Let's add some tapping capabilities to these cards.

Wrap them inside a GestureDetector widget and leverage the onTap event that's available. Assign a callback / handler to it, and inside, use the FlutterBankService service provided at the top of the build method, and call the method setSelectedAccount, passing the currentAccount to it; this will set what the current selected account is, while also triggering the notifyListeners on the service, which in turn notifies all widgets that are listening - in our case, the Consumer widget that is at the root of the AccountActionSelection widget, thus forcing it to rebuild.

Your GestureDetector and its onTap method should look like this:

Deposit Screen

Lastly, on this widget, wrap the GestureDetector inside an Expanded widget - this will make all cards within the Row distribute their width proportionally among them.

Confirm the build method on your AccountActionCard looks like this:

Deposit Screen

If you run what we have through DartPad, here's what you'll see: a brief animation of the loading widget, then the available accounts. Try to tap on one of them, and you'll see how they should be highlighted, thanks to the ability to select them, which triggers a rebuild of this widget. Awesome!

Deposit Screen

Great! Let's continue down this page and create the rest of the components in the AccountActionSelection widget.

The content below the AccountActionSelection should occupy the remaining space of the Column we're returning out of the FutureBuilder widget, so start by adding an Expanded widget. In turn, within this Expanded widget, add yet another Column widget since we want to line the children up vertically, aligned to the left and centered vertically, as follows:

Deposit Screen

Unless the user selects one of the accounts, we don't want to perform any actions or expose any of the controls, so let's wrap the whole Column inside a Visibility widget, and apply its visible property only if the selectedAccount is not null:

Deposit Screen

Inside of this Column, add the first child, a Container with 30px top margin, and as its child, a Text widget with the text "Current Balance** in grey:

Deposit Screen

To display the balance, we'll use a Row widget, with its children center aligned, with an Icon and a Text to display the balance, pulling the value out of the selectAccount object; the balance will be displayed as a two-decimal amount and a dollar sign prepended to it, with the proper styling:

Deposit Screen

Run it through DartPad; notice you don't see anything until you tap on an account card (that triggers the Visibility widget since no account was selected); then tap on one, then the other, then notice how the balances change, triggering a change on the AccountActionSelection widget, the selectedAccount changing values and triggering a rebuild on the widgets below, updating as expected:

Deposit Screen

Sweet! Now that we have these pieces in place, let's work on the capability to capture a balance amount to be saved.

Let's now create a widget that allows the user to provide an amount to be saved back to Firebase as a deposit. This is what the widget consists of, as shown in the schematics below:

Deposit Screen

This widget will be making use of the DepositService to both maintain its own state and to be able to notify widgets that decide to listen and are interested in the changes that occur with the DepositService.

When the user moves the slider, it changes the value in the label above, which will trigger a notification on every value change. Other widgets will be listening to whether there has been a valid value provided by this widget, thus enabling or disabling themselves accordingly. We'll see that later.

Let's proceed.

We'll create a class called AccountDepositSlider that extends StatelessWidget; from its build method, return a Consumer that listens to the DepositService, which in turn will return a Column as its core widget structure:

Deposit Screen

Align the children widgets in the Column both vertically and horizontally:

Deposit Screen

As the first two items of the Column widget, add a Text widget with the text "Amount To Deposit", in grey; show the captured value of the amount to deposit in another Text widget, formatted as an integer, with a font size of 60px and black:

Deposit Screen

And finally, we'll add a Slider widget, which will allow us to change values from a range, using the following properties from the Slider class:

Your code should look like this:

Deposit Screen

We should be done with this widget. Now let's come up with a way to integrate it into the application.

The way we'll inject it is via the AccountActionSelection widget - the same way we added the actionTypeLabel. We will create a parameter that can accept any kind of object of type Widget, and this widget will place it in the location we desire - right under the balance row, wrapped inside an Expanded widget. That way we can use the AccountActionSelection widget in a very polymorphic way as we can use it also for the widthdrawal widget page, and we can insert our own withdrawal slider widget here as well.

Let's go back to the AccountActionSelection widget, and in addition to the actionTypeLabel, add a property of type Widget called amountChange, which will be a placeholder for any widget that will handle the changing of the amount (i.e. whether for withdrawal or deposit). We will make this constructor parameter required in order to make sure a Widget is always supplied to the AccountActionSelection widget.

The updated widget class should look like this afterwards:

Deposit Screen

Right below the Row widget displaying the current balance value, add an Expanded widget, and as its child, the amountChanger! widget, and then this becomes a placeholder for whatever widget we inject via the amountChanger property.

Deposit Screen

Back on the FlutterBankDeposit widget, in the (currently broken) AccountActionSelection, now provide our newly created widget AccountDepositSlider assigning it to the amountChanger, as such:

Deposit Screen

Running this implementation through DartPad and you can see the results as below:

Deposit Screen

As a recap: When the user selects one of the accounts, it triggers the visibility of the balance information via rebuilding the widget after being notified via the FlutterBankService's setSelectedAccount since the service has a notifyListeners invocation which triggers that widget.

When the user moves the slider, it calls its onChanged event, which calls the DepositService's setAmountToDeposit, which internally makes a call to its notifyListeners; since we have a Consumer widget listening for changes in the DepositService in our AccountDepositSlider, this triggers the widget to rebuild itself, updating accordingly based on the updated amount to deposit being set.

Instead of using something like setState and using a StatefulWidget, we are instead triggering widget rebuilds and state manangement using Provider services that extend ChangeNotifier so listeners can rebuild and update accordingly, keeping things neat, well architected and decoupled.

We're making great progress! Let's move on to the next step.

Now let's bring the last widget of this page, which will be the "Make Deposit" button. This button will be what ultimately triggers the deposit workflow and performs the save to Firebase Cloud Firestore.

As the last item in the FlutterBankDeposit main Column structure, below the AccountActionSelection wrapper Expanded widget, add a Consumer that also listens to the DepositService; we'll do this so that when the AccountDepositSlider widget changes its value to a valid value to be deposited, our Consumer will rebuild and evaluate a condition whether to enable / disable the button we will display to perform the saving.

So, add the Consumer<DepositService> at the end of the Column:

Deposit Screen

Out of the Consumer's builder method, return a FlutterBankMainButton with the following specs:

Confirm your code looks like this after implementing it:

Deposit Screen

Let's run this in DartPad and take it for a spin; move the slider and change its value; make sure sometimes you bring the value to zero to notice how the button disables itself (since the condition is hit) and so on.

You should see the FlutterBankMainButton behaving like below:

Deposit Screen

Both deposit or withdrawal will be delegated to a separate page, which we'll call TransactionCompletionPage; this page will perform the call, return the result and display it to the user, as well as redirect them to the main page for further transactions. Let's see what this looks like:

Deposit Screen

And this is how the workflow transitions through the many states in which it could potentially be:

Deposit Screen

This should be a simple page, so let's get right to it.

Create a custom class called TransactionCompletionPage that takes one parameter via its constructor: a flag to denote whether it is a deposit or withdrawal called isDeposit (you can think of other ways in which you could optimize this approach; I'm keeping it simple this time since I only have two types of transactions). From its build method, return a Scaffold:

Deposit Screen

As usual, set the backgroundColor of the Scaffold to white as well as adding the common AppBar properties, as such:

Deposit Screen

Let's make sure that at the top of the build method, we are extracting the FlutterBankService out of the Provider, as follows:

Deposit Screen

As the body of the Scaffold, set it to a Center widget, as all widgets displayed here will be center aligned:

Deposit Screen

Then, as a child of the Center widget, set it to be a FutureBuilder since we will be performing either the depositing or withdrawing from here, and both transactions are asynchronous and return Future objects. For now, add the FutureBuilder as a child of Center and assign its future property to the bankService.performDeposit(context) since we'll be doing the deposit this time around, later we'll wrap up the withdrawal:

Deposit Screen

As before, let's handle the lack of data and error conditions, and display the corresponding component based on that:

Deposit Screen

Once all those scenarios are taken care of, we should also handle the success. Let's create another custom widget called FlutterBankTransactionCompleted widget, just to handle that scenario.

In this new class, you can add the same structure as in the other small widgets: a Column widget with its children centered both horizontally and vertically, with an Icon widget, some spacing and a Text label underneath it, as below:

Deposit Screen

Go back to the TransactionCompletionPage and handle the remaining condition, which is the "happy path":

Deposit Screen

Nice, we've taken care of this page. Let's hook it up to the "Make Deposit" button on our FlutterBankDeposit page from earlier.

Go back to the FlutterBankDeposit, locate the Consumer widget wrapping the FlutterBankMainButton for the deposit, and on the onTap event, let's do the following logic: let's check for whether the amount to make a deposit is valid (greater than zero), if so, then we'll hook up an event to it that navigates the user to our TransactionCompletionPage (using the Navigator.of(context) and calling pushReplacement).

Then, to the TransactionCompletionPage widget, pass as a parameter the flag isDeposit to true, otherwise, just assign null to the onTap event handler (setting a button's event handler to null just makes it disabled to clicking).

Your code should look like this:

Deposit Screen

If you save and try it in DartPad, you will see the value being saved, and the transaction page showing it completed successfully.

One additional improvement that we want to do is that we don't want to just leave the user here on this page, and force them to have to hit the back button on the header bar.

We want to improve the experience: we'll introduce a tiny delay and after that, we'll redirect the user back to the main page.

Back on the TransactionCompletionPage widget page, at the top of the build method (it could be right below the FlutterBankService reference), use the Future.delayed to introduce a small delay (3 seconds), in which all we do is pop the existing screen at the moment (the same TransactionCompletionPage). If the user hits back within those 3 seconds, it's ok - it's what happens after those 3 seconds - the app will automatically redirect the user back to the main page, since the deposit page was already swapped by this one in the Navigator.of().pushReplacement call:

Deposit Screen

With that in place, I believe we should be good to go!

Let's bring this whole workflow from end to end.

Now we need to find the way to trigger this page we just created, and we have just the place: the bottom navigation bar.

Remember we had there some unfinished event handlers attached to each of the actions in the bottom bar. Well, let's populate those now.

Deposit Screen

Let's refactor some code.

Go to the Utils class, and in the getBottomBarItems, refactor this method so it can take now a BuildContext object; that way, within the getBottomBarItems we can access the Navigator by doing a Navigator.of with the context.

Still inside the getBottomBarItems method, locate the FlutterBankBottomBarItem with label "Deposit", and on its action handler method, perform a navigation to the FlutterBankDeposit, using the provided context to this method. Your code should look like this:

Deposit Screen

Go now to the FlutterBankBottomBar since we now need to pass the context inside the Utils.getBottomBarItems call located here, as in the code below:

Deposit Screen

I believe we're pretty much done, aside from a few things we could improve. At this point, if you are in the main page (FlutterBankMain), navigate to the deposits page (FlutterBankDeposit), select an account, then go back to the main, and then back to deposits, you will see that the selection got persisted. We don't want to persist the previous selection, we want to reset the selection when we hit the back button, also when the transaction is complete and we go back to main, also we want to reset the selection - not persist it.

Deposit Screen

The way to accomplish that is through a widget called WillPopScope. With this widget you can pretty much intercept the dismissal of the current page in order to perform one last action (like a clean-up action, in our case, resetting the selected account).

Let's work on this.

Let's go to the FlutterBankDeposit widget page and wrap the whole root widget in your build method (in our case, the Scaffold) inside the WillPopScope widget. Add the required argument onWillPop which is a callback where you can handle your cleanup before the page is dismissed, as such:

Deposit Screen

Inside the onWillPop, use the Provider.of to pull a reference of the FlutterBankService here:

Deposit Screen

Back in the FlutterBankService, add a method called resetSelections() where we'll take care of resetting a few things here, including the selectedAccount property of the FlutterBankService

Deposit Screen

Back on the FlutterBankDeposit widget page, finish the onWillPop method, and call this method out of the FlutterBankService instance we just retrieved, and add at the end of the call, return a Future.value(true), required by this method, because if the callback returns a Future that resolves to false, the enclosing route will not be popped.

Check your code against the one below:

Deposit Screen

If you try it now, you will see that getting back to the main and then back to deposit, it won't persist the selection, since we are resetting it before leaving the page.

Do the same approach on the TransactionCompletionPage, by wrapping the Scaffold inside a WillPopScope as well, and using the exising FlutterBankService reference and calling the resetSelections method in the onWillPop; also do it inside the Future.delayed since there we are automatically popping the screen as well.

See the code below to ensure you did it correctly; this is what the TransactionCompletionPage's build method looks like after making those changes:

Deposit Screen

And with that, we've completed the deposit page for this application! I hope you've learned some additional tricks and best practices that you can go ahead and implement on your own Flutter apps. Cheers!

In this codelab, we accomplished the following:

Please don't forget to follow me on social media:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:provider/provider.dart';
import 'package:intl/intl.dart';

void main() async {

  WidgetsFlutterBinding.ensureInitialized();
  
  await Firebase.initializeApp(
    options: const FirebaseOptions(
      apiKey: "AIzaSyBwSM_bH2-kid-TtJPxZUo0Xw_QO8kgsU8",
      authDomain: "flutter-bank-app-6ec93.firebaseapp.com",
      projectId: "flutter-bank-app-6ec93",
      storageBucket: "flutter-bank-app-6ec93.appspot.com",
      messagingSenderId: "182673651632",
      appId: "1:182673651632:web:aad3511575ff2677108875"
    )
  );

  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) => LoginService(),
        ),
        ChangeNotifierProvider(
          create: (_) => FlutterBankService(),
        ),
        ChangeNotifierProvider(
          create: (_) => DepositService(),
        ),
      ],
      child: FlutterBankApp()
    )
  );
}

class FlutterBankApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        textTheme: GoogleFonts.poppinsTextTheme(
          Theme.of(context).textTheme
        )
      ),
      debugShowCheckedModeBanner: false,
      home: FlutterBankSplash() 
    );
  }
}

class FlutterBankSplash extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {

    Future.delayed(const Duration(seconds: 2), () {
       Navigator.of(context).push(
        MaterialPageRoute(builder: (context) => FlutterBankLogin())
       );   
     });

    return Scaffold(
      backgroundColor: Utils.mainThemeColor,
      body: Stack(
        children: const [
          Center(
            child: Icon(Icons.savings, color: Colors.white, size: 60)
          ),
          Center(
            child: SizedBox(
              width: 100,
              height: 100,
              child: CircularProgressIndicator(
                strokeWidth: 8,
                valueColor: AlwaysStoppedAnimation<Color>(Colors.white)
              )
            )
          )
        ],
      )
    );
  }
}

class FlutterBankLogin extends StatefulWidget {
  @override
  FlutterBankLoginState createState() => FlutterBankLoginState();
}

class FlutterBankLoginState extends State<FlutterBankLogin>{

  TextEditingController usernameController = TextEditingController();
  TextEditingController passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {

    LoginService loginService  = Provider.of<LoginService>(context, listen: false);

    return Scaffold(
      backgroundColor: Colors.white,
      body: Container(
        padding: const EdgeInsets.all(30),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              width: 80,
              height: 80,
              decoration: BoxDecoration(
                border: Border.all(
                  width: 7,
                  color: Utils.mainThemeColor
                ),
                borderRadius: BorderRadius.circular(100)
              ),
              child: const Icon(Icons.savings, color: Utils.mainThemeColor, size: 45)
            ),
            const SizedBox(height: 30),
            const Text('Welcome to', style: TextStyle(color: Colors.grey, fontSize: 15)),
            const Text('Flutter\nSavings Bank', 
            style: TextStyle(color: Utils.mainThemeColor, fontSize: 30)),
            Expanded(
              child: Center(
                child: Column(
                  mainAxisAlignment: MainAxisAlignment.center, 
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    const Text('Sign Into Your Bank Account', 
                         textAlign: TextAlign.center, 
                         style: TextStyle(color: Colors.grey, fontSize: 12)
                    ),
                    const SizedBox(height: 10),
                    Container(
                      padding: const EdgeInsets.all(5),
                      decoration: BoxDecoration(
                        color: Colors.grey.withOpacity(0.2),
                        borderRadius: BorderRadius.circular(50)
                      ),
                      child: TextField(
                        onChanged: (text) {
                          setState(() {});
                        },
                        decoration: const InputDecoration(
                          border: InputBorder.none,
                          prefixIcon: Icon(Icons.email, color: Utils.mainThemeColor),
                          focusedBorder: InputBorder.none,
                          enabledBorder: InputBorder.none,
                          errorBorder: InputBorder.none,
                          disabledBorder: InputBorder.none,
                          contentPadding: EdgeInsets.only(
                            left: 20, bottom: 11, top: 11, right: 15
                          ),
                          hintText: "Email"
                        ),
                        style: const TextStyle(fontSize: 16),
                        controller: usernameController
                      ) 
                    ),
                    const SizedBox(height: 20),
                    
                    // password Container wrapper 
                    Container(
                      padding: const EdgeInsets.all(5),
                      decoration: BoxDecoration(
                        color: Colors.grey.withOpacity(0.2),
                        borderRadius: BorderRadius.circular(50)
                      ),
                      child: TextField(
                        onChanged: (text) {
                          setState(() {});
                        },
                        obscureText: true,
                        obscuringCharacter: "*",
                        decoration: const InputDecoration(
                          prefixIcon: Icon(Icons.lock, color: Utils.mainThemeColor),
                          border: InputBorder.none,
                          focusedBorder: InputBorder.none,
                          enabledBorder: InputBorder.none,
                          errorBorder: InputBorder.none,
                          disabledBorder: InputBorder.none,
                          contentPadding: EdgeInsets.only(
                            left: 15, bottom: 11, top: 11, right: 15
                          ),
                          hintText: "Password"
                        ),
                        controller: passwordController,
                        style: const TextStyle(fontSize: 16),
                      )
                    ),
                    Consumer<LoginService>(
                      builder: (context, lService, child) {

                        String errorMsg = lService.getErrorMessage();
                      
                        if (errorMsg.isEmpty) {
                          return const SizedBox(height: 40);
                        }

                        return Container(
                          padding: const EdgeInsets.all(10),
                          child: Row(
                            children: [
                              const Icon(Icons.warning, color: Colors.red),
                              const SizedBox(width: 10),
                              Expanded(
                                child: Text(
                                  errorMsg, 
                                  style: const TextStyle(color: Colors.red)
                                )
                              )
                            ]
                          )
                        );
                      }
                    )
                  ]
                )
              )
            ),
            FlutterBankMainButton(
              label: 'Sign In',
              enabled: validateEmailAndPassword(),
              onTap: () async {
                var username = usernameController.value.text;
                var pwd = passwordController.value.text;

                bool isLoggedIn = await loginService.signInWithEmailAndPassword(username, pwd);

                if (isLoggedIn) {
                  usernameController.clear();
                  passwordController.clear();
                  Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => FlutterBankMain())
                  );
                }
              }
            ),
            const SizedBox(height: 10),
            FlutterBankMainButton(
              label: 'Register',
              icon: Icons.account_circle,
              onTap: () {
                Navigator.of(context).push(
                  MaterialPageRoute(builder: (context) => FlutterAccountRegistration())
                );
              },
              backgroundColor: Utils.mainThemeColor.withOpacity(0.05),
              iconColor: Utils.mainThemeColor,
              labelColor: Utils.mainThemeColor
            ) 
          ]
        ),
      ),
    );
  }

  @override
  void dispose() {
    usernameController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  bool validateEmailAndPassword() {
    return usernameController.value.text.isNotEmpty && 
      passwordController.value.text.isNotEmpty 
        && Utils.validateEmail(usernameController.value.text);
  }
}

class FlutterAccountRegistration extends StatefulWidget {
  @override
  FlutterAccountRegistrationState createState() => FlutterAccountRegistrationState();
}

class FlutterAccountRegistrationState extends State<FlutterAccountRegistration> {

  TextEditingController usernameController = TextEditingController();
  TextEditingController passwordController = TextEditingController();
  TextEditingController confirmPasswordController = TextEditingController();
  
  @override
  void dispose() {
    usernameController.dispose();
    passwordController.dispose();
    confirmPasswordController.dispose();
    super.dispose();
  }
  
  @override
  Widget build(BuildContext context) {

    LoginService loginService = Provider.of<LoginService>(context, listen: false);

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        elevation: 0,
        iconTheme: const IconThemeData(color: Utils.mainThemeColor),
        backgroundColor: Colors.transparent,
        title: const Icon(Icons.savings, color: Utils.mainThemeColor, size: 40),
        centerTitle: true
      ),
      body: Container(
        padding: const EdgeInsets.all(30),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // title
                  Container(
                    margin: const EdgeInsets.only(bottom: 40),
                    child: const Text('Create New Account', 
                      style: TextStyle(color: Utils.mainThemeColor, fontSize: 20)
                    )
                  ),         
                  // email field
                  Utils.generateInputField('Email', Icons.email, 
                    usernameController, 
                    false, (text) {
                      setState(() {});
                  }),
                  // password field
                  Utils.generateInputField('Password', Icons.lock, 
                    passwordController, 
                    true, (text) {
                      setState(() {});
                  }),
                  // password confirmation field
                  Utils.generateInputField('Confirm Password', Icons.lock, 
                    confirmPasswordController, 
                    true, (text) {
                      setState(() {});
                  }),
                ]
              )
            ),
            FlutterBankMainButton(
              label: 'Register',
              enabled: validateFormFields(),
              onTap: () async {
                String username = usernameController.value.text;
                String pwd = passwordController.value.text;

                bool accountCreated = 
                    await loginService.createUserWithEmailAndPassword(username, pwd);

                if (accountCreated) {
                  Navigator.of(context).pop();
                }
              }
            )
          ]
        )
      )
    );
  }

  bool validateFormFields() {
    return Utils.validateEmail(usernameController.value.text) &&
     usernameController.value.text.isNotEmpty &&
      passwordController.value.text.isNotEmpty &&
      confirmPasswordController.value.text.isNotEmpty &&
      (passwordController.value.text == confirmPasswordController.value.text);
  }
}

class FlutterBankMainButton extends StatelessWidget {
  
  final Function? onTap;
  final String? label;
  final bool? enabled;
  final IconData? icon;
  final Color? backgroundColor;
  final Color? iconColor;
  final Color? labelColor;
  
  const FlutterBankMainButton({
    Key? key, this.label, this.onTap, 
    this.icon, 
    this.backgroundColor = Utils.mainThemeColor, 
    this.iconColor = Colors.white,
    this.labelColor = Colors.white,
    this.enabled = true })
  : super(key: key);
  
  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        ClipRRect(
          borderRadius: BorderRadius.circular(50),
          child: Material(
            color: enabled! ? backgroundColor : backgroundColor!.withOpacity(0.5),
              child: InkWell(
              onTap: enabled! ? () {
                onTap!();
              } : null,
              highlightColor: Colors.white.withOpacity(0.2),
              splashColor: Colors.white.withOpacity(0.1),
              child: Container(
                padding: const EdgeInsets.all(15),
                decoration: BoxDecoration(
                  borderRadius: BorderRadius.circular(50)
                ),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Visibility(
                        visible: icon != null,
                        child: Container(
                          margin: const EdgeInsets.only(right: 20),
                          child: Icon(icon, color: iconColor, size: 20),
                        )
                      ),
                      Text(label!, textAlign: TextAlign.center, 
                        style: TextStyle(
                          color: labelColor, 
                          fontWeight: FontWeight.bold
                        )
                    )
                  ]
                )
              ),
            ),
          ),
        )
      ],
    );
  }
}

class FlutterBankDeposit extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return WillPopScope(
      onWillPop: () {
        FlutterBankService bankService = 
          Provider.of<FlutterBankService>(context, listen: false);
        bankService.resetSelections();
        return Future.value(true);
      },
      child: Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          elevation: 0,
          iconTheme: const IconThemeData(color: Utils.mainThemeColor),
          backgroundColor: Colors.transparent,
          title: const Icon(Icons.savings, color: Utils.mainThemeColor, size: 40),
          centerTitle: true
        ),
        body: Container(
          padding: const EdgeInsets.all(20),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            children: [
              AccountActionHeader(headerTitle: 'Deposit', icon: Icons.login),
              Expanded(
                child: AccountActionSelection(
                  actionTypeLabel: 'To',
                  amountChanger: AccountDepositSlider(),  
                ),
              ),
              Consumer<DepositService>(
                builder: (context, depositService, child) {
                  return FlutterBankMainButton(
                    enabled: depositService.checkAmountToDeposit(),
                    label: 'Make Deposit',
                    onTap: (depositService.checkAmountToDeposit() ? () {
                      Navigator.of(context).pushReplacement(
                        MaterialPageRoute(
                          builder: (context) => 
                            const TransactionCompletionPage(isDeposit: true)
                          )
                      );
                    } : null)
                  );
                }
              )
            ]
          )
        )
      ),
    );
  }
}

class TransactionCompletionPage extends StatelessWidget {
  
  final bool? isDeposit;
  const TransactionCompletionPage({Key? key, this.isDeposit }):
    super(key: key);

  @override
  Widget build(BuildContext context) {

    FlutterBankService bankService = 
      Provider.of<FlutterBankService>(context, listen: false);

    Future.delayed(const Duration(seconds: 3), () {
      bankService.resetSelections();
      Navigator.of(context).pop();
    });

    return WillPopScope(
      onWillPop: () {
        bankService.resetSelections();
        return Future.value(true);
      },
      child: Scaffold(
        backgroundColor: Colors.white,
        appBar: AppBar(
          elevation: 0,
          iconTheme: const IconThemeData(color: Utils.mainThemeColor),
          backgroundColor: Colors.transparent,
          title: const Icon(Icons.savings, color: Utils.mainThemeColor, size: 40),
          centerTitle: true
        ),
        body: Center(
          child: FutureBuilder(
            future: bankService.performDeposit(context),
            builder: (context, snapshot) {
              if (!snapshot.hasData) {
                return FlutterBankLoading();
              }

              if (snapshot.hasError) {
                return FlutterBankError();
              }

              return FlutterBankTransactionCompleted();
            }
          )
        )
      )
    );
  }
}

class FlutterBankTransactionCompleted extends StatelessWidget {

  @override 
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: const [
        Icon(Icons.check_circle_outline_outlined, 
          color: Utils.mainThemeColor, size: 80
        ),
        SizedBox(height: 20),
        Text('Transaction Completed', 
          style: TextStyle(color: Utils.mainThemeColor, fontSize: 20)
        ),
      ]
    );
  }
}

class AccountActionSelection extends StatelessWidget {

  final String? actionTypeLabel;
  final Widget? amountChanger;

  const AccountActionSelection({ 
    this.actionTypeLabel,
    required this.amountChanger
  });

  @override 
  Widget build(BuildContext context) {
    return Consumer<FlutterBankService>(
      builder: (context, service, child) {
        
         return FutureBuilder(
            future: service.getAccounts(context),
            builder: (context, snapshot) {
              
              if (!snapshot.hasData) {
                return FlutterBankLoading();
              }

              if (snapshot.hasError) {
                return FlutterBankError();
              }

              var selectedAccount = service.getSelectedAccount();
              List<Account> accounts = snapshot.data as List<Account>;

              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.start,
                children: [
                  Text(actionTypeLabel!, 
                    style: TextStyle(color: Colors.grey, fontSize: 15)
                  ),
                  const SizedBox(height: 10),
                  AccountActionCard(
                    selectedAccount:  selectedAccount,
                    accounts: accounts,  
                  ),
                  
                  Expanded(
                    child: Visibility(
                      visible: selectedAccount != null,
                      child:
                      
                      Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        crossAxisAlignment: CrossAxisAlignment.center,
                        children: [
                          Container(
                            margin: const EdgeInsets.only(top: 30),
                            child: Text('Current Balance', 
                              style: const TextStyle(color: Colors.grey)
                            ),
                          ),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              const Icon(Icons.monetization_on, 
                                  color: Utils.mainThemeColor, size: 25
                                ),
                              Text(selectedAccount != null ? 
                                   '\$' + selectedAccount.balance!.toStringAsFixed(2): '', 
                                style: const TextStyle(color: Colors.black, fontSize: 35)
                              )
                            ]
                          ),
                          Expanded(
                            child: amountChanger!,
                          )
                        ]
                      ),


                    )
                  )
                ]
              );


            }
         );

      }
    );
  }
}

class AccountDepositSlider extends StatelessWidget {

  @override 
  Widget build(BuildContext context) {
    return Consumer<DepositService>(
      builder: (context, depositService, child) {
        return Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Amount To Deposit', style: 
            TextStyle(color: Colors.grey)),
            Text('\$${depositService.amountToDeposit.toInt().toString()}',
              style: const TextStyle(color: Colors.black, fontSize: 60)
            ),
            Slider(
              value: depositService.amountToDeposit,
              max: 1000,
              activeColor: Utils.mainThemeColor,
              inactiveColor: Colors.grey.withOpacity(0.5),
              thumbColor: Utils.mainThemeColor,
              onChanged: (double value) {
                depositService.setAmountToDeposit(value);
              }
            )                            
          ]
        );
      }
    );
  }
}

class AccountActionCard extends StatelessWidget {

  final List<Account>? accounts;
  final Account? selectedAccount;

  const AccountActionCard({ this.accounts, this.selectedAccount });

  @override
  Widget build(BuildContext context) {

    FlutterBankService bankService = 
      Provider.of<FlutterBankService>(context, listen: false);

    return Row(
      children: List.generate(accounts!.length, (index) {
        var currentAccount = accounts![index];
        return Expanded(
          child: GestureDetector(
            onTap: () {
              bankService.setSelectedAccount(currentAccount);
            },
            child: Container(
              margin: const EdgeInsets.all(5),
              padding: const EdgeInsets.all(15),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(10),
                color: Colors.white,
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.1),
                    blurRadius: 20, offset: const Offset(0.0, 5.0)
                  )
                ],
                border: Border.all(
                  width: 5,
                  color: selectedAccount != null && 
                    selectedAccount!.id == currentAccount.id ? 
                      Utils.mainThemeColor : Colors.transparent
                ),
              ),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('${currentAccount.type!.toUpperCase()} ACCT', 
                    style: const TextStyle(color: Utils.mainThemeColor)
                  ),
                  Text(currentAccount.accountNumber!)
                ]
              )
            ),
          ),
        );
      })
    );
  }


}

class FlutterBankError extends StatelessWidget {

  @override 
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: const [
          Icon(Icons.warning_outlined, color: Utils.mainThemeColor, size: 80),
          SizedBox(height: 20),
          Text('Error fetching data', 
            style: TextStyle(color: Utils.mainThemeColor, fontSize: 20)
          ),
          Text('Please try again', 
            style: TextStyle(color: Colors.grey, fontSize: 12)
          )
        ]
      )
    );
  }
}

class AccountActionHeader extends StatelessWidget {

  final String? headerTitle;
  final IconData? icon;

  const AccountActionHeader({ this.headerTitle, this.icon });
  
  @override 
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 20),
      child: Row(
        children: [
          Icon(icon, color: Utils.mainThemeColor, size: 30),
          const SizedBox(width: 10),
          Text(headerTitle!, 
            style: const TextStyle(color: Utils.mainThemeColor, fontSize: 20)
          )
        ]
      ),
    );
  }
}

class FlutterBankMain extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      drawer: Drawer(child: FlutterBankDrawer()),
      appBar: AppBar(
        elevation: 0,
        iconTheme: const IconThemeData(color: Utils.mainThemeColor),
        backgroundColor: Colors.transparent,
        title: const Icon(Icons.savings, color: Utils.mainThemeColor, size: 40),
        centerTitle: true
      ),
      body: Container(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Row(
              children: const [
                Icon(Icons.account_balance_wallet, 
                  color: Utils.mainThemeColor, size: 30),
                SizedBox(width: 10),
                Text('My Accounts', 
                  style: TextStyle(color: Utils.mainThemeColor, fontSize: 20)
                )
              ]
            ),
            const SizedBox(height: 20),
            Expanded(
              child: Consumer<FlutterBankService>(
                builder: (context, bankService, child) {
                  return FutureBuilder(
                    future: bankService.getAccounts(context),
                    builder: (BuildContext context, AsyncSnapshot snapshot) {

                      if (snapshot.connectionState != ConnectionState.done || !snapshot.hasData) {
                        return FlutterBankLoading();
                      }

                      List<Account> accounts = snapshot.data as List<Account>;
                      
                      if (accounts.isEmpty) {
                        return Center(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.center,
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: const [
                              Icon(Icons.account_balance_wallet, color: Utils.mainThemeColor, size: 50),
                              SizedBox(height: 20),
                              Text('You don\'t have any accounts\nassociated with your profile.', 
                              textAlign: TextAlign.center, style: TextStyle(color: Utils.mainThemeColor))
                            ]
                          )
                        );
                      }

                      return ListView.builder(
                        itemCount: accounts.length,
                        itemBuilder: (context, index) {
                          var acct = accounts[index];
                          return AccountCard(account: acct);
                        }
                      );
                    }
                  );
                }
              )
            )
          ]
        )
      ),
      bottomNavigationBar: FlutterBankBottomBar(),
    );
  }
}

class AccountCard extends StatelessWidget {
  
  final Account? account;
  const AccountCard({ Key? key, this.account }) : super(key: key);
  
  @override
  Widget build(BuildContext context) {

    return Container(
      height: 180,
      padding: const EdgeInsets.all(20),
      margin: const EdgeInsets.only(bottom: 20),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(25),
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 15,
            offset: const Offset(0.0, 5.0)
          )
        ]
      ),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Column(
            children: [
              Text('${account!.type!.toUpperCase()} ACCT', textAlign: TextAlign.left, 
                style: const TextStyle(color: Utils.mainThemeColor, fontSize: 12)),
              Text('**** ${account!.accountNumber}')
            ]
          ),
          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              const Text('Balance', textAlign: TextAlign.left, 
                style: TextStyle(color: Utils.mainThemeColor, fontSize: 12)
              ),
              Row(
                children: [
                  const Icon(Icons.monetization_on, color: Utils.mainThemeColor, size: 30),
                  Text('\$${account!.balance!.toStringAsFixed(2)}', 
                    style: const TextStyle(color: Colors.black, fontSize: 35)
                  )
                ]
              ),
              Text('As of ${DateFormat.yMd().add_jm().format(DateTime.now())}', 
                style: const TextStyle(fontSize: 10, color: Colors.grey)
              )
            ]
          )
        ]
      )
    );
  }
}

class FlutterBankBottomBar extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {

    var bottomItems = Utils.getBottomBarItems(context);

    return Container(
      height: 100,
      padding: const EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Utils.mainThemeColor.withOpacity(0.05),
            blurRadius: 10,
            offset: Offset.zero
          )
        ]
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: List.generate(
          bottomItems.length, (index) {
            FlutterBankBottomBarItem bottomItem = bottomItems[index];
            
            return Material(
              color: Colors.transparent,
              borderRadius: BorderRadius.circular(10),
              clipBehavior: Clip.antiAlias,
              child: InkWell(
                highlightColor: Utils.mainThemeColor.withOpacity(0.2),
                splashColor: Utils.mainThemeColor.withOpacity(0.1),
                onTap: () {
                  bottomItem.action!();
                },
                child: Container(
                  constraints: BoxConstraints(minWidth: 80),
                  padding: const EdgeInsets.all(10),
                  child: Column(
                  mainAxisAlignment: MainAxisAlignment.end,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    children: [
                      Icon(bottomItem.icon, color: Utils.mainThemeColor, size: 20),
                      Text(bottomItem.label!, 
                        style: TextStyle(color: Utils.mainThemeColor, fontSize: 10)
                      )
                    ]
                  )
                )
              )
            );
          }
        )
      )
    );
  }
}

class FlutterBankLoading extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        width: 80,
        height: 80,
        child: Stack(
          children: const [
            Center(
              child: SizedBox(
                width: 80,
                height: 80,
                child: CircularProgressIndicator(
                  strokeWidth: 8,
                  valueColor: AlwaysStoppedAnimation<Color>(Utils.mainThemeColor)
                )
              )
            ),
            Center(
              child: Icon(Icons.savings, color: Utils.mainThemeColor, size: 40)
            )
          ]
        )
      )
    );
  }
}

class FlutterBankDrawer extends StatelessWidget {
  
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Utils.mainThemeColor,
      padding: const EdgeInsets.all(30),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          const Icon(Icons.savings, color: Colors.white, size: 60),
          const SizedBox(height: 40),
          Material(
            color: Colors.transparent,

            // rest of the code omitted for brevity...
            child: TextButton(
              style: ButtonStyle(
                 backgroundColor: MaterialStateProperty.all<Color>(Colors.white.withOpacity(0.1))
              ),
              child: const Text('Sign Out', textAlign: TextAlign.left, 
                style: TextStyle(color: Colors.white)
              ),
              onPressed: () {
                Navigator.of(context).pop();
                Utils.signOutDialog(context);
              },
            )


          )
        ]
      )
    );
  }
}


// UTILITIES
class Utils {
  static const Color mainThemeColor = Color(0xFF8700C3);

  static bool validateEmail(String? value) {
    String pattern =
        r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]"
        r"{0,253}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]"
        r"{0,253}[a-zA-Z0-9])?)*$";
    RegExp regex = RegExp(pattern);
    
    return (value != null || value!.isNotEmpty || regex.hasMatch(value));
  }

  static Widget generateInputField(
    String hintText, 
    IconData iconData, 
    TextEditingController controller, 
    bool isPasswordField, 
    Function onChanged) {

    return Container(
      padding: const EdgeInsets.all(5),
      margin: const EdgeInsets.only(bottom: 20),
      decoration: BoxDecoration(
        color: Colors.grey.withOpacity(0.2),
        borderRadius: BorderRadius.circular(50)
      ),
      child: TextField(
        onChanged: (text) {
          onChanged(text);
        },
        obscureText: isPasswordField,
        obscuringCharacter: "*",
        decoration: InputDecoration(
          prefixIcon: Icon(iconData, color: Utils.mainThemeColor),
          border: InputBorder.none,
          focusedBorder: InputBorder.none,
          enabledBorder: InputBorder.none,
          errorBorder: InputBorder.none,
          disabledBorder: InputBorder.none,
          contentPadding: EdgeInsets.only(left: 15, bottom: 11, top: 11, right: 15),
          hintText: hintText
        ),
        controller: controller,
        style: const TextStyle(fontSize: 16),
      )
    );
  }

  static void signOutDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (BuildContext ctx) {
        return AlertDialog(
          title: const Text('Flutter Savings Bank Logout', 
          style: TextStyle(color: Utils.mainThemeColor)),
          content: Container(
            padding: const EdgeInsets.all(20),
            child: const Text('Are you sure you want to log out of your account?')
          ),
          actions: [
            TextButton(
              child: const Text('Yes', style: TextStyle(color: Utils.mainThemeColor)),
              onPressed: () async {
                
                Navigator.of(ctx).pop();
                LoginService loginService = Provider.of<LoginService>(ctx, listen: false);
                await loginService.signOut();
                Navigator.of(ctx).pop();

              },
            ),
          ],
        );
      },
    );
  }

  static List<FlutterBankBottomBarItem> getBottomBarItems(BuildContext context) {
    return [
      FlutterBankBottomBarItem(
        label: 'Withdraw',
        icon: Icons.logout,
        action: () {}
      ),
      FlutterBankBottomBarItem(
        label: 'Deposit',
        icon: Icons.login,
        action: () {
          Navigator.of(context).push(
            MaterialPageRoute(builder: (context) => FlutterBankDeposit())
          );
        }
      ),
      FlutterBankBottomBarItem(
        label: 'Expenses',
        icon: Icons.payments,
        action: () {}
      )
    ];
  }


}

// SERVICES 
class LoginService extends ChangeNotifier {

  String _userId = '';
  String _errorMessage = '';
  
  String getErrorMessage() {
    return _errorMessage;
  }

  void setLoginErrorMessage(String msg) {
    _errorMessage = msg;
    notifyListeners();
  }

  String getUserId() {
    return _userId;
  }

  Future<bool> signOut() {
    Completer<bool> signOutCompleter = Completer();

    FirebaseAuth.instance.signOut().then(
      (value) {
        signOutCompleter.complete(true);
      },
      onError: (error) {
        signOutCompleter.completeError({ 'error': error });
      }
    );

    return signOutCompleter.future;
  }

  Future<bool> createUserWithEmailAndPassword(String email, String pwd) async {

    try {
      UserCredential userCredentials = 
        await FirebaseAuth.instance.createUserWithEmailAndPassword(email: email, password: pwd);
      
      return true; // or userCredentials != null;
      
    } on FirebaseAuthException {
      return false;
    }
  }

  Future<bool> signInWithEmailAndPassword(String email, String password) async {
    setLoginErrorMessage('');

    try {
      UserCredential credentials = await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: email,
        password: password,
      );
      
      _userId = credentials.user!.uid;
      
      return true;
      
    } on FirebaseAuthException catch (ex) {
      setLoginErrorMessage('Error during sign-in: ' + ex.message!);
      return false;
    }
  }
}

class FlutterBankService extends ChangeNotifier {

  Account? selectedAccount;

  void setSelectedAccount(Account? acct) {
    selectedAccount = acct;
    notifyListeners();
  }

  void resetSelections() {
    setSelectedAccount(null);
  }
  
  Account? getSelectedAccount() {
    return selectedAccount;
  }

  Future<List<Account>> getAccounts(BuildContext context) {

    LoginService loginService = Provider.of<LoginService>(context, listen: false);
    String userId = loginService.getUserId();

    List<Account> accounts = [];

    Completer<List<Account>> accountsCompleter = Completer();

    FirebaseFirestore.instance
      .collection('accounts')
      .doc(userId)
      .collection('user_accounts')
      .get().then(
        (QuerySnapshot collection) {

          for(var doc in collection.docs) {
              var acctDoc = doc.data() as Map<String, dynamic>;
              var acct = Account.fromJson(acctDoc, doc.id);
              accounts.add(acct);
          }

          Future.delayed(const Duration(seconds: 1), () {
            accountsCompleter.complete(accounts);
          });
        },
        onError: (error) {
          accountsCompleter.completeError({ 'error': error });
        }
      );

    return accountsCompleter.future;
  }

  Future<bool> performDeposit(BuildContext context) {
    
    Completer<bool> depositComplete = Completer();

    LoginService loginService = Provider.of<LoginService>(context, listen: false);
    String userId = loginService.getUserId();
    
    DepositService depositService = Provider.of<DepositService>(context, listen: false);
    int amountToDeposit = depositService.amountToDeposit.toInt();

    DocumentReference doc = 
    FirebaseFirestore.instance
      .collection('accounts')
      .doc(userId)
      .collection('user_accounts')
      .doc(selectedAccount!.id!);

    doc.update({
      'balance': selectedAccount!.balance! + amountToDeposit
    }).then((value) {
      depositService.resetDepositService();
      depositComplete.complete(true);
    }, onError: (error) {
      depositComplete.completeError({ 'error': error });
    });

    return depositComplete.future;
  }
}

class DepositService extends ChangeNotifier {

  double amountToDeposit = 0;
  
  void setAmountToDeposit(double amount) {
    amountToDeposit = amount;
    notifyListeners();
  }
  
  void resetDepositService() {
    amountToDeposit = 0;
    notifyListeners();
  }
  
  bool checkAmountToDeposit() {
    return amountToDeposit > 0;
  }
}

// MODELS
class Account {
  
  String? id;
  String? type;
  String? accountNumber;
  double? balance;
  
  Account({ this.id, this.type, this.accountNumber, this.balance });
  
  factory Account.fromJson(Map<String, dynamic> json, String docId) {
    return Account(
      id: docId,
      type: json['type'],
      accountNumber: json['account_number'],
      balance: json['balance']
    );
  }
}

class FlutterBankBottomBarItem {

  String? label;
  IconData? icon;
  Function? action;

  FlutterBankBottomBarItem({ this.label, this.icon, this.action });
}