Created with <3 with dartpad.dev.
Created
October 8, 2023 17:24
-
-
Save plotsklapps/5aad6a23dfbb1f39988996eed450c60a to your computer and use it in GitHub Desktop.
Flutter Fullstack #5
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(); | |
| }), | |
| ChangeNotifierProvider(create: (_) { | |
| return DepositService(); | |
| }), | |
| ], | |
| 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(), | |
| ); | |
| } | |
| } | |
| /// PAGES | |
| 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(), | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankDeposit extends StatelessWidget { | |
| const FlutterBankDeposit({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return WillPopScope( | |
| onWillPop: () async { | |
| FlutterBankService bankService = Provider.of<FlutterBankService>( | |
| context, | |
| listen: false, | |
| ); | |
| bankService.resetSelections(); | |
| return Future.value(true); | |
| }, | |
| child: 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(20.0), | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.start, | |
| children: [ | |
| const AccountActionHeader( | |
| headerTitle: 'Deposit', | |
| icon: Icons.login, | |
| ), | |
| const Expanded( | |
| child: AccountActionSelection( | |
| actionTypeLabel: 'To', | |
| amountChanger: AccountDepositSlider(), | |
| ), | |
| ), | |
| Consumer<DepositService>( | |
| builder: (context, depositService, child) { | |
| return FlutterBankMainButton( | |
| label: 'Make Deposit', | |
| enabled: depositService.checkAmountToDeposit(), | |
| onTap: depositService.checkAmountToDeposit() | |
| ? () { | |
| Navigator.push(context, | |
| MaterialPageRoute(builder: (context) { | |
| return const TransActionCompletePage( | |
| isDeposit: true, | |
| ); | |
| })); | |
| } | |
| : null, | |
| ); | |
| }, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| class TransActionCompletePage extends StatelessWidget { | |
| final bool? isDeposit; | |
| const TransActionCompletePage({ | |
| super.key, | |
| this.isDeposit, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| FlutterBankService bankService = Provider.of<FlutterBankService>( | |
| context, | |
| listen: false, | |
| ); | |
| Future.delayed(const Duration(seconds: 3), () { | |
| bankService.resetSelections(); | |
| Navigator.pop(context); | |
| }); | |
| return WillPopScope( | |
| onWillPop: () async { | |
| bankService.resetSelections(); | |
| return Future.value(true); | |
| }, | |
| child: 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: Center( | |
| child: FutureBuilder( | |
| future: bankService.performDeposit(context), | |
| builder: (context, snapshot) { | |
| if (snapshot.connectionState != ConnectionState.done || | |
| !snapshot.hasData) { | |
| return const FlutterBankLoading(); | |
| } else if (snapshot.hasError) { | |
| return const FlutterBankError(); | |
| } | |
| return const FlutterBankTransactionCompleted(); | |
| }, | |
| ), | |
| ), | |
| ), | |
| ), | |
| ); | |
| } | |
| } | |
| /// 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()} ACCT', | |
| 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 FlutterBankBottomBar extends StatelessWidget { | |
| const FlutterBankBottomBar({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| List<FlutterBankBottomBarItem> bottomItems = | |
| Utils.getBottomBarItems(context); | |
| 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 AccountActionCard extends StatelessWidget { | |
| final List<Account>? accounts; | |
| final Account? selectedAccount; | |
| const AccountActionCard({ | |
| super.key, | |
| this.accounts, | |
| this.selectedAccount, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| FlutterBankService bankService = Provider.of<FlutterBankService>( | |
| context, | |
| listen: false, | |
| ); | |
| return Row( | |
| children: List.generate(accounts!.length, (index) { | |
| var currentAccount = accounts![index]; | |
| return Expanded( | |
| child: GestureDetector( | |
| onTap: () { | |
| bankService.setSelectedAccount(currentAccount); | |
| }, | |
| child: Container( | |
| margin: const EdgeInsets.all(5.0), | |
| padding: const EdgeInsets.all(15.0), | |
| decoration: BoxDecoration( | |
| color: Colors.white, | |
| borderRadius: BorderRadius.circular(10.0), | |
| boxShadow: [ | |
| BoxShadow( | |
| color: Colors.black.withOpacity(0.1), | |
| blurRadius: 20.0, | |
| offset: const Offset(0.0, 5.0), | |
| ), | |
| ], | |
| border: Border.all( | |
| color: selectedAccount != null && | |
| selectedAccount!.id == currentAccount.id | |
| ? Utils.mainThemeColor | |
| : Colors.transparent, | |
| width: 5.0, | |
| ), | |
| ), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| '${currentAccount.type?.toUpperCase()} ACCT', | |
| style: const TextStyle(color: Utils.mainThemeColor), | |
| ), | |
| Text(currentAccount.accountNumber!), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ); | |
| }), | |
| ); | |
| } | |
| } | |
| class AccountActionHeader extends StatelessWidget { | |
| final String? headerTitle; | |
| final IconData? icon; | |
| const AccountActionHeader({ | |
| super.key, | |
| this.headerTitle, | |
| this.icon, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Container( | |
| margin: const EdgeInsets.only(bottom: 20.0), | |
| child: Row( | |
| children: [ | |
| Icon( | |
| icon, | |
| color: Utils.mainThemeColor, | |
| size: 30.0, | |
| ), | |
| const SizedBox(width: 10.0), | |
| Text( | |
| headerTitle!, | |
| style: const TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 20.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class AccountActionSelection extends StatelessWidget { | |
| final String? actionTypeLabel; | |
| final Widget? amountChanger; | |
| const AccountActionSelection({ | |
| super.key, | |
| this.actionTypeLabel, | |
| required this.amountChanger, | |
| }); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Consumer<FlutterBankService>(builder: (context, bankService, child) { | |
| return FutureBuilder( | |
| future: bankService.getAccounts(context), | |
| builder: (context, snapshot) { | |
| if (snapshot.connectionState != ConnectionState.done || | |
| !snapshot.hasData) { | |
| return const FlutterBankLoading(); | |
| } else if (snapshot.hasError) { | |
| return const FlutterBankError(); | |
| } | |
| var selectedAccount = bankService.getSelectedAccount(); | |
| List<Account> accounts = snapshot.data as List<Account>; | |
| return Column( | |
| mainAxisAlignment: MainAxisAlignment.start, | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| actionTypeLabel!, | |
| style: const TextStyle( | |
| color: Colors.grey, | |
| fontSize: 15.0, | |
| ), | |
| ), | |
| const SizedBox(height: 10.0), | |
| AccountActionCard( | |
| selectedAccount: selectedAccount, | |
| accounts: accounts, | |
| ), | |
| Expanded( | |
| child: Visibility( | |
| visible: selectedAccount != null, | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.start, | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| children: [ | |
| Container( | |
| margin: const EdgeInsets.only(top: 30.0), | |
| child: const Text( | |
| 'Current Balance', | |
| style: TextStyle(color: Colors.grey), | |
| ), | |
| ), | |
| Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| const Icon( | |
| Icons.monetization_on, | |
| color: Utils.mainThemeColor, | |
| size: 25.0, | |
| ), | |
| Text( | |
| selectedAccount != null | |
| ? '\$${selectedAccount.balance!.toStringAsFixed(2)}' | |
| : '', | |
| style: const TextStyle( | |
| color: Colors.black, | |
| fontSize: 35.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| Expanded( | |
| child: amountChanger!, | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| }); | |
| } | |
| } | |
| class AccountDepositSlider extends StatelessWidget { | |
| const AccountDepositSlider({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Consumer<DepositService>( | |
| builder: (context, depositService, child) { | |
| return Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| children: [ | |
| const Text( | |
| 'Amount To Deposit', | |
| style: TextStyle( | |
| color: Colors.grey, | |
| ), | |
| ), | |
| Text( | |
| '\$${depositService.amountToDeposit.toInt().toString()}', | |
| style: const TextStyle( | |
| color: Colors.black, | |
| fontSize: 60.0, | |
| ), | |
| ), | |
| Slider( | |
| value: depositService.amountToDeposit, | |
| max: 1000, | |
| activeColor: Utils.mainThemeColor, | |
| inactiveColor: Colors.grey.withOpacity(0.5), | |
| thumbColor: Utils.mainThemeColor, | |
| onChanged: (double value) { | |
| depositService.setAmountToDeposit(value); | |
| }, | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| class FlutterBankError extends StatelessWidget { | |
| const FlutterBankError({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return const Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| Icons.warning_outlined, | |
| color: Utils.mainThemeColor, | |
| size: 80.0, | |
| ), | |
| SizedBox( | |
| height: 20.0, | |
| ), | |
| Text( | |
| 'There was an error processing your request.', | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 20.0, | |
| ), | |
| ), | |
| Text( | |
| 'Please try again later.', | |
| style: TextStyle( | |
| color: Colors.grey, | |
| fontSize: 12.0, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| } | |
| class FlutterBankTransactionCompleted extends StatelessWidget { | |
| const FlutterBankTransactionCompleted({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return const Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| crossAxisAlignment: CrossAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| Icons.check_circle_outline_outlined, | |
| color: Utils.mainThemeColor, | |
| size: 80.0, | |
| ), | |
| SizedBox(height: 20.0), | |
| Text( | |
| 'Transaction Completed', | |
| style: TextStyle( | |
| color: Utils.mainThemeColor, | |
| fontSize: 20.0, | |
| ), | |
| ), | |
| ], | |
| ); | |
| } | |
| } | |
| /// MODELS | |
| class FlutterBankBottomBarItem { | |
| String? label; | |
| IconData? icon; | |
| Function? onTap; | |
| FlutterBankBottomBarItem({ | |
| this.label, | |
| this.icon, | |
| this.onTap, | |
| }); | |
| } | |
| /// UTILS | |
| class Utils { | |
| static const Color mainThemeColor = Color(0xFF8700C3); | |
| static List<FlutterBankBottomBarItem> getBottomBarItems( | |
| BuildContext context) { | |
| return [ | |
| FlutterBankBottomBarItem( | |
| label: 'Withdraw', | |
| icon: Icons.logout, | |
| onTap: () {}, | |
| ), | |
| FlutterBankBottomBarItem( | |
| label: 'Deposit', | |
| icon: Icons.login, | |
| onTap: () { | |
| Navigator.push(context, MaterialPageRoute(builder: (context) { | |
| return const FlutterBankDeposit(); | |
| })); | |
| }, | |
| ), | |
| 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 { | |
| Account? selectedAccount; | |
| void setSelectedAccount(Account? acct) { | |
| selectedAccount = acct; | |
| notifyListeners(); | |
| } | |
| void resetSelections() { | |
| setSelectedAccount(null); | |
| notifyListeners(); | |
| } | |
| Account? getSelectedAccount() { | |
| return selectedAccount; | |
| } | |
| Future<bool> performDeposit(BuildContext context) { | |
| Completer<bool> depositComplete = Completer(); | |
| // Get the userID from the LoginService | |
| LoginService loginService = | |
| Provider.of<LoginService>(context, listen: false); | |
| String userId = loginService.getUserId(); | |
| // Get the amount to deposit from the DepositService | |
| DepositService depositService = | |
| Provider.of<DepositService>(context, listen: false); | |
| int amountToDeposit = depositService.amountToDeposit.toInt(); | |
| // Grab the document associated with the selected account: | |
| // Start at the root collecation 'accounts', then search for the document | |
| // by user id, then search inside that document found inside it's nested | |
| // collection 'user-accounts', and in turn, find a document associated | |
| // with the selected account's id. At the end, store the reference to | |
| // this document in a variable called doc. | |
| DocumentReference doc = FirebaseFirestore.instance | |
| .collection('accounts') | |
| .doc(userId) | |
| .collection('user_accounts') | |
| .doc(selectedAccount!.id!); | |
| // Now, we can perform the update to its balance field | |
| doc.update({'balance': selectedAccount!.balance! + amountToDeposit}).then( | |
| (value) { | |
| // If the update is successful, reset the service and complete the Future | |
| // with true | |
| depositService.resetDepositService(); | |
| depositComplete.complete(true); | |
| }, onError: (error) { | |
| // If the update fails, complete the Future with an error | |
| depositComplete.completeError({'error': error}); | |
| }); | |
| return depositComplete.future; | |
| } | |
| 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); | |
| }); | |
| }); | |
| // Where does this code go? | |
| onError: | |
| (error) { | |
| accountsCompleter.completeError({'error': error}); | |
| }; | |
| 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>>). | |
| } | |
| } | |
| class DepositService extends ChangeNotifier { | |
| double amountToDeposit = 0; | |
| void setAmountToDeposit(double amount) { | |
| amountToDeposit = amount; | |
| notifyListeners(); | |
| } | |
| void resetDepositService() { | |
| amountToDeposit = 0; | |
| notifyListeners(); | |
| } | |
| bool checkAmountToDeposit() { | |
| return amountToDeposit > 0; | |
| } | |
| } | |
| // MODELS | |
| class Account { | |
| String? id; | |
| String? type; | |
| String? accountNumber; | |
| 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