Created with <3 with dartpad.dev.
Last active
October 7, 2023 19:35
-
-
Save plotsklapps/c7e023b17da9a6c02606964de4810775 to your computer and use it in GitHub Desktop.
Flutter Fullstack #4
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 'dart:async'; | |
| import 'package:cloud_firestore/cloud_firestore.dart'; | |
| 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:intl/intl.dart'; | |
| import 'package:provider/provider.dart'; | |
| void main() async { | |
| WidgetsFlutterBinding.ensureInitialized(); | |
| await Firebase.initializeApp( | |
| options: 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(); | |
| }), | |
| ChangeNotifierProvider(create: (_) { | |
| return FlutterBankService(); | |
| }), | |
| ], | |
| 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 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( | |
| backgroundColor: Colors.white, | |
| 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( | |
| controller: emailController, | |
| onChanged: (text) { | |
| setState(() { | |
| // We use setState to update the state of the widget. In this case, | |
| // we are updating the state of the 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(20.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( | |
| controller: passwordController, | |
| onChanged: (text) { | |
| setState(() { | |
| // We use setState to update the state of the widget. In this case, | |
| // we are updating the state of the 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.push(context, | |
| 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.push(context, MaterialPageRoute(builder: (context) { | |
| return const FlutterBankRegister(); | |
| })); | |
| }, | |
| backgroundColor: Utils.mainThemeColor.withOpacity(0.1), | |
| iconColor: Utils.mainThemeColor, | |
| labelColor: Utils.mainThemeColor, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankRegister extends StatefulWidget { | |
| const FlutterBankRegister({super.key}); | |
| @override | |
| State<FlutterBankRegister> createState() { | |
| return 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, | |
| backgroundColor: Colors.transparent, | |
| iconTheme: const IconThemeData(color: Utils.mainThemeColor), | |
| 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, | |
| ), | |
| ), | |
| ), | |
| 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.text; | |
| String password = passwordController.text; | |
| bool isAccountCreated = await loginService | |
| .createUserWithEmailAndPassword(email, password); | |
| if (isAccountCreated) { | |
| if (mounted) { | |
| Navigator.pop(context); | |
| } | |
| } | |
| }), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankMain extends StatelessWidget { | |
| const FlutterBankMain({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return SafeArea( | |
| child: Scaffold( | |
| backgroundColor: Colors.white, | |
| drawer: const Drawer( | |
| child: FlutterBankDrawer(), | |
| ), | |
| appBar: AppBar( | |
| elevation: 0.0, | |
| backgroundColor: Colors.transparent, | |
| iconTheme: const IconThemeData(color: Utils.mainThemeColor), | |
| title: const Icon(Icons.savings, | |
| color: Utils.mainThemeColor, size: 40.0), | |
| centerTitle: true, | |
| ), | |
| body: Container( | |
| padding: const EdgeInsets.all(20.0), | |
| child: Column( | |
| children: [ | |
| const Row( | |
| children: [ | |
| Icon( | |
| Icons.account_balance_wallet, | |
| color: Utils.mainThemeColor, | |
| size: 30.0, | |
| ), | |
| SizedBox(width: 10.0), | |
| Text( | |
| 'My Accounts', | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 20.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 20.0), | |
| Expanded( | |
| // Consume the service FlutterBankService using a Consumer | |
| // widget, since we want to be notified of any changes | |
| // within this service so we can rebuild accordingly. | |
| child: Consumer<FlutterBankService>( | |
| builder: (context, bankService, child) { | |
| // FutureBuilder widgets take a future (in our case, the one returned | |
| // from FlutterBankService's getAccounts), and a builder (that gets | |
| // triggered upon the Future changing its state), which is a callback | |
| // method that gets the current BuildContext and a snapshot - a | |
| // wrapper object (type AsyncSnapshot that contains the value returned | |
| // by the Future object in question (in our case, a List of Account objects) | |
| return FutureBuilder( | |
| future: bankService.getAccounts(context), | |
| builder: (BuildContext context, AsyncSnapshot snapshot) { | |
| if (snapshot.connectionState != ConnectionState.done || | |
| !snapshot.hasData) { | |
| return const FlutterBankLoading(); | |
| } | |
| List<Account> accounts = snapshot.data as List<Account>; | |
| if (accounts.isEmpty) { | |
| return const Center( | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| Icons.account_balance_wallet, | |
| color: Utils.mainThemeColor, | |
| size: 50.0, | |
| ), | |
| SizedBox(height: 20.0), | |
| 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: const FlutterBankBottomBar(), | |
| ), | |
| ); | |
| } | |
| } | |
| // 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 AccountCard extends StatelessWidget { | |
| final Account? account; | |
| const AccountCard({Key? key, this.account}) : super(key: key); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Container( | |
| height: 180.0, | |
| padding: const EdgeInsets.all(20.0), | |
| margin: const EdgeInsets.only(bottom: 20.0), | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(20.0), | |
| color: Colors.white, | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withOpacity(0.1), | |
| blurRadius: 15.0, | |
| offset: const Offset(0.0, 5.0), | |
| ), | |
| ], | |
| ), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Column( | |
| children: [ | |
| Text( | |
| '${account!.type?.toUpperCase()} ACCOUNT', | |
| textAlign: TextAlign.left, | |
| style: const TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 12.0, | |
| ), | |
| ), | |
| Text('*** ${account!.accountNumber}'), | |
| ], | |
| ), | |
| Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| 'Balance', | |
| textAlign: TextAlign.left, | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 12.0, | |
| ), | |
| ), | |
| Row( | |
| children: [ | |
| const Icon(Icons.monetization_on, | |
| color: Utils.mainThemeColor, size: 30.0), | |
| Text( | |
| '\$${account!.balance?.toStringAsFixed(2)}', | |
| style: const TextStyle( | |
| color: Colors.black, | |
| fontSize: 35.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| Text( | |
| 'As of ${DateFormat.yMd().add_jm().format(DateTime.now())}', | |
| style: const TextStyle( | |
| fontSize: 10.0, | |
| color: Colors.grey, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankLoading extends StatelessWidget { | |
| const FlutterBankLoading({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return const Center( | |
| child: SizedBox( | |
| height: 80.0, | |
| width: 80.0, | |
| child: Stack( | |
| children: [ | |
| Center( | |
| child: SizedBox( | |
| width: 80.0, | |
| height: 80.0, | |
| child: CircularProgressIndicator( | |
| strokeWidth: 8.0, | |
| valueColor: | |
| AlwaysStoppedAnimation<Color>(Utils.mainThemeColor), | |
| ), | |
| ), | |
| ), | |
| Center( | |
| child: Icon( | |
| Icons.savings, | |
| color: Utils.mainThemeColor, | |
| size: 40.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankBottomBarItem { | |
| String? label; | |
| IconData? icon; | |
| Function? onTap; | |
| FlutterBankBottomBarItem({ | |
| this.label, | |
| this.icon, | |
| this.onTap, | |
| }); | |
| } | |
| class FlutterBankBottomBar extends StatelessWidget { | |
| const FlutterBankBottomBar({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| List<FlutterBankBottomBarItem> bottomItems = Utils.getBottomBarItems(); | |
| return Container( | |
| padding: const EdgeInsets.all(20.0), | |
| height: 100.0, | |
| decoration: BoxDecoration( | |
| color: Colors.white, | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withOpacity(0.1), | |
| blurRadius: 10.0, | |
| 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.0), | |
| clipBehavior: Clip.antiAlias, | |
| child: InkWell( | |
| highlightColor: Utils.mainThemeColor.withOpacity(0.2), | |
| splashColor: Utils.mainThemeColor.withOpacity(0.1), | |
| onTap: () { | |
| bottomItem.onTap!(); | |
| }, | |
| child: Container( | |
| constraints: const BoxConstraints( | |
| minWidth: 80.0, | |
| ), | |
| padding: const EdgeInsets.all(10.0), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.end, | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| bottomItem.icon, | |
| color: Utils.mainThemeColor, | |
| size: 20.0, | |
| ), | |
| Text( | |
| bottomItem.label!, | |
| style: const TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 10.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| }), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankDrawer extends StatelessWidget { | |
| const FlutterBankDrawer({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Container( | |
| padding: const EdgeInsets.all(30.0), | |
| color: Utils.mainThemeColor, | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Icon(Icons.savings, color: Colors.white, size: 60.0), | |
| const SizedBox(height: 40.0), | |
| Material( | |
| color: Colors.transparent, | |
| 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: () {}, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class Utils { | |
| static const Color mainThemeColor = Color(0xFF8700C3); | |
| static List<FlutterBankBottomBarItem> getBottomBarItems() { | |
| return [ | |
| FlutterBankBottomBarItem( | |
| label: 'Withdraw', | |
| icon: Icons.logout, | |
| onTap: () {}, | |
| ), | |
| FlutterBankBottomBarItem( | |
| label: 'Deposit', | |
| icon: Icons.login, | |
| onTap: () {}, | |
| ), | |
| FlutterBankBottomBarItem( | |
| label: 'Expenses', | |
| icon: Icons.payments, | |
| onTap: () {}, | |
| ), | |
| ]; | |
| } | |
| 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 icon, | |
| TextEditingController controller, | |
| 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( | |
| controller: controller, | |
| onChanged: (text) { | |
| onChanged(text); | |
| }, | |
| obscureText: isPasswordField, | |
| decoration: InputDecoration( | |
| prefixIcon: Icon( | |
| icon, | |
| 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, | |
| ), | |
| style: const TextStyle( | |
| fontSize: 16.0, | |
| ), | |
| ), | |
| ); | |
| } | |
| static void signOutDialog(BuildContext context) { | |
| showDialog( | |
| context: context, | |
| builder: (BuildContext context) { | |
| return AlertDialog( | |
| title: const Text( | |
| 'Flutter Savings Bank Sign Out', | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| ), | |
| ), | |
| content: Container( | |
| padding: const EdgeInsets.all(20.0), | |
| child: const Text('Are you ' | |
| 'sure you want to sign out?')), | |
| actions: [ | |
| TextButton( | |
| onPressed: () { | |
| Navigator.pop(context); | |
| }, | |
| child: const Text('Cancel'), | |
| ), | |
| TextButton( | |
| onPressed: () async { | |
| LoginService loginService = | |
| Provider.of<LoginService>(context, listen: false); | |
| await loginService.signOut(); | |
| if (context.mounted) { | |
| Navigator.pushReplacement(context, | |
| MaterialPageRoute(builder: (context) { | |
| return const FlutterBankLogin(); | |
| })); | |
| } | |
| }, | |
| child: const Text('YES, SIGN OUT'), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| 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; | |
| } | |
| } | |
| Future<bool> signOut() { | |
| Completer<bool> signOutCompleter = Completer(); | |
| FirebaseAuth.instance.signOut().then((value) { | |
| signOutCompleter.complete(true); | |
| }, onError: (error) { | |
| signOutCompleter.completeError({'error': error}); | |
| }); | |
| return signOutCompleter.future; | |
| } | |
| } | |
| class FlutterBankService extends ChangeNotifier { | |
| Future<List<Account>> getAccounts(BuildContext context) { | |
| LoginService loginService = | |
| Provider.of<LoginService>(context, listen: false); | |
| String userId = loginService.getUserId(); | |
| List<Account> accounts = []; | |
| // A Completer allows you to create Futures from scratch and for | |
| // situations when you have callback-based API calls (like using .then()) | |
| // or when you want to delay the completing of the Future to a later moment. | |
| Completer<List<Account>> accountsCompleter = Completer(); | |
| // In the one-time read, data is fetched only once, using the .get() call. | |
| // When querying for a collection of documents, you capture the result in a | |
| // QuerySnapshot which returns a collection of document references | |
| // (not the actual documents); when querying for a single document, you | |
| // capture the result in a DocumentSnapshot, which is a reference to the | |
| // document (not the actual document); you get the document data when you | |
| // call .data() on the reference. | |
| FirebaseFirestore.instance | |
| .collection('accounts') | |
| .doc('IJrYcBqjkzcKgqBmSGlPxr2qWmn1') | |
| .collection('user_accounts') | |
| .get() | |
| .then((QuerySnapshot collection) { | |
| // Inside the callback, loop through the document references returned | |
| // via the QuerySnapshot by pulling all document references | |
| // (via the .docs property of the QuerySnapshot). Get the data | |
| // out of each document reference (via the .data() call) and | |
| // cast it to a **Map≶String, dynamic>). | |
| for (var doc in collection.docs) { | |
| // map this data to a strongly-type model (our Account model | |
| // created earlier) via its factory method .fromJson. | |
| // Feed both the data and document it this method which | |
| // should return an instance of Acount. Push this instance | |
| // to our accounts collection. | |
| var acctDoc = doc.data() as Map<String, dynamic>; | |
| var acct = Account.fromJson(acctDoc, doc.id); | |
| accounts.add(acct); | |
| } | |
| // After mapping and collecting all values, introduce a | |
| // small delay using Future's utility method delayed and add a | |
| // 2-second delay, then complete the Future generated by the | |
| // completer by calling accountsCompleter.complete, passing | |
| // the already populated collection: | |
| Future.delayed(const Duration(seconds: 1), () { | |
| accountsCompleter.complete(accounts); | |
| }); | |
| }); | |
| return accountsCompleter.future; | |
| // What this will do is: we'll return a Future from the Completer immediately, | |
| // to the user. The user will await on this Future while we fetch the data | |
| // from Firebase; once the data is ready (mapped and collected), we notify | |
| // them through that same Future via the Completer's .complete call, | |
| // passing the same type of data this method is returning via the method | |
| // signature (Future<List<Account>>). | |
| } | |
| } | |
| // MODELS | |
| class Account { | |
| String? id; | |
| String? type; | |
| String? accountNumber; | |
| int? balance; | |
| Account({ | |
| this.id, | |
| this.type, | |
| this.accountNumber, | |
| this.balance, | |
| }); | |
| // Create a factory method to map the incoming JSON structure from Firebase | |
| // (as a Map<String, dynamic>), and takes both the Map as well as the | |
| // unique document's ID. | |
| factory Account.fromJson(Map<String, dynamic> json, String docId) { | |
| return Account( | |
| id: docId, | |
| type: json['type'], | |
| accountNumber: json['account_number'], | |
| balance: json['balance'], | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment