dart format

This commit is contained in:
2025-11-10 16:50:29 +05:30
parent d6f61ebb31
commit 8cfca113bf
28 changed files with 1995 additions and 1973 deletions

View File

@@ -142,24 +142,22 @@ class AuthService {
return; return;
} }
Future setTncflag() async{ Future setTncflag() async {
try { try {
final response = await _dio.post( final response = await _dio.post(
'/api/auth/tnc', '/api/auth/tnc',
data: {"flag": 'Y'}, data: {"flag": 'Y'},
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw AuthException('Failed to proceed with T&C'); throw AuthException('Failed to proceed with T&C');
} }
} } on DioException catch (e) {
on DioException catch (e) {
if (kDebugMode) { if (kDebugMode) {
print(e.toString()); print(e.toString());
} }
throw NetworkException('Network error during T&C Setup'); throw NetworkException('Network error during T&C Setup');
} catch (e) { } catch (e) {
throw UnexpectedException( throw UnexpectedException('Unexpected error: ${e.toString()}');
'Unexpected error: ${e.toString()}');
} }
} }
} }

View File

@@ -39,10 +39,10 @@ class LimitService {
} }
} }
void editLimit( double newLimit) async { void editLimit(double newLimit) async {
try { try {
final response = await _dio.post('/api/customer/daily-limit', final response = await _dio.post('/api/customer/daily-limit',
data: '{"amount": $newLimit}'); data: '{"amount": $newLimit}');
if (response.statusCode == 200) { if (response.statusCode == 200) {
log('Response: ${response.data}'); log('Response: ${response.data}');
} else { } else {

View File

@@ -10,7 +10,7 @@ import '../features/dashboard/screens/dashboard_screen.dart';
// import '../features/transactions/screens/transactions_screen.dart'; // import '../features/transactions/screens/transactions_screen.dart';
// import '../features/payments/screens/payments_screen.dart'; // import '../features/payments/screens/payments_screen.dart';
// import '../features/settings/screens/settings_screen.dart'; // import '../features/settings/screens/settings_screen.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart'; import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
class AppRoutes { class AppRoutes {
// Private constructor to prevent instantiation // Private constructor to prevent instantiation
@@ -36,7 +36,8 @@ class AppRoutes {
case login: case login:
return MaterialPageRoute(builder: (_) => const LoginScreen()); return MaterialPageRoute(builder: (_) => const LoginScreen());
case TncRequiredScreen.routeName: // Renamed class case TncRequiredScreen.routeName: // Renamed class
return MaterialPageRoute(builder: (_) => const TncRequiredScreen()); // Renamed class return MaterialPageRoute(
builder: (_) => const TncRequiredScreen()); // Renamed class
case mPin: case mPin:
return MaterialPageRoute( return MaterialPageRoute(
builder: (_) => const MPinScreen( builder: (_) => const MPinScreen(

View File

@@ -25,7 +25,9 @@ class Beneficiary {
return Beneficiary( return Beneficiary(
accountNo: json['account_no'] ?? json['accountNo'] ?? '', accountNo: json['account_no'] ?? json['accountNo'] ?? '',
accountType: json['account_type'] ?? json['accountType'] ?? '', accountType: json['account_type'] ?? json['accountType'] ?? '',
createdAt: json['createdAt'] == null ? null : DateTime.tryParse(json['createdAt']), createdAt: json['createdAt'] == null
? null
: DateTime.tryParse(json['createdAt']),
name: json['name'] ?? '', name: json['name'] ?? '',
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '', ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
bankName: json['bank_name'] ?? json['bankName'] ?? '', bankName: json['bank_name'] ?? json['bankName'] ?? '',

View File

@@ -17,7 +17,8 @@ class AuthRepository {
AuthRepository(this._authService, this._userService, this._secureStorage); AuthRepository(this._authService, this._userService, this._secureStorage);
Future<(List<User>, AuthToken)> login(String customerNo, String password) async { Future<(List<User>, AuthToken)> login(
String customerNo, String password) async {
// Create credentials and call service // Create credentials and call service
final credentials = final credentials =
AuthCredentials(customerNo: customerNo, password: password); AuthCredentials(customerNo: customerNo, password: password);
@@ -64,21 +65,22 @@ class AuthRepository {
final authToken = AuthToken( final authToken = AuthToken(
accessToken: accessToken, accessToken: accessToken,
expiresAt: DateTime.parse(expiryString), expiresAt: DateTime.parse(expiryString),
tnc: tncString == 'true', // Parse 'true' string to true, otherwise false tnc:
tncString == 'true', // Parse 'true' string to true, otherwise false
); );
return authToken; return authToken;
} }
return null; return null;
} }
Future<void> acceptTnc() async { Future<void> acceptTnc() async {
// This method calls the setTncFlag function // This method calls the setTncFlag function
try { try {
await _authService.setTncflag(); await _authService.setTncflag();
} catch (e) { } catch (e) {
// Handle or rethrow the error as needed // Handle or rethrow the error as needed
print('Error setting TNC flag: $e'); print('Error setting TNC flag: $e');
rethrow; rethrow;
} }
} }
} }

View File

@@ -61,15 +61,15 @@ Future<void> setupDependencies() async {
); );
// Register controllers/cubits // Register controllers/cubits
getIt.registerFactory<AuthCubit>( getIt.registerFactory<AuthCubit>(() => AuthCubit(
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>())); getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>()));
} }
Dio _createDioClient() { Dio _createDioClient() {
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: baseUrl:
'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
//'https://kccbmbnk.net', //prod small //'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60), connectTimeout: const Duration(seconds: 60),

View File

@@ -42,57 +42,55 @@ class AuthCubit extends Cubit<AuthState> {
} }
} }
Future<void> login(String customerNo, String password) async { Future<void> login(String customerNo, String password) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
final (users, authToken) = await _authRepository.login(customerNo, password); final (users, authToken) =
await _authRepository.login(customerNo, password);
if (authToken.tnc == false) { if (authToken.tnc == false) {
emit(ShowTncDialog(authToken, users)); emit(ShowTncDialog(authToken, users));
} else { } else {
await _checkMpinAndNavigate(users); await _checkMpinAndNavigate(users);
} }
} catch (e) { } catch (e) {
emit(AuthError(e is AuthException ? e.message : e.toString())); emit(AuthError(e is AuthException ? e.message : e.toString()));
} }
} }
Future<void> onTncDialogResult( Future<void> onTncDialogResult(
bool agreed, AuthToken authToken, List<User> users) async { bool agreed, AuthToken authToken, List<User> users) async {
if (agreed) { if (agreed) {
try { try {
await _authRepository.acceptTnc(); await _authRepository.acceptTnc();
// The user is NOT fully authenticated yet. Just check for MPIN. // The user is NOT fully authenticated yet. Just check for MPIN.
await _checkMpinAndNavigate(users); await _checkMpinAndNavigate(users);
} catch (e) { } catch (e) {
emit(AuthError('Failed to accept TNC: $e')); emit(AuthError('Failed to accept TNC: $e'));
} }
} else { } else {
emit(NavigateToTncRequiredScreen()); emit(NavigateToTncRequiredScreen());
} }
} }
void mpinSetupCompleted() { void mpinSetupCompleted() {
if (state is NavigateToMpinSetupScreen) { if (state is NavigateToMpinSetupScreen) {
final users = (state as NavigateToMpinSetupScreen).users; final users = (state as NavigateToMpinSetupScreen).users;
emit(Authenticated(users)); emit(Authenticated(users));
} else { } else {
// Handle unexpected state if necessary // Handle unexpected state if necessary
emit(AuthError("Invalid state during MPIN setup completion.")); emit(AuthError("Invalid state during MPIN setup completion."));
} }
} }
Future<void> _checkMpinAndNavigate(List<User> users) async {
final mpin = await _secureStorage.read('mpin');
if (mpin == null) {
// No MPIN, tell UI to navigate to MPIN setup, carrying user data
emit(NavigateToMpinSetupScreen(users));
} else {
// MPIN exists, user is authenticated
emit(Authenticated(users));
}
}
Future<void> _checkMpinAndNavigate(List<User> users) async {
final mpin = await _secureStorage.read('mpin');
if (mpin == null) {
// No MPIN, tell UI to navigate to MPIN setup, carrying user data
emit(NavigateToMpinSetupScreen(users));
} else {
// MPIN exists, user is authenticated
emit(Authenticated(users));
}
}
} }

View File

@@ -46,13 +46,13 @@ class ShowTncDialog extends AuthState {
// States to trigger specific navigations from the UI // States to trigger specific navigations from the UI
class NavigateToTncRequiredScreen extends AuthState {} class NavigateToTncRequiredScreen extends AuthState {}
class NavigateToMpinSetupScreen extends AuthState { class NavigateToMpinSetupScreen extends AuthState {
final List<User> users; final List<User> users;
const NavigateToMpinSetupScreen(this.users); const NavigateToMpinSetupScreen(this.users);
@override @override
List<Object> get props => [users]; List<Object> get props => [users];
} }
class NavigateToDashboardScreen extends AuthState {} class NavigateToDashboardScreen extends AuthState {}

View File

@@ -14,24 +14,25 @@ class AuthToken extends Equatable {
required this.tnc, required this.tnc,
}); });
factory AuthToken.fromJson(Map<String, dynamic> json) { factory AuthToken.fromJson(Map<String, dynamic> json) {
final token = json['token']; final token = json['token'];
// Safely extract tnc.mobile directly from the outer JSON // Safely extract tnc.mobile directly from the outer JSON
bool tncMobileValue = false; // Default to false if not found or invalid bool tncMobileValue = false; // Default to false if not found or invalid
if (json.containsKey('tnc') && json['tnc'] is Map<String, dynamic>) { if (json.containsKey('tnc') && json['tnc'] is Map<String, dynamic>) {
final tncMap = json['tnc'] as Map<String, dynamic>; final tncMap = json['tnc'] as Map<String, dynamic>;
if (tncMap.containsKey('mobile') && tncMap['mobile'] is bool) { if (tncMap.containsKey('mobile') && tncMap['mobile'] is bool) {
tncMobileValue = tncMap['mobile'] as bool; tncMobileValue = tncMap['mobile'] as bool;
}
} }
}
return AuthToken( return AuthToken(
accessToken: token, accessToken: token,
expiresAt: _decodeExpiryFromToken(token), // This method is still valid for JWT expiry expiresAt: _decodeExpiryFromToken(
tnc: tncMobileValue, // Use the correctly extracted value token), // This method is still valid for JWT expiry
); tnc: tncMobileValue, // Use the correctly extracted value
} );
}
static DateTime _decodeExpiryFromToken(String token) { static DateTime _decodeExpiryFromToken(String token) {
try { try {
@@ -55,7 +56,7 @@ class AuthToken extends Equatable {
return DateTime.now().add(const Duration(hours: 1)); return DateTime.now().add(const Duration(hours: 1));
} }
} }
// static bool _decodeTncFromToken(String token) { // static bool _decodeTncFromToken(String token) {
// try { // try {
// final parts = token.split('.'); // final parts = token.split('.');

View File

@@ -41,201 +41,201 @@ class LoginScreenState extends State<LoginScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// return Scaffold( // return Scaffold(
// body: BlocConsumer<AuthCubit, AuthState>( // body: BlocConsumer<AuthCubit, AuthState>(
// listener: (context, state) async { // listener: (context, state) async {
// if (state is ShowTncDialog) { // if (state is ShowTncDialog) {
// // The dialog now returns a boolean for the 'disagree' case, // // The dialog now returns a boolean for the 'disagree' case,
// // or it completes when the 'proceed' action is finished. // // or it completes when the 'proceed' action is finished.
// final agreed = await showDialog<bool>( // final agreed = await showDialog<bool>(
// context: context, // context: context,
// barrierDismissible: false, // barrierDismissible: false,
// builder: (dialogContext) => TncDialog( // builder: (dialogContext) => TncDialog(
// onProceed: () async { // onProceed: () async {
// // This function is passed to the dialog. // // This function is passed to the dialog.
// // It calls the cubit and completes when the cubit's work is done. // // It calls the cubit and completes when the cubit's work is done.
// await context // await context
// .read<AuthCubit>() // .read<AuthCubit>()
// .onTncDialogResult(true, state.authToken, state.users); // .onTncDialogResult(true, state.authToken, state.users);
// }, // },
// ), // ),
// ); // );
// // If 'agreed' is false, it means the user clicked 'Disagree'. // // If 'agreed' is false, it means the user clicked 'Disagree'.
// if (agreed == false) { // if (agreed == false) {
// if (!context.mounted) return; // if (!context.mounted) return;
// context // context
// .read<AuthCubit>() // .read<AuthCubit>()
// .onTncDialogResult(false, state.authToken, state.users); // .onTncDialogResult(false, state.authToken, state.users);
// } // }
// } else if (state is NavigateToTncRequiredScreen) { // } else if (state is NavigateToTncRequiredScreen) {
// Navigator.of(context).pushNamed(TncRequiredScreen.routeName); // Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
// } else if (state is NavigateToMpinSetupScreen) { // } else if (state is NavigateToMpinSetupScreen) {
// Navigator.of(context).push( // Use push, NOT pushReplacement // Navigator.of(context).push( // Use push, NOT pushReplacement
// MaterialPageRoute( // MaterialPageRoute(
// builder: (_) => MPinScreen( // builder: (_) => MPinScreen(
// mode: MPinMode.set, // mode: MPinMode.set,
// onCompleted: (_) { // onCompleted: (_) {
// // This clears the entire stack and pushes the dashboard // // This clears the entire stack and pushes the dashboard
// Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( // Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()), // MaterialPageRoute(builder: (_) => const NavigationScaffold()),
// (route) => false, // (route) => false,
// ); // );
// }, // },
// ), // ),
// ), // ),
// ); // );
// } else if (state is NavigateToDashboardScreen) { // } else if (state is NavigateToDashboardScreen) {
// Navigator.of(context).pushReplacement( // Navigator.of(context).pushReplacement(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()), // MaterialPageRoute(builder: (_) => const NavigationScaffold()),
// ); // );
// } else if (state is AuthError) { // } else if (state is AuthError) {
// if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') { // if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
// Navigator.of(context).push(MaterialPageRoute( // Navigator.of(context).push(MaterialPageRoute(
// builder: (_) => SetPasswordScreen( // builder: (_) => SetPasswordScreen(
// customerNo: _customerNumberController.text.trim(), // customerNo: _customerNumberController.text.trim(),
// ))); // )));
// } else { // } else {
// ScaffoldMessenger.of(context) // ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(content: Text(state.message))); // .showSnackBar(SnackBar(content: Text(state.message)));
// } // }
// } // }
// }, // },
// builder: (context, state) { // builder: (context, state) {
// // The commented out section is removed for clarity, the logic is now above. // // The commented out section is removed for clarity, the logic is now above.
// return Padding( // return Padding(
// padding: const EdgeInsets.all(24.0), // padding: const EdgeInsets.all(24.0),
// child: Form( // child: Form(
// key: _formKey, // key: _formKey,
// child: Column( // child: Column(
// mainAxisAlignment: MainAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.center,
// children: [ // children: [
// Image.asset( // Image.asset(
// 'assets/images/logo.png', // 'assets/images/logo.png',
// width: 150, // width: 150,
// height: 150, // height: 150,
// errorBuilder: (context, error, stackTrace) { // errorBuilder: (context, error, stackTrace) {
// return Icon( // return Icon(
// Icons.account_balance, // Icons.account_balance,
// size: 100, // size: 100,
// color: Theme.of(context).primaryColor, // color: Theme.of(context).primaryColor,
// ); // );
// }, // },
// ), // ),
// const SizedBox(height: 16), // const SizedBox(height: 16),
// Text( // Text(
// AppLocalizations.of(context).kccb, // AppLocalizations.of(context).kccb,
// style: TextStyle( // style: TextStyle(
// fontSize: 32, // fontSize: 32,
// fontWeight: FontWeight.bold, // fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor, // color: Theme.of(context).primaryColor,
// ), // ),
// ), // ),
// const SizedBox(height: 48), // const SizedBox(height: 48),
// TextFormField( // TextFormField(
// controller: _customerNumberController, // controller: _customerNumberController,
// decoration: InputDecoration( // decoration: InputDecoration(
// labelText: AppLocalizations.of(context).customerNumber, // labelText: AppLocalizations.of(context).customerNumber,
// border: const OutlineInputBorder(), // border: const OutlineInputBorder(),
// isDense: true, // isDense: true,
// filled: true, // filled: true,
// fillColor: Theme.of(context).scaffoldBackgroundColor, // fillColor: Theme.of(context).scaffoldBackgroundColor,
// enabledBorder: OutlineInputBorder( // enabledBorder: OutlineInputBorder(
// borderSide: BorderSide( // borderSide: BorderSide(
// color: Theme.of(context).colorScheme.outline), // color: Theme.of(context).colorScheme.outline),
// ), // ),
// focusedBorder: OutlineInputBorder( // focusedBorder: OutlineInputBorder(
// borderSide: BorderSide( // borderSide: BorderSide(
// color: Theme.of(context).colorScheme.primary, // color: Theme.of(context).colorScheme.primary,
// width: 2), // width: 2),
// ), // ),
// ), // ),
// keyboardType: TextInputType.number, // keyboardType: TextInputType.number,
// textInputAction: TextInputAction.next, // textInputAction: TextInputAction.next,
// validator: (value) { // validator: (value) {
// if (value == null || value.isEmpty) { // if (value == null || value.isEmpty) {
// return AppLocalizations.of(context).pleaseEnterUsername; // return AppLocalizations.of(context).pleaseEnterUsername;
// } // }
// return null; // return null;
// }, // },
// ), // ),
// const SizedBox(height: 24), // const SizedBox(height: 24),
// TextFormField( // TextFormField(
// controller: _passwordController, // controller: _passwordController,
// obscureText: _obscurePassword, // obscureText: _obscurePassword,
// textInputAction: TextInputAction.done, // textInputAction: TextInputAction.done,
// onFieldSubmitted: (_) => _submitForm(), // onFieldSubmitted: (_) => _submitForm(),
// decoration: InputDecoration( // decoration: InputDecoration(
// labelText: AppLocalizations.of(context).password, // labelText: AppLocalizations.of(context).password,
// border: const OutlineInputBorder(), // border: const OutlineInputBorder(),
// isDense: true, // isDense: true,
// filled: true, // filled: true,
// fillColor: Theme.of(context).scaffoldBackgroundColor, // fillColor: Theme.of(context).scaffoldBackgroundColor,
// enabledBorder: OutlineInputBorder( // enabledBorder: OutlineInputBorder(
// borderSide: BorderSide( // borderSide: BorderSide(
// color: Theme.of(context).colorScheme.outline), // color: Theme.of(context).colorScheme.outline),
// ), // ),
// focusedBorder: OutlineInputBorder( // focusedBorder: OutlineInputBorder(
// borderSide: BorderSide( // borderSide: BorderSide(
// color: Theme.of(context).colorScheme.primary, // color: Theme.of(context).colorScheme.primary,
// width: 2), // width: 2),
// ), // ),
// suffixIcon: IconButton( // suffixIcon: IconButton(
// icon: Icon( // icon: Icon(
// _obscurePassword // _obscurePassword
// ? Icons.visibility // ? Icons.visibility
// : Icons.visibility_off, // : Icons.visibility_off,
// ), // ),
// onPressed: () { // onPressed: () {
// setState(() { // setState(() {
// _obscurePassword = !_obscurePassword; // _obscurePassword = !_obscurePassword;
// }); // });
// }, // },
// ), // ),
// ), // ),
// validator: (value) { // validator: (value) {
// if (value == null || value.isEmpty) { // if (value == null || value.isEmpty) {
// return AppLocalizations.of(context).pleaseEnterPassword; // return AppLocalizations.of(context).pleaseEnterPassword;
// } // }
// return null; // return null;
// }, // },
// ), // ),
// const SizedBox(height: 24), // const SizedBox(height: 24),
// SizedBox( // SizedBox(
// width: 250, // width: 250,
// child: ElevatedButton( // child: ElevatedButton(
// onPressed: state is AuthLoading ? null : _submitForm, // onPressed: state is AuthLoading ? null : _submitForm,
// style: ElevatedButton.styleFrom( // style: ElevatedButton.styleFrom(
// shape: const StadiumBorder(), // shape: const StadiumBorder(),
// padding: const EdgeInsets.symmetric(vertical: 16), // padding: const EdgeInsets.symmetric(vertical: 16),
// backgroundColor: // backgroundColor:
// Theme.of(context).scaffoldBackgroundColor, // Theme.of(context).scaffoldBackgroundColor,
// foregroundColor: Theme.of(context).primaryColorDark, // foregroundColor: Theme.of(context).primaryColorDark,
// side: BorderSide( // side: BorderSide(
// color: Theme.of(context).colorScheme.outline, // color: Theme.of(context).colorScheme.outline,
// width: 1), // width: 1),
// elevation: 0, // elevation: 0,
// ), // ),
// child: state is AuthLoading // child: state is AuthLoading
// ? const CircularProgressIndicator() // ? const CircularProgressIndicator()
// : Text( // : Text(
// AppLocalizations.of(context).login, // AppLocalizations.of(context).login,
// style: TextStyle( // style: TextStyle(
// color: Theme.of(context) // color: Theme.of(context)
// .colorScheme // .colorScheme
// .onPrimaryContainer), // .onPrimaryContainer),
// ), // ),
// ), // ),
// ), // ),
// const SizedBox(height: 25), // const SizedBox(height: 25),
// ], // ],
// ), // ),
// ), // ),
// ); // );
// }, // },
// ), // ),
// ); // );
return Scaffold( return Scaffold(
body: BlocConsumer<AuthCubit, AuthState>( body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) { listener: (context, state) {
if (state is ShowTncDialog) { if (state is ShowTncDialog) {
@@ -255,7 +255,8 @@ class LoginScreenState extends State<LoginScreen>
} else if (state is NavigateToTncRequiredScreen) { } else if (state is NavigateToTncRequiredScreen) {
Navigator.of(context).pushNamed(TncRequiredScreen.routeName); Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
} else if (state is NavigateToMpinSetupScreen) { } else if (state is NavigateToMpinSetupScreen) {
Navigator.of(context).push( // Use push, NOT pushReplacement Navigator.of(context).push(
// Use push, NOT pushReplacement
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MPinScreen( builder: (_) => MPinScreen(
mode: MPinMode.set, mode: MPinMode.set,

View File

@@ -185,16 +185,16 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
), ),
); );
break; break;
case MPinMode.confirm: case MPinMode.confirm:
if (widget.initialPin == pin) { if (widget.initialPin == pin) {
// 1) persist the pin // 1) persist the pin
await storage.write('mpin', pin); await storage.write('mpin', pin);
// 2) Call the onCompleted callback to let the parent handle navigation // 2) Call the onCompleted callback to let the parent handle navigation
if (mounted) { if (mounted) {
widget.onCompleted?.call(pin); widget.onCompleted?.call(pin);
} }
} else { } else {
setState(() { setState(() {
_isError = true; _isError = true;
errorText = AppLocalizations.of(context).pinsDoNotMatch; errorText = AppLocalizations.of(context).pinsDoNotMatch;

View File

@@ -1,39 +1,40 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TncRequiredScreen extends StatelessWidget { // Renamed class class TncRequiredScreen extends StatelessWidget {
const TncRequiredScreen({Key? key}) : super(key: key); // Renamed class
const TncRequiredScreen({Key? key}) : super(key: key);
static const routeName = '/tnc-required'; static const routeName = '/tnc-required';
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Terms and Conditions'), title: const Text('Terms and Conditions'),
), ),
body: Center( body: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text( const Text(
'You must accept the Terms and Conditions to use the application.', 'You must accept the Terms and Conditions to use the application.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 18), style: TextStyle(fontSize: 18),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
// This will take the user back to the previous screen // This will take the user back to the previous screen
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text('Go Back'), child: const Text('Go Back'),
), ),
], ],
), ),
), ),
), ),
); );
} }
} }

View File

@@ -138,7 +138,7 @@ class _DashboardScreenState extends State<DashboardScreen>
// Convert to title case // Convert to title case
switch (accountType.toLowerCase()) { switch (accountType.toLowerCase()) {
case 'sa': case 'sa':
return AppLocalizations.of(context).savingsAccount; return AppLocalizations.of(context).savingsAccount;
case 'sb': case 'sb':
return AppLocalizations.of(context).savingsAccount; return AppLocalizations.of(context).savingsAccount;
case 'ln': case 'ln':
@@ -269,7 +269,9 @@ class _DashboardScreenState extends State<DashboardScreen>
final users = state.users; final users = state.users;
final currAccount = users[selectedAccountIndex]; final currAccount = users[selectedAccountIndex];
final accountType = currAccount.accountType?.toLowerCase(); final accountType = currAccount.accountType?.toLowerCase();
final isPaymentDisabled = accountType != 'sa' && accountType != 'sb' && accountType != 'ca'; final isPaymentDisabled = accountType != 'sa' &&
accountType != 'sb' &&
accountType != 'ca';
// firsttime load // firsttime load
if (!_txInitialized) { if (!_txInitialized) {
_txInitialized = true; _txInitialized = true;
@@ -484,36 +486,35 @@ class _DashboardScreenState extends State<DashboardScreen>
); );
}, },
), ),
_buildQuickLink( _buildQuickLink(
Symbols.currency_rupee, Symbols.currency_rupee,
AppLocalizations.of(context).quickPay, AppLocalizations.of(context).quickPay,
() { () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => QuickPayScreen( builder: (context) => QuickPayScreen(
debitAccount: currAccount.accountNo!, debitAccount: currAccount.accountNo!,
), ),
), ),
); );
}, },
disable: isPaymentDisabled, disable: isPaymentDisabled,
), ),
_buildQuickLink(Symbols.send_money, _buildQuickLink(Symbols.send_money,
AppLocalizations.of(context).fundTransfer, () { AppLocalizations.of(context).fundTransfer, () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FundTransferScreen( builder: (context) => FundTransferScreen(
creditAccountNo: creditAccountNo:
users[selectedAccountIndex] users[selectedAccountIndex]
.accountNo!, .accountNo!,
remitterName: remitterName:
users[selectedAccountIndex] users[selectedAccountIndex].name!,
.name!, // Pass the full list of accounts
// Pass the full list of accounts accounts: users)));
accounts: users))); }, disable: isPaymentDisabled),
}, disable: isPaymentDisabled),
_buildQuickLink( _buildQuickLink(
Symbols.server_person, Symbols.server_person,
AppLocalizations.of(context).accountInfo, AppLocalizations.of(context).accountInfo,

View File

@@ -1,90 +1,91 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class CooldownTimer extends StatefulWidget { class CooldownTimer extends StatefulWidget {
final DateTime createdAt; final DateTime createdAt;
final VoidCallback onTimerFinish; final VoidCallback onTimerFinish;
const CooldownTimer({ const CooldownTimer({
Key? key, Key? key,
required this.createdAt, required this.createdAt,
required this.onTimerFinish, required this.onTimerFinish,
}) : super(key: key); }) : super(key: key);
@override @override
_CooldownTimerState createState() => _CooldownTimerState(); _CooldownTimerState createState() => _CooldownTimerState();
} }
class _CooldownTimerState extends State<CooldownTimer> { class _CooldownTimerState extends State<CooldownTimer> {
late Timer _timer; late Timer _timer;
late Duration _timeRemaining; late Duration _timeRemaining;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_updateRemainingTime(); _updateRemainingTime();
// Update the timer every second // Update the timer every second
_timer = Timer.periodic(const Duration(seconds: 1), (timer) { _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
_updateRemainingTime(); _updateRemainingTime();
}); });
} }
void _updateRemainingTime() { void _updateRemainingTime() {
final cooldownEnd = widget.createdAt.add(const Duration(minutes: 60)); final cooldownEnd = widget.createdAt.add(const Duration(minutes: 60));
final now = DateTime.now(); final now = DateTime.now();
if (now.isAfter(cooldownEnd)) { if (now.isAfter(cooldownEnd)) {
_timeRemaining = Duration.zero; _timeRemaining = Duration.zero;
_timer.cancel(); _timer.cancel();
// Notify the parent widget that the timer is done // Notify the parent widget that the timer is done
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
widget.onTimerFinish(); widget.onTimerFinish();
}); });
} else { } else {
_timeRemaining = cooldownEnd.difference(now); _timeRemaining = cooldownEnd.difference(now);
} }
// Trigger a rebuild // Trigger a rebuild
setState(() {}); setState(() {});
} }
@override @override
void dispose() { void dispose() {
_timer.cancel(); _timer.cancel();
super.dispose(); super.dispose();
} }
String _formatDuration(Duration duration) { String _formatDuration(Duration duration) {
final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$minutes:$seconds'; return '$minutes:$seconds';
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_timeRemaining == Duration.zero) { if (_timeRemaining == Duration.zero) {
return const SizedBox.shrink(); // Or some other widget indicating it's enabled return const SizedBox
} .shrink(); // Or some other widget indicating it's enabled
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
'Enabled after:', 'Enabled after:',
style: TextStyle( style: TextStyle(
color: Colors.red.shade700, color: Colors.red.shade700,
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
Text( Text(
_formatDuration(_timeRemaining), _formatDuration(_timeRemaining),
style: TextStyle( style: TextStyle(
color: Colors.red.shade700, color: Colors.red.shade700,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
], ],
); );
} }
} }

