32 Commits

Author SHA1 Message Date
71e0521dec kmobile logo changed 2025-11-17 16:57:46 +05:30
f6e851a9ee New KCCB logo 2025-11-17 16:06:07 +05:30
547f534037 localized Beneficiary delete 2025-11-17 15:24:58 +05:30
66b2e71140 Quick Links Screen Icon to List 2025-11-17 10:58:44 +05:30
43d92d799b Quick Links added 2025-11-14 15:24:02 +05:30
3135116f26 Branch and ATM Locator added 2025-11-14 14:36:06 +05:30
39165d631e Watermark added, Card commented out and account opening commented out 2025-11-12 15:59:41 +05:30
shital
ef481ec879 Security settings improvements 2025-11-11 14:18:37 +05:30
shital
36702b198f Fix daily limit parsing type error 2025-11-11 01:10:45 +05:30
shital
f0718e9d68 removed unused imports and blocks 2025-11-11 00:49:31 +05:30
shital
d2cce89efb Fix biometric switch and UI improvements 2025-11-11 00:44:43 +05:30
8cfca113bf dart format 2025-11-10 16:50:29 +05:30
d6f61ebb31 Cooldown Added in Beneficiary 2025-11-10 16:42:29 +05:30
078e715d20 T&C Finalized Test APK 2025-11-10 13:24:52 +05:30
5c8df8ace3 TNC Route Fixed 2025-11-10 12:39:31 +05:30
3e88aad43f T&C #1 2025-11-09 15:42:50 +05:30
5b7f3f0096 Change TPIn #5 2025-11-08 20:10:35 +05:30
b5b6c6ed49 Change TPIn #4 2025-11-08 20:07:04 +05:30
c26cc507a1 Change TPIn #3 2025-11-08 16:56:54 +05:30
87fd36b748 Change TPIn #2 2025-11-08 12:25:56 +05:30
3417c4b0e5 Change TPIN #1 2025-11-08 11:23:38 +05:30
151140d563 Fingerprint Toggle fixed 2025-11-07 17:40:56 +05:30
a8ee7833be Loan And TD funds transfer disabled 2025-11-04 15:24:57 +05:30
f73faaa635 Merge branch 'testing' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into testing 2025-11-04 14:56:16 +05:30
5ac977e903 Self-Transfer #1 2025-11-04 14:53:14 +05:30
f15b8ac3f7 ICICI logo fixed 2025-11-03 12:28:05 +05:30
8f8fdb70e6 Proceed or Swipe to pay disabled on over limit 2025-10-31 16:51:37 +05:30
d86ff2c427 Snackbar added in amount screens 2025-10-31 16:31:04 +05:30
527111c1de Limit loaded and less that 2 lakhs 2025-10-31 13:17:31 +05:30
dfbdb3238d Loading Spinner added in Limit 2025-10-31 12:37:04 +05:30
3d13edf676 Limit Loading Change #1 2025-10-31 12:15:49 +05:30
32e8b85cee APK #1 2025-10-30 12:23:56 +05:30
98 changed files with 6199 additions and 2787 deletions

5
.gitignore vendored
View File

@@ -44,3 +44,8 @@ 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,6 +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
@@ -51,15 +57,21 @@ 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 {
// TODO: Add your own signing config for the release build. signingConfig signingConfigs.release
// 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'
} }
} }
} }

View File

@@ -40,6 +40,20 @@
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: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/images/logo_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -141,4 +141,23 @@ 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

@@ -0,0 +1,137 @@
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,4 +67,21 @@ 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

@@ -0,0 +1,57 @@
// ignore_for_file: collection_methods_unrelated_type
import 'dart:developer';
import 'package:dio/dio.dart';
class Limit {
final double dailyLimit;
final double usedLimit;
Limit({
required this.dailyLimit,
required this.usedLimit,
});
factory Limit.fromJson(Map<String, dynamic> json) {
return Limit(
dailyLimit: (json['dailyLimit'] as num).toDouble(),
usedLimit: (json['usedLimit'] as num).toDouble(),
);
}
}
class LimitService {
final Dio _dio;
LimitService(this._dio);
Future<Limit> getLimit() async {
try {
final response = await _dio.get('/api/customer/daily-limit');
if (response.statusCode == 200) {
log('Response: ${response.data}');
return Limit.fromJson(response.data);
} else {
throw Exception('Failed to load');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
void editLimit(double newLimit) async {
try {
final response = await _dio.post('/api/customer/daily-limit',
data: '{"amount": $newLimit}');
if (response.statusCode == 200) {
log('Response: ${response.data}');
} else {
throw Exception('Failed to load');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
}

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/details'); final response = await _dio.get('/api/customer');
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

@@ -316,7 +316,7 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardScreen(), const DashboardScreen(),
const CardManagementScreen(), // const CardManagementScreen(),
const ServiceScreen(), const ServiceScreen(),
]; ];
@@ -374,10 +374,10 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
icon: const Icon(Icons.home_filled), icon: const Icon(Icons.home_filled),
label: AppLocalizations.of(context).home, label: AppLocalizations.of(context).home,
), ),
BottomNavigationBarItem( // BottomNavigationBarItem(
icon: const Icon(Icons.credit_card), // icon: const Icon(Icons.credit_card),
label: AppLocalizations.of(context).card, // label: AppLocalizations.of(context).card,
), // ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services), icon: const Icon(Icons.miscellaneous_services),
label: AppLocalizations.of(context).services, label: AppLocalizations.of(context).services,

View File

@@ -10,6 +10,7 @@ import '../features/dashboard/screens/dashboard_screen.dart';
// import '../features/transactions/screens/transactions_screen.dart'; // import '../features/transactions/screens/transactions_screen.dart';
// import '../features/payments/screens/payments_screen.dart'; // import '../features/payments/screens/payments_screen.dart';
// import '../features/settings/screens/settings_screen.dart'; // import '../features/settings/screens/settings_screen.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
class AppRoutes { class AppRoutes {
// Private constructor to prevent instantiation // Private constructor to prevent instantiation
@@ -34,7 +35,9 @@ class AppRoutes {
return MaterialPageRoute(builder: (_) => const SplashScreen()); 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

@@ -2,6 +2,7 @@ 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;
@@ -11,6 +12,7 @@ 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,
@@ -21,6 +23,9 @@ class Beneficiary {
return Beneficiary( return Beneficiary(
accountNo: json['account_no'] ?? json['accountNo'] ?? '', accountNo: json['account_no'] ?? json['accountNo'] ?? '',
accountType: json['account_type'] ?? json['accountType'] ?? '', accountType: json['account_type'] ?? json['accountType'] ?? '',
createdAt: json['createdAt'] == null
? null
: DateTime.tryParse(json['createdAt']),
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,10 +13,12 @@ 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>> login(String customerNo, String password) async { Future<(List<User>, AuthToken)> login(
String customerNo, String password) async {
// Create credentials and call service // Create credentials and call service
final credentials = final credentials =
AuthCredentials(customerNo: customerNo, password: password); AuthCredentials(customerNo: customerNo, password: password);
@@ -27,7 +29,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; return (users, authToken);
} }
Future<bool> isLoggedIn() async { Future<bool> isLoggedIn() async {
@@ -47,6 +49,7 @@ 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 {
@@ -56,13 +59,28 @@ 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) {
return AuthToken( final authToken = AuthToken(
accessToken: accessToken, accessToken: accessToken,
expiresAt: DateTime.parse(expiryString), expiresAt: DateTime.parse(expiryString),
tnc:
tncString == 'true', // Parse 'true' string to true, otherwise false
); );
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,3 +1,5 @@
import 'package:kmobile/api/services/branch_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';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -46,9 +48,11 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>())); getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>())); getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
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>()),
); );
@@ -59,22 +63,23 @@ Future<void> setupDependencies() async {
); );
// Register controllers/cubits // Register controllers/cubits
getIt.registerFactory<AuthCubit>( getIt.registerFactory<AuthCubit>(() => AuthCubit(
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>())); getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>()));
} }
Dio _createDioClient() { Dio _createDioClient() {
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: baseUrl:
'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test // 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
//'https://kccbmbnk.net', '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: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'X-Login-Type': 'MB',
}, },
), ),
); );

View File

@@ -34,59 +34,78 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
.accountInfo .accountInfo
.replaceFirst(RegExp('\n'), '')), .replaceFirst(RegExp('\n'), '')),
), ),
body: ListView( body: Stack(
padding: const EdgeInsets.all(16.0),
children: [ children: [
Text( ListView(
AppLocalizations.of(context).accountNumber, padding: const EdgeInsets.all(16.0),
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), children: [
), Text(
AppLocalizations.of(context).accountNumber,
style:
const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
),
DropdownButton<User>( DropdownButton<User>(
value: selectedUser, value: selectedUser,
onChanged: (User? newUser) { onChanged: (User? newUser) {
if (newUser != null) { if (newUser != null) {
setState(() { setState(() {
selectedUser = newUser; selectedUser = newUser;
}); });
} }
}, },
items: widget.users.map((user) { items: widget.users.map((user) {
return DropdownMenuItem<User>( return DropdownMenuItem<User>(
value: user, value: user,
child: Text(user.accountNo.toString()), child: Text(user.accountNo.toString()),
); );
}).toList(), }).toList(),
), ),
InfoRow( InfoRow(
title: AppLocalizations.of(context).customerNumber, title: AppLocalizations.of(context).customerNumber,
value: selectedUser.cifNumber ?? 'N/A', value: selectedUser.cifNumber ?? 'N/A',
), ),
InfoRow( InfoRow(
title: AppLocalizations.of(context).productName, title: AppLocalizations.of(context).productName,
value: selectedUser.productType ?? 'N/A', value: selectedUser.productType ?? 'N/A',
), ),
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'), // InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'),
InfoRow( InfoRow(
title: AppLocalizations.of(context).accountStatus, title: AppLocalizations.of(context).accountStatus,
value: 'OPEN', value: 'OPEN',
), ),
InfoRow( InfoRow(
title: AppLocalizations.of(context).availableBalance, title: AppLocalizations.of(context).availableBalance,
value: selectedUser.availableBalance ?? 'N/A', value: selectedUser.availableBalance ?? 'N/A',
), ),
InfoRow( InfoRow(
title: AppLocalizations.of(context).currentBalance, title: AppLocalizations.of(context).currentBalance,
value: selectedUser.currentBalance ?? 'N/A', value: selectedUser.currentBalance ?? 'N/A',
), ),
users[selectedIndex].approvedAmount != null users[selectedIndex].approvedAmount != null
? InfoRow( ? InfoRow(
title: AppLocalizations.of(context).approvedAmount, title: AppLocalizations.of(context).approvedAmount,
value: selectedUser.approvedAmount ?? 'N/A', value: selectedUser.approvedAmount ?? 'N/A',
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
],
),
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

@@ -133,201 +133,225 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(12.0), children: [
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(12.0),
children: [ child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
"${AppLocalizations.of(context).accountNumber}: ", children: [
style: const TextStyle( Text(
fontSize: 17, "${AppLocalizations.of(context).accountNumber}: ",
fontWeight: FontWeight.bold, style: const TextStyle(
), fontSize: 17,
), fontWeight: FontWeight.bold,
Text(widget.accountNo, style: const TextStyle(fontSize: 17)), ),
],
),
const SizedBox(height: 15),
Row(
children: [
Text(
"${AppLocalizations.of(context).availableBalance}: ",
style: const TextStyle(
fontSize: 17,
),
),
Text('${widget.balance}',
style: const TextStyle(fontSize: 17)),
],
),
const SizedBox(height: 15),
Text(
AppLocalizations.of(context).filters,
style: const TextStyle(fontSize: 17),
),
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => _selectFromDate(context),
child: buildDateBox(
AppLocalizations.of(context).fromDate,
fromDate,
), ),
), Text(widget.accountNo,
style: const TextStyle(fontSize: 17)),
],
), ),
const SizedBox(width: 10), const SizedBox(height: 15),
Expanded( Row(
child: GestureDetector( children: [
onTap: () => _selectToDate(context), Text(
child: buildDateBox( "${AppLocalizations.of(context).availableBalance}: ",
AppLocalizations.of(context).toDate, style: const TextStyle(
toDate, fontSize: 17,
),
), ),
), Text('${widget.balance}',
style: const TextStyle(fontSize: 17)),
],
), ),
], const SizedBox(height: 15),
), Text(
const SizedBox(height: 20), AppLocalizations.of(context).filters,
SizedBox( style: const TextStyle(fontSize: 17),
width: double.infinity,
child: ElevatedButton(
onPressed: _loadTransactions,
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.symmetric(vertical: 16),
), ),
child: Text( const SizedBox(height: 15),
AppLocalizations.of(context).search, Row(
style: TextStyle( children: [
color: Theme.of(context).colorScheme.onPrimaryContainer, Expanded(
fontSize: 16, child: GestureDetector(
), onTap: () => _selectFromDate(context),
), child: buildDateBox(
), AppLocalizations.of(context).fromDate,
), fromDate,
const SizedBox(height: 15),
if (!_txLoading &&
_transactions.isNotEmpty &&
fromDate == null &&
toDate == null)
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
AppLocalizations.of(context).lastTenTransactions,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: _txLoading
? ListView.builder(
itemCount: 3,
itemBuilder: (_, __) => ListTile(
leading: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: CircleAvatar(
radius: 12,
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
),
title: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 10,
width: 100,
color: Theme.of(context).scaffoldBackgroundColor,
),
),
subtitle: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 8,
width: 60,
color: Theme.of(context).scaffoldBackgroundColor,
),
), ),
), ),
) ),
: _transactions.isEmpty const SizedBox(width: 10),
? Center( Expanded(
child: Text( child: GestureDetector(
AppLocalizations.of(context).noTransactions, onTap: () => _selectToDate(context),
style: TextStyle( child: buildDateBox(
fontSize: 16, AppLocalizations.of(context).toDate,
color: Theme.of(context).colorScheme.onSurface, toDate,
)), ),
),
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _loadTransactions,
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(
AppLocalizations.of(context).search,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 16,
),
),
),
),
const SizedBox(height: 15),
if (!_txLoading &&
_transactions.isNotEmpty &&
fromDate == null &&
toDate == null)
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
AppLocalizations.of(context).lastTenTransactions,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: _txLoading
? ListView.builder(
itemCount: 3,
itemBuilder: (_, __) => ListTile(
leading: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: CircleAvatar(
radius: 12,
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
),
title: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 10,
width: 100,
color:
Theme.of(context).scaffoldBackgroundColor,
),
),
subtitle: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 8,
width: 60,
color:
Theme.of(context).scaffoldBackgroundColor,
),
),
),
) )
: ListView.separated( : _transactions.isEmpty
itemCount: _transactions.length, ? Center(
itemBuilder: (context, index) { child: Text(
final tx = _transactions[index]; AppLocalizations.of(context).noTransactions,
return ListTile( style: TextStyle(
leading: Icon( fontSize: 16,
tx.type == 'CR' color:
? Symbols.call_received Theme.of(context).colorScheme.onSurface,
: Symbols.call_made, )),
color: tx.type == 'CR' )
? Colors.green : ListView.separated(
: Theme.of(context).colorScheme.error, itemCount: _transactions.length,
), itemBuilder: (context, index) {
title: Text( final tx = _transactions[index];
tx.date ?? '', return ListTile(
style: const TextStyle(fontSize: 15), leading: Icon(
), tx.type == 'CR'
subtitle: Text( ? Symbols.call_received
tx.name != null : Symbols.call_made,
? (tx.name!.length > 22 color: tx.type == 'CR'
? tx.name!.substring(0, 22) ? Colors.green
: tx.name!) : Theme.of(context).colorScheme.error,
: '',
style: const TextStyle(fontSize: 12),
),
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${tx.amount}",
style: const TextStyle(fontSize: 17),
), ),
Text( title: Text(
"Bal: ₹${tx.balance}", tx.date ?? '',
style: const TextStyle( style: const TextStyle(fontSize: 15),
fontSize: 12), // Style matches tx.name
), ),
], subtitle: Text(
), tx.name != null
onTap: () { ? (tx.name!.length > 22
Navigator.push( ? tx.name!.substring(0, 22)
context, : tx.name!)
MaterialPageRoute( : '',
builder: (_) => TransactionDetailsScreen( style: const TextStyle(fontSize: 12),
transaction: tx),
), ),
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${tx.amount}",
style: const TextStyle(fontSize: 17),
),
Text(
"Bal: ₹${tx.balance}",
style: const TextStyle(
fontSize:
12), // Style matches tx.name
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
TransactionDetailsScreen(
transaction: tx),
),
);
},
); );
}, },
); separatorBuilder: (context, index) {
}, return const Divider();
separatorBuilder: (context, index) { },
return const Divider(); ),
}, ),
), ],
), ),
], ),
), 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: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {

View File

@@ -14,72 +14,93 @@ 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: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: Column( Padding(
children: [ padding: const EdgeInsets.all(16.0),
Expanded( child: Column(
flex: 3, children: [
child: Center( Expanded(
child: Column( flex: 3,
mainAxisSize: MainAxisSize.min, child: Center(
children: [ child: Column(
// Amount + icon + Share Button
Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( // Amount + icon + Share Button
"${transaction.amount}", Row(
style: const TextStyle( mainAxisSize: MainAxisSize.min,
fontSize: 40, children: [
fontWeight: FontWeight.bold, 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 ? Colors.green : Colors.red,
size: 28,
),
],
), ),
const SizedBox(width: 8), const SizedBox(height: 8),
Icon( // Date centered
isCredit ? Symbols.call_received : Symbols.call_made, Text(
color: isCredit ? Colors.green : Colors.red, transaction.date ?? "",
size: 28, style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
textAlign: TextAlign.center,
), ),
], ],
), ),
const SizedBox(height: 8), ),
// Date centered ),
Text( const Divider(),
transaction.date ?? "", Expanded(
style: const TextStyle( flex: 5,
fontSize: 16, child: ListView(
color: Colors.grey, children: [
), _buildDetailRow(
textAlign: TextAlign.center, 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),
],
),
),
],
),
),
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,14 +1,19 @@
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) : super(AuthInitial()) { AuthCubit(this._authRepository, this._userService, this._secureStorage)
: super(AuthInitial()) {
checkAuthStatus(); checkAuthStatus();
} }
@@ -29,22 +34,62 @@ class AuthCubit extends Cubit<AuthState> {
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 = await _authRepository.login(customerNo, password); final (users, authToken) =
emit(Authenticated(users)); await _authRepository.login(customerNo, password);
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,9 +1,12 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import '../../../data/models/user.dart'; import 'package:kmobile/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 {}
@@ -12,20 +15,44 @@ class AuthLoading 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,16 +6,31 @@ 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: json['token'], accessToken: token,
expiresAt: _decodeExpiryFromToken(json['token']), expiresAt: _decodeExpiryFromToken(
token), // This method is still valid for JWT expiry
tnc: tncMobileValue, // Use the correctly extracted value
); );
} }
@@ -42,8 +57,45 @@ 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]; List<Object> get props => [accessToken, expiresAt, tnc];
} }

View File

@@ -1,12 +1,11 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/app.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/set_password_screen.dart'; import 'package:kmobile/features/auth/screens/set_password_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
import '../../../app.dart'; import 'package:kmobile/widgets/tnc_dialog.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import '../controllers/auth_cubit.dart'; import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart'; import '../controllers/auth_state.dart';
@@ -23,7 +22,6 @@ class LoginScreenState extends State<LoginScreen>
final _customerNumberController = TextEditingController(); final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
bool _obscurePassword = true; bool _obscurePassword = true;
//bool _showWelcome = true;
@override @override
void dispose() { void dispose() {
@@ -43,37 +41,238 @@ class LoginScreenState extends State<LoginScreen>
@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(
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>( body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) async { listener: (context, state) {
if (state is Authenticated) { if (state is ShowTncDialog) {
final storage = getIt<SecureStorage>(); showDialog<bool>(
final mpin = await storage.read('mpin'); context: context,
if (!context.mounted) return; barrierDismissible: false,
if (mpin == null) { builder: (dialogContext) => TncDialog(
Navigator.of(context).pushReplacement( onProceed: () async {
MaterialPageRoute( // Pop the dialog before the cubit action
builder: (_) => MPinScreen( Navigator.of(dialogContext).pop();
mode: MPinMode.set, await context
onCompleted: (_) { .read<AuthCubit>()
Navigator.of( .onTncDialogResult(true, state.authToken, state.users);
context, },
rootNavigator: true, ),
).pushReplacement( );
MaterialPageRoute( } else if (state is NavigateToTncRequiredScreen) {
builder: (_) => const NavigationScaffold(), 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: (_) {
// Call the cubit to signal MPIN setup is complete
context.read<AuthCubit>().mpinSetupCompleted();
},
), ),
); ),
} else { );
Navigator.of(context).pushReplacement( } else if (state is Authenticated) {
MaterialPageRoute(builder: (_) => const NavigationScaffold()), // This is the single source of truth for navigating to the dashboard
); Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
} MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
} else if (state is AuthError) { } else if (state is AuthError) {
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') { if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
@@ -87,6 +286,7 @@ class LoginScreenState extends State<LoginScreen>
} }
}, },
builder: (context, state) { builder: (context, state) {
// The builder part remains largely the same, focusing on UI display
return Padding( return Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),
child: Form( child: Form(
@@ -107,7 +307,6 @@ class LoginScreenState extends State<LoginScreen>
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Title
Text( Text(
AppLocalizations.of(context).kccb, AppLocalizations.of(context).kccb,
style: TextStyle( style: TextStyle(
@@ -117,12 +316,10 @@ class LoginScreenState extends State<LoginScreen>
), ),
), ),
const SizedBox(height: 48), const SizedBox(height: 48),
TextFormField( TextFormField(
controller: _customerNumberController, controller: _customerNumberController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).customerNumber, labelText: AppLocalizations.of(context).customerNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
@@ -147,7 +344,6 @@ class LoginScreenState extends State<LoginScreen>
}, },
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Password
TextFormField( TextFormField(
controller: _passwordController, controller: _passwordController,
obscureText: _obscurePassword, obscureText: _obscurePassword,
@@ -189,7 +385,6 @@ class LoginScreenState extends State<LoginScreen>
}, },
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
//Login Button
SizedBox( SizedBox(
width: 250, width: 250,
child: ElevatedButton( child: ElevatedButton(
@@ -216,40 +411,7 @@ class LoginScreenState extends State<LoginScreen>
), ),
), ),
), ),
const SizedBox(height: 15),
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 16),
// child: Row(
// children: [
// const Expanded(child: Divider()),
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 8),
// child: Text(AppLocalizations.of(context).or),
// ),
// //const Expanded(child: Divider()),
// ],
// ),
// ),
const SizedBox(height: 25), const SizedBox(height: 25),
// Register Button
// SizedBox(
// width: 250,
// child: ElevatedButton(
// //disable until registration is implemented
// onPressed: null,
// style: OutlinedButton.styleFrom(
// shape: const StadiumBorder(),
// padding: const EdgeInsets.symmetric(vertical: 16),
// backgroundColor: Theme.of(context).colorScheme.primary,
// foregroundColor: Theme.of(context).colorScheme.onPrimary,
// ),
// child: Text(AppLocalizations.of(context).register,
// style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),),
// ),
// ),
], ],
), ),
), ),

View File

@@ -4,7 +4,6 @@ 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';
@@ -16,12 +15,18 @@ 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
@@ -78,7 +83,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) { if (widget.mode == MPinMode.enter && !widget.disableBiometric) {
_tryBiometricBeforePin(); _tryBiometricBeforePin();
} }
} }
@@ -173,29 +178,36 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
break; break;
case MPinMode.set: case MPinMode.set:
// propagate parent onCompleted into confirm step // Navigate to confirm and wait for result
Navigator.push( final result = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MPinScreen( builder: (_) => MPinScreen(
mode: MPinMode.confirm, mode: MPinMode.confirm,
initialPin: pin, initialPin: pin,
onCompleted: widget.onCompleted, // <-- use parent callback onCompleted: (confirmedPin) {
// 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);
// 3) now clear the entire navigation stack and go to your main scaffold // 2) Call the onCompleted callback to let the parent handle navigation
if (mounted) { if (mounted) {
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( widget.onCompleted?.call(pin);
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
} }
} else { } else {
setState(() { setState(() {
@@ -339,6 +351,9 @@ 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

@@ -0,0 +1,58 @@
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

@@ -264,278 +264,306 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
centerTitle: false, centerTitle: false,
), ),
body: SafeArea( body: SafeArea(
child: Form( child: Stack(
key: _formKey, children: [
child: Column( Form(
children: [ key: _formKey,
Expanded( child: Column(
child: SingleChildScrollView( children: [
physics: const AlwaysScrollableScrollPhysics(), Expanded(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(10.0), physics: const AlwaysScrollableScrollPhysics(),
child: Column( child: Padding(
children: [ padding: const EdgeInsets.all(10.0),
TextFormField( child: Column(
key: _accountNumberFieldKey, children: [
controller: accountNumberController, TextFormField(
decoration: InputDecoration( key: _accountNumberFieldKey,
labelText: AppLocalizations.of( controller: accountNumberController,
context, decoration: InputDecoration(
).accountNumber, labelText: AppLocalizations.of(
// prefixIcon: Icon(Icons.person), context,
border: const OutlineInputBorder(), ).accountNumber,
isDense: true, // prefixIcon: Icon(Icons.person),
), border: const OutlineInputBorder(),
obscureText: true, isDense: 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,
const SizedBox(height: 24), textInputAction: TextInputAction.next,
if (!_isBeneficiaryValidated) onChanged: (value) {
Padding( nameController.clear();
padding: const EdgeInsets.only(bottom: 24), setState(() {
child: SizedBox( _isBeneficiaryValidated = false;
width: double.infinity, });
child: ElevatedButton( },
onPressed: _isValidating || validator: (value) {
ifscController.text.length != 11 if (value == null || value.length < 10) {
? null return AppLocalizations.of(
: () { context,
final isAccountValid = ).enterValidAccountNumber;
_accountNumberFieldKey.currentState! }
.validate(); return null;
final isConfirmAccountValid = },
_confirmAccountNumberFieldKey ),
.currentState! const SizedBox(height: 24),
.validate(); // Confirm Account Number
final isIfscValid = _ifscFieldKey TextFormField(
.currentState! key: _confirmAccountNumberFieldKey,
.validate(); controller: confirmAccountNumberController,
decoration: InputDecoration(
if (isAccountValid && labelText: AppLocalizations.of(
isConfirmAccountValid && context,
isIfscValid) { ).confirmAccountNumber,
_validateBeneficiary(); // prefixIcon: Icon(Icons.person),
} border: const OutlineInputBorder(),
}, isDense: true,
child: _isValidating ),
? const SizedBox( keyboardType: TextInputType.number,
width: 20, textInputAction: TextInputAction.next,
height: 20, validator: (value) {
child: CircularProgressIndicator( if (value == null || value.isEmpty) {
strokeWidth: 2), return AppLocalizations.of(
) context,
: Text(AppLocalizations.of(context) ).reenterAccountNumber;
.validateBeneficiary), }
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),
//Beneficiary Name (Disabled) // 🔹 Branch Name (Disabled)
// 🔹 Account Type Dropdown TextFormField(
DropdownButtonFormField<String>( controller: branchNameController,
value: accountType, enabled:
decoration: InputDecoration( false, // changed from readOnly to disabled
labelText: AppLocalizations.of(context).accountType, decoration: InputDecoration(
border: const OutlineInputBorder(), labelText:
isDense: true, AppLocalizations.of(context).branchName,
), border: const OutlineInputBorder(),
items: [ isDense: true,
'Savings', ),
'Current', ),
] if (_isBeneficiaryValidated)
.map( Column(
(type) => DropdownMenuItem( children: [
value: type, const SizedBox(height: 24),
child: Text(type), TextFormField(
), controller: nameController,
) enabled: false,
.toList(), decoration: InputDecoration(
onChanged: (value) { suffixIcon: _isBeneficiaryValidated
setState(() { ? const Icon(
accountType = value!; 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!
.validate();
const SizedBox(height: 24), if (isAccountValid &&
TextFormField( isConfirmAccountValid &&
controller: phoneController, isIfscValid) {
keyboardType: TextInputType.phone, _validateBeneficiary();
decoration: InputDecoration( }
labelText: AppLocalizations.of(context).phone, },
prefixIcon: const Icon(Icons.phone), child: _isValidating
border: const OutlineInputBorder(), ? const SizedBox(
isDense: true, width: 20,
), height: 20,
textInputAction: TextInputAction.done, child: CircularProgressIndicator(
validator: (value) => strokeWidth: 2),
value == null || value.length != 10 )
: Text(AppLocalizations.of(context)
.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!;
});
},
),
const SizedBox(height: 24),
TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).phone,
prefixIcon: const Icon(Icons.phone),
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.done,
validator: (value) => 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,7 +22,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
_showSuccessDialog(context); _showSuccessDialog(context);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete beneficiary: $e')), SnackBar(content: Text('${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
); );
} }
} }
@@ -32,11 +32,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('Success'), title: Text(AppLocalizations.of(context).success),
content: const Text('Beneficiary deleted successfully.'), content: Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: const Text('OK'), child: Text(AppLocalizations.of(context).ok),
onPressed: () { onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
}, },
@@ -52,18 +52,18 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('Delete Beneficiary'), title: Text(AppLocalizations.of(context).deleteBeneficiary),
content: content:
const Text('Are you sure you want to delete this beneficiary?'), Text(AppLocalizations.of(context).areYouSureYouWantToDeleteThisBeneficiary),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: const Text('Cancel'), child: Text(AppLocalizations.of(context).cancel),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
TextButton( TextButton(
child: const Text('Delete'), child: Text(AppLocalizations.of(context).delete),
onPressed: () { onPressed: () {
_deleteBeneficiary(context); _deleteBeneficiary(context);
}, },
@@ -81,60 +81,80 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
title: Text(AppLocalizations.of(context).beneficiarydetails), title: Text(AppLocalizations.of(context).beneficiarydetails),
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(16.0),
children: [ child: Column(
Row( crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CircleAvatar( Row(
radius: 24, children: [
backgroundColor: Colors.transparent, CircleAvatar(
child: getBankLogo(beneficiary.bankName, context), radius: 24,
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(width: 16), const SizedBox(height: 24),
Text( _buildDetailRow('${AppLocalizations.of(context).bankName} ',
beneficiary.name, beneficiary.bankName ?? 'N/A'),
style: const TextStyle( _buildDetailRow(
fontSize: 20, fontWeight: FontWeight.bold), '${AppLocalizations.of(context).accountNumber} ',
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), ),
_buildDetailRow('${AppLocalizations.of(context).bankName} ', IgnorePointer(
beneficiary.bankName ?? 'N/A'), child: Center(
_buildDetailRow('${AppLocalizations.of(context).accountNumber} ', child: Opacity(
beneficiary.accountNo), opacity: 0.07, // Reduced opacity
_buildDetailRow('${AppLocalizations.of(context).accountType} ', child: ClipOval(
beneficiary.accountType), child: Image.asset(
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ', 'assets/images/logo.png',
beneficiary.ifscCode), width: 200, // Adjust size as needed
_buildDetailRow('${AppLocalizations.of(context).branchName} ', height: 200, // Adjust size as needed
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

@@ -109,7 +109,25 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries), title: Text(AppLocalizations.of(context).beneficiaries),
), ),
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(), body: Stack(
children: [
_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

@@ -61,132 +61,156 @@ class _BlockCardScreen extends State<BlockCardScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(10.0), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(10.0),
child: ListView( child: Form(
children: [ key: _formKey,
const SizedBox(height: 10), child: ListView(
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: [
Expanded( const SizedBox(height: 10),
child: TextFormField( TextFormField(
controller: _cvvController, controller: _cardController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv, labelText: AppLocalizations.of(context).cardNumber,
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(width: 16), const SizedBox(height: 24),
Expanded( Row(
child: TextFormField( children: [
controller: _expiryController, Expanded(
readOnly: true, child: TextFormField(
onTap: _pickExpiryDate, controller: _cvvController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).expiryDate, labelText: AppLocalizations.of(context).cvv,
suffixIcon: const Icon(Icons.calendar_today), border: const OutlineInputBorder(),
border: const OutlineInputBorder(), isDense: true,
isDense: true, filled: true,
filled: true, fillColor:
fillColor: Theme.of(context).scaffoldBackgroundColor, 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,
), ),
), ),
validator: (value) => value != null && value.isNotEmpty const SizedBox(width: 16),
? null Expanded(
: AppLocalizations.of(context).selectExpiryDate, child: TextFormField(
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(
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),
),
),
),
],
), ),
), 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

@@ -9,27 +9,45 @@ class CardDetailsScreen extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text("My Cards"), title: const Text("My Cards"),
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: ListView( Padding(
children: const [ padding: const EdgeInsets.all(16.0),
CardTile( child: ListView(
cardNumber: "**** **** **** 1234", children: const [
cardNetwork: "VISA", CardTile(
cardType: "Debit Card", cardNumber: "**** **** **** 1234",
validFrom: "01/22", cardNetwork: "VISA",
validTo: "01/27", cardType: "Debit Card",
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( IgnorePointer(
cardNumber: "**** **** **** 5678", child: Center(
cardNetwork: "Mastercard", child: Opacity(
cardType: "Debit Card", opacity: 0.07, // Reduced opacity
validFrom: "07/21", child: ClipOval(
validTo: "07/26", child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
), ),
], ),
), ],
), ),
); );
} }

View File

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

@@ -51,132 +51,156 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(10.0), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(10.0),
child: ListView( child: Form(
children: [ key: _formKey,
const SizedBox(height: 10), child: ListView(
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: [
Expanded( const SizedBox(height: 10),
child: TextFormField( TextFormField(
controller: _cvvController, controller: _cardController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv, labelText: AppLocalizations.of(context).cardNumber,
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(width: 16), const SizedBox(height: 24),
Expanded( Row(
child: TextFormField( children: [
controller: _expiryController, Expanded(
readOnly: true, child: TextFormField(
onTap: _pickExpiryDate, controller: _cvvController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).expiryDate, labelText: AppLocalizations.of(context).cvv,
suffixIcon: const Icon(Icons.calendar_today), border: const OutlineInputBorder(),
border: const OutlineInputBorder(), isDense: true,
isDense: true, filled: true,
filled: true, fillColor:
fillColor: Theme.of(context).scaffoldBackgroundColor, 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,
), ),
), ),
validator: (value) => value != null && value.isNotEmpty const SizedBox(width: 16),
? null Expanded(
: AppLocalizations.of(context).selectExpiryDate, child: TextFormField(
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(
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),
),
),
),
],
), ),
), 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

@@ -51,87 +51,105 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(16.0),
child: Column( child: Form(
children: [ key: _formKey,
TextFormField( child: Column(
controller: _pinController, children: [
obscureText: true, TextFormField(
decoration: InputDecoration( controller: _pinController,
labelText: AppLocalizations.of(context).enterNewPin, obscureText: true,
border: const OutlineInputBorder(), decoration: InputDecoration(
isDense: true, labelText: AppLocalizations.of(context).enterNewPin,
filled: true, border: const OutlineInputBorder(),
fillColor: Theme.of(context).scaffoldBackgroundColor, isDense: true,
enabledBorder: const OutlineInputBorder( filled: true,
borderSide: BorderSide(color: Colors.black), fillColor: Theme.of(context).scaffoldBackgroundColor,
), enabledBorder: const OutlineInputBorder(
focusedBorder: const OutlineInputBorder( borderSide: BorderSide(color: Colors.black),
borderSide: BorderSide(color: Colors.black, width: 2), ),
), 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),
TextFormField(
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(
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), 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),
TextFormField(
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(
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),
),
),
),
],
),
),
),
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

@@ -22,50 +22,69 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: ListView( body: Stack(
children: [ children: [
const SizedBox(height: 15), ListView(
ChequeManagementTile( children: [
icon: Symbols.add, const SizedBox(height: 15),
label: AppLocalizations.of(context).requestChequeBook, ChequeManagementTile(
onTap: () {}, icon: Symbols.add,
label: AppLocalizations.of(context).requestChequeBook,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.data_alert,
label: AppLocalizations.of(context).enquiry,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EnquiryScreen()),
);
},
),
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),
],
), ),
const Divider(height: 1), IgnorePointer(
ChequeManagementTile( child: Center(
icon: Symbols.data_alert, child: Opacity(
label: AppLocalizations.of(context).enquiry, opacity: 0.07, // Reduced opacity
onTap: () { child: ClipOval(
Navigator.push( child: Image.asset(
context, 'assets/images/logo.png',
MaterialPageRoute(builder: (context) => const EnquiryScreen()), width: 200, // Adjust size as needed
); 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

@@ -33,74 +33,92 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
.replaceFirst(RegExp('\n'), ''), .replaceFirst(RegExp('\n'), ''),
), ),
), ),
body: SingleChildScrollView( body: Stack(
physics: const AlwaysScrollableScrollPhysics(), children: [
child: Padding( SingleChildScrollView(
padding: const EdgeInsets.all(16.0), physics: const AlwaysScrollableScrollPhysics(),
child: SafeArea( child: Padding(
child: Center( padding: const EdgeInsets.all(16.0),
child: Column( child: SafeArea(
children: [ child: Center(
const SizedBox(height: 30), child: Column(
CircleAvatar( children: [
radius: 50, const SizedBox(height: 30),
child: SvgPicture.asset( CircleAvatar(
'assets/images/avatar_male.svg', radius: 50,
width: 150, child: SvgPicture.asset(
height: 150, 'assets/images/avatar_male.svg',
fit: BoxFit.cover, width: 150,
), height: 150,
), fit: BoxFit.cover,
Padding( ),
padding: const EdgeInsets.only(top: 10.0),
child: Text(
user.name ?? '',
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
), ),
), Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
user.name ?? '',
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
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
],
), ),
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
],
), ),
), ),
), ),
), 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

@@ -15,6 +15,7 @@ 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';
@@ -147,6 +148,8 @@ 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";
default: default:
return AppLocalizations.of(context).unknownAccount; return AppLocalizations.of(context).unknownAccount;
} }
@@ -266,6 +269,10 @@ 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;
@@ -493,6 +500,7 @@ class _DashboardScreenState extends State<DashboardScreen>
), ),
); );
}, },
disable: isPaymentDisabled,
), ),
_buildQuickLink(Symbols.send_money, _buildQuickLink(Symbols.send_money,
AppLocalizations.of(context).fundTransfer, () { AppLocalizations.of(context).fundTransfer, () {
@@ -504,9 +512,10 @@ class _DashboardScreenState extends State<DashboardScreen>
users[selectedAccountIndex] users[selectedAccountIndex]
.accountNo!, .accountNo!,
remitterName: remitterName:
users[selectedAccountIndex] users[selectedAccountIndex].name!,
.name!))); // Pass the full list of accounts
}, disable: false), accounts: users)));
}, disable: isPaymentDisabled),
_buildQuickLink( _buildQuickLink(
Symbols.server_person, Symbols.server_person,
AppLocalizations.of(context).accountInfo, AppLocalizations.of(context).accountInfo,
@@ -539,9 +548,14 @@ class _DashboardScreenState extends State<DashboardScreen>
.accountType!, .accountType!,
))); )));
}), }),
_buildQuickLink(Symbols.checkbook, _buildQuickLink(Icons.location_pin, "Branch Locator",
AppLocalizations.of(context).handleCheque, () {}, () {
disable: true), Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const BranchLocatorScreen()));
}, disable: false),
_buildQuickLink(Icons.group, _buildQuickLink(Icons.group,
AppLocalizations.of(context).manageBeneficiary, AppLocalizations.of(context).manageBeneficiary,
() { () {

View File

@@ -1,5 +1,3 @@
// 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';
@@ -12,31 +10,45 @@ 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(scheme: 'mailto', path: email); final Uri emailUri = Uri(
scheme: 'mailto',
path: email,
query: 'subject=Enquiry', // Pre-fills the subject line
);
if (await canLaunchUrl(emailUri)) { if (await canLaunchUrl(emailUri)) {
await launchUrl(emailUri); // Use external application mode
await launchUrl(emailUri, mode: LaunchMode.externalApplication);
} else { } else {
debugPrint('${AppLocalizations.of(context).emailLaunchError} $email'); ScaffoldMessenger.of(context).showSnackBar(
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 {
debugPrint('${AppLocalizations.of(context).dialerLaunchError} $phone'); ScaffoldMessenger.of(context).showSnackBar(
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)) {
await launchUrl(uri); // Use external application mode
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else { } else {
// Consider adding a 'urlLaunchError' key to your AppLocalizations ScaffoldMessenger.of(context).showSnackBar(
debugPrint('Could not launch $url'); SnackBar(content: Text('Could not launch $url')),
);
} }
} }
@@ -57,7 +69,7 @@ class _EnquiryScreen extends State<EnquiryScreen> {
onTap: () => _launchPhoneNumber(phone), onTap: () => _launchPhoneNumber(phone),
child: Text(phone, child: Text(phone,
style: style:
TextStyle(color: Theme.of(context).scaffoldBackgroundColor)), TextStyle(color: Theme.of(context).colorScheme.primary)), // Changed color for visibility
), ),
], ],
); );
@@ -70,67 +82,87 @@ class _EnquiryScreen extends State<EnquiryScreen> {
title: Text(AppLocalizations.of(context).enquiry), title: Text(AppLocalizations.of(context).enquiry),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(16.0),
children: [ child: Column(
const SizedBox(height: 20), crossAxisAlignment: CrossAxisAlignment.start,
GestureDetector( children: [
onTap: () => _launchUrl("https://kccb.in/complaint-form"), const SizedBox(height: 20),
child: Row(mainAxisSize: MainAxisSize.min, children: [ GestureDetector(
Text( onTap: () => _launchUrl("https://kccbhp.bank.in/complaint-form/"),
"Complaint Form", child: Row(mainAxisSize: MainAxisSize.min, children: [
style: TextStyle( Text(
fontSize: 17, "Complaint Form",
color: Theme.of(context).colorScheme.primary, style: TextStyle(
decorationColor: Theme.of(context).colorScheme.primary, fontSize: 17,
), color: Theme.of(context).colorScheme.primary,
), decoration: TextDecoration.underline, // Added underline for link clarity
const SizedBox(width: 4), decorationColor:
Icon( Theme.of(context).colorScheme.primary,
Icons.open_in_new, ),
),
const SizedBox(width: 4),
Icon(
Icons.open_in_new,
color: Theme.of(context).colorScheme.primary,
size: 16.0,
),
])),
const SizedBox(height: 40),
Text(
AppLocalizations.of(context).keyContacts,
style: TextStyle(
fontSize: 17,
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
size: 16.0,
), ),
])), // horizontal line
const SizedBox(height: 40), ),
Text( Divider(color: Theme.of(context).colorScheme.outline),
AppLocalizations.of(context).keyContacts, const SizedBox(height: 16),
style: TextStyle( _buildContactItem(
fontSize: 17, AppLocalizations.of(context).chairman,
color: Theme.of(context).colorScheme.primary, "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",
),
],
),
),
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

@@ -0,0 +1,92 @@
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

@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -40,13 +42,69 @@ class FundTransferAmountScreen extends StatefulWidget {
} }
class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> { class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
bool _isAmountOverLimit = false;
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
void initState() {
super.initState();
_loadLimit(); // Call the new method
_amountController.addListener(_checkAmountLimit);
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
@override @override
void dispose() { void dispose() {
_amountController.removeListener(_checkAmountLimit);
_amountController.dispose(); _amountController.dispose();
_remarksController.dispose(); _remarksController.dispose();
super.dispose(); super.dispose();
@@ -304,149 +362,175 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')), title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(16.0),
child: Column( child: Form(
crossAxisAlignment: CrossAxisAlignment.start, key: _formKey,
children: [ child: Column(
// Debit Account (User) crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
loc.debitFrom, // Debit Account (User)
style: Theme.of(context).textTheme.titleSmall, Text(
), loc.debitFrom,
Card( style: Theme.of(context).textTheme.titleSmall,
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), Card(
subtitle: Text(widget.debitAccountNo), elevation: 0,
), margin: const EdgeInsets.symmetric(vertical: 8.0),
), child: ListTile(
const SizedBox(height: 24), leading: Image.asset(
'assets/images/logo.png',
// Credit Account (Beneficiary) width: 40,
Text( height: 40,
AppLocalizations.of(context).creditedTo, ),
style: Theme.of(context).textTheme.titleSmall, title: Text(widget.remitterName),
), subtitle: Text(widget.debitAccountNo),
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( const SizedBox(height: 24),
isSelected: [
_selectedMode == TransactionMode.neft, // Credit Account (Beneficiary)
_selectedMode == TransactionMode.rtgs, Text(
_selectedMode == TransactionMode.imps, AppLocalizations.of(context).creditedTo,
], style: Theme.of(context).textTheme.titleSmall,
onPressed: (index) { ),
setState(() { Card(
_selectedMode = TransactionMode.values[index]; 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;
}, },
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: 8),
const SizedBox(height: 24), if (_isLoadingLimit) Text(AppLocalizations.of(context).fetchingDailyLimit),
], if (!_isLoadingLimit && _limit != null)
//Remarks Text(
TextFormField( 'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
controller: _remarksController, style: Theme.of(context).textTheme.bodySmall,
decoration: InputDecoration( ),
labelText: AppLocalizations.of(context).remarks, const Spacer(),
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 Spacer(),
// Proceed Button // Proceed Button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _onProceed, onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).proceed),
),
),
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
), ),
child: Text(AppLocalizations.of(context).proceed),
), ),
), ),
const SizedBox(height: 10), ),
],
), ),
), ],
), ),
), ),
); );

View File

@@ -1,4 +1,5 @@
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';
@@ -81,38 +82,73 @@ class _FundTransferBeneficiaryScreenState
itemCount: _beneficiaries.length, itemCount: _beneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final beneficiary = _beneficiaries[index]; final beneficiary = _beneficiaries[index];
return ListTile(
leading: CircleAvatar( // --- Cooldown Logic ---
radius: 24, bool isCoolingDown = false;
backgroundColor: Colors.transparent, if (beneficiary.createdAt != null) {
child: getBankLogo(beneficiary.bankName, context), final sixtyMinutesAgo =
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 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,
),
),
);
},
); );
}, },
); );
@@ -124,7 +160,25 @@ class _FundTransferBeneficiaryScreenState
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries), title: Text(AppLocalizations.of(context).beneficiaries),
), ),
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(), body: Stack(
children: [
_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,63 +1,117 @@
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'; import '../../../l10n/app_localizations.dart'; // Keep localizations
class FundTransferScreen extends StatelessWidget { class FundTransferScreen extends StatelessWidget {
final String creditAccountNo; final String creditAccountNo;
final String remitterName; final String remitterName;
final List<User> accounts; // Continue to accept the list of accounts
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'), '')),
), ),
body: ListView( // Wrap with BlocBuilder to check the authentication state
children: [ body: BlocBuilder<AuthCubit, AuthState>(
FundTransferManagementTile( builder: (context, state) {
icon: Symbols.input_circle, return Stack(
label: AppLocalizations.of(context).ownBank, children: [
onTap: () { ListView(
Navigator.push( children: [
context, FundTransferManagementTile(
MaterialPageRoute( icon: Symbols.person,
builder: (context) => FundTransferBeneficiaryScreen( // Restore localization for the label
creditAccountNo: creditAccountNo, label: "Self Pay",
remitterName: remitterName, onTap: () {
isOwnBank: true, // The accounts list is passed directly from the constructor
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferSelfAccountsScreen(
debitAccountNo: creditAccountNo,
remitterName: remitterName,
accounts: accounts,
),
),
);
},
// Disable the tile if the state is not Authenticated
disable: state is! Authenticated,
),
const Divider(height: 1),
FundTransferManagementTile(
icon: Symbols.input_circle,
// Restore localization for the label
label: AppLocalizations.of(context).ownBank,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: true,
),
),
);
},
),
const Divider(height: 1),
FundTransferManagementTile(
icon: Symbols.output_circle,
// Restore localization for the label
label: AppLocalizations.of(context).outsideBank,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: false,
),
),
);
},
),
const Divider(height: 1),
],
),
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),
],
), ),
); );
} }

View File

@@ -0,0 +1,112 @@
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 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

@@ -0,0 +1,262 @@
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,36 +116,62 @@ 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: 32, width: 50,
margin: const EdgeInsets.symmetric(horizontal: 8), height: 60,
child: TextField( margin: const EdgeInsets.symmetric(horizontal: 6),
controller: _controllers[i], child: Stack(
focusNode: _focusNodes[i], alignment: Alignment.center,
keyboardType: TextInputType.number, children: [
textAlign: TextAlign.center, TextField(
maxLength: 1, controller: _controllers[i],
obscureText: true, focusNode: _focusNodes[i],
obscuringCharacter: '*', keyboardType: TextInputType.number,
decoration: InputDecoration( textAlign: TextAlign.center,
counterText: '', maxLength: 1,
filled: true, style: const TextStyle(
fillColor: Theme.of(context).primaryColorLight, color: Colors.transparent,
border: OutlineInputBorder( fontSize: 24,
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2,
), ),
), decoration: InputDecoration(
focusedBorder: OutlineInputBorder( counterText: '',
borderRadius: BorderRadius.circular(12), filled: true,
borderSide: BorderSide( fillColor: Colors.grey[200],
color: theme.colorScheme.primary, contentPadding:
width: 2.5, 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)
onChanged: (val) => _onOtpChanged(i, val), Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.black87,
shape: BoxShape.circle,
),
),
],
), ),
); );
}), }),

View File

@@ -143,6 +143,20 @@ 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

@@ -0,0 +1,96 @@
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,40 +70,58 @@ 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: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: _isLoading Padding(
? const Center(child: CircularProgressIndicator()) padding: const EdgeInsets.all(16.0),
: Column( child: _isLoading
crossAxisAlignment: CrossAxisAlignment.center, ? const Center(child: CircularProgressIndicator())
children: [ : Column(
Text( crossAxisAlignment: CrossAxisAlignment.center,
AppLocalizations.of(context).otpSent, children: [
textAlign: TextAlign.center, Text(
style: const TextStyle(fontSize: 16), AppLocalizations.of(context).otpSent,
), textAlign: TextAlign.center,
const SizedBox(height: 24), style: const TextStyle(fontSize: 16),
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),
), 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

@@ -90,67 +90,85 @@ 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: Padding( body: Stack(
padding: const EdgeInsets.all(16), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(16),
child: Column( child: Form(
children: [ key: _formKey,
TextFormField( child: Column(
controller: currentPasswordController, children: [
obscureText: !_showCurrentPassword, TextFormField(
decoration: InputDecoration( controller: currentPasswordController,
labelText: AppLocalizations.of(context).currentpwd, obscureText: !_showCurrentPassword,
suffixIcon: IconButton( decoration: InputDecoration(
icon: Icon(_showCurrentPassword labelText: AppLocalizations.of(context).currentpwd,
? Icons.visibility suffixIcon: IconButton(
: Icons.visibility_off), icon: Icon(_showCurrentPassword
onPressed: () => setState( ? Icons.visibility
() => _showCurrentPassword = !_showCurrentPassword), : Icons.visibility_off),
onPressed: () => setState(
() => _showCurrentPassword = !_showCurrentPassword),
),
),
validator: validateCurrentPassword,
), ),
), const SizedBox(height: 16),
validator: validateCurrentPassword, TextFormField(
), controller: newPasswordController,
const SizedBox(height: 16), obscureText: !_showNewPassword,
TextFormField( decoration: InputDecoration(
controller: newPasswordController, labelText: AppLocalizations.of(context).newpwd,
obscureText: !_showNewPassword, suffixIcon: IconButton(
decoration: InputDecoration( icon: Icon(_showNewPassword
labelText: AppLocalizations.of(context).newpwd, ? Icons.visibility
suffixIcon: IconButton( : Icons.visibility_off),
icon: Icon(_showNewPassword onPressed: () => setState(
? Icons.visibility () => _showNewPassword = !_showNewPassword),
: Icons.visibility_off), ),
onPressed: () => ),
setState(() => _showNewPassword = !_showNewPassword), validator: validateNewPassword,
), ),
), const SizedBox(height: 16),
validator: validateNewPassword, TextFormField(
), controller: confirmPasswordController,
const SizedBox(height: 16), obscureText: !_showConfirmPassword,
TextFormField( decoration: InputDecoration(
controller: confirmPasswordController, labelText: AppLocalizations.of(context).confirmpwd,
obscureText: !_showConfirmPassword, suffixIcon: IconButton(
decoration: InputDecoration( icon: Icon(_showConfirmPassword
labelText: AppLocalizations.of(context).confirmpwd, ? Icons.visibility
suffixIcon: IconButton( : Icons.visibility_off),
icon: Icon(_showConfirmPassword onPressed: () => setState(
? Icons.visibility () => _showConfirmPassword = !_showConfirmPassword),
: Icons.visibility_off), ),
onPressed: () => setState( ),
() => _showConfirmPassword = !_showConfirmPassword), validator: validateConfirmPassword,
), ),
), const SizedBox(height: 24),
validator: validateConfirmPassword, ElevatedButton(
onPressed: _proceed,
child: Text(AppLocalizations.of(context).proceed),
),
],
), ),
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
),
),
),
),
),
],
), ),
); );
} }

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; 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/di/injection.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';
@@ -11,13 +13,27 @@ class DailyLimitScreen extends StatefulWidget {
class _DailyLimitScreenState extends State<DailyLimitScreen> { class _DailyLimitScreenState extends State<DailyLimitScreen> {
double? _currentLimit; double? _currentLimit;
double? _spentAmount = 0.0;
final _limitController = TextEditingController(); final _limitController = TextEditingController();
var service = getIt<LimitService>();
Limit? limit;
bool _isLoading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Now just taking null, but for real time limit will be fetched using API call _loadlimits();
_currentLimit = null; }
Future<void> _loadlimits() async {
setState(() {
_isLoading = true;
});
final limit_data = await service.getLimit();
setState(() {
limit = limit_data;
_isLoading = false;
});
} }
@override @override
@@ -30,8 +46,9 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
_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: (context) { builder: (dialogContext) {
final localizations = AppLocalizations.of(context); final localizations = AppLocalizations.of(dialogContext);
final theme = Theme.of(dialogContext);
return AlertDialog( return AlertDialog(
title: Text( title: Text(
_currentLimit == null _currentLimit == null
@@ -53,14 +70,26 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(localizations.cancel), child: Text(localizations.cancel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
final value = double.tryParse(_limitController.text); final value = double.tryParse(_limitController.text);
if (value != null && value > 0) { if (value == null || value <= 0) return;
Navigator.of(context).pop(value);
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), child: Text(localizations.save),
@@ -69,24 +98,40 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
); );
}, },
); );
if (newLimit != null) {
setState(() {
_currentLimit = newLimit;
});
}
}
void _removeLimit() { if (newLimit != null) {
setState(() { _loadlimits();
_currentLimit = null; if (!mounted) return;
}); ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Limit Updated"),
behavior: SnackBarBehavior.floating,
),
);
}
} }
@override @override
Widget build(BuildContext context) { 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 localizations = AppLocalizations.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final remainingLimit =
_currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(localizations.dailylimit), title: Text(localizations.dailylimit),
@@ -116,6 +161,23 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
: theme.colorScheme.primary, : 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), const SizedBox(height: 48),
if (_currentLimit == null) if (_currentLimit == null)
ElevatedButton.icon( ElevatedButton.icon(
@@ -142,14 +204,14 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton.icon( // TextButton.icon(
onPressed: _removeLimit, // onPressed: _removeLimit,
icon: const Icon(Icons.remove_circle_outline), // icon: const Icon(Icons.remove_circle_outline),
label: Text(localizations.removeLimit), // label: Text(localizations.removeLimit),
style: TextButton.styleFrom( // style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.error, // foregroundColor: theme.colorScheme.error,
), // ),
), // ),
], ],
), ),
], ],

View File

@@ -20,43 +20,61 @@ class PreferenceScreen extends StatelessWidget {
), ),
body: BlocBuilder<ThemeCubit, ThemeState>( body: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) { builder: (context, state) {
return ListView( return Stack(
children: [ children: [
//Set Prefered Username ListView(
// ListTile( children: [
// leading: const Icon(Icons.person), //Set Prefered Username
// title: const Text("Set Prefered Username"), // ListTile(
// onTap: () { // leading: const Icon(Icons.person),
// }), // title: const Text("Set Prefered Username"),
// Language Selection // onTap: () {
ListTile( // }),
leading: const Icon(Icons.language), // Language Selection
title: Text(loc.language), ListTile(
onTap: () { leading: const Icon(Icons.language),
showDialog( title: Text(loc.language),
context: context, onTap: () {
builder: (_) => const LanguageDialog(), showDialog(
); context: context,
}, builder: (_) => const LanguageDialog(),
);
},
),
//Theme Mode Switch (Light/Dark)
ListTile(
leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode),
onTap: () {
showThemeModeDialog(context);
},
),
//Color_Theme_Selection
ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
onTap: () {
showDialog(
context: context,
builder: (_) => const ColorThemeDialog(),
);
}),
],
), ),
//Theme Mode Switch (Light/Dark) IgnorePointer(
ListTile( child: Center(
leading: const Icon(Icons.brightness_6), child: Opacity(
title: Text(AppLocalizations.of(context).themeMode), opacity: 0.07, // Reduced opacity
onTap: () { child: ClipOval(
showThemeModeDialog(context); child: Image.asset(
}, '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,8 +2,9 @@ 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/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:package_info_plus/package_info_plus.dart';
@@ -35,9 +36,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
Future<void> _loadBiometricStatus() async { Future<void> _loadBiometricStatus() async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final isEnabled = await storage.read('biometric_enabled'); final enabled = await storage.read('biometric_enabled');
setState(() { setState(() {
_isBiometricEnabled = isEnabled == 'true'; _isBiometricEnabled = enabled ?? false;
}); });
} }
@@ -56,16 +57,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) { if (!canCheck) {
// Optional: Show a snackbar or dialog if biometrics are not available if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context).biometricsNotAvailable)), content:
); Text(AppLocalizations.of(context).biometricsNotAvailable)),
);
}
return; return;
} }
if (enable) { if (enable) {
// Show "Enable" dialog
final optIn = await showDialog<bool>( final optIn = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -95,17 +97,31 @@ class _ProfileScreenState extends State<ProfileScreen> {
), ),
); );
if (didAuth) { if (didAuth) {
await storage.write('biometric_enabled', 'true'); await storage.write('biometric_enabled', true);
setState(() { if (mounted) {
_isBiometricEnabled = true; setState(() {
}); _isBiometricEnabled = true;
});
}
} else {
// Authentication failed, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
} }
} catch (e) { } catch (e) {
// Handle authentication errors // Handle exceptions, reload state to ensure consistency
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,
@@ -126,10 +142,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
); );
if (optOut == true) { if (optOut == true) {
await storage.write('biometric_enabled', 'false'); await storage.write('biometric_enabled', false);
setState(() { if (mounted) {
_isBiometricEnabled = false; setState(() {
}); _isBiometricEnabled = false;
});
}
} else {
// User cancelled, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
} }
} }
} }
@@ -142,116 +165,139 @@ class _ProfileScreenState extends State<ProfileScreen> {
appBar: AppBar( appBar: AppBar(
title: Text(loc.profile), // Localized "Profile" title: Text(loc.profile), // Localized "Profile"
), ),
body: ListView( body: Stack(
children: [ children: [
ListTile( ListView(
leading: const Icon(Icons.settings), children: [
title: Text(loc.preferences), ListTile(
onTap: () { leading: const Icon(Icons.settings),
Navigator.push( title: Text(loc.preferences),
context, trailing: const Icon(Icons.chevron_right),
MaterialPageRoute( onTap: () {
builder: (context) => const PreferenceScreen()), Navigator.push(
); context,
}, MaterialPageRoute(
), builder: (context) => const PreferenceScreen()),
SwitchListTile(
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
),
ListTile(
leading: const Icon(Icons.password),
title: Text(loc.changeLoginPassword),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordScreen(
mobileNumber: widget.mobileNumber,
)),
);
},
),
// ListTile(
// leading: const Icon(Icons.password),
// title: const Text("Manage TPIN"),
// onTap: () async {
// },
// ),
// ListTile(
// leading: const Icon(Icons.password),
// title: const Text("Change Login MPIN"),
// onTap: () async {
// },
// ),
ListTile(
leading: const Icon(Icons.smartphone),
title: const Text("App Version"),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Show a loading indicator while waiting for the future to complete
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return const Text("Error");
} else {
// Display the version number once the future is complete
return Text(
snapshot.data ?? "N/A",
selectionColor: const Color(0xFFFFFFFF),
); );
} },
}, ),
), ListTile(
), leading: const Icon(Icons.security),
ListTile( title: Text(loc.securitySettings),
leading: const Icon(Icons.exit_to_app), trailing: const Icon(Icons.chevron_right),
title: Text(AppLocalizations.of(context).logout), onTap: () {
onTap: () async { Navigator.push(
final shouldExit = await showDialog<bool>( context,
context: context, MaterialPageRoute(
builder: (context) => AlertDialog( builder: (context) => SecuritySettingsScreen(
title: Text(AppLocalizations.of(context).logout), mobileNumber: widget.mobileNumber,
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), ),
), ListTile(
], leading: const Icon(Icons.currency_rupee),
title: Text(AppLocalizations.of(context).dailylimit),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DailyLimitScreen()),
);
},
),
SwitchListTile(
title:
Text(AppLocalizations.of(context).enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
// The state is now managed within _handleBiometricToggle
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
),
ListTile(
leading: const Icon(Icons.smartphone),
title: const Text("App Version"),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Show a loading indicator while waiting for the future to complete
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return const Text("Error");
} else {
// Display the version number once the future is complete
return Text(
snapshot.data ?? "N/A",
selectionColor: const Color(0xFFFFFFFF),
);
}
},
), ),
); ),
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),
),
],
),
);
if (shouldExit == true) { if (shouldExit == true) {
if (Platform.isAndroid) { if (Platform.isAndroid) {
SystemNavigator.pop(); SystemNavigator.pop();
} }
exit(0); 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(),
);
if (shouldLogout == true) {
await _handleLogout(context);
}
},
),
],
), ),
ListTile( IgnorePointer(
leading: const Icon(Icons.logout), child: Center(
title: Text(AppLocalizations.of(context).deregister), child: Opacity(
onTap: () async { opacity: 0.07, // Reduced opacity
final shouldLogout = await showDialog<bool>( child: ClipOval(
context: context, child: Image.asset(
builder: (_) => const LogoutDialog(), 'assets/images/logo.png',
); width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
if (shouldLogout == true) { ),
await _handleLogout(context); ),
} ),
}, ),
), ),
], ],
), ),

View File

@@ -0,0 +1,140 @@
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(
children: [
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,
),
),
);
},
),
const Divider(height: 1),
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: Colors.green,
),
);
}
},
),
const Divider(height: 1),
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

@@ -0,0 +1,125 @@
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

@@ -0,0 +1,147 @@
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

@@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/data/models/imps_transaction.dart'; import 'package:kmobile/data/models/imps_transaction.dart';
@@ -28,7 +30,10 @@ class QuickPayOutsideBankScreen extends StatefulWidget {
class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> { class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
// Controllers // Controllers
final accountNumberController = TextEditingController(); final accountNumberController = TextEditingController();
final confirmAccountNumberController = TextEditingController(); final confirmAccountNumberController = TextEditingController();
@@ -41,6 +46,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final remarksController = TextEditingController(); final remarksController = TextEditingController();
final _ifscFocusNode = FocusNode(); final _ifscFocusNode = FocusNode();
final service = getIt<BeneficiaryService>(); final service = getIt<BeneficiaryService>();
bool _isAmountOverLimit = false;
late String accountType; late String accountType;
bool _isValidating = false; bool _isValidating = false;
@@ -50,6 +56,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit();
_ifscFocusNode.addListener(() { _ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) { if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC(); _validateIFSC();
@@ -60,6 +67,50 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
accountType = 'Savings'; accountType = 'Savings';
}); });
}); });
amountController.addListener(_checkAmountLimit);
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
} }
void _validateIFSC() async { void _validateIFSC() async {
@@ -407,415 +458,488 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(12), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(12),
child: ListView( child: Form(
children: [ key: _formKey,
const SizedBox(height: 10), child: ListView(
Text(
AppLocalizations.of(context).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.debitAccount),
subtitle: Text(AppLocalizations.of(context).ownBank),
),
),
const SizedBox(height: 24),
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,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).accountNumberRequired;
} else if (value.length < 7 || value.length > 20) {
return AppLocalizations.of(context).accno7to20;
}
return null;
},
),
const SizedBox(height: 24),
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;
},
),
const SizedBox(height: 25),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextFormField(
focusNode: _ifscFocusNode,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ifscCode,
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: ifscController,
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(
width: 10,
),
Expanded(
child: DropdownButtonFormField<String>(
value: accountType,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).accountType,
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),
),
),
items: [
'Savings',
'Current',
]
.map(
(e) => DropdownMenuItem(value: e, child: Text(e)),
)
.toList(),
onChanged: (value) => setState(() {
accountType = value!;
}),
),
),
],
),
const SizedBox(height: 25),
TextFormField(
controller: bankNameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
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(
controller: branchNameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
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: 24),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
_isValidating || ifscController.text.length != 11
? null
: () {
if (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 (_validationError != null)
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: Text(
_validationError!,
style:
TextStyle(color: Theme.of(context).colorScheme.error),
),
),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).name,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).nameRequired;
}
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),
Row(
children: [
Expanded(
child: TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
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: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null || value.isEmpty
? AppLocalizations.of(context).phoneRequired
: null,
),
),
const SizedBox(width: 10),
Expanded(
child: 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: 30),
Row(
children: [ children: [
const SizedBox(height: 10),
Text( Text(
AppLocalizations.of(context).transactionMode, AppLocalizations.of(context).debitFrom,
style: const TextStyle(fontWeight: FontWeight.w500), 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.debitAccount),
subtitle: Text(AppLocalizations.of(context).ownBank),
),
),
const SizedBox(height: 24),
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,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context)
.accountNumberRequired;
} else if (value.length < 7 || value.length > 20) {
return AppLocalizations.of(context).accno7to20;
}
return null;
},
),
const SizedBox(height: 24),
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;
},
),
const SizedBox(height: 25),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextFormField(
focusNode: _ifscFocusNode,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ifscCode,
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: ifscController,
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(
width: 10,
),
Expanded(
child: DropdownButtonFormField<String>(
value: accountType,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).accountType,
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),
),
),
items: [
'Savings',
'Current',
]
.map(
(e) =>
DropdownMenuItem(value: e, child: Text(e)),
)
.toList(),
onChanged: (value) => setState(() {
accountType = value!;
}),
),
),
],
),
const SizedBox(height: 25),
TextFormField(
controller: bankNameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
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(
controller: branchNameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
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: 24),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed:
_isValidating || ifscController.text.length != 11
? null
: () {
if (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 (_validationError != null)
Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: Text(
_validationError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error),
),
),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).name,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).nameRequired;
}
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),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: TextFormField(
controller: phoneController,
keyboardType: TextInputType.phone,
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: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context)
.colorScheme
.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color:
Theme.of(context).colorScheme.primary,
width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null ||
value.isEmpty
? AppLocalizations.of(context).phoneRequired
: null,
),
),
const SizedBox(width: 10),
Expanded(
child: 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 Padding(
padding: EdgeInsets.only(left: 8.0),
child: Text('Fetching daily limit...'),
),
if (!_isLoadingLimit && _limit != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
),
const SizedBox(height: 30),
Row(
children: [
Text(
AppLocalizations.of(context).transactionMode,
style: const TextStyle(fontWeight: FontWeight.w500),
),
const SizedBox(width: 12),
Expanded(child: buildTransactionModeSelector()),
],
),
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,
onSwipe: () {
if (_isAmountOverLimit) {
return; // Do nothing if amount is over the limit
}
_onProceedToPay();
},
child: Text(
AppLocalizations.of(context).swipeToPay,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
), ),
const SizedBox(width: 12),
Expanded(child: buildTransactionModeSelector()),
], ],
), ),
const SizedBox(height: 45), ),
Align( ),
alignment: Alignment.center, IgnorePointer(
child: SwipeButton.expand( child: Center(
thumb: Icon(Icons.arrow_forward, child: Opacity(
color: Theme.of(context).dialogBackgroundColor), opacity: 0.07, // Reduced opacity
activeThumbColor: Theme.of(context).colorScheme.primary, child: ClipOval(
activeTrackColor: child: Image.asset(
Theme.of(context).colorScheme.secondary.withAlpha(100), 'assets/images/logo.png',
borderRadius: BorderRadius.circular(30), width: 200, // Adjust size as needed
height: 56, height: 200, // Adjust size as needed
onSwipe: _onProceedToPay,
child: Text(
AppLocalizations.of(context).swipeToPay,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
), ),
), ),
), ),
], ),
), ),
), ],
), ),
); );
} }

View File

@@ -21,38 +21,56 @@ class _QuickPayScreen extends State<QuickPayScreen> {
AppLocalizations.of(context).quickPay.replaceAll('\n', ''), AppLocalizations.of(context).quickPay.replaceAll('\n', ''),
), ),
), ),
body: ListView( body: Stack(
children: [ children: [
QuickPayManagementTile( ListView(
icon: Symbols.input_circle, children: [
label: AppLocalizations.of(context).ownBank, QuickPayManagementTile(
onTap: () { icon: Symbols.input_circle,
Navigator.push( label: AppLocalizations.of(context).ownBank,
context, onTap: () {
MaterialPageRoute( Navigator.push(
builder: (context) => QuickPayWithinBankScreen( context,
debitAccount: widget.debitAccount, MaterialPageRoute(
builder: (context) => QuickPayWithinBankScreen(
debitAccount: widget.debitAccount,
),
),
);
},
),
const Divider(height: 1),
QuickPayManagementTile(
icon: Symbols.output_circle,
label: AppLocalizations.of(context).outsideBank,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuickPayOutsideBankScreen(
debitAccount: widget.debitAccount,
),
),
);
},
),
const Divider(height: 1),
],
),
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),
QuickPayManagementTile(
icon: Symbols.output_circle,
label: AppLocalizations.of(context).outsideBank,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => QuickPayOutsideBankScreen(
debitAccount: widget.debitAccount,
),
),
);
},
),
const Divider(height: 1),
], ],
), ),
); );

View File

@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_swipe_button/flutter_swipe_button.dart'; import 'package:flutter_swipe_button/flutter_swipe_button.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/beneficiary_service.dart'; import 'package:kmobile/api/services/beneficiary_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/payment_service.dart'; import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
@@ -19,14 +21,17 @@ 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>();
Limit? _limit;
bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final TextEditingController accountNumberController = TextEditingController(); final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController = final TextEditingController confirmAccountNumberController =
TextEditingController(); TextEditingController();
final TextEditingController amountController = TextEditingController(); final TextEditingController amountController = TextEditingController();
final TextEditingController remarksController = TextEditingController(); final TextEditingController remarksController = TextEditingController();
String? _selectedAccountType; String? _selectedAccountType;
bool _isAmountOverLimit = false;
String? _beneficiaryName; String? _beneficiaryName;
bool _isValidating = false; bool _isValidating = false;
bool _isBeneficiaryValidated = false; bool _isBeneficiaryValidated = false;
@@ -35,8 +40,53 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit();
accountNumberController.addListener(_resetBeneficiaryValidation); accountNumberController.addListener(_resetBeneficiaryValidation);
confirmAccountNumberController.addListener(_resetBeneficiaryValidation); confirmAccountNumberController.addListener(_resetBeneficiaryValidation);
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;
});
}
}
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,
),
);
}
// Update state only if it changes to avoid unnecessary rebuilds
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
} }
void _resetBeneficiaryValidation() { void _resetBeneficiaryValidation() {
@@ -53,6 +103,7 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
@override @override
void dispose() { void dispose() {
amountController.removeListener(_checkAmountLimit);
accountNumberController.removeListener(_resetBeneficiaryValidation); accountNumberController.removeListener(_resetBeneficiaryValidation);
confirmAccountNumberController.removeListener(_resetBeneficiaryValidation); confirmAccountNumberController.removeListener(_resetBeneficiaryValidation);
accountNumberController.dispose(); accountNumberController.dispose();
@@ -98,296 +149,350 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: Padding( body: Stack(
padding: const EdgeInsets.all(16.0), children: [
child: Form( Padding(
key: _formKey, padding: const EdgeInsets.all(16.0),
child: Column( child: Form(
children: [ key: _formKey,
const SizedBox(height: 10), child: SingleChildScrollView(
TextFormField( child: Column(
decoration: InputDecoration( children: [
labelText: AppLocalizations.of(context).debitAccountNumber, const SizedBox(height: 10),
border: const OutlineInputBorder(), TextFormField(
isDense: true, decoration: InputDecoration(
filled: true, labelText:
fillColor: Theme.of(context).scaffoldBackgroundColor, AppLocalizations.of(context).debitAccountNumber,
), border: const OutlineInputBorder(),
readOnly: true, isDense: true,
controller: TextEditingController(text: widget.debitAccount), filled: true,
keyboardType: TextInputType.number, fillColor: Theme.of(context).scaffoldBackgroundColor,
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),
), ),
], readOnly: true,
), controller:
), TextEditingController(text: widget.debitAccount),
if (_validationError != null) keyboardType: TextInputType.number,
Padding( textInputAction: TextInputAction.next,
padding: const EdgeInsets.only(top: 8.0), enabled: false,
child: Text( ),
_validationError!, const SizedBox(height: 20),
style: const TextStyle(color: Colors.red), TextFormField(
), decoration: InputDecoration(
), labelText: AppLocalizations.of(context).accountNumber,
const SizedBox(height: 24), border: const OutlineInputBorder(),
DropdownButtonFormField<String>( isDense: true,
decoration: InputDecoration( filled: true,
labelText: AppLocalizations.of( fillColor: Theme.of(context).scaffoldBackgroundColor,
context, enabledBorder: OutlineInputBorder(
).beneficiaryAccountType, borderSide: BorderSide(
border: const OutlineInputBorder(), color: Theme.of(context).colorScheme.outline),
isDense: true, ),
filled: true, focusedBorder: OutlineInputBorder(
fillColor: Theme.of(context).scaffoldBackgroundColor, borderSide: BorderSide(
enabledBorder: OutlineInputBorder( color: Theme.of(context).colorScheme.primary,
borderSide: BorderSide( width: 2),
color: Theme.of(context).colorScheme.outline), ),
), ),
focusedBorder: OutlineInputBorder( controller: accountNumberController,
borderSide: BorderSide( keyboardType: TextInputType.number,
color: Theme.of(context).colorScheme.primary, width: 2), obscureText: true,
), textInputAction: TextInputAction.next,
), validator: (value) {
value: _selectedAccountType, if (value == null || value.isEmpty) {
items: [ return AppLocalizations.of(context)
DropdownMenuItem( .accountNumberRequired;
value: 'SB', } else if (value.length != 11) {
child: Text(AppLocalizations.of(context).savings), return AppLocalizations.of(context)
), .validAccountNumber;
DropdownMenuItem( }
value: 'LN', return null;
child: Text(AppLocalizations.of(context).loan), },
), ),
], const SizedBox(height: 25),
onChanged: (value) { TextFormField(
setState(() { controller: confirmAccountNumberController,
_selectedAccountType = value; decoration: InputDecoration(
}); labelText:
}, AppLocalizations.of(context).confirmAccountNumber,
validator: (value) { // prefixIcon: Icon(Icons.person),
if (value == null || value.isEmpty) { border: const OutlineInputBorder(),
return AppLocalizations.of(context).selectAccountType; isDense: true,
} filled: true,
return null; fillColor: Theme.of(context).scaffoldBackgroundColor,
}, enabledBorder: OutlineInputBorder(
), borderSide: BorderSide(
const SizedBox(height: 25), color: Theme.of(context).colorScheme.outline),
TextFormField( ),
controller: remarksController, focusedBorder: OutlineInputBorder(
decoration: InputDecoration( borderSide: BorderSide(
labelText: AppLocalizations.of(context).remarks, color: Theme.of(context).colorScheme.primary,
border: const OutlineInputBorder(), width: 2),
isDense: true, ),
filled: true, ),
fillColor: Theme.of(context).scaffoldBackgroundColor, keyboardType: TextInputType.number,
enabledBorder: OutlineInputBorder( textInputAction: TextInputAction.next,
borderSide: BorderSide( validator: (value) {
color: Theme.of(context).colorScheme.outline), if (value == null || value.isEmpty) {
), return AppLocalizations.of(context)
focusedBorder: OutlineInputBorder( .reenterAccountNumber;
borderSide: BorderSide( }
color: Theme.of(context).colorScheme.primary, width: 2), if (value != accountNumberController.text) {
), return AppLocalizations.of(context).accountMismatch;
), }
), return null;
const SizedBox(height: 25), },
TextFormField( ),
decoration: InputDecoration( if (!_isBeneficiaryValidated)
labelText: AppLocalizations.of(context).amount, Padding(
border: const OutlineInputBorder(), padding: const EdgeInsets.only(top: 12.0),
isDense: true, child: SizedBox(
filled: true, width: double.infinity,
fillColor: Theme.of(context).scaffoldBackgroundColor, child: ElevatedButton(
enabledBorder: OutlineInputBorder( onPressed: _isValidating
borderSide: BorderSide( ? null
color: Theme.of(context).colorScheme.outline), : () {
), if (accountNumberController.text.length ==
focusedBorder: OutlineInputBorder( 11 &&
borderSide: BorderSide( confirmAccountNumberController.text ==
color: Theme.of(context).colorScheme.primary, width: 2), accountNumberController.text) {
), _validateBeneficiary();
), } else {
controller: amountController, setState(() {
keyboardType: TextInputType.number, _validationError =
textInputAction: TextInputAction.next, AppLocalizations.of(context)
validator: (value) { .accountMismatch;
if (value == null || value.isEmpty) { });
return AppLocalizations.of(context).amountRequired; }
} },
final amount = double.tryParse(value); child: _isValidating
if (amount == null || amount <= 0) { ? const SizedBox(
return AppLocalizations.of(context).validAmount; width: 20,
} height: 20,
return null; child: CircularProgressIndicator(
}, strokeWidth: 2),
), )
const SizedBox(height: 45), : Text(AppLocalizations.of(context)
Align( .validateBeneficiary),
alignment: Alignment.center,
child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward,
color: Theme.of(context).dialogBackgroundColor),
activeThumbColor: Theme.of(context).colorScheme.primary,
activeTrackColor: 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 (_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),
),
);
},
), ),
), ),
); ),
} 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),
),
);
},
),
),
);
}
},
),
),
],
), ),
), ),
], ),
), ),
), 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

@@ -11,26 +11,45 @@ class SecurityErrorScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Padding( body: Stack(
padding: const EdgeInsets.all(20.0), children: [
child: Column( Padding(
mainAxisAlignment: MainAxisAlignment.center, padding: const EdgeInsets.all(20.0),
children: [ child: Column(
Lottie.asset('assets/animations/error.json', height: 200), mainAxisAlignment: MainAxisAlignment.center,
const SizedBox(height: 20), children: [
Text( Lottie.asset('assets/animations/error.json', height: 200),
message, const SizedBox(height: 20),
textAlign: TextAlign.center, Text(
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), message,
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), ),
ElevatedButton( IgnorePointer(
onPressed: () => child: Center(
SystemChannels.platform.invokeMethod('SystemNavigator.pop'), child: Opacity(
child: const Text('Okay'), 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

@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.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'; // 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: const Text("ATM Locator"), // 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.currency_rupee), // 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

@@ -0,0 +1,107 @@
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("Branch Name", branch.branch_name),
_buildDetailRow("Branch Code", branch.branch_code),
_buildDetailRow("Zone", branch.zone),
_buildDetailRow("Tehsil", branch.tehsil),
_buildDetailRow("Block", branch.block),
_buildDetailRow("District", branch.distt_name),
_buildDetailRow("Pincode", branch.pincode),
// _buildDetailRow("Post Office", branch.post_office),
// _buildDetailRow("Date of Opening", branch.date_of_opening),
// _buildDetailRow("Branch Type", branch.type_of_branch),
_buildDetailRow("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(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
label == "Telephone No."
? InkWell(
onTap: () => _launchUrl('tel:$value'),
child: Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.blue, // Indicate it's clickable
decoration: TextDecoration.underline,
),
),
)
: Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const Divider(height: 16),
],
),
);
}
Future<void> _launchUrl(String urlString) async {
final Uri url = Uri.parse(urlString);
if (!await launchUrl(url)) {
throw 'Could not launch $urlString';
}
}
}

View File

@@ -2,25 +2,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.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';
// 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});
@@ -29,120 +15,99 @@ class BranchLocatorScreen extends StatefulWidget {
State<BranchLocatorScreen> createState() => _BranchLocatorScreenState(); State<BranchLocatorScreen> createState() => _BranchLocatorScreenState();
} }
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;
List<Branch> _allBranches = [];
List<Branch> _filteredBranches = [];
final List<Location> _allLocations = [ @override
Location(
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
void initState() { void initState() {
super.initState(); super.initState();
// _fetchAndSetLocations(); // _fetchAndSetLocations();
_filteredLocations = _allLocations; _loadBranches();
} }
// Example of a future API fetching function Future<void> _loadBranches() async {
/* final data = await service.fetchBranchList();
Future<void> _fetchAndSetLocations() async { setState(() {
setState(() { _allBranches = data;
_isLoading = true; _filteredBranches = data;
}); _isLoading = false;
try { });
// final locations = await yourApiService.getLocations(); }
// setState(() {
// _allLocations = locations;
// _filteredLocations = locations;
// });
} catch (e) {
// Handle error
} finally {
setState(() {
_isLoading = false;
});
}
}
*/
void _filterLocations(String query) {
setState(() {
if (query.isEmpty) {
_filteredLocations = _allLocations;
} else {
_filteredLocations = _allLocations.where((location) {
final lowerQuery = query.toLowerCase();
return location.name.toLowerCase().contains(lowerQuery) ||
(location.code?.toLowerCase().contains(lowerQuery) ?? false) ||
(location.ifsc?.toLowerCase().contains(lowerQuery) ?? false) ||
location.address.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
@override void _filterBranches(String query) {
setState(() {
if (query.isEmpty) {
_filteredBranches = _allBranches;
} else {
_filteredBranches = _allBranches.where((branch) {
final lowerQuery = query.toLowerCase();
return branch.branch_name.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).branchLocator), title: const Text("Branch Locator"),
), ),
body: Column( body: Stack(
children: [ children: [
Padding( Column(
padding: const EdgeInsets.all(12.0), children: [
child: TextField( Padding(
controller: _searchController, padding: const EdgeInsets.all(12.0),
onChanged: _filterLocations, child: TextField(
decoration: InputDecoration( controller: _searchController,
hintText: AppLocalizations.of(context).searchbranchby, onChanged: _filterBranches, // Updated
prefixIcon: const Icon(Icons.search), decoration: InputDecoration(
border: OutlineInputBorder( hintText: "Branch Name",
borderRadius: BorderRadius.circular(12), prefixIcon: const Icon(Icons.search),
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);
},
),
),
], ],
), ),
); );
@@ -161,28 +126,50 @@ Future<void> _fetchAndSetLocations() async {
); );
} }
// Helper widget to build a single location item // Helper widget to build a single branch item
Widget _buildLocationItem(Location location) {
final isBranch = location.type == LocationType.branch; Widget _buildBranchItem(Branch 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: CircleAvatar( leading: const CircleAvatar(
child: Icon(isBranch ? Icons.location_city : Icons.currency_rupee), child: Icon(Icons.location_city),
), ),
title: Text(location.name, title: Text(branch.branch_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: () {
ScaffoldMessenger.of(context).showSnackBar( // This is the updated part
SnackBar(content: Text("Selected ${location.name}")), Navigator.push(
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),
),
),
),
);
}
} }

View File

@@ -1,6 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/l10n/app_localizations.dart'; import 'package:kmobile/l10n/app_localizations.dart';
// Data model for a single FAQ item
class FaqItem {
final String question;
final String answer;
FaqItem({required this.question, required this.answer});
}
class FaqsScreen extends StatefulWidget { class FaqsScreen extends StatefulWidget {
const FaqsScreen({super.key}); const FaqsScreen({super.key});
@@ -9,39 +17,97 @@ class FaqsScreen extends StatefulWidget {
} }
class _FaqsScreenState extends State<FaqsScreen> { class _FaqsScreenState extends State<FaqsScreen> {
@override // List of FAQs
void initState() { final List<FaqItem> _faqs = [
super.initState(); FaqItem(
_getFaqs(); question: "How do I log in to the mobile banking app?",
} answer:
"You can log in using your customer number and password. Biometric login (fingerprint) and MPIN is also available for supported devices.",
// A placeholder for your future API call ),
Future<void> _getFaqs() async { FaqItem(
// TODO: Implement API call to fetch FAQs data question: "Is my banking information secure on this app?",
// For now, simulating a network call with a delay answer:
await Future.delayed(const Duration(seconds: 1)); "Yes. We use industry-standard encryption and multi-factor authentication to ensure your data is safe.",
// In a real implementation, you would process the API response here ),
} FaqItem(
question: "How can I check my account balance?",
answer:
"Once logged in, your account balance will be displayed on the home screen. You can also view detailed balances under the “Accounts” section.",
),
FaqItem(
question: "Can I transfer money to other bank accounts?",
answer:
"Yes. You can use NEFT, RTGS or IMPS to transfer funds to any bank account in India.",
),
FaqItem(
question: "How do I view my transaction history?",
answer:
"Click on the “Account Statement” icon under the Home Screen to view recent and past transactions.",
),
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Text(AppLocalizations.of(context).faq),
children: [ ),
Flexible( body: Stack(
child: Text( children: [
AppLocalizations.of(context).faq, // Background logo
softWrap: true, IgnorePointer(
style: const TextStyle( child: Center(
fontSize: 16.5, child: Opacity(
opacity: 0.07,
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
),
), ),
textAlign: TextAlign.left,
), ),
), ),
], ),
), // FAQ List
ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: _faqs.length,
itemBuilder: (context, index) {
final faq = _faqs[index];
return Card(
margin:
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: ExpansionTile(
title: Text(
faq.question,
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 16.0),
child: Text(
faq.answer,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.5,
),
),
),
],
),
);
},
),
],
), ),
); );
} }
} }

View File

@@ -1,34 +1,113 @@
import 'package:flutter/material.dart'; //
import 'package:kmobile/l10n/app_localizations.dart';
class QuickLinksScreen extends StatefulWidget { import 'package:flutter/material.dart';
const QuickLinksScreen({super.key}); import 'package:kmobile/l10n/app_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
@override // Data model for a single Quick Link item
State<QuickLinksScreen> createState() => _QuickLinksScreenState(); class QuickLink {
} final String title;
final String url;
// The icon is no longer used in the UI but is kept in the model for potential future use.
final IconData icon;
class _QuickLinksScreenState extends State<QuickLinksScreen> { QuickLink({required this.title, required this.url, required this.icon});
@override }
void initState() {
super.initState();
_getQuickLinks();
}
// A placeholder for your future API call class QuickLinksScreen extends StatefulWidget {
Future<void> _getQuickLinks() async { const QuickLinksScreen({super.key});
// TODO: Implement API call to fetch quick links data
// For now, simulating a network call with a delay
await Future.delayed(const Duration(seconds: 1));
// In a real implementation, you would process the API response here
}
@override @override
Widget build(BuildContext context) { State<QuickLinksScreen> createState() => _QuickLinksScreenState();
return Scaffold( }
appBar: AppBar(
title: Text(AppLocalizations.of(context).quickLinks), class _QuickLinksScreenState extends State<QuickLinksScreen> {
), // List of Quick Links
); final List<QuickLink> _quickLinks = [
} QuickLink(
} title: "National Bank of Agriculture & Rural Development",
url: "http://www.nabard.org/",
icon: Icons.account_balance),
QuickLink(
title: "Reserve Bank of India",
url: "http://www.rbi.org.in/home.aspx",
icon: Icons.account_balance_wallet),
QuickLink(
title: "Indian Institute of Banking & Finance",
url: "http://www.iibf.org.in/",
icon: Icons.school),
QuickLink(
title: "Indian Bank Association",
url: "http://www.iba.org.in/",
icon: Icons.group_work),
QuickLink(
title: "Ministry of Finance",
url: "http://www.finmin.nic.in/",
icon: Icons.business),
QuickLink(
title: "Securities Exchange Board of India",
url: "http://www.sebi.gov.in/",
icon: Icons.show_chart),
QuickLink(
title: "Insurance Regulatory & Development Authority",
url: "https://www.irdai.gov.in/",
icon: Icons.shield_outlined),
];
// Function to launch a URL
Future<void> _launchURL(String url, BuildContext context) async {
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not launch $url')),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).quickLinks),
),
body: Stack(
children: [
// Background logo
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07,
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
),
),
),
),
),
// UPDATED: List of Quick Links
ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: _quickLinks.length,
itemBuilder: (context, index) {
final link = _quickLinks[index];
return Card(
margin:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
child: ListTile(
title: Text(link.title),
trailing: const Icon(Icons.open_in_new, size: 20),
onTap: () => _launchURL(link.url, context),
),
);
},
),
],
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:kmobile/features/service/screens/atm_locator_screen.dart';
import 'package:kmobile/features/service/screens/branch_locator_screen.dart'; import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
import 'package:kmobile/features/service/screens/daily_transaction_limit.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
@@ -24,69 +25,75 @@ class _ServiceScreen extends State<ServiceScreen> {
), ),
centerTitle: false, centerTitle: false,
), ),
body: ListView( body: Stack(
children: [ children: [
ServiceManagementTile( ListView(
icon: Symbols.add, children: [
label: AppLocalizations.of(context).accountOpeningDeposit, // ServiceManagementTile(
onTap: () {}, // icon: Symbols.add,
disabled: true, // label: AppLocalizations.of(context).accountOpeningDeposit,
// onTap: () {},
// disabled: true,
// ),
// const Divider(height: 1),
// ServiceManagementTile(
// icon: Symbols.add,
// label: AppLocalizations.of(context).accountOpeningLoan,
// onTap: () {},
// disabled: true,
// ),
// const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.captive_portal,
label: AppLocalizations.of(context).quickLinks,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const QuickLinksScreen()),
);
},
disabled: false,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.question_mark,
label: AppLocalizations.of(context).faq,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const FaqsScreen()),
);
},
disabled: false,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.location_pin,
label: "ATM Locator",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ATMLocatorScreen()));
},
disabled: false,
),
const Divider(height: 1),
],
), ),
const Divider(height: 1), IgnorePointer(
ServiceManagementTile( child: Center(
icon: Symbols.add, child: Opacity(
label: AppLocalizations.of(context).accountOpeningLoan, opacity: 0.07, // Reduced opacity
onTap: () {}, child: ClipOval(
disabled: true, child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
), ),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.currency_rupee,
label: AppLocalizations.of(context).dailylimit,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const DailyLimitScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.captive_portal,
label: AppLocalizations.of(context).quickLinks,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const QuickLinksScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.question_mark,
label: AppLocalizations.of(context).faq,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const FaqsScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.location_pin,
label: AppLocalizations.of(context).branchLocator,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BranchLocatorScreen()));
},
disabled: true,
),
const Divider(height: 1),
], ],
), ),
); );

View File

@@ -217,6 +217,13 @@
"enterMPIN": "Enter your mPIN", "enterMPIN": "Enter your mPIN",
"setMPIN": "Set your mPIN", "setMPIN": "Set your mPIN",
"confirmMPIN": "Confirm your mPIN", "confirmMPIN": "Confirm your mPIN",
"changeMpin": "Change mPIN",
"enterOldMpin": "Enter your old mPIN",
"enterNewMpin": "Enter your new mPIN",
"confirmNewMpin": "Confirm your new mPIN",
"mpinChangedSuccessfully": "mPIN changed successfully",
"pinMismatch": "PINs do not match",
"securitySettings": "Security Settings",
"kconnect": "Kconnect", "kconnect": "Kconnect",
"kccBankFull": "Kangra Central Co-operative Bank", "kccBankFull": "Kangra Central Co-operative Bank",
"themeColor": "Theme Color", "themeColor": "Theme Color",
@@ -331,5 +338,71 @@
"deregistercheck": "Are you sure you want to De-Register?", "deregistercheck": "Are you sure you want to De-Register?",
"biometricsNotAvailable": "Biometrics not available on this device", "biometricsNotAvailable": "Biometrics not available on this device",
"disableFingerprintLogin": "Disable Fingerprint Login", "disableFingerprintLogin": "Disable Fingerprint Login",
"disableFingerprintMessage": "Are you sure you want to disable fingerprint login?" "disableFingerprintMessage": "Are you sure you want to disable fingerprint login?",
"selectAccount": "Select Account",
"noOtherAccountsFound": "No other accounts found",
"selfPay": "Self Pay",
"fundTransfer": "Fund Transfer",
"fetchingDailyLimit": "Fetching daily limit...",
"okay": "Okay",
"limitUpdated": "Limit Updated",
"changeTpin": "Change TPIN",
"currentTpin": "Current TPIN",
"newTpin": "New TPIN",
"confirmNewTpin": "Confirm New TPIN",
"pleaseEnterAValid6DigitOtp": "Please enter a valid 6-digit OTP",
"tpinChangedSuccessfully": "TPIN changed successfully!",
"verifyChangeTpin": "Verify & Change TPIN",
"tpinNotSet": "TPIN Not Set",
"back": "Back",
"appVersion": "App Version",
"error": "Error",
"atmLocator": "ATM Locator",
"nationalBankOfAgricultureRuralDevelopment": "National Bank of Agriculture & Rural Development",
"reserveBankOfIndia": "Reserve Bank of India",
"indianInstituteOfBankingFinance": "Indian Institute of Banking & Finance",
"indianBankAssociation": "Indian Bank Association",
"ministryOfFinance": "Ministry of Finance",
"securitiesExchangeBoardOfIndia": "Securities Exchange Board of India",
"insuranceRegulatoryDevelopmentAuthority": "Insurance Regulatory & Development Authority",
"noMatchingBranchesFound": "No matching branches found",
"nameAddress": "Name/Address",
"noMatchingAtmsFound": "No matching ATMs found",
"myCards": "My Cards",
"validFrom": "VALID FROM",
"validUpto": "VALID UPTO",
"termsAndConditions": "Terms and Conditions",
"goBack": "Go Back",
"noTransactionsToExport": "No transactions to export.",
"accountStatementKccb": "Account Statement - KCCB",
"description": "Description",
"balance": "Balance",
"storagePermissionIsRequiredToSavePdf": "Storage permission is required to save PDF",
"setNewPassword": "Set New Password",
"setPassword": "Set Password",
"backToLogin": "Back to Login",
"enterDigitPin": "Enter {length}-digit PIN",
"couldNotLaunch": "Could not launch {url}",
"selected": "Selected {atmName}",
"currencySymbol": "₹",
"iAgreeToTheTermsAndConditions": "I agree to the Terms and Conditions",
"disagree": "Disagree",
"couldNotOpenEmailAppFor": "Could not open email app for {email}",
"couldNotOpenDialerFor": "Could not open dialer for {phone}",
"failedToSendOtp": "Failed to send OTP: {error}",
"anErrorOccurred": "An error occurred: {error}",
"pdfSavedTo": "PDF saved to: {filePath}",
"errorSavingPdf": "Error saving PDF: {error}",
"limitToBeSetMustBeLessThan200000": "Limit To be Set must be less than 200000",
"remainingLimitToday": "Remaining Limit Today",
"hindiLanguage": "हनद",
"pleaseFillBothPasswordFields": "Please fill both password fields",
"enter": "Enter",
"setLimit": "Set Limit",
"tehsil": "Tehsil",
"district": "District",
"postOffice": "Post Office",
"rbiCode1": "RBI Code 1",
"rbiCode2": "RBI Code 2",
"latitude": "Latitude"
} }

View File

