2 Commits

Author SHA1 Message Date
f9fd74ea53 Rooted Device Check #1.2 2025-09-02 17:24:43 +05:30
852f708633 Rooted Device Check #1 2025-09-02 17:21:58 +05:30
141 changed files with 3584 additions and 11809 deletions

5
.gitignore vendored
View File

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

View File

@@ -10,7 +10,6 @@
analyzer: analyzer:
errors: errors:
dead_code: ignore dead_code: ignore
non_constant_identifier_names: ignore
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
linter: linter:

View File

@@ -22,19 +22,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0' flutterVersionName = '1.0'
} }
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android { android {
namespace "com.example.kmobile" namespace "com.example.kmobile"
compileSdk flutter.compileSdkVersion compileSdk flutter.compileSdkVersion
ndkVersion "27.0.12077973" ndkVersion "27.0.12077973"
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@@ -52,27 +45,21 @@ android {
applicationId "com.example.kmobile" applicationId "com.example.kmobile"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 29 minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes { buildTypes {
release { release {
signingConfig signingConfigs.release signingConfig signingConfigs.debug
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
} }
} }
} }
@@ -81,6 +68,4 @@ flutter {
source '../..' source '../..'
} }
dependencies { dependencies {}
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

View File

@@ -1,21 +1,77 @@
-keep class io.flutter.app.** { *; } # Keep Flutter embedding and plugin registrant (important)
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.** { *; } -keep class io.flutter.embedding.** { *; }
-keep class io.flutter.embedding.engine.plugins.** { *; } -keep class io.flutter.plugins.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.app.** { *; }
# Keep Application / Activity if you customized them (replace with your package name)
# -keep class com.yourcompany.yourapp.MainActivity { *; }
# -keep class com.yourcompany.yourapp.** { *; }
# Keep classes referenced from AndroidManifest via reflection or lifecycle
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
public <init>(android.content.Context, ...);
}
# Keep native entry points (JNI) if any (example)
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep classes that use reflection heavily (OKHttp/Retrofit/Gson)
# Retrofit/OkHttp
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-keep class retrofit2.** { *; }
-keep interface retrofit2.** { *; }
# Gson (models accessed by reflection)
-keep class com.google.gson.** { *; }
-keepattributes Signature
-keepattributes *Annotation*
# Keep Firebase (if you use it)
-keep class com.google.firebase.** { *; } -keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; } -keep class com.google.android.gms.** { *; }
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin. # WorkManager (if used)
-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication -keep class androidx.work.impl.background.systemjob.SystemJobService { *; }
-dontwarn com.google.android.play.core.splitinstall.SplitInstallException -keep class androidx.work.** { *; }
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory # Room/DB entities - if you use Room, keep annotations and entity classes
-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder -keepclassmembers class * {
-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest @androidx.room.* <fields>;
-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState }
-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener -keep class androidx.room.** { *; }
-dontwarn com.google.android.play.core.tasks.OnFailureListener
-dontwarn com.google.android.play.core.tasks.OnSuccessListener # Keep classes loaded by reflection (e.g. through Class.forName)
-dontwarn com.google.android.play.core.tasks.Task -keepclassmembers,includedescriptorclasses class * {
public static <fields>;
public <init>(...);
}
# Keep Kotlin metadata (for Kotlin reflection)
-keep class kotlin.Metadata { *; }
# Keep names of classes annotated with @Keep
-keep @androidx.annotation.Keep class * { *; }
-keepclassmembers class * {
@androidx.annotation.Keep *;
}
# If using Gson TypeAdapterFactory via reflection
-keepclassmembers class * {
static ** typeAdapterFactory*(...);
}
# Don't remove debug/logging if you want (optional)
#-keep class android.util.Log { *; }
# Keep any generated plugin registrant classes if present (older projects)
-keep class io.flutter.plugins.GeneratedPluginRegistrant { *; }
# (Optional) Keep parcelable implementations if those are serialized dynamically
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

View File

@@ -2,13 +2,10 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC"/> <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application <application
android:label="kmobile" android:label="kmobile"
android:name="${applicationName}" android:name="${applicationName}"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@@ -42,20 +39,6 @@
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. --> In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT"/> <action android:name="android.intent.action.PROCESS_TEXT"/>

View File

