69 Commits

Author SHA1 Message Date
715162b713 Test Sim Binding APK 2025-12-11 12:18:25 +05:30
8149ef2a5b Sim Binding Done #1 2025-12-10 11:50:02 +05:30
1a2dea611b SMS integrated with new ui 2025-12-09 18:11:46 +05:30
72a2c56392 Code Formatting 2025-12-05 16:02:49 +05:30
aef82237ac OTP added to daily limit #2 2025-12-05 15:59:59 +05:30
974f42bf95 OTP added to daily limit 2025-12-05 12:37:38 +05:30
4a8c69bb1e Account Info card changes and snackbar in statement #3 2025-12-04 16:50:08 +05:30
86aaaa1f6d Account Info card changes and snackbar in statement #2 2025-12-04 15:56:22 +05:30
6796793aac Account Info card changes and snackbar in statement 2025-12-04 15:40:24 +05:30
fbf6df7181 Icon and logo issues fixed 2025-12-04 12:44:39 +05:30
c7111d518a PDF Edited 2025-12-03 18:05:34 +05:30
5d307607fd Download notification created and Profile added in quick links 2025-12-02 17:06:49 +05:30
992092052a Customerf Info page changed and landing page changed 2025-12-02 13:31:40 +05:30
64fedabd89 Subtitles added in payment tabs with Localizations 2025-12-01 16:28:42 +05:30
4fc6f54fcd Subtitles added in payment tabs 2025-12-01 16:13:27 +05:30
8c7e94759a APK Build #1 2025-12-01 12:58:17 +05:30
8aa5b170ca Account Statement UI Changed 2025-11-28 12:22:51 +05:30
04a1ce26ec Profile Changed and Customer Info 2025-11-28 11:28:01 +05:30
b19bc2e222 Extras removed 2025-11-27 11:47:39 +05:30
b9147b30d5 Enquiry_screen ui changed 2025-11-25 14:43:52 +05:30
3358ec7669 UI #1 2025-11-25 12:51:29 +05:30
18db360a45 Code Formatted 2025-11-24 18:18:36 +05:30
b7fe6a9d18 View All Created 2025-11-24 18:16:53 +05:30
adb9a5330b Account Info card UI changed 2025-11-24 15:50:01 +05:30
0075abc906 Bottom navigation changed 2025-11-24 13:26:33 +05:30
353ec63916 3 changes 2025-11-24 12:56:06 +05:30
71b52cfb43 dashboard Screen UI changed 2025-11-24 12:20:35 +05:30
c1df43e9b6 dashboard#1 2025-11-20 17:45:29 +05:30
f0d5233afc Beneficiary lists changed 2025-11-20 13:31:42 +05:30
4fe6af4098 dark Theme and 18 other changes done 2025-11-20 12:33:30 +05:30
fda5d075ff Profile Screen 2025-11-18 11:33:22 +05:30
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
58e53d0aeb App Version added and dart format 2025-10-21 13:32:14 +05:30
06ef2ab36b Profile shifted to left 2025-10-19 21:27:47 +05:30
0362bf2013 Fingerprint option added in profile screen 2025-10-17 11:47:20 +05:30
73b96b82f7 Background Timer Feature#1 2025-10-16 17:54:48 +05:30
c78a90dbfe Screenshot Prevention and backgorund Screen blacking 2025-10-15 12:46:56 +05:30
df025babd5 App Functionality 2025-10-15 12:30:01 +05:30
126 changed files with 9675 additions and 3906 deletions

5
.gitignore vendored
View File

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

View File

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

View File

@@ -2,6 +2,8 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:label="kmobile"
android:name="${applicationName}"
@@ -40,6 +42,20 @@
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. -->
<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>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>

View File

@@ -1,5 +1,12 @@
package com.example.kmobile
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.android.FlutterActivity
import android.view.WindowManager.LayoutParams
import android.os.Bundle
class MainActivity: FlutterFragmentActivity()
class MainActivity: FlutterFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.addFlags(LayoutParams.FLAG_SECURE)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 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: 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
assets/images/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/images/profile.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 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

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

@@ -40,8 +40,7 @@ class BeneficiaryService {
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('INVALID IFSC CODE');
}
else if (e.response?.statusCode == 401) {
} else if (e.response?.statusCode == 401) {
throw Exception('INVALID IFSC CODE');
}
} catch (e) {
@@ -127,4 +126,4 @@ class BeneficiaryService {
throw Exception('Unexpected error: ${e.toString()}');
}
}
}
}

View File

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

@@ -10,10 +10,7 @@ class ChangePasswordService {
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {
'mobileNumber': mobileNumber,
'type': "CHANGE_LPWORD"
},
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_LPWORD"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
@@ -22,15 +19,12 @@ class ChangePasswordService {
return response.toString();
}
Future getOtpTpin({
Future getOtpTpin({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {
'mobileNumber': mobileNumber,
'type': "CHANGE_TPIN"
},
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_TPIN"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
@@ -39,15 +33,14 @@ class ChangePasswordService {
return response.toString();
}
Future validateOtp({
Future validateOtp({
required String otp,
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber',
data: {
'otp' : otp,
'otp': otp,
},
);
if (response.statusCode != 200) {
@@ -56,22 +49,39 @@ class ChangePasswordService {
return response.toString();
}
Future validateChangePwd({
required String OldLPsw,
required String newLPsw,
required String confirmLPsw,
}) async {
final response = await _dio.post(
Future validateChangePwd({
required String OldLPsw,
required String newLPsw,
required String confirmLPsw,
}) async {
final response = await _dio.post(
'/api/auth/change/login_password',
data: {
'OldLPsw': OldLPsw,
'newLPsw': newLPsw,
'confirmLPsw': confirmLPsw,
'OldLPsw': OldLPsw,
'newLPsw': newLPsw,
'confirmLPsw': confirmLPsw,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
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,87 @@
// 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()}');
}
}
Future getOtpTLimit({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "TLIMIT"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future validateOtp({
required String otp,
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber',
data: {
'otp': otp,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
}

View File

@@ -0,0 +1,102 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:send_message/send_message.dart' show sendSMS;
import 'package:simcards/sim_card.dart';
import 'package:simcards/simcards.dart';
// This enum provides detailed status back to the UI layer.
enum PermissionStatusResult { granted, denied, permanentlyDenied, restricted }
class SmsService {
final Simcards _simcards = Simcards();
/// Handles the requesting of SMS and Phone permissions.
/// Returns a detailed status: granted, denied, or permanentlyDenied.
Future<PermissionStatusResult> handleSmsPermission() async {
var smsStatus = await Permission.sms.status;
var phoneStatus = await Permission.phone.status;
// Check initial status
if (smsStatus.isGranted && phoneStatus.isGranted) {
return PermissionStatusResult.granted;
}
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) {
return PermissionStatusResult.permanentlyDenied;
}
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
return PermissionStatusResult.restricted;
}
// Request permissions if not granted
print("Requesting SMS and Phone permissions...");
await [Permission.phone, Permission.sms].request();
// Re-check status after request
smsStatus = await Permission.sms.status;
phoneStatus = await Permission.phone.status;
if (smsStatus.isGranted && phoneStatus.isGranted) {
return PermissionStatusResult.granted;
}
if (smsStatus.isPermanentlyDenied || phoneStatus.isPermanentlyDenied) {
return PermissionStatusResult.permanentlyDenied;
}
if (smsStatus.isRestricted || phoneStatus.isRestricted) {
return PermissionStatusResult.restricted;
}
// If none of the above, it's denied
return PermissionStatusResult.denied;
}
/// Tries to send a single verification SMS.
/// This should only be called AFTER permissions have been granted.
Future<bool> sendVerificationSms({
required BuildContext context,
required String destinationNumber,
required String message,
}) async {
try {
List<SimCard> simCardList = await _simcards.getSimCards();
if (simCardList.isEmpty) {
print("No SIM card detected.");
return false;
}
return await _sendSms(destinationNumber, message, simCardList.first);
} catch (e) {
print("An error occurred in the SMS process: $e");
return false;
}
}
/// Private function to perform the SMS sending action.
Future<bool> _sendSms(
String destinationNumber, String message, SimCard selectedSim) async {
if (Platform.isAndroid) {
try {
String smsMessage = message;
String result = await sendSMS(
message: smsMessage,
recipients: [destinationNumber],
sendDirect: true,
);
print("Background SMS send attempt result: $result");
if (result.toLowerCase().contains('sent')) {
print("Success: SMS appears to have been sent.");
return true;
} else {
print("Failure: SMS was not sent. Result: $result");
return false;
}
} catch (e) {
print("Error attempting to send SMS directly: $e");
return false;
}
} else {
print("SMS sending is only supported on Android.");
return false;
}
}
}

View File

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

View File

@@ -12,14 +12,15 @@ import 'package:kmobile/features/auth/controllers/theme_state.dart';
import 'config/routes.dart';
import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart';
import 'features/card/screens/card_management_screen.dart';
import 'features/auth/screens/splash_screen.dart';
import 'features/accounts/screens/account_statement_screen.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart';
import 'features/auth/screens/mpin_screen.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
class KMobile extends StatefulWidget {
const KMobile({super.key});
@@ -34,25 +35,46 @@ class KMobile extends StatefulWidget {
}
}
class _KMobileState extends State<KMobile> {
bool showSplash = true;
class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
Timer? _backgroundTimer;
Locale? _locale;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
loadPreferences();
Future.delayed(const Duration(seconds: 3), () {
setState(() {
showSplash = false;
});
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_backgroundTimer?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
_backgroundTimer?.cancel();
break;
case AppLifecycleState.paused:
_backgroundTimer = Timer(const Duration(minutes: 2), () {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
});
break;
default:
break;
}
}
Future<void> loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
// Load Locale
final String? langCode = prefs.getString('locale');
if (langCode != null) {
setState(() {
@@ -103,9 +125,12 @@ class _KMobileState extends State<KMobile> {
theme: themeState.getLightThemeData(),
darkTheme: themeState.getDarkThemeData(),
themeMode: context.watch<ThemeModeCubit>().state.mode,
navigatorObservers: [
getIt<RouteObserver<ModalRoute<void>>>(),
],
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash,
home: showSplash ? const SplashScreen() : const AuthGate(),
home: const AuthGate(),
);
},
);
@@ -117,7 +142,6 @@ class _KMobileState extends State<KMobile> {
class AuthGate extends StatefulWidget {
const AuthGate({super.key});
@override
State<AuthGate> createState() => _AuthGateState();
}
@@ -178,9 +202,8 @@ class _AuthGateState extends State<AuthGate> {
@override
Widget build(BuildContext context) {
if (_checking) {
return const SplashScreen();
return const LoginScreen();
}
if (_isLoggedIn) {
if (_hasMPin) {
if (_biometricEnabled) {
@@ -188,13 +211,11 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
return const LoginScreen();
}
if (snapshot.data == true) {
return const NavigationScaffold(); // Authenticated
return const NavigationScaffold();
}
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
@@ -225,7 +246,6 @@ class _AuthGateState extends State<AuthGate> {
onCompleted: (_) async {
final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication();
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -246,7 +266,6 @@ class _AuthGateState extends State<AuthGate> {
],
),
);
if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false;
@@ -254,7 +273,6 @@ class _AuthGateState extends State<AuthGate> {
if (context.mounted) {
authEnable = AppLocalizations.of(context).authenticateToEnable;
}
if (canCheck) {
didAuth = await localAuth.authenticate(
localizedReason: authEnable,
@@ -269,7 +287,6 @@ class _AuthGateState extends State<AuthGate> {
await storage.write('biometric_enabled', 'false');
}
}
if (context.mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
@@ -287,7 +304,6 @@ class _AuthGateState extends State<AuthGate> {
class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key});
@override
State<NavigationScaffold> createState() => _NavigationScaffoldState();
}
@@ -295,10 +311,23 @@ class NavigationScaffold extends StatefulWidget {
class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController();
int _selectedIndex = 0;
final List<Widget> _pages = [
const DashboardScreen(),
const CardManagementScreen(),
BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
if (state.users.isNotEmpty) {
return AccountStatementScreen(
users: state.users,
selectedIndex: 0,
);
} else {
return const Center(child: Text("No accounts found."));
}
}
return const Center(child: CircularProgressIndicator());
},
),
const ServiceScreen(),
];
@@ -344,8 +373,7 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
type: BottomNavigationBarType.fixed,
backgroundColor: const Color(0XFF1E58AD),
selectedItemColor: Theme.of(context).colorScheme.onPrimary,
unselectedItemColor:
Theme.of(context).colorScheme.onSecondary,
unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
onTap: (index) {
setState(() {
_selectedIndex = index;
@@ -358,8 +386,8 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
label: AppLocalizations.of(context).home,
),
BottomNavigationBarItem(
icon: const Icon(Icons.credit_card),
label: AppLocalizations.of(context).card,
icon: const Icon(Icons.swap_vert_sharp),
label: AppLocalizations.of(context).transactions,
),
BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services),
@@ -372,11 +400,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
}
}
// Add this widget at the end of the file
class BiometricPromptScreen extends StatelessWidget {
final VoidCallback onCompleted;
const BiometricPromptScreen({super.key, required this.onCompleted});
Future<void> _handleBiometric(BuildContext context) async {
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
@@ -407,7 +433,7 @@ class BiometricPromptScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context));
return const SplashScreen();
return const SizedBox.shrink();
}
Future<void> _showDialog(BuildContext context) async {

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ class Beneficiary {
final String accountNo;
final String accountType;
final String name;
final DateTime? createdAt;
final String ifscCode;
final String? bankName;
final String? branchName;
@@ -11,6 +12,7 @@ class Beneficiary {
required this.accountNo,
required this.accountType,
required this.name,
this.createdAt,
required this.ifscCode,
this.bankName,
this.branchName,
@@ -21,6 +23,9 @@ class Beneficiary {
return Beneficiary(
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
accountType: json['account_type'] ?? json['accountType'] ?? '',
createdAt: json['createdAt'] == null
? null
: DateTime.tryParse(json['createdAt']),
name: json['name'] ?? '',
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
bankName: json['bank_name'] ?? json['bankName'] ?? '',

View File

@@ -15,7 +15,7 @@ class ImpsTransaction {
required this.ifscCode,
this.remitterName,
required this.beneficiaryName,
required this.tpin,
required this.tpin,
this.remarks,
});

View File

@@ -15,7 +15,7 @@ class NeftTransaction {
required this.ifscCode,
required this.remitterName,
required this.beneficiaryName,
required this.tpin,
required this.tpin,
this.remarks,
});

View File

@@ -11,7 +11,7 @@ class Transfer {
required this.toAccount,
required this.toAccountType,
required this.amount,
this.tpin,
this.tpin,
this.remarks,
});

View File

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

View File

@@ -1,5 +1,3 @@
import 'package:dio/dio.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/data/models/transaction.dart';

View File

@@ -1,3 +1,6 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/imps_service.dart';
@@ -19,6 +22,8 @@ import '../security/secure_storage.dart';
final getIt = GetIt.instance;
Future<void> setupDependencies() async {
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
RouteObserver<ModalRoute<void>>());
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
@@ -46,10 +51,14 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>(() => ChangePasswordService(getIt<Dio>()),);
getIt.registerSingleton<BranchService>(BranchService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>(
() => ChangePasswordService(getIt<Dio>()),
);
// Add auth interceptor after repository is available
getIt<Dio>().interceptors.add(
@@ -57,8 +66,8 @@ Future<void> setupDependencies() async {
);
// Register controllers/cubits
getIt.registerFactory<AuthCubit>(
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
getIt.registerFactory<AuthCubit>(() => AuthCubit(
getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>()));
}
Dio _createDioClient() {
@@ -66,13 +75,14 @@ Dio _createDioClient() {
BaseOptions(
baseUrl:
'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
//'https://kccbmbnk.net',
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
//'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Login-Type': 'MB',
},
),
);

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -14,72 +14,95 @@ class TransactionDetailsScreen extends StatelessWidget {
return Scaffold(
appBar:
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
flex: 3,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Amount + icon + Share Button
Row(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
flex: 3,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${transaction.amount}",
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
),
// Amount + icon + Share Button
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${transaction.amount}",
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Icon(
isCredit
? Symbols.call_received
: Symbols.call_made,
color: isCredit
? const Color(0xFF10BB10)
: Theme.of(context).colorScheme.error,
size: 28,
),
],
),
const SizedBox(width: 8),
Icon(
isCredit ? Symbols.call_received : Symbols.call_made,
color: isCredit ? Colors.green : Colors.red,
size: 28,
const SizedBox(height: 8),
// Date centered
Text(
transaction.date ?? "",
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodySmall?.color,
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 8),
// Date centered
Text(
transaction.date ?? "",
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
),
),
Divider(color: Theme.of(context).dividerColor),
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),
],
),
),
],
),
),
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:kmobile/api/services/user_service.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 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository _authRepository;
final UserService _userService;
final SecureStorage _secureStorage;
AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) {
AuthCubit(this._authRepository, this._userService, this._secureStorage)
: super(AuthInitial()) {
checkAuthStatus();
}
@@ -29,22 +34,62 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> refreshUserData() async {
try {
// emit(AuthLoading());
final users = await _userService.getUserDetails();
emit(Authenticated(users));
} catch (e) {
emit(AuthError('Failed to refresh user data: ${e.toString()}'));
// Optionally, re-emit the previous state or handle as needed
}
}
Future<void> login(String customerNo, String password) async {
emit(AuthLoading());
try {
final users = await _authRepository.login(customerNo, password);
emit(Authenticated(users));
final (users, authToken) =
await _authRepository.login(customerNo, password);
if (authToken.tnc == false) {
emit(ShowTncDialog(authToken, users));
} else {
await _checkMpinAndNavigate(users);
}
} catch (e) {
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 '../../../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 {
const AuthState();
@override
List<Object?> get props => [];
List<Object> get props => [];
}
class AuthInitial extends AuthState {}
@@ -12,20 +15,44 @@ class AuthLoading extends AuthState {}
class Authenticated extends AuthState {
final List<User> users;
Authenticated(this.users);
const Authenticated(this.users);
@override
List<Object?> get props => [users];
List<Object> get props => [users];
}
class Unauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
const AuthError(this.message);
@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

@@ -16,26 +16,23 @@ class ThemeState extends Equatable {
List<Object?> get props => [themeType];
}*/
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:kmobile/config/theme_type.dart';
import 'package:kmobile/config/themes.dart';
class ThemeState extends Equatable {
final ThemeType themeType;
const ThemeState({required this.themeType});
ThemeData getLightThemeData() {
return AppThemes.getLightTheme(themeType);
}
ThemeData getDarkThemeData() {
return AppThemes.getDarkTheme(themeType);
}
@override
List<Object?> get props => [themeType];
ThemeData getLightThemeData() {
return AppThemes.getLightTheme(themeType);
}
ThemeData getDarkThemeData() {
return AppThemes.getDarkTheme(themeType);
}
@override
List<Object?> get props => [themeType];
}

View File

@@ -6,16 +6,31 @@ import 'package:equatable/equatable.dart';
class AuthToken extends Equatable {
final String accessToken;
final DateTime expiresAt;
final bool tnc;
const AuthToken({
required this.accessToken,
required this.expiresAt,
required this.tnc,
});
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(
accessToken: json['token'],
expiresAt: _decodeExpiryFromToken(json['token']),
accessToken: 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);
@override
List<Object> get props => [accessToken, expiresAt];
List<Object> get props => [accessToken, expiresAt, tnc];
}

View File

@@ -1,12 +1,12 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.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/set_password_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../app.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/widgets/tnc_dialog.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
@@ -23,7 +23,6 @@ class LoginScreenState extends State<LoginScreen>
final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
//bool _showWelcome = true;
@override
void dispose() {
@@ -32,48 +31,260 @@ class LoginScreenState extends State<LoginScreen>
super.dispose();
}
void _submitForm() {
void _submitForm() async {
if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
final bool? verificationSuccess = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => VerificationScreen(
customerNo: _customerNumberController.text.trim(),
password: _passwordController.text,
),
),
);
if (verificationSuccess == true && mounted) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
}
}
}
@override
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(
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) async {
if (state is Authenticated) {
final storage = getIt<SecureStorage>();
final mpin = await storage.read('mpin');
if (!context.mounted) return;
if (mpin == null) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(
context,
rootNavigator: true,
).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
},
),
listener: (context, state) {
if (state is ShowTncDialog) {
showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => TncDialog(
onProceed: () async {
// Pop the dialog before the cubit action
Navigator.of(dialogContext).pop();
await context
.read<AuthCubit>()
.onTncDialogResult(true, 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: (_) {
// Call the cubit to signal MPIN setup is complete
context.read<AuthCubit>().mpinSetupCompleted();
},
),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
}
),
);
} else if (state is Authenticated) {
// 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) {
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
Navigator.of(context).push(MaterialPageRoute(
@@ -87,6 +298,7 @@ class LoginScreenState extends State<LoginScreen>
}
},
builder: (context, state) {
// The builder part remains largely the same, focusing on UI display
return Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
@@ -107,7 +319,6 @@ class LoginScreenState extends State<LoginScreen>
},
),
const SizedBox(height: 16),
// Title
Text(
AppLocalizations.of(context).kccb,
style: TextStyle(
@@ -117,12 +328,10 @@ class LoginScreenState extends State<LoginScreen>
),
),
const SizedBox(height: 48),
TextFormField(
controller: _customerNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).customerNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
@@ -147,7 +356,6 @@ class LoginScreenState extends State<LoginScreen>
},
),
const SizedBox(height: 24),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
@@ -189,7 +397,6 @@ class LoginScreenState extends State<LoginScreen>
},
),
const SizedBox(height: 24),
//Login Button
SizedBox(
width: 250,
child: ElevatedButton(
@@ -216,40 +423,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),
// 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 'package:flutter/material.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart';
@@ -16,12 +15,18 @@ class MPinScreen extends StatefulWidget {
final MPinMode mode;
final String? initialPin;
final void Function(String pin)? onCompleted;
final bool disableBiometric;
final String? customTitle;
final String? customConfirmTitle;
const MPinScreen({
super.key,
required this.mode,
this.initialPin,
this.onCompleted,
this.disableBiometric = false,
this.customTitle,
this.customConfirmTitle,
});
@override
@@ -78,7 +83,7 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
);
if (widget.mode == MPinMode.enter) {
if (widget.mode == MPinMode.enter && !widget.disableBiometric) {
_tryBiometricBeforePin();
}
}
@@ -173,29 +178,36 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
}
break;
case MPinMode.set:
// propagate parent onCompleted into confirm step
Navigator.push(
// Navigate to confirm and wait for result
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.confirm,
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;
case MPinMode.confirm:
if (widget.initialPin == pin) {
// 1) persist the pin
await storage.write('mpin', pin);
// 3) now clear the entire navigation stack and go to your main scaffold
// 2) Call the onCompleted callback to let the parent handle navigation
if (mounted) {
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
widget.onCompleted?.call(pin);
}
} else {
setState(() {
@@ -339,6 +351,9 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
}
String getTitle() {
if (widget.customTitle != null) {
return widget.customTitle!;
}
switch (widget.mode) {
case MPinMode.enter:
return AppLocalizations.of(context).enterMPIN;

View File

@@ -140,9 +140,10 @@ class _SetPasswordScreenState extends State<SetPasswordScreen> {
if (_error != null) ...[
Text(
_error!,
style: const TextStyle(color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 20),
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 20),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),

View File

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

View File

@@ -1,3 +1,5 @@
import 'package:package_info_plus/package_info_plus.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
@@ -10,11 +12,29 @@ class SplashScreen extends StatefulWidget {
}
class _SplashScreenState extends State<SplashScreen> {
String _version = '';
@override
void initState() {
super.initState();
_loadVersion();
}
Future<void> _loadVersion() async {
final PackageInfo info = await PackageInfo.fromPlatform();
if (mounted) {
// Check if the widget is still in the tree
setState(() {
_version = 'Version ${info.version} (${info.buildNumber})';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
fit: StackFit.expand,
children: <Widget>[
Positioned.fill(
child: Image.asset(
'assets/images/kconnect2.webp',
@@ -51,8 +71,20 @@ class _SplashScreenState extends State<SplashScreen> {
left: 0,
right: 0,
child: Center(
child: CircularProgressIndicator(
color: Color(0xFFFFFFFF)),
child: CircularProgressIndicator(color: Color(0xFFFFFFFF)),
),
),
Positioned(
bottom: 90,
left: 0,
right: 0,
child: Text(
_version,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFFFFFFFF),
fontSize: 14,
),
),
),
],

View File

@@ -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

@@ -0,0 +1,165 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/auth_service.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
class VerificationScreen extends StatefulWidget {
final String customerNo;
final String password;
const VerificationScreen({
super.key,
required this.customerNo,
required this.password,
});
@override
State<VerificationScreen> createState() => _VerificationScreenState();
}
class _VerificationScreenState extends State<VerificationScreen> {
String _statusMessage = "Starting verification...";
Timer? _timer;
int _countdown = 120;
bool _isVerifying = false;
bool _verificationFailed = false;
@override
void initState() {
super.initState();
_startVerificationProcess();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startTimer() {
_timer?.cancel(); // Cancel any existing timer
_countdown = 120;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown > 0) {
setState(() {
_countdown--;
});
} else {
_timer?.cancel();
if (mounted) {
setState(() {
_statusMessage = "Verification timed out. Please try again.";
_isVerifying = false;
_verificationFailed = true;
});
}
}
});
}
Future<void> _startVerificationProcess() async {
setState(() {
_isVerifying = true;
_verificationFailed = false;
_statusMessage = "Starting verification...";
});
_startTimer();
// 1. Send SMS
setState(() {
_statusMessage = "SMS sending...";
});
final smsHelper = SmsVerificationHelper();
final uuid = await smsHelper.initiateSmsSequence(context: context);
if (uuid != null && mounted) {
// SMS sending was successful, now wait before verifying.
setState(() {
_statusMessage = "SMS sent. Waiting for network delivery...";
});
// Adding a 10-second delay to account for SMS network latency.
await Future.delayed(const Duration(seconds: 10));
if (!mounted) return;
// 2. Verify SIM
setState(() {
_statusMessage = "Verifying with server...";
});
final authService = getIt<AuthService>();
try {
await authService.simVerify(uuid, widget.customerNo);
setState(() {
_statusMessage = "Verification successful!";
_isVerifying = false;
});
_timer?.cancel();
// Pop with success result
Navigator.of(context).pop(true);
} catch (e) {
setState(() {
_statusMessage = e.toString();
_isVerifying = false;
_verificationFailed = true;
});
}
} else if (mounted) {
setState(() {
_statusMessage =
"SMS sending failed. Please check permissions and try again.";
_isVerifying = false;
_verificationFailed = true;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Device Verification"),
automaticallyImplyLeading: !_isVerifying,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isVerifying) const CircularProgressIndicator(),
if (!_isVerifying && _verificationFailed)
const Icon(Icons.error_outline, color: Colors.red, size: 50),
if (!_isVerifying && !_verificationFailed)
const Icon(Icons.check_circle_outline,
color: Colors.green, size: 50),
const SizedBox(height: 32),
Text(
_statusMessage,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
if (_isVerifying)
Text(
"Time remaining: $_countdown seconds",
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
if (_verificationFailed && !_isVerifying) ...[
const SizedBox(height: 20),
ElevatedButton(
onPressed: _startVerificationProcess,
child: const Text('Retry'),
),
]
],
),
),
),
);
}
}

View File

@@ -24,8 +24,8 @@ class AddBeneficiaryScreen extends StatefulWidget {
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final _formKey = GlobalKey<FormState>();
final _accountNumberFieldKey = GlobalKey<FormFieldState>();
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
final _ifscFieldKey = GlobalKey<FormFieldState>();
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
final _ifscFieldKey = GlobalKey<FormFieldState>();
final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController =
TextEditingController();
@@ -47,10 +47,10 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
void initState() {
super.initState();
_ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC();
}
});
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
accountType = 'Savings';
@@ -58,26 +58,26 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
});
}
@override
void dispose() {
accountNumberController.dispose();
confirmAccountNumberController.dispose();
nameController.dispose();
bankNameController.dispose();
branchNameController.dispose();
ifscController.dispose();
phoneController.dispose();
_ifscFocusNode.dispose();
super.dispose();
}
@override
void dispose() {
accountNumberController.dispose();
confirmAccountNumberController.dispose();
nameController.dispose();
bankNameController.dispose();
branchNameController.dispose();
ifscController.dispose();
phoneController.dispose();
_ifscFocusNode.dispose();
super.dispose();
}
void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return;
void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return;
try {
final result = await beneficiaryService.validateIFSC(ifsc);
try {
final result = await beneficiaryService.validateIFSC(ifsc);
if (mounted) {
if (result.bankName.isEmpty) {
@@ -94,7 +94,8 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
} catch (e) {
if (mounted) {
final errorMessage = e.toString().toUpperCase();
String snackbarMessage = AppLocalizations.of(context).somethingWentWrong;
String snackbarMessage =
AppLocalizations.of(context).somethingWentWrong;
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
snackbarMessage = AppLocalizations.of(context).invalidIfsc;
@@ -107,7 +108,7 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
branchNameController.clear();
}
}
}
}
void _validateBeneficiary() async {
FocusScope.of(context).unfocus();
@@ -263,269 +264,306 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
centerTitle: false,
),
body: SafeArea(
child: Form(
key: _formKey,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
TextFormField(
key: _accountNumberFieldKey,
controller: accountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).accountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
),
obscureText: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.length < 10) {
return AppLocalizations.of(
context,
).enterValidAccountNumber;
}
return null;
},
),
const SizedBox(height: 24),
// Confirm Account Number
TextFormField(
key: _confirmAccountNumberFieldKey,
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(
context,
).reenterAccountNumber;
}
if (value != accountNumberController.text) {
return AppLocalizations.of(context,).accountMismatch;
}
return null;
},
),
const SizedBox(height: 24),
TextFormField(
focusNode: _ifscFocusNode,
key: _ifscFieldKey,
controller: ifscController,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) {
// clear bank/branch if backspace or changed
bankNameController.clear();
branchNameController.clear();
}
});
},
validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) {
return AppLocalizations.of(context).enterIfsc;
} else if (!pattern.hasMatch(
value.trim().toUpperCase(),
)) {
return AppLocalizations.of(
context,
).invalidIfscFormat;
}
return null;
},
),
const SizedBox(height: 24),
// Bank Name (Disabled)
TextFormField(
controller: bankNameController,
enabled: false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText: AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
),
),
const SizedBox(height: 24),
// 🔹 Branch Name (Disabled)
TextFormField(
controller: branchNameController,
enabled: false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText: AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
),
),
if (_isBeneficiaryValidated)
Column(
children: [
const SizedBox(height: 24),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
suffixIcon: _isBeneficiaryValidated
? const Icon(
Symbols.verified,
size: 25,
fill: 1,
)
: null,
suffixIconColor:
Theme.of(context).colorScheme.primary,
labelText: AppLocalizations.of(context)
.beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
validator: (value) => value == null ||
value.isEmpty
? AppLocalizations.of(context).nameRequired
: null,
child: Stack(
children: [
Form(
key: _formKey,
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
children: [
TextFormField(
key: _accountNumberFieldKey,
controller: accountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).accountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
),
],
),
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();
if (isAccountValid && isConfirmAccountValid && isIfscValid) {
_validateBeneficiary();
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: Text(AppLocalizations.of(context)
.validateBeneficiary),
obscureText: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.length < 10) {
return AppLocalizations.of(
context,
).enterValidAccountNumber;
}
return null;
},
),
const SizedBox(height: 24),
// Confirm Account Number
TextFormField(
key: _confirmAccountNumberFieldKey,
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(
context,
).reenterAccountNumber;
}
if (value != accountNumberController.text) {
return AppLocalizations.of(
context,
).accountMismatch;
}
return null;
},
),
const SizedBox(height: 24),
TextFormField(
focusNode: _ifscFocusNode,
key: _ifscFieldKey,
controller: ifscController,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(),
isDense: true,
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) {
// clear bank/branch if backspace or changed
bankNameController.clear();
branchNameController.clear();
}
});
},
validator: (value) {
final pattern =
RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) {
return AppLocalizations.of(context).enterIfsc;
} else if (!pattern.hasMatch(
value.trim().toUpperCase(),
)) {
return AppLocalizations.of(
context,
).invalidIfscFormat;
}
return null;
},
),
const SizedBox(height: 24),
// Bank Name (Disabled)
TextFormField(
controller: bankNameController,
enabled:
false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
),
),
),
//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),
// 🔹 Branch Name (Disabled)
TextFormField(
controller: branchNameController,
enabled:
false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
),
),
if (_isBeneficiaryValidated)
Column(
children: [
const SizedBox(height: 24),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
suffixIcon: _isBeneficiaryValidated
? const Icon(
Symbols.verified,
size: 25,
fill: 1,
)
: null,
suffixIconColor:
Theme.of(context).colorScheme.primary,
labelText: AppLocalizations.of(context)
.beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
validator: (value) =>
value == null || value.isEmpty
? AppLocalizations.of(context)
.nameRequired
: null,
),
],
),
const SizedBox(height: 24),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating ||
ifscController.text.length != 11
? null
: () {
final isAccountValid =
_accountNumberFieldKey
.currentState!
.validate();
final isConfirmAccountValid =
_confirmAccountNumberFieldKey
.currentState!
.validate();
final isIfscValid = _ifscFieldKey
.currentState!
.validate();
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
if (isAccountValid &&
isConfirmAccountValid &&
isIfscValid) {
_validateBeneficiary();
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: 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
: 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,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
_showSuccessDialog(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete beneficiary: $e')),
SnackBar(
content: Text(
'${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
);
}
}
@@ -32,11 +34,12 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Success'),
content: const Text('Beneficiary deleted successfully.'),
title: Text(AppLocalizations.of(context).success),
content:
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[
TextButton(
child: const Text('OK'),
child: Text(AppLocalizations.of(context).ok),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
@@ -52,18 +55,18 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete Beneficiary'),
content:
const Text('Are you sure you want to delete this beneficiary?'),
title: Text(AppLocalizations.of(context).deleteBeneficiary),
content: Text(AppLocalizations.of(context)
.areYouSureYouWantToDeleteThisBeneficiary),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
child: Text(AppLocalizations.of(context).cancel),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Delete'),
child: Text(AppLocalizations.of(context).delete),
onPressed: () {
_deleteBeneficiary(context);
},
@@ -81,60 +84,80 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
title: Text(AppLocalizations.of(context).beneficiarydetails),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context),
Row(
children: [
CircleAvatar(
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),
Text(
beneficiary.name,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
const SizedBox(height: 24),
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
beneficiary.bankName ?? 'N/A'),
_buildDetailRow(
'${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} ',
beneficiary.bankName ?? 'N/A'),
_buildDetailRow('${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),
),
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,7 +90,10 @@ class _BeneficiaryResultPageState extends State<BeneficiaryResultPage> {
),
child: Text(
AppLocalizations.of(context).done,
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer), // slightly bigger text
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer), // slightly bigger text
),
),
),

View File

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

View File

@@ -56,137 +56,159 @@ class _BlockCardScreen extends State<BlockCardScreen> {
appBar: AppBar(
title: Text(
AppLocalizations.of(context).blockCard,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: ListView(
children: [
const SizedBox(height: 10),
TextFormField(
controller: _cardController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) => value != null && value.length == 11
? null
: AppLocalizations.of(context).enterValidCardNumber,
),
const SizedBox(height: 24),
Row(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Expanded(
child: TextFormField(
controller: _cvvController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv,
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),
),
const SizedBox(height: 10),
TextFormField(
controller: _cardController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
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),
Expanded(
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),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: TextFormField(
controller: _cvvController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv,
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,
obscureText: true,
validator: (value) =>
value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
),
),
validator: (value) => value != null && value.isNotEmpty
? null
: AppLocalizations.of(context).selectExpiryDate,
const SizedBox(width: 16),
Expanded(
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,31 @@ class CardDetailsScreen extends StatelessWidget {
appBar: AppBar(
title: const Text("My Cards"),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: const [
CardTile(
cardNumber: "**** **** **** 1234",
cardNetwork: "VISA",
cardType: "Debit Card",
validFrom: "01/22",
validTo: "01/27",
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: const [
CardTile(
cardNumber: "**** **** **** 1234",
cardNetwork: "VISA",
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(
cardNumber: "**** **** **** 5678",
cardNetwork: "Mastercard",
cardType: "Debit Card",
validFrom: "07/21",
validTo: "07/26",
),
],
),
),
],
),
);
}
@@ -87,7 +91,7 @@ class CardTile extends StatelessWidget {
"Kangra Central Co-operative Bank",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontSize: 15.5,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,

View File

@@ -17,83 +17,101 @@ class CardManagementScreen extends StatefulWidget {
class _CardManagementScreen extends State<CardManagementScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
AppLocalizations.of(context).cardManagement,
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
AppLocalizations.of(context).cardManagement,
),
centerTitle: false,
),
centerTitle: false,
),
body: ListView(
children: [
CardManagementTile(
icon: Symbols.add,
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(),
body: Stack(
children: [
ListView(
children: [
CardManagementTile(
icon: Symbols.add,
label: AppLocalizations.of(context).applyDebitCard,
onTap: () {},
disabled: true, // Add this
),
Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile(
icon: Symbols.remove_moderator,
label: AppLocalizations.of(context).blockUnblockCard,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BlockCardScreen(),
),
);
},
disabled: true,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile(
icon: Symbols.password_2,
label: AppLocalizations.of(context).changeCardPin,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CardPinChangeDetailsScreen(),
),
);
},
disabled: true,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile(
icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CardDetailsScreen(),
),
);
},
disabled: false,
),
Divider(height: 1, color: Theme.of(context).dividerColor),
],
),
IgnorePointer(
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
),
),
);
},
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),
],
),
);
}
),
),
),
],
),
);
}
}
class CardManagementTile extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback onTap;
final bool disabled;
final bool disabled;
const CardManagementTile({
super.key,
required this.icon,
required this.label,
required this.onTap,
this.disabled = false,
this.disabled = false,
});
@override
@@ -102,20 +120,20 @@ class CardManagementTile extends StatelessWidget {
return ListTile(
leading: Icon(
icon,
color: disabled ? theme.disabledColor : null,
color: disabled ? theme.disabledColor : null,
),
title: Text(
label,
style: TextStyle(
color: disabled ? theme.disabledColor : null,
color: disabled ? theme.disabledColor : null,
),
),
trailing: Icon(
Symbols.arrow_right,
size: 20,
color: disabled ? theme.disabledColor : null,
color: disabled ? theme.disabledColor : null,
),
onTap: disabled ? null : onTap,
onTap: disabled ? null : onTap,
);
}
}
}

View File

@@ -46,137 +46,159 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
appBar: AppBar(
title: Text(
AppLocalizations.of(context).cardDetails,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: ListView(
children: [
const SizedBox(height: 10),
TextFormField(
controller: _cardController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) => value != null && value.length == 11
? null
: AppLocalizations.of(context).enterValidCardNumber,
),
const SizedBox(height: 24),
Row(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
child: ListView(
children: [
Expanded(
child: TextFormField(
controller: _cvvController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv,
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),
),
const SizedBox(height: 10),
TextFormField(
controller: _cardController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cardNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
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),
Expanded(
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),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: TextFormField(
controller: _cvvController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).cvv,
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,
obscureText: true,
validator: (value) =>
value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
),
),
validator: (value) => value != null && value.isNotEmpty
? null
: AppLocalizations.of(context).selectExpiryDate,
const SizedBox(width: 16),
Expanded(
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

@@ -46,92 +46,108 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
appBar: AppBar(
title: Text(
AppLocalizations.of(context).cardPin,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _pinController,
obscureText: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterNewPin,
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) {
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,
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _pinController,
obscureText: true,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterNewPin,
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),
),
),
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

@@ -17,55 +17,72 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
appBar: AppBar(
title: Text(
AppLocalizations.of(context).chequeManagement,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
),
body: ListView(
body: Stack(
children: [
const SizedBox(height: 15),
ChequeManagementTile(
icon: Symbols.add,
label: AppLocalizations.of(context).requestChequeBook,
onTap: () {},
ListView(
children: [
const SizedBox(height: 15),
ChequeManagementTile(
icon: Symbols.add,
label: AppLocalizations.of(context).requestChequeBook,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.data_alert,
label: AppLocalizations.of(context).enquiry,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const EnquiryScreen()),
);
},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.approval_delegation,
label: AppLocalizations.of(context).chequeDeposit,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.front_hand,
label: AppLocalizations.of(context).stopCheque,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.cancel_presentation,
label: AppLocalizations.of(context).revokeStop,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile(
icon: Symbols.payments,
label: AppLocalizations.of(context).positivePay,
onTap: () {},
),
Divider(height: 1, color: Theme.of(context).dividerColor),
],
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.data_alert,
label: AppLocalizations.of(context).enquiry,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const EnquiryScreen()),
);
},
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),
ChequeManagementTile(
icon: Symbols.approval_delegation,
label: AppLocalizations.of(context).chequeDeposit,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.front_hand,
label: AppLocalizations.of(context).stopCheque,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.cancel_presentation,
label: AppLocalizations.of(context).revokeStop,
onTap: () {},
),
const Divider(height: 1),
ChequeManagementTile(
icon: Symbols.payments,
label: AppLocalizations.of(context).positivePay,
onTap: () {},
),
const Divider(height: 1),
],
),
);

View File

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

View File

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

View File

@@ -72,4 +72,3 @@ class AccountCard extends StatelessWidget {
);
}
}

View File

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

View File

@@ -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 'package:dio/dio.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/rtgs_service.dart';
import 'package:kmobile/api/services/imps_service.dart';
@@ -40,13 +42,69 @@ class FundTransferAmountScreen extends StatefulWidget {
}
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 _remarksController = TextEditingController();
final _formKey = GlobalKey<FormState>();
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
void dispose() {
_amountController.removeListener(_checkAmountLimit);
_amountController.dispose();
_remarksController.dispose();
super.dispose();
@@ -304,149 +362,176 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Debit Account (User)
Text(
loc.debitFrom,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: Image.asset(
'assets/images/logo.png',
width: 40,
height: 40,
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Debit Account (User)
Text(
loc.debitFrom,
style: Theme.of(context).textTheme.titleSmall,
),
title: Text(widget.remitterName),
subtitle: Text(widget.debitAccountNo),
),
),
const SizedBox(height: 24),
// Credit Account (Beneficiary)
Text(
AppLocalizations.of(context).creditedTo,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading:
getBankLogo(widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
),
const SizedBox(height: 24),
if (!widget.isOwnBank) ...[
// Transaction Mode Selection
Text(
AppLocalizations.of(context).selectTransactionType,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
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),
),
),
child: ToggleButtons(
isSelected: [
_selectedMode == TransactionMode.neft,
_selectedMode == TransactionMode.rtgs,
_selectedMode == TransactionMode.imps,
],
onPressed: (index) {
setState(() {
_selectedMode = TransactionMode.values[index];
});
const SizedBox(height: 24),
// Credit Account (Beneficiary)
Text(
AppLocalizations.of(context).creditedTo,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: getBankLogo(
widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
),
const SizedBox(height: 24),
if (!widget.isOwnBank) ...[
// Transaction Mode Selection
Text(
AppLocalizations.of(context).selectTransactionType,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: ToggleButtons(
isSelected: [
_selectedMode == TransactionMode.neft,
_selectedMode == TransactionMode.rtgs,
_selectedMode == TransactionMode.imps,
],
onPressed: (index) {
setState(() {
_selectedMode = TransactionMode.values[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor:
Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent,
splashColor: Theme.of(context).colorScheme.primary,
highlightColor: Theme.of(context).colorScheme.primary,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).neft),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).rtgs),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).imps),
),
],
),
),
const SizedBox(height: 24),
],
//Remarks
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return loc.amountRequired;
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return loc.validAmount;
}
return null;
},
borderRadius: BorderRadius.circular(10),
selectedColor: Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent,
splashColor: Theme.of(context).colorScheme.primary,
highlightColor: Theme.of(context).colorScheme.primary,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).neft),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).rtgs),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).imps),
),
],
),
),
const SizedBox(height: 24),
],
//Remarks
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return loc.amountRequired;
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return loc.validAmount;
}
return null;
},
),
const Spacer(),
const SizedBox(height: 8),
if (_isLoadingLimit)
Text(AppLocalizations.of(context).fetchingDailyLimit),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).proceed),
),
),
const SizedBox(height: 10),
],
),
),
),
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:kmobile/features/fund_transfer/screens/cooldown.dart';
import 'package:kmobile/widgets/bank_logos.dart';
import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart';
@@ -27,11 +28,22 @@ class _FundTransferBeneficiaryScreenState
var service = getIt<BeneficiaryService>();
bool _isLoading = true;
List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadBeneficiaries() async {
@@ -42,10 +54,25 @@ class _FundTransferBeneficiaryScreenState
? b.bankName!.toLowerCase().contains('kangra central')
: !b.bankName!.toLowerCase().contains('kangra central'))
.toList();
_filteredBeneficiaries = _beneficiaries;
_isLoading = false;
});
}
void _filterBeneficiaries(String query) {
setState(() {
if (query.isEmpty) {
_filteredBeneficiaries = _beneficiaries;
} else {
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
final lowerQuery = query.toLowerCase();
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
Widget _buildShimmerList() {
return ListView.builder(
itemCount: 6,
@@ -73,46 +100,84 @@ class _FundTransferBeneficiaryScreenState
}
Widget _buildBeneficiaryList() {
if (_beneficiaries.isEmpty) {
if (_filteredBeneficiaries.isEmpty) {
return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
}
return ListView.builder(
itemCount: _beneficiaries.length,
itemCount: _filteredBeneficiaries.length,
itemBuilder: (context, index) {
final beneficiary = _beneficiaries[index];
return ListTile(
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]),
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferAmountScreen(
debitAccountNo: widget.creditAccountNo,
creditBeneficiary: beneficiary,
remitterName: widget.remitterName,
isOwnBank: widget.isOwnBank,
),
final beneficiary = _filteredBeneficiaries[index];
// --- Cooldown Logic ---
bool isCoolingDown = false;
if (beneficiary.createdAt != null) {
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 Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Opacity(
opacity: isCoolingDown ? 0.5 : 1.0,
child: ListTile(
// REMOVED the 'enabled' property from here.
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context),
),
);
},
title: Text(beneficiary.name),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(beneficiary.accountNo),
if (beneficiary.bankName != null &&
beneficiary.bankName!.isNotEmpty)
Text(
beneficiary.bankName!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
trailing: isCoolingDown
? CooldownTimer(
createdAt: beneficiary.createdAt!,
onTimerFinish: () {
setState(() {});
},
)
: null,
onTap: () {
if (isCoolingDown) {
// This will now execute correctly on tap
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Beneficiary will be enabled after the cooldown period.'),
behavior: SnackBarBehavior.floating,
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferAmountScreen(
debitAccountNo: widget.creditAccountNo,
creditBeneficiary: beneficiary,
remitterName: widget.remitterName,
isOwnBank: widget.isOwnBank,
),
),
);
}
},
),
),
);
},
);
@@ -124,7 +189,45 @@ class _FundTransferBeneficiaryScreenState
appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries),
),
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
body: Stack(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

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

View File

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

View File

@@ -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

@@ -213,7 +213,8 @@ class _PaymentAnimationScreenState extends State<PaymentAnimationScreen> {
),
label: Text(
AppLocalizations.of(context).share,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
style: TextStyle(
color: Theme.of(context).colorScheme.primary),
),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(

View File

@@ -18,8 +18,9 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
6,
(_) => TextEditingController(),
);
bool _isLoading = false;
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>();
bool _isLoading = false;
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
@override
void dispose() {
@@ -33,7 +34,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
}
void _onOtpChanged(int idx, String value) {
if (value.length == 1 && idx <5) {
if (value.length == 1 && idx < 5) {
_focusNodes[idx + 1].requestFocus();
}
if (value.isEmpty && idx > 0) {
@@ -44,160 +45,186 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
String get _enteredOtp => _controllers.map((c) => c.text).join();
void _verifyOtp() async {
setState(() {
_isLoading = true;
});
void _verifyOtp() async {
setState(() {
_isLoading = true;
});
try {
await _changePasswordService.validateOtp(
otp: _enteredOtp,
mobileNumber: widget.mobileNumber,
);
try {
await _changePasswordService.validateOtp(
otp: _enteredOtp,
mobileNumber: widget.mobileNumber,
);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const TpinSetScreen()),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const TpinSetScreen()),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).enterOtp),
centerTitle: true,
elevation: 0,
),
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
Icon(
Icons.lock_outline,
size: 48,
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).otpVerification,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context).otpSentMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey[700],
),
),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(6, (i) {
return Container(
width: 32,
margin: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
controller: _controllers[i],
focusNode: _focusNodes[i],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
obscureText: true,
obscuringCharacter: '*',
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Theme.of(context).primaryColorLight,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2.5,
),
),
),
onChanged: (val) => _onOtpChanged(i, val),
),
);
}),
),
const SizedBox(height: 32),
ElevatedButton.icon(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.verified_user_rounded),
label: Text(
AppLocalizations.of(context).verifyOtp,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 28,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
// Update onPressed to handle loading state
onPressed: (_enteredOtp.length == 6 && !_isLoading)
? _verifyOtp
: null,
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
// You can also add a getOtp call here for resending
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).otpResent),
),
);
},
child: Text(AppLocalizations.of(context).resendOtp),
),
const SizedBox(height: 60),
],
),
),
),
);
}
}
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).enterOtp),
centerTitle: true,
elevation: 0,
),
body: Center(
child: SingleChildScrollView(
child: Column(
children: [
Icon(
Icons.lock_outline,
size: 48,
color: theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
AppLocalizations.of(context).otpVerification,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 8),
Text(
AppLocalizations.of(context).otpSentMessage,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey[700],
),
),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(6, (i) {
return Container(
width: 50,
height: 60,
margin: const EdgeInsets.symmetric(horizontal: 6),
child: Stack(
alignment: Alignment.center,
children: [
TextField(
controller: _controllers[i],
focusNode: _focusNodes[i],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
style: const TextStyle(
color: Colors.transparent,
fontSize: 24,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Colors.grey[200],
contentPadding:
const EdgeInsets.symmetric(vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey[400]!,
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Colors.grey[400]!,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: theme.colorScheme.primary,
width: 2.5,
),
),
),
onChanged: (val) => _onOtpChanged(i, val),
),
if (_controllers[i].text.isNotEmpty)
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.black87,
shape: BoxShape.circle,
),
),
],
),
);
}),
),
const SizedBox(height: 32),
ElevatedButton.icon(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.verified_user_rounded),
label: Text(
AppLocalizations.of(context).verifyOtp,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 28,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
),
// Update onPressed to handle loading state
onPressed: (_enteredOtp.length == 6 && !_isLoading)
? _verifyOtp
: null,
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
// You can also add a getOtp call here for resending
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).otpResent),
),
);
},
child: Text(AppLocalizations.of(context).resendOtp),
),
const SizedBox(height: 60),
],
),
),
),
);
}
}

View File

@@ -15,44 +15,50 @@ class TpinSetupPromptScreen extends StatefulWidget {
State<TpinSetupPromptScreen> createState() => _TpinSetupPromptScreenState();
}
class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> {
int selectedAccountIndex = 0;
bool _isLoading = false;
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>();
Future<void> _getOtp() async {
setState(() {
_isLoading = true;
});
class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> {
int selectedAccountIndex = 0;
bool _isLoading = false;
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
Future<void> _getOtp() async {
setState(() {
_isLoading = true;
});
try {
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 && selectedAccountIndex < authState.users.length) {
mobileNumberToPass = authState.users[selectedAccountIndex].mobileNo ?? '';
}
}
await _changePasswordService.getOtpTpin(mobileNumber: mobileNumberToPass);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => TpinOtpScreen(mobileNumber: mobileNumberToPass,)),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
try {
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 &&
selectedAccountIndex < authState.users.length) {
mobileNumberToPass =
authState.users[selectedAccountIndex].mobileNo ?? '';
}
}
await _changePasswordService.getOtpTpin(mobileNumber: mobileNumberToPass);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => TpinOtpScreen(
mobileNumber: mobileNumberToPass,
)),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
@@ -86,33 +92,35 @@ class TpinSetupPromptScreen extends StatefulWidget {
),
),
const SizedBox(height: 32),
ElevatedButton.icon(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.arrow_forward_rounded),
label: Text(
AppLocalizations.of(context).setTpin,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 32,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed: _isLoading ? null : _getOtp, // <-- Use the new function
),
ElevatedButton.icon(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.arrow_forward_rounded),
label: Text(
AppLocalizations.of(context).setTpin,
style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 32,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
onPressed:
_isLoading ? null : _getOtp, // <-- Use the new function
),
const SizedBox(height: 18),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
@@ -129,4 +137,4 @@ ElevatedButton.icon(
),
);
}
}
}

