Created with <3 with dartpad.dev.
Last active
October 3, 2023 09:04
-
-
Save plotsklapps/6ef33145d76f284122dcc5e5011c8aa5 to your computer and use it in GitHub Desktop.
Flutter Fullstack #3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:firebase_auth/firebase_auth.dart'; | |
| import 'package:firebase_core/firebase_core.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:google_fonts/google_fonts.dart'; | |
| import 'package:provider/provider.dart'; | |
| void main() async { | |
| WidgetsFlutterBinding.ensureInitialized(); | |
| await Firebase.initializeApp( | |
| options: // For Firebase JS SDK v7.20.0 and later, measurementId is optional | |
| const FirebaseOptions( | |
| apiKey: "AIzaSyB1mTcVNqXCMLviCjb4NZ35l0r2u6jwv0s", | |
| authDomain: "codelab-flutterbankapp.firebaseapp.com", | |
| projectId: "codelab-flutterbankapp", | |
| storageBucket: "codelab-flutterbankapp.appspot.com", | |
| messagingSenderId: "1005151447135", | |
| appId: "1:1005151447135:web:663e127a0473fe49a0d3ea", | |
| measurementId: "G-MH8JWFX1LF"), | |
| ); | |
| runApp( | |
| MultiProvider( | |
| providers: [ | |
| ChangeNotifierProvider(create: (_) { | |
| return LoginService(); | |
| }) | |
| ], | |
| child: const FlutterBankApp(), | |
| ), | |
| ); | |
| } | |
| class FlutterBankApp extends StatelessWidget { | |
| const FlutterBankApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| debugShowCheckedModeBanner: false, | |
| theme: ThemeData( | |
| textTheme: GoogleFonts.poppinsTextTheme( | |
| Theme.of(context).textTheme, | |
| ), | |
| ), | |
| home: const FlutterBankSplash(), | |
| ); | |
| } | |
| } | |
| class FlutterBankSplash extends StatelessWidget { | |
| const FlutterBankSplash({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| // Future.delayed takes two parameters: a Duration object with is seconds | |
| // property set to 2, and a callback. When the 2 seconds have ellapsed, it will call the callback. The callback has inside a trigger to perform a navigation | |
| Future.delayed(const Duration(seconds: 2), () { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute(builder: (context) { | |
| return const FlutterBankLogin(); | |
| }), | |
| ); | |
| }); | |
| return const Scaffold( | |
| backgroundColor: Utils.mainThemeColor, | |
| body: Stack( | |
| children: [ | |
| Center( | |
| child: Icon( | |
| Icons.savings, | |
| size: 60.0, | |
| color: Colors.white, | |
| ), | |
| ), | |
| Center( | |
| child: SizedBox( | |
| height: 100.0, | |
| width: 100.0, | |
| child: CircularProgressIndicator( | |
| strokeWidth: 8.0, | |
| valueColor: AlwaysStoppedAnimation<Color>(Colors.white), | |
| ), | |
| )), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankRegister extends StatefulWidget { | |
| const FlutterBankRegister({super.key}); | |
| @override | |
| State<FlutterBankRegister> createState() => _FlutterBankRegisterState(); | |
| } | |
| class _FlutterBankRegisterState extends State<FlutterBankRegister> { | |
| TextEditingController emailController = TextEditingController(); | |
| TextEditingController passwordController = TextEditingController(); | |
| TextEditingController confirmPasswordController = TextEditingController(); | |
| @override | |
| void dispose() { | |
| // Dispose of the emailController and passwordController instances when the | |
| // widget is disposed of. | |
| emailController.dispose(); | |
| passwordController.dispose(); | |
| confirmPasswordController.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| LoginService loginService = Provider.of<LoginService>( | |
| context, | |
| listen: false, | |
| ); | |
| bool validateFormFields() { | |
| return Utils.validateEmail(emailController.text) && | |
| emailController.text.isNotEmpty && | |
| passwordController.text.isNotEmpty && | |
| confirmPasswordController.text.isNotEmpty && | |
| (passwordController.text == confirmPasswordController.text); | |
| } | |
| return SafeArea( | |
| child: Scaffold( | |
| backgroundColor: Colors.white, | |
| appBar: AppBar( | |
| elevation: 0.0, | |
| iconTheme: const IconThemeData(color: Utils.mainThemeColor), | |
| backgroundColor: Colors.transparent, | |
| title: const Icon( | |
| Icons.savings, | |
| color: Utils.mainThemeColor, | |
| size: 40.0, | |
| ), | |
| centerTitle: true, | |
| ), | |
| body: Container( | |
| padding: const EdgeInsets.all(30.0), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Expanded( | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Container( | |
| margin: const EdgeInsets.only(bottom: 40.0), | |
| child: const Text( | |
| 'Create New Account', | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 20.0, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 40.0), | |
| Utils.generateInputField( | |
| 'Email Address', Icons.email, emailController, false, | |
| (text) { | |
| setState(() {}); | |
| }), | |
| Utils.generateInputField( | |
| 'Password', Icons.lock, passwordController, true, | |
| (text) { | |
| setState(() {}); | |
| }), | |
| Utils.generateInputField('Confirm Password', Icons.lock, | |
| confirmPasswordController, true, (text) { | |
| setState(() {}); | |
| }), | |
| ], | |
| ), | |
| ), | |
| FlutterBankMainButton( | |
| label: 'Register', | |
| enabled: validateFormFields(), | |
| onTap: () async { | |
| String email = emailController.value.text; | |
| String password = passwordController.value.text; | |
| bool isAccountCreated = await loginService | |
| .createUserWithEmailAndPassword(email, password); | |
| if (isAccountCreated) { | |
| if (mounted) Navigator.of(context).pop(); | |
| } | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankLogin extends StatefulWidget { | |
| const FlutterBankLogin({super.key}); | |
| @override | |
| State<FlutterBankLogin> createState() { | |
| return _FlutterBankLoginState(); | |
| } | |
| } | |
| class _FlutterBankLoginState extends State<FlutterBankLogin> { | |
| // At the top of the FlutterBankLoginState class, add two | |
| // TextEditingController instances, one for each of our fields. The | |
| // TextEditingController allows us to controller the actions on a TextField | |
| // widget, get notifications on text field updates, set initial values, | |
| // get its provided input, etc. | |
| final TextEditingController emailController = TextEditingController(); | |
| final TextEditingController passwordController = TextEditingController(); | |
| validateEmailAndPassword() { | |
| return emailController.text.isNotEmpty && | |
| passwordController.text.isNotEmpty && | |
| Utils.validateEmail(emailController.text); | |
| } | |
| @override | |
| void dispose() { | |
| // Dispose of the emailController and passwordController instances when the | |
| // widget is disposed of. | |
| emailController.dispose(); | |
| passwordController.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| // 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. | |
| LoginService loginService = Provider.of<LoginService>( | |
| context, | |
| listen: false, | |
| ); | |
| return SafeArea( | |
| child: Scaffold( | |
| body: Container( | |
| padding: const EdgeInsets.all(30.0), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Container( | |
| width: 80.0, | |
| height: 80.0, | |
| decoration: BoxDecoration( | |
| border: Border.all( | |
| color: Utils.mainThemeColor, | |
| width: 7.0, | |
| ), | |
| borderRadius: BorderRadius.circular(100.0), | |
| ), | |
| child: const Icon( | |
| Icons.savings, | |
| size: 45.0, | |
| color: Utils.mainThemeColor, | |
| ), | |
| ), | |
| const SizedBox(height: 30.0), | |
| const Text( | |
| 'Welcome to', | |
| style: TextStyle( | |
| color: Colors.grey, | |
| fontSize: 15.0, | |
| ), | |
| ), | |
| const Text( | |
| 'Flutter\nSavings Bank', | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 30.0, | |
| ), | |
| ), | |
| Expanded( | |
| child: Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: [ | |
| const Text( | |
| 'Sign Into Your Bank Account', | |
| style: TextStyle( | |
| color: Colors.grey, | |
| fontSize: 12.0, | |
| ), | |
| textAlign: TextAlign.center, | |
| ), | |
| const SizedBox(height: 12.0), | |
| Container( | |
| padding: const EdgeInsets.all(5.0), | |
| decoration: BoxDecoration( | |
| color: Colors.grey.withOpacity(0.2), | |
| borderRadius: BorderRadius.circular(50.0), | |
| ), | |
| child: TextField( | |
| onChanged: (text) { | |
| setState(() { | |
| // We use setState to update the state of the widget. In this case, | |
| // we are updating the state of the emailController. | |
| }); | |
| }, | |
| controller: emailController, | |
| decoration: const InputDecoration( | |
| prefixIcon: Icon( | |
| Icons.email, | |
| color: Utils.mainThemeColor, | |
| ), | |
| border: InputBorder.none, | |
| focusedBorder: InputBorder.none, | |
| enabledBorder: InputBorder.none, | |
| errorBorder: InputBorder.none, | |
| disabledBorder: InputBorder.none, | |
| contentPadding: | |
| EdgeInsets.fromLTRB(15.0, 11.0, 15.0, 11.0), | |
| hintText: 'Email Address', | |
| ), | |
| style: const TextStyle( | |
| fontSize: 16.0, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(height: 16.0), | |
| Container( | |
| padding: const EdgeInsets.all(5.0), | |
| decoration: BoxDecoration( | |
| color: Colors.grey.withOpacity(0.2), | |
| borderRadius: BorderRadius.circular(50.0), | |
| ), | |
| child: TextField( | |
| onChanged: (text) { | |
| setState(() { | |
| // We use setState to update the state of the widget. In this case, | |
| // we are updating the state of the passwordController. | |
| }); | |
| }, | |
| controller: passwordController, | |
| obscureText: true, | |
| 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.fromLTRB(20.0, 11.0, 15.0, 11.0), | |
| hintText: 'Password', | |
| ), | |
| style: const TextStyle( | |
| fontSize: 16.0, | |
| ), | |
| ), | |
| ), | |
| Consumer<LoginService>( | |
| builder: (context, loginService, child) { | |
| String errorMessage = loginService.getErrorMessage(); | |
| if (errorMessage.isEmpty) { | |
| return const SizedBox(height: 40.0); | |
| } else { | |
| return Container( | |
| padding: const EdgeInsets.all(10.0), | |
| child: Row( | |
| children: [ | |
| const Icon( | |
| Icons.warning, | |
| color: Colors.red, | |
| ), | |
| const SizedBox(width: 10.0), | |
| Expanded( | |
| child: Text( | |
| errorMessage, | |
| style: const TextStyle( | |
| color: Colors.red, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| FlutterBankMainButton( | |
| label: 'Sign In', | |
| enabled: validateEmailAndPassword(), | |
| onTap: () async { | |
| String email = emailController.text; | |
| String password = passwordController.text; | |
| bool isLoggedIn = | |
| await loginService.signInWithEmailAndPassword( | |
| email, | |
| password, | |
| ); | |
| if (isLoggedIn) { | |
| emailController.clear(); | |
| passwordController.clear(); | |
| if (mounted) { | |
| Navigator.of(context).push( | |
| MaterialPageRoute(builder: (context) { | |
| return const FlutterBankMain(); | |
| }), | |
| ); | |
| } | |
| } else { | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar( | |
| content: Text('Invalid credentials'), | |
| behavior: SnackBarBehavior.floating, | |
| ), | |
| ); | |
| } | |
| } | |
| }, | |
| ), | |
| const SizedBox(height: 16.0), | |
| FlutterBankMainButton( | |
| label: 'Register', | |
| icon: Icons.account_circle, | |
| enabled: true, | |
| onTap: () { | |
| Navigator.of(context) | |
| .push(MaterialPageRoute(builder: (context) { | |
| return const FlutterBankRegister(); | |
| })); | |
| }, | |
| backgroundColor: Utils.mainThemeColor.withOpacity(0.1), | |
| iconColor: Utils.mainThemeColor, | |
| labelColor: Utils.mainThemeColor, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankMain extends StatelessWidget { | |
| const FlutterBankMain({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return const SafeArea( | |
| child: Scaffold( | |
| body: Center( | |
| child: Text('Main Page'), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| // Widgets | |
| 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({ | |
| super.key, | |
| this.onTap, | |
| this.label, | |
| this.enabled = true, | |
| this.icon, | |
| this.backgroundColor = Utils.mainThemeColor, | |
| this.iconColor = Colors.white, | |
| this.labelColor = Colors.white, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.stretch, | |
| children: [ | |
| ClipRRect( | |
| borderRadius: BorderRadius.circular(50.0), | |
| child: Material( | |
| color: | |
| enabled! ? backgroundColor : backgroundColor!.withOpacity(0.5), | |
| child: InkWell( | |
| // Assign a callback that wraps the trigger this widget's onTap event only | |
| // if the enabled property is true, null otherwise. | |
| onTap: enabled! | |
| ? () { | |
| onTap!(); | |
| } | |
| : null, | |
| highlightColor: Colors.white.withOpacity(0.2), | |
| splashColor: Colors.white.withOpacity(0.1), | |
| child: Container( | |
| padding: const EdgeInsets.all(15.0), | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(50.0), | |
| ), | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| children: [ | |
| Visibility( | |
| visible: icon != null, | |
| child: Container( | |
| margin: const EdgeInsets.only(right: 20.0), | |
| child: Icon( | |
| icon, | |
| color: iconColor, | |
| size: 20.0, | |
| ), | |
| )), | |
| Text( | |
| label!, | |
| textAlign: TextAlign.center, | |
| style: TextStyle( | |
| color: labelColor, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| 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 textEditingController, | |
| bool isPasswordField, | |
| Function onChanged, | |
| ) { | |
| return Container( | |
| padding: const EdgeInsets.all(5.0), | |
| margin: const EdgeInsets.only(bottom: 20.0), | |
| decoration: BoxDecoration( | |
| color: Colors.grey.withOpacity(0.2), | |
| borderRadius: BorderRadius.circular(50.0), | |
| ), | |
| child: TextField( | |
| onChanged: (text) { | |
| onChanged(text); | |
| }, | |
| controller: textEditingController, | |
| decoration: InputDecoration( | |
| prefixIcon: const Icon( | |
| Icons.lock, | |
| color: Utils.mainThemeColor, | |
| ), | |
| border: InputBorder.none, | |
| focusedBorder: InputBorder.none, | |
| enabledBorder: InputBorder.none, | |
| errorBorder: InputBorder.none, | |
| disabledBorder: InputBorder.none, | |
| contentPadding: const EdgeInsets.fromLTRB(20.0, 11.0, 15.0, 11.0), | |
| hintText: hintText, | |
| ), | |
| obscureText: isPasswordField, | |
| obscuringCharacter: '*', | |
| style: const TextStyle( | |
| fontSize: 16.0, | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class LoginService extends ChangeNotifier { | |
| String userId = ''; | |
| String errorMessage = ''; | |
| String getUserId() { | |
| return userId; | |
| } | |
| String getErrorMessage() { | |
| return errorMessage; | |
| } | |
| void setLoginErrorMessage(String message) { | |
| errorMessage = message; | |
| notifyListeners(); | |
| } | |
| void setSignUpErrorMessage(String message) { | |
| errorMessage = message; | |
| notifyListeners(); | |
| } | |
| Future<bool> createUserWithEmailAndPassword( | |
| String email, String password) async { | |
| setLoginErrorMessage(''); | |
| try { | |
| UserCredential userCredential = | |
| await FirebaseAuth.instance.createUserWithEmailAndPassword( | |
| email: email, | |
| password: password, | |
| ); | |
| userId = userCredential.user!.uid; | |
| return true; | |
| } on FirebaseAuthException catch (exc) { | |
| setSignUpErrorMessage( | |
| 'There was an error during sign-up: ${exc.message}'); | |
| return false; | |
| } | |
| } | |
| Future<bool> signInWithEmailAndPassword(String email, String password) async { | |
| setLoginErrorMessage(''); | |
| try { | |
| UserCredential userCredential = | |
| await FirebaseAuth.instance.signInWithEmailAndPassword( | |
| email: email, | |
| password: password, | |
| ); | |
| userId = userCredential.user!.uid; | |
| return true; | |
| } on FirebaseAuthException catch (exc) { | |
| setLoginErrorMessage('There was an error during sign-in: ${exc.message}'); | |
| return false; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment