Skip to content

Instantly share code, notes, and snippets.

@plotsklapps
Created October 8, 2023 17:24
Show Gist options
  • Select an option

  • Save plotsklapps/5aad6a23dfbb1f39988996eed450c60a to your computer and use it in GitHub Desktop.

Select an option

Save plotsklapps/5aad6a23dfbb1f39988996eed450c60a to your computer and use it in GitHub Desktop.
Flutter Fullstack #5

Flutter Fullstack #5

Created with <3 with dartpad.dev.

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