We will add the following features by implementing a serverless backend powered by Firebase:
You will also learn about the following:
We'll hook up Firebase to our login page, and as bonus, we'll learn how to create new accounts to connect our users to.
We'll create our very own first Firebase project, where we'll set up the serverless backend that will power this application.
Open a browser tab and navigate to the Firebase Console, where you'll create your first Firebase project under the free tier (called Spark) - no worries, you won't be charged or asked for a credit card.
You'll be asked to log in with your Gmail account. Go ahead and provide it so you an log in.
On the Firebase Console's landing page, click on the Create a Project button:
Immediately you get presented with a wizard, asking you for a project name. Enter "flutter-bank-app". Accept the Firebase Terms and hit Continue:
Next steps asks for enabling Google Analytics for your project. Let's disable it for now by just unchecking the Enable Google Analytics for this project toggle - we don't need it for this project. Then click on Create Project:
Be patient as Firebase is provisioning your project on the Google Cloud, so you'll get shown the following screen for a bit:
Until the project is fully created, then you can click on Continue:
Your brand new project should look like follows:
Notice that you are on the Free Tier (Spark), with the option to Upgrade to Blaze (Pay-As-You-Go), but for the things we'll implement we don't have to upgrade:
Expand the left-hand side menu named Build. Under Build you should find Authentication. Navigate to it and click on Get Started:
Firebase Authentication allows you to provide an extra layer of security to your apps by allowing your users to authenticate prior to using it in order to protect sensitive areas of your app that should be protected under user authentication. It also provides backend services, easy-to-use SDKs, and ready-made UI libraries to authenticate users to your app. It supports authentication using passwords, phone numbers, popular federated identity providers like Google, Facebook and Twitter, and more.
In this workshop we'll be focused on email and password authentication only, but feel free to explore the rest of the options available, such as third-party authentication (also free).
Right on the Authentication page, make sure you're in the Sign-in method tab. Find the Email/Password Sign-in provider option, and click on it:
Click Enable in the Email/Password panel. We will only work on the simple email/password workflow for this workshop, but I invite you to explore the other email/password workflow features available, such as passwordless sign-in using an email link, email address verification, password recovery, etc.
Click Enable, then Save:
It should look like this after enabling:
Your project is ready for email/password authentication - all we need is our first user! Let's proceed and add our first user.
Right on the Authentication page, click on the Users tab. Click on the Add User button:
On the dialog form that appears, add a valid email address (doesn't even have to be a real one, as long as it's valid ;)) and a password (make sure you remember it for when we test). Use something like *client@gmail.com with password (of at least 6 characters) 123456, then click on Add user:
Your new user should look like this after being created:
And we're all set to hook up this project to our Flutter Bank App! Let's proceed!
Let's now plug our project to Firebase so we can authenticate our users before letting them in further. For now we will be using the test account we created earlier in Firebase, but further down we'll show how to create these accounts programmatically as well, if the need arises.
We'll be using the Firebase SDK which luckily comes pre-installed with DartPad, so we don't have to install anything. You can verify this in DartPad by clicking on the information icon on the lower right corner, which shows a dialog with all packages installed and their versions:
We need to grab some configuration values generated by Firebase that we'll use to initialize an instance of the Firebase SDK in our app. Each platform has its proprietary way of consuming these configuration values, which in Firebase they are available as apps. Let's proceed and create a web app in our Firebase project.
Back in the Firebase Console, Click on Project Overview, then click on the web icon to set up a web app:
In the wizard, you're asked to add a nickname to register your app under - put Flutter Savings Bank. Then click Register App. No need to set up Firebase Hosting (which is awesome!) but not needed for this project.
In the next step, you're required to add the Firebase SDK - in our case all we need is the chunk of code provided in this step to initialize / bootstrap the Firebase SDK already installed on DartPad. No worries if you can't copy it at this point, I'll show you where to find it in the project. For now, click at the bottom where it says Continue to console.
This action will take you back to the project, where you can see now that the newly web app has been created and available. Go ahead and click on the cog next tho the web app name:
This option takes you to the project settings associated to this web app we just created. Right on the General tab, you get a lot of relevant information.
Scroll all the way to the bottom of the General tab and you should find the configuration settings you need to grab to initialize the Firebase SDK in DartPad.
Grab the configuration delimited by the const firebaseConfig since we'll need back on our DartPad project.
Now, back on our Flutter code, right at the very top, import the Firebase-related packages we will be using in this project (Firebase Core, Auth and Cloud Firestore):
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
Your code should look like this:
Now, locate the main method (usually at the top of the file) and inside the method, right on top of the runApp method, perform the initialization of Firebase, preceded by a call to WidgetsFlutterBinding.ensureInitialized - this ensures that the widget binding from the framework is initialized before we make any other calls. Call the Firebase.initializeApp method, passing the name parameter option and providing the properties inside config object we copied earlier from the Firebase SDK configuration from Firebase Console. Also make sure to decorate the method with the async keyword since the Firebase.initializeApp is an asynchronous call.
Your code should look like this:
With that in place, this initializes the Firebase instance and we can start making calls against it. Let's proceed now to tap into the authentication functionality.
Instead of tapping into the authentication functionality straight from the Firebase instance, let's do our due diligence and create a wrapper service class around this functionality which we'll conveniently call LoginService. This approach is more in line with best practices from the industry in which you hide the implementation details and business logic behind a service, and just provide an API or entrypoint methods to access its functionality.
For this approach we will leverage Flutter's Provider package, which provides (pun intended!) a simple approach to state management for both small and large applications.
Let's start by creating a class called LoginService that extends ChangeNotifier since we want components that are listening to this service to be notified of changes that occur within itself, thus rebuilding themselves accordingly.
You can place this class anywhere at the bottom of the file:
Let's add our first feature, which is the ability to sign in using a provided username and password. Create a method called signInWithEmailAndPassword which takes two parameters (email, password). Make it return a Future type bool since this method call will be an asynchronous call:
This method must return something so let's continue adding the actual call to Firebase authentication.
Let's make a call to one of the classes provided to perform the Firebase Authentication called FirebaseAuth, pull the currently initialized instance and call the conveniently named signInWithEmailAndPassword from it, passing both email and password supplied to the wrapper method. This FirebaseAuth method returns an object of type UserCredential, which encapsulates metadata associated with the authenticated user (such as display name, authenticated user Id, display name, profile photo, among other useful metadata).
For now we'll only capture the authenticated user id and store it internally, so I'll provision a property called _userId type String and hold its value there. Return true assuming this operation was successful:
To be more diligent about whether this will succeed or not, let's wrap our implementation inside a try/catch, capturing a very convenient and specific FirebaseAuthException in the event of an error. Return false from this wrapper method to signify that the operation was not successful:
I'll also provide a simple getter method to fetch the user id when it's set:
Now that we've created our login service with an API to perform an authenticated call provided an email and password, let's consume this service.
First since we are leveraging the Provider pattern, we need to inject this newly created service at the root of our program.
Go to the runApp method, and wrap our FlutterBankApp inside a MultiProvider widget. We'll be using a MultiProvider widget since we'll be providing / injecting more than one service later on; we're setting ourselves for success pretty much. Supply an empty array for the providers property in the MultiProvider widget.
In the event of having a single service, a Provider would've sufficed. Make sure to import the provider package at the top using the line:
import 'package:provider/provider.dart';
Your code should look as follows:
Now that we have the MultiProvider in place, inject the service to be provided to the application - in our case the LoginService. Add it to the list of providers by first wrapping it inside a ChangeNotifierProvider (a provider for services that notify their listeners) and creating an instance of it:
Now that the service is injected, we are ready to consume it anywhere.
Let's start by invoking it upon the user providing something in the username and password fields. We'll extract their values using their corresponding controllers, then supplying the values to our LoginService's signInWithEmailAndPassword method.
If the operation is successful (the user is authenticated) we want to allow the user to proceed inside the application. Let's create the placeholder widget page for what will be our landing page.
Create a class called FlutterBankMain that extends StatelessWidget. For now, make it return a Scaffold with a Center widget wrapping a Text widget that says "main page" as the Scaffold's body:
And now that we have a place to land, let's go to the FlutterBankLogin page widget.
Inside its build method, create an instance of the LoginService service, fetching by via the Provider.of factory method, passing the BuildContext and the listen: false flag so it is a one-time fetch and our widget doesn't rebuild all the time.
Proceed to the Sign In FlutterBankMainButton widget, inside its onTap handler, retrieve the value of the TextField widgets via their corresponding controllers and store them in variables (username and pwd respectively):
Now, tap into the provided service (LoginService) and call its signInWithEmailAndPassword passing the username and password collected, and capture the returned boolean flag in a variable called isLoggedIn. Make sure to await on this call as it is asynchronous, and also decorate the onTap handler with the async keyword, as such:
If the operation succeeded, then proceed to clear the fields via their controllers as well (for the next try), and navigate the user to our shell page for the main page (FlutterBankMain) we created earlier:
Take this for a spin (of course, remember this is the "happy path" - make sure to provide the same username and password of the test user we configured in our Firebase project - we don't have validation of any kind; we'll implement some version of that later).
If you did everything correctly (and supplied the correct email and password) you should be redirected to the dummy main page after hitting the Sign In button, as shown below:
If you type the wrong stuff, nothing happens - it just hangs there. That's because we didn't implement any error handling nor validation. Let's add some simple validation, disabling of buttons when no input is provided, etc.
Let's add some guard rails around this login page, to prevent fat-fingering and make this more close to a real implementation.
Let's create a useful and simple email validation logic using Regex that we can reuse. Go to the Utils class, and add a method called validateEmail that takes a String parameter.
Validate the provided String whether its is not null nor empty and it passes the Regex email validation, as follows:
Hey, let me give you a hand with that Regex - copy / paste it from the snipped below:
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));
}
Back on our FlutterBankLogin widget, let's create a convenient method called validateEmailAndPassword which returns a boolean flag, and validates whether any of the fields are empty plus the email is correct using that utility email validation method we just did; add it anywhere inside the FlutterBankLoginState class, as such:
Now, locate the Sign In FlutterBankMainButton widget and set its enabled property (currently true) to the returning value of this method, as follows:
What is this doing? Upon the textfields triggering changes as the user enters text, a notification is triggered from each of the textfields' onChanged event, which subsequently triggers a rebuild of the widget; the wigdet rebuilds itself, evaluates the content of the textfields, and returns true of false depending on the validation, allowing the FlutterBankMainButton to re-render based on this value and showing enabled or disabled. Pretty cool!
Remember we can still provide a bad but valid email and a wrong password. We really want to know what happens when we do. We are currently capturing the FirebaseAuthException exception, but we are not doing anything with it. Let's see what we can do with it.
In the LoginService, create an additional property for the error message; call it _errorMessage, type String. Create a corresponding getter for it:
I'll also create a convenient setter method, which internally resets the error message property to empty, as well as call its internal notifyListeners thanks to extending the ChangeNotifier class, as follows:
The notifyListeners will make it so that any widget listening ot the LoginService will be notified to rebuild itself based on changes (in this case, we'll reset or set the error message property, which we'll display on the UI in a moment).
Call this method inside the signInWithEmailAndPassword, both at the beginning and inside the catching of the FirebaseAuthException, so we reset the value initially, and in the event of an error, we capture it and set it, as follows:
Back in the FlutterBankLogin widget, inside the text fields' structure, below the password field, we'll insert a Consumer widget that will listen to the LoginService upon any changes that occur in this service (mostly related to the error message) and thus this widget will display the state of the error message in a simple widget structure composed of an Icon and a Text widget.
Let's proceed:
Right below the password TextField, insert a Consumer widget that listens to the LoginService; return an empty container from its required builder method:
Now, this widget, upon any changes triggered by the LoginService (via calling its notifyListeners method internally) will cause its builder method to execute, thus rebuilding its contents. So on every turn, we check for the existence of an error message by calling the provided LoginService instance through its builder method (lService) and its convenient method getErrorMessage. If the error is empty, then just return an empty SizedBox widget, 40px height (to compensate for the absence of a label):
Otherwise, return a widget structure leveraging the existing Container widget being returned, as follows:
Your code should look like this:
Now, let's take this implementation for a spin. Type the right email, but with the wrong password. Then try a wrong email (valid also), and with the wrong password. Observe the behavior - what errors do you get? It should behave as in the animation below:
What I love about this implementation is that the Firebase Authentication is providing me with these relevant and proper error messages based on the validation it performs on its end, that way my app is supported on both fronts. Thank you Firebase!
Let's perform some clean-up tasks in our FlutterBankLogin widget now that we're pretty much done with this screen, for example we need to dispose of some of the components we've created and used, such as the TextEditingController instances we used for each of our fields.
In the FlutterBankLoginState class, override the dispose method; this method is used for those same purposes: cleaning up resources before this widget itself gets disposed by the framework. Let's go ahead and dispose of the controllers in this method, prior to disposing of this widget itself, as follows:
In this codelab, we accomplished the following:
In the next codelab, we'll work on the landing page where we'll be consuming data from Cloud Firestore, creating more custom widgets, set up the signing out functionality, and more, so head on to the next codelab! See you there!
This is a nice-to-have, but great if you could integrate it in your app, which is the ability to create new user accounts programmatically right from the app as opposed to manually the way we did it earlier through the Firebase console; that's why I'm providing it as a bonus codelab section.
The following is what we'll be accomplishing:
Let's proceed and create the screen for it.
We'll do the same pretty much as in the login screen, where we'll create a class that extends StatefulWidget since we need to maintain some local state to trigger rebuilds based on the state of the textfields and buttons. Let's call it FlutterAccountRegistration, and return an empty Scaffold from its build method:
Let's start adding some pieces to our Scaffold widget and make sure that we can see it while we develop it.
Add something to the Scaffold's body real quick to see it on the screen; like a Center widget with a Text widget as its child, like so:
Running this through DartPad to confirm we can see the account registration widget we're building:
Let's continue!
In the FlutterAccountRegistrationState class, add the required controllers; we will be managing three textfields (new username, password and password confirmation); just so we don't forget, go ahead and override the dispose method and dispose of them now as opposed to leave it ‘til the end:
Back to the widget's build method; in the Scaffold, set its background color to white, plus add a custom-styled AppBar widget so it matches our design, by setting the Scaffold's appBar property. Add the following specs to it:
Your code should look like this inside the build method:
And if you run this through DartPad, you should see this:
Let's move along and focus on the inner structure. We'll need a Column widget to lay out our widgets vertically (the three fields and the button) so we'll do just that.
Replace the existing Center widget in the body of the Scaffold by a Container widget with 30px of padding all around, and as its child a Column with its items aligned to the left horizontally:
Let's work on the top portion of the account registration widget structure.
This will be composed of a Text widget for a title, plus the three textfields. We want this section to cover most of the real estate in the column, so we'll build it inside an Expanded widget, that in turn wraps a Column widget with its children left align, as follows:
I'll start by adding the title of this page widget using a Text widget with the proper style, and adding some spacing by wrapping it inside a Container with 40px of margin below it:
Let's start looking at how it's coming out on DartPad:
Just like before, we'll enclose our textfields in a Container widget with some styling to make them look round and borderless, with 20px of margin underneath them, plus all the styles they require. I feel we could refactor a bit how we build these widgets and use some helper methods, shall we?
Instead of typing out all the long InputDecoration block, as well as repeat the Container styles for each of these fields, I'll create a utility method for the fields that generate this chunk of widget code for me.
Let's go to the Utils class, and create a method called generateInputField that will take the following parameters:
Your code should look like this:
In the provided Container inside this utility method, then populate it with the Container related styles, and the TextField widget, populating all parameters being supplied as follows:
This way we can automate and optimize the way we are creating these widgets.
Now back to our account registration page, you can use this utility method as such:
Running it through DartPad and we get this:
A bit tight in between the fields. Well, just go back to the Utils.generateInputField method, and add a margin: const EdgeInsets.only(bottom: 20) to the parent *Container widget and you'll see how it applies to all of them right away:
Run it again through DartPad:
Nice! See how easy is to create utility methods that generate boilerplate widget code for you! Such a beauty!
Let's keep rolling - now we'll work on the bottom button that will trigger the registration process.
Right under the Expanded widget enclosing all fields, let's add (yeah, another one of our reusable widgets - now notice the advantage of creating custom reusable widgets!) our FlutterBankMainButton here, with the label Register, enabled true with an empty callback handler for its onTap event:
View it on DartPad by hitting Run:
Nice - our UI is fully in place. Now let's add the functionality it requires to trigger the registration, starting from the Service.
We don't have this functionality yet so let's add it to our login service.
Let's add a method called createUserWithEmailAndPassword, which will take a String for the email, and another one for the password, and returns a Future of bool as it will be an asynchronous call and we should wait on it, therefore decorate the block with the async keyword as well:
Just like before, add a try/catch (or a more condensed version, a try/on) exception block to make the account creation call inside of it; call the FirebaseAuth.instance.createUserWithEmailAndPassword, passing both the email and password provided, and return true assuming the returned userCredentials are available, otherwise return false from the captured FirebaseAuthException, as below:
With that in place, let's go ahead and consume it on our account registration widget.
Back on the FlutterAccountRegistrationState class, inside and at the top of the build method, let's bring an instance of the LoginService using the Provider.of, passing the content and the listen: false flag:
Navigate to the "Register" FlutterBankMainButton widget within this structure, and on its onTap handler, do the following:
Your code should look like this:
Before we wrap up, let's add some needed validation. I'll leave the additional error handling and displaying to your as homework ;).
Inside this same FlutterAccountRegistrationState class, let's add a simple method to perform the validation on the values in our fields, checking for the following:
Your code should look like this:
Back to the Register button, replace the hardcoded enabled value of true by whatever this validation method returns, as such:
The validation then should look like this:
Now for this to work we have to navigate to this page from the login page, otherwise we'll get an exception when we attempt to do a Navigator.of(context).pop() since we don't have anything to get back to (remember what we did on the FlutterBankApp widget?). Let's undo that (bring back the splash page):
Now, go to the FlutterBankLoginState widget page, find the FlutterBankMainButton for the Register action, and on its onTap event handler, just simply navigate to the FlutterAccountRegistration using a Navigator.of(context), pushing a new MaterialPageRoute that wraps this page:
Take this for a spin on DartPad and execute the account registration workflow:
Ensure it is an account that doesn't exist for this to work (I'll try testclient@gmail.com, with password 123456) and check out if it made it all the way to Firebase's list of registered users.
And indeed my new user is registered, now I can go ahead and log in with this new user! Amazing work, y'all!
And with this, we officially wrap up this bonus codelab portion. Kudos for the extra work done and thanks for sticking around all the way!
In case you fell behind on this codelab, below is the whole code for this codelab in a way you can copy / paste directly into DartPad:
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';
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(),
)
],
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: () {},
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 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 FlutterBankMain extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('main page'),
)
);
}
}
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));
}
}
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> 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;
}
}
}
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';
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(),
)
],
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: 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 FlutterBankMain extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('main page'),
)
);
}
}
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),
)
);
}
}
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> 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;
}
}
}