@@ -218,6 +218,13 @@
"enterMPIN": "अपना mPIN दर्ज करें", "enterMPIN": "अपना mPIN दर्ज करें",
"setMPIN": "अपना mPIN सेट करें", "setMPIN": "अपना mPIN सेट करें",
"confirmMPIN": "अपना mPIN की पुष्टि करें", "confirmMPIN": "अपना mPIN की पुष्टि करें",
"changeMpin": "mPIN बदलें",
"enterOldMpin": "अपना पुराना mPIN दर्ज करें",
"enterNewMpin": "अपना नया mPIN दर्ज करें",
"confirmNewMpin": "अपना नया mPIN की पुष्टि करें",
"mpinChangedSuccessfully": "mPIN सफलतापूर्वक बदल गया",
"pinMismatch": "PIN मेल नहीं खा रहे हैं",
"securitySettings": "सुरक्षा सेटिंग्स",
"kconnect": "के-कनेक्ट", "kconnect": "के-कनेक्ट",
"kccBankFull": "कांगड़ा सेंट्रल को-ऑपरेटिव बैंक", "kccBankFull": "कांगड़ा सेंट्रल को-ऑपरेटिव बैंक",
"themeColor": "थीम रंग", "themeColor": "थीम रंग",
@@ -332,5 +339,71 @@
"deregistercheck": "क्या आप वाकई पंजीकरण रद्द करना चाहते हैं??", "deregistercheck": "क्या आप वाकई पंजीकरण रद्द करना चाहते हैं??",
"biometricsNotAvailable": "इस डिवाइस पर बायोमेट्रिक्स उपलब्ध नहीं है", "biometricsNotAvailable": "इस डिवाइस पर बायोमेट्रिक्स उपलब्ध नहीं है",
"disableFingerprintLogin": "फ़िंगरप्रिंट लॉगिन अक्षम करें", "disableFingerprintLogin": "फ़िंगरप्रिंट लॉगिन अक्षम करें",
"disableFingerprintMessage": "क्या आप फ़िंगरप्रिंट लॉगिन अक्षम करना चाहते हैं?" "disableFingerprintMessage": "क्या आप फ़िंगरप्रिंट लॉगिन अक्षम करना चाहते हैं?",
"selectAccount": "खाता चुनें",
"noOtherAccountsFound": "कोई अन्य खाता नहीं मिला",
"selfPay": "स्वयं भुगतान",
"fundTransfer": "धन हस्तांतरण",
"fetchingDailyLimit": "दैनिक सीमा प्राप्त की जा रही है...",
"okay": "ठीक है",
"limitUpdated": "सीमा अपडेट की गई",
"changeTpin": "टी-पिन बदलें",
"currentTpin": "वर्तमान टी-पिन",
"newTpin": "नया टी-पिन",
"confirmNewTpin": "नए टी-पिन की पुष्टि करें",
"pleaseEnterAValid6DigitOtp": "कृपया एक मान्य 6-अंकीय ओटीपी दर्ज करें",
"tpinChangedSuccessfully": "टी-पिन सफलतापूर्वक बदला गया!",
"verifyChangeTpin": "सत्यापित करें और टी-पिन बदलें",
"tpinNotSet": "टी-पिन सेट नहीं है",
"back": "वापस",
"appVersion": "ऐप संस्करण",
"error": "त्रुटि",
"atmLocator": "एटीएम लोकेटर",
"nationalBankOfAgricultureRuralDevelopment": "राष्ट्रीय कृषि और ग्रामीण विकास बैंक",
"reserveBankOfIndia": "भारतीय रिज़र्व बैंक",
"indianInstituteOfBankingFinance": "भारतीय बैंकिंग और वित्त संस्थान",
"indianBankAssociation": "भारतीय बैंक संघ",
"ministryOfFinance": "वित्त मंत्रालय",
"securitiesExchangeBoardOfIndia": "भारतीय प्रतिभूति और विनिमय बोर्ड",
"insuranceRegulatoryDevelopmentAuthority": "बीमा नियामक और विकास प्राधिकरण",
"noMatchingBranchesFound": "कोई मिलती-जुलती शाखा नहीं मिली",
"nameAddress": "नाम/पता",
"noMatchingAtmsFound": "कोई मिलते-जुलते एटीएम नहीं मिले",
"myCards": "मेरे कार्ड",
"validFrom": "से मान्य",
"validUpto": "तक मान्य",
"termsAndConditions": "नियम और शर्तें",
"goBack": "वापस जाएं",
"noTransactionsToExport": "निर्यात करने के लिए कोई लेन-देन नहीं है।",
"accountStatementKccb": "खाता विवरण - KCCB",
"description": "विवरण",
"balance": "शेष राशि",
"storagePermissionIsRequiredToSavePdf": "पीडीएफ सहेजने के लिए स्टोरेज अनुमति आवश्यक है",
"setNewPassword": "नया पासवर्ड सेट करें",
"setPassword": "पासवर्ड सेट करें",
"backToLogin": "लॉगिन पर वापस जाएं",
"enterDigitPin": "{length}-अंकीय पिन दर्ज करें",
"couldNotLaunch": "{url} लॉन्च नहीं किया जा सका",
"selected": "{atmName} चयनित",
"currencySymbol": "₹",
"iAgreeToTheTermsAndConditions": "मैं नियम और शर्तों से सहमत हूं",
"disagree": "असहमत",
"couldNotOpenEmailAppFor": "{email} के लिए ईमेल ऐप नहीं खोला जा सका",
"couldNotOpenDialerFor": "{phone} के लिए डायलर नहीं खोला जा सका",
"failedToSendOtp": "ओटीपी भेजने में विफल: {error}",
"anErrorOccurred": "एक त्रुटि हुई: {error}",
"pdfSavedTo": "पीडीएफ यहां सहेजा गया: {filePath}",
"errorSavingPdf": "पीडीएफ सहेजने में त्रुटि: {error}",
"limitToBeSetMustBeLessThan200000": "सेट की जाने वाली सीमा 200000 से कम होनी चाहिए",
"remainingLimitToday": "आज की शेष सीमा",
"hindiLanguage": "हिन्दी",
"pleaseFillBothPasswordFields": "कृपया दोनों पासवर्ड फ़ील्ड भरें",
"enter": "दर्ज करें",
"setLimit": "सीमा सेट करें",
"tehsil": "तहसील",
"district": "जिला",
"postOffice": "डाकघर",
"rbiCode1": "आरबीआई कोड 1",
"rbiCode2": "आरबीआई कोड 2",
"latitude": "अक्षांश"
} }

View File

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

View File

@@ -22,7 +22,7 @@ Widget getBankLogo(String? bankName, BuildContext context) {
height: 40, height: 40,
); );
} }
if (bankName != null && bankName.toLowerCase().contains('icici bank ltd')) { if (bankName != null && bankName.toLowerCase().contains('icici')) {
return Image.asset( return Image.asset(
'assets/images/icici_logo.png', 'assets/images/icici_logo.png',
width: 40, width: 40,

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PinInputField extends StatelessWidget {
final TextEditingController controller;
final int length;
final FormFieldValidator<String>? validator;
const PinInputField({
super.key,
required this.controller,
this.length = 6,
this.validator,
});
@override
Widget build(BuildContext context) {
return FormField<String>(
validator: validator,
builder: (FormFieldState<String> state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: controller,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(length),
],
obscureText: true,
obscuringCharacter: '*',
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Enter $length-digit PIN',
counterText: '', // Hide the counter
),
onChanged: (value) {
state.didChange(value);
},
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 8.0, left: 12.0),
child: Text(
state.errorText!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
],
);
},
);
}
}

211
lib/widgets/tnc_dialog.dart Normal file
View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
class TncDialog extends StatefulWidget {
final Future<void> Function() onProceed;
const TncDialog({Key? key, required this.onProceed}) : super(key: key);
@override
_TncDialogState createState() => _TncDialogState();
}
class _TncDialogState extends State<TncDialog> {
bool _isAgreed = false;
bool _isLoading = false;
// --- NEW: ScrollController for the TNC text ---
final ScrollController _scrollController = ScrollController();
final String _termsAndConditionsText = """
Effective Date: November 10, 2025
These Terms and Conditions ("Terms") govern your access to and use of The Bank mobile banking application (the "App") and the services
provided through it (the "Services").
By downloading, installing, accessing, or using the App, you agree to be bound by these Terms and our Privacy Policy. If you do not
agree to these Terms, you must not download, install, access, or use the App.
1. Definitions
- App: Refers to The Bank mobile banking application.
- Bank/We/Us/Our: Refers to The Bank.
- User/You/Your: Refers to the individual using the App.
- Device: Refers to any compatible mobile phone, tablet, or other device on which you install and use the App.
- Security Credentials: Refers to your username, password, PIN, biometric data (e.g., fingerprint, facial recognition), and any other
authentication methods used to access the App and Services.
2. Acceptance of Terms
Your use of the App constitutes your acceptance of these Terms. We recommend that you print or save a copy of these Terms for your
records.
3. License to Use
We grant you a limited, non-exclusive, non-transferable, revocable license to install and use the App on a Device that you own or
control, solely for your personal, non-commercial use in connection with your accounts at The Bank. This license does not permit you to
use the App on any Device that you do not own or control.
4. User Responsibilities
You agree to:
- Use the App only for lawful purposes and in accordance with these Terms.
- Keep your Device and Security Credentials secure and confidential.
- Notify us immediately if you suspect any unauthorized use of your Security Credentials or Device, or if your Device is lost or stolen.
- Ensure that any information you provide to us through the App is accurate and up-to-date.
- Comply with all reasonable instructions we issue regarding the safe use of your Device and the App.
- Not use the App in any unlawful manner, for any unlawful purpose, or in any manner inconsistent with these Terms, or act fraudulently
or maliciously (e.g., by hacking into or inserting malicious code into the App or your Device's operating system).
- Not download the App from anywhere other than an app store approved by us (e.g., Apple App Store, Google Play Store) or install or use
it on a jail-broken or rooted device.
- Delete the App if you change or dispose of a Device that you use to access the Services.
5. Security
We employ reasonable security measures to protect your information and transactions conducted through the App. However, you acknowledge
that no system is entirely secure. You are responsible for maintaining the security of your Device and Security Credentials. We are not
liable for damages arising from virus contamination in your IT system or if the parameters of your browser are different from the
required technical conditions.
6. Privacy
Your privacy is important to us. Our Privacy Policy explains how we collect, use, and protect your personal information in connection
with your use of the App and Services. By using the App, you consent to such collection, use, and protection as described in our Privacy
Policy.
7. Limitations of Liability and Disclaimer of Warranty
THE APP AND SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. We do not
guarantee continuous, uninterrupted, or secure access to any part of our Service, and operation of the App or the Services may be
interfered with by numerous factors outside of our control.
IN NO EVENT SHALL WE OR OUR AFFILIATES, LICENSORS, OR CONTRACTORS BE LIABLE FOR ANY CLAIM, ARISING FROM OR RELATED TO THE MOBILE BANKING
APP OR THE SERVICES, THAT YOU DO NOT STATE IN WRITING IN A COMPLAINT FILED IN A COURT OR ARBITRATION PROCEEDING WITHIN TWO (2) YEARS OF
THE DATE THAT THE EVENT GIVING RISE TO THE CLAIM OCCURRED. THESE LIMITATIONS WILL APPLY TO ALL CAUSES OF ACTION, WHETHER ARISING FROM
BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE) OR ANY OTHER LEGAL THEORY.
8. Intellectual Property
All intellectual property rights in the App and its content (excluding user-generated content) are owned by The Bank or its licensors.
You are granted a limited license to use the App as set forth in these Terms, but no ownership rights are transferred to you. You must
not remove or tamper with any copyright notice attached to or contained within the App.
9. Termination
We may terminate or suspend your access to the App and Services immediately, without prior notice or liability, for any reason
whatsoever, including without limitation if you breach these Terms. You may stop using the App at any time. If you wish to deregister
your digital banking access, you need to notify us.
10. Changes to Terms
We reserve the right to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice
prior to any new terms taking effect. Your continued use of the App after any such changes constitutes your acceptance of the new Terms.
11. Governing Law and Dispute Resolution
These Terms shall be governed and construed in accordance with the laws of your local jurisdiction, without regard to its conflict of
law provisions. Any dispute arising under these Terms shall be resolved in the courts located in your local jurisdiction.
12. Contact Information
If you have any questions about these Terms, please contact us through the channels provided on our official website or within the App.
13. Electronic Communications
By using the App, you consent to receive electronic communications from us. These communications may include notices about your account,
transactional information, and marketing materials.
14. Third-Party Services
The App may integrate with or provide links to third-party services. We are not responsible for the content, privacy policies, or
practices of any third-party websites or services.
15. Indemnification
You agree to indemnify and hold harmless The Bank, its affiliates, officers, directors, employees, and agents from any and all claims,
liabilities, damages, losses, and expenses, including reasonable attorneys' fees, arising out of or in any way connected with your
access to or use of the App and Services.
""";
void _handleProceed() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
});
await widget.onProceed();
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
@override
void dispose() {
_scrollController.dispose(); // --- NEW: Dispose the ScrollController ---
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return AlertDialog(
title: const Text('Terms and Conditions'),
content: SizedBox(
height: screenSize.height * 0.5, // 50% of screen height
width: screenSize.width * 0.9, // 90% of screen width
// --- MODIFIED: Use a Column to separate scrollable text from fixed checkbox ---
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// --- NEW: Expanded Scrollbar for the TNC text ---
Expanded(
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true, // Always show the scrollbar thumb
// To place the scrollbar on the left, you might need to wrap
// this in a Directionality widget or use a custom scrollbar.
// For now, it will appear on the right as is standard.
child: SingleChildScrollView(
controller: _scrollController,
child: _isLoading
? const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
)
: Text(_termsAndConditionsText),
),
),
),
const SizedBox(height: 16), // Space between text and checkbox
// --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView ---
Row(
children: [
Checkbox(
value: _isAgreed,
onChanged: (bool? value) {
setState(() {
_isAgreed = value ?? false;
});
},
),
const Flexible(
child: Text('I agree to the Terms and Conditions')),
],
),
],
),
),
actions: [
TextButton(
onPressed: _isLoading
? null
: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'You must agree to the terms and conditions to proceed.'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text('Disagree'),
),
ElevatedButton(
onPressed: _isAgreed && !_isLoading ? _handleProceed : null,
child: const Text('Proceed'),
),
],
);
}
}

View File

@@ -69,10 +69,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -93,18 +93,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.1"
confetti: confetti:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -181,10 +181,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -385,10 +385,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
jailbreak_root_detection: jailbreak_root_detection:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -417,26 +417,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -497,10 +497,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -521,10 +521,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -561,10 +561,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
@@ -817,7 +817,7 @@ packages:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -838,18 +838,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.12.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@@ -870,10 +870,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.6"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -982,10 +982,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -1043,5 +1043,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.5.0 <4.0.0" dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.24.0"

View File

@@ -144,4 +144,4 @@ flutter:
flutter_icons: flutter_icons:
android: true android: true
ios: true ios: true
image_path: assets/images/icon.png image_path: assets/images/logo.png