Compare commits

..

4 Commits

24 changed files with 963 additions and 368 deletions

View File

@ -1,4 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<application
android:label="kmobile"
android:name="${applicationName}"

BIN
assets/fonts/Rubik-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

BIN
assets/images/avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -45,5 +45,7 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to secure your data.</string>
</dict>
</plist>

View File

@ -1,14 +1,20 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../app.dart';
import '../../features/auth/models/auth_token.dart';
import '../../features/auth/models/auth_credentials.dart';
import '../../data/models/user.dart';
import '../../core/errors/exceptions.dart';
import 'package:local_auth/local_auth.dart';
import '../../features/dashboard/screens/dashboard_screen.dart';
class AuthService {
final Dio _dio;
AuthService(this._dio);
Future<AuthToken> login(AuthCredentials credentials) async {

View File

@ -1,13 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/screens/customer_info_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'api/services/auth_service.dart';
import 'config/themes.dart';
import 'config/routes.dart';
import 'data/repositories/auth_repository.dart';
import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart';
import 'features/auth/controllers/auth_state.dart';
import 'features/auth/screens/Card_screen.dart';
import 'features/auth/screens/login_screen.dart';
import 'features/auth/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class KMobile extends StatelessWidget {
const KMobile({super.key});
@ -52,8 +59,12 @@ class KMobile extends StatelessWidget {
return const _SplashScreen();
}
if(state is ShowBiometricPermission){
return const BiometricPermissionScreen();
}
if (state is Authenticated) {
return const DashboardScreen();
return const NavigationScaffold();
}
return const LoginScreen();
@ -104,3 +115,93 @@ class _SplashScreen extends StatelessWidget {
);
}
}
class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key});
@override
State<NavigationScaffold> createState() => _NavigationScaffoldState();
}
class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController();
int _selectedIndex = 0;
final List<Widget> _pages = [
DashboardScreen(),
CardScreen(),
ServiceScreen(),
CustomerInfoScreen()
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_filled),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Symbols.credit_card),
label: 'Card',
),
BottomNavigationBarItem(
icon: Icon(Symbols.concierge),
label: 'Services',
),
],
onTap: _onItemTapped,
backgroundColor: const Color(0xFFE0F7FA), // Light blue background
selectedItemColor: Colors.blue[800],
unselectedItemColor: Colors.black54,
),
);
}
}
class BiometricPermissionScreen extends StatelessWidget {
const BiometricPermissionScreen({super.key});
@override
Widget build(BuildContext context) {
final cubit = context.read<AuthCubit>();
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Would you like to enable biometric authentication?'),
ElevatedButton(
onPressed: () => cubit.handleBiometricChoice(true),
child: const Text('Yes'),
),
TextButton(
onPressed: () => cubit.handleBiometricChoice(false),
child: const Text('No, thanks'),
),
],
),
),
);
}
}

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/customer_info_screen.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import '../app.dart';
import '../features/auth/screens/login_screen.dart';
// import '../features/auth/screens/forgot_password_screen.dart';
// import '../features/auth/screens/register_screen.dart';
@ -15,12 +18,15 @@ class AppRoutes {
// Route names
static const String splash = '/';
static const String login = '/login';
static const String mPin = '/mPin';
static const String register = '/register';
static const String forgotPassword = '/forgot-password';
static const String navigationScaffold = '/navigation-scaffold';
static const String dashboard = '/dashboard';
static const String accounts = '/accounts';
static const String transactions = '/transactions';
static const String payments = '/payments';
static const String customer_info = '/customer-info';
// Route generator
static Route<dynamic> generateRoute(RouteSettings settings) {
@ -28,6 +34,9 @@ class AppRoutes {
case login:
return MaterialPageRoute(builder: (_) => const LoginScreen());
case mPin:
return MaterialPageRoute(builder: (_) => const MPinScreen());
case register:
// Placeholder - create the RegisterScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const RegisterScreen());
@ -38,9 +47,15 @@ class AppRoutes {
// return MaterialPageRoute(builder: (_) => const ForgotPasswordScreen());
return _errorRoute();
case navigationScaffold:
return MaterialPageRoute(builder: (_) => const NavigationScaffold());
case dashboard:
return MaterialPageRoute(builder: (_) => const DashboardScreen());
case customer_info:
return MaterialPageRoute(builder: (_) => const CustomerInfoScreen());
case accounts:
// Placeholder - create the AccountsScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const AccountsScreen());

View File

@ -22,51 +22,61 @@ class AppThemes {
fontSize: 96,
fontWeight: FontWeight.w300,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
displayMedium: TextStyle(
fontSize: 60,
fontWeight: FontWeight.w300,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
displaySmall: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
headlineMedium: TextStyle(
fontSize: 34,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
headlineSmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFF757575),
fontFamily: 'Rubik',
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
);
@ -86,6 +96,7 @@ class AppThemes {
// Light theme
static final ThemeData lightTheme = ThemeData(
useMaterial3: true,
fontFamily: 'Rubik',
colorScheme: const ColorScheme.light(
primary: _primaryColorLight,
secondary: _secondaryColorLight,
@ -167,6 +178,7 @@ class AppThemes {
// Dark theme
static final ThemeData darkTheme = ThemeData(
fontFamily: 'Rubik',
useMaterial3: true,
colorScheme: const ColorScheme.dark(
primary: _primaryColorDark,

View File

@ -1,4 +1,10 @@
import 'package:bloc/bloc.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../app.dart';
import '../../../data/repositories/auth_repository.dart';
import 'auth_state.dart';
@ -47,4 +53,60 @@ class AuthCubit extends Cubit<AuthState> {
emit(AuthError(e.toString()));
}
}
Future<void> checkFirstLaunch() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
final isFirstLaunch = prefs.getBool('isFirstLaunch') ?? true;
if (isFirstLaunch) {
emit(ShowBiometricPermission());
} else {
// Continue to authentication logic (e.g., check token)
emit(AuthLoading()); // or Unauthenticated/Authenticated
}
}
Future<void> handleBiometricChoice(bool enabled) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setBool('biometric_opt_in', enabled);
await prefs.setBool('isFirstLaunch', false);
// Then continue to auth logic or home
if (enabled) {
authenticateBiometric(); // implement biometric logic
} else {
emit(Unauthenticated());
}
}
Future<void> authenticateBiometric() async {
final LocalAuthentication auth = LocalAuthentication();
try {
final isAvailable = await auth.canCheckBiometrics;
final isDeviceSupported = await auth.isDeviceSupported();
if (isAvailable && isDeviceSupported) {
final authenticated = await auth.authenticate(
localizedReason: 'Touch the fingerprint sensor',
options: const AuthenticationOptions(
biometricOnly: true,
stickyAuth: true,
),
);
if (authenticated) {
// Continue to normal auth logic (e.g., auto login)
emit(AuthLoading());
await checkAuthStatus(); // Your existing method to verify token/session
} else {
emit(Unauthenticated());
}
} else {
emit(Unauthenticated());
}
} catch (e) {
emit(Unauthenticated());
}
}
}

View File

@ -10,6 +10,8 @@ class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class ShowBiometricPermission extends AuthState {}
class Authenticated extends AuthState {
final User user;

View File

@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
class CardScreen extends StatefulWidget {
const CardScreen({super.key});
@override
State<CardScreen> createState() => _CardScreen();
}
class _CardScreen extends State<CardScreen>{
@override
Widget build(BuildContext context) {
return Scaffold(
);
}
}

View File

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class CustomerInfoScreen extends StatefulWidget {
const CustomerInfoScreen({super.key});
@override
State<CustomerInfoScreen> createState() => _CustomerInfoScreen();
}
class _CustomerInfoScreen extends State<CustomerInfoScreen>{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},),
title: const Text('kMobile', style: TextStyle(color: Colors.black,
fontWeight: FontWeight.w500),),
actions: const [
Padding(
padding: EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
radius: 20,
),
),
],
),
body: const SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Padding(
padding: EdgeInsets.all(16.0),
child: SafeArea(
child: Center(
child: Column(
children: [
SizedBox(height: 30),
CircleAvatar(
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
radius: 50,
),
Padding(
padding: EdgeInsets.only(top: 10.0),
child: Text('Trina Bakshi', style: TextStyle(fontSize: 20,
color: Colors.black, fontWeight: FontWeight.w500),),
),
Text('CIF: 2553677487774', style: TextStyle(fontSize: 16, color: Colors.grey),),
SizedBox(height: 30,),
InfoField(label: 'Number of Active Accounts', value: '3'),
InfoField(label: 'Mobile Number', value: '987XXXXX78'),
InfoField(label: 'Date of Birth', value: '12-07-1984'),
InfoField(label: 'Branch', value: 'Krishnapur'),
InfoField(label: 'Aadhar Number', value: '7665 XXXX 1276'),
InfoField(label: 'PAN Number', value: '700127638009871'),
],
),
),
),
)),
);
}
}
class InfoField extends StatelessWidget {
final String label;
final String value;
const InfoField({Key? key, required this.label, required this.value}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 3),
Text(
value,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
],
),
);
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import '../../../app.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
import '../../dashboard/screens/dashboard_screen.dart';
@ -13,35 +16,39 @@ class LoginScreen extends StatefulWidget {
class LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _usernameController = TextEditingController();
final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_usernameController.dispose();
_customerNumberController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login(
_usernameController.text.trim(),
_passwordController.text,
);
}
// if (_formKey.currentState!.validate()) {
// context.read<AuthCubit>().login(
// _customerNumberController.text.trim(),
// _passwordController.text,
// );
// }
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const MPinScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is Authenticated) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const DashboardScreen()),
MaterialPageRoute(builder: (context) => const NavigationScaffold()),
);
} else if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
@ -56,19 +63,35 @@ class LoginScreenState extends State<LoginScreen> {
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
// crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Bank logo or app branding
const FlutterLogo(size: 80),
SvgPicture.asset('assets/images/kccb_logo.svg', width: 100, height: 100,),
const SizedBox(height: 16),
// Title
const Text(
'KCCB',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.blue),
),
const SizedBox(height: 48),
TextFormField(
controller: _usernameController,
controller: _customerNumberController,
decoration: const InputDecoration(
labelText: 'Username',
prefixIcon: Icon(Icons.person),
labelText: 'Customer Number',
// prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Colors.white,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
@ -83,8 +106,17 @@ class LoginScreenState extends State<LoginScreen> {
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock),
// prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Colors.white,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
@ -107,43 +139,72 @@ class LoginScreenState extends State<LoginScreen> {
},
),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () {
// Navigate to forgot password screen
},
child: const Text('Forgot Password?'),
),
),
// Align(
// alignment: Alignment.centerRight,
// child: TextButton(
// onPressed: () {
// // Navigate to forgot password screen
// },
// child: const Text('Forgot Password?'),
// ),
// ),
const SizedBox(height: 24),
ElevatedButton(
onPressed: state is AuthLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: state is AuthLoading
? const CircularProgressIndicator()
: const Text('LOGIN', style: TextStyle(fontSize: 16)),
),
const SizedBox(height: 16),
// Registration option
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("Don't have an account?"),
TextButton(
onPressed: () {
// Navigate to registration screen
},
child: const Text('Register'),
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: state is AuthLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.white,
foregroundColor: Colors.blueAccent,
side: const BorderSide(color: Colors.black, width: 1),
elevation: 0
),
],
child: state is AuthLoading
? const CircularProgressIndicator()
: const Text('Login', style: TextStyle(fontSize: 16),),
),
),
const SizedBox(height: 15),
// OR Divider
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('OR'),
),
Expanded(child: Divider()),
],
),
),
const SizedBox(height: 25),
// Register Button
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: () {
// Handle register
},
style: OutlinedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.lightBlue[100],
foregroundColor: Colors.black
),
child: const Text('Register'),
),
),
],
),
),

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../app.dart';
class MPinScreen extends StatefulWidget {
const MPinScreen({super.key});
@override
MPinScreenState createState() => MPinScreenState();
}
class MPinScreenState extends State<MPinScreen> {
List<String> mPin = [];
void addDigit(String digit) {
if (mPin.length < 4) {
setState(() {
mPin.add(digit);
});
}
}
void deleteDigit() {
if (mPin.isNotEmpty) {
setState(() {
mPin.removeLast();
});
}
}
Widget buildMPinDots() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
return Container(
margin: const EdgeInsets.all(8),
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index < mPin.length ? Colors.black : Colors.grey[400],
),
);
}),
);
}
Widget buildNumberPad() {
List<List<String>> keys = [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['Enter', '0', '<']
];
return Column(
children: keys.map((row) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
if (key == '<') {
deleteDigit();
} else if (key == 'Enter') {
if (mPin.length == 4) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const NavigationScaffold()),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please enter 4 digits")),
);
}
} else if (key.isNotEmpty) {
addDigit(key);
}
},
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[200],
),
alignment: Alignment.center,
child: key == 'Enter' ? const Icon(Symbols.check) : Text(
key == '<' ? '' : key,
style: TextStyle(
fontSize: 20,
fontWeight: key == 'Enter' ?
FontWeight.normal : FontWeight.normal,
color: key == 'Enter' ? Colors.blue : Colors.black,
),
),
),
),
);
}).toList(),
);
}).toList(),
);
}
@override
Widget build(BuildContext context) {
final cubit = context.read<AuthCubit>();
return Scaffold(
body: SafeArea(
child: Column(
children: [
const Spacer(),
// Logo
const FlutterLogo(size: 100),
const SizedBox(height: 20),
const Text(
"Enter your mPIN",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
buildMPinDots(),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(onPressed: () {
cubit.authenticateBiometric();
}, child: const Text("Try another way")),
TextButton(onPressed: () {}, child: const Text("Register?")),
],
),
buildNumberPad(),
const Spacer(),
],
),
),
);
}
}