View File

@@ -43,62 +43,64 @@ class FundTransferAmountScreen extends StatefulWidget {
class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> { class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
final _limitService = getIt<LimitService>(); final _limitService = getIt<LimitService>();
Limit? _limit; Limit? _limit;
bool _isLoadingLimit = true; bool _isLoadingLimit = true;
bool _isAmountOverLimit = false; bool _isAmountOverLimit = false;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _remarksController = TextEditingController(); final _remarksController = TextEditingController();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
TransactionMode _selectedMode = TransactionMode.neft; TransactionMode _selectedMode = TransactionMode.neft;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit(); // Call the new method _loadLimit(); // Call the new method
_amountController.addListener(_checkAmountLimit); _amountController.addListener(_checkAmountLimit);
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
} }
if (_isAmountOverLimit != isOverLimit) { Future<void> _loadLimit() async {
setState(() { setState(() {
_isAmountOverLimit = isOverLimit; _isLoadingLimit = true;
}); });
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
} }
}
@override @override
void dispose() { void dispose() {
@@ -486,27 +488,26 @@ void _checkAmountLimit() {
return null; return null;
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (_isLoadingLimit) if (_isLoadingLimit) const Text('Fetching daily limit...'),
const Text('Fetching daily limit...'), if (!_isLoadingLimit && _limit != null)
if (!_isLoadingLimit && _limit != null) Text(
Text( 'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}', style: Theme.of(context).textTheme.bodySmall,
style: Theme.of(context).textTheme.bodySmall, ),
),
const Spacer(), const Spacer(),
// Proceed Button // Proceed Button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed, onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
), ),
child: Text(AppLocalizations.of(context).proceed), child: Text(AppLocalizations.of(context).proceed),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
], ],
), ),

View File

@@ -73,8 +73,7 @@ class _FundTransferBeneficiaryScreenState
); );
} }
Widget _buildBeneficiaryList() {
Widget _buildBeneficiaryList() {
if (_beneficiaries.isEmpty) { if (_beneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
@@ -155,7 +154,6 @@ class _FundTransferBeneficiaryScreenState
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(

View File

@@ -1,126 +1,126 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart'; import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart'; import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart';
import 'package:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../../l10n/app_localizations.dart'; // Keep localizations import '../../../l10n/app_localizations.dart'; // Keep localizations
class FundTransferScreen extends StatelessWidget { class FundTransferScreen extends StatelessWidget {
final String creditAccountNo; final String creditAccountNo;
final String remitterName; final String remitterName;
final List<User> accounts; // Continue to accept the list of accounts final List<User> accounts; // Continue to accept the list of accounts
const FundTransferScreen({ const FundTransferScreen({
super.key, super.key,
required this.creditAccountNo, required this.creditAccountNo,
required this.remitterName, required this.remitterName,
required this.accounts, // It is passed from the dashboard required this.accounts, // It is passed from the dashboard
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// Restore localization for the title // Restore localization for the title
title: Text(AppLocalizations.of(context) title: Text(AppLocalizations.of(context)
.fundTransfer .fundTransfer
.replaceFirst(RegExp('\n'), '')), .replaceFirst(RegExp('\n'), '')),
), ),
// Wrap with BlocBuilder to check the authentication state // Wrap with BlocBuilder to check the authentication state
body: BlocBuilder<AuthCubit, AuthState>( body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) { builder: (context, state) {
return ListView( return ListView(
children: [ children: [
FundTransferManagementTile( FundTransferManagementTile(
icon: Symbols.person, icon: Symbols.person,
// Restore localization for the label // Restore localization for the label
label: "Self Pay", label: "Self Pay",
onTap: () { onTap: () {
// The accounts list is passed directly from the constructor // The accounts list is passed directly from the constructor
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FundTransferSelfAccountsScreen( builder: (context) => FundTransferSelfAccountsScreen(
debitAccountNo: creditAccountNo, debitAccountNo: creditAccountNo,
remitterName: remitterName, remitterName: remitterName,
accounts: accounts, accounts: accounts,
), ),
), ),
); );
}, },
// Disable the tile if the state is not Authenticated // Disable the tile if the state is not Authenticated
disable: state is! Authenticated, disable: state is! Authenticated,
), ),
const Divider(height: 1), const Divider(height: 1),
FundTransferManagementTile( FundTransferManagementTile(
icon: Symbols.input_circle, icon: Symbols.input_circle,
// Restore localization for the label // Restore localization for the label
label: AppLocalizations.of(context).ownBank, label: AppLocalizations.of(context).ownBank,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen( builder: (context) => FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo, creditAccountNo: creditAccountNo,
remitterName: remitterName, remitterName: remitterName,
isOwnBank: true, isOwnBank: true,
), ),
), ),
); );
}, },
), ),
const Divider(height: 1), const Divider(height: 1),
FundTransferManagementTile( FundTransferManagementTile(
icon: Symbols.output_circle, icon: Symbols.output_circle,
// Restore localization for the label // Restore localization for the label
label: AppLocalizations.of(context).outsideBank, label: AppLocalizations.of(context).outsideBank,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen( builder: (context) => FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo, creditAccountNo: creditAccountNo,
remitterName: remitterName, remitterName: remitterName,
isOwnBank: false, isOwnBank: false,
), ),
), ),
); );
}, },
), ),
const Divider(height: 1), const Divider(height: 1),
], ],
); );
}, },
), ),
); );
} }
} }
class FundTransferManagementTile extends StatelessWidget { class FundTransferManagementTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final VoidCallback onTap; final VoidCallback onTap;
final bool disable; final bool disable;
const FundTransferManagementTile({ const FundTransferManagementTile({
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
required this.onTap, required this.onTap,
this.disable = false, this.disable = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( return ListTile(
leading: Icon(icon), leading: Icon(icon),
title: Text(label), title: Text(label),
trailing: const Icon(Symbols.arrow_right, size: 20), trailing: const Icon(Symbols.arrow_right, size: 20),
onTap: onTap, onTap: onTap,
enabled: !disable, enabled: !disable,
); );
} }
} }

View File

@@ -1,94 +1,93 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart';
import 'package:kmobile/widgets/bank_logos.dart'; import 'package:kmobile/widgets/bank_logos.dart';
class FundTransferSelfAccountsScreen extends StatelessWidget { class FundTransferSelfAccountsScreen extends StatelessWidget {
final String debitAccountNo; final String debitAccountNo;
final String remitterName; final String remitterName;
final List<User> accounts; final List<User> accounts;
const FundTransferSelfAccountsScreen({ const FundTransferSelfAccountsScreen({
super.key, super.key,
required this.debitAccountNo, required this.debitAccountNo,
required this.remitterName, required this.remitterName,
required this.accounts, required this.accounts,
}); });
// Helper function to get the full account type name from the short code // Helper function to get the full account type name from the short code
String _getFullAccountType(String? accountType) { String _getFullAccountType(String? accountType) {
if (accountType == null || accountType.isEmpty) return 'N/A'; if (accountType == null || accountType.isEmpty) return 'N/A';
switch (accountType.toLowerCase()) { switch (accountType.toLowerCase()) {
case 'sa': case 'sa':
case 'sb': case 'sb':
return "Savings Account"; return "Savings Account";
case 'ln': case 'ln':
return "Loan Account"; return "Loan Account";
case 'td': case 'td':
return "Term Deposit"; return "Term Deposit";
case 'rd': case 'rd':
return "Recurring Deposit"; return "Recurring Deposit";
case 'ca': case 'ca':
return "Current Account"; return "Current Account";
default: default:
return "Unknown Account"; return "Unknown Account";
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Filter out the account from which the transfer is being made // Filter out the account from which the transfer is being made
final filteredAccounts = final filteredAccounts =
accounts.where((acc) => acc.accountNo != debitAccountNo).toList(); accounts.where((acc) => acc.accountNo != debitAccountNo).toList();
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Select Account"), title: const Text("Select Account"),
), ),
body: filteredAccounts.isEmpty body: filteredAccounts.isEmpty
? const Center( ? const Center(
child: Text("No other accounts found"), child: Text("No other accounts found"),
) )
: ListView.builder( : ListView.builder(
itemCount: filteredAccounts.length, itemCount: filteredAccounts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final account = filteredAccounts[index]; final account = filteredAccounts[index];
return ListTile( return ListTile(
leading: CircleAvatar( leading: CircleAvatar(
radius: 24, radius: 24,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: child: getBankLogo(
getBankLogo('Kangra Central Co-operative Bank', context), 'Kangra Central Co-operative Bank', context),
), ),
title: Text(account.name ?? 'N/A'), title: Text(account.name ?? 'N/A'),
subtitle: Column( subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(account.accountNo ?? 'N/A'), Text(account.accountNo ?? 'N/A'),
Text( Text(
_getFullAccountType(account.accountType), _getFullAccountType(account.accountType),
style: style: TextStyle(fontSize: 12, color: Colors.grey[600]),
TextStyle(fontSize: 12, color: Colors.grey[600]), ),
), ],
], ),
), onTap: () {
onTap: () { // Navigate to the amount screen, passing the selected User object directly.
// Navigate to the amount screen, passing the selected User object directly. // No Beneficiary object is created.
// No Beneficiary object is created. Navigator.push(
Navigator.push( context,
context, MaterialPageRoute(
MaterialPageRoute( builder: (context) => FundTransferSelfAmountScreen(
builder: (context) => FundTransferSelfAmountScreen( debitAccountNo: debitAccountNo,
debitAccountNo: debitAccountNo, creditAccount: account, // Pass the User object
creditAccount: account, // Pass the User object remitterName: remitterName,
remitterName: remitterName, ),
), ),
), );
); },
}, );
); },
}, ),
), );
); }
} }
}

View File

@@ -1,245 +1,244 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:kmobile/api/services/limit_service.dart'; import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/payment_service.dart'; import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart'; import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart';
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
import 'package:kmobile/widgets/bank_logos.dart'; import 'package:kmobile/widgets/bank_logos.dart';
class FundTransferSelfAmountScreen extends StatefulWidget { class FundTransferSelfAmountScreen extends StatefulWidget {
final String debitAccountNo; final String debitAccountNo;
final User creditAccount; final User creditAccount;
final String remitterName; final String remitterName;
const FundTransferSelfAmountScreen({ const FundTransferSelfAmountScreen({
super.key, super.key,
required this.debitAccountNo, required this.debitAccountNo,
required this.creditAccount, required this.creditAccount,
required this.remitterName, required this.remitterName,
}); });
@override @override
State<FundTransferSelfAmountScreen> createState() => State<FundTransferSelfAmountScreen> createState() =>
_FundTransferSelfAmountScreenState(); _FundTransferSelfAmountScreenState();
} }
class _FundTransferSelfAmountScreenState class _FundTransferSelfAmountScreenState
extends State<FundTransferSelfAmountScreen> { extends State<FundTransferSelfAmountScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _remarksController = TextEditingController(); final _remarksController = TextEditingController();
// --- Limit Checking Variables --- // --- Limit Checking Variables ---
final _limitService = getIt<LimitService>(); final _limitService = getIt<LimitService>();
Limit? _limit; Limit? _limit;
bool _isLoadingLimit = true; bool _isLoadingLimit = true;
bool _isAmountOverLimit = false; bool _isAmountOverLimit = false;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
@override
@override void initState() {
void initState() { super.initState();
super.initState(); _loadLimit(); // Fetch the daily limit
_loadLimit(); // Fetch the daily limit _amountController
_amountController.addListener(_checkAmountLimit); // Listen for amount changes .addListener(_checkAmountLimit); // Listen for amount changes
}
@override
void dispose() {
_amountController.removeListener(_checkAmountLimit);
_amountController.dispose();
_remarksController.dispose();
super.dispose();
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
setState(() {
_isLoadingLimit = false;
});
}
}
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
});
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
void _onProceed() {
if (_formKey.currentState!.validate()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TransactionPinScreen(
onPinCompleted: (pinScreenContext, tpin) async {
final transfer = Transfer(
fromAccount: widget.debitAccountNo,
toAccount: widget.creditAccount.accountNo!,
toAccountType: 'Savings', // Assuming 'SB' for savings
amount: _amountController.text,
tpin: tpin,
);
final paymentService = getIt<PaymentService>();
final paymentResponseFuture =
paymentService.processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement(
MaterialPageRoute(
builder: (_) =>
PaymentAnimationScreen(paymentResponse: paymentResponseFuture),
),
);
},
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Fund Transfer"),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Debit Account (User)
Text(
"Debit From",
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: Image.asset(
'assets/images/logo.png',
width: 40,
height: 40,
),
title: Text(widget.remitterName),
subtitle: Text(widget.debitAccountNo),
),
),
const SizedBox(height: 24),
// Credit Account (Self)
Text(
"Credited To",
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading:
getBankLogo('Kangra Central Co-operative Bank', context),
title: Text(widget.creditAccount.name ?? 'N/A'),
subtitle: Text(widget.creditAccount.accountNo ?? 'N/A'),
),
),
const SizedBox(height: 24),
// Remarks
TextFormField(
controller: _remarksController,
decoration: const InputDecoration(
labelText: "Remarks (Optional)",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "Amount",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return "Amount is required";
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return "Please enter a valid amount";
}
return null;
},
),
const SizedBox(height: 8),
// Daily Limit Display
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text("Proceed"),
),
),
const SizedBox(height: 10),
],
),
),
),
),
);
}
} }
@override
void dispose() {
_amountController.removeListener(_checkAmountLimit);
_amountController.dispose();
_remarksController.dispose();
super.dispose();
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
setState(() {
_isLoadingLimit = false;
});
}
}
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
WidgetsBinding.instance.addPostFrameCallback((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
});
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
void _onProceed() {
if (_formKey.currentState!.validate()) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TransactionPinScreen(
onPinCompleted: (pinScreenContext, tpin) async {
final transfer = Transfer(
fromAccount: widget.debitAccountNo,
toAccount: widget.creditAccount.accountNo!,
toAccountType: 'Savings', // Assuming 'SB' for savings
amount: _amountController.text,
tpin: tpin,
);
final paymentService = getIt<PaymentService>();
final paymentResponseFuture =
paymentService.processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement(
MaterialPageRoute(
builder: (_) => PaymentAnimationScreen(
paymentResponse: paymentResponseFuture),
),
);
},
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Fund Transfer"),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Debit Account (User)
Text(
"Debit From",
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: Image.asset(
'assets/images/logo.png',
width: 40,
height: 40,
),
title: Text(widget.remitterName),
subtitle: Text(widget.debitAccountNo),
),
),
const SizedBox(height: 24),
// Credit Account (Self)
Text(
"Credited To",
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: getBankLogo(
'Kangra Central Co-operative Bank', context),
title: Text(widget.creditAccount.name ?? 'N/A'),
subtitle: Text(widget.creditAccount.accountNo ?? 'N/A'),
),
),
const SizedBox(height: 24),
// Remarks
TextFormField(
controller: _remarksController,
decoration: const InputDecoration(
labelText: "Remarks (Optional)",
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: "Amount",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return "Amount is required";
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return "Please enter a valid amount";
}
return null;
},
),
const SizedBox(height: 8),
// Daily Limit Display
if (_isLoadingLimit) const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text("Proceed"),
),
),
const SizedBox(height: 10),
],
),
),
),
),
);
}
}

