api integration

This commit is contained in:
2025-06-02 10:29:32 +05:30
parent 7c9e089c62
commit 805aa5e015
289 changed files with 40017 additions and 1082 deletions

View File

@@ -1,49 +1,71 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class AccountInfoScreen extends StatefulWidget {
const AccountInfoScreen({super.key});
final User user;
const AccountInfoScreen({super.key, required this.user});
@override
State<AccountInfoScreen> createState() => _AccountInfoScreen();
}
class _AccountInfoScreen extends State<AccountInfoScreen>{
class _AccountInfoScreen extends State<AccountInfoScreen> {
@override
Widget build(BuildContext context) {
final user = widget.user;
return Scaffold(
appBar: AppBar(
leading: IconButton(icon: const Icon(Symbols.arrow_back_ios_new),
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},),
title: const Text('Account Info', style: TextStyle(color: Colors.black,
fontWeight: FontWeight.w500),),
},
),
title: const Text(
'Account Info',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
actions: const [
actions: [
Padding(
padding: EdgeInsets.only(right: 10.0),
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundImage: AssetImage('assets/images/avatar.jpg'), // Replace with your image
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16.0),
children: const [
InfoRow(title: 'Account Number', value: '700127638009871'),
// InfoRow(title: 'Nominee Customer No', value: '700127638009871'),
InfoRow(title: 'SMS Service', value: 'Active'),
InfoRow(title: 'Missed Call Service', value: 'Active'),
InfoRow(title: 'Customer Number', value: '9000875272000212'),
InfoRow(title: 'Product Name', value: 'SAVINGS-PERSONAL'),
InfoRow(title: 'Account Opening Date', value: '12-09-2012'),
InfoRow(title: 'Account Status', value: 'OPEN'),
InfoRow(title: 'Available Balance', value: '12,000 CR'),
InfoRow(title: 'Interest Rate', value: '12.00'),
children: [
InfoRow(title: 'Account Number', value: user.accountNo ?? 'N/A'),
// InfoRow(title: 'Nominee Customer No', value: user.nomineeCustomerNo),
// InfoRow(title: 'SMS Service', value: user.smsService),
// InfoRow(title: 'Missed Call Service', value: user.missedCallService),
InfoRow(title: 'Customer Number', value: user.cifNumber ?? 'N/A'),
InfoRow(title: 'Product Name', value: user.productType ?? 'N/A'),
// InfoRow(title: 'Account Opening Date', value: user.accountOpeningDate ?? 'N/A'),
const InfoRow(title: 'Account Status', value: 'OPEN'),
InfoRow(
title: 'Available Balance',
value: user.availableBalance ?? 'N/A'),
InfoRow(
title: 'Current Balance', value: user.currentBalance ?? 'N/A'),
user.approvedAmount != null
? InfoRow(
title: 'Approved Amount', value: user.approvedAmount ?? 'N/A')
: const SizedBox.shrink(),
],
),
);
@@ -58,19 +80,30 @@ class InfoRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.0),
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 14)),
const SizedBox(height: 4),
Text(value, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 10),
Text(
title,
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,13 +1,14 @@
import 'package:bloc/bloc.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:kmobile/api/services/user_service.dart';
import 'package:kmobile/core/errors/exceptions.dart';
import '../../../data/repositories/auth_repository.dart';
import 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository _authRepository;
final UserService _userService;
AuthCubit(this._authRepository) : super(AuthInitial()) {
AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) {
checkAuthStatus();
}
@@ -16,12 +17,8 @@ class AuthCubit extends Cubit<AuthState> {
try {
final isLoggedIn = await _authRepository.isLoggedIn();
if (isLoggedIn) {
final user = await _authRepository.getCurrentUser();
if (user != null) {
emit(Authenticated(user));
} else {
emit(Unauthenticated());
}
final users = await _userService.getUserDetails();
emit(Authenticated(users));
} else {
emit(Unauthenticated());
}
@@ -30,79 +27,24 @@ class AuthCubit extends Cubit<AuthState> {
}
}
Future<void> login(String username, String password) async {
Future<void> refreshUserData() async {
try {
// emit(AuthLoading());
final users = await _userService.getUserDetails();
emit(Authenticated(users));
} catch (e) {
emit(AuthError('Failed to refresh user data: ${e.toString()}'));
// Optionally, re-emit the previous state or handle as needed
}
}
Future<void> login(String customerNo, String password) async {
emit(AuthLoading());
try {
final user = await _authRepository.login(username, password);
emit(Authenticated(user));
final users = await _authRepository.login(customerNo, password);
emit(Authenticated(users));
} catch (e) {
emit(AuthError(e.toString()));
}
}
Future<void> logout() async {
emit(AuthLoading());
try {
await _authRepository.logout();
emit(Unauthenticated());
} catch (e) {
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());
emit(AuthError(e is AuthException ? e.message : e.toString()));
}
}
}

View File

@@ -10,15 +10,13 @@ class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {}
class ShowBiometricPermission extends AuthState {}
class Authenticated extends AuthState {
final User user;
final List<User> users;
Authenticated(this.user);
Authenticated(this.users);
@override
List<Object?> get props => [user];
List<Object?> get props => [users];
}
class Unauthenticated extends AuthState {}

View File

@@ -1,11 +1,11 @@
class AuthCredentials {
final String username;
final String customerNo;
final String password;
AuthCredentials({required this.username, required this.password});
AuthCredentials({required this.customerNo, required this.password});
Map<String, dynamic> toJson() => {
'customer_no': username,
'customerNo': customerNo,
'password': password,
};
}

View File

@@ -1,26 +1,48 @@
import 'dart:convert';
import 'dart:developer';
import 'package:equatable/equatable.dart';
class AuthToken extends Equatable {
final String accessToken;
final String refreshToken;
final DateTime expiresAt;
const AuthToken({
required this.accessToken,
required this.refreshToken,
required this.expiresAt,
});
factory AuthToken.fromJson(Map<String, dynamic> json) {
return AuthToken(
accessToken: json['access_token'],
refreshToken: json['refresh_token'],
expiresAt: DateTime.parse(json['expires_at']),
accessToken: json['token'],
expiresAt: _decodeExpiryFromToken(json['token']),
);
}
static DateTime _decodeExpiryFromToken(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) {
throw Exception('Invalid JWT');
}
final payload = parts[1];
// Pad the payload if necessary
String normalized = base64Url.normalize(payload);
final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized)));
if (payloadMap is! Map<String, dynamic> || !payloadMap.containsKey('exp')) {
throw Exception('Invalid payload');
}
final exp = payloadMap['exp'];
return DateTime.fromMillisecondsSinceEpoch(exp * 1000);
} catch (e) {
// Fallback: 1 hour from now if decoding fails
log(e.toString());
return DateTime.now().add(const Duration(hours: 1));
}
}
bool get isExpired => DateTime.now().isAfter(expiresAt);
@override
List<Object> get props => [accessToken, refreshToken, expiresAt];
List<Object> get props => [accessToken, expiresAt];
}

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../app.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
@@ -27,16 +28,12 @@ class LoginScreenState extends State<LoginScreen> {
}
void _submitForm() {
// if (_formKey.currentState!.validate()) {
// context.read<AuthCubit>().login(
// _customerNumberController.text.trim(),
// _passwordController.text,
// );
// }
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const MPinScreen()),
);
if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
}
}
@override
@@ -44,11 +41,30 @@ class LoginScreenState extends State<LoginScreen> {
return Scaffold(
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
listener: (context, state) async {
if (state is Authenticated) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const NavigationScaffold()),
);
final storage = getIt<SecureStorage>();
final mpin = await storage.read('mpin');
if (mpin == null) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(context, rootNavigator: true)
.pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold()),
);
},
),
),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
}
} else if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
@@ -62,18 +78,23 @@ class LoginScreenState extends State<LoginScreen> {
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
// crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Bank logo or app branding
SvgPicture.asset('assets/images/kccb_logo.svg', width: 100, height: 100,),
Image.asset('assets/images/logo.png', width: 150, height: 150,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.account_balance,
size: 100, color: Colors.blue);
}),
const SizedBox(height: 16),
// Title
const Text(
'KCCB',
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: Colors.blue),
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue),
),
const SizedBox(height: 48),
TextFormField(
controller: _customerNumberController,
decoration: const InputDecoration(
@@ -99,8 +120,8 @@ class LoginScreenState extends State<LoginScreen> {
return null;
},
),
const SizedBox(height: 16),
const SizedBox(height: 24),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
@@ -118,8 +139,8 @@ class LoginScreenState extends State<LoginScreen> {
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
@@ -138,40 +159,29 @@ class LoginScreenState extends State<LoginScreen> {
return null;
},
),
// Align(
// alignment: Alignment.centerRight,
// child: TextButton(
// onPressed: () {
// // Navigate to forgot password screen
// },
// child: const Text('Forgot Password?'),
// ),
// ),
const SizedBox(height: 24),
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
),
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 Text(
'Login',
style: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 15),
// OR Divider
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Row(
@@ -185,26 +195,22 @@ class LoginScreenState extends State<LoginScreen> {
],
),
),
const SizedBox(height: 25),
// Register Button
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: () {
// Handle register
},
//disable until registration is implemented
onPressed: null,
style: OutlinedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.lightBlue[100],
foregroundColor: Colors.black
),
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.lightBlue[100],
foregroundColor: Colors.black),
child: const Text('Register'),
),
),
],
),
),
@@ -213,4 +219,4 @@ class LoginScreenState extends State<LoginScreen> {
),
);
}
}
}