View File

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
class ServiceScreen extends StatefulWidget {
const ServiceScreen({super.key});
@override
State<ServiceScreen> createState() => _ServiceScreen();
}
class _ServiceScreen extends State<ServiceScreen>{
@override
Widget build(BuildContext context) {
return Scaffold(
);
}
}

View File

@ -1,11 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../auth/controllers/auth_cubit.dart';
import '../../auth/controllers/auth_state.dart';
import '../widgets/account_card.dart';
import '../widgets/transaction_list_item.dart';
import '../../accounts/models/account.dart';
import '../../transactions/models/transaction.dart';
import 'package:kmobile/features/auth/screens/customer_info_screen.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@ -15,331 +10,150 @@ class DashboardScreen extends StatefulWidget {
}
class _DashboardScreenState extends State<DashboardScreen> {
// Mock data for demonstration
final List<Account> _accounts = [
Account(
id: '1',
accountNumber: '**** 4589',
accountType: 'Savings',
balance: 12450.75,
currency: 'USD',
),
Account(
id: '2',
accountNumber: '**** 7823',
accountType: 'Checking',
balance: 3840.50,
currency: 'USD',
),
];
final List<Transaction> _recentTransactions = [
Transaction(
id: 't1',
description: 'Coffee Shop',
amount: -4.50,
date: DateTime.now().subtract(const Duration(hours: 3)),
category: 'Food & Drink',
),
Transaction(
id: 't2',
description: 'Salary Deposit',
amount: 2500.00,
date: DateTime.now().subtract(const Duration(days: 2)),
category: 'Income',
),
Transaction(
id: 't3',
description: 'Electric Bill',
amount: -85.75,
date: DateTime.now().subtract(const Duration(days: 3)),
category: 'Utilities',
),
Transaction(
id: 't4',
description: 'Amazon Purchase',
amount: -32.50,
date: DateTime.now().subtract(const Duration(days: 5)),
category: 'Shopping',
),
// Mock data for transactions
final List<Map<String, String>> transactions = [
{'name': 'Raj Kumar', 'amount': '₹1,000', 'date': '09 March, 2025 16:04', 'type': 'in'},
{'name': 'Sunita Joshi', 'amount': '₹1,45,000', 'date': '07 March, 2025 16:04', 'type': 'out'},
{'name': 'Manoj Singh', 'amount': '₹2,400', 'date': '07 March, 2025 16:04', 'type': 'in'},
{'name': 'Raj Kumar', 'amount': '₹11,500', 'date': '09 March, 2025 16:04', 'type': 'in'},
{'name': 'Manoj Singh', 'amount': '₹1,000', 'date': '', 'type': 'in'},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Dashboard'),
automaticallyImplyLeading: false,
title: const Text('kMobile', style: TextStyle(color: Colors.blueAccent,
fontWeight: FontWeight.w500),),
actions: [
IconButton(
icon: const Icon(Icons.notifications_outlined),
onPressed: () {
// Navigate to notifications
},
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
_showLogoutConfirmation(context);
},
// IconButton(
// icon: const Icon(Icons.notifications_outlined),
// onPressed: () {
// // Navigate to notifications
// },
// ),
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: (){
// Navigator.push(context, MaterialPageRoute(
// builder: (context) => const CustomerInfoScreen()));
},
child: const CircleAvatar(
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
radius: 20,
),
),
),
],
),
body: RefreshIndicator(
onRefresh: () async {
// Implement refresh logic to fetch updated data
await Future.delayed(const Duration(seconds: 1));
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Greeting section
BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Hello, ${state.user.username}',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 4),
Text(
'Welcome back to your banking app',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey[600],
),
),
],
);
}
return Container();
},
),
body: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16),
const SizedBox(height: 24),
// Account cards section
const Text(
'Your Accounts',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
// Account Info Card
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue[700],
borderRadius: BorderRadius.circular(16),
),
const SizedBox(height: 12),
SizedBox(
height: 180,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _accounts.length + 1, // +1 for the "Add Account" card
itemBuilder: (context, index) {
if (index < _accounts.length) {
return Padding(
padding: const EdgeInsets.only(right: 16.0),
child: AccountCard(account: _accounts[index]),
);
} else {
// "Add Account" card
return Container(
width: 300,
margin: const EdgeInsets.only(right: 16.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_circle_outline,
size: 40,
color: Theme.of(context).primaryColor,
),
const SizedBox(height: 8),
Text(
'Add New Account',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
}
},
),
),
const SizedBox(height: 32),
// Quick Actions section
const Text(
'Quick Actions',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildQuickActionButton(
icon: Icons.swap_horiz,
label: 'Transfer',
onTap: () {
// Navigate to transfer screen
},
Row(
children: [
Text("Account Number: ", style:
TextStyle(color: Colors.white, fontSize: 12)),
Text("03000156789462302", style:
TextStyle(color: Colors.white, fontSize: 14)),
Padding(
padding: EdgeInsets.only(left: 5.0),
child: Icon(Symbols.keyboard_arrow_down, color: Colors.white),
),
],
),
_buildQuickActionButton(
icon: Icons.payment,
label: 'Pay Bills',
onTap: () {
// Navigate to bill payment screen
},
),
_buildQuickActionButton(
icon: Icons.qr_code_scanner,
label: 'Scan & Pay',
onTap: () {
// Navigate to QR code scanner
},
),
_buildQuickActionButton(
icon: Icons.more_horiz,
label: 'More',
onTap: () {
// Show more options
},
SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("₹ *****", style: TextStyle(color: Colors.white,
fontSize: 24, fontWeight: FontWeight.w700)),
Icon(Symbols.visibility_lock, color: Colors.white),
],
),
SizedBox(height: 20),
],
),
),
const SizedBox(height: 18),
const Text('Quick Links', style: TextStyle(fontSize: 15),),
const SizedBox(height: 16),
const SizedBox(height: 32),
// Quick Links
GridView.count(
crossAxisCount: 4,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
_buildQuickLink(Symbols.id_card, "Customer \n Info", () {
Navigator.push(context, MaterialPageRoute(
builder: (context) => const CustomerInfoScreen()));;
}),
_buildQuickLink(Symbols.currency_rupee, "Quick \n Pay",
() => print("")),
_buildQuickLink(Symbols.send_money, "Fund \n Transfer",
() => print("")),
_buildQuickLink(Symbols.server_person, "Account \n Info",
() => print("")),
_buildQuickLink(Symbols.receipt_long, "Account \n History",
() => print("")),
_buildQuickLink(Symbols.checkbook, "Handle \n Cheque",
() => print("")),
_buildQuickLink(Icons.group, "Manage \n Beneficiary",
() => print("")),
_buildQuickLink(Symbols.support_agent, "Contact \n Us",
() => print("")),
],
),
const SizedBox(height: 10),
// Recent Transactions section
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Recent Transactions',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () {
// Navigate to all transactions
},
child: const Text('See All'),
),
],
),
const SizedBox(height: 8),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: _recentTransactions.length,
itemBuilder: (context, index) {
return TransactionListItem(
transaction: _recentTransactions[index],
);
},
),
],
),
// Recent Transactions
const Align(
alignment: Alignment.centerLeft,
child: Text("Recent Transactions", style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
),
...transactions.map((tx) => ListTile(
leading: Icon(tx['type'] == 'in' ? Symbols.call_received : Symbols.call_made, color: tx['type'] == 'in' ? Colors.green : Colors.red),
title: Text(tx['name']!),
subtitle: Text(tx['date']!),
trailing: Text(tx['amount']!),
)),
],
),
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: 0,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard),
label: 'Home',
),
BottomNavigationBarItem(
icon: Icon(Icons.account_balance_wallet),
label: 'Accounts',
),
BottomNavigationBarItem(
icon: Icon(Icons.show_chart),
label: 'Insights',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: 'Profile',
),
],
onTap: (index) {
// Handle navigation
},
),
);
}
Widget _buildQuickActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap) {
return InkWell(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withAlpha((0.1 * 255).toInt()),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: Theme.of(context).primaryColor,
size: 28,
),
),
const SizedBox(height: 8),
Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
void _showLogoutConfirmation(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Logout'),
content: const Text('Are you sure you want to logout?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('CANCEL'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
context.read<AuthCubit>().logout();
},
child: const Text('LOGOUT'),
),
Icon(icon, size: 30, color: Colors.blue[700]),
const SizedBox(height: 4),
Text(label, textAlign: TextAlign.center, style: const TextStyle(fontSize: 12)),
],
),
);