@@ -1,12 +1,5 @@
package com.example.kmobile 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() { import io.flutter.embedding.android.FlutterFragmentActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) class MainActivity: FlutterFragmentActivity()
window.addFlags(LayoutParams.FLAG_SECURE)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -47,19 +47,5 @@
<true/> <true/>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>We use Face ID to secure your data.</string> <string>We use Face ID to secure your data.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes and take pictures.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to save and share images.</string>
<key>NSContactsUsageDescription</key>
<string>This app needs contacts access to easily send money to your contacts.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice commands.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -8,41 +8,6 @@ class AuthService {
final Dio _dio; final Dio _dio;
AuthService(this._dio); AuthService(this._dio);
Future<void> simVerify(String uuid, String cifNo) async {
try {
final response = await _dio.post('/api/sim-details-verify', data: {
'uuid': uuid,
'cifNo': cifNo,
});
if (response.statusCode == 200) {
final String message = response.data.toString().toUpperCase();
if (message.contains("VERIFIED")) {
return; // Success
} else {
throw AuthException(message); // Throw message received
}
} else {
throw AuthException('Verification Failed');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
if (e.response?.statusCode == 401) {
throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
}
throw NetworkException('Network error during verification');
} catch (e) {
throw UnexpectedException(
'Unexpected error during verification: ${e.toString()}');
}
}
Future<AuthToken> login(AuthCredentials credentials) async { Future<AuthToken> login(AuthCredentials credentials) async {
try { try {
final response = await _dio.post( final response = await _dio.post(
@@ -60,8 +25,7 @@ class AuthService {
print(e.toString()); print(e.toString());
} }
if (e.response?.statusCode == 401) { if (e.response?.statusCode == 401) {
throw AuthException( throw AuthException('Invalid credentials');
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
} }
throw NetworkException('Network error during login'); throw NetworkException('Network error during login');
} catch (e) { } catch (e) {
@@ -127,72 +91,4 @@ class AuthService {
'Unexpected error during TPIN setup: ${e.toString()}'); 'Unexpected error during TPIN setup: ${e.toString()}');
} }
} }
Future<void> sendOtpForSettingPassword(String customerNo) async {
try {
final response =
await _dio.get('/api/otp/send/set-password', queryParameters: {
'customerNo': customerNo,
});
if (response.statusCode != 200) {
throw Exception(response.data['error'] ?? 'Failed to send OTP');
}
return;
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
Future<String> verifyOtpForSettingPassword(
String customerNo, String otp) async {
try {
final response = await _dio.get(
'/api/otp/verify/set-password',
queryParameters: {'customerNo': customerNo, 'otp': otp},
);
if (response.statusCode == 200) {
return response.data['token'];
} else {
throw Exception(response.data['error'] ?? 'Failed to verify OTP');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
Future changePassword(String newPassword, String token) async {
final response = await _dio.post(
'/api/auth/login_password',
data: {'login_password': newPassword},
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
if (response.statusCode != 200) {
throw Exception('Error setting password');
}
return;
}
Future setTncflag() async {
try {
final response = await _dio.post(
'/api/auth/tnc',
data: {"flag": 'Y'},
);
if (response.statusCode != 200) {
throw AuthException('Failed to proceed with T&C');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
throw NetworkException('Network error during T&C Setup');
} catch (e) {
throw UnexpectedException('Unexpected error: ${e.toString()}');
}
}
} }

View File

@@ -1,6 +1,5 @@
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:kmobile/core/errors/exceptions.dart';
import 'package:kmobile/data/models/ifsc.dart'; import 'package:kmobile/data/models/ifsc.dart';
import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/data/models/beneficiary.dart';
@@ -40,12 +39,9 @@ class BeneficiaryService {
} on DioException catch (e) { } on DioException catch (e) {
if (e.response?.statusCode == 404) { if (e.response?.statusCode == 404) {
throw Exception('INVALID IFSC CODE'); throw Exception('INVALID IFSC CODE');
} else if (e.response?.statusCode == 401) {
throw Exception('INVALID IFSC CODE');
} }
} catch (e) { } catch (e) {
throw UnexpectedException( rethrow;
'Unexpected error during login: ${e.toString()}');
} }
return Ifsc.fromJson({}); return Ifsc.fromJson({});
} }
@@ -63,10 +59,6 @@ class BeneficiaryService {
'ifscCode': ifscCode, 'ifscCode': ifscCode,
'remitterName': remitterName, 'remitterName': remitterName,
}, },
options: Options(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
),
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception("Invalid Beneficiary Details"); throw Exception("Invalid Beneficiary Details");
@@ -74,7 +66,7 @@ class BeneficiaryService {
return response.data['name']; return response.data['name'];
} }
// Beneficiary Validate And ADD // Send Data for Validation
Future<bool> sendForValidation(Beneficiary beneficiary) async { Future<bool> sendForValidation(Beneficiary beneficiary) async {
try { try {
final response = await _dio.post( final response = await _dio.post(

View File

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

View File

@@ -1,87 +0,0 @@
import 'package:dio/dio.dart';
class ChangePasswordService {
final Dio _dio;
ChangePasswordService(this._dio);
Future getOtp({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_LPWORD"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future getOtpTpin({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_TPIN"},
);
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();
}
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,
},
);
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

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

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

View File

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

View File

@@ -2,8 +2,6 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_state.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart'; import './l10n/app_localizations.dart';
@@ -12,15 +10,14 @@ import 'package:kmobile/features/auth/controllers/theme_state.dart';
import 'config/routes.dart'; import 'config/routes.dart';
import 'di/injection.dart'; import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart'; import 'features/auth/controllers/auth_cubit.dart';
import 'features/accounts/screens/account_statement_screen.dart'; import 'features/card/screens/card_management_screen.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart'; import 'features/auth/screens/splash_screen.dart';
import 'features/auth/screens/login_screen.dart'; import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart'; import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart'; import 'features/dashboard/screens/dashboard_screen.dart';
import 'features/auth/screens/mpin_screen.dart'; import 'features/auth/screens/mpin_screen.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
class KMobile extends StatefulWidget { class KMobile extends StatefulWidget {
const KMobile({super.key}); const KMobile({super.key});
@@ -35,46 +32,25 @@ class KMobile extends StatefulWidget {
} }
} }
class _KMobileState extends State<KMobile> with WidgetsBindingObserver { class _KMobileState extends State<KMobile> {
Timer? _backgroundTimer; bool showSplash = true;
Locale? _locale; Locale? _locale;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
loadPreferences(); loadPreferences();
} Future.delayed(const Duration(seconds: 3), () {
setState(() {
@override showSplash = false;
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 { Future<void> loadPreferences() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
// Load Locale
final String? langCode = prefs.getString('locale'); final String? langCode = prefs.getString('locale');
if (langCode != null) { if (langCode != null) {
setState(() { setState(() {
@@ -102,12 +78,9 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
providers: [ providers: [
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()), BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
BlocProvider<ThemeCubit>(create: (_) => ThemeCubit()), BlocProvider<ThemeCubit>(create: (_) => ThemeCubit()),
BlocProvider<ThemeModeCubit>(create: (_) => ThemeModeCubit()),
], ],
child: BlocBuilder<ThemeCubit, ThemeState>( child: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, themeState) { builder: (context, themeState) {
return BlocBuilder<ThemeModeCubit, ThemeModeState>(
builder: (context, modeState) {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
locale: _locale ?? const Locale('en'), locale: _locale ?? const Locale('en'),
@@ -122,17 +95,12 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
title: 'kMobile', title: 'kMobile',
theme: themeState.getLightThemeData(), theme: themeState.getThemeData(),
darkTheme: themeState.getDarkThemeData(), //darkTheme: themeState.getThemeData(),
themeMode: context.watch<ThemeModeCubit>().state.mode, themeMode: ThemeMode.light,
navigatorObservers: [
getIt<RouteObserver<ModalRoute<void>>>(),
],
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash, initialRoute: AppRoutes.splash,
home: const AuthGate(), home: showSplash ? const SplashScreen() : const AuthGate(),
);
},
); );
}, },
), ),
@@ -142,6 +110,7 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
class AuthGate extends StatefulWidget { class AuthGate extends StatefulWidget {
const AuthGate({super.key}); const AuthGate({super.key});
@override @override
State<AuthGate> createState() => _AuthGateState(); State<AuthGate> createState() => _AuthGateState();
} }
@@ -202,8 +171,9 @@ class _AuthGateState extends State<AuthGate> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_checking) { if (_checking) {
return const LoginScreen(); return const SplashScreen();
} }
if (_isLoggedIn) { if (_isLoggedIn) {
if (_hasMPin) { if (_hasMPin) {
if (_biometricEnabled) { if (_biometricEnabled) {
@@ -211,11 +181,13 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(), future: _tryBiometric(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const LoginScreen(); return const SplashScreen();
} }
if (snapshot.data == true) { if (snapshot.data == true) {
return const NavigationScaffold(); return const NavigationScaffold(); // Authenticated
} }
return MPinScreen( return MPinScreen(
mode: MPinMode.enter, mode: MPinMode.enter,
onCompleted: (_) { onCompleted: (_) {
@@ -246,6 +218,7 @@ class _AuthGateState extends State<AuthGate> {
onCompleted: (_) async { onCompleted: (_) async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final optIn = await showDialog<bool>( final optIn = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -266,6 +239,7 @@ class _AuthGateState extends State<AuthGate> {
], ],
), ),
); );
if (optIn == true) { if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false; bool didAuth = false;
@@ -273,6 +247,7 @@ class _AuthGateState extends State<AuthGate> {
if (context.mounted) { if (context.mounted) {
authEnable = AppLocalizations.of(context).authenticateToEnable; authEnable = AppLocalizations.of(context).authenticateToEnable;
} }
if (canCheck) { if (canCheck) {
didAuth = await localAuth.authenticate( didAuth = await localAuth.authenticate(
localizedReason: authEnable, localizedReason: authEnable,
@@ -287,6 +262,7 @@ class _AuthGateState extends State<AuthGate> {
await storage.write('biometric_enabled', 'false'); await storage.write('biometric_enabled', 'false');
} }
} }
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
@@ -304,6 +280,7 @@ class _AuthGateState extends State<AuthGate> {
class NavigationScaffold extends StatefulWidget { class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key}); const NavigationScaffold({super.key});
@override @override
State<NavigationScaffold> createState() => _NavigationScaffoldState(); State<NavigationScaffold> createState() => _NavigationScaffoldState();
} }
@@ -311,23 +288,10 @@ class NavigationScaffold extends StatefulWidget {
class _NavigationScaffoldState extends State<NavigationScaffold> { class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController(); final PageController _pageController = PageController();
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardScreen(), const DashboardScreen(),
BlocBuilder<AuthCubit, AuthState>( const CardManagementScreen(),
builder: (context, state) {
if (state is Authenticated) {
if (state.users.isNotEmpty) {
return AccountStatementScreen(
users: state.users,
selectedIndex: 0,
);
} else {
return const Center(child: Text("No accounts found."));
}
}
return const Center(child: CircularProgressIndicator());
},
),
const ServiceScreen(), const ServiceScreen(),
]; ];
@@ -371,9 +335,10 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
backgroundColor: const Color(0XFF1E58AD), backgroundColor: Theme.of(context)
selectedItemColor: Theme.of(context).colorScheme.onPrimary, .scaffoldBackgroundColor, // Light blue background
unselectedItemColor: Theme.of(context).colorScheme.onSecondary, selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: Colors.black54,
onTap: (index) { onTap: (index) {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
@@ -386,8 +351,8 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
label: AppLocalizations.of(context).home, label: AppLocalizations.of(context).home,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.swap_vert_sharp), icon: const Icon(Icons.credit_card),
label: AppLocalizations.of(context).transactions, label: AppLocalizations.of(context).card,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services), icon: const Icon(Icons.miscellaneous_services),
@@ -400,9 +365,11 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
} }
} }
// Add this widget at the end of the file
class BiometricPromptScreen extends StatelessWidget { class BiometricPromptScreen extends StatelessWidget {
final VoidCallback onCompleted; final VoidCallback onCompleted;
const BiometricPromptScreen({super.key, required this.onCompleted}); const BiometricPromptScreen({super.key, required this.onCompleted});
Future<void> _handleBiometric(BuildContext context) async { Future<void> _handleBiometric(BuildContext context) async {
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
@@ -433,7 +400,7 @@ class BiometricPromptScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context)); Future.microtask(() => _showDialog(context));
return const SizedBox.shrink(); return const SplashScreen();
} }
Future<void> _showDialog(BuildContext context) async { Future<void> _showDialog(BuildContext context) async {

View File

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

View File

@@ -3,5 +3,4 @@ enum ThemeType {
green, green,
orange, orange,
blue, blue,
yellow,
} }

View File

@@ -1,72 +1,39 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'theme_type.dart'; import 'theme_type.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
class AppThemes { class AppThemes {
static ThemeData getLightTheme(ThemeType type) { static ThemeData getLightTheme(ThemeType type) {
final Color seedColor = _getSeedColor(type); final Color seedColor;
final colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
);
return ThemeData.from(
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) {
final Color seedColor = _getSeedColor(type);
log(seedColor.toString());
final colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
);
return ThemeData.from(
colorScheme: colorScheme,
useMaterial3: true,
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) {
switch (type) { switch (type) {
case ThemeType.green: case ThemeType.green:
return Colors.green; seedColor = Colors.green;
break;
case ThemeType.orange: case ThemeType.orange:
return Colors.orange; seedColor = Colors.orange;
break;
case ThemeType.blue: case ThemeType.blue:
return Colors.blue; seedColor = Colors.blue;
break;
case ThemeType.violet: case ThemeType.violet:
return Colors.deepPurple; seedColor = Colors.deepPurple;
case ThemeType.yellow: break;
return Colors.yellow; }
}
final colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light, // Explicitly set for a light theme
);
return ThemeData.from(
colorScheme: colorScheme,
useMaterial3: true, // Recommended for modern Flutter apps
textTheme: GoogleFonts.rubikTextTheme())
.copyWith(
scaffoldBackgroundColor: Colors.white,
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: colorScheme.surface,
),
);
} }
} }

View File

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

View File

@@ -6,7 +6,6 @@ class ImpsTransaction {
final String? remitterName; final String? remitterName;
final String beneficiaryName; final String beneficiaryName;
final String tpin; final String tpin;
final String? remarks;
ImpsTransaction({ ImpsTransaction({
required this.fromAccount, required this.fromAccount,
@@ -16,7 +15,6 @@ class ImpsTransaction {
this.remitterName, this.remitterName,
required this.beneficiaryName, required this.beneficiaryName,
required this.tpin, required this.tpin,
this.remarks,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -28,7 +26,6 @@ class ImpsTransaction {
'remitterName': remitterName, 'remitterName': remitterName,
'beneficiaryName': beneficiaryName, 'beneficiaryName': beneficiaryName,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -6,7 +6,6 @@ class NeftTransaction {
final String remitterName; final String remitterName;
final String beneficiaryName; final String beneficiaryName;
final String tpin; final String tpin;
final String? remarks;
NeftTransaction({ NeftTransaction({
required this.fromAccount, required this.fromAccount,
@@ -16,7 +15,6 @@ class NeftTransaction {
required this.remitterName, required this.remitterName,
required this.beneficiaryName, required this.beneficiaryName,
required this.tpin, required this.tpin,
this.remarks,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -28,7 +26,6 @@ class NeftTransaction {
'remitterName': remitterName, 'remitterName': remitterName,
'beneficiaryName': beneficiaryName, 'beneficiaryName': beneficiaryName,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -6,7 +6,6 @@ class RtgsTransaction {
final String remitterName; final String remitterName;
final String beneficiaryName; final String beneficiaryName;
final String tpin; final String tpin;
final String? remarks;
RtgsTransaction({ RtgsTransaction({
required this.fromAccount, required this.fromAccount,
@@ -16,7 +15,6 @@ class RtgsTransaction {
required this.remitterName, required this.remitterName,
required this.beneficiaryName, required this.beneficiaryName,
required this.tpin, required this.tpin,
this.remarks,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -28,7 +26,6 @@ class RtgsTransaction {
'remitterName': remitterName, 'remitterName': remitterName,
'beneficiaryName': beneficiaryName, 'beneficiaryName': beneficiaryName,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -4,18 +4,8 @@ class Transaction {
final String? date; final String? date;
final String? amount; final String? amount;
final String? type; final String? type;
final String? balance;
final String? balanceType;
Transaction(
{this.id,
this.name,
this.date,
this.amount,
this.type,
this.balance,
this.balanceType});
Transaction({this.id, this.name, this.date, this.amount, this.type});
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
@@ -23,8 +13,6 @@ class Transaction {
'date': date, 'date': date,
'amount': amount, 'amount': amount,
'type': type, 'type': type,
'balance': balance,
'balanceType': balanceType
}; };
} }
@@ -35,7 +23,6 @@ class Transaction {
date: json['date'] as String?, date: json['date'] as String?,
amount: json['amount'] as String?, amount: json['amount'] as String?,
type: json['type'] as String?, type: json['type'] as String?,
balance: json['balance'] as String?, );
balanceType: json['balanceType'] as String?);
} }
} }

View File

@@ -4,7 +4,6 @@ class Transfer {
final String toAccountType; final String toAccountType;
final String amount; final String amount;
String? tpin; String? tpin;
String? remarks;
Transfer({ Transfer({
required this.fromAccount, required this.fromAccount,
@@ -12,7 +11,6 @@ class Transfer {
required this.toAccountType, required this.toAccountType,
required this.amount, required this.amount,
this.tpin, this.tpin,
this.remarks,
}); });
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@@ -22,7 +20,6 @@ class Transfer {
'toAccountType': toAccountType, 'toAccountType': toAccountType,
'amount': amount, 'amount': amount,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

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

View File

@@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/models/transaction.dart';
@@ -23,6 +25,8 @@ class TransactionRepositoryImpl implements TransactionRepository {
queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate); queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate);
} }
log('query params below');
log(queryParameters.toString());
final resp = await _dio.get( final resp = await _dio.get(
'/api/transactions/account/$accountNo', '/api/transactions/account/$accountNo',
queryParameters: queryParameters.isNotEmpty ? queryParameters : null, queryParameters: queryParameters.isNotEmpty ? queryParameters : null,

View File

@@ -1,6 +1,3 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -11,10 +8,8 @@ import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/api/services/user_service.dart'; import 'package:kmobile/api/services/user_service.dart';
import 'package:kmobile/data/repositories/transaction_repository.dart'; import 'package:kmobile/data/repositories/transaction_repository.dart';
import 'package:kmobile/features/auth/controllers/theme_cubit.dart'; import 'package:kmobile/features/auth/controllers/theme_cubit.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
import '../api/services/auth_service.dart'; import '../api/services/auth_service.dart';
import '../api/interceptors/auth_interceptor.dart'; import '../api/interceptors/auth_interceptor.dart';
import '../api/services/change_password_service.dart';
import '../data/repositories/auth_repository.dart'; import '../data/repositories/auth_repository.dart';
import '../features/auth/controllers/auth_cubit.dart'; import '../features/auth/controllers/auth_cubit.dart';
import '../security/secure_storage.dart'; import '../security/secure_storage.dart';
@@ -22,12 +17,9 @@ import '../security/secure_storage.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
Future<void> setupDependencies() async { Future<void> setupDependencies() async {
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
RouteObserver<ModalRoute<void>>());
//getIt.registerLazySingleton<ThemeController>(() => ThemeController()); //getIt.registerLazySingleton<ThemeController>(() => ThemeController());
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController()); //getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
getIt.registerSingleton<ThemeCubit>(ThemeCubit()); getIt.registerSingleton<ThemeCubit>(ThemeCubit());
getIt.registerSingleton<ThemeModeCubit>(ThemeModeCubit());
// Register Dio client // Register Dio client
getIt.registerSingleton<Dio>(_createDioClient()); getIt.registerSingleton<Dio>(_createDioClient());
@@ -51,14 +43,9 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>())); getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>())); getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>())); getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>())); getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>())); getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
getIt.registerSingleton<BranchService>(BranchService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>(
() => ChangePasswordService(getIt<Dio>()),
);
// Add auth interceptor after repository is available // Add auth interceptor after repository is available
getIt<Dio>().interceptors.add( getIt<Dio>().interceptors.add(
@@ -66,23 +53,21 @@ Future<void> setupDependencies() async {
); );
// Register controllers/cubits // Register controllers/cubits
getIt.registerFactory<AuthCubit>(() => AuthCubit( getIt.registerFactory<AuthCubit>(
getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>())); () => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
} }
Dio _createDioClient() { Dio _createDioClient() {
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: baseUrl:
'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080',
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod //'http://localhost:8081',
//'https://kccbmbnk.net', //prod small connectTimeout: const Duration(seconds: 5),
connectTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 60),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'X-Login-Type': 'MB',
}, },
), ),
); );

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -14,9 +14,7 @@ class TransactionDetailsScreen extends StatelessWidget {
return Scaffold( return Scaffold(
appBar: appBar:
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)), AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
body: Stack( body: Padding(
children: [
Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
@@ -39,12 +37,8 @@ class TransactionDetailsScreen extends StatelessWidget {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Icon( Icon(
isCredit isCredit ? Symbols.call_received : Symbols.call_made,
? Symbols.call_received color: isCredit ? Colors.green : Colors.red,
: Symbols.call_made,
color: isCredit
? const Color(0xFF10BB10)
: Theme.of(context).colorScheme.error,
size: 28, size: 28,
), ),
], ],
@@ -53,9 +47,9 @@ class TransactionDetailsScreen extends StatelessWidget {
// Date centered // Date centered
Text( Text(
transaction.date ?? "", transaction.date ?? "",
style: TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
color: Theme.of(context).textTheme.bodySmall?.color, color: Colors.grey,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -63,47 +57,28 @@ class TransactionDetailsScreen extends StatelessWidget {
), ),
), ),
), ),
Divider(color: Theme.of(context).dividerColor), const Divider(),
Expanded( Expanded(
flex: 5, flex: 5,
child: ListView( child: ListView(
children: [ children: [
_buildDetailRow( _buildDetailRow(AppLocalizations.of(context).transactionType,
AppLocalizations.of(context).transactionType,
transaction.type ?? ""), transaction.type ?? ""),
_buildDetailRow(AppLocalizations.of(context).transferType, _buildDetailRow(AppLocalizations.of(context).transferType,
transaction.name.split("/").first ?? ""), transaction.name.split("/").first ?? ""),
// if (transaction.name.length > 12) ...[ if (transaction.name.length > 12) ...[
// _buildDetailRow(AppLocalizations.of(context).utrNo, _buildDetailRow(AppLocalizations.of(context).utrNo,
// transaction.name.split("= ")[1].split(" ")[0] ?? ""), transaction.name.split("= ")[1].split(" ")[0] ?? ""),
// _buildDetailRow( _buildDetailRow(
// AppLocalizations.of(context).beneficiaryAccountNo, AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "") 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
),
),
),
),
),
],
),
); );
} }

View File

@@ -1,19 +1,14 @@
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:kmobile/api/services/user_service.dart'; import 'package:kmobile/api/services/user_service.dart';
import 'package:kmobile/core/errors/exceptions.dart'; import 'package:kmobile/core/errors/exceptions.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/features/auth/models/auth_token.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../data/repositories/auth_repository.dart'; import '../../../data/repositories/auth_repository.dart';
import 'auth_state.dart'; import 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> { class AuthCubit extends Cubit<AuthState> {
final AuthRepository _authRepository; final AuthRepository _authRepository;
final UserService _userService; final UserService _userService;
final SecureStorage _secureStorage;
AuthCubit(this._authRepository, this._userService, this._secureStorage) AuthCubit(this._authRepository, this._userService) : super(AuthInitial()) {
: super(AuthInitial()) {
checkAuthStatus(); checkAuthStatus();
} }
@@ -34,62 +29,22 @@ class AuthCubit extends Cubit<AuthState> {
Future<void> refreshUserData() async { Future<void> refreshUserData() async {
try { try {
// emit(AuthLoading());
final users = await _userService.getUserDetails(); final users = await _userService.getUserDetails();
emit(Authenticated(users)); emit(Authenticated(users));
} catch (e) { } catch (e) {
emit(AuthError('Failed to refresh user data: ${e.toString()}')); emit(AuthError('Failed to refresh user data: ${e.toString()}'));
// Optionally, re-emit the previous state or handle as needed
} }
} }
Future<void> login(String customerNo, String password) async { Future<void> login(String customerNo, String password) async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
final (users, authToken) = final users = await _authRepository.login(customerNo, password);
await _authRepository.login(customerNo, password); emit(Authenticated(users));
if (authToken.tnc == false) {
emit(ShowTncDialog(authToken, users));
} else {
await _checkMpinAndNavigate(users);
}
} catch (e) { } catch (e) {
emit(AuthError(e is AuthException ? e.message : e.toString())); emit(AuthError(e is AuthException ? e.message : e.toString()));
} }
} }
Future<void> onTncDialogResult(
bool agreed, AuthToken authToken, List<User> users) async {
if (agreed) {
try {
await _authRepository.acceptTnc();
// The user is NOT fully authenticated yet. Just check for MPIN.
await _checkMpinAndNavigate(users);
} catch (e) {
emit(AuthError('Failed to accept TNC: $e'));
}
} else {
emit(NavigateToTncRequiredScreen());
}
}
void mpinSetupCompleted() {
if (state is NavigateToMpinSetupScreen) {
final users = (state as NavigateToMpinSetupScreen).users;
emit(Authenticated(users));
} else {
// Handle unexpected state if necessary
emit(AuthError("Invalid state during MPIN setup completion."));
}
}
Future<void> _checkMpinAndNavigate(List<User> users) async {
final mpin = await _secureStorage.read('mpin');
if (mpin == null) {
// No MPIN, tell UI to navigate to MPIN setup, carrying user data
emit(NavigateToMpinSetupScreen(users));
} else {
// MPIN exists, user is authenticated
emit(Authenticated(users));
}
}
} }

View File

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

View File

@@ -1,23 +0,0 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'theme_mode_state.dart';
class ThemeModeCubit extends Cubit<ThemeModeState> {
ThemeModeCubit() : super(const ThemeModeState(mode: ThemeMode.system)) {
loadThemeMode();
}
Future<void> loadThemeMode() async {
final prefs = await SharedPreferences.getInstance();
final modeIndex = prefs.getInt('theme_mode') ?? 0; // default system
final mode = ThemeMode.values[modeIndex];
emit(ThemeModeState(mode: mode));
}
Future<void> changeThemeMode(ThemeMode mode) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('theme_mode', mode.index);
emit(ThemeModeState(mode: mode));
}
}

View File

@@ -1,11 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
class ThemeModeState extends Equatable {
final ThemeMode mode;
const ThemeModeState({required this.mode});
@override
List<Object?> get props => [mode];
}

View File

@@ -1,4 +1,4 @@
/*import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/config/theme_type.dart'; import 'package:kmobile/config/theme_type.dart';
import 'package:kmobile/config/themes.dart'; import 'package:kmobile/config/themes.dart';
@@ -12,27 +12,6 @@ class ThemeState extends Equatable {
return AppThemes.getLightTheme(themeType); return AppThemes.getLightTheme(themeType);
} }
@override
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 @override
List<Object?> get props => [themeType]; List<Object?> get props => [themeType];
} }

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'dart:math';
// import 'dart:developer'; import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart';
enum MPinMode { enter, set, confirm } enum MPinMode { enter, set, confirm }
@@ -15,91 +14,34 @@ class MPinScreen extends StatefulWidget {
final MPinMode mode; final MPinMode mode;
final String? initialPin; final String? initialPin;
final void Function(String pin)? onCompleted; final void Function(String pin)? onCompleted;
final bool disableBiometric;
final String? customTitle;
final String? customConfirmTitle;
const MPinScreen({ const MPinScreen({
super.key, super.key,
required this.mode, required this.mode,
this.initialPin, this.initialPin,
this.onCompleted, this.onCompleted,
this.disableBiometric = false,
this.customTitle,
this.customConfirmTitle,
}); });
@override @override
State<MPinScreen> createState() => _MPinScreenState(); State<MPinScreen> createState() => _MPinScreenState();
} }
class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin { class _MPinScreenState extends State<MPinScreen> {
List<String> mPin = []; List<String> mPin = [];
String? errorText; String? errorText;
// Animation controllers
late final AnimationController _bounceController;
late final Animation<double> _bounceAnimation;
late final AnimationController _shakeController;
late final AnimationController _waveController;
late final Animation<double> _waveAnimation;
// State flags for animations
bool _isError = false;
bool _isSuccess = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Bounce animation for single dot entry if (widget.mode == MPinMode.enter) {
_bounceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_bounceAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(
parent: _bounceController,
curve: Curves.elasticIn,
reverseCurve: Curves.elasticOut,
),
)..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_bounceController.reverse();
}
});
// Shake animation for error
_shakeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
// Wave animation for success
_waveController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_waveAnimation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
);
if (widget.mode == MPinMode.enter && !widget.disableBiometric) {
_tryBiometricBeforePin(); _tryBiometricBeforePin();
} }
} }
@override
void dispose() {
_bounceController.dispose();
_shakeController.dispose();
_waveController.dispose();
super.dispose();
}
Future<void> _tryBiometricBeforePin() async { Future<void> _tryBiometricBeforePin() async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final enabled = await storage.read('biometric_enabled'); final enabled = await storage.read('biometric_enabled');
// log('biometric_enabled: $enabled'); log('biometric_enabled: $enabled');
if (enabled != null && enabled) { if (enabled != null && enabled) {
final auth = LocalAuthentication(); final auth = LocalAuthentication();
if (await auth.canCheckBiometrics) { if (await auth.canCheckBiometrics) {
@@ -120,13 +62,11 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
void addDigit(String digit) { void addDigit(String digit) {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.length < 4) { if (mPin.length < 4) {
setState(() { setState(() {
mPin.add(digit); mPin.add(digit);
errorText = null; errorText = null;
}); });
_bounceController.forward(from: 0);
if (mPin.length == 4) { if (mPin.length == 4) {
_handleComplete(); _handleComplete();
} }
@@ -134,7 +74,6 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
void deleteDigit() { void deleteDigit() {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.isNotEmpty) { if (mPin.isNotEmpty) {
setState(() { setState(() {
mPin.removeLast(); mPin.removeLast();
@@ -144,148 +83,71 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
Future<void> _handleComplete() async { Future<void> _handleComplete() async {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
final pin = mPin.join(); final pin = mPin.join();
final storage = SecureStorage(); final storage = SecureStorage();
switch (widget.mode) { switch (widget.mode) {
case MPinMode.enter: case MPinMode.enter:
final storedPin = await storage.read('mpin'); final storedPin = await storage.read('mpin');
// log('storedPin: $storedPin'); log('storedPin: $storedPin');
if (storedPin == int.tryParse(pin)) { if (storedPin == int.tryParse(pin)) {
// Correct PIN
setState(() {
_isSuccess = true;
errorText = null;
});
await Future.delayed(const Duration(milliseconds: 100));
_waveController.forward(from: 0).whenComplete(() {
widget.onCompleted?.call(pin); widget.onCompleted?.call(pin);
});
} else { } else {
// Incorrect PIN
setState(() { setState(() {
_isError = true;
errorText = AppLocalizations.of(context).incorrectMPIN; errorText = AppLocalizations.of(context).incorrectMPIN;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear(); mPin.clear();
_isError = false;
// Keep error text until next digit is entered
}); });
} }
break; break;
case MPinMode.set: case MPinMode.set:
// Navigate to confirm and wait for result // propagate parent onCompleted into confirm step
final result = await Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MPinScreen( builder: (_) => MPinScreen(
mode: MPinMode.confirm, mode: MPinMode.confirm,
initialPin: pin, initialPin: pin,
onCompleted: (confirmedPin) { onCompleted: widget.onCompleted, // <-- use parent callback
// Just pop with the pin, don't call parent callback yet
Navigator.of(context).pop(confirmedPin);
},
disableBiometric: widget.disableBiometric,
customTitle: widget.customConfirmTitle,
), ),
), ),
); );
// If confirm succeeded, call parent callback
if (result != null && mounted) {
widget.onCompleted?.call(result);
}
break; break;
case MPinMode.confirm: case MPinMode.confirm:
if (widget.initialPin == pin) { if (widget.initialPin == pin) {
// 1) persist the pin // 1) persist the pin
await storage.write('mpin', pin); await storage.write('mpin', pin);
// 2) Call the onCompleted callback to let the parent handle navigation // 3) now clear the entire navigation stack and go to your main scaffold
if (mounted) { if (mounted) {
widget.onCompleted?.call(pin); Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
} }
} else { } else {
setState(() { setState(() {
_isError = true;
errorText = AppLocalizations.of(context).pinsDoNotMatch; errorText = AppLocalizations.of(context).pinsDoNotMatch;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear(); mPin.clear();
_isError = false;
}); });
} }
break; break;
} }
} }
Widget buildMPinDots(BuildContext context) { Widget buildMPinDots() {
return AnimatedBuilder( return Row(
animation: Listenable.merge(
[_bounceController, _shakeController, _waveController]),
builder: (context, child) {
double shakeOffset = 0;
if (_shakeController.isAnimating) {
// 4 cycles of sine wave for shake
shakeOffset = sin(_shakeController.value * 4 * pi) * 12;
}
return Transform.translate(
offset: Offset(shakeOffset, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) { children: List.generate(4, (index) {
final isFilled = index < mPin.length; return Container(
// Bounce animation for single dot
final isAnimatingBounce =
index == mPin.length - 1 && _bounceController.isAnimating;
double bounceScale =
isAnimatingBounce ? _bounceAnimation.value : 1.0;
// Success wave animation
double waveScale = 1.0;
if (_isSuccess) {
final waveDelay = index * 0.15;
final waveValue =
(_waveAnimation.value - waveDelay).clamp(0.0, 1.0);
// Grow and shrink
waveScale = 1.0 + sin(waveValue * pi) * 0.4;
}
// Determine dot color
Color dotColor;
if (_isError) {
dotColor = Theme.of(context).colorScheme.error;
} else if (_isSuccess) {
dotColor = Colors.green.shade600;
} else {
dotColor = isFilled
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context).colorScheme.surfaceContainerHighest;
}
return Transform.scale(
scale: bounceScale * waveScale,
child: Container(
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
width: 25, width: 15,
height: 25, height: 15,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: dotColor, color: index < mPin.length ? Colors.black : Colors.grey[400],
),
), ),
); );
}), }),
),
);
},
); );
} }
@@ -303,10 +165,9 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) { children: row.map((key) {
return Padding( return Padding(
padding: const EdgeInsets.all(1.0), padding: const EdgeInsets.all(8.0),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
HapticFeedback.lightImpact();
if (key == '<') { if (key == '<') {
deleteDigit(); deleteDigit();
} else if (key == 'Enter') { } else if (key == 'Enter') {
@@ -324,10 +185,11 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
}, },
child: Container( child: Container(
width: 80, width: 70,
height: 80, height: 70,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.grey[200],
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: key == 'Enter' child: key == 'Enter'
@@ -335,10 +197,10 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
: Text( : Text(
key == '<' ? '' : key, key == '<' ? '' : key,
style: TextStyle( style: TextStyle(
fontSize: 30, fontSize: 20,
color: key == 'Enter' color: key == 'Enter'
? Theme.of(context).primaryColor ? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.onSurface, : Colors.black,
), ),
), ),
), ),
@@ -351,9 +213,6 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
} }
String getTitle() { String getTitle() {
if (widget.customTitle != null) {
return widget.customTitle!;
}
switch (widget.mode) { switch (widget.mode) {
case MPinMode.enter: case MPinMode.enter:
return AppLocalizations.of(context).enterMPIN; return AppLocalizations.of(context).enterMPIN;
@@ -370,7 +229,7 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 50), const Spacer(),
// Logo // Logo
Image.asset('assets/images/logo.png', height: 100), Image.asset('assets/images/logo.png', height: 100),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -378,18 +237,19 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
getTitle(), getTitle(),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
), ),
const Spacer(), const SizedBox(height: 20),
buildMPinDots(context), buildMPinDots(),
if (errorText != null) if (errorText != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: Text(
errorText!, errorText!,
style: TextStyle(color: Theme.of(context).colorScheme.error), style: const TextStyle(color: Colors.red),
), ),
), ),
const Spacer(), const Spacer(),
buildNumberPad(), buildNumberPad(),
const Spacer(),
], ],
), ),
), ),

View File

@@ -1,296 +0,0 @@
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/login_screen.dart';
import '../../../l10n/app_localizations.dart';
class SetPasswordScreen extends StatefulWidget {
final String customerNo;
const SetPasswordScreen({super.key, required this.customerNo});
@override
State<SetPasswordScreen> createState() => _SetPasswordScreenState();
}
enum SetPasswordStep { initial, otp, setPassword, success }
class _SetPasswordScreenState extends State<SetPasswordScreen> {
final _authService = getIt<AuthService>();
SetPasswordStep _currentStep = SetPasswordStep.initial;
bool _isLoading = false;
String? _error;
String? _otpToken;
final _otpController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
@override
void dispose() {
_otpController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
Future<void> _sendOtp() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
await _authService.sendOtpForSettingPassword(widget.customerNo);
setState(() {
_currentStep = SetPasswordStep.otp;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _verifyOtp() async {
if (_otpController.text.isEmpty) {
setState(() {
_error = 'Please enter OTP';
});
return;
}
setState(() {
_isLoading = true;
_error = null;
});
try {
final token = await _authService.verifyOtpForSettingPassword(
widget.customerNo, _otpController.text);
setState(() {
_otpToken = token;
_currentStep = SetPasswordStep.setPassword;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = AppLocalizations.of(context).invalidOtp;
_isLoading = false;
});
}
}
Future<void> _setPassword() async {
if (_passwordController.text.isEmpty ||
_confirmPasswordController.text.isEmpty) {
setState(() {
_error = 'Please fill both password fields';
});
return;
}
if (_passwordController.text != _confirmPasswordController.text) {
setState(() {
_error = 'Passwords do not match';
});
return;
}
if (_otpToken == null) {
setState(() {
_error = 'OTP token is missing. Please restart the process.';
_currentStep = SetPasswordStep.initial;
});
return;
}
setState(() {
_isLoading = true;
_error = null;
});
try {
await _authService.changePassword(_passwordController.text, _otpToken!);
setState(() {
_currentStep = SetPasswordStep.success;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Set New Password'),
),
body: Padding(
padding: const EdgeInsets.all(24.0),
child: Center(
child: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_error != null) ...[
Text(
_error!,
style: const TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
fontSize: 20),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
if (_isLoading)
const CircularProgressIndicator()
else
_buildStepContent(),
],
),
),
),
),
);
}
Widget _buildStepContent() {
switch (_currentStep) {
case SetPasswordStep.initial:
return _buildInitialStep();
case SetPasswordStep.otp:
return _buildOtpStep();
case SetPasswordStep.setPassword:
return _buildSetPasswordStep();
case SetPasswordStep.success:
return _buildSuccessStep();
}
}
Widget _buildInitialStep() {
return Column(
children: [
const Text(
'You need to set a new password to continue. We will send an OTP to your registered mobile number.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _sendOtp,
child: const Text('Proceed'),
),
],
);
}
Widget _buildOtpStep() {
return Column(
children: [
const Text(
'An OTP has been sent to your registered mobile number. Please enter it below.',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextField(
controller: _otpController,
decoration: const InputDecoration(
labelText: 'Enter OTP',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _verifyOtp,
child: const Text('Verify OTP'),
),
],
);
}
Widget _buildSetPasswordStep() {
return Column(
children: [
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
decoration: InputDecoration(
labelText: 'New Password',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility : Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
),
const SizedBox(height: 24),
TextFormField(
controller: _confirmPasswordController,
obscureText: _obscureConfirmPassword,
decoration: InputDecoration(
labelText: 'Confirm New Password',
border: const OutlineInputBorder(),
suffixIcon: IconButton(
icon: Icon(
_obscureConfirmPassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscureConfirmPassword = !_obscureConfirmPassword;
});
},
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _setPassword,
child: const Text('Set Password'),
),
],
);
}
Widget _buildSuccessStep() {
return Column(
children: [
const Icon(Icons.check_circle, color: Colors.green, size: 80),
const SizedBox(height: 24),
const Text(
'Password set successfully!',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'You can now log in with your new password.',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const LoginScreen()),
(route) => false,
);
},
child: const Text('Back to Login'),
),
],
);
}
}

View File

@@ -1,177 +0,0 @@
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,5 +1,3 @@
import 'package:package_info_plus/package_info_plus.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -12,29 +10,11 @@ class SplashScreen extends StatefulWidget {
} }
class _SplashScreenState extends State<SplashScreen> { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Stack( body: Stack(
fit: StackFit.expand, children: [
children: <Widget>[
Positioned.fill( Positioned.fill(
child: Image.asset( child: Image.asset(
'assets/images/kconnect2.webp', 'assets/images/kconnect2.webp',
@@ -46,7 +26,7 @@ class _SplashScreenState extends State<SplashScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
AppLocalizations.of(context).kccbMobile, AppLocalizations.of(context).kconnect,
style: const TextStyle( style: const TextStyle(
fontSize: 36, fontSize: 36,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -66,25 +46,13 @@ class _SplashScreenState extends State<SplashScreen> {
], ],
), ),
), ),
const Positioned( Positioned(
bottom: 40, bottom: 40,
left: 0, left: 0,
right: 0, right: 0,
child: Center( child: Center(
child: CircularProgressIndicator(color: Color(0xFFFFFFFF)), child: CircularProgressIndicator(
), color: Theme.of(context).scaffoldBackgroundColor),
),
Positioned(
bottom: 90,
left: 0,
right: 0,
child: Text(
_version,
textAlign: TextAlign.center,
style: const TextStyle(
color: Color(0xFFFFFFFF),
fontSize: 14,
),
), ),
), ),
], ],

View File

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

View File

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

@@ -1,10 +1,11 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/api/services/beneficiary_service.dart'; import 'package:kmobile/api/services/beneficiary_service.dart';
import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/data/models/beneficiary.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'beneficiary_result_page.dart'; import 'beneficiary_result_page.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../di/injection.dart'; import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
@@ -23,9 +24,7 @@ class AddBeneficiaryScreen extends StatefulWidget {
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> { class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _accountNumberFieldKey = GlobalKey<FormFieldState>();
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
final _ifscFieldKey = GlobalKey<FormFieldState>();
final TextEditingController accountNumberController = TextEditingController(); final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController = final TextEditingController confirmAccountNumberController =
TextEditingController(); TextEditingController();
@@ -34,7 +33,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final TextEditingController branchNameController = TextEditingController(); final TextEditingController branchNameController = TextEditingController();
final TextEditingController ifscController = TextEditingController(); final TextEditingController ifscController = TextEditingController();
final TextEditingController phoneController = TextEditingController(); final TextEditingController phoneController = TextEditingController();
final _ifscFocusNode = FocusNode();
final service = getIt<BeneficiaryService>(); final service = getIt<BeneficiaryService>();
bool _isValidating = false; bool _isValidating = false;
@@ -46,11 +44,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
accountType = 'Savings'; accountType = 'Savings';
@@ -58,29 +51,15 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
}); });
} }
@override
void dispose() {
accountNumberController.dispose();
confirmAccountNumberController.dispose();
nameController.dispose();
bankNameController.dispose();
branchNameController.dispose();
ifscController.dispose();
phoneController.dispose();
_ifscFocusNode.dispose();
super.dispose();
}
void _validateIFSC() async { void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>(); var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase(); final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return; if (ifsc.isEmpty) return;
try {
final result = await beneficiaryService.validateIFSC(ifsc); final result = await beneficiaryService.validateIFSC(ifsc);
if (mounted) { if (mounted) {
if (result.bankName.isEmpty) { if (result.bankName == '') {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)), SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
); );
@@ -91,23 +70,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
branchNameController.text = result.branchName; branchNameController.text = result.branchName;
} }
} }
} catch (e) {
if (mounted) {
final errorMessage = e.toString().toUpperCase();
String snackbarMessage =
AppLocalizations.of(context).somethingWentWrong;
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
snackbarMessage = AppLocalizations.of(context).invalidIfsc;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(snackbarMessage)),
);
bankNameController.clear();
branchNameController.clear();
}
}
} }
void _validateBeneficiary() async { void _validateBeneficiary() async {
@@ -258,15 +220,36 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text( title: Text(
AppLocalizations.of(context).addBeneficiary, AppLocalizations.of(context).addBeneficiary,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
],
), ),
body: SafeArea( body: SafeArea(
child: Stack( child: Form(
children: [
Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
children: [ children: [
@@ -278,7 +261,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
child: Column( child: Column(
children: [ children: [
TextFormField( TextFormField(
key: _accountNumberFieldKey,
controller: accountNumberController, controller: accountNumberController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of( labelText: AppLocalizations.of(
@@ -287,6 +269,18 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
// prefixIcon: Icon(Icons.person), // prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, 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,
),
),
), ),
obscureText: true, obscureText: true,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -309,7 +303,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
// Confirm Account Number // Confirm Account Number
TextFormField( TextFormField(
key: _confirmAccountNumberFieldKey,
controller: confirmAccountNumberController, controller: confirmAccountNumberController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of( labelText: AppLocalizations.of(
@@ -318,6 +311,18 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
// prefixIcon: Icon(Icons.person), // prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, 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, keyboardType: TextInputType.number,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
@@ -336,35 +341,45 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
}, },
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// 🔹 IFSC Code Field
TextFormField( TextFormField(
focusNode: _ifscFocusNode,
key: _ifscFieldKey,
controller: ifscController, controller: ifscController,
maxLength: 11, maxLength: 11,
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(11), LengthLimitingTextInputFormatter(11),
], ],
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText: AppLocalizations.of(context).ifscCode,
AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, 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,
),
),
), ),
textCapitalization: TextCapitalization.characters, textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_validateIFSC();
},
onChanged: (value) { onChanged: (value) {
setState(() {
final trimmed = value.trim().toUpperCase(); final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) { if (trimmed.length < 11) {
// clear bank/branch if backspace or changed // clear bank/branch if backspace or changed
bankNameController.clear(); bankNameController.clear();
branchNameController.clear(); branchNameController.clear();
} }
});
}, },
validator: (value) { validator: (value) {
final pattern = final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
return AppLocalizations.of(context).enterIfsc; return AppLocalizations.of(context).enterIfsc;
} else if (!pattern.hasMatch( } else if (!pattern.hasMatch(
@@ -378,61 +393,49 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
}, },
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// Bank Name (Disabled) // 🔹 Bank Name (Disabled)
TextFormField( TextFormField(
controller: bankNameController, controller: bankNameController,
enabled: enabled: false, // changed from readOnly to disabled
false, // changed from readOnly to disabled
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText: AppLocalizations.of(context).bankName,
AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true,
fillColor: Theme.of(context)
.dialogBackgroundColor, // disabled color
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black,
width: 2,
),
),
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// 🔹 Branch Name (Disabled) // 🔹 Branch Name (Disabled)
TextFormField( TextFormField(
controller: branchNameController, controller: branchNameController,
enabled: enabled: false, // changed from readOnly to disabled
false, // changed from readOnly to disabled
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText: AppLocalizations.of(context).branchName,
AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black,
width: 2,
), ),
), ),
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), const SizedBox(height: 24),
if (!_isBeneficiaryValidated) if (!_isBeneficiaryValidated)
@@ -441,26 +444,18 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isValidating || onPressed: _isValidating
ifscController.text.length != 11
? null ? null
: () { : () {
final isAccountValid = if (confirmAccountNumberController
_accountNumberFieldKey .text ==
.currentState! accountNumberController.text) {
.validate();
final isConfirmAccountValid =
_confirmAccountNumberFieldKey
.currentState!
.validate();
final isIfscValid = _ifscFieldKey
.currentState!
.validate();
if (isAccountValid &&
isConfirmAccountValid &&
isIfscValid) {
_validateBeneficiary(); _validateBeneficiary();
} else {
setState(() {
_validationError =
'Please enter a valid and matching account number.';
});
} }
}, },
child: _isValidating child: _isValidating
@@ -476,14 +471,49 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
), ),
), ),
//Beneficiary Name (Disabled) //Beneficiary Name (Disabled)
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
labelText:
AppLocalizations.of(context).beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide:
BorderSide(color: Colors.black, width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null || value.isEmpty
? AppLocalizations.of(context).nameRequired
: null,
),
const SizedBox(height: 24),
// 🔹 Account Type Dropdown // 🔹 Account Type Dropdown
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
value: accountType, value: accountType,
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText: AppLocalizations.of(context).accountType,
AppLocalizations.of(context).accountType,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, 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,
),
),
), ),
items: [ items: [
'Savings', 'Savings',
@@ -512,10 +542,22 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
prefixIcon: const Icon(Icons.phone), prefixIcon: const Icon(Icons.phone),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, 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, textInputAction: TextInputAction.done,
validator: (value) => value == null || validator: (value) =>
value.length != 10 value == null || value.length != 10
? AppLocalizations.of(context).enterValidPhone ? AppLocalizations.of(context).enterValidPhone
: null, : null,
), ),
@@ -526,7 +568,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.all(16.0),
child: SizedBox( child: SizedBox(
width: 250, width: 250,
child: ElevatedButton( child: ElevatedButton(
@@ -534,37 +576,14 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: const StadiumBorder(), shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer),
child: Text(
AppLocalizations.of(context).validateAndAdd,
style: const TextStyle(fontSize: 16),
), ),
child: Text(AppLocalizations.of(context).validateAndAdd),
), ),
), ),
), ),
], ],
), ),
), ),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
), ),
); );
} }

View File

@@ -22,9 +22,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
_showSuccessDialog(context); _showSuccessDialog(context);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text('Failed to delete beneficiary: $e')),
content: Text(
'${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
); );
} }
} }
@@ -34,12 +32,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).success), title: const Text('Success'),
content: content: const Text('Beneficiary deleted successfully.'),
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(AppLocalizations.of(context).ok), child: const Text('OK'),
onPressed: () { onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
}, },
@@ -55,19 +52,20 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).deleteBeneficiary), title: const Text('Delete Beneficiary'),
content: Text(AppLocalizations.of(context) content:
.areYouSureYouWantToDeleteThisBeneficiary), const Text('Are you sure you want to delete this beneficiary?'),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(AppLocalizations.of(context).cancel), child: const Text('Cancel'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),
TextButton( TextButton(
child: Text(AppLocalizations.of(context).delete), child: const Text('Delete'),
onPressed: () { onPressed: () {
//Navigator.of(context).pop();
_deleteBeneficiary(context); _deleteBeneficiary(context);
}, },
), ),
@@ -83,10 +81,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiarydetails), title: Text(AppLocalizations.of(context).beneficiarydetails),
), ),
body: SafeArea( body: Padding(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -96,7 +91,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
CircleAvatar( CircleAvatar(
radius: 24, radius: 24,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context), child: getBankLogo(beneficiary.bankName),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text( Text(
@@ -109,11 +104,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
_buildDetailRow('${AppLocalizations.of(context).bankName} ', _buildDetailRow('${AppLocalizations.of(context).bankName} ',
beneficiary.bankName ?? 'N/A'), beneficiary.bankName ?? 'N/A'),
_buildDetailRow( _buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
'${AppLocalizations.of(context).accountNumber} ',
beneficiary.accountNo), beneficiary.accountNo),
_buildDetailRow( _buildDetailRow('${AppLocalizations.of(context).accountType} ',
'${AppLocalizations.of(context).accountType} ',
beneficiary.accountType), beneficiary.accountType),
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ', _buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
beneficiary.ifscCode), beneficiary.ifscCode),
@@ -143,23 +136,6 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
], ],
), ),
), ),
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,10 +90,7 @@ class _BeneficiaryResultPageState extends State<BeneficiaryResultPage> {
), ),
child: Text( child: Text(
AppLocalizations.of(context).done, AppLocalizations.of(context).done,
style: TextStyle( style: const TextStyle(fontSize: 18), // slightly bigger text
color: Theme.of(context)
.colorScheme
.onPrimaryContainer), // slightly bigger text
), ),
), ),
), ),

View File

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

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
class BlockCardScreen extends StatefulWidget { class BlockCardScreen extends StatefulWidget {
const BlockCardScreen({super.key}); const BlockCardScreen({super.key});
@@ -54,14 +56,35 @@ class _BlockCardScreen extends State<BlockCardScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text( title: Text(
AppLocalizations.of(context).blockCard, AppLocalizations.of(context).blockCard,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), actions: [
body: Stack(
children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
@@ -100,21 +123,18 @@ class _BlockCardScreen extends State<BlockCardScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: fillColor: Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: borderSide: BorderSide(color: Colors.black, width: 2),
BorderSide(color: Colors.black, width: 2),
), ),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
obscureText: true, obscureText: true,
validator: (value) => validator: (value) => value != null && value.length == 3
value != null && value.length == 3
? null ? null
: AppLocalizations.of(context).cvv3Digits, : AppLocalizations.of(context).cvv3Digits,
), ),
@@ -131,18 +151,15 @@ class _BlockCardScreen extends State<BlockCardScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: fillColor: Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: borderSide: BorderSide(color: Colors.black, width: 2),
BorderSide(color: Colors.black, width: 2),
), ),
), ),
validator: (value) => value != null && validator: (value) => value != null && value.isNotEmpty
value.isNotEmpty
? null ? null
: AppLocalizations.of(context).selectExpiryDate, : AppLocalizations.of(context).selectExpiryDate,
), ),
@@ -194,22 +211,6 @@ class _BlockCardScreen extends State<BlockCardScreen> {
), ),
), ),
), ),
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,9 +9,7 @@ class CardDetailsScreen extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: const Text("My Cards"), title: const Text("My Cards"),
), ),
body: Stack( body: Padding(
children: [
Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: ListView( child: ListView(
children: const [ children: const [
@@ -33,8 +31,6 @@ class CardDetailsScreen extends StatelessWidget {
], ],
), ),
), ),
],
),
); );
} }
} }
@@ -91,7 +87,7 @@ class CardTile extends StatelessWidget {
"Kangra Central Co-operative Bank", "Kangra Central Co-operative Bank",
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 15.5, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@@ -1,6 +1,5 @@
// ignore_for_file: unused_import
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/features/card/screens/block_card_screen.dart'; import 'package:kmobile/features/card/screens/block_card_screen.dart';
import 'package:kmobile/features/card/screens/card_details_screen.dart'; import 'package:kmobile/features/card/screens/card_details_screen.dart';
import 'package:kmobile/features/card/screens/card_pin_change_details_screen.dart'; import 'package:kmobile/features/card/screens/card_pin_change_details_screen.dart';
@@ -22,20 +21,34 @@ class _CardManagementScreen extends State<CardManagementScreen> {
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Text( title: Text(
AppLocalizations.of(context).cardManagement, AppLocalizations.of(context).cardManagement,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
), ),
body: Stack( ),
children: [ ),
ListView( ],
),
body: ListView(
children: [ children: [
CardManagementTile( CardManagementTile(
icon: Symbols.add, icon: Symbols.add,
label: AppLocalizations.of(context).applyDebitCard, label: AppLocalizations.of(context).applyDebitCard,
onTap: () {}, onTap: () {},
disabled: true, // Add this
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
CardManagementTile( CardManagementTile(
icon: Symbols.remove_moderator, icon: Symbols.remove_moderator,
label: AppLocalizations.of(context).blockUnblockCard, label: AppLocalizations.of(context).blockUnblockCard,
@@ -47,23 +60,21 @@ class _CardManagementScreen extends State<CardManagementScreen> {
), ),
); );
}, },
disabled: true,
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
CardManagementTile( CardManagementTile(
icon: Symbols.password_2, icon: Symbols.password_2,
label: AppLocalizations.of(context).changeCardPin, label: AppLocalizations.of(context).changeCardPin,
onTap: () { onTap: () {
Navigator.push( // Navigator.push(
context, // context,
MaterialPageRoute( // MaterialPageRoute(
builder: (context) => const CardPinChangeDetailsScreen(), // builder: (context) => const CardPinChangeDetailsScreen(),
), // ),
); // );
}, },
disabled: true,
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
CardManagementTile( CardManagementTile(
icon: Symbols.payment_card, icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils, label: AppLocalizations.of(context).viewCardDeatils,
@@ -75,65 +86,34 @@ class _CardManagementScreen extends State<CardManagementScreen> {
), ),
); );
}, },
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
),
),
),
),
), ),
const Divider(height: 1),
], ],
), ),
); );
} }
} }
class CardManagementTile extends StatelessWidget { class CardManagementTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final VoidCallback onTap; final VoidCallback onTap;
final bool disabled;
const CardManagementTile({ const CardManagementTile({
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
required this.onTap, required this.onTap,
this.disabled = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListTile( return ListTile(
leading: Icon( leading: Icon(icon),
icon, title: Text(label),
color: disabled ? theme.disabledColor : null, trailing: const Icon(Symbols.arrow_right, size: 20),
), onTap: onTap,
title: Text(
label,
style: TextStyle(
color: disabled ? theme.disabledColor : null,
),
),
trailing: Icon(
Symbols.arrow_right,
size: 20,
color: disabled ? theme.disabledColor : null,
),
onTap: disabled ? null : onTap,
); );
} }
} }

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:kmobile/features/card/screens/card_pin_set_screen.dart'; import 'package:kmobile/features/card/screens/card_pin_set_screen.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class CardPinChangeDetailsScreen extends StatefulWidget { class CardPinChangeDetailsScreen extends StatefulWidget {
@@ -44,14 +46,35 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text( title: Text(
AppLocalizations.of(context).cardDetails, AppLocalizations.of(context).cardDetails,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), actions: [
body: Stack(
children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
@@ -90,21 +113,18 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: fillColor: Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: borderSide: BorderSide(color: Colors.black, width: 2),
BorderSide(color: Colors.black, width: 2),
), ),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
obscureText: true, obscureText: true,
validator: (value) => validator: (value) => value != null && value.length == 3
value != null && value.length == 3
? null ? null
: AppLocalizations.of(context).cvv3Digits, : AppLocalizations.of(context).cvv3Digits,
), ),
@@ -121,18 +141,15 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: fillColor: Theme.of(context).scaffoldBackgroundColor,
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Colors.black),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: const OutlineInputBorder(
borderSide: borderSide: BorderSide(color: Colors.black, width: 2),
BorderSide(color: Colors.black, width: 2),
), ),
), ),
validator: (value) => value != null && validator: (value) => value != null && value.isNotEmpty
value.isNotEmpty
? null ? null
: AppLocalizations.of(context).selectExpiryDate, : AppLocalizations.of(context).selectExpiryDate,
), ),
@@ -184,22 +201,6 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
), ),
), ),
), ),
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,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class CardPinSetScreen extends StatefulWidget { class CardPinSetScreen extends StatefulWidget {
@@ -44,14 +46,35 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text( title: Text(
AppLocalizations.of(context).cardPin, AppLocalizations.of(context).cardPin,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), actions: [
body: Stack(
children: [
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
@@ -133,22 +156,6 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
), ),
), ),
), ),
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,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart'; import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -15,14 +16,35 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text( title: Text(
AppLocalizations.of(context).chequeManagement, AppLocalizations.of(context).chequeManagement,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
), ),
body: Stack( ),
children: [ ),
ListView( ],
),
body: ListView(
children: [ children: [
const SizedBox(height: 15), const SizedBox(height: 15),
ChequeManagementTile( ChequeManagementTile(
@@ -30,59 +52,42 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
label: AppLocalizations.of(context).requestChequeBook, label: AppLocalizations.of(context).requestChequeBook,
onTap: () {}, onTap: () {},
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.data_alert, icon: Symbols.data_alert,
label: AppLocalizations.of(context).enquiry, label: AppLocalizations.of(context).enquiry,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(builder: (context) => const EnquiryScreen()),
builder: (context) => const EnquiryScreen()),
); );
}, },
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.approval_delegation, icon: Symbols.approval_delegation,
label: AppLocalizations.of(context).chequeDeposit, label: AppLocalizations.of(context).chequeDeposit,
onTap: () {}, onTap: () {},
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.front_hand, icon: Symbols.front_hand,
label: AppLocalizations.of(context).stopCheque, label: AppLocalizations.of(context).stopCheque,
onTap: () {}, onTap: () {},
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.cancel_presentation, icon: Symbols.cancel_presentation,
label: AppLocalizations.of(context).revokeStop, label: AppLocalizations.of(context).revokeStop,
onTap: () {}, onTap: () {},
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.payments, icon: Symbols.payments,
label: AppLocalizations.of(context).positivePay, label: AppLocalizations.of(context).positivePay,
onTap: () {}, onTap: () {},
), ),
Divider(height: 1, color: Theme.of(context).dividerColor), const Divider(height: 1),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
], ],
), ),
); );

View File

@@ -1,7 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class CustomerInfoScreen extends StatefulWidget { class CustomerInfoScreen extends StatefulWidget {
@@ -14,181 +13,54 @@ class CustomerInfoScreen extends StatefulWidget {
class _CustomerInfoScreenState extends State<CustomerInfoScreen> { class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
late final User user = widget.user; late final User user = widget.user;
int _selectedCard = 0; // 0 for Personal Info, 1 for KYC
String _maskPrimaryId(String? primaryId) {
if (primaryId == null || primaryId.length <= 4) {
return primaryId ?? 'N/A';
}
final lastFour = primaryId.substring(primaryId.length - 4);
return '*' * (primaryId.length - 4) + lastFour;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context) AppLocalizations.of(context).customerInfo,
.customerInfo
.replaceFirst(RegExp('\n'), ''),
), ),
), ),
body: SafeArea( body: SingleChildScrollView(
child: Stack(
children: [
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: SafeArea(
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),
),
),
),
],
),
),
),
IgnorePointer(
child: Center( child: Center(
child: Opacity( child: Column(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
));
}
Widget _buildPersonalInfo(ThemeData theme) {
return Column(
key: const ValueKey('personal_info'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 30),
CircleAvatar(
radius: 50,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 150,
height: 150,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
user.name ?? '',
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
),
),
),
Text( Text(
AppLocalizations.of(context).personaldetails, '${AppLocalizations.of(context).cif}: ${user.cifNumber ?? 'N/A'}',
style: theme.textTheme.titleMedium?.copyWith( style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16,
color: theme.colorScheme.onSurfaceVariant),
), ),
), const SizedBox(height: 30),
const SizedBox(height: 16),
InfoField( InfoField(
label: AppLocalizations.of(context).activeAccounts, label: AppLocalizations.of(context).activeAccounts,
value: user.activeAccounts?.toString() ?? 'N/A', value: user.activeAccounts?.toString() ?? '6',
), ),
InfoField( InfoField(
label: AppLocalizations.of(context).mobileNumber, label: AppLocalizations.of(context).mobileNumber,
@@ -196,39 +68,29 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
), ),
InfoField( InfoField(
label: AppLocalizations.of(context).dateOfBirth, label: AppLocalizations.of(context).dateOfBirth,
value: (user.dateOfBirth != null && user.dateOfBirth!.length == 8) value: (user.dateOfBirth != null &&
user.dateOfBirth!.length == 8)
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}' ? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
: 'N/A', : 'N/A',
), ), // Replace with DOB if available
InfoField( InfoField(
label: AppLocalizations.of(context).branchCode, label: AppLocalizations.of(context).branchCode,
value: user.branchId ?? 'N/A', value: user.branchId ?? 'N/A',
), ),
InfoField( InfoField(
label: AppLocalizations.of(context).address, label: AppLocalizations.of(context).branchAddress,
value: user.address ?? 'N/A', value: user.address ?? 'N/A',
), ), // Replace with Aadhar if available
],
);
}
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( InfoField(
label: AppLocalizations.of(context).primaryId, label: AppLocalizations.of(context).primaryId,
value: _maskPrimaryId(user.primaryId), value: user.primaryId ?? 'N/A',
), ), // Replace with PAN if available
], ],
),
),
),
),
),
); );
} }
} }
@@ -250,16 +112,16 @@ class InfoField extends StatelessWidget {
children: [ children: [
Text( Text(
label, label,
style: theme.textTheme.bodySmall?.copyWith( style: TextStyle(
color: theme.colorScheme.onSurface.withOpacity(0.6), fontSize: 15,
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 3),
Text( Text(
value.isEmpty ? 'N/A' : value, value,
style: theme.textTheme.titleMedium?.copyWith( style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
fontWeight: FontWeight.w600,
),
), ),
], ],
), ),

View File

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

View File

@@ -13,7 +13,7 @@ class AccountCard extends StatelessWidget {
width: 300, width: 300,
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -33,7 +33,7 @@ class AccountCard extends StatelessWidget {
Text( Text(
account.accountType, account.accountType,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).scaffoldBackgroundColor,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -42,7 +42,7 @@ class AccountCard extends StatelessWidget {
account.accountType == 'Savings' account.accountType == 'Savings'
? Icons.savings ? Icons.savings
: Icons.account_balance, : Icons.account_balance,
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).scaffoldBackgroundColor,
), ),
], ],
), ),
@@ -50,13 +50,13 @@ class AccountCard extends StatelessWidget {
Text( Text(
account.accountNumber, account.accountNumber,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary, fontSize: 16), color: Theme.of(context).scaffoldBackgroundColor, fontSize: 16),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
Text( Text(
'${account.currency} ${account.balance.toStringAsFixed(2)}', '${account.currency} ${account.balance.toStringAsFixed(2)}',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary, color: Theme.of(context).scaffoldBackgroundColor,
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -65,7 +65,7 @@ class AccountCard extends StatelessWidget {
Text( Text(
AppLocalizations.of(context).availableBalance, AppLocalizations.of(context).availableBalance,
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary, fontSize: 16), color: Theme.of(context).scaffoldBackgroundColor, fontSize: 16),
), ),
], ],
), ),

View File

@@ -1,4 +1,8 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -10,97 +14,43 @@ class EnquiryScreen extends StatefulWidget {
} }
class _EnquiryScreen extends State<EnquiryScreen> { class _EnquiryScreen extends State<EnquiryScreen> {
// Updated to launch externally and pre-fill the subject
Future<void> _launchEmailAddress(String email) async { Future<void> _launchEmailAddress(String email) async {
final Uri emailUri = Uri( final Uri emailUri = Uri(scheme: 'mailto', path: email);
scheme: 'mailto',
path: email,
query: 'subject=Enquiry', // Pre-fills the subject line
);
if (await canLaunchUrl(emailUri)) { if (await canLaunchUrl(emailUri)) {
// Use external application mode await launchUrl(emailUri);
await launchUrl(emailUri, mode: LaunchMode.externalApplication);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( debugPrint('${AppLocalizations.of(context).emailLaunchError} $email');
SnackBar(content: Text('Could not open email app for $email')),
);
} }
} }
// Updated with better error handling
Future<void> _launchPhoneNumber(String phone) async { Future<void> _launchPhoneNumber(String phone) async {
final Uri phoneUri = Uri(scheme: 'tel', path: phone); final Uri phoneUri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(phoneUri)) { if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri); await launchUrl(phoneUri);
} else { } else {
ScaffoldMessenger.of(context).showSnackBar( debugPrint('${AppLocalizations.of(context).dialerLaunchError} $phone');
SnackBar(content: Text('Could not open dialer for $phone')),
);
}
}
// Updated to launch externally
Future<void> _launchUrl(String url) async {
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) { Widget _buildContactItem(String role, String email, String phone) {
return Card( return Column(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(role, Text(role, style: const TextStyle(color: Colors.grey)),
style: TextStyle( const SizedBox(height: 4),
color: Theme.of(context).colorScheme.onSurface,
fontSize: 15,
fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () => _launchEmailAddress(email), onTap: () => _launchEmailAddress(email),
child: Row(
children: [
const Icon(Icons.email),
const SizedBox(width: 8),
Expanded(
child: Text(email, child: Text(email,
style: TextStyle( style: TextStyle(color: Theme.of(context).primaryColor)),
color: Theme.of(context).colorScheme.primary,
fontSize: 14)),
), ),
], const SizedBox(height: 4),
),
),
const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () => _launchPhoneNumber(phone), onTap: () => _launchPhoneNumber(phone),
child: Row(
children: [
const Icon(Icons.phone),
const SizedBox(width: 8),
Expanded(
child: Text(phone, child: Text(phone,
style: TextStyle( style:
color: Theme.of(context).colorScheme.primary, TextStyle(color: Theme.of(context).scaffoldBackgroundColor)),
fontSize: 14)),
), ),
], ],
),
),
],
),
),
); );
} }
@@ -108,70 +58,80 @@ class _EnquiryScreen extends State<EnquiryScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).enquiry), leading: IconButton(
centerTitle: false, icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
), ),
body: Stack( title: Text(
children: [ AppLocalizations.of(context).enquiry,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false,
actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0),
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Card( // … existing Mail us / Call us / Write to us …
elevation: 4, const SizedBox(height: 20),
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( Text(
"Complaint Form", AppLocalizations.of(context).writeToUs,
style: TextStyle( style: const TextStyle(color: Colors.grey),
fontSize: 15,
color: Theme.of(context).colorScheme.primary,
), ),
const SizedBox(height: 4),
Text(
"complaint@kccb.in",
style: TextStyle(color: Theme.of(context).primaryColor),
), ),
Icon(
Icons.open_in_new, const SizedBox(height: 20),
color: Theme.of(context).colorScheme.primary,
size: 16.0,
),
],
),
),
),
),
const Divider(height: 32),
Text( Text(
AppLocalizations.of(context).keyContacts, AppLocalizations.of(context).keyContacts,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontSize: 17,
fontWeight: FontWeight.bold, color: Theme.of(context).primaryColor,
), ),
// horizontal line
), ),
Divider(color: Colors.grey[300]),
const SizedBox(height: 16), const SizedBox(height: 16),
Expanded(
child: ListView(
children: [
_buildContactItem( _buildContactItem(
AppLocalizations.of(context).chairman, AppLocalizations.of(context).chairman,
"chairman@kccb.in", "chairman@kccb.in",
"01892-222677", "01892-222677",
), ),
const SizedBox(height: 16),
_buildContactItem( _buildContactItem(
AppLocalizations.of(context).managingDirector, AppLocalizations.of(context).managingDirector,
"md@kccb.in", "md@kccb.in",
"01892-224969", "01892-224969",
), ),
const SizedBox(height: 16),
_buildContactItem( _buildContactItem(
AppLocalizations.of(context).gmWest, AppLocalizations.of(context).gmWest,
"gmw@kccb.in", "gmw@kccb.in",
"01892-223280", "01892-223280",
), ),
const SizedBox(height: 16),
_buildContactItem( _buildContactItem(
AppLocalizations.of(context).gmNorth, AppLocalizations.of(context).gmNorth,
"gmn@kccb.in", "gmn@kccb.in",
@@ -180,25 +140,6 @@ class _EnquiryScreen extends State<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
),
),
),
),
),
],
),
); );
} }
} }

View File

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

View File

@@ -2,8 +2,6 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -19,7 +17,6 @@ import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.da
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:kmobile/api/services/payment_service.dart'; import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';
enum TransactionMode { neft, rtgs, imps } enum TransactionMode { neft, rtgs, imps }
class FundTransferAmountScreen extends StatefulWidget { class FundTransferAmountScreen extends StatefulWidget {
@@ -42,71 +39,13 @@ class FundTransferAmountScreen extends StatefulWidget {
} }
class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> { class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
bool _isAmountOverLimit = false;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _remarksController = TextEditingController();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
TransactionMode _selectedMode = TransactionMode.neft; TransactionMode _selectedMode = TransactionMode.neft;
@override
void initState() {
super.initState();
_loadLimit(); // Call the new method
_amountController.addListener(_checkAmountLimit);
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
@override @override
void dispose() { void dispose() {
_amountController.removeListener(_checkAmountLimit);
_amountController.dispose(); _amountController.dispose();
_remarksController.dispose();
super.dispose(); super.dispose();
} }
@@ -173,7 +112,6 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
remitterName: widget.remitterName, remitterName: widget.remitterName,
beneficiaryName: widget.creditBeneficiary.name, beneficiaryName: widget.creditBeneficiary.name,
tpin: tpin, tpin: tpin,
remarks: _remarksController.text,
); );
final neftService = getIt<NeftService>(); final neftService = getIt<NeftService>();
final completer = Completer<PaymentResponse>(); final completer = Completer<PaymentResponse>();
@@ -241,7 +179,6 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
remitterName: widget.remitterName, remitterName: widget.remitterName,
beneficiaryName: widget.creditBeneficiary.name, beneficiaryName: widget.creditBeneficiary.name,
tpin: tpin, tpin: tpin,
remarks: _remarksController.text,
); );
final impsService = getIt<ImpsService>(); final impsService = getIt<ImpsService>();
final completer = Completer<PaymentResponse>(); final completer = Completer<PaymentResponse>();
@@ -298,7 +235,6 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
remitterName: widget.remitterName, remitterName: widget.remitterName,
beneficiaryName: widget.creditBeneficiary.name, beneficiaryName: widget.creditBeneficiary.name,
tpin: tpin, tpin: tpin,
remarks: _remarksController.text,
); );
final rtgsService = getIt<RtgsService>(); final rtgsService = getIt<RtgsService>();
final completer = Completer<PaymentResponse>(); final completer = Completer<PaymentResponse>();
@@ -359,12 +295,9 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')), title: Text(loc.fundTransfer),
), ),
body: SafeArea( body: Padding(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
@@ -400,8 +333,7 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
elevation: 0, elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0), margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile( child: ListTile(
leading: getBankLogo( leading: getBankLogo(widget.creditBeneficiary.bankName),
widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name), title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo), subtitle: Text(widget.creditBeneficiary.accountNo),
), ),
@@ -433,14 +365,15 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
}); });
}, },
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
selectedColor: selectedColor: Theme.of(context).colorScheme.onPrimary,
Theme.of(context).colorScheme.onPrimary, fillColor: Theme.of(context).primaryColor,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent, borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent, selectedBorderColor: Colors.transparent,
splashColor: Theme.of(context).colorScheme.primary, splashColor:
highlightColor: Theme.of(context).colorScheme.primary, Theme.of(context).primaryColor,
highlightColor:
Theme.of(context).primaryColor,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -462,15 +395,6 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
//Remarks
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount // Amount
TextFormField( TextFormField(
controller: _amountController, controller: _amountController,
@@ -491,49 +415,23 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
return null; return null;
}, },
), ),
const SizedBox(height: 8),
if (_isLoadingLimit)
Text(AppLocalizations.of(context).fetchingDailyLimit),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(), const Spacer(),
// Proceed Button // Proceed Button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed, onPressed: _onProceed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
), ),
child: Text(AppLocalizations.of(context).proceed), child: Text(AppLocalizations.of(context).proceed),
), ),
), ),
const SizedBox(height: 10),
], ],
), ),
), ),
), ),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
),
); );
} }
} }

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/cooldown.dart';
import 'package:kmobile/widgets/bank_logos.dart'; import 'package:kmobile/widgets/bank_logos.dart';
import 'package:kmobile/data/models/beneficiary.dart'; import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart';
@@ -28,22 +27,11 @@ class _FundTransferBeneficiaryScreenState
var service = getIt<BeneficiaryService>(); var service = getIt<BeneficiaryService>();
bool _isLoading = true; bool _isLoading = true;
List<Beneficiary> _beneficiaries = []; List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBeneficiaries(); _loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
} }
Future<void> _loadBeneficiaries() async { Future<void> _loadBeneficiaries() async {
@@ -51,28 +39,14 @@ class _FundTransferBeneficiaryScreenState
setState(() { setState(() {
_beneficiaries = data _beneficiaries = data
.where((b) => widget.isOwnBank .where((b) => widget.isOwnBank
? b.bankName!.toLowerCase().contains('kangra central') ? b.bankName ==
: !b.bankName!.toLowerCase().contains('kangra central')) 'THE KANGRA CENTRAL CO-OP BANK LIMITED' // Assuming 'KCCB' is your bank's name
: b.bankName != 'THE KANGRA CENTRAL CO-OP BANK LIMITED')
.toList(); .toList();
_filteredBeneficiaries = _beneficiaries;
_isLoading = false; _isLoading = false;
}); });
} }
void _filterBeneficiaries(String query) {
setState(() {
if (query.isEmpty) {
_filteredBeneficiaries = _beneficiaries;
} else {
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
final lowerQuery = query.toLowerCase();
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
Widget _buildShimmerList() { Widget _buildShimmerList() {
return ListView.builder( return ListView.builder(
itemCount: 6, itemCount: 6,
@@ -100,36 +74,19 @@ class _FundTransferBeneficiaryScreenState
} }
Widget _buildBeneficiaryList() { Widget _buildBeneficiaryList() {
if (_filteredBeneficiaries.isEmpty) { if (_beneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
} }
return ListView.builder( return ListView.builder(
itemCount: _filteredBeneficiaries.length, itemCount: _beneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final beneficiary = _filteredBeneficiaries[index]; final beneficiary = _beneficiaries[index];
return ListTile(
// --- Cooldown Logic ---
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( leading: CircleAvatar(
radius: 24, radius: 24,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context), child: getBankLogo(beneficiary.bankName),
), ),
title: Text(beneficiary.name), title: Text(beneficiary.name),
subtitle: Column( subtitle: Column(
@@ -144,25 +101,7 @@ class _FundTransferBeneficiaryScreenState
), ),
], ],
), ),
trailing: isCoolingDown
? CooldownTimer(
createdAt: beneficiary.createdAt!,
onTimerFinish: () {
setState(() {});
},
)
: null,
onTap: () { 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( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -174,10 +113,7 @@ class _FundTransferBeneficiaryScreenState
), ),
), ),
); );
}
}, },
),
),
); );
}, },
); );
@@ -189,45 +125,7 @@ class _FundTransferBeneficiaryScreenState
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries), title: Text(AppLocalizations.of(context).beneficiaries),
), ),
body: Stack( body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
); );
} }
} }

View File

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

View File

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

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