View File

@@ -212,4 +212,3 @@ class _TransactionPinScreenState extends State<TransactionPinScreen>
);
}
}

View File

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

View File

@@ -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

@@ -14,12 +14,13 @@ class ChangePasswordOTPScreen extends StatefulWidget {
const ChangePasswordOTPScreen({
required this.currentPassword,
required this.newPassword,
required this.confirmPassword,
required this.confirmPassword,
required this.mobileNumber,
});
@override
State<ChangePasswordOTPScreen> createState() => _ChangePasswordOTPScreenState();
State<ChangePasswordOTPScreen> createState() =>
_ChangePasswordOTPScreenState();
}
class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
@@ -36,74 +37,92 @@ class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
});
});
}
final changePasswordService = getIt<ChangePasswordService>();
Future<void> _validateOTP() async {
try {
await changePasswordService.validateOtp(
otp: otpController.text,
mobileNumber: widget.mobileNumber,
);
try {
await changePasswordService.validateOtp(
otp: otpController.text,
mobileNumber: widget.mobileNumber,
);
// If OTP is valid, then change the password
await changePasswordService.validateChangePwd(
OldLPsw: widget.currentPassword,
newLPsw: widget.newPassword,
confirmLPsw: widget.confirmPassword,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).pwdchangeSuccess)),
);
// If OTP is valid, then change the password
await changePasswordService.validateChangePwd(
OldLPsw: widget.currentPassword,
newLPsw: widget.newPassword,
confirmLPsw: widget.confirmPassword,
);
// Navigate back to profile or login
Navigator.of(context).popUntil((route) => route.isFirst);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).pwdchangeSuccess)),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
// Navigate back to profile or login
Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
AppLocalizations.of(context).otpSent,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
TextFormField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterOTP,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateOTP,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
AppLocalizations.of(context).otpSent,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
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

@@ -56,100 +56,120 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
}
return null;
}
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>();
void _proceed() async {
if (_formKey.currentState!.validate()) {
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
void _proceed() async {
if (_formKey.currentState!.validate()) {
try {
await _changePasswordService.getOtp(mobileNumber: widget.mobileNumber);
try {
await _changePasswordService.getOtp(mobileNumber: widget.mobileNumber);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordOTPScreen(
currentPassword: currentPasswordController.text,
newPassword: newPasswordController.text,
confirmPassword: confirmPasswordController.text,
mobileNumber: widget.mobileNumber,
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordOTPScreen(
currentPassword: currentPasswordController.text,
newPassword: newPasswordController.text,
confirmPassword: confirmPasswordController.text,
mobileNumber: widget.mobileNumber,
),
),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).failedtosentOTP}: $e')),
);
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('${AppLocalizations.of(context).failedtosentOTP}: $e')),
);
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: currentPasswordController,
obscureText: !_showCurrentPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).currentpwd,
suffixIcon: IconButton(
icon: Icon(_showCurrentPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () =>
setState(() => _showCurrentPassword = !_showCurrentPassword),
appBar:
AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)),
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: currentPasswordController,
obscureText: !_showCurrentPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).currentpwd,
suffixIcon: IconButton(
icon: Icon(_showCurrentPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showCurrentPassword = !_showCurrentPassword),
),
),
validator: validateCurrentPassword,
),
),
validator: validateCurrentPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: newPasswordController,
obscureText: !_showNewPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).newpwd,
suffixIcon: IconButton(
icon: Icon(_showNewPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () =>
setState(() => _showNewPassword = !_showNewPassword),
const SizedBox(height: 16),
TextFormField(
controller: newPasswordController,
obscureText: !_showNewPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).newpwd,
suffixIcon: IconButton(
icon: Icon(_showNewPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showNewPassword = !_showNewPassword),
),
),
validator: validateNewPassword,
),
),
validator: validateNewPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: !_showConfirmPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmpwd,
suffixIcon: IconButton(
icon: Icon(_showConfirmPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () =>
setState(() => _showConfirmPassword = !_showConfirmPassword),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: !_showConfirmPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmpwd,
suffixIcon: IconButton(
icon: Icon(_showConfirmPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showConfirmPassword = !_showConfirmPassword),
),
),
validator: validateConfirmPassword,
),
),
validator: validateConfirmPassword,
const SizedBox(height: 24),
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

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

