10 Commits

Author SHA1 Message Date
5823eaede8 After Login Button 2025-12-08 17:45:12 +05:30
1ae3e7c0a6 Limit Added to SMS enabled device 2025-11-04 15:29:56 +05:30
530e5c0493 Header Added 2025-10-28 18:04:14 +05:30
e1c1a58086 SMS Screen Modifiaction 2025-10-28 17:59:05 +05:30
dd3e94a69e SMS succesfully sent 2025-10-27 17:36:28 +05:30
2743f92283 Loger in Mobile App 2025-10-22 17:43:24 +05:30
72a9d5711a SMS Testing 2025-10-22 13:47:05 +05:30
1edb2804f1 SMS Send formatting 2025-10-22 13:19:33 +05:30
c9c52b39fa SMS Send without manual sim ard choosing option 2025-10-22 12:49:38 +05:30
7a0265ad8d Send SMS File created 2025-10-22 12:23:23 +05:30
112 changed files with 4191 additions and 8904 deletions

5
.gitignore vendored
View File

@@ -44,8 +44,3 @@ app.*.map.json
lib/l10n/app_localizations.dart lib/l10n/app_localizations.dart
lib/l10n/app_localizations_en.dart lib/l10n/app_localizations_en.dart
lib/l10n/app_localizations_hi.dart lib/l10n/app_localizations_hi.dart
# Keystore files
android/key.properties
android/*.jks
android/*.keystore

View File

@@ -22,19 +22,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
namespace "com.example.kmobile" namespace "com.example.kmobile"
compileSdk flutter.compileSdkVersion compileSdk flutter.compileSdkVersion
ndkVersion "27.0.12077973" ndkVersion "27.0.12077973"
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@@ -58,21 +51,15 @@ android {
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release // TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
} }
} }
} }
@@ -81,6 +68,4 @@ flutter {
source '../..' source '../..'
} }
dependencies { dependencies {}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

View File

@@ -3,7 +3,7 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/> <uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application <application
android:label="kmobile" android:label="kmobile"
android:name="${applicationName}" android:name="${applicationName}"
@@ -42,20 +42,6 @@
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -8,41 +8,6 @@ class AuthService {
final Dio _dio; final Dio _dio;
AuthService(this._dio); AuthService(this._dio);
Future<void> simVerify(String uuid, String cifNo) async {
try {
final response = await _dio.post('/api/sim-details-verify', data: {
'uuid': uuid,
'cifNo': cifNo,
});
if (response.statusCode == 200) {
final String message = response.data.toString().toUpperCase();
if (message.contains("VERIFIED")) {
return; // Success
} else {
throw AuthException(message); // Throw message received
}
} else {
throw AuthException('Verification Failed');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
if (e.response?.statusCode == 401) {
throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
}
throw NetworkException('Network error during verification');
} catch (e) {
throw UnexpectedException(
'Unexpected error during verification: ${e.toString()}');
}
}
Future<AuthToken> login(AuthCredentials credentials) async { Future<AuthToken> login(AuthCredentials credentials) async {
try { try {
final response = await _dio.post( final response = await _dio.post(
@@ -176,23 +141,4 @@ class AuthService {
} }
return; return;
} }
Future setTncflag() async {
try {
final response = await _dio.post(
'/api/auth/tnc',
data: {"flag": 'Y'},
);
if (response.statusCode != 200) {
throw AuthException('Failed to proceed with T&C');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
throw NetworkException('Network error during T&C Setup');
} catch (e) {
throw UnexpectedException('Unexpected error: ${e.toString()}');
}
}
} }

View File

@@ -1,136 +0,0 @@
import 'package:dio/dio.dart';
class Branch {
final String branch_code;
final String branch_name;
final String zone;
final String tehsil;
final String block;
final String block_code;
final String distt_name;
final String distt_code_slbc;
final String date_of_opening;
final String rbi_code_1;
final String rbi_code_2;
final String telephone_no;
final String type_of_branch;
final String rtgs_acct_no;
final String br_lattitude;
final String br_longitude;
final String pincode;
final String post_office;
Branch({
required this.branch_code,
required this.branch_name,
required this.zone,
required this.tehsil,
required this.block,
required this.block_code,
required this.distt_name,
required this.distt_code_slbc,
required this.date_of_opening,
required this.rbi_code_1,
required this.rbi_code_2,
required this.telephone_no,
required this.type_of_branch,
required this.rtgs_acct_no,
required this.br_lattitude,
required this.br_longitude,
required this.pincode,
required this.post_office,
});
factory Branch.fromJson(Map<String, dynamic> json) {
return Branch(
branch_code: json['branch_code'] ?? json['branch_code'] ?? '',
branch_name: json['branch_name'] ?? json['branch_name'] ?? '',
zone: json['zone'] ?? json['zone'] ?? '',
tehsil: json['tehsil'] ?? json['tehsil'] ?? '',
block: json['block'] ?? json['block'] ?? '',
block_code: json['block_code'] ?? json['block_code'] ?? '',
distt_name: json['distt_name'] ?? json['distt_name'] ?? '',
distt_code_slbc: json['distt_code_slbc'] ?? json['distt_code_slbc'] ?? '',
date_of_opening: json['date_of_opening'] ?? json['date_of_opening'] ?? '',
rbi_code_1: json['rbi_code_1'] ?? json['rbi_code_1'] ?? '',
rbi_code_2: json['rbi_code_2'] ?? json['rbi_code_2'] ?? '',
telephone_no: json['telephone_no'] ?? json['telephone_no'] ?? '',
type_of_branch: json['type_of_branch'] ?? json['type_of_branch'] ?? '',
rtgs_acct_no: json['rtgs_acct_no'] ?? json['rtgs_acct_no'] ?? '',
br_lattitude: json['br_lattitude'] ?? json['br_lattitude'] ?? '',
br_longitude: json['br_longitude'] ?? json['br_longitude'] ?? '',
pincode: json['pincode'] ?? json['pincode'] ?? '',
post_office: json['post_office'] ?? json['post_office'] ?? '',
);
}
static List<Branch> listFromJson(List<dynamic> jsonList) {
final beneficiaryList =
jsonList.map((beneficiary) => Branch.fromJson(beneficiary)).toList();
return beneficiaryList;
}
}
class Atm {
final String name;
Atm({required this.name});
factory Atm.fromJson(Map<String, dynamic> json) {
return Atm(
name: json['name'] ?? '', // Assuming the API returns a 'name' field
);
}
static List<Atm> listFromJson(List<dynamic> jsonList) {
return jsonList.map((atm) => Atm.fromJson(atm)).toList();
}
}
class BranchService {
final Dio _dio;
BranchService(this._dio);
Future<List<Branch>> fetchBranchList() async {
try {
final response = await _dio.get(
"/api/branch",
options: Options(
headers: {
"Content-Type": "application/json",
},
),
);
if (response.statusCode == 200) {
return Branch.listFromJson(response.data);
} else {
throw Exception("Failed to fetch beneficiaries");
}
} catch (e) {
return [];
}
}
Future<List<Atm>> fetchAtmList() async {
try {
final response = await _dio.get(
"/api/atm",
options: Options(
headers: {
"Content-Type": "application/json",
},
),
);
if (response.statusCode == 200) {
return Atm.listFromJson(response.data);
} else {
throw Exception("Failed to fetch ATM list: ${response.statusCode}");
}
} catch (e) {
// You might want to log the error here for debugging
print("Error fetching ATM list: $e");
return [];
}
}
}

View File

@@ -67,21 +67,4 @@ class ChangePasswordService {
} }
return response.toString(); return response.toString();
} }
Future validateChangeTpin({
required String oldTpin,
required String newTpin,
}) async {
final response = await _dio.post(
'/api/auth/change/tpin',
data: {
'oldTpin': oldTpin,
'newTpin': newTpin,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
} }

View File

@@ -13,8 +13,8 @@ class Limit {
factory Limit.fromJson(Map<String, dynamic> json) { factory Limit.fromJson(Map<String, dynamic> json) {
return Limit( return Limit(
dailyLimit: (json['dailyLimit'] as num).toDouble(), dailyLimit: json['dailyLimit']!,
usedLimit: (json['usedLimit'] as num).toDouble(), usedLimit: json['usedLimit']!,
); );
} }
} }
@@ -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 {
@@ -54,34 +54,4 @@ class LimitService {
throw Exception('Unexpected error: ${e.toString()}'); throw Exception('Unexpected error: ${e.toString()}');
} }
} }
Future getOtpTLimit({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "TLIMIT"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future validateOtp({
required String otp,
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber',
data: {
'otp': otp,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
} }

View File

@@ -1,102 +1,247 @@
import 'dart:io'; // // ignore_for_file: avoid_print
import 'package:flutter/material.dart'; // import 'dart:io';
import 'package:permission_handler/permission_handler.dart'; // import 'package:flutter/material.dart';
import 'package:send_message/send_message.dart' show sendSMS; // import 'package:send_message/send_message.dart' show sendSMS;
import 'package:simcards/sim_card.dart'; // import 'package:simcards/sim_card.dart';
import 'package:simcards/simcards.dart'; // import 'package:simcards/simcards.dart';
// This enum provides detailed status back to the UI layer. // import 'package:uuid/uuid.dart';
enum PermissionStatusResult { granted, denied, permanentlyDenied, restricted }
class SmsService { // class SmsService {
final Simcards _simcards = Simcards(); // final Simcards _simcards = Simcards();
/// Handles the requesting of SMS and Phone permissions. // Future<void> sendVerificationSms({
/// Returns a detailed status: granted, denied, or permanentlyDenied. // required BuildContext context,
Future<PermissionStatusResult> handleSmsPermission() async { // required String destinationNumber,
var smsStatus = await Permission.sms.status; // required String message,
var phoneStatus = await Permission.phone.status; // }) async {
// try {
// await _simcards.requestPermission();
// Check initial status // bool permissionGranted = await _simcards.hasPermission();
if (smsStatus.isGranted && phoneStatus.isGranted) { // if (!permissionGranted) {
return PermissionStatusResult.granted; // print("Permission denied." );
} // return;
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) { // }
return PermissionStatusResult.permanentlyDenied;
}
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
return PermissionStatusResult.restricted;
}
// Request permissions if not granted // List<SimCard> simCardList = await _simcards.getSimCards();
print("Requesting SMS and Phone permissions..."); // if (simCardList.isEmpty) {
await [Permission.phone, Permission.sms].request(); // print("No SIM detected." );
// return;
// }
// Re-check status after request // await _sendSms(destinationNumber, message, simCardList.first);
smsStatus = await Permission.sms.status;
phoneStatus = await Permission.phone.status;
if (smsStatus.isGranted && phoneStatus.isGranted) { // } catch (e) {
return PermissionStatusResult.granted; // print("Error in SMS process: $e");
} // }
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) { // }
return PermissionStatusResult.permanentlyDenied;
}
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
return PermissionStatusResult.restricted;
}
// If none of the above, it's denied
return PermissionStatusResult.denied;
}
/// Tries to send a single verification SMS. // Future<void> _sendSms(
/// This should only be called AFTER permissions have been granted. // String destinationNumber, String message, SimCard selectedSim) async {
Future<bool> sendVerificationSms({ // if (Platform.isAndroid) {
required BuildContext context, // try {
required String destinationNumber, // var uuid = const Uuid();
required String message, // String uniqueId = uuid.v4();
}) async {
try {
List<SimCard> simCardList = await _simcards.getSimCards();
if (simCardList.isEmpty) {
print("No SIM card detected.");
return false;
}
return await _sendSms(destinationNumber, message, simCardList.first);
} catch (e) {
print("An error occurred in the SMS process: $e");
return false;
}
}
/// Private function to perform the SMS sending action. // String smsMessage = uniqueId;
Future<bool> _sendSms( // String result = await sendSMS(
String destinationNumber, String message, SimCard selectedSim) async { // message: smsMessage,
if (Platform.isAndroid) { // recipients: [destinationNumber],
try { // sendDirect: false,
String smsMessage = message; // );
String result = await sendSMS( // print("SMS send result: $result. Sent via ${selectedSim.displayName} (Note: OS default SIM isused).");
message: smsMessage,
recipients: [destinationNumber],
sendDirect: true,
);
print("Background SMS send attempt result: $result");
if (result.toLowerCase().contains('sent')) { // } catch (e) {
print("Success: SMS appears to have been sent."); // print("Error sending SMS: $e");
return true; // }
} else { // } else {
print("Failure: SMS was not sent. Result: $result"); // print("SMS sending is only supported on Android.");
return false; // }
} // }
} catch (e) { // }
print("Error attempting to send SMS directly: $e");
return false; // import 'dart:io';
} // import 'package:flutter/material.dart';
} else { // import 'package:permission_handler/permission_handler.dart'; // Import permission_handler
print("SMS sending is only supported on Android."); // import 'package:send_message/send_message.dart' show sendSMS;
return false; // import 'package:simcards/sim_card.dart';
} // import 'package:simcards/simcards.dart';
}
} // class SmsService {
// final Simcards _simcards = Simcards();
// Future<bool> sendVerificationSms({
// required BuildContext context,
// required String destinationNumber,
// required String message,
// }) async {
// try {
// // --- NEW PERMISSION LOGIC ---
// // 1. Request both Phone and SMS permissions
// Map<Permission, PermissionStatus> statuses = await [
// Permission.phone,
// Permission.sms,
// ].request();
// // 2. Check if both permissions were granted
// if (statuses[Permission.phone]!.isGranted && statuses[Permission.sms]!.isGranted) {
// print("Phone and SMS permissions are granted.");
// } else {
// print("Permission was denied. Phone status: ${statuses[Permission.phone]}, SMS status: ${statuses[Permission.sms]}");
// // Optionally, you can open app settings to let the user grant it manually
// // openAppSettings();
// return false;
// }
// // --- END OF NEW PERMISSION LOGIC ---
// // Check for SIM card (this part remains the same)
// List<SimCard> simCardList = await _simcards.getSimCards();
// if (simCardList.isEmpty) {
// print("No SIM card detected.");
// return false;
// }
// // Try sending the SMS and return the result
// return await _sendSms(destinationNumber, message, simCardList.first);
// } catch (e) {
// print("An error occurred in the SMS process: $e");
// return false;
// }
// }
// Future<bool> _sendSms(
// String destinationNumber, String message, SimCard selectedSim) async {
// if (Platform.isAndroid) {
// try {
// String smsMessage = message;
// String result = await sendSMS(
// message: smsMessage,
// recipients: [destinationNumber],
// sendDirect: true, // Still attempting direct send as requested
// );
// print("Background SMS send attempt result: $result");
// if (result.toLowerCase().contains('sent')) {
// print("Success: SMS appears to have been sent.");
// return true;
// } else {
// print("Failure: SMS was not sent. Result: $result");
// return false;
// }
// } catch (e) {
// print("Error attempting to send SMS directly: $e");
// return false;
// }
// } else {
// print("SMS sending is only supported on Android.");
// return false;
// }
// }
// }
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:send_message/send_message.dart' show sendSMS;
import 'package:simcards/sim_card.dart';
import 'package:simcards/simcards.dart';
// This enum provides detailed status back to the UI layer.
enum PermissionStatusResult { granted, denied, permanentlyDenied, restricted }
class SmsService {
final Simcards _simcards = Simcards();
/// Handles the requesting of SMS and Phone permissions.
/// Returns a detailed status: granted, denied, or permanentlyDenied.
Future<PermissionStatusResult> handleSmsPermission() async {
var smsStatus = await Permission.sms.status;
var phoneStatus = await Permission.phone.status;
// Check initial status
if (smsStatus.isGranted && phoneStatus.isGranted) {
return PermissionStatusResult.granted;
}
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) {
return PermissionStatusResult.permanentlyDenied;
}
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
return PermissionStatusResult.restricted;
}
// Request permissions if not granted
print("Requesting SMS and Phone permissions...");
await [Permission.phone, Permission.sms].request();
// Re-check status after request
smsStatus = await Permission.sms.status;
phoneStatus = await Permission.phone.status;
if (smsStatus.isGranted && phoneStatus.isGranted) {
return PermissionStatusResult.granted;
}
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) {
return PermissionStatusResult.permanentlyDenied;
}
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
return PermissionStatusResult.restricted;
}
// If none of the above, it's denied
return PermissionStatusResult.denied;
}
/// Tries to send a single verification SMS.
/// This should only be called AFTER permissions have been granted.
Future<bool> sendVerificationSms({
required BuildContext context,
required String destinationNumber,
required String message,
}) async {
try {
List<SimCard> simCardList = await _simcards.getSimCards();
if (simCardList.isEmpty) {
print("No SIM card detected.");
return false;
}
return await _sendSms(destinationNumber, message, simCardList.first);
} catch (e) {
print("An error occurred in the SMS process: $e");
return false;
}
}
/// Private function to perform the SMS sending action.
Future<bool> _sendSms(
String destinationNumber, String message, SimCard selectedSim) async {
if (Platform.isAndroid) {
try {
String smsMessage = message;
String result = await sendSMS(
message: smsMessage,
recipients: [destinationNumber],
sendDirect: true,
);
print("Background SMS send attempt result: $result");
if (result.toLowerCase().contains('sent')) {
print("Success: SMS appears to have been sent.");
return true;
} else {
print("Failure: SMS was not sent. Result: $result");
return false;
}
} catch (e) {
print("Error attempting to send SMS directly: $e");
return false;
}
} else {
print("SMS sending is only supported on Android.");
return false;
}
}
}

View File

@@ -10,7 +10,7 @@ class UserService {
Future<List<User>> getUserDetails() async { Future<List<User>> getUserDetails() async {
try { try {
final response = await _dio.get('/api/customer'); final response = await _dio.get('/api/customer/details');
if (response.statusCode == 200) { if (response.statusCode == 200) {
log('Response: ${response.data}'); log('Response: ${response.data}');
return (response.data as List) return (response.data as List)

View File

@@ -4,6 +4,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart'; import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_state.dart'; import 'package:kmobile/features/auth/controllers/theme_mode_state.dart';
import 'package:kmobile/features/auth/screens/login_screen.dart';
//import 'package:kmobile/features/auth/screens/sms_verification_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart'; import './l10n/app_localizations.dart';
@@ -12,9 +14,7 @@ import 'package:kmobile/features/auth/controllers/theme_state.dart';
import 'config/routes.dart'; import 'config/routes.dart';
import 'di/injection.dart'; import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart'; import 'features/auth/controllers/auth_cubit.dart';
import 'features/accounts/screens/account_statement_screen.dart'; import 'features/card/screens/card_management_screen.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart'; import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart'; import 'features/dashboard/screens/dashboard_screen.dart';
import 'features/auth/screens/mpin_screen.dart'; import 'features/auth/screens/mpin_screen.dart';
@@ -125,11 +125,7 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
theme: themeState.getLightThemeData(), theme: themeState.getLightThemeData(),
darkTheme: themeState.getDarkThemeData(), darkTheme: themeState.getDarkThemeData(),
themeMode: context.watch<ThemeModeCubit>().state.mode, themeMode: context.watch<ThemeModeCubit>().state.mode,
navigatorObservers: [
getIt<RouteObserver<ModalRoute<void>>>(),
],
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash,
home: const AuthGate(), home: const AuthGate(),
); );
}, },
@@ -202,7 +198,11 @@ class _AuthGateState extends State<AuthGate> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_checking) { if (_checking) {
return const LoginScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
if (_isLoggedIn) { if (_isLoggedIn) {
if (_hasMPin) { if (_hasMPin) {
@@ -211,7 +211,11 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(), future: _tryBiometric(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const LoginScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
if (snapshot.data == true) { if (snapshot.data == true) {
return const NavigationScaffold(); return const NavigationScaffold();
@@ -313,21 +317,7 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardScreen(), const DashboardScreen(),
BlocBuilder<AuthCubit, AuthState>( const CardManagementScreen(),
builder: (context, state) {
if (state is Authenticated) {
if (state.users.isNotEmpty) {
return AccountStatementScreen(
users: state.users,
selectedIndex: 0,
);
} else {
return const Center(child: Text("No accounts found."));
}
}
return const Center(child: CircularProgressIndicator());
},
),
const ServiceScreen(), const ServiceScreen(),
]; ];
@@ -386,8 +376,8 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
label: AppLocalizations.of(context).home, label: AppLocalizations.of(context).home,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.swap_vert_sharp), icon: const Icon(Icons.credit_card),
label: AppLocalizations.of(context).transactions, label: AppLocalizations.of(context).card,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services), icon: const Icon(Icons.miscellaneous_services),
@@ -433,7 +423,11 @@ class BiometricPromptScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context)); Future.microtask(() => _showDialog(context));
return const SizedBox.shrink(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
Future<void> _showDialog(BuildContext context) async { Future<void> _showDialog(BuildContext context) async {

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/splash_screen.dart';
import '../app.dart'; import '../app.dart';
import '../features/auth/screens/login_screen.dart'; import '../features/auth/screens/login_screen.dart';
// import '../features/auth/screens/forgot_password_screen.dart'; // import '../features/auth/screens/forgot_password_screen.dart';
@@ -10,7 +9,6 @@ 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';
class AppRoutes { class AppRoutes {
// Private constructor to prevent instantiation // Private constructor to prevent instantiation
@@ -31,13 +29,9 @@ class AppRoutes {
// Route generator // Route generator
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) { switch (settings.name) {
case splash:
return MaterialPageRoute(builder: (_) => const SplashScreen());
case login: case login:
return MaterialPageRoute(builder: (_) => const LoginScreen()); return MaterialPageRoute(builder: (_) => const LoginScreen());
case TncRequiredScreen.routeName: // 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

@@ -15,17 +15,7 @@ class AppThemes {
colorScheme: colorScheme, colorScheme: colorScheme,
useMaterial3: true, useMaterial3: true,
textTheme: GoogleFonts.rubikTextTheme(), textTheme: GoogleFonts.rubikTextTheme(),
).copyWith( );
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF01A04C),
titleTextStyle: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w700,
fontSize: 20,
),
iconTheme: IconThemeData(color: colorScheme.onPrimary),
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
));
} }
static ThemeData getDarkTheme(ThemeType type) { static ThemeData getDarkTheme(ThemeType type) {
@@ -42,17 +32,7 @@ class AppThemes {
textTheme: GoogleFonts.rubikTextTheme( textTheme: GoogleFonts.rubikTextTheme(
ThemeData(brightness: Brightness.dark).textTheme, ThemeData(brightness: Brightness.dark).textTheme,
), ),
).copyWith( );
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF01A04C),
titleTextStyle: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w700,
fontSize: 20,
),
iconTheme: IconThemeData(color: colorScheme.onPrimary),
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
));
} }
static Color _getSeedColor(ThemeType type) { static Color _getSeedColor(ThemeType type) {

15
lib/core/logger.dart Normal file
View File

@@ -0,0 +1,15 @@
import 'package:kmobile/core/toast.dart';
class Logger {
static void info(String message) {
showToast('INFO: $message');
}
static void warning(String message) {
showToast('WARNING: $message');
}
static void error(String message) {
showToast('ERROR: $message');
}
}

14
lib/core/toast.dart Normal file
View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
void showToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black,
textColor: Colors.white,
fontSize: 16.0,
);
}

View File

@@ -2,7 +2,6 @@ class Beneficiary {
final String accountNo; final String accountNo;
final String accountType; final String accountType;
final String name; final String name;
final DateTime? createdAt;
final String ifscCode; final String ifscCode;
final String? bankName; final String? bankName;
final String? branchName; final String? branchName;
@@ -12,7 +11,6 @@ class Beneficiary {
required this.accountNo, required this.accountNo,
required this.accountType, required this.accountType,
required this.name, required this.name,
this.createdAt,
required this.ifscCode, required this.ifscCode,
this.bankName, this.bankName,
this.branchName, this.branchName,
@@ -23,9 +21,6 @@ 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']),
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

@@ -13,12 +13,10 @@ class AuthRepository {
static const _accessTokenKey = 'access_token'; static const _accessTokenKey = 'access_token';
static const _tokenExpiryKey = 'token_expiry'; static const _tokenExpiryKey = 'token_expiry';
static const _tncKey = 'tnc';
AuthRepository(this._authService, this._userService, this._secureStorage); AuthRepository(this._authService, this._userService, this._secureStorage);
Future<(List<User>, AuthToken)> login( Future<List<User>> login(String customerNo, String password) async {
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);
@@ -29,7 +27,7 @@ class AuthRepository {
// Get and save user profile // Get and save user profile
final users = await _userService.getUserDetails(); final users = await _userService.getUserDetails();
return (users, authToken); return users;
} }
Future<bool> isLoggedIn() async { Future<bool> isLoggedIn() async {
@@ -49,7 +47,6 @@ class AuthRepository {
await _secureStorage.write(_accessTokenKey, token.accessToken); await _secureStorage.write(_accessTokenKey, token.accessToken);
await _secureStorage.write( await _secureStorage.write(
_tokenExpiryKey, token.expiresAt.toIso8601String()); _tokenExpiryKey, token.expiresAt.toIso8601String());
await _secureStorage.write(_tncKey, token.tnc.toString());
} }
Future<void> clearAuthTokens() async { Future<void> clearAuthTokens() async {
@@ -59,28 +56,13 @@ class AuthRepository {
Future<AuthToken?> _getAuthToken() async { Future<AuthToken?> _getAuthToken() async {
final accessToken = await _secureStorage.read(_accessTokenKey); final accessToken = await _secureStorage.read(_accessTokenKey);
final expiryString = await _secureStorage.read(_tokenExpiryKey); final expiryString = await _secureStorage.read(_tokenExpiryKey);
final tncString = await _secureStorage.read(_tncKey);
if (accessToken != null && expiryString != null) { if (accessToken != null && expiryString != null) {
final authToken = AuthToken( return AuthToken(
accessToken: accessToken, accessToken: accessToken,
expiresAt: DateTime.parse(expiryString), expiresAt: DateTime.parse(expiryString),
tnc:
tncString == 'true', // Parse 'true' string to true, otherwise false
); );
return authToken;
} }
return null; return null;
} }
Future<void> acceptTnc() async {
// This method calls the setTncFlag function
try {
await _authService.setTncflag();
} catch (e) {
// Handle or rethrow the error as needed
print('Error setting TNC flag: $e');
rethrow;
}
}
} }

View File

@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/api/services/limit_service.dart'; import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
@@ -22,8 +20,6 @@ import '../security/secure_storage.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
Future<void> setupDependencies() async { Future<void> setupDependencies() async {
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
RouteObserver<ModalRoute<void>>());
//getIt.registerLazySingleton<ThemeController>(() => ThemeController()); //getIt.registerLazySingleton<ThemeController>(() => ThemeController());
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController()); //getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
getIt.registerSingleton<ThemeCubit>(ThemeCubit()); getIt.registerSingleton<ThemeCubit>(ThemeCubit());
@@ -55,7 +51,6 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>())); getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>())); getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>())); getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
getIt.registerSingleton<BranchService>(BranchService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>( getIt.registerLazySingleton<ChangePasswordService>(
() => ChangePasswordService(getIt<Dio>()), () => ChangePasswordService(getIt<Dio>()),
); );
@@ -66,17 +61,17 @@ Future<void> setupDependencies() async {
); );
// Register controllers/cubits // Register controllers/cubits
getIt.registerFactory<AuthCubit>(() => AuthCubit( getIt.registerFactory<AuthCubit>(
getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>())); () => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
} }
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),
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60),
headers: { headers: {

View File

@@ -18,159 +18,75 @@ class AccountInfoScreen extends StatefulWidget {
class _AccountInfoScreen extends State<AccountInfoScreen> { class _AccountInfoScreen extends State<AccountInfoScreen> {
late User selectedUser; late User selectedUser;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedUser = widget.users[widget.selectedIndex]; selectedUser = widget.users[widget.selectedIndex];
} }
String getFullAccountType(String? accountType) {
if (accountType == null || accountType.isEmpty) return 'N/A';
// Convert to title case
switch (accountType.toLowerCase()) {
case 'sa':
return AppLocalizations.of(context).savingsAccount;
case 'sb':
return AppLocalizations.of(context).savingsAccount;
case 'ln':
return AppLocalizations.of(context).loanAccount;
case 'td':
return AppLocalizations.of(context).termDeposit;
case 'rd':
return AppLocalizations.of(context).recurringDeposit;
case 'ca':
return "Current Account";
case 'cc':
return "Cash Credit Account";
case 'od':
return "Overdraft Account";
default:
return AppLocalizations.of(context).unknownAccount;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final users = widget.users; final users = widget.users;
int selectedIndex = widget.selectedIndex; int selectedIndex = widget.selectedIndex;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context) title: Text(AppLocalizations.of(context)
.accountInfo .accountInfo
.replaceFirst(RegExp('\n'), '')), .replaceFirst(RegExp('\n'), '')),
), ),
body: Stack( body: ListView(
padding: const EdgeInsets.all(16.0),
children: [ children: [
Padding( Text(
padding: const EdgeInsets.all(8.0), AppLocalizations.of(context).accountNumber,
child: Column( style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
children: [
Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).accountNumber,
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
),
DropdownButton<User>(
value: selectedUser,
onChanged: (User? newUser) {
if (newUser != null) {
setState(() {
selectedUser = newUser;
});
}
},
items: widget.users.map((user) {
return DropdownMenuItem<User>(
value: user,
child: Text(
user.accountNo.toString(),
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
);
}).toList(),
isExpanded: true,
),
],
),
),
),
Expanded(
child: Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
InfoRow(
title: AppLocalizations.of(context).customerNumber,
value: selectedUser.cifNumber ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).accountType,
value: getFullAccountType(selectedUser.accountType),
),
InfoRow(
title: AppLocalizations.of(context).productName,
value: selectedUser.productType ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).accountStatus,
value: 'OPEN',
),
InfoRow(
title:
AppLocalizations.of(context).availableBalance,
value: selectedUser.availableBalance ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).currentBalance,
value: selectedUser.currentBalance ?? 'N/A',
),
if (users[selectedIndex].approvedAmount != null)
InfoRow(
title:
AppLocalizations.of(context).approvedAmount,
value: selectedUser.approvedAmount ?? 'N/A',
),
],
),
),
),
),
],
),
), ),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval( DropdownButton<User>(
child: Image.asset( value: selectedUser,
'assets/images/logo.png', onChanged: (User? newUser) {
if (newUser != null) {
width: 200, // Adjust size as needed setState(() {
selectedUser = newUser;
height: 200, // Adjust size as needed });
), }
), },
), items: widget.users.map((user) {
), return DropdownMenuItem<User>(
value: user,
child: Text(user.accountNo.toString()),
);
}).toList(),
), ),
InfoRow(
title: AppLocalizations.of(context).customerNumber,
value: selectedUser.cifNumber ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).productName,
value: selectedUser.productType ?? 'N/A',
),
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'),
InfoRow(
title: AppLocalizations.of(context).accountStatus,
value: 'OPEN',
),
InfoRow(
title: AppLocalizations.of(context).availableBalance,
value: selectedUser.availableBalance ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).currentBalance,
value: selectedUser.currentBalance ?? 'N/A',
),
users[selectedIndex].approvedAmount != null
? InfoRow(
title: AppLocalizations.of(context).approvedAmount,
value: selectedUser.approvedAmount ?? 'N/A',
)
: const SizedBox.shrink(),
], ],
), ),
); );
@@ -195,18 +111,15 @@ class InfoRow extends StatelessWidget {
Text( Text(
title, title,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 15,
fontWeight: FontWeight.bold, fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(
value, value,
style: TextStyle( style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
fontSize: 20,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface),
), ),
], ],
), ),

File diff suppressed because it is too large Load Diff

View File

@@ -1,155 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart';
class AllAccountsScreen extends StatefulWidget {
final List<User> users;
const AllAccountsScreen({super.key, required this.users});
@override
State<AllAccountsScreen> createState() => _AllAccountsScreenState();
}
class _AllAccountsScreenState extends State<AllAccountsScreen> {
final Map<String, bool> _visibilityMap = {};
String getFullAccountType(BuildContext context, String? accountType) {
// This is duplicated from dashboard_screen.dart.
// In a real app, this should be moved to a utility/helper class.
if (accountType == null || accountType.isEmpty) return 'N/A';
switch (accountType.toLowerCase()) {
case 'sa':
return AppLocalizations.of(context).savingsAccount;
case 'sb':
return AppLocalizations.of(context).savingsAccount;
case 'ln':
return AppLocalizations.of(context).loanAccount;
case 'td':
return AppLocalizations.of(context).termDeposit;
case 'rd':
return AppLocalizations.of(context).recurringDeposit;
case 'ca':
return "Current Account";
case 'cc':
return "Cash Credit Account";
case 'od':
return "Overdraft Account";
default:
return AppLocalizations.of(context).unknownAccount;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).viewall),
),
body: Column(
children: [
const SizedBox(height: 16.0), // Added space below the app bar
Expanded(
child: ListView.builder(
itemCount: widget.users.length,
itemBuilder: (context, index) {
final user = widget.users[index];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
child: _buildAccountCard(user),
);
},
),
), // Closing Expanded
], // Closing Column
),
);
}
Widget _buildAccountCard(User user) {
final theme = Theme.of(context);
final accountNo = user.accountNo ?? '';
final isVisible = _visibilityMap[accountNo] ?? false;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF01A04C),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top section: Account Type and Number
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getFullAccountType(context, user.accountType),
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
Text(
user.accountNo ?? 'N/A',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 14,
fontWeight: FontWeight.w700,
),
),
],
),
const SizedBox(height: 16),
// Bottom section: Balance and Toggle
Row(
children: [
Text(
"",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
isVisible ? user.currentBalance ?? '0.00' : '*****',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
),
),
const Spacer(),
InkWell(
onTap: () {
setState(() {
_visibilityMap[accountNo] = !isVisible;
});
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isVisible ? Symbols.visibility_lock : Symbols.visibility,
color: theme.scaffoldBackgroundColor,
weight: 800,
),
),
),
],
),
],
),
);
}
}

View File

@@ -14,95 +14,72 @@ class TransactionDetailsScreen extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: appBar:
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)), AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: Column(
padding: const EdgeInsets.all(16.0), children: [
child: Column( Expanded(
children: [ flex: 3,
Expanded( child: Center(
flex: 3, child: Column(
child: Center( mainAxisSize: MainAxisSize.min,
child: Column( children: [
// Amount + icon + Share Button
Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Amount + icon + Share Button
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${transaction.amount}",
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Icon(
isCredit
? Symbols.call_received
: Symbols.call_made,
color: isCredit
? const Color(0xFF10BB10)
: Theme.of(context).colorScheme.error,
size: 28,
),
],
),
const SizedBox(height: 8),
// Date centered
Text( Text(
transaction.date ?? "", "${transaction.amount}",
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 40,
color: Theme.of(context).textTheme.bodySmall?.color, fontWeight: FontWeight.bold,
), ),
textAlign: TextAlign.center, ),
const SizedBox(width: 8),
Icon(
isCredit ? Symbols.call_received : Symbols.call_made,
color: isCredit ? Colors.green : Colors.red,
size: 28,
), ),
], ],
), ),
), const SizedBox(height: 8),
), // Date centered
Divider(color: Theme.of(context).dividerColor), Text(
Expanded( transaction.date ?? "",
flex: 5, style: const TextStyle(
child: ListView( fontSize: 16,
children: [ color: Colors.grey,
_buildDetailRow( ),
AppLocalizations.of(context).transactionType, textAlign: TextAlign.center,
transaction.type ?? ""), ),
_buildDetailRow(AppLocalizations.of(context).transferType, ],
transaction.name.split("/").first ?? ""),
// if (transaction.name.length > 12) ...[
// _buildDetailRow(AppLocalizations.of(context).utrNo,
// transaction.name.split("= ")[1].split(" ")[0] ?? ""),
// _buildDetailRow(
// AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "")
// ]
_buildDetailRow(AppLocalizations.of(context).details,
transaction.name),
],
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
), ),
), ),
), ),
), const Divider(),
], Expanded(
flex: 5,
child: ListView(
children: [
_buildDetailRow(AppLocalizations.of(context).transactionType,
transaction.type ?? ""),
_buildDetailRow(AppLocalizations.of(context).transferType,
transaction.name.split("/").first ?? ""),
// if (transaction.name.length > 12) ...[
// _buildDetailRow(AppLocalizations.of(context).utrNo,
// transaction.name.split("= ")[1].split(" ")[0] ?? ""),
// _buildDetailRow(
// AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "")
// ]
_buildDetailRow(
AppLocalizations.of(context).details, transaction.name),
],
),
),
],
),
), ),
); );
} }

View File

@@ -1,22 +1,21 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:kmobile/api/services/user_service.dart'; import 'package:kmobile/api/services/user_service.dart';
import 'package:kmobile/core/errors/exceptions.dart'; import 'package:kmobile/core/errors/exceptions.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/features/auth/models/auth_token.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../data/repositories/auth_repository.dart'; import '../../../data/repositories/auth_repository.dart';
import 'auth_state.dart'; import 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> { class AuthCubit extends Cubit<AuthState> {
final AuthRepository _authRepository; final AuthRepository _authRepository;
final UserService _userService; final UserService _userService;
final SecureStorage _secureStorage;
AuthCubit(this._authRepository, this._userService, this._secureStorage) AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) {
: super(AuthInitial()) {
checkAuthStatus(); checkAuthStatus();
} }
void reset() {
emit(AuthInitial());
}
Future<void> checkAuthStatus() async { Future<void> checkAuthStatus() async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
@@ -32,64 +31,28 @@ class AuthCubit extends Cubit<AuthState> {
} }
} }
void startVerification() {
emit(AuthVerificationInProgress());
}
Future<void> refreshUserData() async { Future<void> refreshUserData() async {
try { try {
// emit(AuthLoading());
final users = await _userService.getUserDetails(); final users = await _userService.getUserDetails();
emit(Authenticated(users)); emit(Authenticated(users));
} catch (e) { } catch (e) {
emit(AuthError('Failed to refresh user data: ${e.toString()}')); emit(AuthError('Failed to refresh user data: ${e.toString()}'));
// Optionally, re-emit the previous state or handle as needed
} }
} }
Future<void> login(String customerNo, String password) async { Future<void> login(String customerNo, String password) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
final (users, authToken) = final users = await _authRepository.login(customerNo, password);
await _authRepository.login(customerNo, password); emit(Authenticated(users));
if (authToken.tnc == false) {
emit(ShowTncDialog(authToken, users));
} else {
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(
bool agreed, AuthToken authToken, List<User> users) async {
if (agreed) {
try {
await _authRepository.acceptTnc();
// The user is NOT fully authenticated yet. Just check for MPIN.
await _checkMpinAndNavigate(users);
} catch (e) {
emit(AuthError('Failed to accept TNC: $e'));
}
} else {
emit(NavigateToTncRequiredScreen());
}
}
void mpinSetupCompleted() {
if (state is NavigateToMpinSetupScreen) {
final users = (state as NavigateToMpinSetupScreen).users;
emit(Authenticated(users));
} else {
// Handle unexpected state if necessary
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));
}
}
} }

View File

@@ -1,58 +1,33 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:kmobile/data/models/user.dart'; import '../../../data/models/user.dart';
import 'package:kmobile/features/auth/models/auth_token.dart';
abstract class AuthState extends Equatable { abstract class AuthState extends Equatable {
const AuthState();
@override @override
List<Object> get props => []; List<Object?> get props => [];
} }
class AuthInitial extends AuthState {} class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {} class AuthLoading extends AuthState {}
class AuthVerificationInProgress extends AuthState {}
class Authenticated extends AuthState { class Authenticated extends AuthState {
final List<User> users; final List<User> users;
const Authenticated(this.users);
Authenticated(this.users);
@override @override
List<Object> get props => [users]; List<Object?> get props => [users];
} }
class Unauthenticated extends AuthState {} class Unauthenticated extends AuthState {}
class AuthError extends AuthState { class AuthError extends AuthState {
final String message; final String message;
const AuthError(this.message);
AuthError(this.message);
@override @override
List<Object> get props => [message]; List<Object?> get props => [message];
} }
// --- New States for Navigation and Dialog ---
// State to indicate that the TNC dialog needs to be shown
class ShowTncDialog extends AuthState {
final AuthToken authToken;
final List<User> users;
const ShowTncDialog(this.authToken, this.users);
@override
List<Object> get props => [authToken, users];
}
// States to trigger specific navigations from the UI
class NavigateToTncRequiredScreen extends AuthState {}
class NavigateToMpinSetupScreen extends AuthState {
final List<User> users;
const NavigateToMpinSetupScreen(this.users);
@override
List<Object> get props => [users];
}
class NavigateToDashboardScreen extends AuthState {}

View File

@@ -6,31 +6,16 @@ import 'package:equatable/equatable.dart';
class AuthToken extends Equatable { class AuthToken extends Equatable {
final String accessToken; final String accessToken;
final DateTime expiresAt; final DateTime expiresAt;
final bool tnc;
const AuthToken({ const AuthToken({
required this.accessToken, required this.accessToken,
required this.expiresAt, required this.expiresAt,
required this.tnc,
}); });
factory AuthToken.fromJson(Map<String, dynamic> json) { factory AuthToken.fromJson(Map<String, dynamic> json) {
final token = json['token'];
// Safely extract tnc.mobile directly from the outer JSON
bool tncMobileValue = false; // Default to false if not found or invalid
if (json.containsKey('tnc') && json['tnc'] is Map<String, dynamic>) {
final tncMap = json['tnc'] as Map<String, dynamic>;
if (tncMap.containsKey('mobile') && tncMap['mobile'] is bool) {
tncMobileValue = tncMap['mobile'] as bool;
}
}
return AuthToken( return AuthToken(
accessToken: token, accessToken: json['token'],
expiresAt: _decodeExpiryFromToken( expiresAt: _decodeExpiryFromToken(json['token']),
token), // This method is still valid for JWT expiry
tnc: tncMobileValue, // Use the correctly extracted value
); );
} }
@@ -57,45 +42,8 @@ class AuthToken extends Equatable {
} }
} }
// static bool _decodeTncFromToken(String token) {
// try {
// final parts = token.split('.');
// if (parts.length != 3) {
// throw Exception('Invalid JWT format for TNC decoding');
// }
// final payload = parts[1];
// String normalized = base64Url.normalize(payload);
// final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized)));
// if (payloadMap is! Map<String, dynamic> || !payloadMap.containsKey('tnc')) {
// // If 'tnc' is not present in the payload, default to false
// return false;
// }
// final tncValue = payloadMap['tnc'];
// // Handle different representations of 'true'
// if (tncValue is bool) {
// return tncValue;
// }
// if (tncValue is String) {
// return tncValue.toLowerCase() == 'true';
// }
// if (tncValue is int) {
// return tncValue == 1;
// }
// // Default to false for any other case
// return false;
// } catch (e) {
// log('Error decoding tnc from token: $e');
// // Default to false if decoding fails or 'tnc' is not found/invalid
// return false;
// }
// }
bool get isExpired => DateTime.now().isAfter(expiresAt); bool get isExpired => DateTime.now().isAfter(expiresAt);
@override @override
List<Object> get props => [accessToken, expiresAt, tnc]; List<Object> get props => [accessToken, expiresAt];
} }

View File

@@ -1,14 +1,8 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/set_password_screen.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/widgets/tnc_dialog.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../controllers/auth_cubit.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../controllers/auth_state.dart'; import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/l10n/app_localizations.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@@ -24,6 +18,12 @@ class LoginScreenState extends State<LoginScreen>
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
bool _obscurePassword = true; bool _obscurePassword = true;
@override
void initState() {
super.initState();
context.read<AuthCubit>().reset();
}
@override @override
void dispose() { void dispose() {
_customerNumberController.dispose(); _customerNumberController.dispose();
@@ -31,9 +31,9 @@ class LoginScreenState extends State<LoginScreen>
super.dispose(); super.dispose();
} }
void _submitForm() async { void _submitForm() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
final bool? verificationSuccess = await Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => VerificationScreen( builder: (_) => VerificationScreen(
customerNo: _customerNumberController.text.trim(), customerNo: _customerNumberController.text.trim(),
@@ -41,395 +41,148 @@ class LoginScreenState extends State<LoginScreen>
), ),
), ),
); );
if (verificationSuccess == true && mounted) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
}
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// return Scaffold(
// body: BlocConsumer<AuthCubit, AuthState>(
// listener: (context, state) async {
// if (state is ShowTncDialog) {
// // The dialog now returns a boolean for the 'disagree' case,
// // or it completes when the 'proceed' action is finished.
// final agreed = await showDialog<bool>(
// context: context,
// barrierDismissible: false,
// builder: (dialogContext) => TncDialog(
// onProceed: () async {
// // This function is passed to the dialog.
// // It calls the cubit and completes when the cubit's work is done.
// await context
// .read<AuthCubit>()
// .onTncDialogResult(true, state.authToken, state.users);
// },
// ),
// );
// // If 'agreed' is false, it means the user clicked 'Disagree'.
// if (agreed == false) {
// if (!context.mounted) return;
// context
// .read<AuthCubit>()
// .onTncDialogResult(false, state.authToken, state.users);
// }
// } else if (state is NavigateToTncRequiredScreen) {
// Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
// } else if (state is NavigateToMpinSetupScreen) {
// Navigator.of(context).push( // Use push, NOT pushReplacement
// MaterialPageRoute(
// builder: (_) => MPinScreen(
// mode: MPinMode.set,
// onCompleted: (_) {
// // This clears the entire stack and pushes the dashboard
// Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
// (route) => false,
// );
// },
// ),
// ),
// );
// } else if (state is NavigateToDashboardScreen) {
// Navigator.of(context).pushReplacement(
// MaterialPageRoute(builder: (_) => const NavigationScaffold()),
// );
// } else if (state is AuthError) {
// if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
// Navigator.of(context).push(MaterialPageRoute(
// builder: (_) => SetPasswordScreen(
// customerNo: _customerNumberController.text.trim(),
// )));
// } else {
// ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(content: Text(state.message)));
// }
// }
// },
// builder: (context, state) {
// // The commented out section is removed for clarity, the logic is now above.
// return Padding(
// padding: const EdgeInsets.all(24.0),
// child: Form(
// key: _formKey,
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Image.asset(
// 'assets/images/logo.png',
// width: 150,
// height: 150,
// errorBuilder: (context, error, stackTrace) {
// return Icon(
// Icons.account_balance,
// size: 100,
// color: Theme.of(context).primaryColor,
// );
// },
// ),
// const SizedBox(height: 16),
// Text(
// AppLocalizations.of(context).kccb,
// style: TextStyle(
// fontSize: 32,
// fontWeight: FontWeight.bold,
// color: Theme.of(context).primaryColor,
// ),
// ),
// const SizedBox(height: 48),
// TextFormField(
// controller: _customerNumberController,
// decoration: InputDecoration(
// labelText: AppLocalizations.of(context).customerNumber,
// 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).pleaseEnterUsername;
// }
// return null;
// },
// ),
// const SizedBox(height: 24),
// TextFormField(
// controller: _passwordController,
// obscureText: _obscurePassword,
// textInputAction: TextInputAction.done,
// onFieldSubmitted: (_) => _submitForm(),
// decoration: InputDecoration(
// labelText: AppLocalizations.of(context).password,
// 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),
// ),
// suffixIcon: IconButton(
// icon: Icon(
// _obscurePassword
// ? Icons.visibility
// : Icons.visibility_off,
// ),
// onPressed: () {
// setState(() {
// _obscurePassword = !_obscurePassword;
// });
// },
// ),
// ),
// validator: (value) {
// if (value == null || value.isEmpty) {
// return AppLocalizations.of(context).pleaseEnterPassword;
// }
// return null;
// },
// ),
// const SizedBox(height: 24),
// SizedBox(
// width: 250,
// child: ElevatedButton(
// onPressed: state is AuthLoading ? null : _submitForm,
// style: ElevatedButton.styleFrom(
// shape: const StadiumBorder(),
// padding: const EdgeInsets.symmetric(vertical: 16),
// backgroundColor:
// Theme.of(context).scaffoldBackgroundColor,
// foregroundColor: Theme.of(context).primaryColorDark,
// side: BorderSide(
// color: Theme.of(context).colorScheme.outline,
// width: 1),
// elevation: 0,
// ),
// child: state is AuthLoading
// ? const CircularProgressIndicator()
// : Text(
// AppLocalizations.of(context).login,
// style: TextStyle(
// color: Theme.of(context)
// .colorScheme
// .onPrimaryContainer),
// ),
// ),
// ),
// const SizedBox(height: 25),
// ],
// ),
// ),
// );
// },
// ),
// );
return Scaffold( return Scaffold(
body: BlocConsumer<AuthCubit, AuthState>( // appBar: AppBar(title: const Text('Login')),
listener: (context, state) { body: Padding(
if (state is ShowTncDialog) { padding: const EdgeInsets.all(24.0),
showDialog<bool>( child: Form(
context: context, key: _formKey,
barrierDismissible: false, child: Column(
builder: (dialogContext) => TncDialog( mainAxisAlignment: MainAxisAlignment.center,
onProceed: () async { children: [
// Pop the dialog before the cubit action Image.asset(
Navigator.of(dialogContext).pop(); 'assets/images/logo.png',
await context width: 150,
.read<AuthCubit>() height: 150,
.onTncDialogResult(true, state.authToken, state.users); errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.account_balance,
size: 100,
color: Theme.of(context).primaryColor,
);
}, },
), ),
); const SizedBox(height: 16),
} else if (state is NavigateToTncRequiredScreen) { // Title
Navigator.of(context).pushNamed(TncRequiredScreen.routeName); Text(
} else if (state is NavigateToMpinSetupScreen) { AppLocalizations.of(context).kccb,
Navigator.of(context).push( style: TextStyle(
// Use push, NOT pushReplacement fontSize: 32,
MaterialPageRoute( fontWeight: FontWeight.bold,
builder: (_) => MPinScreen( color: Theme.of(context).primaryColor,
mode: MPinMode.set,
onCompleted: (_) {
// Call the cubit to signal MPIN setup is complete
context.read<AuthCubit>().mpinSetupCompleted();
},
), ),
), ),
); const SizedBox(height: 48),
} else if (state is Authenticated) {
// This is the single source of truth for navigating to the dashboard TextFormField(
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( controller: _customerNumberController,
MaterialPageRoute(builder: (_) => const NavigationScaffold()), decoration: InputDecoration(
(route) => false, labelText: AppLocalizations.of(context).customerNumber,
); // prefixIcon: Icon(Icons.person),
} else if (state is AuthError) { border: const OutlineInputBorder(),
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') { isDense: true,
Navigator.of(context).push(MaterialPageRoute( filled: true,
builder: (_) => SetPasswordScreen( fillColor: Theme.of(context).scaffoldBackgroundColor,
customerNo: _customerNumberController.text.trim(), enabledBorder: OutlineInputBorder(
))); borderSide: BorderSide(
} else { color: Theme.of(context).colorScheme.outline),
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
}
}
},
builder: (context, state) {
// The builder part remains largely the same, focusing on UI display
return Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
width: 150,
height: 150,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.account_balance,
size: 100,
color: Theme.of(context).primaryColor,
);
},
), ),
const SizedBox(height: 16), focusedBorder: OutlineInputBorder(
Text( borderSide: BorderSide(
AppLocalizations.of(context).kccb, color: Theme.of(context).colorScheme.primary,
style: TextStyle( width: 2),
fontSize: 32,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
), ),
const SizedBox(height: 48), ),
TextFormField( keyboardType: TextInputType.number,
controller: _customerNumberController, textInputAction: TextInputAction.next,
decoration: InputDecoration( validator: (value) {
labelText: AppLocalizations.of(context).customerNumber, if (value == null || value.isEmpty) {
border: const OutlineInputBorder(), return AppLocalizations.of(context).pleaseEnterUsername;
isDense: true, }
filled: true, return null;
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).pleaseEnterUsername;
}
return null;
},
),
const SizedBox(height: 24),
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
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),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterPassword;
}
return null;
},
),
const SizedBox(height: 24),
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: state is AuthLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorDark,
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1),
elevation: 0,
),
child: state is AuthLoading
? const CircularProgressIndicator()
: Text(
AppLocalizations.of(context).login,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
),
const SizedBox(height: 25),
],
), ),
), const SizedBox(height: 24),
); // Password
}, TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
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),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterPassword;
}
return null;
},
),
const SizedBox(height: 24),
//Login Button
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorDark,
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1),
elevation: 0,
),
child: Text(
AppLocalizations.of(context).login,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
),
const SizedBox(height: 15),
const SizedBox(height: 25),
],
),
),
), ),
); );
} }
} }

View File

@@ -4,6 +4,7 @@ import 'dart:math';
// import 'dart:developer'; // import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
@@ -15,18 +16,12 @@ class MPinScreen extends StatefulWidget {
final MPinMode mode; final MPinMode mode;
final String? initialPin; final String? initialPin;
final void Function(String pin)? onCompleted; final void Function(String pin)? onCompleted;
final bool disableBiometric;
final String? customTitle;
final String? customConfirmTitle;
const MPinScreen({ const MPinScreen({
super.key, super.key,
required this.mode, required this.mode,
this.initialPin, this.initialPin,
this.onCompleted, this.onCompleted,
this.disableBiometric = false,
this.customTitle,
this.customConfirmTitle,
}); });
@override @override
@@ -83,7 +78,7 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut), CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
); );
if (widget.mode == MPinMode.enter && !widget.disableBiometric) { if (widget.mode == MPinMode.enter) {
_tryBiometricBeforePin(); _tryBiometricBeforePin();
} }
} }
@@ -178,36 +173,29 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
break; break;
case MPinMode.set: case MPinMode.set:
// Navigate to confirm and wait for result // propagate parent onCompleted into confirm step
final result = await Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MPinScreen( builder: (_) => MPinScreen(
mode: MPinMode.confirm, mode: MPinMode.confirm,
initialPin: pin, initialPin: pin,
onCompleted: (confirmedPin) { onCompleted: widget.onCompleted, // <-- use parent callback
// Just pop with the pin, don't call parent callback yet
Navigator.of(context).pop(confirmedPin);
},
disableBiometric: widget.disableBiometric,
customTitle: widget.customConfirmTitle,
), ),
), ),
); );
// If confirm succeeded, call parent callback
if (result != null && mounted) {
widget.onCompleted?.call(result);
}
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 // 3) now clear the entire navigation stack and go to your main scaffold
if (mounted) { if (mounted) {
widget.onCompleted?.call(pin); Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
} }
} else { } else {
setState(() { setState(() {
@@ -351,9 +339,6 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
String getTitle() { String getTitle() {
if (widget.customTitle != null) {
return widget.customTitle!;
}
switch (widget.mode) { switch (widget.mode) {
case MPinMode.enter: case MPinMode.enter:
return AppLocalizations.of(context).enterMPIN; return AppLocalizations.of(context).enterMPIN;

View File

@@ -1,3 +1,5 @@
// lib/features/auth/screens/sms_verification_helper.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/api/services/send_sms_service.dart'; import 'package:kmobile/api/services/send_sms_service.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@@ -6,30 +8,6 @@ import 'package:uuid/uuid.dart';
class SmsVerificationHelper { class SmsVerificationHelper {
final SmsService _smsService = SmsService(); final SmsService _smsService = SmsService();
Future<void> _showPermanentlyDeniedDialog(BuildContext context) async {
await showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Permission Required"),
content: const Text(
"SMS and Phone permissions are required for device verification. Please enable them in your app settings to continue."),
actions: [
TextButton(
child: const Text("Cancel"),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text("Open Settings"),
onPressed: () {
openAppSettings(); // Opens the phone's settings screen for this app
Navigator.of(context).pop();
},
),
],
),
);
}
Future<void> _showRestrictedSmsDialog(BuildContext context) async { Future<void> _showRestrictedSmsDialog(BuildContext context) async {
await showDialog( await showDialog(
context: context, context: context,
@@ -39,24 +17,17 @@ class SmsVerificationHelper {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text("It seems your device is restricting this app from sending SMS messages, which is required for verification. Please follow these steps to enable it:\n"),
"It seems your device is restricting this app from sending SMS messages, which is required for verification. Please follow these steps to enable it:\n"), Text("1. Open your device Settings.", style: TextStyle(fontWeight: FontWeight.bold)),
Text("1. Open your device Settings.",
style: TextStyle(fontWeight: FontWeight.bold)),
Text("2. Go to 'Apps' or 'Apps & notifications'."), Text("2. Go to 'Apps' or 'Apps & notifications'."),
Text("3. Find and tap on this app ('KMobile')."), Text("3. Find and tap on this app ('KMobile')."),
Text("4. Tap on the three dots (⋮) in the top right corner."), Text("4. Tap on the three dots (⋮) in the top right corner."),
Text( Text("5. Select 'Allow restricted settings' and confirm. This is crucial to allow SMS permission."),
"5. Select 'Allow restricted settings' and confirm. This is crucial to allow SMS permission."),
Text("6. Now you have two options to allow SMS permission:"), Text("6. Now you have two options to allow SMS permission:"),
Text( Text(" a. Tap on 'Permissions', then find 'SMS' is set to 'Allow'."),
" a. Tap on 'Permissions', then find 'SMS' is set to 'Allow'."), Text(" b. Alternatively, you can return to the KMobile app, and the SMS permission pop-up should appear again, allowing you to grant it directly."),
Text( Text("\nSome devices have an additional setting for 'Premium SMS'. If the above doesn't work, look for a 'Premium SMS access' setting (you can search for it in your Settings app) and set it to 'Always Allow' for this app.\n"),
" b. Alternatively, you can return to the KMobile app, and the SMS permission pop-up should appear again, allowing you to grant it directly."), Text("After you've enabled the permission, please come back to the app."),
Text(
"\nSome devices have an additional setting for 'Premium SMS'. If the above doesn't work, look for a 'Premium SMS access' setting (you can search for it in your Settings app) and set it to 'Always Allow' for this app.\n"),
Text(
"After you've enabled the permission, please come back to the app."),
], ],
), ),
), ),
@@ -65,113 +36,96 @@ class SmsVerificationHelper {
child: const Text("I've Enabled It"), child: const Text("I've Enabled It"),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
), ),
TextButton(
child: const Text("Open Settings"),
onPressed: () {
openAppSettings();
Navigator.of(context).pop();
},
),
], ],
), ),
); );
} }
Future<String?> initiateSmsSequence({ void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
),
);
}
Future<void> initiateSmsSequence({
required BuildContext context, required BuildContext context,
}) async { }) async {
bool hasPermission = false; bool hasPermission = false;
// --- PERMISSION LOOP --- // --- PERMISSION LOOP ---
while (!hasPermission) { while (!hasPermission) {
// handleSmsPermission will check the status and request if not granted.
final status = await _smsService.handleSmsPermission(); final status = await _smsService.handleSmsPermission();
switch (status) { switch (status) {
case PermissionStatusResult.granted: case PermissionStatusResult.granted:
ScaffoldMessenger.of(context).showSnackBar( _showSnackBar(context, "Permissions Granted! Proceeding...");
const SnackBar(
content: Text("Permissions Granted! Proceeding..."),
duration: Duration(seconds: 2)),
);
hasPermission = true; // This will break the loop hasPermission = true; // This will break the loop
break; break;
case PermissionStatusResult.denied: case PermissionStatusResult.denied:
// The user denied the permission. We show a dialog to explain why we need it _showSnackBar(context, "SMS and Phone permissions are required. Please try again.");
// and give them a chance to cancel or let the loop try again. await Future.delayed(const Duration(seconds: 3));
final tryAgain = await showDialog<bool>( break;
case PermissionStatusResult.permanentlyDenied:
await showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text("Permission Required"), title: const Text("Permission Required"),
content: const Text( content: const Text("SMS and Phone permissions are required for device verification. Please enable them in your app settings to continue."),
"This app requires SMS and Phone permissions to verify your device. Please grant the permissions to continue."),
actions: [ actions: [
TextButton( TextButton(
child: const Text("Cancel"), child: const Text("Cancel"),
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(),
), ),
TextButton( TextButton(
child: const Text("Try Again"), child: const Text("Open Settings"),
onPressed: () => Navigator.of(context).pop(true), onPressed: () {
openAppSettings(); // Opens the phone's settings screen for this app
Navigator.of(context).pop();
},
), ),
], ],
), ),
); );
if (tryAgain != true) { // Wait for user to return from settings
return null; // User chose to cancel.
}
// If they chose "Try Again", the loop will repeat.
break;
case PermissionStatusResult.permanentlyDenied:
await _showPermanentlyDeniedDialog(context);
// Give user time to come back from settings
await Future.delayed(const Duration(seconds: 5)); await Future.delayed(const Duration(seconds: 5));
// The loop will repeat and re-check the status.
break; break;
case PermissionStatusResult.restricted: case PermissionStatusResult.restricted:
await _showRestrictedSmsDialog(context); await _showRestrictedSmsDialog(context);
// Give user time to come back from settings // Wait for user to return from settings
await Future.delayed(const Duration(seconds: 5)); await Future.delayed(const Duration(seconds: 10));
// The loop will repeat and re-check the status.
break; break;
} }
} }
// --- SMS SENDING LOOP --- // --- SMS SENDING LOOP ---
// This part will only be reached if hasPermission is true. bool isSmsSent = false;
int retries = 3; while (!isSmsSent) {
while (retries > 0) {
var uuid = const Uuid(); var uuid = const Uuid();
String uniqueId = uuid.v4(); String uniqueId = uuid.v4();
String smsMessage = uniqueId; String smsMessage = uniqueId;
_showSnackBar(context, "Attempting to send verification SMS...");
ScaffoldMessenger.of(context).showSnackBar( isSmsSent = await _smsService.sendVerificationSms(
SnackBar(
content:
Text("Attempting to send verification SMS... (${4 - retries})"),
duration: const Duration(seconds: 2)),
);
bool isSmsSent = await _smsService.sendVerificationSms(
context: context, context: context,
destinationNumber: '9580079717', // Replace with your number destinationNumber: '9580079717', // Replace with your number
message: smsMessage, message: smsMessage,
); );
if (isSmsSent) { if (isSmsSent) {
ScaffoldMessenger.of(context).showSnackBar( _showSnackBar(context, "SMS sent successfully! Proceeding to login.");
const SnackBar( break;
content: Text("SMS sent successfully!"),
duration: Duration(seconds: 2)),
);
return uniqueId;
} else { } else {
retries--; _showSnackBar(context, "SMS failed to send. Retrying in 5 seconds...");
if (retries > 0) { await Future.delayed(const Duration(seconds: 5));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("SMS failed to send. Retrying in 5 seconds..."),
duration: Duration(seconds: 4)),
);
await Future.delayed(const Duration(seconds: 5));
}
} }
} }
// If all retries fail
return null;
} }
} }

View File

@@ -1,94 +0,0 @@
import 'package:package_info_plus/package_info_plus.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
String _version = '';
@override
void initState() {
super.initState();
_loadVersion();
}
Future<void> _loadVersion() async {
final PackageInfo info = await PackageInfo.fromPlatform();
if (mounted) {
// Check if the widget is still in the tree
setState(() {
_version = 'Version ${info.version} (${info.buildNumber})';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: <Widget>[
Positioned.fill(
child: Image.asset(
'assets/images/kconnect2.webp',
fit: BoxFit.cover,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).kccbMobile,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
),
),
const SizedBox(height: 12),
Text(
AppLocalizations.of(context).kccBankFull,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18,
color: Color(0xFFFFFFFF),
letterSpacing: 1.2,
),
),
],
),
),
const Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: CircularProgressIndicator(color: Color(0xFFFFFFFF)),
),
),
Positioned(
bottom: 90,
left: 0,
right: 0,
child: Text(
_version,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFFFFFFFF),
fontSize: 14,
),
),
),
],
),
);
}
}

View File

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

View File

@@ -1,8 +1,13 @@
// lib/features/auth/screens/verification_screen.dart
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/api/services/auth_service.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart'; import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
import '../../../app.dart';
class VerificationScreen extends StatefulWidget { class VerificationScreen extends StatefulWidget {
final String customerNo; final String customerNo;
@@ -19,145 +24,121 @@ class VerificationScreen extends StatefulWidget {
} }
class _VerificationScreenState extends State<VerificationScreen> { class _VerificationScreenState extends State<VerificationScreen> {
String _statusMessage = "Starting verification..."; final SmsVerificationHelper _smsVerificationHelper = SmsVerificationHelper();
Timer? _timer; late Timer _timer;
int _countdown = 120; int _start = 120;
bool _isVerifying = false; String _message = "Attempting verification...";
bool _verificationFailed = false; String? _error;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_startVerificationProcess(); context.read<AuthCubit>().startVerification();
startTimer();
_verifySmsAndLogin();
}
void startTimer() {
const oneSec = Duration(seconds: 1);
_timer = Timer.periodic(
oneSec,
(Timer timer) {
if (_start == 0) {
timer.cancel();
if (mounted) {
setState(() {
_error = "Verification timed out.";
});
}
} else {
setState(() {
_start--;
});
}
},
);
}
Future<void> _verifySmsAndLogin() async {
await _smsVerificationHelper.initiateSmsSequence(context: context);
// After SMS sequence completes, proceed with login
_timer.cancel(); // Stop the timer
if (mounted) {
context.read<AuthCubit>().login(widget.customerNo, widget.password);
}
} }
@override @override
void dispose() { void dispose() {
_timer?.cancel(); _timer.cancel();
super.dispose(); super.dispose();
} }
void _startTimer() {
_timer?.cancel(); // Cancel any existing timer
_countdown = 120;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown > 0) {
setState(() {
_countdown--;
});
} else {
_timer?.cancel();
if (mounted) {
setState(() {
_statusMessage = "Verification timed out. Please try again.";
_isVerifying = false;
_verificationFailed = true;
});
}
}
});
}
Future<void> _startVerificationProcess() async {
setState(() {
_isVerifying = true;
_verificationFailed = false;
_statusMessage = "Starting verification...";
});
_startTimer();
// 1. Send SMS
setState(() {
_statusMessage = "SMS sending...";
});
final smsHelper = SmsVerificationHelper();
final uuid = await smsHelper.initiateSmsSequence(context: context);
if (uuid != null && mounted) {
// SMS sending was successful, now wait before verifying.
setState(() {
_statusMessage = "SMS sent. Waiting for network delivery...";
});
// Adding a 10-second delay to account for SMS network latency.
await Future.delayed(const Duration(seconds: 10));
if (!mounted) return;
// 2. Verify SIM
setState(() {
_statusMessage = "Verifying with server...";
});
final authService = getIt<AuthService>();
try {
await authService.simVerify(uuid, widget.customerNo);
setState(() {
_statusMessage = "Verification successful!";
_isVerifying = false;
});
_timer?.cancel();
// Pop with success result
Navigator.of(context).pop(true);
} catch (e) {
setState(() {
_statusMessage = e.toString();
_isVerifying = false;
_verificationFailed = true;
});
}
} else if (mounted) {
setState(() {
_statusMessage =
"SMS sending failed. Please check permissions and try again.";
_isVerifying = false;
_verificationFailed = true;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( body: BlocListener<AuthCubit, AuthState>(
title: const Text("Device Verification"), listenWhen: (previous, current) {
automaticallyImplyLeading: !_isVerifying, return current is! AuthVerificationInProgress && current is! AuthInitial;
), },
body: Center( listener: (context, state) {
child: Padding( if (state is Authenticated) {
padding: const EdgeInsets.all(24.0), _timer.cancel();
child: Column( Navigator.of(context).pushReplacement(
mainAxisAlignment: MainAxisAlignment.center, MaterialPageRoute(
children: [ builder: (_) => MPinScreen(
if (_isVerifying) const CircularProgressIndicator(), mode: MPinMode.set,
if (!_isVerifying && _verificationFailed) onCompleted: (_) {
const Icon(Icons.error_outline, color: Colors.red, size: 50), Navigator.of(
if (!_isVerifying && !_verificationFailed) context,
const Icon(Icons.check_circle_outline, rootNavigator: true,
color: Colors.green, size: 50), ).pushReplacement(
const SizedBox(height: 32), MaterialPageRoute(
Text( builder: (_) => const NavigationScaffold(),
_statusMessage, ),
textAlign: TextAlign.center, );
style: const TextStyle(fontSize: 18), },
),
), ),
const SizedBox(height: 16), );
if (_isVerifying) } else if (state is AuthError) {
Text( _timer.cancel();
"Time remaining: $_countdown seconds", setState(() {
style: const TextStyle(fontSize: 16, color: Colors.grey), _error = state.message;
});
}
},
child: Center(
child: _error != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 80),
const SizedBox(height: 16),
Text(
_error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red, fontSize: 18),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Back to Login"),
)
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_message),
const SizedBox(height: 16),
Text("Time remaining: $_start seconds"),
],
), ),
if (_verificationFailed && !_isVerifying) ...[
const SizedBox(height: 20),
ElevatedButton(
onPressed: _startVerificationProcess,
child: const Text('Retry'),
),
]
],
),
), ),
), ),
); );

View File

@@ -264,306 +264,278 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
centerTitle: false, centerTitle: false,
), ),
body: SafeArea( body: SafeArea(
child: Stack( child: Form(
children: [ key: _formKey,
Form( child: Column(
key: _formKey, children: [
child: Column( Expanded(
children: [ child: SingleChildScrollView(
Expanded( physics: const AlwaysScrollableScrollPhysics(),
child: SingleChildScrollView( child: Padding(
physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(10.0),
child: Padding( child: Column(
padding: const EdgeInsets.all(10.0), children: [
child: Column( TextFormField(
children: [ key: _accountNumberFieldKey,
TextFormField( controller: accountNumberController,
key: _accountNumberFieldKey, decoration: InputDecoration(
controller: accountNumberController, labelText: AppLocalizations.of(
decoration: InputDecoration( context,
labelText: AppLocalizations.of( ).accountNumber,
context, // prefixIcon: Icon(Icons.person),
).accountNumber, border: const OutlineInputBorder(),
// prefixIcon: Icon(Icons.person), isDense: true,
border: const OutlineInputBorder(), ),
isDense: true, obscureText: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.length < 10) {
return AppLocalizations.of(
context,
).enterValidAccountNumber;
}
return null;
},
),
const SizedBox(height: 24),
// Confirm Account Number
TextFormField(
key: _confirmAccountNumberFieldKey,
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
),
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;
},
),
const SizedBox(height: 24),
TextFormField(
focusNode: _ifscFocusNode,
key: _ifscFieldKey,
controller: ifscController,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) {
// clear bank/branch if backspace or changed
bankNameController.clear();
branchNameController.clear();
}
});
},
validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) {
return AppLocalizations.of(context).enterIfsc;
} else if (!pattern.hasMatch(
value.trim().toUpperCase(),
)) {
return AppLocalizations.of(
context,
).invalidIfscFormat;
}
return null;
},
),
const SizedBox(height: 24),
// Bank Name (Disabled)
TextFormField(
controller: bankNameController,
enabled: false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText: AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
),
),
const SizedBox(height: 24),
// 🔹 Branch Name (Disabled)
TextFormField(
controller: branchNameController,
enabled: false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText: AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
),
),
if (_isBeneficiaryValidated)
Column(
children: [
const SizedBox(height: 24),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
suffixIcon: _isBeneficiaryValidated
? const Icon(
Symbols.verified,
size: 25,
fill: 1,
)
: null,
suffixIconColor:
Theme.of(context).colorScheme.primary,
labelText: AppLocalizations.of(context)
.beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
validator: (value) => value == null ||
value.isEmpty
? AppLocalizations.of(context).nameRequired
: null,
), ),
obscureText: true, ],
keyboardType: TextInputType.number, ),
textInputAction: TextInputAction.next, const SizedBox(height: 24),
onChanged: (value) { if (!_isBeneficiaryValidated)
nameController.clear(); Padding(
setState(() { padding: const EdgeInsets.only(bottom: 24),
_isBeneficiaryValidated = false; child: SizedBox(
}); width: double.infinity,
}, child: ElevatedButton(
validator: (value) { onPressed: _isValidating ||
if (value == null || value.length < 10) { ifscController.text.length != 11
return AppLocalizations.of( ? null
context, : () {
).enterValidAccountNumber; final isAccountValid =
} _accountNumberFieldKey.currentState!
return null; .validate();
}, final isConfirmAccountValid =
), _confirmAccountNumberFieldKey
const SizedBox(height: 24),
// Confirm Account Number
TextFormField(
key: _confirmAccountNumberFieldKey,
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
),
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;
},
),
const SizedBox(height: 24),
TextFormField(
focusNode: _ifscFocusNode,
key: _ifscFieldKey,
controller: ifscController,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) {
// clear bank/branch if backspace or changed
bankNameController.clear();
branchNameController.clear();
}
});
},
validator: (value) {
final pattern =
RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) {
return AppLocalizations.of(context).enterIfsc;
} else if (!pattern.hasMatch(
value.trim().toUpperCase(),
)) {
return AppLocalizations.of(
context,
).invalidIfscFormat;
}
return null;
},
),
const SizedBox(height: 24),
// Bank Name (Disabled)
TextFormField(
controller: bankNameController,
enabled:
false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
),
),
const SizedBox(height: 24),
// 🔹 Branch Name (Disabled)
TextFormField(
controller: branchNameController,
enabled:
false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
),
),
if (_isBeneficiaryValidated)
Column(
children: [
const SizedBox(height: 24),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
suffixIcon: _isBeneficiaryValidated
? const Icon(
Symbols.verified,
size: 25,
fill: 1,
)
: null,
suffixIconColor:
Theme.of(context).colorScheme.primary,
labelText: AppLocalizations.of(context)
.beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
validator: (value) =>
value == null || value.isEmpty
? AppLocalizations.of(context)
.nameRequired
: null,
),
],
),
const SizedBox(height: 24),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating ||
ifscController.text.length != 11
? null
: () {
final isAccountValid =
_accountNumberFieldKey
.currentState!
.validate();
final isConfirmAccountValid =
_confirmAccountNumberFieldKey
.currentState!
.validate();
final isIfscValid = _ifscFieldKey
.currentState! .currentState!
.validate(); .validate();
final isIfscValid = _ifscFieldKey
.currentState!
.validate();
if (isAccountValid && if (isAccountValid &&
isConfirmAccountValid && isConfirmAccountValid &&
isIfscValid) { isIfscValid) {
_validateBeneficiary(); _validateBeneficiary();
} }
}, },
child: _isValidating child: _isValidating
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeWidth: 2), strokeWidth: 2),
) )
: Text(AppLocalizations.of(context) : Text(AppLocalizations.of(context)
.validateBeneficiary), .validateBeneficiary),
),
),
), ),
//Beneficiary Name (Disabled)
// 🔹 Account Type Dropdown
DropdownButtonFormField<String>(
value: accountType,
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).accountType,
border: const OutlineInputBorder(),
isDense: true,
),
items: [
'Savings',
'Current',
]
.map(
(type) => DropdownMenuItem(
value: type,
child: Text(type),
),
)
.toList(),
onChanged: (value) {
setState(() {
accountType = value!;
});
},
), ),
),
//Beneficiary Name (Disabled)
// 🔹 Account Type Dropdown
DropdownButtonFormField<String>(
value: accountType,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).accountType,
border: const OutlineInputBorder(),
isDense: true,
),
items: [
'Savings',
'Current',
]
.map(
(type) => DropdownMenuItem(
value: type,
child: Text(type),
),
)
.toList(),
onChanged: (value) {
setState(() {
accountType = value!;
});
},
),
const SizedBox(height: 24), const SizedBox(height: 24),
TextFormField( TextFormField(
controller: phoneController, controller: phoneController,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).phone, labelText: AppLocalizations.of(context).phone,
prefixIcon: const Icon(Icons.phone), prefixIcon: const Icon(Icons.phone),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
), ),
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
validator: (value) => value == null || validator: (value) =>
value.length != 10 value == null || value.length != 10
? AppLocalizations.of(context).enterValidPhone ? AppLocalizations.of(context).enterValidPhone
: null, : null,
),
const SizedBox(height: 35),
],
), ),
), const SizedBox(height: 35),
), ],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: validateAndAddBeneficiary,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer),
child: Text(
AppLocalizations.of(context).validateAndAdd,
style: const TextStyle(fontSize: 16),
),
),
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
), ),
), ),
), ),
), ),
), Padding(
], padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: validateAndAddBeneficiary,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer),
child: Text(
AppLocalizations.of(context).validateAndAdd,
style: const TextStyle(fontSize: 16),
),
),
),
),
],
),
), ),
), ),
); );

View File

@@ -22,9 +22,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
_showSuccessDialog(context); _showSuccessDialog(context);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text('Failed to delete beneficiary: $e')),
content: Text(
'${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
); );
} }
} }
@@ -34,12 +32,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).success), title: const Text('Success'),
content: content: const Text('Beneficiary deleted successfully.'),
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(AppLocalizations.of(context).ok), child: const Text('OK'),
onPressed: () { onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
}, },
@@ -55,18 +52,18 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).deleteBeneficiary), title: const Text('Delete Beneficiary'),
content: Text(AppLocalizations.of(context) content:
.areYouSureYouWantToDeleteThisBeneficiary), const Text('Are you sure you want to delete this beneficiary?'),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(AppLocalizations.of(context).cancel), child: const Text('Cancel'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
TextButton( TextButton(
child: Text(AppLocalizations.of(context).delete), child: const Text('Delete'),
onPressed: () { onPressed: () {
_deleteBeneficiary(context); _deleteBeneficiary(context);
}, },
@@ -84,80 +81,60 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
title: Text(AppLocalizations.of(context).beneficiarydetails), title: Text(AppLocalizations.of(context).beneficiarydetails),
), ),
body: SafeArea( body: SafeArea(
child: Stack( child: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: Column(
padding: const EdgeInsets.all(16.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, Row(
children: [ children: [
Row( CircleAvatar(
children: [ radius: 24,
CircleAvatar( backgroundColor: Colors.transparent,
radius: 24, child: getBankLogo(beneficiary.bankName, context),
backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context),
),
const SizedBox(width: 16),
Text(
beneficiary.name,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
],
), ),
const SizedBox(height: 24), const SizedBox(width: 16),
_buildDetailRow('${AppLocalizations.of(context).bankName} ', Text(
beneficiary.bankName ?? 'N/A'), beneficiary.name,
_buildDetailRow( style: const TextStyle(
'${AppLocalizations.of(context).accountNumber} ', fontSize: 20, fontWeight: FontWeight.bold),
beneficiary.accountNo),
_buildDetailRow(
'${AppLocalizations.of(context).accountType} ',
beneficiary.accountType),
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
beneficiary.ifscCode),
_buildDetailRow('${AppLocalizations.of(context).branchName} ',
beneficiary.branchName ?? 'N/A'),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// ElevatedButton.icon(
// onPressed: () {
// // Set Transaction Limit for this beneficiary
// },
// icon: const Icon(Icons.currency_rupee),
// label: const Text('Set Limit'),
// ),
ElevatedButton.icon(
onPressed: () {
// Delete beneficiary option
_showDeleteConfirmationDialog(context);
},
icon: const Icon(Icons.delete),
label: Text(AppLocalizations.of(context).delete),
),
],
), ),
], ],
), ),
), const SizedBox(height: 24),
IgnorePointer( _buildDetailRow('${AppLocalizations.of(context).bankName} ',
child: Center( beneficiary.bankName ?? 'N/A'),
child: Opacity( _buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
opacity: 0.07, // Reduced opacity beneficiary.accountNo),
child: ClipOval( _buildDetailRow('${AppLocalizations.of(context).accountType} ',
child: Image.asset( beneficiary.accountType),
'assets/images/logo.png', _buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
width: 200, // Adjust size as needed beneficiary.ifscCode),
height: 200, // Adjust size as needed _buildDetailRow('${AppLocalizations.of(context).branchName} ',
), beneficiary.branchName ?? 'N/A'),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// ElevatedButton.icon(
// onPressed: () {
// // Set Transaction Limit for this beneficiary
// },
// icon: const Icon(Icons.currency_rupee),
// label: const Text('Set Limit'),
// ),
ElevatedButton.icon(
onPressed: () {
// Delete beneficiary option
_showDeleteConfirmationDialog(context);
},
icon: const Icon(Icons.delete),
label: Text(AppLocalizations.of(context).delete),
), ),
), ],
), ),
), ],
], ),
), ),
), ),
); );

View File

@@ -21,47 +21,21 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
var service = getIt<BeneficiaryService>(); var service = getIt<BeneficiaryService>();
bool _isLoading = true; bool _isLoading = true;
List<Beneficiary> _beneficiaries = []; List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBeneficiaries(); _loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
} }
Future<void> _loadBeneficiaries() async { Future<void> _loadBeneficiaries() async {
final data = await service.fetchBeneficiaryList(); final data = await service.fetchBeneficiaryList();
setState(() { setState(() {
_beneficiaries = data; _beneficiaries = data;
_filteredBeneficiaries = data;
_isLoading = false; _isLoading = false;
}); });
} }
void _filterBeneficiaries(String query) {
setState(() {
if (query.isEmpty) {
_filteredBeneficiaries = _beneficiaries;
} else {
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
final lowerQuery = query.toLowerCase();
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
Widget _buildShimmerList() { Widget _buildShimmerList() {
return ListView.builder( return ListView.builder(
itemCount: 6, itemCount: 6,
@@ -89,43 +63,40 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
} }
Widget _buildBeneficiaryList() { Widget _buildBeneficiaryList() {
if (_filteredBeneficiaries.isEmpty) { if (_beneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
} }
return ListView.builder( return ListView.builder(
itemCount: _filteredBeneficiaries.length, itemCount: _beneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = _filteredBeneficiaries[index]; final item = _beneficiaries[index];
return Card( return ListTile(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), leading: CircleAvatar(
child: ListTile( radius: 24,
leading: CircleAvatar( backgroundColor: Colors.transparent,
radius: 24, child: getBankLogo(item.bankName, context),
backgroundColor: Colors.transparent,
child: getBankLogo(item.bankName, context),
),
title: Text(item.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.accountNo),
if (item.bankName != null && item.bankName!.isNotEmpty)
Text(
item.bankName!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BeneficiaryDetailsScreen(beneficiary: item),
),
);
},
), ),
title: Text(item.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.accountNo),
if (item.bankName != null && item.bankName!.isNotEmpty)
Text(
item.bankName!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BeneficiaryDetailsScreen(beneficiary: item),
),
);
},
); );
}, },
); );
@@ -138,45 +109,7 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries), title: Text(AppLocalizations.of(context).beneficiaries),
), ),
body: Stack( body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: FloatingActionButton( child: FloatingActionButton(

View File

@@ -56,159 +56,137 @@ class _BlockCardScreen extends State<BlockCardScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).blockCard, AppLocalizations.of(context).blockCard,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(10.0),
Padding( child: Form(
padding: const EdgeInsets.all(10.0), key: _formKey,
child: Form( child: ListView(
key: _formKey, children: [
child: ListView( const SizedBox(height: 10),
TextFormField(
controller: _cardController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) => value != null && value.length == 11
? null
: AppLocalizations.of(context).enterValidCardNumber,
),
const SizedBox(height: 24),
Row(
children: [ children: [
const SizedBox(height: 10), Expanded(
TextFormField( child: TextFormField(
controller: _cardController, controller: _cvvController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber, labelText: AppLocalizations.of(context).cvv,
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: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(color: Colors.black, width: 2),
),
), ),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
obscureText: true,
validator: (value) => value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
), ),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) => value != null && value.length == 11
? null
: AppLocalizations.of(context).enterValidCardNumber,
), ),
const SizedBox(height: 24), const SizedBox(width: 16),
Row( Expanded(
children: [ child: TextFormField(
Expanded( controller: _expiryController,
child: TextFormField( readOnly: true,
controller: _cvvController, onTap: _pickExpiryDate,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv, labelText: AppLocalizations.of(context).expiryDate,
border: const OutlineInputBorder(), suffixIcon: const Icon(Icons.calendar_today),
isDense: true, border: const OutlineInputBorder(),
filled: true, isDense: true,
fillColor: filled: true,
Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: borderSide: BorderSide(color: Colors.black, width: 2),
BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
obscureText: true,
validator: (value) =>
value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
), ),
), ),
const SizedBox(width: 16), validator: (value) => value != null && value.isNotEmpty
Expanded( ? null
child: TextFormField( : AppLocalizations.of(context).selectExpiryDate,
controller: _expiryController,
readOnly: true,
onTap: _pickExpiryDate,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).expiryDate,
suffixIcon: const Icon(Icons.calendar_today),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Colors.black, width: 2),
),
),
validator: (value) => value != null &&
value.isNotEmpty
? null
: AppLocalizations.of(context).selectExpiryDate,
),
),
],
),
const SizedBox(height: 24),
TextFormField(
controller: _phoneController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).phone,
prefixIcon: const Icon(Icons.phone),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.done,
keyboardType: TextInputType.phone,
validator: (value) => value != null && value.length >= 10
? null
: AppLocalizations.of(context).enterValidPhone,
),
const SizedBox(height: 45),
Align(
alignment: Alignment.center,
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _blockCard,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).block),
),
), ),
), ),
], ],
), ),
), const SizedBox(height: 24),
), TextFormField(
IgnorePointer( controller: _phoneController,
child: Center( decoration: InputDecoration(
child: Opacity( labelText: AppLocalizations.of(context).phone,
opacity: 0.07, // Reduced opacity prefixIcon: const Icon(Icons.phone),
child: ClipOval( border: const OutlineInputBorder(),
child: Image.asset( isDense: true,
'assets/images/logo.png', filled: true,
width: 200, // Adjust size as needed fillColor: Theme.of(context).scaffoldBackgroundColor,
height: 200, // Adjust size as needed enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.done,
keyboardType: TextInputType.phone,
validator: (value) => value != null && value.length >= 10
? null
: AppLocalizations.of(context).enterValidPhone,
),
const SizedBox(height: 45),
Align(
alignment: Alignment.center,
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _blockCard,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).block),
), ),
), ),
), ),
), ],
), ),
], ),
), ),
); );
} }

View File

@@ -9,31 +9,27 @@ class CardDetailsScreen extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text("My Cards"), title: const Text("My Cards"),
), ),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: ListView(
padding: const EdgeInsets.all(16.0), children: const [
child: ListView( CardTile(
children: const [ cardNumber: "**** **** **** 1234",
CardTile( cardNetwork: "VISA",
cardNumber: "**** **** **** 1234", cardType: "Debit Card",
cardNetwork: "VISA", validFrom: "01/22",
cardType: "Debit Card", validTo: "01/27",
validFrom: "01/22",
validTo: "01/27",
),
SizedBox(height: 16),
CardTile(
cardNumber: "**** **** **** 5678",
cardNetwork: "Mastercard",
cardType: "Debit Card",
validFrom: "07/21",
validTo: "07/26",
),
],
), ),
), SizedBox(height: 16),
], CardTile(
cardNumber: "**** **** **** 5678",
cardNetwork: "Mastercard",
cardType: "Debit Card",
validFrom: "07/21",
validTo: "07/26",
),
],
),
), ),
); );
} }
@@ -91,7 +87,7 @@ class CardTile extends StatelessWidget {
"Kangra Central Co-operative Bank", "Kangra Central Co-operative Bank",
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 15.5, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@@ -25,75 +25,57 @@ class _CardManagementScreen extends State<CardManagementScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: ListView(
children: [ children: [
ListView( CardManagementTile(
children: [ icon: Symbols.add,
CardManagementTile( label: AppLocalizations.of(context).applyDebitCard,
icon: Symbols.add, onTap: () {},
label: AppLocalizations.of(context).applyDebitCard, disabled: true, // Add this
onTap: () {},
disabled: true, // Add this
),
Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile(
icon: Symbols.remove_moderator,
label: AppLocalizations.of(context).blockUnblockCard,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BlockCardScreen(),
),
);
},
disabled: true,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile(
icon: Symbols.password_2,
label: AppLocalizations.of(context).changeCardPin,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CardPinChangeDetailsScreen(),
),
);
},
disabled: true,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile(
icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CardDetailsScreen(),
),
);
},
disabled: false,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
],
), ),
IgnorePointer( const Divider(height: 1),
child: Center( CardManagementTile(
child: Opacity( icon: Symbols.remove_moderator,
opacity: 0.07, // Reduced opacity label: AppLocalizations.of(context).blockUnblockCard,
child: ClipOval( onTap: () {
child: Image.asset( Navigator.push(
'assets/images/logo.png', context,
width: 200, // Adjust size as needed MaterialPageRoute(
height: 200, // Adjust size as needed builder: (context) => const BlockCardScreen(),
),
), ),
), );
), },
disabled: true,
), ),
const Divider(height: 1),
CardManagementTile(
icon: Symbols.password_2,
label: AppLocalizations.of(context).changeCardPin,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CardPinChangeDetailsScreen(),
),
);
},
disabled: true,
),
const Divider(height: 1),
CardManagementTile(
icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CardDetailsScreen(),
),
);
},
disabled: true,
),
const Divider(height: 1),
], ],
), ),
); );

View File

@@ -46,159 +46,137 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).cardDetails, AppLocalizations.of(context).cardDetails,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(10.0),
Padding( child: Form(
padding: const EdgeInsets.all(10.0), key: _formKey,
child: Form( child: ListView(
key: _formKey, children: [
child: ListView( const SizedBox(height: 10),
TextFormField(
controller: _cardController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) => value != null && value.length == 11
? null
: AppLocalizations.of(context).enterValidCardNumber,
),
const SizedBox(height: 24),
Row(
children: [ children: [
const SizedBox(height: 10), Expanded(
TextFormField( child: TextFormField(
controller: _cardController, controller: _cvvController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber, labelText: AppLocalizations.of(context).cvv,
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: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(color: Colors.black, width: 2),
),
), ),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
obscureText: true,
validator: (value) => value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
), ),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) => value != null && value.length == 11
? null
: AppLocalizations.of(context).enterValidCardNumber,
), ),
const SizedBox(height: 24), const SizedBox(width: 16),
Row( Expanded(
children: [ child: TextFormField(
Expanded( controller: _expiryController,
child: TextFormField( readOnly: true,
controller: _cvvController, onTap: _pickExpiryDate,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv, labelText: AppLocalizations.of(context).expiryDate,
border: const OutlineInputBorder(), suffixIcon: const Icon(Icons.calendar_today),
isDense: true, border: const OutlineInputBorder(),
filled: true, isDense: true,
fillColor: filled: true,
Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: borderSide: BorderSide(color: Colors.black, width: 2),
BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
obscureText: true,
validator: (value) =>
value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
), ),
), ),
const SizedBox(width: 16), validator: (value) => value != null && value.isNotEmpty
Expanded( ? null
child: TextFormField( : AppLocalizations.of(context).selectExpiryDate,
controller: _expiryController,
readOnly: true,
onTap: _pickExpiryDate,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).expiryDate,
suffixIcon: const Icon(Icons.calendar_today),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Colors.black, width: 2),
),
),
validator: (value) => value != null &&
value.isNotEmpty
? null
: AppLocalizations.of(context).selectExpiryDate,
),
),
],
),
const SizedBox(height: 24),
TextFormField(
controller: _phoneController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).phone,
prefixIcon: const Icon(Icons.phone),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.done,
keyboardType: TextInputType.phone,
validator: (value) => value != null && value.length >= 10
? null
: AppLocalizations.of(context).enterValidPhone,
),
const SizedBox(height: 45),
Align(
alignment: Alignment.center,
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _nextButton,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).next),
),
), ),
), ),
], ],
), ),
), const SizedBox(height: 24),
), TextFormField(
IgnorePointer( controller: _phoneController,
child: Center( decoration: InputDecoration(
child: Opacity( labelText: AppLocalizations.of(context).phone,
opacity: 0.07, // Reduced opacity prefixIcon: const Icon(Icons.phone),
child: ClipOval( border: const OutlineInputBorder(),
child: Image.asset( isDense: true,
'assets/images/logo.png', filled: true,
width: 200, // Adjust size as needed fillColor: Theme.of(context).scaffoldBackgroundColor,
height: 200, // Adjust size as needed enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.done,
keyboardType: TextInputType.phone,
validator: (value) => value != null && value.length >= 10
? null
: AppLocalizations.of(context).enterValidPhone,
),
const SizedBox(height: 45),
Align(
alignment: Alignment.center,
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _nextButton,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).next),
), ),
), ),
), ),
), ],
), ),
], ),
), ),
); );
} }

View File

@@ -46,108 +46,92 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).cardPin, AppLocalizations.of(context).cardPin,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: Form(
padding: const EdgeInsets.all(16.0), key: _formKey,
child: Form( child: Column(
key: _formKey, children: [
child: Column( TextFormField(
children: [ controller: _pinController,
TextFormField( obscureText: true,
controller: _pinController, decoration: InputDecoration(
obscureText: true, labelText: AppLocalizations.of(context).enterNewPin,
decoration: InputDecoration( border: const OutlineInputBorder(),
labelText: AppLocalizations.of(context).enterNewPin, isDense: true,
border: const OutlineInputBorder(), filled: true,
isDense: true, fillColor: Theme.of(context).scaffoldBackgroundColor,
filled: true, enabledBorder: const OutlineInputBorder(
fillColor: Theme.of(context).scaffoldBackgroundColor, borderSide: BorderSide(color: Colors.black),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterNewPin;
}
if (value.length < 4) {
return AppLocalizations.of(context).pin4Digits;
}
return null;
},
), ),
const SizedBox(height: 24), focusedBorder: const OutlineInputBorder(
TextFormField( borderSide: BorderSide(color: Colors.black, width: 2),
controller: _confirmPinController,
obscureText: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterAgain,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
validator: (value) {
if (value != _pinController.text) {
return AppLocalizations.of(context).pinsDoNotMatch;
}
return null;
},
), ),
const SizedBox(height: 45), ),
Align( keyboardType: TextInputType.number,
alignment: Alignment.center, textInputAction: TextInputAction.next,
child: SizedBox( validator: (value) {
width: 250, if (value == null || value.isEmpty) {
child: ElevatedButton( return AppLocalizations.of(context).pleaseEnterNewPin;
onPressed: _submit, }
style: ElevatedButton.styleFrom( if (value.length < 4) {
shape: const StadiumBorder(), return AppLocalizations.of(context).pin4Digits;
padding: const EdgeInsets.symmetric(vertical: 16), }
backgroundColor: Theme.of(context).primaryColor, return null;
foregroundColor: },
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).submit),
),
),
),
],
), ),
), const SizedBox(height: 24),
), TextFormField(
IgnorePointer( controller: _confirmPinController,
child: Center( obscureText: true,
child: Opacity( decoration: InputDecoration(
opacity: 0.07, // Reduced opacity labelText: AppLocalizations.of(context).enterAgain,
child: ClipOval( border: const OutlineInputBorder(),
child: Image.asset( isDense: true,
'assets/images/logo.png', filled: true,
width: 200, // Adjust size as needed fillColor: Theme.of(context).scaffoldBackgroundColor,
height: 200, // Adjust size as needed enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
validator: (value) {
if (value != _pinController.text) {
return AppLocalizations.of(context).pinsDoNotMatch;
}
return null;
},
),
const SizedBox(height: 45),
Align(
alignment: Alignment.center,
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
child: Text(AppLocalizations.of(context).submit),
), ),
), ),
), ),
), ],
), ),
], ),
), ),
); );
} }

View File

@@ -17,72 +17,55 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).chequeManagement, AppLocalizations.of(context).chequeManagement,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: ListView(
children: [ children: [
ListView( const SizedBox(height: 15),
children: [ ChequeManagementTile(
const SizedBox(height: 15), icon: Symbols.add,
ChequeManagementTile( label: AppLocalizations.of(context).requestChequeBook,
icon: Symbols.add, onTap: () {},
label: AppLocalizations.of(context).requestChequeBook,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.data_alert,
label: AppLocalizations.of(context).enquiry,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EnquiryScreen()),
);
},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.approval_delegation,
label: AppLocalizations.of(context).chequeDeposit,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.front_hand,
label: AppLocalizations.of(context).stopCheque,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.cancel_presentation,
label: AppLocalizations.of(context).revokeStop,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.payments,
label: AppLocalizations.of(context).positivePay,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
],
), ),
IgnorePointer( const Divider(height: 1),
child: Center( ChequeManagementTile(
child: Opacity( icon: Symbols.data_alert,
opacity: 0.07, // Reduced opacity label: AppLocalizations.of(context).enquiry,
child: ClipOval( onTap: () {
child: Image.asset( Navigator.push(
'assets/images/logo.png', context,
width: 200, // Adjust size as needed MaterialPageRoute(builder: (context) => const EnquiryScreen()),
height: 200, // Adjust size as needed );
), },
),
),
),
), ),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.approval_delegation,
label: AppLocalizations.of(context).chequeDeposit,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.front_hand,
label: AppLocalizations.of(context).stopCheque,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.cancel_presentation,
label: AppLocalizations.of(context).revokeStop,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.payments,
label: AppLocalizations.of(context).positivePay,
onTap: () {},
),
const Divider(height: 1),
], ],
), ),
); );

View File

@@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class CustomerInfoScreen extends StatefulWidget { class CustomerInfoScreen extends StatefulWidget {
@@ -14,7 +13,6 @@ class CustomerInfoScreen extends StatefulWidget {
class _CustomerInfoScreenState extends State<CustomerInfoScreen> { class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
late final User user = widget.user; late final User user = widget.user;
int _selectedCard = 0; // 0 for Personal Info, 1 for KYC
String _maskPrimaryId(String? primaryId) { String _maskPrimaryId(String? primaryId) {
if (primaryId == null || primaryId.length <= 4) { if (primaryId == null || primaryId.length <= 4) {
@@ -28,207 +26,82 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context) AppLocalizations.of(context)
.customerInfo .customerInfo
.replaceFirst(RegExp('\n'), ''), .replaceFirst(RegExp('\n'), ''),
),
), ),
body: SafeArea( ),
child: Stack( body: SingleChildScrollView(
children: [ physics: const AlwaysScrollableScrollPhysics(),
SingleChildScrollView( child: Padding(
physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16.0),
child: Padding( child: SafeArea(
padding: const EdgeInsets.all(16.0), child: Center(
child: Column( child: Column(
children: [ children: [
Card( const SizedBox(height: 30),
elevation: 0, CircleAvatar(
shape: RoundedRectangleBorder( radius: 50,
borderRadius: BorderRadius.circular(12), child: SvgPicture.asset(
side: BorderSide( 'assets/images/avatar_male.svg',
color: theme.colorScheme.outline.withOpacity(0.2), width: 150,
width: 1, height: 150,
), fit: BoxFit.cover,
), ),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const SizedBox(
width: 56,
height: 56,
child: CircleAvatar(
radius: 50,
child: Icon(
Symbols.person,
size: 56,
),
),
),
const SizedBox(width: 12),
// Name + mobile
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
// If you want to show the user's name instead, replace below.
user.name ?? '',
style:
theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
user.cifNumber ?? '',
style:
theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withOpacity(0.7),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
// Toggle Buttons for Personal Info and KYC
SizedBox(
width: double.infinity,
child: CupertinoSlidingSegmentedControl<int>(
groupValue: _selectedCard,
thumbColor: Theme.of(context)
.colorScheme
.onPrimary, // Set selected switch color to theme primary color
onValueChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_selectedCard = newValue;
});
}
},
children: {
0: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child: Text(
AppLocalizations.of(context).personaldetails),
),
1: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child:
Text(AppLocalizations.of(context).kycdetails),
),
},
),
),
const SizedBox(height: 16),
// Card that shows content based on the toggle
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _selectedCard == 0
? _buildPersonalInfo(theme)
: _buildKycDetails(theme),
),
),
),
],
), ),
), Padding(
), padding: const EdgeInsets.only(top: 10.0),
IgnorePointer( child: Text(
child: Center( user.name ?? '',
child: Opacity( style: TextStyle(
opacity: 0.07, // Reduced opacity fontSize: 20,
child: ClipOval( color: theme.colorScheme.onSurface,
child: Image.asset( fontWeight: FontWeight.w500,
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
), ),
), ),
), ),
), Text(
'${AppLocalizations.of(context).cif}: ${user.cifNumber ?? 'N/A'}',
style: TextStyle(
fontSize: 16,
color: theme.colorScheme.onSurfaceVariant),
),
const SizedBox(height: 30),
InfoField(
label: AppLocalizations.of(context).activeAccounts,
value: user.activeAccounts?.toString() ?? '6',
),
InfoField(
label: AppLocalizations.of(context).mobileNumber,
value: user.mobileNo ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).dateOfBirth,
value: (user.dateOfBirth != null &&
user.dateOfBirth!.length == 8)
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
: 'N/A',
), // Replace with DOB if available
InfoField(
label: AppLocalizations.of(context).branchCode,
value: user.branchId ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).branchAddress,
value: user.address ?? 'N/A',
), // Replace with Aadhar if available
InfoField(
label: AppLocalizations.of(context).primaryId,
value: _maskPrimaryId(user.primaryId),
), // Replace with PAN if available
],
), ),
], ),
),
));
}
Widget _buildPersonalInfo(ThemeData theme) {
return Column(
key: const ValueKey('personal_info'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).personaldetails,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 16), ),
InfoField(
label: AppLocalizations.of(context).activeAccounts,
value: user.activeAccounts?.toString() ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).mobileNumber,
value: user.mobileNo ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).dateOfBirth,
value: (user.dateOfBirth != null && user.dateOfBirth!.length == 8)
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
: 'N/A',
),
InfoField(
label: AppLocalizations.of(context).branchCode,
value: user.branchId ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).address,
value: user.address ?? 'N/A',
),
],
);
}
Widget _buildKycDetails(ThemeData theme) {
return Column(
key: const ValueKey('kyc_details'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).kycdetails,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
InfoField(
label: AppLocalizations.of(context).primaryId,
value: _maskPrimaryId(user.primaryId),
),
],
); );
} }
} }
@@ -250,16 +123,16 @@ class InfoField extends StatelessWidget {
children: [ children: [
Text( Text(
label, label,
style: theme.textTheme.bodySmall?.copyWith( style: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6), fontSize: 15,
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 3),
Text( Text(
value.isEmpty ? 'N/A' : value, value,
style: theme.textTheme.titleMedium?.copyWith( style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
fontWeight: FontWeight.w600,
),
), ),
], ],
), ),

View File

@@ -1,11 +1,12 @@
import 'dart:developer';
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:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/repositories/transaction_repository.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/accounts/screens/account_info_screen.dart'; import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart'; import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
import 'package:kmobile/features/accounts/screens/all_accounts_screen.dart'; import 'package:kmobile/features/accounts/screens/transaction_details_screen.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/customer_info/screens/customer_info_screen.dart'; import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
@@ -14,12 +15,12 @@ import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_screen.dart';
import 'package:kmobile/features/profile/profile_screen.dart'; import 'package:kmobile/features/profile/profile_screen.dart';
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart'; import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:kmobile/data/models/transaction.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class DashboardScreen extends StatefulWidget { class DashboardScreen extends StatefulWidget {
@@ -30,189 +31,47 @@ class DashboardScreen extends StatefulWidget {
} }
class _DashboardScreenState extends State<DashboardScreen> class _DashboardScreenState extends State<DashboardScreen>
with SingleTickerProviderStateMixin, RouteAware { with SingleTickerProviderStateMixin {
int selectedAccountIndex = 0; int selectedAccountIndex = 0;
Map<String, bool> _visibilityMap = {}; bool isVisible = false;
bool isRefreshing = false; bool isRefreshing = false;
bool isBalanceLoading = false;
bool _biometricPromptShown = false; bool _biometricPromptShown = false;
bool _txLoading = false;
List<Transaction> _transactions = [];
bool _txInitialized = false; bool _txInitialized = false;
PageController? _pageController;
final routeObserver = getIt<RouteObserver<ModalRoute<void>>>();
@override Future<void> _loadTransactions(String accountNo) async {
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
_pageController?.dispose();
_visibilityMap.clear();
super.dispose();
}
@override
void didPushNext() {
// This method is called when another route is pushed on top of this one.
// We clear the map and call setState to ensure the UI is updated
// if the user navigates back.
setState(() { setState(() {
_visibilityMap.clear(); _txLoading = true;
_transactions = [];
}); });
} try {
final repo = getIt<TransactionRepository>();
Widget _buildAccountCard(User user, bool isSelected) { final txs = await repo.fetchTransactions(accountNo);
final theme = Theme.of(context); var fiveTxns = <Transaction>[];
final bool isCardVisible = _visibilityMap[user.accountNo] ?? false; //only take the first 5 transactions
// Animated scale for the selected card if (txs.length > 5) {
final scale = isSelected ? 1.02 : 0.9; fiveTxns = txs.sublist(0, 5);
return Padding( } else {
padding: const EdgeInsets.symmetric(horizontal: 3.0), fiveTxns = txs;
child: AnimatedScale( }
duration: const Duration(milliseconds: 200), setState(() => _transactions = fiveTxns);
scale: scale, } catch (e) {
child: Container( log(accountNo, error: e);
padding: const EdgeInsets.symmetric( if (!mounted) return;
horizontal: 18, ScaffoldMessenger.of(context).showSnackBar(
vertical: 10, SnackBar(
), content: Text(
decoration: BoxDecoration( AppLocalizations.of(context).failedToLoad(e.toString()),
color: const Color(0xFF01A04C),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top section with account type and number (no refresh button here)
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getFullAccountType(user.accountType),
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
Text(
user.accountNo ?? 'N/A',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 14,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
if (isSelected) // Show logo only if card is selected
CircleAvatar(
radius: 20,
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 30,
height: 30,
fit: BoxFit.cover,
),
),
),
),
],
),
const Spacer(),
// Bottom section with balance and combined toggle/refresh
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isRefreshing && isSelected)
Expanded(child: _buildBalanceShimmer())
else
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Row(
children: [
Text(
"",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
Text(
isCardVisible
? user.currentBalance ?? '0.00'
: '*****',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
const SizedBox(width: 10), // A steady space
if (isSelected) // Only show toggle for selected card
InkWell(
onTap: () async {
if (isRefreshing)
return; // Prevent taps while refreshing
final accountNo = user.accountNo;
if (accountNo == null) return;
final bool currentVisibility =
_visibilityMap[accountNo] ?? false;
if (!currentVisibility) {
// If hidden, refresh data and then show the balance
await _refreshAccountData(context);
if (mounted) {
setState(() {
_visibilityMap[accountNo] = true;
});
}
} else {
// If visible, just hide it
setState(() {
_visibilityMap[accountNo] = false;
});
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isCardVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: theme.scaffoldBackgroundColor,
weight: 800,
),
),
),
],
),
const Spacer(),
],
), ),
), ),
), );
); } finally {
if (mounted) {
setState(() => _txLoading = false);
}
}
} }
Future<void> _refreshAccountData(BuildContext context) async { Future<void> _refreshAccountData(BuildContext context) async {
@@ -242,7 +101,8 @@ class _DashboardScreenState extends State<DashboardScreen>
return Shimmer.fromColors( return Shimmer.fromColors(
baseColor: theme.colorScheme.primary, baseColor: theme.colorScheme.primary,
highlightColor: theme.colorScheme.onPrimary, highlightColor: theme.colorScheme.onPrimary,
child: Container(height: 36, color: theme.scaffoldBackgroundColor), child: Container(
width: 200, height: 42, color: theme.scaffoldBackgroundColor),
); );
} }
@@ -287,12 +147,6 @@ class _DashboardScreenState extends State<DashboardScreen>
return AppLocalizations.of(context).termDeposit; return AppLocalizations.of(context).termDeposit;
case 'rd': case 'rd':
return AppLocalizations.of(context).recurringDeposit; return AppLocalizations.of(context).recurringDeposit;
case 'ca':
return "Current Account";
case 'cc':
return "Cash Credit Account";
case 'od':
return "Overdraft Account";
default: default:
return AppLocalizations.of(context).unknownAccount; return AppLocalizations.of(context).unknownAccount;
} }
@@ -343,19 +197,6 @@ class _DashboardScreenState extends State<DashboardScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
String customerNo = '';
String customerName = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 &&
selectedAccountIndex < authState.users.length) {
mobileNumberToPass =
authState.users[selectedAccountIndex].mobileNo ?? '';
customerNo = authState.users[selectedAccountIndex].cifNumber ?? '';
customerName = authState.users[selectedAccountIndex].name ?? '';
}
}
return BlocListener<AuthCubit, AuthState>( return BlocListener<AuthCubit, AuthState>(
listener: (context, state) async { listener: (context, state) async {
if (state is Authenticated && !_biometricPromptShown) { if (state is Authenticated && !_biometricPromptShown) {
@@ -370,67 +211,52 @@ class _DashboardScreenState extends State<DashboardScreen>
child: Scaffold( child: Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor,
leading: Padding( leading: Padding(
padding: const EdgeInsets.only(left: 10.0), padding: const EdgeInsets.only(left: 10.0),
child: Material( child: InkWell(
elevation: 4.0, borderRadius: BorderRadius.circular(20),
clipBehavior: Clip.antiAlias, onTap: () {
shape: RoundedRectangleBorder( final authState = context.read<AuthCubit>().state;
side: const BorderSide(color: Colors.white, width: 1.5), String mobileNumberToPass = '';
borderRadius: BorderRadius.circular(12.0),
), if (authState is Authenticated) {
child: Container( if (selectedAccountIndex >= 0 &&
width: 40, selectedAccountIndex < authState.users.length) {
height: 40, mobileNumberToPass =
decoration: const BoxDecoration( authState.users[selectedAccountIndex].mobileNo ?? '';
color: Colors.white, }
), }
child: Image.asset(
'assets/images/logo.png', Navigator.push(
fit: BoxFit.fill, context,
MaterialPageRoute(
builder: (context) =>
ProfileScreen(mobileNumber: mobileNumberToPass),
),
);
},
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
), ),
), ),
), ),
), ),
title: Text( title: Text(
AppLocalizations.of(context).kccBankFull.replaceAll('-', '\u2011'), AppLocalizations.of(context).kccbMobile,
textAlign: TextAlign.center, textAlign: TextAlign.left,
softWrap: true, // Explicitly allow wrapping
maxLines: 2, // Allow text to wrap to a maximum of 2 lines
style: TextStyle( style: TextStyle(
color: theme.colorScheme.onPrimary, color: theme.colorScheme.primary,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 20,
), ),
), ),
// Removed centerTitle: true to give more space for text wrapping centerTitle: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileScreen(
mobileNumber: mobileNumberToPass,
customerNo: customerNo,
customerName: customerName),
),
);
},
child: const CircleAvatar(
radius: 21,
child: Icon(
Symbols.person,
size: 30,
),
),
),
),
],
), ),
body: BlocBuilder<AuthCubit, AuthState>( body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) { builder: (context, state) {
@@ -440,30 +266,24 @@ class _DashboardScreenState extends State<DashboardScreen>
if (state is Authenticated) { if (state is Authenticated) {
final users = state.users; final users = state.users;
final currAccount = users[selectedAccountIndex]; final currAccount = users[selectedAccountIndex];
final accountType = currAccount.accountType?.toLowerCase();
final isPaymentDisabled = accountType != 'sa' &&
accountType != 'sb' &&
accountType != 'ca';
// firsttime load // firsttime load
if (!_txInitialized) { if (!_txInitialized) {
_txInitialized = true; _txInitialized = true;
WidgetsBinding.instance.addPostFrameCallback((_) {}); WidgetsBinding.instance.addPostFrameCallback((_) {
_loadTransactions(currAccount.accountNo!);
});
} }
_pageController ??= PageController(
initialPage: selectedAccountIndex,
viewportFraction: 0.75,
);
final firstName = getProcessedFirstName(currAccount.name); final firstName = getProcessedFirstName(currAccount.name);
return SingleChildScrollView( return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 16), // Added spacing
Padding( Padding(
padding: const EdgeInsets.only(left: 4.0), padding: const EdgeInsets.only(left: 8.0),
child: Text( child: Text(
"${AppLocalizations.of(context).hi} $firstName!", "${AppLocalizations.of(context).hi} $firstName!",
style: GoogleFonts.baumans().copyWith( style: GoogleFonts.baumans().copyWith(
@@ -475,76 +295,176 @@ class _DashboardScreenState extends State<DashboardScreen>
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Account Info Cards // Account Info Card
SizedBox( Container(
height: 160, padding: const EdgeInsets.symmetric(
child: PageView.builder( horizontal: 18,
clipBehavior: Clip.none, vertical: 10,
controller: _pageController,
itemCount:
users.length, // Keep this to show adjacent cards
onPageChanged: (int newIndex) async {
if (newIndex == selectedAccountIndex) return;
// Hide the balance of the old card when scrolling away
final oldAccountNo =
users[selectedAccountIndex].accountNo;
if (oldAccountNo != null) {
_visibilityMap[oldAccountNo] = false;
}
setState(() {
selectedAccountIndex = newIndex;
});
},
itemBuilder: (context, index) {
final user = users[index];
final isSelected = index == selectedAccountIndex;
return _buildAccountCard(user, isSelected);
},
), ),
), decoration: BoxDecoration(
const SizedBox(height: 8), color: Color(0xFF01A04C),
Row( borderRadius: BorderRadius.circular(16),
mainAxisAlignment: MainAxisAlignment.end, ),
children: [ child: Column(
GestureDetector( crossAxisAlignment: CrossAxisAlignment.start,
onTap: () { children: [
Navigator.push( Row(
context, children: [
MaterialPageRoute( Text(
builder: (context) => "${getFullAccountType(currAccount.accountType)}: ",
AllAccountsScreen(users: users), style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
), ),
); DropdownButton<int>(
}, value: selectedAccountIndex,
child: Text( dropdownColor: theme.colorScheme.primary,
AppLocalizations.of(context).viewall, underline: const SizedBox(),
style: TextStyle( icon: const Icon(Icons.keyboard_arrow_down),
fontSize: 14, iconEnabledColor: theme.colorScheme.onPrimary,
fontWeight: FontWeight.bold, style: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.onPrimary,
), fontSize: 18,
),
items: List.generate(users.length, (index) {
return DropdownMenuItem<int>(
value: index,
child: Text(
users[index].accountNo ?? 'N/A',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
);
}),
onChanged: (int? newIndex) async {
if (newIndex == null ||
newIndex == selectedAccountIndex) {
return;
}
if (isBalanceLoading) return;
if (isVisible) {
setState(() {
isBalanceLoading = true;
selectedAccountIndex = newIndex;
});
await Future.delayed(
const Duration(milliseconds: 200),
);
setState(() {
isBalanceLoading = false;
});
} else {
setState(() {
selectedAccountIndex = newIndex;
});
}
await _loadTransactions(
users[newIndex].accountNo!,
);
},
),
const Spacer(),
IconButton(
icon: isRefreshing
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: theme.colorScheme.onPrimary,
strokeWidth: 2,
),
)
: Icon(
Symbols.refresh,
color: theme.colorScheme.onPrimary,
),
onPressed: isRefreshing
? null
: () => _refreshAccountData(context),
tooltip: 'Refresh',
),
],
), ),
), const SizedBox(height: 15),
], Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
isRefreshing || isBalanceLoading
? _buildBalanceShimmer()
: Text(
isVisible
? currAccount.currentBalance ??
'0.00'
: '*****',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
InkWell(
onTap: () async {
if (isBalanceLoading) return;
if (!isVisible) {
setState(() {
isBalanceLoading = true;
});
await Future.delayed(
const Duration(seconds: 1),
);
setState(() {
isVisible = true;
isBalanceLoading = false;
});
} else {
setState(() {
isVisible = false;
});
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: theme.scaffoldBackgroundColor,
weight: 800,
),
),
),
],
),
const SizedBox(height: 15),
],
),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
Text( Text(
AppLocalizations.of(context).quickLinks, AppLocalizations.of(context).quickLinks,
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
const SizedBox(height: 24), const SizedBox(height: 16),
// Quick Links // Quick Links
GridView.count( GridView.count(
crossAxisCount: 3, crossAxisCount: 4,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1,
children: [ children: [
_buildQuickLink( _buildQuickLink(
Symbols.id_card, Symbols.id_card,
@@ -573,7 +493,6 @@ class _DashboardScreenState extends State<DashboardScreen>
), ),
); );
}, },
disable: isPaymentDisabled,
), ),
_buildQuickLink(Symbols.send_money, _buildQuickLink(Symbols.send_money,
AppLocalizations.of(context).fundTransfer, () { AppLocalizations.of(context).fundTransfer, () {
@@ -585,10 +504,9 @@ class _DashboardScreenState extends State<DashboardScreen>
users[selectedAccountIndex] users[selectedAccountIndex]
.accountNo!, .accountNo!,
remitterName: remitterName:
users[selectedAccountIndex].name!, users[selectedAccountIndex]
// Pass the full list of accounts .name!)));
accounts: users))); }, disable: false),
}, disable: isPaymentDisabled),
_buildQuickLink( _buildQuickLink(
Symbols.server_person, Symbols.server_person,
AppLocalizations.of(context).accountInfo, AppLocalizations.of(context).accountInfo,
@@ -612,18 +530,18 @@ class _DashboardScreenState extends State<DashboardScreen>
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
AccountStatementScreen( AccountStatementScreen(
users: users, accountNo: users[selectedAccountIndex]
selectedIndex: selectedAccountIndex, .accountNo!,
balance: users[selectedAccountIndex]
.availableBalance!,
accountType:
users[selectedAccountIndex]
.accountType!,
))); )));
}), }),
_buildQuickLink(Icons.location_pin, _buildQuickLink(Symbols.checkbook,
AppLocalizations.of(context).branchlocator, () { AppLocalizations.of(context).handleCheque, () {},
Navigator.push( disable: true),
context,
MaterialPageRoute(
builder: (context) =>
const BranchLocatorScreen()));
}, disable: false),
_buildQuickLink(Icons.group, _buildQuickLink(Icons.group,
AppLocalizations.of(context).manageBeneficiary, AppLocalizations.of(context).manageBeneficiary,
() { () {
@@ -642,24 +560,69 @@ class _DashboardScreenState extends State<DashboardScreen>
builder: (context) => builder: (context) =>
const EnquiryScreen())); const EnquiryScreen()));
}), }),
_buildQuickLink( ],
Symbols.person, ),
AppLocalizations.of(context).profile, const SizedBox(height: 5),
() {
// Recent Transactions
Text(
AppLocalizations.of(context).recentTransactions,
style: const TextStyle(fontSize: 17),
),
const SizedBox(height: 16),
if (_txLoading)
..._buildTransactionShimmer()
else if (_transactions.isNotEmpty)
..._transactions.map(
(tx) => ListTile(
leading: Icon(
tx.type == 'CR'
? Symbols.call_received
: Symbols.call_made,
color: tx.type == 'CR'
? const Color(0xFF10BB10)
: theme.colorScheme.error,
),
title: Text(
tx.date ?? '',
style: const TextStyle(fontSize: 15),
),
subtitle: Text(
tx.name != null
? (tx.name!.length > 22
? tx.name!.substring(0, 22)
: tx.name!)
: '',
style: const TextStyle(fontSize: 12),
),
trailing: Text(
"${tx.amount}",
style: const TextStyle(fontSize: 17),
),
onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ProfileScreen( builder: (_) =>
mobileNumber: mobileNumberToPass, TransactionDetailsScreen(transaction: tx),
customerNo: customerNo,
customerName: customerName),
), ),
); );
}, },
disable: false,
), ),
], )
), else
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Center(
child: Text(
AppLocalizations.of(context).noTransactions,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.outline,
),
),
),
),
], ],
), ),
), ),
@@ -674,6 +637,32 @@ class _DashboardScreenState extends State<DashboardScreen>
); );
} }
List<Widget> _buildTransactionShimmer() {
final theme = Theme.of(context);
return List.generate(3, (i) {
return ListTile(
leading: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: CircleAvatar(
radius: 12, backgroundColor: theme.scaffoldBackgroundColor),
),
title: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 10, width: 100, color: theme.scaffoldBackgroundColor),
),
subtitle: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 8, width: 60, color: theme.scaffoldBackgroundColor),
),
);
});
}
Widget _buildQuickLink( Widget _buildQuickLink(
IconData icon, IconData icon,
String label, String label,
@@ -681,35 +670,27 @@ class _DashboardScreenState extends State<DashboardScreen>
bool disable = false, bool disable = false,
}) { }) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Card( return InkWell(
elevation: 2, onTap: disable ? null : onTap,
shape: RoundedRectangleBorder( child: Column(
borderRadius: BorderRadius.circular(12.0), mainAxisSize: MainAxisSize.min,
), children: [
child: InkWell( Icon(
onTap: disable ? null : onTap, icon,
borderRadius: BorderRadius.circular(12.0), size: 30,
child: Column( color: disable
mainAxisAlignment: MainAxisAlignment.center, ? theme.colorScheme.onSurface.withOpacity(0.3)
children: [ : theme.colorScheme.primary,
Icon( grade: 200,
icon, weight: 700,
size: 30, ),
color: disable ? theme.disabledColor : theme.colorScheme.primary, const SizedBox(height: 4),
), Text(
const SizedBox(height: 4), label,
Text( textAlign: TextAlign.center,
label, style: const TextStyle(fontSize: 13),
textAlign: TextAlign.center, ),
style: theme.textTheme.titleMedium?.copyWith( ],
fontWeight: FontWeight.bold,
fontSize: 12,
color:
disable ? theme.disabledColor : theme.colorScheme.onSurface,
),
),
],
),
), ),
); );
} }

View File

@@ -1,3 +1,5 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -10,97 +12,54 @@ class EnquiryScreen extends StatefulWidget {
} }
class _EnquiryScreen extends State<EnquiryScreen> { class _EnquiryScreen extends State<EnquiryScreen> {
// Updated to launch externally and pre-fill the subject
Future<void> _launchEmailAddress(String email) async { Future<void> _launchEmailAddress(String email) async {
final Uri emailUri = Uri( final Uri emailUri = Uri(scheme: 'mailto', path: email);
scheme: 'mailto',
path: email,
query: 'subject=Enquiry', // Pre-fills the subject line
);
if (await canLaunchUrl(emailUri)) { if (await canLaunchUrl(emailUri)) {
// Use external application mode await launchUrl(emailUri);
await launchUrl(emailUri, mode: LaunchMode.externalApplication);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( debugPrint('${AppLocalizations.of(context).emailLaunchError} $email');
SnackBar(content: Text('Could not open email app for $email')),
);
} }
} }
// Updated with better error handling
Future<void> _launchPhoneNumber(String phone) async { Future<void> _launchPhoneNumber(String phone) async {
final Uri phoneUri = Uri(scheme: 'tel', path: phone); final Uri phoneUri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(phoneUri)) { if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri); await launchUrl(phoneUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( debugPrint('${AppLocalizations.of(context).dialerLaunchError} $phone');
SnackBar(content: Text('Could not open dialer for $phone')),
);
} }
} }
// Updated to launch externally
Future<void> _launchUrl(String url) async { Future<void> _launchUrl(String url) async {
final Uri uri = Uri.parse(url); final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
// Use external application mode await launchUrl(uri);
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( // Consider adding a 'urlLaunchError' key to your AppLocalizations
SnackBar(content: Text('Could not launch $url')), debugPrint('Could not launch $url');
);
} }
} }
Widget _buildContactItem(String role, String email, String phone) { Widget _buildContactItem(String role, String email, String phone) {
return Card( return Column(
elevation: 4, crossAxisAlignment: CrossAxisAlignment.start,
margin: const EdgeInsets.symmetric(vertical: 8), children: [
child: Padding( Text(role,
padding: const EdgeInsets.all(16.0), style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
child: Column( const SizedBox(height: 4),
crossAxisAlignment: CrossAxisAlignment.start, GestureDetector(
children: [ onTap: () => _launchEmailAddress(email),
Text(role, child: Text(email,
style: TextStyle( style: TextStyle(color: Theme.of(context).colorScheme.primary)),
color: Theme.of(context).colorScheme.onSurface,
fontSize: 15,
fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _launchEmailAddress(email),
child: Row(
children: [
const Icon(Icons.email),
const SizedBox(width: 8),
Expanded(
child: Text(email,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14)),
),
],
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _launchPhoneNumber(phone),
child: Row(
children: [
const Icon(Icons.phone),
const SizedBox(width: 8),
Expanded(
child: Text(phone,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14)),
),
],
),
),
],
), ),
), const SizedBox(height: 4),
GestureDetector(
onTap: () => _launchPhoneNumber(phone),
child: Text(phone,
style:
TextStyle(color: Theme.of(context).scaffoldBackgroundColor)),
),
],
); );
} }
@@ -111,93 +70,66 @@ class _EnquiryScreen extends State<EnquiryScreen> {
title: Text(AppLocalizations.of(context).enquiry), title: Text(AppLocalizations.of(context).enquiry),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: Column(
padding: const EdgeInsets.all(16.0), crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, const SizedBox(height: 20),
children: [ GestureDetector(
Card( onTap: () => _launchUrl("https://kccb.in/complaint-form"),
elevation: 4, child: Row(mainAxisSize: MainAxisSize.min, children: [
child: InkWell( Text(
onTap: () => "Complaint Form",
_launchUrl("https://kccbhp.bank.in/complaint-form/"), style: TextStyle(
child: Padding( fontSize: 17,
padding: const EdgeInsets.all(16.0), color: Theme.of(context).colorScheme.primary,
child: Row( decorationColor: Theme.of(context).colorScheme.primary,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Complaint Form",
style: TextStyle(
fontSize: 15,
color: Theme.of(context).colorScheme.primary,
),
),
Icon(
Icons.open_in_new,
color: Theme.of(context).colorScheme.primary,
size: 16.0,
),
],
),
), ),
), ),
), const SizedBox(width: 4),
const Divider(height: 32), Icon(
Text( Icons.open_in_new,
AppLocalizations.of(context).keyContacts, color: Theme.of(context).colorScheme.primary,
style: const TextStyle( size: 16.0,
fontSize: 15,
fontWeight: FontWeight.bold,
), ),
), ])),
const SizedBox(height: 16), const SizedBox(height: 40),
Expanded( Text(
child: ListView( AppLocalizations.of(context).keyContacts,
children: [ style: TextStyle(
_buildContactItem( fontSize: 17,
AppLocalizations.of(context).chairman, color: Theme.of(context).colorScheme.primary,
"chairman@kccb.in",
"01892-222677",
),
_buildContactItem(
AppLocalizations.of(context).managingDirector,
"md@kccb.in",
"01892-224969",
),
_buildContactItem(
AppLocalizations.of(context).gmWest,
"gmw@kccb.in",
"01892-223280",
),
_buildContactItem(
AppLocalizations.of(context).gmNorth,
"gmn@kccb.in",
"01892-224607",
),
],
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
), ),
// horizontal line
), ),
), Divider(color: Theme.of(context).colorScheme.outline),
], const SizedBox(height: 16),
_buildContactItem(
AppLocalizations.of(context).chairman,
"chairman@kccb.in",
"01892-222677",
),
const SizedBox(height: 16),
_buildContactItem(
AppLocalizations.of(context).managingDirector,
"md@kccb.in",
"01892-224969",
),
const SizedBox(height: 16),
_buildContactItem(
AppLocalizations.of(context).gmWest,
"gmw@kccb.in",
"01892-223280",
),
const SizedBox(height: 16),
_buildContactItem(
AppLocalizations.of(context).gmNorth,
"gmn@kccb.in",
"01892-224607",
),
],
),
), ),
); );
} }

View File

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

View File

@@ -43,64 +43,62 @@ 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,
),
);
} }
Future<void> _loadLimit() async { if (_isAmountOverLimit != isOverLimit) {
setState(() { setState(() {
_isLoadingLimit = true; _isAmountOverLimit = isOverLimit;
}); });
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() {
@@ -362,176 +360,157 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')), title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
), ),
body: SafeArea( body: SafeArea(
child: Stack( child: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: Form(
padding: const EdgeInsets.all(16.0), key: _formKey,
child: Form( child: Column(
key: _formKey, crossAxisAlignment: CrossAxisAlignment.start,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, // Debit Account (User)
children: [ Text(
// Debit Account (User) loc.debitFrom,
Text( style: Theme.of(context).textTheme.titleSmall,
loc.debitFrom,
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 (Beneficiary)
Text(
AppLocalizations.of(context).creditedTo,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: getBankLogo(
widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
),
const SizedBox(height: 24),
if (!widget.isOwnBank) ...[
// Transaction Mode Selection
Text(
AppLocalizations.of(context).selectTransactionType,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: ToggleButtons(
isSelected: [
_selectedMode == TransactionMode.neft,
_selectedMode == TransactionMode.rtgs,
_selectedMode == TransactionMode.imps,
],
onPressed: (index) {
setState(() {
_selectedMode = TransactionMode.values[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor:
Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent,
splashColor: Theme.of(context).colorScheme.primary,
highlightColor: Theme.of(context).colorScheme.primary,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).neft),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).rtgs),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).imps),
),
],
),
),
const SizedBox(height: 24),
],
//Remarks
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return loc.amountRequired;
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return loc.validAmount;
}
return null;
},
),
const SizedBox(height: 8),
if (_isLoadingLimit)
Text(AppLocalizations.of(context).fetchingDailyLimit),
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: Text(AppLocalizations.of(context).proceed),
),
),
const SizedBox(height: 10),
],
), ),
), Card(
), elevation: 0,
IgnorePointer( margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Center( child: ListTile(
child: Opacity( leading: Image.asset(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png', 'assets/images/logo.png',
width: 200, // Adjust size as needed width: 40,
height: 200, // Adjust size as needed height: 40,
), ),
title: Text(widget.remitterName),
subtitle: Text(widget.debitAccountNo),
), ),
), ),
), const SizedBox(height: 24),
// Credit Account (Beneficiary)
Text(
AppLocalizations.of(context).creditedTo,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading:
getBankLogo(widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
),
const SizedBox(height: 24),
if (!widget.isOwnBank) ...[
// Transaction Mode Selection
Text(
AppLocalizations.of(context).selectTransactionType,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: ToggleButtons(
isSelected: [
_selectedMode == TransactionMode.neft,
_selectedMode == TransactionMode.rtgs,
_selectedMode == TransactionMode.imps,
],
onPressed: (index) {
setState(() {
_selectedMode = TransactionMode.values[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor: Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent,
splashColor: Theme.of(context).colorScheme.primary,
highlightColor: Theme.of(context).colorScheme.primary,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).neft),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).rtgs),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).imps),
),
],
),
),
const SizedBox(height: 24),
],
//Remarks
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return loc.amountRequired;
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return loc.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 Spacer(),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).proceed),
),
),
const SizedBox(height: 10),
],
), ),
], ),
), ),
), ),
); );

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/cooldown.dart';
import 'package:kmobile/widgets/bank_logos.dart'; import 'package:kmobile/widgets/bank_logos.dart';
import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart';
@@ -28,22 +27,11 @@ class _FundTransferBeneficiaryScreenState
var service = getIt<BeneficiaryService>(); var service = getIt<BeneficiaryService>();
bool _isLoading = true; bool _isLoading = true;
List<Beneficiary> _beneficiaries = []; List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBeneficiaries(); _loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
} }
Future<void> _loadBeneficiaries() async { Future<void> _loadBeneficiaries() async {
@@ -54,25 +42,10 @@ class _FundTransferBeneficiaryScreenState
? b.bankName!.toLowerCase().contains('kangra central') ? b.bankName!.toLowerCase().contains('kangra central')
: !b.bankName!.toLowerCase().contains('kangra central')) : !b.bankName!.toLowerCase().contains('kangra central'))
.toList(); .toList();
_filteredBeneficiaries = _beneficiaries;
_isLoading = false; _isLoading = false;
}); });
} }
void _filterBeneficiaries(String query) {
setState(() {
if (query.isEmpty) {
_filteredBeneficiaries = _beneficiaries;
} else {
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
final lowerQuery = query.toLowerCase();
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
Widget _buildShimmerList() { Widget _buildShimmerList() {
return ListView.builder( return ListView.builder(
itemCount: 6, itemCount: 6,
@@ -100,84 +73,46 @@ class _FundTransferBeneficiaryScreenState
} }
Widget _buildBeneficiaryList() { Widget _buildBeneficiaryList() {
if (_filteredBeneficiaries.isEmpty) { if (_beneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
} }
return ListView.builder( return ListView.builder(
itemCount: _filteredBeneficiaries.length, itemCount: _beneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final beneficiary = _filteredBeneficiaries[index]; final beneficiary = _beneficiaries[index];
return ListTile(
// --- Cooldown Logic --- leading: CircleAvatar(
bool isCoolingDown = false; radius: 24,
if (beneficiary.createdAt != null) { backgroundColor: Colors.transparent,
final sixtyMinutesAgo = child: getBankLogo(beneficiary.bankName, context),
DateTime.now().subtract(const Duration(minutes: 60));
isCoolingDown = beneficiary.createdAt!.isAfter(sixtyMinutesAgo);
}
// --- End of Cooldown Logic ---
// By wrapping the ListTile in an Opacity widget, we can make it look
// disabled while ensuring the onTap callback still works.
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Opacity(
opacity: isCoolingDown ? 0.5 : 1.0,
child: ListTile(
// REMOVED the 'enabled' property from here.
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context),
),
title: Text(beneficiary.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(beneficiary.accountNo),
if (beneficiary.bankName != null &&
beneficiary.bankName!.isNotEmpty)
Text(
beneficiary.bankName!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
trailing: isCoolingDown
? CooldownTimer(
createdAt: beneficiary.createdAt!,
onTimerFinish: () {
setState(() {});
},
)
: null,
onTap: () {
if (isCoolingDown) {
// This will now execute correctly on tap
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Beneficiary will be enabled after the cooldown period.'),
behavior: SnackBarBehavior.floating,
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferAmountScreen(
debitAccountNo: widget.creditAccountNo,
creditBeneficiary: beneficiary,
remitterName: widget.remitterName,
isOwnBank: widget.isOwnBank,
),
),
);
}
},
),
), ),
title: Text(beneficiary.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(beneficiary.accountNo),
if (beneficiary.bankName != null &&
beneficiary.bankName!.isNotEmpty)
Text(
beneficiary.bankName!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferAmountScreen(
debitAccountNo: widget.creditAccountNo,
creditBeneficiary: beneficiary,
remitterName: widget.remitterName,
isOwnBank: widget.isOwnBank,
),
),
);
},
); );
}, },
); );
@@ -189,45 +124,7 @@ class _FundTransferBeneficiaryScreenState
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries), title: Text(AppLocalizations.of(context).beneficiaries),
), ),
body: Stack( body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
); );
} }
} }

View File

@@ -1,129 +1,63 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.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:material_symbols_icons/symbols.dart'; import 'package:material_symbols_icons/symbols.dart';
import '../../../l10n/app_localizations.dart'; // Keep localizations import '../../../l10n/app_localizations.dart';
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
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
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
// 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 body: ListView(
body: BlocBuilder<AuthCubit, AuthState>( children: [
builder: (context, state) { FundTransferManagementTile(
return Stack( icon: Symbols.input_circle,
children: [ label: AppLocalizations.of(context).ownBank,
Padding( onTap: () {
padding: const EdgeInsets.symmetric(vertical: 16.0), Navigator.push(
child: Column( context,
crossAxisAlignment: CrossAxisAlignment.stretch, MaterialPageRoute(
children: [ builder: (context) => FundTransferBeneficiaryScreen(
Expanded( creditAccountNo: creditAccountNo,
child: FundTransferManagementTile( remitterName: remitterName,
icon: Symbols.person, isOwnBank: true,
label: "Self Pay",
subtitle:
AppLocalizations.of(context).ftselfpaysubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
FundTransferSelfAccountsScreen(
debitAccountNo: creditAccountNo,
remitterName: remitterName,
accounts: accounts,
),
),
);
},
disable: state is! Authenticated,
),
),
const SizedBox(height: 16),
Expanded(
child: FundTransferManagementTile(
icon: Symbols.input_circle,
label: AppLocalizations.of(context).ownBank,
subtitle: AppLocalizations.of(context).ftownsubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: true,
),
),
);
},
),
),
const SizedBox(height: 16),
Expanded(
child: FundTransferManagementTile(
icon: Symbols.output_circle,
label: AppLocalizations.of(context).outsideBank,
subtitle:
AppLocalizations.of(context).ftoutsidesubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: false,
),
),
);
},
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
), ),
), ),
), );
], },
); ),
}, const Divider(height: 1),
FundTransferManagementTile(
icon: Symbols.output_circle,
label: AppLocalizations.of(context).outsideBank,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: false,
),
),
);
},
),
const Divider(height: 1),
],
), ),
); );
} }
@@ -132,7 +66,6 @@ class FundTransferScreen extends StatelessWidget {
class FundTransferManagementTile extends StatelessWidget { class FundTransferManagementTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final String? subtitle;
final VoidCallback onTap; final VoidCallback onTap;
final bool disable; final bool disable;
@@ -140,65 +73,18 @@ class FundTransferManagementTile extends StatelessWidget {
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
this.subtitle,
required this.onTap, required this.onTap,
this.disable = false, this.disable = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); return ListTile(
return Card( leading: Icon(icon),
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), title: Text(label),
shape: RoundedRectangleBorder( trailing: const Icon(Symbols.arrow_right, size: 20),
borderRadius: BorderRadius.circular(12.0), onTap: onTap,
), enabled: !disable,
elevation: 4, // Add some elevation for better visual separation
child: InkWell(
onTap:
disable ? null : onTap, // Disable InkWell if the tile is disabled
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 48, // Make icon larger
color:
disable ? theme.disabledColor : theme.colorScheme.primary,
),
const SizedBox(height: 12),
Text(
label,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: disable
? theme.disabledColor
: theme.colorScheme.onSurface,
),
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
subtitle!,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: disable
? theme.disabledColor
: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
),
),
); );
} }
} }

View File

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

View File

@@ -1,262 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/data/models/transfer.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/di/injection.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/widgets/bank_logos.dart';
class FundTransferSelfAmountScreen extends StatefulWidget {
final String debitAccountNo;
final User creditAccount;
final String remitterName;
const FundTransferSelfAmountScreen({
super.key,
required this.debitAccountNo,
required this.creditAccount,
required this.remitterName,
});
@override
State<FundTransferSelfAmountScreen> createState() =>
_FundTransferSelfAmountScreenState();
}
class _FundTransferSelfAmountScreenState
extends State<FundTransferSelfAmountScreen> {
final _formKey = GlobalKey<FormState>();
final _amountController = TextEditingController();
final _remarksController = TextEditingController();
// --- Limit Checking Variables ---
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
bool _isAmountOverLimit = false;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
@override
void initState() {
super.initState();
_loadLimit(); // Fetch the daily limit
_amountController
.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: Stack(
children: [
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),
],
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
),
);
}
}

View File

@@ -116,62 +116,36 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(6, (i) { children: List.generate(6, (i) {
return Container( return Container(
width: 50, width: 32,
height: 60, margin: const EdgeInsets.symmetric(horizontal: 8),
margin: const EdgeInsets.symmetric(horizontal: 6), child: TextField(
child: Stack( controller: _controllers[i],
alignment: Alignment.center, focusNode: _focusNodes[i],
children: [ keyboardType: TextInputType.number,
TextField( textAlign: TextAlign.center,
controller: _controllers[i], maxLength: 1,
focusNode: _focusNodes[i], obscureText: true,
keyboardType: TextInputType.number, obscuringCharacter: '*',
textAlign: TextAlign.center, decoration: InputDecoration(
maxLength: 1, counterText: '',
style: const TextStyle( filled: true,
color: Colors.transparent, fillColor: Theme.of(context).primaryColorLight,
fontSize: 24, border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2,
), ),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Colors.grey[200],
contentPadding:
const EdgeInsets.symmetric(vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey[400]!,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey[400]!,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2.5,
),
),
),
onChanged: (val) => _onOtpChanged(i, val),
), ),
if (_controllers[i].text.isNotEmpty) focusedBorder: OutlineInputBorder(
Container( borderRadius: BorderRadius.circular(12),
width: 16, borderSide: BorderSide(
height: 16, color: theme.colorScheme.primary,
decoration: BoxDecoration( width: 2.5,
color: Colors.black87,
shape: BoxShape.circle,
),
), ),
], ),
),
onChanged: (val) => _onOtpChanged(i, val),
), ),
); );
}), }),

View File

@@ -143,20 +143,6 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
), ),
), ),
), ),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
], ],
), ),
), ),

View File

@@ -1,120 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/limit_service.dart';
import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart';
class ChangeLimitOTPScreen extends StatefulWidget {
final String newLimit;
final String mobileNumber;
// ignore: use_key_in_widget_constructors
const ChangeLimitOTPScreen({
required this.newLimit,
required this.mobileNumber,
});
@override
State<ChangeLimitOTPScreen> createState() => _ChangeLimitOTPScreenState();
}
class _ChangeLimitOTPScreenState extends State<ChangeLimitOTPScreen> {
bool _isLoading = true;
final TextEditingController otpController = TextEditingController();
@override
void initState() {
super.initState();
// Simulate OTP sending delay
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_isLoading = false;
});
});
}
final limitService = getIt<LimitService>();
Future<void> _validateOTP() async {
try {
await limitService.validateOtp(
otp: otpController.text,
mobileNumber: widget.mobileNumber,
);
// If OTP is valid, then change the limit
limitService.editLimit(
double.parse(widget.newLimit),
);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Limit has been Changed"),
));
// Navigate back to profile or login
Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)),
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
AppLocalizations.of(context).otpSent,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
TextFormField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterOTP,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateOTP,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).validateOTP),
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:kmobile/di/injection.dart';
import '../../l10n/app_localizations.dart';
class ChangeMpinScreen extends StatefulWidget {
const ChangeMpinScreen({super.key});
@override
State<ChangeMpinScreen> createState() => _ChangeMpinScreenState();
}
class _ChangeMpinScreenState extends State<ChangeMpinScreen> {
@override
void initState() {
super.initState();
// Start the flow after the widget is built
WidgetsBinding.instance.addPostFrameCallback((_) {
_startChangeMpin();
});
}
void _startChangeMpin() async {
final loc = AppLocalizations.of(context);
// Step 1: Verify old PIN
final oldPinVerified = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.enter,
disableBiometric: true,
customTitle: loc.enterOldMpin,
onCompleted: (oldPin) => _verifyOldPin(oldPin),
),
),
);
if (oldPinVerified != true) {
if (mounted) Navigator.of(context).pop(false);
return;
}
// Step 2 & 3: Set new PIN (which will internally navigate to confirm)
// The onCompleted will be called after both set and confirm succeed
final success = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
customTitle: loc.enterNewMpin,
customConfirmTitle: loc.confirmNewMpin,
onCompleted: (newPin) async {
// This is called after confirm succeeds and PIN is saved
if (context.mounted) {
Navigator.of(context).pop(true);
}
},
),
),
);
if (mounted) {
Navigator.of(context).pop(success == true);
}
}
Future<void> _verifyOldPin(String oldPin) async {
final storage = getIt<SecureStorage>();
final storedPin = await storage.read('mpin');
if (storedPin == int.tryParse(oldPin)) {
// Old PIN is correct
if (mounted) {
Navigator.of(context).pop(true);
}
} else {
// This shouldn't happen as MPinScreen handles validation
if (mounted) {
Navigator.of(context).pop(false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).changeMpin),
centerTitle: true,
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
}

View File

@@ -70,58 +70,40 @@ class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)), appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: _isLoading
padding: const EdgeInsets.all(16.0), ? const Center(child: CircularProgressIndicator())
child: _isLoading : Column(
? const Center(child: CircularProgressIndicator()) crossAxisAlignment: CrossAxisAlignment.center,
: Column( children: [
crossAxisAlignment: CrossAxisAlignment.center, Text(
children: [ AppLocalizations.of(context).otpSent,
Text( textAlign: TextAlign.center,
AppLocalizations.of(context).otpSent, style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
TextFormField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterOTP,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateOTP,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).validateOTP),
),
),
],
), ),
), const SizedBox(height: 24),
IgnorePointer( TextFormField(
child: Center( controller: otpController,
child: Opacity( keyboardType: TextInputType.number,
opacity: 0.07, // Reduced opacity decoration: InputDecoration(
child: ClipOval( labelText: AppLocalizations.of(context).enterOTP,
child: Image.asset( border: const OutlineInputBorder(),
'assets/images/logo.png', ),
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
), ),
), const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateOTP,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).validateOTP),
),
),
],
), ),
),
),
],
), ),
); );
} }

View File

@@ -90,85 +90,67 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
return Scaffold( return Scaffold(
appBar: appBar:
AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)), AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16),
Padding( child: Form(
padding: const EdgeInsets.all(16), key: _formKey,
child: Form( child: Column(
key: _formKey, children: [
child: Column( TextFormField(
children: [ controller: currentPasswordController,
TextFormField( obscureText: !_showCurrentPassword,
controller: currentPasswordController, decoration: InputDecoration(
obscureText: !_showCurrentPassword, labelText: AppLocalizations.of(context).currentpwd,
decoration: InputDecoration( suffixIcon: IconButton(
labelText: AppLocalizations.of(context).currentpwd, icon: Icon(_showCurrentPassword
suffixIcon: IconButton( ? Icons.visibility
icon: Icon(_showCurrentPassword : Icons.visibility_off),
? Icons.visibility onPressed: () => setState(
: Icons.visibility_off), () => _showCurrentPassword = !_showCurrentPassword),
onPressed: () => setState(
() => _showCurrentPassword = !_showCurrentPassword),
),
),
validator: validateCurrentPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: newPasswordController,
obscureText: !_showNewPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).newpwd,
suffixIcon: IconButton(
icon: Icon(_showNewPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showNewPassword = !_showNewPassword),
),
),
validator: validateNewPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: !_showConfirmPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmpwd,
suffixIcon: IconButton(
icon: Icon(_showConfirmPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showConfirmPassword = !_showConfirmPassword),
),
),
validator: validateConfirmPassword,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _proceed,
child: Text(AppLocalizations.of(context).proceed),
),
],
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
), ),
), ),
validator: validateCurrentPassword,
), ),
), const SizedBox(height: 16),
TextFormField(
controller: newPasswordController,
obscureText: !_showNewPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).newpwd,
suffixIcon: IconButton(
icon: Icon(_showNewPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () =>
setState(() => _showNewPassword = !_showNewPassword),
),
),
validator: validateNewPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: !_showConfirmPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmpwd,
suffixIcon: IconButton(
icon: Icon(_showConfirmPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showConfirmPassword = !_showConfirmPassword),
),
),
validator: validateConfirmPassword,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _proceed,
child: Text(AppLocalizations.of(context).proceed),
),
],
), ),
], ),
), ),
); );
} }

View File

@@ -2,13 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:kmobile/api/services/limit_service.dart'; import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/profile/change_limit_otp_screen.dart';
import 'package:kmobile/l10n/app_localizations.dart'; import 'package:kmobile/l10n/app_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DailyLimitScreen extends StatefulWidget { class DailyLimitScreen extends StatefulWidget {
final String mobileNumber; const DailyLimitScreen({super.key});
const DailyLimitScreen({super.key, required this.mobileNumber});
@override @override
State<DailyLimitScreen> createState() => _DailyLimitScreenState(); State<DailyLimitScreen> createState() => _DailyLimitScreenState();
} }
@@ -18,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
@@ -44,192 +42,187 @@ 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: () async {
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 {
try {
await service.getOtpTLimit(
mobileNumber: widget.mobileNumber);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeLimitOTPScreen(
newLimit: value.toString(),
mobileNumber: widget.mobileNumber,
),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Error: $e"),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.error,
),
);
}
}
},
child: Text(localizations.save),
),
], ],
); decoration: InputDecoration(
}, labelText: localizations.limitAmount,
); prefixText: '',
border: const OutlineInputBorder(),
if (newLimit != null) {
_loadlimits();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Limit Updated"),
behavior: SnackBarBehavior.floating,
),
);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
final localizations = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(localizations.dailylimit),
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
_currentLimit = limit?.dailyLimit;
_spentAmount = limit?.usedLimit;
final localizations = AppLocalizations.of(context);
final theme = Theme.of(context);
final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final remainingLimit =
_currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
return Scaffold(
appBar: AppBar(
title: Text(localizations.dailylimit),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
localizations.currentDailyLimit,
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
const SizedBox(height: 16),
Text(
_currentLimit == null
? localizations.noLimitSet
: formatCurrency.format(_currentLimit),
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _currentLimit == null
? theme.colorScheme.secondary
: theme.colorScheme.primary,
),
),
if (_currentLimit != null) ...[
const SizedBox(height: 24),
Text(
"Remaining Limit Today", // This should be localized
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
formatCurrency.format(remainingLimit),
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: remainingLimit > 0
? Colors.green
: theme.colorScheme.error,
),
),
],
const SizedBox(height: 48),
if (_currentLimit == null)
ElevatedButton.icon(
onPressed: _showAddOrEditLimitDialog,
icon: const Icon(Icons.add_circle_outline),
label: Text(localizations.addLimit),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
textStyle: theme.textTheme.titleMedium,
),
)
else
Column(
children: [
ElevatedButton.icon(
onPressed: _showAddOrEditLimitDialog,
icon: const Icon(Icons.edit_outlined),
label: Text(localizations.editLimit),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
textStyle: theme.textTheme.titleMedium,
),
),
const SizedBox(height: 16),
],
),
],
), ),
), ),
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,
), ),
); );
} }
} }
void _removeLimit() {
setState(() {
_currentLimit = null;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
final localizations = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(localizations.dailylimit),
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
_currentLimit = limit?.dailyLimit;
_spentAmount = limit?.usedLimit;
final localizations = AppLocalizations.of(context);
final theme = Theme.of(context);
final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final remainingLimit = _currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
return Scaffold(
appBar: AppBar(
title: Text(localizations.dailylimit),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
localizations.currentDailyLimit,
style: theme.textTheme.headlineSmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
),
),
const SizedBox(height: 16),
Text(
_currentLimit == null
? localizations.noLimitSet
: formatCurrency.format(_currentLimit),
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
color: _currentLimit == null
? theme.colorScheme.secondary
: theme.colorScheme.primary,
),
),
if (_currentLimit != null) ...[
const SizedBox(height: 24),
Text(
"Remaining Limit Today", // This should be localized
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
formatCurrency.format(remainingLimit),
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: remainingLimit > 0
? Colors.green
: theme.colorScheme.error,
),
),
],
const SizedBox(height: 48),
if (_currentLimit == null)
ElevatedButton.icon(
onPressed: _showAddOrEditLimitDialog,
icon: const Icon(Icons.add_circle_outline),
label: Text(localizations.addLimit),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
textStyle: theme.textTheme.titleMedium,
),
)
else
Column(
children: [
ElevatedButton.icon(
onPressed: _showAddOrEditLimitDialog,
icon: const Icon(Icons.edit_outlined),
label: Text(localizations.editLimit),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24, vertical: 12),
textStyle: theme.textTheme.titleMedium,
),
),
const SizedBox(height: 16),
// TextButton.icon(
// onPressed: _removeLimit,
// icon: const Icon(Icons.remove_circle_outline),
// label: Text(localizations.removeLimit),
// style: TextButton.styleFrom(
// foregroundColor: theme.colorScheme.error,
// ),
// ),
],
),
],
),
),
),
);
}
}

View File

@@ -20,69 +20,43 @@ class PreferenceScreen extends StatelessWidget {
), ),
body: BlocBuilder<ThemeCubit, ThemeState>( body: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) { builder: (context, state) {
return Stack( return ListView(
children: [ children: [
ListView( //Set Prefered Username
padding: // ListTile(
const EdgeInsets.symmetric(horizontal: 16, vertical: 12), // leading: const Icon(Icons.person),
children: [ // title: const Text("Set Prefered Username"),
// Language Selection // onTap: () {
Card( // }),
margin: const EdgeInsets.only(bottom: 10), // Language Selection
child: ListTile( ListTile(
leading: const Icon(Icons.language), leading: const Icon(Icons.language),
title: Text(loc.language), title: Text(loc.language),
trailing: const Icon(Icons.chevron_right), onTap: () {
onTap: () { showDialog(
showDialog( context: context,
context: context, builder: (_) => const LanguageDialog(),
builder: (_) => const LanguageDialog(), );
); },
},
),
),
//Theme Mode Switch (Light/Dark)
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showThemeModeDialog(context);
},
),
),
//Color_Theme_Selection
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (_) => const ColorThemeDialog(),
);
}),
),
],
), ),
IgnorePointer( //Theme Mode Switch (Light/Dark)
child: Center( ListTile(
child: Opacity( leading: const Icon(Icons.brightness_6),
opacity: 0.07, // Reduced opacity title: Text(AppLocalizations.of(context).themeMode),
child: ClipOval( onTap: () {
child: Image.asset( showThemeModeDialog(context);
'assets/images/logo.png', },
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
), ),
//Color_Theme_Selection
ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
onTap: () {
showDialog(
context: context,
builder: (_) => const ColorThemeDialog(),
);
}),
], ],
); );
}, },

View File

@@ -2,26 +2,20 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:kmobile/data/repositories/auth_repository.dart'; import 'package:kmobile/data/repositories/auth_repository.dart';
import 'package:kmobile/features/profile/change_password/change_password_screen.dart';
import 'package:kmobile/features/profile/daily_transaction_limit.dart'; import 'package:kmobile/features/profile/daily_transaction_limit.dart';
import 'package:kmobile/features/profile/logout_dialog.dart'; import 'package:kmobile/features/profile/logout_dialog.dart';
import 'package:kmobile/features/profile/security_settings_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../di/injection.dart'; import '../../di/injection.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import 'package:kmobile/features/profile/preferences/preference_screen.dart'; import 'package:kmobile/features/profile/preferences/preference_screen.dart';
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
final String mobileNumber; final String mobileNumber;
final String customerNo; const ProfileScreen({super.key, required this.mobileNumber});
final String customerName;
const ProfileScreen(
{super.key,
required this.mobileNumber,
required this.customerNo,
required this.customerName});
@override @override
State<ProfileScreen> createState() => _ProfileScreenState(); State<ProfileScreen> createState() => _ProfileScreenState();
@@ -36,14 +30,15 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
Future<String> _getAppVersion() async { Future<String> _getAppVersion() async {
return 'Version 1.0.1 (1))'; final PackageInfo info = await PackageInfo.fromPlatform();
return 'Version ${info.version} (${info.buildNumber})';
} }
Future<void> _loadBiometricStatus() async { Future<void> _loadBiometricStatus() async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final enabled = await storage.read('biometric_enabled'); final isEnabled = await storage.read('biometric_enabled');
setState(() { setState(() {
_isBiometricEnabled = enabled ?? false; _isBiometricEnabled = isEnabled == true;
}); });
} }
@@ -62,17 +57,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) { if (!canCheck) {
if (mounted) { // Optional: Show a snackbar or dialog if biometrics are not available
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: content: Text(AppLocalizations.of(context).biometricsNotAvailable)),
Text(AppLocalizations.of(context).biometricsNotAvailable)), );
);
}
return; return;
} }
if (enable) { if (enable) {
// Show "Enable" dialog
final optIn = await showDialog<bool>( final optIn = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -102,31 +96,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
); );
if (didAuth) { if (didAuth) {
await storage.write('biometric_enabled', true); await storage.write('biometric_enabled', 'true');
if (mounted) { setState(() {
setState(() { _isBiometricEnabled = true;
_isBiometricEnabled = true; });
});
}
} else {
// Authentication failed, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
} }
} catch (e) { } catch (e) {
// Handle exceptions, reload state to ensure consistency // Handle authentication errors
if (mounted) {
await _loadBiometricStatus();
}
}
} else {
// User cancelled, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
} }
} }
} else { } else {
// Show "Disable" dialog
final optOut = await showDialog<bool>( final optOut = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -147,17 +127,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
); );
if (optOut == true) { if (optOut == true) {
await storage.write('biometric_enabled', false); await storage.write('biometric_enabled', 'false');
if (mounted) { setState(() {
setState(() { _isBiometricEnabled = false;
_isBiometricEnabled = false; });
});
}
} else {
// User cancelled, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
} }
} }
} }
@@ -165,283 +138,135 @@ class _ProfileScreenState extends State<ProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(loc.profile), title: Text(loc.profile), // Localized "Profile"
elevation: 0,
), ),
body: Stack( body: ListView(
children: [ children: [
ListView( ListTile(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), leading: const Icon(Icons.settings),
children: [ title: Text(loc.preferences),
// ===== Profile Header ===== onTap: () {
Card( Navigator.push(
child: Padding( context,
padding: const EdgeInsets.all(16.0), MaterialPageRoute(
child: Row( builder: (context) => const PreferenceScreen()),
children: [ );
// Avatar },
Container( ),
width: 56, ListTile(
height: 56, leading: const Icon(Icons.currency_rupee),
child: const CircleAvatar( title: Text(AppLocalizations.of(context).dailylimit),
radius: 50, onTap: () {
child: Icon( Navigator.push(
Symbols.person, context,
size: 56, MaterialPageRoute(
), builder: (context) => const DailyLimitScreen()),
), );
), },
const SizedBox(width: 12), ),
// Name + mobile SwitchListTile(
Expanded( title: Text(AppLocalizations.of(context).enableFingerprintLogin),
child: Column( value: _isBiometricEnabled,
crossAxisAlignment: CrossAxisAlignment.start, onChanged: (bool value) {
children: [ _handleBiometricToggle(value);
Text( },
// If you want to show the user's name instead, replace below. secondary: const Icon(Icons.fingerprint),
widget.customerName, ),
style: theme.textTheme.titleLarge?.copyWith( ListTile(
fontWeight: FontWeight.w600, leading: const Icon(Icons.password),
), title: Text(loc.changeLoginPassword),
), onTap: () {
const SizedBox(height: 4), Navigator.push(
Text( context,
widget.customerNo, MaterialPageRoute(
style: theme.textTheme.bodyMedium?.copyWith( builder: (context) => ChangePasswordScreen(
color: theme.colorScheme.onSurface mobileNumber: widget.mobileNumber,
.withOpacity(0.7), )),
), );
), },
], ),
), // ListTile(
), // leading: const Icon(Icons.password),
], // title: const Text("Manage TPIN"),
), // onTap: () async {
), // },
), // ),
// ListTile(
const SizedBox(height: 16), // leading: const Icon(Icons.password),
// title: const Text("Change Login MPIN"),
// ===== Section: Settings ===== // onTap: () async {
Padding( // },
padding: const EdgeInsets.symmetric(horizontal: 8.0), // ),
child: Text( ListTile(
"Settings", leading: const Icon(Icons.smartphone),
style: theme.textTheme.labelLarge?.copyWith( title: const Text("App Version"),
fontWeight: FontWeight.w600, trailing: FutureBuilder<String>(
letterSpacing: 0.2, future: _getAppVersion(),
), builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
), if (snapshot.connectionState == ConnectionState.waiting) {
), // Show a loading indicator while waiting for the future to complete
const SizedBox(height: 8), return const CircularProgressIndicator();
} else if (snapshot.hasError) {
_SectionTile( return const Text("Error");
leadingIcon: Icons.settings, } else {
title: loc.preferences, // Display the version number once the future is complete
onTap: () { return Text(
Navigator.push( snapshot.data ?? "N/A",
context, selectionColor: const Color(0xFFFFFFFF),
MaterialPageRoute( );
builder: (context) => const PreferenceScreen(), }
},
),
),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: Text(AppLocalizations.of(context).logout),
onTap: () async {
final shouldExit = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).logout),
content: Text(AppLocalizations.of(context).logoutCheck),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).no),
), ),
); TextButton(
}, onPressed: () => Navigator.of(context).pop(true),
), child: Text(AppLocalizations.of(context).yes),
_SectionTile(
leadingIcon: Icons.security,
title: loc.securitySettings,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecuritySettingsScreen(
mobileNumber: widget.mobileNumber,
),
), ),
); ],
},
),
_SectionTile(
leadingIcon: Icons.currency_rupee,
title: loc.dailylimit,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DailyLimitScreen(mobileNumber: widget.mobileNumber),
),
);
},
),
Card(
child: SwitchListTile(
title: Text(loc.enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
), ),
), );
const SizedBox(height: 16), if (shouldExit == true) {
const Divider(height: 24), if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
}
},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text(AppLocalizations.of(context).deregister),
onTap: () async {
final shouldLogout = await showDialog<bool>(
context: context,
builder: (_) => const LogoutDialog(),
);
// ===== Section: Security & App ===== if (shouldLogout == true) {
Padding( await _handleLogout(context);
padding: const EdgeInsets.symmetric(horizontal: 8.0), }
child: Text( },
loc.appVersion,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
),
const SizedBox(height: 8),
// Fingerprint toggle inside a styled container
Card(
child: ListTile(
leading: const Icon(Icons.smartphone),
title: Text(loc.appVersion),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
);
} else if (snapshot.hasError) {
return Text(loc.error);
} else {
return Text(
snapshot.data ?? "N/A",
);
}
},
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
const SizedBox(height: 16),
const Divider(height: 24),
// ===== Section: Actions =====
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
"Exit",
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
),
const SizedBox(height: 8),
_SectionTile(
leadingIcon: Icons.exit_to_app,
title: loc.logout,
trailChevron: false, // action tile, no chevron
onTap: () async {
final shouldExit = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(loc.logout),
content: Text(loc.logoutCheck),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(loc.no),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(loc.yes),
),
],
),
);
if (shouldExit == true) {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
}
},
),
_SectionTile(
leadingIcon: Icons.logout,
title: loc.deregister,
trailChevron: false,
onTap: () async {
final shouldLogout = await showDialog<bool>(
context: context,
builder: (_) => const LogoutDialog(),
);
if (shouldLogout == true) {
await _handleLogout(context);
}
},
),
const SizedBox(height: 24),
],
), ),
], ],
), ),
); );
} }
} }
class _SectionTile extends StatelessWidget {
const _SectionTile({
required this.leadingIcon,
required this.title,
this.onTap,
this.trailChevron = true,
});
final IconData leadingIcon;
final String title;
final VoidCallback? onTap;
final bool trailChevron;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(leadingIcon, color: theme.colorScheme.onSurface),
title: Text(title, style: theme.textTheme.bodyLarge),
trailing: trailChevron
? Icon(Icons.chevron_right, color: theme.colorScheme.onSurface)
: null,
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
}

View File

@@ -1,149 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/features/profile/change_password/change_password_screen.dart';
import 'package:kmobile/features/profile/tpin/change_tpin_screen.dart';
import 'package:kmobile/features/profile/change_mpin_screen.dart';
import 'package:kmobile/api/services/auth_service.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
import 'package:kmobile/di/injection.dart';
import '../../l10n/app_localizations.dart';
class SecuritySettingsScreen extends StatelessWidget {
final String mobileNumber;
const SecuritySettingsScreen({super.key, required this.mobileNumber});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.securitySettings),
centerTitle: true,
),
body: Stack(
children: [
ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.lock_outline),
title: Text(loc.changeLoginPassword),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordScreen(
mobileNumber: mobileNumber,
),
),
);
},
),
),
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.pin),
title: Text(loc.changeMpin),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChangeMpinScreen(),
),
);
if (result == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(loc.mpinChangedSuccessfully),
backgroundColor:
Theme.of(context).colorScheme.secondary,
),
);
}
},
),
),
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.password),
title: const Text('Change TPIN'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final authService = getIt<AuthService>();
final isTpinSet = await authService.checkTpin();
if (!isTpinSet) {
if (context.mounted) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('TPIN Not Set'),
content: const Text(
'You have not set a TPIN yet. Please set a TPIN to proceed.'),
actions: <Widget>[
TextButton(
child: const Text('Back'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Proceed'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const TpinSetScreen(),
),
);
},
),
],
);
},
);
}
} else {
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeTpinScreen(mobileNumber: mobileNumber),
),
);
}
}
},
),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

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

View File

@@ -1,147 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/profile/tpin/change_tpin_otp_screen.dart';
import 'package:kmobile/widgets/pin_input_field.dart';
import '../../../api/services/change_password_service.dart';
class ChangeTpinScreen extends StatefulWidget {
final String mobileNumber;
const ChangeTpinScreen({super.key, required this.mobileNumber});
@override
State<ChangeTpinScreen> createState() => _ChangeTpinScreenState();
}
class _ChangeTpinScreenState extends State<ChangeTpinScreen> {
final _formKey = GlobalKey<FormState>();
final _oldTpinController = TextEditingController();
final _newTpinController = TextEditingController();
final _confirmTpinController = TextEditingController();
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
bool _isLoading = false;
@override
void dispose() {
_oldTpinController.dispose();
_newTpinController.dispose();
_confirmTpinController.dispose();
super.dispose();
}
void _handleChangeTpin() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
// 1. Get OTP for TPIN change.
await _changePasswordService.getOtpTpin(
mobileNumber: widget.mobileNumber);
// 2. Navigate to the OTP screen on success.
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeTpinOtpScreen(
oldTpin: _oldTpinController.text,
newTpin: _newTpinController.text,
mobileNumber: widget.mobileNumber,
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send OTP: $e')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Change TPIN'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Current TPIN'),
const SizedBox(height: 8),
PinInputField(
controller: _oldTpinController,
validator: (value) {
if (value == null || value.length != 6) {
return 'Please enter your 6-digit old TPIN';
}
return null;
},
),
const SizedBox(height: 24),
const Text('New TPIN'),
const SizedBox(height: 8),
PinInputField(
controller: _newTpinController,
validator: (value) {
if (value == null || value.length != 6) {
return 'Please enter a 6-digit new TPIN';
}
if (value == _oldTpinController.text) {
return 'New TPIN must be different from the old one.';
}
return null;
},
),
const SizedBox(height: 24),
const Text('Confirm New TPIN'),
const SizedBox(height: 8),
PinInputField(
controller: _confirmTpinController,
validator: (value) {
if (value == null || value.length != 6) {
return 'Please confirm your new TPIN';
}
if (value != _newTpinController.text) {
return 'TPINs do not match';
}
return null;
},
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleChangeTpin,
child: _isLoading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: const Text('Proceed'),
),
),
],
),
),
),
);
}
}

View File

@@ -21,65 +21,38 @@ class _QuickPayScreen extends State<QuickPayScreen> {
AppLocalizations.of(context).quickPay.replaceAll('\n', ''), AppLocalizations.of(context).quickPay.replaceAll('\n', ''),
), ),
), ),
body: Stack( body: ListView(
children: [ children: [
Padding( QuickPayManagementTile(
padding: const EdgeInsets.symmetric(vertical: 16.0), icon: Symbols.input_circle,
child: Column( label: AppLocalizations.of(context).ownBank,
crossAxisAlignment: CrossAxisAlignment.stretch, onTap: () {
children: [ Navigator.push(
Expanded( context,
child: QuickPayManagementTile( MaterialPageRoute(
icon: Symbols.input_circle, builder: (context) => QuickPayWithinBankScreen(
label: AppLocalizations.of(context).ownBank, debitAccount: widget.debitAccount,
subtitle: AppLocalizations.of(context).quickownsubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuickPayWithinBankScreen(
debitAccount: widget.debitAccount,
),
),
);
},
), ),
), ),
const SizedBox(height: 16), );
Expanded( },
child: QuickPayManagementTile(
icon: Symbols.output_circle,
label: AppLocalizations.of(context).outsideBank,
subtitle: AppLocalizations.of(context).quickoutsidesubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuickPayOutsideBankScreen(
debitAccount: widget.debitAccount,
),
),
);
},
),
),
],
),
), ),
IgnorePointer( const Divider(height: 1),
child: Center( QuickPayManagementTile(
child: Opacity( icon: Symbols.output_circle,
opacity: 0.07, // Reduced opacity label: AppLocalizations.of(context).outsideBank,
child: ClipOval( onTap: () {
child: Image.asset( Navigator.push(
'assets/images/logo.png', context,
width: 200, // Adjust size as needed MaterialPageRoute(
height: 200, // Adjust size as needed builder: (context) => QuickPayOutsideBankScreen(
debitAccount: widget.debitAccount,
), ),
), ),
), );
), },
), ),
const Divider(height: 1),
], ],
), ),
); );
@@ -89,7 +62,6 @@ class _QuickPayScreen extends State<QuickPayScreen> {
class QuickPayManagementTile extends StatelessWidget { class QuickPayManagementTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final String? subtitle;
final VoidCallback onTap; final VoidCallback onTap;
final bool disable; final bool disable;
@@ -97,63 +69,18 @@ class QuickPayManagementTile extends StatelessWidget {
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
this.subtitle,
required this.onTap, required this.onTap,
this.disable = false, this.disable = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); return ListTile(
return Card( leading: Icon(icon),
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), title: Text(label),
shape: RoundedRectangleBorder( trailing: const Icon(Symbols.arrow_right, size: 20),
borderRadius: BorderRadius.circular(12.0), onTap: onTap,
), enabled: !disable,
elevation: 4, // Add some elevation for better visual separation
child: InkWell(
onTap:
disable ? null : onTap, // Disable InkWell if the tile is disabled
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 36.0, horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64, // Make icon larger for two cards
color:
disable ? theme.disabledColor : theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
label,
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: disable
? theme.disabledColor
: theme.colorScheme.onSurface,
),
),
if (subtitle != null) // Conditionally display subtitle
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
subtitle!,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: disable
? theme.disabledColor
: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
),
); );
} }
} }

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,48 +46,47 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
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(() {
_isLoadingLimit = true; _limit = limitData;
_isLoadingLimit = false;
}); });
try { } catch (e) {
final limitData = await _limitService.getLimit(); // Handle error if needed
setState(() { setState(() {
_limit = limitData; _isLoadingLimit = false;
_isLoadingLimit = false; });
}); }
} catch (e) { }
// Handle error if needed
setState(() { void _checkAmountLimit() {
_isLoadingLimit = false; 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,
),
);
} }
void _checkAmountLimit() { // Update state only if it changes to avoid unnecessary rebuilds
if (_limit == null) return; if (_isAmountOverLimit != isOverLimit) {
setState(() {
final amount = double.tryParse(amountController.text) ?? 0; _isAmountOverLimit = isOverLimit;
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 ||
@@ -143,353 +142,319 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).quickPayOwnBank, AppLocalizations.of(context).quickPayOwnBank,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(16.0),
Padding( child: Form(
padding: const EdgeInsets.all(16.0), key: _formKey,
child: Form( child: SingleChildScrollView(
key: _formKey, child: Column(
child: SingleChildScrollView( children: [
child: Column( const SizedBox(height: 10),
children: [ TextFormField(
const SizedBox(height: 10), decoration: InputDecoration(
TextFormField( labelText: AppLocalizations.of(context).debitAccountNumber,
decoration: InputDecoration( border: const OutlineInputBorder(),
labelText: isDense: true,
AppLocalizations.of(context).debitAccountNumber, filled: true,
border: const OutlineInputBorder(), fillColor: Theme.of(context).scaffoldBackgroundColor,
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
),
readOnly: true,
controller:
TextEditingController(text: widget.debitAccount),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
enabled: false,
),
const SizedBox(height: 20),
TextFormField(
decoration: InputDecoration(
labelText: AppLocalizations.of(context).accountNumber,
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: 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;
},
),
const SizedBox(height: 25),
TextFormField(
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
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;
},
),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating
? null
: () {
if (accountNumberController.text.length ==
11 &&
confirmAccountNumberController.text ==
accountNumberController.text) {
_validateBeneficiary();
} else {
setState(() {
_validationError =
AppLocalizations.of(context)
.accountMismatch;
});
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: Text(AppLocalizations.of(context)
.validateBeneficiary),
),
),
),
if (_beneficiaryName != null && _isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
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),
),
),
),
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 paymentResponseFuture = paymentService
.processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext)
.pushReplacement(
MaterialPageRoute(
builder: (_) => PaymentAnimationScreen(
paymentResponse:
paymentResponseFuture),
),
);
},
),
),
);
}
},
),
),
],
), ),
readOnly: true,
controller: TextEditingController(text: widget.debitAccount),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
enabled: false,
), ),
), const SizedBox(height: 20),
), TextFormField(
IgnorePointer( decoration: InputDecoration(
child: Center( labelText: AppLocalizations.of(context).accountNumber,
child: Opacity( border: const OutlineInputBorder(),
opacity: 0.07, // Reduced opacity isDense: true,
child: ClipOval( filled: true,
child: Image.asset( fillColor: Theme.of(context).scaffoldBackgroundColor,
'assets/images/logo.png', enabledBorder: OutlineInputBorder(
width: 200, // Adjust size as needed borderSide: BorderSide(
height: 200, // Adjust size as needed 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;
},
),
const SizedBox(height: 25),
TextFormField(
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
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;
},
),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating
? null
: () {
if (accountNumberController.text.length == 11 &&
confirmAccountNumberController.text ==
accountNumberController.text) {
_validateBeneficiary();
} else {
setState(() {
_validationError =
AppLocalizations.of(context)
.accountMismatch;
});
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(
AppLocalizations.of(context).validateBeneficiary),
),
),
),
if (_beneficiaryName != null && _isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: Row(
children: [
const Icon(Icons.check_circle, color: Colors.green),
const SizedBox(width: 8),
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),
), ),
), ),
), ),
), 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 paymentResponseFuture = paymentService
.processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement(
MaterialPageRoute(
builder: (_) => PaymentAnimationScreen(
paymentResponse: paymentResponseFuture),
),
);
},
),
),
);
}
},
),
),
],
), ),
], ),
),
), ),
); );
} }

View File

@@ -11,45 +11,26 @@ class SecurityErrorScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( body: Padding(
children: [ padding: const EdgeInsets.all(20.0),
Padding( child: Column(
padding: const EdgeInsets.all(20.0), mainAxisAlignment: MainAxisAlignment.center,
child: Column( children: [
mainAxisAlignment: MainAxisAlignment.center, Lottie.asset('assets/animations/error.json', height: 200),
children: [ const SizedBox(height: 20),
Lottie.asset('assets/animations/error.json', height: 200), Text(
const SizedBox(height: 20), message,
Text( textAlign: TextAlign.center,
message, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () => SystemChannels.platform
.invokeMethod('SystemNavigator.pop'),
child: const Text('Okay'),
),
],
), ),
), const SizedBox(height: 40),
IgnorePointer( ElevatedButton(
child: Center( onPressed: () =>
child: Opacity( SystemChannels.platform.invokeMethod('SystemNavigator.pop'),
opacity: 0.07, // Reduced opacity child: const Text('Okay'),
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
), ),
), ],
], ),
), ),
); );
} }

View File

@@ -1,165 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart'; // Added: Import BranchService for Atm model and API calls
import 'package:kmobile/di/injection.dart'; // Added: Import for dependency injection (getIt)
import 'package:shimmer/shimmer.dart';
import '../../../l10n/app_localizations.dart'; // Added: Import for shimmer loading effect
// Removed: The local 'Location' class is no longer needed as we use the 'Atm' model from branch_service.dart
class ATMLocatorScreen extends StatefulWidget {
const ATMLocatorScreen({super.key});
@override
State<ATMLocatorScreen> createState() => _ATMLocatorScreenState();
}
class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
final TextEditingController _searchController = TextEditingController();
var service =
getIt<BranchService>(); // Added: Instance of BranchService for API calls
bool _isLoading = true; // State variable to manage loading status
List<Atm> _allAtms = []; // Changed: List to hold all fetched Atm objects
List<Atm> _filteredAtms =
[]; // Changed: List to hold filtered Atm objects for display
@override
void initState() {
super.initState();
_loadAtms(); // Changed: Call _loadAtms to fetch data on initialization
}
/// Fetches the list of ATMs from the API using BranchService.
Future<void> _loadAtms() async {
final data =
await service.fetchAtmList(); // Call the new fetchAtmList method
setState(() {
_allAtms = data; // Update the list of all ATMs
_filteredAtms = data; // Initialize filtered list with all ATMs
_isLoading = false; // Set loading to false once data is fetched
});
}
/// Filters the list of ATMs based on the search query.
void _filterAtms(String query) {
// Changed: Renamed from _filterLocations
setState(() {
if (query.isEmpty) {
_filteredAtms = _allAtms; // If query is empty, show all ATMs
} else {
_filteredAtms = _allAtms.where((atm) {
// Changed: Filter based on Atm object
final lowerQuery = query.toLowerCase();
return atm.name
.toLowerCase()
.contains(lowerQuery); // Filter by atm.name
}).toList();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
AppLocalizations.of(context).atmlocator), // Title for the app bar
),
body: Stack(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
onChanged:
_filterAtms, // Updated: Call _filterAtms on text change
decoration: InputDecoration(
hintText: "Name/Address", // Hint text for the search bar
prefixIcon: const Icon(Icons.search), // Search icon
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
// Content area: Displays loading shimmer, "no ATMs found", or the list of ATMs
Expanded(
child: _isLoading
? _buildShimmerList() // Display shimmer while loading
: _filteredAtms.isEmpty
? const Center(
child: Text(
"No matching ATMs found")) // Message if no ATMs found
: ListView.builder(
itemCount: _filteredAtms
.length, // Number of items in the filtered list
itemBuilder: (context, index) {
final atm = _filteredAtms[
index]; // Get the current Atm object
return _buildAtmItem(
atm); // Build the ATM list item
},
),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png', // Background logo image
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
/// Helper widget to build a single ATM list item.
Widget _buildAtmItem(Atm atm) {
// Changed: Takes an Atm object
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: const Icon(Icons.credit_card), // Icon for ATM
title: Text(atm.name, // Display the ATM's name
style: const TextStyle(fontWeight: FontWeight.bold)),
onTap: () {
// No action on tap as per user request
},
),
);
}
/// Helper widget to display a shimmer loading effect.
Widget _buildShimmerList() {
return ListView.builder(
itemCount: 10, // Number of shimmer items to display
itemBuilder: (context, index) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: ListTile(
leading: const CircleAvatar(
radius: 24,
backgroundColor: Colors.white,
),
title: Container(
height: 16,
color: Colors.white,
margin: const EdgeInsets.symmetric(vertical: 4),
),
),
),
);
}
}

View File

@@ -1,109 +0,0 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:url_launcher/url_launcher.dart';
class BranchDetailsScreen extends StatelessWidget {
final Branch branch;
const BranchDetailsScreen({super.key, required this.branch});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(branch.branch_name),
),
body: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow(context, "Branch Name", branch.branch_name),
_buildDetailRow(context, "Branch Code", branch.branch_code),
_buildDetailRow(context, "Zone", branch.zone),
_buildDetailRow(context, "Tehsil", branch.tehsil),
_buildDetailRow(context, "Block", branch.block),
_buildDetailRow(context, "District", branch.distt_name),
_buildDetailRow(context, "Pincode", branch.pincode),
// _buildDetailRow(context, "Post Office", branch.post_office),
// _buildDetailRow(context, "Date of Opening", branch.date_of_opening),
// _buildDetailRow(context, "Branch Type", branch.type_of_branch),
_buildDetailRow(context, "Telephone No.", branch.telephone_no),
// _buildDetailRow("RTGS Account No.", branch.rtgs_acct_no),
// _buildDetailRow("RBI Code 1", branch.rbi_code_1),
// _buildDetailRow("RBI Code 2", branch.rbi_code_2),
// _buildDetailRow("Latitude", branch.br_lattitude),
// _buildDetailRow("Longitude", branch.br_longitude),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
Widget _buildDetailRow(BuildContext context, String label, String value) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 4),
label == "Telephone No."
? InkWell(
onTap: () => _launchUrl('tel:$value'),
child: Text(
value,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color:
theme.colorScheme.primary, // Indicate it's clickable
decoration: TextDecoration.underline,
),
),
)
: Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
Divider(height: 16, color: theme.dividerColor),
],
),
);
}
Future<void> _launchUrl(String urlString) async {
final Uri url = Uri.parse(urlString);
if (!await launchUrl(url)) {
throw 'Could not launch $urlString';
}
}
}

View File

@@ -1,13 +1,27 @@
// ignore_for_file: unused_element // ignore_for_file: unused_element
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/di/injection.dart';
import 'package:shimmer/shimmer.dart';
import 'package:kmobile/features/service/screens/branch_details_screen.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
// Enum to define the type of location
enum LocationType { branch, atm }
class Location {
final String name;
final String? code; // Nullable for ATMs
final String? ifsc; // Nullable for ATMs
final String address;
final LocationType type;
Location({
required this.name,
this.code,
this.ifsc,
required this.address,
required this.type,
});
}
class BranchLocatorScreen extends StatefulWidget { class BranchLocatorScreen extends StatefulWidget {
const BranchLocatorScreen({super.key}); const BranchLocatorScreen({super.key});
@@ -17,35 +31,76 @@ class BranchLocatorScreen extends StatefulWidget {
class _BranchLocatorScreenState extends State<BranchLocatorScreen> { class _BranchLocatorScreenState extends State<BranchLocatorScreen> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
var service = getIt<BranchService>();
bool _isLoading = true; final List<Location> _allLocations = [
List<Branch> _allBranches = []; Location(
List<Branch> _filteredBranches = []; name: "Dharamsala - Head Office",
code: "002",
ifsc: "KACE0000002",
address: "Civil Lines Dharmashala, Kangra, HP - 176215",
type: LocationType.branch,
),
Location(
name: "Kangra",
code: "033",
ifsc: "KACE0000033",
address: "Rajput Bhawankangrapo, Kangra, HP ",
type: LocationType.branch,
),
Location(
name: "Dharamsala ATM",
address: "Near Main Square, Dharamsala",
type: LocationType.atm,
),
Location(
name: "Kangra ATM",
address: "Opposite Bus Stand, Kangra",
type: LocationType.atm,
),
];
List<Location> _filteredLocations = [];
bool _isLoading = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// _fetchAndSetLocations(); // _fetchAndSetLocations();
_loadBranches(); _filteredLocations = _allLocations;
} }
Future<void> _loadBranches() async { // Example of a future API fetching function
final data = await service.fetchBranchList(); /*
Future<void> _fetchAndSetLocations() async {
setState(() {
_isLoading = true;
});
try {
// final locations = await yourApiService.getLocations();
// setState(() {
// _allLocations = locations;
// _filteredLocations = locations;
// });
} catch (e) {
// Handle error
} finally {
setState(() { setState(() {
_allBranches = data;
_filteredBranches = data;
_isLoading = false; _isLoading = false;
}); });
} }
}
void _filterBranches(String query) { */
void _filterLocations(String query) {
setState(() { setState(() {
if (query.isEmpty) { if (query.isEmpty) {
_filteredBranches = _allBranches; _filteredLocations = _allLocations;
} else { } else {
_filteredBranches = _allBranches.where((branch) { _filteredLocations = _allLocations.where((location) {
final lowerQuery = query.toLowerCase(); final lowerQuery = query.toLowerCase();
return branch.branch_name.toLowerCase().contains(lowerQuery); return location.name.toLowerCase().contains(lowerQuery) ||
(location.code?.toLowerCase().contains(lowerQuery) ?? false) ||
(location.ifsc?.toLowerCase().contains(lowerQuery) ?? false) ||
location.address.toLowerCase().contains(lowerQuery);
}).toList(); }).toList();
} }
}); });
@@ -55,62 +110,39 @@ class _BranchLocatorScreenState extends State<BranchLocatorScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(AppLocalizations.of(context).branchLocator),
AppLocalizations.of(context).branchlocator,
),
), ),
body: Stack( body: Column(
children: [ children: [
Column( Padding(
children: [ padding: const EdgeInsets.all(12.0),
Padding( child: TextField(
padding: const EdgeInsets.all(12.0), controller: _searchController,
child: TextField( onChanged: _filterLocations,
controller: _searchController, decoration: InputDecoration(
onChanged: _filterBranches, // Updated hintText: AppLocalizations.of(context).searchbranchby,
decoration: InputDecoration( prefixIcon: const Icon(Icons.search),
hintText: "Branch Name", border: OutlineInputBorder(
prefixIcon: const Icon(Icons.search), borderRadius: BorderRadius.circular(12),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
// Content area
Expanded(
child: _isLoading
? _buildShimmerList() // Changed to shimmer
: _filteredBranches.isEmpty
? const Center(
child: Text(
"No matching branches found")) // Updated tex
: ListView.builder(
itemCount: _filteredBranches.length,
itemBuilder: (context, index) {
final branch =
_filteredBranches[index]; // Changed to
return _buildBranchItem(branch); // Updated
},
),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
), ),
), ),
), ),
), ),
// Content area
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredLocations.isEmpty
? const Center(child: Text("No matching locations found"))
: ListView.builder(
itemCount: _filteredLocations.length,
itemBuilder: (context, index) {
final location = _filteredLocations[index];
return _buildLocationItem(location);
},
),
),
], ],
), ),
); );
@@ -129,49 +161,28 @@ class _BranchLocatorScreenState extends State<BranchLocatorScreen> {
); );
} }
// Helper widget to build a single branch item // Helper widget to build a single location item
Widget _buildLocationItem(Location location) {
Widget _buildBranchItem(Branch branch) { final isBranch = location.type == LocationType.branch;
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile( child: ListTile(
leading: const CircleAvatar( leading: CircleAvatar(
child: Icon(Icons.account_balance), child: Icon(isBranch ? Icons.location_city : Icons.currency_rupee),
), ),
title: Text(branch.branch_name, title: Text(location.name,
style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
isBranch
? "Code: ${location.code} | IFSC: ${location.ifsc}\nAddress: ${location.address}"
: "Address: ${location.address}",
),
onTap: () { onTap: () {
// This is the updated part ScaffoldMessenger.of(context).showSnackBar(
Navigator.push( SnackBar(content: Text("Selected ${location.name}")),
context,
MaterialPageRoute(
builder: (_) => BranchDetailsScreen(branch: branch),
),
); );
}, },
), ),
); );
} }
// Shimmer loading list
Widget _buildShimmerList() {
return ListView.builder(
itemCount: 10, // Number of shimmer items
itemBuilder: (context, index) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: ListTile(
leading: const CircleAvatar(
radius: 24,
backgroundColor: Colors.white,
),
title: Container(
height: 16,
color: Colors.white,
margin: const EdgeInsets.symmetric(vertical: 4),
),
),
),
);
}
} }

Some files were not shown because too many files have changed in this diff Show More