View File

@@ -1,24 +1,72 @@
import 'dart:developer';
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';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart';
enum MPinMode { enter, set, confirm }
class MPinScreen extends StatefulWidget {
const MPinScreen({super.key});
final MPinMode mode;
final String? initialPin;
final void Function(String pin)? onCompleted;
const MPinScreen({
super.key,
required this.mode,
this.initialPin,
this.onCompleted,
});
@override
MPinScreenState createState() => MPinScreenState();
State<MPinScreen> createState() => _MPinScreenState();
}
class MPinScreenState extends State<MPinScreen> {
class _MPinScreenState extends State<MPinScreen> {
List<String> mPin = [];
String? errorText;
@override
void initState() {
super.initState();
if (widget.mode == MPinMode.enter) {
_tryBiometricBeforePin();
}
}
Future<void> _tryBiometricBeforePin() async {
final storage = getIt<SecureStorage>();
final enabled = await storage.read('biometric_enabled');
log('biometric_enabled: $enabled');
if (enabled) {
final auth = LocalAuthentication();
if (await auth.canCheckBiometrics) {
final didAuth = await auth.authenticate(
localizedReason: 'Authenticate to access kMobile',
options: const AuthenticationOptions(biometricOnly: true),
);
if (didAuth && mounted) {
// success → directly “complete” your flow
widget.onCompleted?.call('');
// or navigate yourself:
// Navigator.of(context).pushReplacement(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()));
}
}
}
}
void addDigit(String digit) {
if (mPin.length < 4) {
setState(() {
mPin.add(digit);
errorText = null;
});
if (mPin.length == 4) {
_handleComplete();
}
}
}
@@ -26,10 +74,63 @@ class MPinScreenState extends State<MPinScreen> {
if (mPin.isNotEmpty) {
setState(() {
mPin.removeLast();
errorText = null;
});
}
}
Future<void> _handleComplete() async {
final pin = mPin.join();
final storage = SecureStorage();
switch (widget.mode) {
case MPinMode.enter:
final storedPin = await storage.read('mpin');
log('storedPin: $storedPin');
if (storedPin == int.tryParse(pin)) {
widget.onCompleted?.call(pin);
} else {
setState(() {
errorText = "Incorrect mPIN. Try again.";
mPin.clear();
});
}
break;
case MPinMode.set:
// propagate parent onCompleted into confirm step
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.confirm,
initialPin: pin,
onCompleted: widget.onCompleted, // <-- use parent callback
),
),
);
break;
case MPinMode.confirm:
if (widget.initialPin == pin) {
// 1) persist the pin
await storage.write('mpin', pin);
// 3) now clear the entire navigation stack and go to your main scaffold
if (mounted) {
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
}
} else {
setState(() {
errorText = "Pins do not match. Try again.";
mPin.clear();
});
}
break;
}
}
Widget buildMPinDots() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
@@ -63,24 +164,16 @@ class MPinScreenState extends State<MPinScreen> {
return Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () async {
onTap: () {
if (key == '<') {
deleteDigit();
} else if (key == 'Enter') {
if (mPin.length == 4) {
// String storedMpin = await SecureStorage().read("mpin");
// if(!mounted) return;
// if(storedMpin == mPin.join()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const NavigationScaffold()),
);
// }
_handleComplete();
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please enter 4 digits")),
);
setState(() {
errorText = "Please enter 4 digits";
});
}
} else if (key.isNotEmpty) {
addDigit(key);
@@ -94,15 +187,15 @@ class MPinScreenState extends State<MPinScreen> {
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,
),
),
child: key == 'Enter'
? const Icon(Icons.check)
: Text(
key == '<' ? '' : key,
style: TextStyle(
fontSize: 20,
color: key == 'Enter' ? Colors.blue : Colors.black,
),
),
),
),
);
@@ -112,34 +205,40 @@ class MPinScreenState extends State<MPinScreen> {
);
}
String getTitle() {
switch (widget.mode) {
case MPinMode.enter:
return "Enter your mPIN";
case MPinMode.set:
return "Set your new mPIN";
case MPinMode.confirm:
return "Confirm your mPIN";
}
}
@override
Widget build(BuildContext context) {
final cubit = context.read<AuthCubit>();
return Scaffold(
body: SafeArea(
child: Column(
children: [
const Spacer(),
// Logo
const FlutterLogo(size: 100),
Image.asset('assets/images/logo.png', height: 100),
const SizedBox(height: 20),
const Text(
"Enter your mPIN",
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
Text(
getTitle(),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
buildMPinDots(),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child:
Text(errorText!, style: const TextStyle(color: Colors.red)),
),
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(),
],
@@ -147,4 +246,4 @@ class MPinScreenState extends State<MPinScreen> {
),
);
}
}
}

View File

@@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/screens/mpin_setup_confirm.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class MPinSetupScreen extends StatefulWidget {
const MPinSetupScreen({super.key});
@override
MPinSetupScreenState createState() => MPinSetupScreenState();
}
class MPinSetupScreenState extends State<MPinSetupScreen> {
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.of(context).push(
MaterialPageRoute(builder: (context) => MPinSetupConfirmScreen(mPin: mPin,)),
);
} 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

@@ -1,154 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../../app.dart';
class MPinSetupConfirmScreen extends StatefulWidget {
final List<String> mPin;
const MPinSetupConfirmScreen({super.key, required this.mPin});
@override
MPinSetupConfirmScreenState createState() => MPinSetupConfirmScreenState();
}
class MPinSetupConfirmScreenState extends State<MPinSetupConfirmScreen> {
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: () async {
if (key == '<') {
deleteDigit();
} else if (key == 'Enter') {
if (mPin.length == 4 && mPin.join() == widget.mPin.join()) {
await SecureStorage().write("mpin", mPin.join());
await SharedPreferences.getInstance()
.then((prefs) => prefs.setBool('mpin_set', true));
if(!mounted) return;
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

@@ -1,69 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class CustomerInfoScreen extends StatefulWidget {
const CustomerInfoScreen({super.key});
final User user;
const CustomerInfoScreen({super.key, required this.user});
@override
State<CustomerInfoScreen> createState() => _CustomerInfoScreen();
State<CustomerInfoScreen> createState() => _CustomerInfoScreenState();
}
class _CustomerInfoScreen extends State<CustomerInfoScreen>{
class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
late final User user = widget.user;
@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,
),
appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
],
),
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,
title: const Text(
'kMobile',
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 100,
height: 100,
fit: BoxFit.cover,
),
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'),
],
),
),
),
)),
);
],
),
body: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SafeArea(
child: Center(
child: Column(
children: [
const SizedBox(height: 30),
CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 50,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 150,
height: 150,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
user.name ?? '',
style: const TextStyle(
fontSize: 20,
color: Colors.black,
fontWeight: FontWeight.w500),
),
),
Text(
'CIF: ${user.cifNumber ?? 'N/A'}',
style:
const TextStyle(fontSize: 16, color: Colors.grey),
),
const SizedBox(height: 30),
InfoField(
label: 'Number of Active Accounts',
value: user.activeAccounts?.toString() ?? '6'),
InfoField(
label: 'Mobile Number',
value: user.mobileNo ?? 'N/A'),
InfoField(
label: 'Date of Birth',
value: (user.dateOfBirth != null &&
user.dateOfBirth!.length == 8)
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
: 'N/A'), // Replace with DOB if available
InfoField(label: 'Branch', value: user.branchId ?? 'N/A'),
InfoField(
label: 'Address',
value: user.address ??
'N/A'), // Replace with Aadhar if available
InfoField(
label: 'Primary Id',
value: user.primaryId ??
'N/A'), // Replace with PAN if available
],
),
),
),
)));
}
}
@@ -71,7 +111,7 @@ class InfoField extends StatelessWidget {
final String label;
final String value;
const InfoField({Key? key, required this.label, required this.value}) : super(key: key);
const InfoField({super.key, required this.label, required this.value});
@override
Widget build(BuildContext context) {
@@ -101,4 +141,4 @@ class InfoField extends StatelessWidget {
),
);
}
}
}

View File

@@ -1,14 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
import 'package:kmobile/features/beneficiaries/screens/manage_beneficiaries_screen.dart';
import 'package:kmobile/features/dashboard/widgets/transaction_list_placeholder.dart';
import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:kmobile/src/preferences/preference.dart';
import 'package:local_auth/local_auth.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:shimmer/shimmer.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@@ -18,275 +27,460 @@ class DashboardScreen extends StatefulWidget {
}
class _DashboardScreenState extends State<DashboardScreen> {
// 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'},
];
List<String> accountNumbers = [
'0300015678903456',
'0300015678903678',
'0300015678903325',
];
String selectedAccount = '0300015678903456';
int selectedAccountIndex = 0;
bool isVisible = false;
bool isRefreshing = false;
bool isBalanceLoading = false;
bool _biometricPromptShown = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff5f9fc),
appBar: AppBar(
backgroundColor: const Color(0xfff5f9fc),
automaticallyImplyLeading: false,
title: Text(
'kMobile',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500),
),
actions: [
// 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 Preference()));
},
child: const CircleAvatar(
backgroundImage: AssetImage('assets/images/avatar.jpg'),
// Replace with your image
radius: 20,
),
),
),
],
),
body: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Hi Trina Bakshi",
style: GoogleFonts.sriracha().copyWith(fontSize: 20),
),
),
const SizedBox(height: 16),
Future<void> _refreshAccountData(BuildContext context) async {
setState(() {
isRefreshing = true;
});
try {
// Call your AuthCubit or repository to refresh user/accounts data
await context.read<AuthCubit>().refreshUserData();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Failed to refresh data')),
);
}
setState(() {
isRefreshing = false;
});
}
// Account Info Card
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text("Account Number: ",
style:
TextStyle(color: Colors.white, fontSize: 12)),
DropdownButton<String>(
value: selectedAccount,
dropdownColor: Theme.of(context).primaryColor,
underline: const SizedBox(),
icon: const Icon(Icons.keyboard_arrow_down),
iconEnabledColor: Colors.white,
style: const TextStyle(
color: Colors.white, fontSize: 14),
items: accountNumbers.map((String acc) {
return DropdownMenuItem<String>(
value: acc,
child: Text(acc,
style: const TextStyle(
color: Colors.white, fontSize: 14)),
);
}).toList(),
onChanged: (String? newValue) {
setState(() {
selectedAccount = newValue!;
});
},
),
],
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text("",
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.w700)),
Text(isVisible ? "1,23,456" : "*****",
style: const TextStyle(
color: Colors.white,
fontSize: 30,
fontWeight: FontWeight.w700)),
const Spacer(),
InkWell(
onTap: () {
setState(() {
isVisible = !isVisible;
});
},
child: Icon(
isVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: Colors.white)),
],
),
const SizedBox(height: 20),
],
),
),
const SizedBox(height: 18),
const Text(
'Quick Links',
style: TextStyle(fontSize: 15),
),
const SizedBox(height: 16),
// 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", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const QuickPayScreen()));
}),
_buildQuickLink(Symbols.send_money, "Fund \n Transfer", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const FundTransferBeneficiaryScreen()));
}),
_buildQuickLink(Symbols.server_person, "Account \n Info", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AccountInfoScreen()));
}),
_buildQuickLink(Symbols.receipt_long, "Account \n Statement",
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const AccountStatementScreen()));
}),
// _buildQuickLink(Symbols.checkbook, "Handle \n Cheque", () {
// Navigator.push(
// context,
// MaterialPageRoute(
// builder: (context) =>
// const ChequeManagementScreen()));
// }),
_buildQuickLink(Icons.group, "Manage \n Beneficiary", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ManageBeneficiariesScreen()));
}),
_buildQuickLink(Symbols.support_agent, "Contact \n Us", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EnquiryScreen()));
}),
],
),
const SizedBox(height: 10),
// 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']!),
)),
],
),
),
Widget _buildBalanceShimmer() {
return Shimmer.fromColors(
baseColor: Colors.white.withOpacity(0.7),
highlightColor: Colors.white.withOpacity(0.3),
child: Container(
width: 100,
height: 32,
color: Colors.white,
),
);
}
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap) {
String getProcessedFirstName(String? name) {
if (name == null || name.trim().isEmpty) return '';
// Remove common titles
final titles = [
'mr.',
'mrs.',
'ms.',
'miss',
'dr.',
'shri',
'smt.',
'kumari'
];
String processed = name.trim().toLowerCase();
for (final title in titles) {
if (processed.startsWith(title)) {
processed = processed.replaceFirst(title, '').trim();
break;
}
}
// Take the first word (first name)
final firstName = processed.split(' ').first;
// Convert to title case
if (firstName.isEmpty) return '';
return firstName[0].toUpperCase() + firstName.substring(1);
}
String getFullAccountType(String? accountType) {
if (accountType == null || accountType.isEmpty) return 'N/A';
// Convert to title case
switch (accountType.toLowerCase()) {
case 'sa':
return 'Savings Account';
case 'ln':
return 'Loan Account';
case 'td':
return 'Term Deposit Account';
case 'rd':
return 'Recurring Deposit Account';
default:
return 'Unknown Account Type';
}
}
Future<void> _showBiometricOptInDialog() async {
final storage = SecureStorage();
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Enable Biometric Authentication'),
content: const Text('Use fingerprint/face ID for faster login?'),
actions: [
TextButton(
onPressed: () async {
await storage.write('biometric_prompt_shown', 'true');
Navigator.of(context).pop();
},
child: const Text('Later'),
),
TextButton(
onPressed: () async {
final auth = LocalAuthentication();
final canCheck = await auth.canCheckBiometrics;
bool ok = false;
if (canCheck) {
ok = await auth.authenticate(
localizedReason: 'Scan to enable biometric login',
);
}
if (ok) {
await storage.write('biometric_enabled', 'true');
}
await storage.write('biometric_prompt_shown', 'true');
Navigator.of(context).pop();
},
child: const Text('Enable'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return BlocListener<AuthCubit, AuthState>(
listener: (context, state) async {
if (state is Authenticated && !_biometricPromptShown) {
_biometricPromptShown = true;
final storage = getIt<SecureStorage>();
final already = await storage.read('biometric_prompt_shown');
if (already == null) {
_showBiometricOptInDialog();
}
}
},
child: Scaffold(
backgroundColor: const Color(0xfff5f9fc),
appBar: AppBar(
backgroundColor: const Color(0xfff5f9fc),
automaticallyImplyLeading: false,
title: Text(
'kMobile',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.w500),
),
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const Preference()));
},
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
),
],
),
body: BlocBuilder<AuthCubit, AuthState>(builder: (context, state) {
if (state is AuthLoading || state is AuthInitial) {
return const Center(child: CircularProgressIndicator());
}
if (state is Authenticated) {
final users = state.users;
final currAccount = users[selectedAccountIndex];
final firstName = getProcessedFirstName(currAccount.name);
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Hi! $firstName",
style: GoogleFonts.montserrat().copyWith(
fontSize: 25,
color: Theme.of(context).primaryColorDark,
fontWeight: FontWeight.w700),
),
),
const SizedBox(height: 16),
// Account Info Card
Container(
padding: const EdgeInsets.symmetric(
horizontal: 18, vertical: 10),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Text("Account Number: ",
style: TextStyle(
color: Colors.white, fontSize: 12)),
DropdownButton<int>(
value: selectedAccountIndex,
dropdownColor: Theme.of(context).primaryColor,
underline: const SizedBox(),
icon: const Icon(Icons.keyboard_arrow_down),
iconEnabledColor: Colors.white,
style: const TextStyle(
color: Colors.white, fontSize: 14),
items: List.generate(users.length, (index) {
return DropdownMenuItem<int>(
value: index,
child: Text(
users[index].accountNo ?? 'N/A',
style: const TextStyle(
color: Colors.white, fontSize: 14),
),
);
}),
onChanged: (int? newIndex) async {
if (newIndex == null ||
newIndex == selectedAccountIndex) {
return;
}
if (isBalanceLoading) return;
if (isVisible) {
setState(() {
isBalanceLoading = true;
selectedAccountIndex = newIndex;
});
await Future.delayed(
const Duration(seconds: 1));
setState(() {
isBalanceLoading = false;
});
} else {
setState(() {
selectedAccountIndex = newIndex;
});
}
},
),
const Spacer(),
IconButton(
icon: isRefreshing
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.refresh,
color: Colors.white),
onPressed: isRefreshing
? null
: () => _refreshAccountData(context),
tooltip: 'Refresh',
),
],
),
Text(
getFullAccountType(currAccount.accountType),
style: const TextStyle(
color: Colors.white, fontSize: 16),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text("",
style: TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.w700)),
isRefreshing || isBalanceLoading
? _buildBalanceShimmer()
: Text(
isVisible
? currAccount.currentBalance ?? '0.00'
: '********',
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.w700)),
const Spacer(),
InkWell(
onTap: () async {
if (isBalanceLoading) return;
if (!isVisible) {
setState(() {
isBalanceLoading = true;
});
await Future.delayed(
const Duration(seconds: 1));
setState(() {
isVisible = true;
isBalanceLoading = false;
});
} else {
setState(() {
isVisible = false;
});
}
},
child: Icon(
isVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: Colors.white),
),
],
),
const SizedBox(height: 15),
],
),
),
const SizedBox(height: 18),
const Text(
'Quick Links',
style: TextStyle(fontSize: 17),
),
const SizedBox(height: 16),
// 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) => CustomerInfoScreen(
user: users[selectedAccountIndex],
)));
}),
_buildQuickLink(Symbols.currency_rupee, "Quick \n Pay",
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const QuickPayScreen()));
}),
_buildQuickLink(Symbols.send_money, "Fund \n Transfer",
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const FundTransferBeneficiaryScreen()));
}),
_buildQuickLink(
Symbols.server_person, "Account \n Info", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AccountInfoScreen(
user: users[selectedAccountIndex])));
}),
_buildQuickLink(
Symbols.receipt_long, "Account \n Statement", () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const AccountStatementScreen()));
}),
_buildQuickLink(
Symbols.checkbook, "Handle \n Cheque", () {},
disable: true),
_buildQuickLink(Icons.group, "Manage \n Beneficiary",
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const ManageBeneficiariesScreen()));
}),
_buildQuickLink(Symbols.support_agent, "Contact \n Us",
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EnquiryScreen()));
}),
],
),
const SizedBox(height: 5),
// Recent Transactions
const Text(
'Recent Transactions',
style: TextStyle(fontSize: 17),
),
const SizedBox(height: 16),
if (currAccount.transactions != null &&
currAccount.transactions!.isNotEmpty)
...currAccount.transactions!.map((tx) => ListTile(
leading: Icon(
tx.type == 'CR'
? Symbols.call_received
: Symbols.call_made,
color: tx.type == 'CR'
? Colors.green
: Colors.red),
title: Text(tx.name ?? ''),
subtitle: Text(tx.date ?? ''),
trailing: Text("${tx.amount}",
style: const TextStyle(
fontSize: 16,
)),
))
else
const EmptyTransactionsPlaceholder(),
],
),
),
);
}
return const Center(child: Text("Something went wrong"));
}),
),
);
}
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap,
{bool disable = false}) {
return InkWell(
onTap: onTap,
onTap: disable ? null : onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 30, color: Theme.of(context).primaryColor),
Icon(icon,
size: 30,
color: disable ? Colors.grey : Theme.of(context).primaryColor),
const SizedBox(height: 4),
Text(label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 12)),
style: const TextStyle(fontSize: 13)),
],
),
);

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
class EmptyTransactionsPlaceholder extends StatelessWidget {
const EmptyTransactionsPlaceholder({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: List.generate(5, (index) {
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// Placeholder for icon
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(width: 16),
// Placeholder for transaction details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 120,
height: 14,
color: Colors.grey[350],
),
const SizedBox(height: 8),
Container(
width: 80,
height: 10,
color: Colors.grey[300],
),
],
),
),
const SizedBox(width: 16),
// Placeholder for amount
Container(
width: 60,
height: 16,
color: Colors.grey[350],
),
],
),
);
}),
);
}
}

View File

@@ -3,7 +3,6 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:screenshot/screenshot.dart';
import 'package:social_share/social_share.dart';
import '../../../app.dart';
@@ -27,10 +26,10 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
final imagePath = File('${directory.path}/transaction_success.png');
await imagePath.writeAsBytes(imageBytes);
SocialShare.shareOptions(
"Transaction Successful",
imagePath: imagePath.path,
);
// SocialShare.shareOptions(
// "Transaction Successful",
// imagePath: imagePath.path,
// );
}
}