View File

@@ -16,24 +16,24 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
double? _spentAmount = 0.0; double? _spentAmount = 0.0;
final _limitController = TextEditingController(); final _limitController = TextEditingController();
var service = getIt<LimitService>(); var service = getIt<LimitService>();
Limit? limit; Limit? limit;
bool _isLoading = true; bool _isLoading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadlimits(); _loadlimits();
} }
Future<void> _loadlimits() async { Future<void> _loadlimits() async {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
final limit_data = await service.getLimit(); final limit_data = await service.getLimit();
setState(() { setState(() {
limit = limit_data; limit = limit_data;
_isLoading = false; _isLoading = false;
}); });
} }
@override @override
@@ -42,74 +42,74 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
super.dispose(); super.dispose();
} }
Future<void> _showAddOrEditLimitDialog() async { Future<void> _showAddOrEditLimitDialog() async {
_limitController.text = _currentLimit?.toStringAsFixed(0) ?? ''; _limitController.text = _currentLimit?.toStringAsFixed(0) ?? '';
final newLimit = await showDialog<double>( final newLimit = await showDialog<double>(
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
final localizations = AppLocalizations.of(dialogContext); final localizations = AppLocalizations.of(dialogContext);
final theme = Theme.of(dialogContext); final theme = Theme.of(dialogContext);
return AlertDialog( return AlertDialog(
title: Text( title: Text(
_currentLimit == null _currentLimit == null
? localizations.addLimit ? localizations.addLimit
: localizations.editLimit, : localizations.editLimit,
), ),
content: TextField( content: TextField(
controller: _limitController, controller: _limitController,
autofocus: true, autofocus: true,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [ inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'^\d+')), FilteringTextInputFormatter.allow(RegExp(r'^\d+')),
],
decoration: InputDecoration(
labelText: localizations.limitAmount,
prefixText: '',
border: const OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(localizations.cancel),
),
ElevatedButton(
onPressed: () {
final value = double.tryParse(_limitController.text);
if (value == null || value <= 0) return;
if (value > 200000) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text(
"Limit To be Set must be less than 200000"),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.error,
),
);
} else {
service.editLimit(value);
Navigator.of(dialogContext).pop(value);
}
},
child: Text(localizations.save),
),
], ],
decoration: InputDecoration( );
labelText: localizations.limitAmount, },
prefixText: '',
border: const OutlineInputBorder(),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(localizations.cancel),
),
ElevatedButton(
onPressed: () {
final value = double.tryParse(_limitController.text);
if (value == null || value <= 0) return;
if (value > 200000) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("Limit To be Set must be less than 200000"),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.error,
),
);
} else {
service.editLimit(value);
Navigator.of(dialogContext).pop(value);
}
},
child: Text(localizations.save),
),
],
);
},
);
if (newLimit != null) {
_loadlimits();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Limit Updated"),
behavior: SnackBarBehavior.floating,
),
); );
}
}
if (newLimit != null) {
_loadlimits();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Limit Updated"),
behavior: SnackBarBehavior.floating,
),
);
}
}
void _removeLimit() { void _removeLimit() {
setState(() { setState(() {
@@ -117,112 +117,113 @@ Future<void> _showAddOrEditLimitDialog() async {
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) { if (_isLoading) {
final localizations = AppLocalizations.of(context); final localizations = AppLocalizations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(localizations.dailylimit), title: Text(localizations.dailylimit),
), ),
body: const Center( body: const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
); );
} }
_currentLimit = limit?.dailyLimit; _currentLimit = limit?.dailyLimit;
_spentAmount = limit?.usedLimit; _spentAmount = limit?.usedLimit;
final localizations = AppLocalizations.of(context); final localizations = AppLocalizations.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final remainingLimit = _currentLimit != null ? _currentLimit! - _spentAmount! : 0.0; final remainingLimit =
_currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(localizations.dailylimit), title: Text(localizations.dailylimit),
), ),
body: Center( body: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( Text(
localizations.currentDailyLimit, localizations.currentDailyLimit,
style: theme.textTheme.headlineSmall?.copyWith( style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7), color: theme.colorScheme.onSurface.withOpacity(0.7),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
_currentLimit == null _currentLimit == null
? localizations.noLimitSet ? localizations.noLimitSet
: formatCurrency.format(_currentLimit), : formatCurrency.format(_currentLimit),
style: theme.textTheme.headlineMedium?.copyWith( style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: _currentLimit == null color: _currentLimit == null
? theme.colorScheme.secondary ? theme.colorScheme.secondary
: theme.colorScheme.primary, : theme.colorScheme.primary,
), ),
), ),
if (_currentLimit != null) ...[ if (_currentLimit != null) ...[
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
"Remaining Limit Today", // This should be localized "Remaining Limit Today", // This should be localized
style: theme.textTheme.titleMedium, style: theme.textTheme.titleMedium,
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
formatCurrency.format(remainingLimit), formatCurrency.format(remainingLimit),
style: theme.textTheme.headlineSmall?.copyWith( style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: remainingLimit > 0 color: remainingLimit > 0
? Colors.green ? Colors.green
: theme.colorScheme.error, : theme.colorScheme.error,
), ),
), ),
], ],
const SizedBox(height: 48), const SizedBox(height: 48),
if (_currentLimit == null) if (_currentLimit == null)
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _showAddOrEditLimitDialog, onPressed: _showAddOrEditLimitDialog,
icon: const Icon(Icons.add_circle_outline), icon: const Icon(Icons.add_circle_outline),
label: Text(localizations.addLimit), label: Text(localizations.addLimit),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12), horizontal: 24, vertical: 12),
textStyle: theme.textTheme.titleMedium, textStyle: theme.textTheme.titleMedium,
), ),
) )
else else
Column( Column(
children: [ children: [
ElevatedButton.icon( ElevatedButton.icon(
onPressed: _showAddOrEditLimitDialog, onPressed: _showAddOrEditLimitDialog,
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
label: Text(localizations.editLimit), label: Text(localizations.editLimit),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12), horizontal: 24, vertical: 12),
textStyle: theme.textTheme.titleMedium, textStyle: theme.textTheme.titleMedium,
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// TextButton.icon( // TextButton.icon(
// onPressed: _removeLimit, // onPressed: _removeLimit,
// icon: const Icon(Icons.remove_circle_outline), // icon: const Icon(Icons.remove_circle_outline),
// label: Text(localizations.removeLimit), // label: Text(localizations.removeLimit),
// style: TextButton.styleFrom( // style: TextButton.styleFrom(
// foregroundColor: theme.colorScheme.error, // foregroundColor: theme.colorScheme.error,
// ), // ),
// ), // ),
], ],
), ),
], ],
), ),
), ),
), ),
); );
} }
} }

View File

@@ -16,7 +16,6 @@ import 'package:kmobile/features/profile/preferences/preference_screen.dart';
import 'package:kmobile/api/services/auth_service.dart'; import 'package:kmobile/api/services/auth_service.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
final String mobileNumber; final String mobileNumber;
const ProfileScreen({super.key, required this.mobileNumber}); const ProfileScreen({super.key, required this.mobileNumber});
@@ -38,12 +37,12 @@ class _ProfileScreenState extends State<ProfileScreen> {
return 'Version ${info.version} (${info.buildNumber})'; return 'Version ${info.version} (${info.buildNumber})';
} }
Future<void> _loadBiometricStatus() async { Future<void> _loadBiometricStatus() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
setState(() { setState(() {
_isBiometricEnabled = prefs.getBool('biometric_enabled') ?? false; _isBiometricEnabled = prefs.getBool('biometric_enabled') ?? false;
}); });
} }
Future<void> _handleLogout(BuildContext context) async { Future<void> _handleLogout(BuildContext context) async {
final auth = getIt<AuthRepository>(); final auth = getIt<AuthRepository>();
@@ -54,90 +53,90 @@ class _ProfileScreenState extends State<ProfileScreen> {
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false); Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
} }
Future<void> _handleBiometricToggle(bool enable) async { Future<void> _handleBiometricToggle(bool enable) async {
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) { if (!canCheck) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context).biometricsNotAvailable)), content: Text(AppLocalizations.of(context).biometricsNotAvailable)),
); );
return; return;
} }
if (enable) { if (enable) {
final optIn = await showDialog<bool>( final optIn = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context).enableFingerprintLogin), title: Text(AppLocalizations.of(context).enableFingerprintLogin),
content: Text(AppLocalizations.of(context).enableFingerprintMessage), content: Text(AppLocalizations.of(context).enableFingerprintMessage),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(false), onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no), child: Text(AppLocalizations.of(context).no),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(true), onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes), child: Text(AppLocalizations.of(context).yes),
), ),
], ],
), ),
); );
if (optIn == true) { if (optIn == true) {
try { try {
final didAuth = await localAuth.authenticate( final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).authenticateToEnable, localizedReason: AppLocalizations.of(context).authenticateToEnable,
options: const AuthenticationOptions( options: const AuthenticationOptions(
stickyAuth: true, stickyAuth: true,
biometricOnly: true, biometricOnly: true,
), ),
); );
if (didAuth) { if (didAuth) {
await prefs.setBool('biometric_enabled', true); await prefs.setBool('biometric_enabled', true);
if (mounted) { if (mounted) {
setState(() { setState(() {
_isBiometricEnabled = true; _isBiometricEnabled = true;
}); });
} }
} }
} catch (e) { } catch (e) {
// Handle exceptions, state remains unchanged. // Handle exceptions, state remains unchanged.
} }
} }
} else { } else {
final optOut = await showDialog<bool>( final optOut = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context).disableFingerprintLogin), title: Text(AppLocalizations.of(context).disableFingerprintLogin),
content: Text(AppLocalizations.of(context).disableFingerprintMessage), content: Text(AppLocalizations.of(context).disableFingerprintMessage),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(false), onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no), child: Text(AppLocalizations.of(context).no),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(ctx).pop(true), onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes), child: Text(AppLocalizations.of(context).yes),
), ),
], ],
), ),
); );
if (optOut == true) { if (optOut == true) {
await prefs.setBool('biometric_enabled', false); await prefs.setBool('biometric_enabled', false);
if (mounted) { if (mounted) {
setState(() { setState(() {
_isBiometricEnabled = false; _isBiometricEnabled = false;
}); });
} }
} }
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -172,14 +171,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
}, },
), ),
SwitchListTile( SwitchListTile(
title: Text(AppLocalizations.of(context).enableFingerprintLogin), title: Text(AppLocalizations.of(context).enableFingerprintLogin),
value: _isBiometricEnabled, value: _isBiometricEnabled,
onChanged: (bool value) { onChanged: (bool value) {
// The state is now managed within _handleBiometricToggle // The state is now managed within _handleBiometricToggle
_handleBiometricToggle(value); _handleBiometricToggle(value);
}, },
secondary: const Icon(Icons.fingerprint), secondary: const Icon(Icons.fingerprint),
), ),
ListTile( ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
title: Text(loc.changeLoginPassword), title: Text(loc.changeLoginPassword),
@@ -195,55 +194,57 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
ListTile( ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
title: Text('Change TPIN'), title: Text('Change TPIN'),
onTap: () async { onTap: () async {
// 1. Get the AuthService instance // 1. Get the AuthService instance
final authService = getIt<AuthService>(); final authService = getIt<AuthService>();
// 2. Call checkTpin() to see if TPIN is set // 2. Call checkTpin() to see if TPIN is set
final isTpinSet = await authService.checkTpin(); final isTpinSet = await authService.checkTpin();
// 3. If TPIN is not set, show the dialog // 3. If TPIN is not set, show the dialog
if (!isTpinSet) { if (!isTpinSet) {
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text('TPIN Not Set'), title: Text('TPIN Not Set'),
content: Text('You have not set a TPIN yet. Please set a TPIN to proceed.'), content: Text(
actions: <Widget>[ 'You have not set a TPIN yet. Please set a TPIN to proceed.'),
TextButton( actions: <Widget>[
child: Text('Back'), TextButton(
onPressed: () { child: Text('Back'),
Navigator.of(context).pop(); onPressed: () {
}, Navigator.of(context).pop();
), },
TextButton( ),
child: Text('Proceed'), TextButton(
onPressed: () { child: Text('Proceed'),
Navigator.of(context).pop(); // Dismiss the dialog onPressed: () {
// Navigate to the TPIN set screen Navigator.of(context).pop(); // Dismiss the dialog
Navigator.of(context).push( // Navigate to the TPIN set screen
MaterialPageRoute( Navigator.of(context).push(
builder: (context) => TpinSetScreen(), MaterialPageRoute(
), builder: (context) => TpinSetScreen(),
); ),
}, );
), },
], ),
); ],
}, );
); },
} else { );
// Case 2: TPIN is set } else {
Navigator.of(context).push( // Case 2: TPIN is set
MaterialPageRoute( Navigator.of(context).push(
builder: (context) => ChangeTpinScreen(mobileNumber: widget.mobileNumber), MaterialPageRoute(
), builder: (context) =>
); ChangeTpinScreen(mobileNumber: widget.mobileNumber),
} ),
}, );
), }
},
),
// ListTile( // ListTile(
// leading: const Icon(Icons.password), // leading: const Icon(Icons.password),
// title: const Text("Change Login MPIN"), // title: const Text("Change Login MPIN"),

View File

@@ -1,125 +1,125 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/widgets/pin_input_field.dart'; import 'package:kmobile/widgets/pin_input_field.dart';
import '../../../api/services/change_password_service.dart'; import '../../../api/services/change_password_service.dart';
class ChangeTpinOtpScreen extends StatefulWidget { class ChangeTpinOtpScreen extends StatefulWidget {
final String oldTpin; final String oldTpin;
final String newTpin; final String newTpin;
final String mobileNumber; final String mobileNumber;
const ChangeTpinOtpScreen({ const ChangeTpinOtpScreen({
super.key, super.key,
required this.oldTpin, required this.oldTpin,
required this.newTpin, required this.newTpin,
required this.mobileNumber, required this.mobileNumber,
}); });
@override @override
State<ChangeTpinOtpScreen> createState() => _ChangeTpinOtpScreenState(); State<ChangeTpinOtpScreen> createState() => _ChangeTpinOtpScreenState();
} }
class _ChangeTpinOtpScreenState extends State<ChangeTpinOtpScreen> { class _ChangeTpinOtpScreenState extends State<ChangeTpinOtpScreen> {
final _otpController = TextEditingController(); final _otpController = TextEditingController();
final ChangePasswordService _changePasswordService = final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>(); getIt<ChangePasswordService>();
bool _isLoading = false; bool _isLoading = false;
void _handleVerifyOtp() async { void _handleVerifyOtp() async {
if (_otpController.text.length != 6) { if (_otpController.text.length != 6) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a valid 6-digit OTP')), const SnackBar(content: Text('Please enter a valid 6-digit OTP')),
); );
return; return;
} }
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
try { try {
// 1. Validate the OTP first. // 1. Validate the OTP first.
await _changePasswordService.validateOtp( await _changePasswordService.validateOtp(
otp: _otpController.text, otp: _otpController.text,
mobileNumber: widget.mobileNumber, mobileNumber: widget.mobileNumber,
); );
// 2. If OTP is valid, then call validateChangeTpin. // 2. If OTP is valid, then call validateChangeTpin.
await _changePasswordService.validateChangeTpin( await _changePasswordService.validateChangeTpin(
oldTpin: widget.oldTpin, oldTpin: widget.oldTpin,
newTpin: widget.newTpin, newTpin: widget.newTpin,
); );
// 3. Show success message. // 3. Show success message.
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text('TPIN changed successfully!'), content: Text('TPIN changed successfully!'),
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
// 4. Navigate back to the profile screen or home. // 4. Navigate back to the profile screen or home.
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('An error occurred: $e'), content: Text('An error occurred: $e'),
backgroundColor: Colors.red, backgroundColor: Colors.red,
), ),
); );
} }
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
} }
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Verify OTP'), title: const Text('Verify OTP'),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
const SizedBox(height: 24), const SizedBox(height: 24),
const Text( const Text(
'Enter the OTP sent to your registered mobile number.', 'Enter the OTP sent to your registered mobile number.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
PinInputField( PinInputField(
controller: _otpController, controller: _otpController,
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isLoading ? null : _handleVerifyOtp, onPressed: _isLoading ? null : _handleVerifyOtp,
child: _isLoading child: _isLoading
? const SizedBox( ? const SizedBox(
height: 24, height: 24,
width: 24, width: 24,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2.5, strokeWidth: 2.5,
), ),
) )
: const Text('Verify & Change TPIN'), : const Text('Verify & Change TPIN'),
), ),
), ),
], ],
), ),
), ),
); );
} }
} }

View File

@@ -36,7 +36,8 @@ class _ChangeTpinScreenState extends State<ChangeTpinScreen> {
}); });
try { try {
// 1. Get OTP for TPIN change. // 1. Get OTP for TPIN change.
await _changePasswordService.getOtpTpin(mobileNumber: widget.mobileNumber); await _changePasswordService.getOtpTpin(
mobileNumber: widget.mobileNumber);
// 2. Navigate to the OTP screen on success. // 2. Navigate to the OTP screen on success.
if (mounted) { if (mounted) {
@@ -143,4 +144,4 @@ class _ChangeTpinScreenState extends State<ChangeTpinScreen> {
), ),
); );
} }
} }

View File

@@ -31,9 +31,9 @@ class QuickPayOutsideBankScreen extends StatefulWidget {
class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> { class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>(); final _limitService = getIt<LimitService>();
Limit? _limit; Limit? _limit;
bool _isLoadingLimit = true; bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
// Controllers // Controllers
final accountNumberController = TextEditingController(); final accountNumberController = TextEditingController();
final confirmAccountNumberController = TextEditingController(); final confirmAccountNumberController = TextEditingController();
@@ -56,7 +56,7 @@ final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '₹');
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit(); _loadLimit();
_ifscFocusNode.addListener(() { _ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) { if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC(); _validateIFSC();
@@ -70,47 +70,48 @@ final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '₹');
amountController.addListener(_checkAmountLimit); amountController.addListener(_checkAmountLimit);
} }
Future<void> _loadLimit() async { Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() { setState(() {
_limit = limitData; _isLoadingLimit = true;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
}); });
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
} }
}
// Add this method to check the amount against the limit // Add this method to check the amount against the limit
void _checkAmountLimit() { void _checkAmountLimit() {
if (_limit == null) return; if (_limit == null) return;
final amount = double.tryParse(amountController.text) ?? 0; final amount = double.tryParse(amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit; final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit; final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) { if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'), content: Text(
backgroundColor: Colors.red, 'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
), backgroundColor: Colors.red,
); ),
} );
}
if (_isAmountOverLimit != isOverLimit) { if (_isAmountOverLimit != isOverLimit) {
setState(() { setState(() {
_isAmountOverLimit = isOverLimit; _isAmountOverLimit = isOverLimit;
}); });
} }
} }
void _validateIFSC() async { void _validateIFSC() async {
final ifsc = ifscController.text.trim().toUpperCase(); final ifsc = ifscController.text.trim().toUpperCase();
@@ -769,89 +770,92 @@ Future<void> _loadLimit() async {
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [ children: [
Expanded( Row(
child: TextFormField( children: [
controller: phoneController, Expanded(
keyboardType: TextInputType.phone, child: TextFormField(
decoration: InputDecoration( controller: phoneController,
labelText: AppLocalizations.of(context).phone, keyboardType: TextInputType.phone,
prefixIcon: const Icon(Icons.phone), decoration: InputDecoration(
border: const OutlineInputBorder(), labelText: AppLocalizations.of(context).phone,
isDense: true, prefixIcon: const Icon(Icons.phone),
filled: true, border: const OutlineInputBorder(),
fillColor: Theme.of(context).scaffoldBackgroundColor, isDense: true,
enabledBorder: OutlineInputBorder( filled: true,
borderSide: BorderSide( fillColor:
color: Theme.of(context).colorScheme.outline), Theme.of(context).scaffoldBackgroundColor,
), enabledBorder: OutlineInputBorder(
focusedBorder: OutlineInputBorder( borderSide: BorderSide(
borderSide: BorderSide( color: Theme.of(context).colorScheme.outline),
color: Theme.of(context).colorScheme.primary, ),
width: 2), focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null || value.isEmpty
? AppLocalizations.of(context).phoneRequired
: null,
), ),
), ),
textInputAction: TextInputAction.next, const SizedBox(width: 10),
validator: (value) => value == null || value.isEmpty Expanded(
? AppLocalizations.of(context).phoneRequired child: TextFormField(
: null, decoration: InputDecoration(
), labelText: AppLocalizations.of(context).amount,
), border: const OutlineInputBorder(),
const SizedBox(width: 10), isDense: true,
Expanded( filled: true,
child: TextFormField( fillColor:
decoration: InputDecoration( Theme.of(context).scaffoldBackgroundColor,
labelText: AppLocalizations.of(context).amount, enabledBorder: OutlineInputBorder(
border: const OutlineInputBorder(), borderSide: BorderSide(
isDense: true, color: Theme.of(context).colorScheme.outline),
filled: true, ),
fillColor: Theme.of(context).scaffoldBackgroundColor, focusedBorder: OutlineInputBorder(
enabledBorder: OutlineInputBorder( borderSide: BorderSide(
borderSide: BorderSide( color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.outline), width: 2),
), ),
focusedBorder: OutlineInputBorder( ),
borderSide: BorderSide( controller: amountController,
color: Theme.of(context).colorScheme.primary, keyboardType: TextInputType.number,
width: 2), textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context)
.amountRequired;
}
final amount = double.tryParse(value);
if (amount == null || amount <= 0) {
return AppLocalizations.of(context).validAmount;
}
return null;
},
), ),
), ),
controller: amountController, ],
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).amountRequired;
}
final amount = double.tryParse(value);
if (amount == null || amount <= 0) {
return AppLocalizations.of(context).validAmount;
}
return null;
},
),
), ),
], ],
),
],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (_isLoadingLimit) if (_isLoadingLimit)
const Padding( const Padding(
padding: EdgeInsets.only(left: 8.0), padding: EdgeInsets.only(left: 8.0),
child: Text('Fetching daily limit...'), child: Text('Fetching daily limit...'),
), ),
if (!_isLoadingLimit && _limit != null) if (!_isLoadingLimit && _limit != null)
Padding( Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}', 'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Row( Row(
children: [ children: [
@@ -864,31 +868,34 @@ if (!_isLoadingLimit && _limit != null)
], ],
), ),
const SizedBox(height: 45), const SizedBox(height: 45),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SwipeButton.expand( child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward, thumb: Icon(Icons.arrow_forward,
color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor), color: _isAmountOverLimit
activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 : ? Colors.grey
Theme.of(context).colorScheme.primary, : Theme.of(context).dialogBackgroundColor),
activeTrackColor: _isAmountOverLimit activeThumbColor: _isAmountOverLimit
? Colors.grey.shade300 ? Colors.grey.shade700
: Theme.of(context).colorScheme.secondary.withAlpha(100), : Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(30), activeTrackColor: _isAmountOverLimit
height: 56, ? Colors.grey.shade300
onSwipe: () { : Theme.of(context).colorScheme.secondary.withAlpha(100),
if (_isAmountOverLimit) { borderRadius: BorderRadius.circular(30),
return; // Do nothing if amount is over the limit height: 56,
} onSwipe: () {
_onProceedToPay(); if (_isAmountOverLimit) {
}, return; // Do nothing if amount is over the limit
child: Text( }
AppLocalizations.of(context).swipeToPay, _onProceedToPay();
style: const TextStyle( },
fontSize: 16, fontWeight: FontWeight.bold), child: Text(
), AppLocalizations.of(context).swipeToPay,
), style: const TextStyle(
), fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
], ],
), ),
), ),

View File

@@ -22,9 +22,9 @@ class QuickPayWithinBankScreen extends StatefulWidget {
class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> { class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>(); final _limitService = getIt<LimitService>();
Limit? _limit; Limit? _limit;
bool _isLoadingLimit = true; bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final TextEditingController accountNumberController = TextEditingController(); final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController = final TextEditingController confirmAccountNumberController =
TextEditingController(); TextEditingController();
@@ -46,47 +46,48 @@ final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '₹');
amountController.addListener(_checkAmountLimit); amountController.addListener(_checkAmountLimit);
} }
Future<void> _loadLimit() async { Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() { setState(() {
_limit = limitData; _isLoadingLimit = true;
_isLoadingLimit = false;
}); });
} catch (e) { try {
// Handle error if needed final limitData = await _limitService.getLimit();
setState(() { setState(() {
_isLoadingLimit = false; _limit = limitData;
}); _isLoadingLimit = false;
} });
} } catch (e) {
// Handle error if needed
void _checkAmountLimit() { setState(() {
if (_limit == null) return; _isLoadingLimit = false;
});
final amount = double.tryParse(amountController.text) ?? 0; }
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
} }
// Update state only if it changes to avoid unnecessary rebuilds void _checkAmountLimit() {
if (_isAmountOverLimit != isOverLimit) { if (_limit == null) return;
setState(() {
_isAmountOverLimit = isOverLimit; final amount = double.tryParse(amountController.text) ?? 0;
}); final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
// Update state only if it changes to avoid unnecessary rebuilds
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
} }
}
void _resetBeneficiaryValidation() { void _resetBeneficiaryValidation() {
if (_isBeneficiaryValidated || if (_isBeneficiaryValidated ||
@@ -153,306 +154,314 @@ void _checkAmountLimit() {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
TextFormField( TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).debitAccountNumber, labelText: AppLocalizations.of(context).debitAccountNumber,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
),
readOnly: true,
controller: TextEditingController(text: widget.debitAccount),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
enabled: false,
), ),
readOnly: true, const SizedBox(height: 20),
controller: TextEditingController(text: widget.debitAccount), TextFormField(
keyboardType: TextInputType.number, decoration: InputDecoration(
textInputAction: TextInputAction.next, labelText: AppLocalizations.of(context).accountNumber,
enabled: false, border: const OutlineInputBorder(),
), isDense: true,
const SizedBox(height: 20), filled: true,
TextFormField( fillColor: Theme.of(context).scaffoldBackgroundColor,
decoration: InputDecoration( enabledBorder: OutlineInputBorder(
labelText: AppLocalizations.of(context).accountNumber, borderSide: BorderSide(
border: const OutlineInputBorder(), color: Theme.of(context).colorScheme.outline),
isDense: true, ),
filled: true, focusedBorder: OutlineInputBorder(
fillColor: Theme.of(context).scaffoldBackgroundColor, borderSide: BorderSide(
enabledBorder: OutlineInputBorder( color: Theme.of(context).colorScheme.primary,
borderSide: BorderSide( width: 2),
color: Theme.of(context).colorScheme.outline), ),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
controller: accountNumberController,
keyboardType: TextInputType.number,
obscureText: true,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).accountNumberRequired;
} else if (value.length != 11) {
return AppLocalizations.of(context).validAccountNumber;
}
return null;
},
), ),
controller: accountNumberController, const SizedBox(height: 25),
keyboardType: TextInputType.number, TextFormField(
obscureText: true, controller: confirmAccountNumberController,
textInputAction: TextInputAction.next, decoration: InputDecoration(
validator: (value) { labelText:
if (value == null || value.isEmpty) { AppLocalizations.of(context).confirmAccountNumber,
return AppLocalizations.of(context).accountNumberRequired; // prefixIcon: Icon(Icons.person),
} else if (value.length != 11) { border: const OutlineInputBorder(),
return AppLocalizations.of(context).validAccountNumber; isDense: true,
} filled: true,
return null; fillColor: Theme.of(context).scaffoldBackgroundColor,
}, enabledBorder: OutlineInputBorder(
), borderSide: BorderSide(
const SizedBox(height: 25), color: Theme.of(context).colorScheme.outline),
TextFormField( ),
controller: confirmAccountNumberController, focusedBorder: OutlineInputBorder(
decoration: InputDecoration( borderSide: BorderSide(
labelText: AppLocalizations.of(context).confirmAccountNumber, color: Theme.of(context).colorScheme.primary,
// prefixIcon: Icon(Icons.person), width: 2),
border: const OutlineInputBorder(), ),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).reenterAccountNumber;
}
if (value != accountNumberController.text) {
return AppLocalizations.of(context).accountMismatch;
}
return null;
},
), ),
keyboardType: TextInputType.number, if (!_isBeneficiaryValidated)
textInputAction: TextInputAction.next, Padding(
validator: (value) { padding: const EdgeInsets.only(top: 12.0),
if (value == null || value.isEmpty) { child: SizedBox(
return AppLocalizations.of(context).reenterAccountNumber; width: double.infinity,
} child: ElevatedButton(
if (value != accountNumberController.text) { onPressed: _isValidating
return AppLocalizations.of(context).accountMismatch; ? null
} : () {
return null; if (accountNumberController.text.length == 11 &&
}, confirmAccountNumberController.text ==
), accountNumberController.text) {
if (!_isBeneficiaryValidated) _validateBeneficiary();
Padding( } else {
padding: const EdgeInsets.only(top: 12.0), setState(() {
child: SizedBox( _validationError =
width: double.infinity, AppLocalizations.of(context)
child: ElevatedButton( .accountMismatch;
onPressed: _isValidating });
? null }
: () { },
if (accountNumberController.text.length == 11 && child: _isValidating
confirmAccountNumberController.text == ? const SizedBox(
accountNumberController.text) { width: 20,
_validateBeneficiary(); height: 20,
} else { child:
setState(() { CircularProgressIndicator(strokeWidth: 2),
_validationError = )
AppLocalizations.of(context) : Text(AppLocalizations.of(context)
.accountMismatch; .validateBeneficiary),
}); ),
} ),
}, ),
child: _isValidating if (_beneficiaryName != null && _isBeneficiaryValidated)
? const SizedBox( Padding(
width: 20, padding: const EdgeInsets.only(top: 12.0),
height: 20, child: Row(
child: CircularProgressIndicator(strokeWidth: 2), children: [
) const Icon(Icons.check_circle, color: Colors.green),
: Text( const SizedBox(width: 8),
AppLocalizations.of(context).validateBeneficiary), Text(
'${AppLocalizations.of(context).beneficiaryName}: $_beneficiaryName',
style: const TextStyle(
color: Colors.green, fontWeight: FontWeight.bold),
),
],
),
),
if (_validationError != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_validationError!,
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 24),
DropdownButtonFormField<String>(
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).beneficiaryAccountType,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
value: _selectedAccountType,
items: [
DropdownMenuItem(
value: 'SB',
child: Text(AppLocalizations.of(context).savings),
),
DropdownMenuItem(
value: 'LN',
child: Text(AppLocalizations.of(context).loan),
),
],
onChanged: (value) {
setState(() {
_selectedAccountType = value;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).selectAccountType;
}
return null;
},
),
const SizedBox(height: 25),
TextFormField(
controller: remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
), ),
), ),
), ),
if (_beneficiaryName != null && _isBeneficiaryValidated) const SizedBox(height: 25),
Padding( TextFormField(
padding: const EdgeInsets.only(top: 12.0), decoration: InputDecoration(
child: Row( labelText: AppLocalizations.of(context).amount,
children: [ border: const OutlineInputBorder(),
const Icon(Icons.check_circle, color: Colors.green), isDense: true,
const SizedBox(width: 8), filled: true,
Text( fillColor: Theme.of(context).scaffoldBackgroundColor,
'${AppLocalizations.of(context).beneficiaryName}: $_beneficiaryName', enabledBorder: OutlineInputBorder(
style: const TextStyle( borderSide: BorderSide(
color: Colors.green, fontWeight: FontWeight.bold), color: Theme.of(context).colorScheme.outline),
), ),
], focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
), ),
controller: amountController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).amountRequired;
}
final amount = double.tryParse(value);
if (amount == null || amount <= 0) {
return AppLocalizations.of(context).validAmount;
}
return null;
},
), ),
if (_validationError != null) const SizedBox(height: 8),
Padding( if (_isLoadingLimit) const Text('Fetching daily limit...'),
padding: const EdgeInsets.only(top: 8.0), if (!_isLoadingLimit && _limit != null)
child: Text( Text(
_validationError!, 'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: const TextStyle(color: Colors.red), style: Theme.of(context).textTheme.bodySmall,
), ),
), const SizedBox(height: 45),
const SizedBox(height: 24), Align(
DropdownButtonFormField<String>( alignment: Alignment.center,
decoration: InputDecoration( child: SwipeButton.expand(
labelText: AppLocalizations.of( thumb: Icon(Icons.arrow_forward,
context, color: _isAmountOverLimit
).beneficiaryAccountType, ? Colors.grey
border: const OutlineInputBorder(), : Theme.of(context).dialogBackgroundColor),
isDense: true, activeThumbColor: _isAmountOverLimit
filled: true, ? Colors.grey.shade700
fillColor: Theme.of(context).scaffoldBackgroundColor, : Theme.of(context).colorScheme.primary,
enabledBorder: OutlineInputBorder( activeTrackColor: _isAmountOverLimit
borderSide: BorderSide( ? Colors.grey.shade300
color: Theme.of(context).colorScheme.outline), : Theme.of(
), context,
focusedBorder: OutlineInputBorder( ).colorScheme.secondary.withAlpha(100),
borderSide: BorderSide( borderRadius: BorderRadius.circular(30),
color: Theme.of(context).colorScheme.primary, width: 2), height: 56,
), child: Text(
), AppLocalizations.of(context).swipeToPay,
value: _selectedAccountType, style: const TextStyle(fontSize: 16),
items: [ ),
DropdownMenuItem( onSwipe: () {
value: 'SB', if (_isAmountOverLimit) {
child: Text(AppLocalizations.of(context).savings), return; // Do nothing if amount is over limit
), }
DropdownMenuItem( if (_formKey.currentState!.validate()) {
value: 'LN', if (!_isBeneficiaryValidated) {
child: Text(AppLocalizations.of(context).loan), setState(() {
), _validationError = AppLocalizations.of(context)
], .validateBeneficiaryproceeding;
onChanged: (value) { });
setState(() { return;
_selectedAccountType = value; }
}); // Perform payment logic
}, Navigator.push(
validator: (value) { context,
if (value == null || value.isEmpty) { MaterialPageRoute(
return AppLocalizations.of(context).selectAccountType; builder: (context) => TransactionPinScreen(
} onPinCompleted: (pinScreenContext, tpin) async {
return null; final transfer = Transfer(
}, fromAccount: widget.debitAccount,
), toAccount: accountNumberController.text,
const SizedBox(height: 25), toAccountType: _selectedAccountType!,
TextFormField( amount: amountController.text,
controller: remarksController, tpin: tpin,
decoration: InputDecoration( remarks: remarksController.text,
labelText: AppLocalizations.of(context).remarks, );
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
),
const SizedBox(height: 25),
TextFormField(
decoration: InputDecoration(
labelText: AppLocalizations.of(context).amount,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
controller: amountController,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).amountRequired;
}
final amount = double.tryParse(value);
if (amount == null || amount <= 0) {
return AppLocalizations.of(context).validAmount;
}
return null;
},
),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 45),
Align(
alignment: Alignment.center,
child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward,
color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
Theme.of(context).colorScheme.primary,
activeTrackColor: _isAmountOverLimit
? Colors.grey.shade300
: Theme.of(
context,
).colorScheme.secondary.withAlpha(100),
borderRadius: BorderRadius.circular(30),
height: 56,
child: Text(
AppLocalizations.of(context).swipeToPay,
style: const TextStyle(fontSize: 16),
),
onSwipe: () {
if (_isAmountOverLimit) {
return; // Do nothing if amount is over limit
}
if (_formKey.currentState!.validate()) {
if (!_isBeneficiaryValidated) {
setState(() {
_validationError = AppLocalizations.of(context)
.validateBeneficiaryproceeding;
});
return;
}
// Perform payment logic
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TransactionPinScreen(
onPinCompleted: (pinScreenContext, tpin) async {
final transfer = Transfer(
fromAccount: widget.debitAccount,
toAccount: accountNumberController.text,
toAccountType: _selectedAccountType!,
amount: amountController.text,
tpin: tpin,
remarks: remarksController.text,
);
final paymentService = getIt<PaymentService>(); final paymentService = getIt<PaymentService>();
final paymentResponseFuture = paymentService final paymentResponseFuture = paymentService
.processQuickPayWithinBank(transfer); .processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement( Navigator.of(pinScreenContext).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => PaymentAnimationScreen( builder: (_) => PaymentAnimationScreen(
paymentResponse: paymentResponseFuture), paymentResponse: paymentResponseFuture),
), ),
); );
}, },
), ),
), ),
); );
} }
}, },
), ),
), ),
], ],
), ),
), ),
), ),
), ),

View File

@@ -15,13 +15,13 @@ void main() async {
]); ]);
// Check for device compromise // Check for device compromise
// final compromisedMessage = await SecurityService.deviceCompromisedMessage; final compromisedMessage = await SecurityService.deviceCompromisedMessage;
// if (compromisedMessage != null) { if (compromisedMessage != null) {
// runApp(MaterialApp( runApp(MaterialApp(
// home: SecurityErrorScreen(message: compromisedMessage), home: SecurityErrorScreen(message: compromisedMessage),
// )); ));
// return; return;
// } }
await setupDependencies(); await setupDependencies();
runApp(const KMobile()); runApp(const KMobile());
} }

View File

@@ -55,4 +55,4 @@ class PinInputField extends StatelessWidget {
}, },
); );
} }
} }

View File

@@ -1,21 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TncDialog extends StatefulWidget { class TncDialog extends StatefulWidget {
final Future<void> Function() onProceed; final Future<void> Function() onProceed;
const TncDialog({Key? key, required this.onProceed}) : super(key: key); const TncDialog({Key? key, required this.onProceed}) : super(key: key);
@override @override
_TncDialogState createState() => _TncDialogState(); _TncDialogState createState() => _TncDialogState();
} }
class _TncDialogState extends State<TncDialog> { class _TncDialogState extends State<TncDialog> {
bool _isAgreed = false; bool _isAgreed = false;
bool _isLoading = false; bool _isLoading = false;
// --- NEW: ScrollController for the TNC text --- // --- NEW: ScrollController for the TNC text ---
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
final String _termsAndConditionsText = """ final String _termsAndConditionsText = """
Effective Date: November 10, 2025 Effective Date: November 10, 2025
These Terms and Conditions ("Terms") govern your access to and use of The Bank mobile banking application (the "App") and the services These Terms and Conditions ("Terms") govern your access to and use of The Bank mobile banking application (the "App") and the services
@@ -111,101 +111,101 @@
access to or use of the App and Services. access to or use of the App and Services.
"""; """;
void _handleProceed() async { void _handleProceed() async {
if (_isLoading) return; if (_isLoading) return;
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
await widget.onProceed(); await widget.onProceed();
if (mounted) { if (mounted) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
} }
} }
@override @override
void dispose() { void dispose() {
_scrollController.dispose(); // --- NEW: Dispose the ScrollController --- _scrollController.dispose(); // --- NEW: Dispose the ScrollController ---
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
return AlertDialog( return AlertDialog(
title: const Text('Terms and Conditions'), title: const Text('Terms and Conditions'),
content: SizedBox( content: SizedBox(
height: screenSize.height * 0.5, // 50% of screen height height: screenSize.height * 0.5, // 50% of screen height
width: screenSize.width * 0.9, // 90% of screen width width: screenSize.width * 0.9, // 90% of screen width
// --- MODIFIED: Use a Column to separate scrollable text from fixed checkbox --- // --- MODIFIED: Use a Column to separate scrollable text from fixed checkbox ---
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// --- NEW: Expanded Scrollbar for the TNC text --- // --- NEW: Expanded Scrollbar for the TNC text ---
Expanded( Expanded(
child: Scrollbar( child: Scrollbar(
controller: _scrollController, controller: _scrollController,
thumbVisibility: true, // Always show the scrollbar thumb thumbVisibility: true, // Always show the scrollbar thumb
// To place the scrollbar on the left, you might need to wrap // To place the scrollbar on the left, you might need to wrap
// this in a Directionality widget or use a custom scrollbar. // this in a Directionality widget or use a custom scrollbar.
// For now, it will appear on the right as is standard. // For now, it will appear on the right as is standard.
child: SingleChildScrollView( child: SingleChildScrollView(
controller: _scrollController, controller: _scrollController,
child: _isLoading child: _isLoading
? const Center( ? const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
) )
: Text(_termsAndConditionsText), : Text(_termsAndConditionsText),
), ),
), ),
), ),
const SizedBox(height: 16), // Space between text and checkbox const SizedBox(height: 16), // Space between text and checkbox
// --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView --- // --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView ---
Row( Row(
children: [ children: [
Checkbox( Checkbox(
value: _isAgreed, value: _isAgreed,
onChanged: (bool? value) { onChanged: (bool? value) {
setState(() { setState(() {
_isAgreed = value ?? false; _isAgreed = value ?? false;
}); });
}, },
), ),
const Flexible( const Flexible(
child: Text('I agree to the Terms and Conditions')), child: Text('I agree to the Terms and Conditions')),
], ],
), ),
], ],
), ),
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: _isLoading onPressed: _isLoading
? null ? null
: () { : () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text( content: Text(
'You must agree to the terms and conditions to proceed.'), 'You must agree to the terms and conditions to proceed.'),
behavior: SnackBarBehavior.floating, behavior: SnackBarBehavior.floating,
), ),
); );
}, },
child: const Text('Disagree'), child: const Text('Disagree'),
), ),
ElevatedButton( ElevatedButton(
onPressed: _isAgreed && !_isLoading ? _handleProceed : null, onPressed: _isAgreed && !_isLoading ? _handleProceed : null,
child: const Text('Proceed'), child: const Text('Proceed'),
), ),
], ],
); );
} }
} }