View File

@@ -8,8 +8,8 @@ class LogoutDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).logout),
content: Text(AppLocalizations.of(context).logoutCheck),
title: Text(AppLocalizations.of(context).deregister),
content: Text(AppLocalizations.of(context).deregistercheck),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false), // dismiss
@@ -22,4 +22,4 @@ class LogoutDialog extends StatelessWidget {
],
);
}
}
}

View File

@@ -20,43 +20,69 @@ class PreferenceScreen extends StatelessWidget {
),
body: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) {
return ListView(
return Stack(
children: [
//Set Prefered Username
// ListTile(
// leading: const Icon(Icons.person),
// title: const Text("Set Prefered Username"),
// onTap: () {
// }),
// Language Selection
ListTile(
leading: const Icon(Icons.language),
title: Text(loc.language),
onTap: () {
showDialog(
context: context,
builder: (_) => const LanguageDialog(),
);
},
ListView(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
// Language Selection
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.language),
title: Text(loc.language),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (_) => const LanguageDialog(),
);
},
),
),
//Theme Mode Switch (Light/Dark)
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showThemeModeDialog(context);
},
),
),
//Color_Theme_Selection
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (_) => const ColorThemeDialog(),
);
}),
),
],
),
//Theme Mode Switch (Light/Dark)
ListTile(
leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode),
onTap: () {
showThemeModeDialog(context);
},
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
),
),
),
),
),
//Color_Theme_Selection
ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
onTap: () {
showDialog(
context: context,
builder: (_) => const ColorThemeDialog(),
);
}),
],
);
},

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