View File

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
@ -25,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
chalkdart:
dependency: transitive
description:
name: chalkdart
sha256: "7ffc6bd39c81453fb9ba8dbce042a9c960219b75ea1c07196a7fa41c2fab9e86"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
characters:
dependency: transitive
description:
@ -97,6 +113,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
flutter:
dependency: "direct main"
description: flutter
@ -118,6 +142,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e
url: "https://pub.dev"
source: hosted
version: "2.0.28"
flutter_secure_storage:
dependency: "direct main"
description:
@ -166,6 +198,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1
url: "https://pub.dev"
source: hosted
version: "2.1.0"
flutter_test:
dependency: "direct dev"
description: flutter
@ -184,6 +224,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "8.0.3"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http:
dependency: transitive
description:
name: http
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
url: "https://pub.dev"
source: hosted
version: "1.3.0"
http_parser:
dependency: transitive
description:
@ -240,6 +296,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
local_auth:
dependency: "direct main"
description:
name: local_auth
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
local_auth_android:
dependency: transitive
description:
name: local_auth_android
sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e"
url: "https://pub.dev"
source: hosted
version: "1.0.49"
local_auth_darwin:
dependency: transitive
description:
name: local_auth_darwin
sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2"
url: "https://pub.dev"
source: hosted
version: "1.4.3"
local_auth_platform_interface:
dependency: transitive
description:
name: local_auth_platform_interface
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
url: "https://pub.dev"
source: hosted
version: "1.0.10"
local_auth_windows:
dependency: transitive
description:
name: local_auth_windows
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
url: "https://pub.dev"
source: hosted
version: "1.0.11"
matcher:
dependency: transitive
description:
@ -256,6 +352,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.11.1"
material_symbols_icons:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: d45b6c36c3effa8cb51b1afb8698107d5ff1f88fa4631428f34a8a01abc295d7
url: "https://pub.dev"
source: hosted
version: "4.2815.0"
meta:
dependency: transitive
description:
@ -280,6 +384,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
path_provider:
dependency: transitive
description:
@ -292,10 +404,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
source: hosted
version: "2.2.16"
version: "2.2.17"
path_provider_foundation:
dependency: transitive
description:
@ -328,6 +440,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
platform:
dependency: transitive
description:
@ -352,6 +472,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.4"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
@ -413,6 +589,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de"
url: "https://pub.dev"
source: hosted
version: "1.1.18"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.dev"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad"
url: "https://pub.dev"
source: hosted
version: "1.1.16"
vector_math:
dependency: transitive
description:
@ -453,6 +653,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.27.0"

View File

@ -42,6 +42,10 @@ dependencies:
flutter_bloc: ^9.1.0
get_it: ^8.0.3
intl: any
flutter_svg: ^2.1.0
local_auth: ^2.3.0
material_symbols_icons: ^4.2815.0
shared_preferences: ^2.5.3
dev_dependencies:
flutter_test:
@ -66,9 +70,9 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
assets:
- assets/images/kccb_logo.svg
- assets/images/avatar.jpg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
@ -81,11 +85,12 @@ flutter:
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
fonts:
- family: Rubik
fonts:
- asset: assets/fonts/Rubik-Regular.ttf
- asset: assets/fonts/Rubik-Bold.ttf
weight: 700
# style: italic
# - family: Trajan Pro
# fonts: