90 Commits

Author SHA1 Message Date
5823eaede8 After Login Button 2025-12-08 17:45:12 +05:30
1ae3e7c0a6 Limit Added to SMS enabled device 2025-11-04 15:29:56 +05:30
530e5c0493 Header Added 2025-10-28 18:04:14 +05:30
e1c1a58086 SMS Screen Modifiaction 2025-10-28 17:59:05 +05:30
dd3e94a69e SMS succesfully sent 2025-10-27 17:36:28 +05:30
2743f92283 Loger in Mobile App 2025-10-22 17:43:24 +05:30
72a9d5711a SMS Testing 2025-10-22 13:47:05 +05:30
1edb2804f1 SMS Send formatting 2025-10-22 13:19:33 +05:30
c9c52b39fa SMS Send without manual sim ard choosing option 2025-10-22 12:49:38 +05:30
7a0265ad8d Send SMS File created 2025-10-22 12:23:23 +05:30
58e53d0aeb App Version added and dart format 2025-10-21 13:32:14 +05:30
06ef2ab36b Profile shifted to left 2025-10-19 21:27:47 +05:30
0362bf2013 Fingerprint option added in profile screen 2025-10-17 11:47:20 +05:30
73b96b82f7 Background Timer Feature#1 2025-10-16 17:54:48 +05:30
c78a90dbfe Screenshot Prevention and backgorund Screen blacking 2025-10-15 12:46:56 +05:30
df025babd5 App Functionality 2025-10-15 12:30:01 +05:30
4d19bf6146 APK Build Pre_versioning 2025-10-14 16:18:23 +05:30
d36cad31c1 Masking of Primary ID and Compliant Form Link created 2025-10-13 18:10:39 +05:30
39e7a02ca5 Daily Transaction Limit #2 2025-10-13 14:59:00 +05:30
0c7470d74b Daily Transaction Limit #1 2025-10-13 11:28:16 +05:30
32463680e8 Own bank List fixed 2025-10-08 15:54:19 +05:30
00cb98ae83 Icon Fixed 2025-10-07 11:33:19 +05:30
18844495c2 FQAs and Quick Links Screens created 2025-09-26 12:14:12 +05:30
3f7869677c Merge branch 'live-1' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into live-1 2025-09-25 12:56:23 +05:30
0e4072fe8f FAQs link added and Branch and ATM locator updated 2025-09-25 12:56:03 +05:30
b5acae85d5 Final bug fixed 2025-09-24 17:54:55 +05:30
cc7c7a8042 IFSC optimized 2025-09-24 11:28:36 +05:30
475f30a4bb IFSC bug fixed 2025-09-22 15:10:19 +05:30
b00bc6c8c0 iOS Build #3 2025-09-19 13:15:07 +05:30
c47862bd60 iOS Build #2 2025-09-19 13:02:47 +05:30
8ef4f1327f iOS Build #1 2025-09-19 12:13:40 +05:30
87e00d540f Merge branch 'live-1' of https://git.7o9o.net/md.asif5/kmobile into live-1 2025-09-19 11:35:34 +05:30
44a6307995 added production URL 2025-09-19 11:35:14 +05:30
f6b24e4c6f APK build #1 2025-09-18 17:52:53 +05:30
9cf4c44bb0 New url 2025-09-18 16:18:08 +05:30
b3b51d423d transaction details bug fixed 2025-09-17 17:35:56 +05:30
b1f4d380c6 kcconnect to KCCB 2025-09-17 16:42:40 +05:30
17ebf8626a kconnect changed to ?? 2025-09-17 11:46:09 +05:30
e466dff424 Remarks Field in Transaction Page #3 2025-09-16 16:47:01 +05:30
8f2b981b5b Remarks Field in Transaction Page #2 2025-09-16 13:34:50 +05:30
ba1ef0ed24 IFSC Field Testing #2 2025-09-16 12:59:31 +05:30
d2044d49b5 IFSC Field Testing #1 2025-09-16 12:40:46 +05:30
db39cfbcc9 Remarks Field in Transaction Page 2025-09-16 12:11:57 +05:30
bf23627e0a Live Test #2 2025-09-12 18:16:25 +05:30
eba38c2e42 Live Testing #1 2025-09-12 15:01:39 +05:30
asif
d4bba6dc49 added base url to point to production backend 2025-09-11 22:45:00 +05:30
b56bf0d7df Localization Changes #9 2025-09-11 18:15:51 +05:30
82e057d804 Mobile Number Implemented in OTP 2025-09-11 18:13:39 +05:30
0f205873a9 TPIN Set Screen OTP Implemented 2025-09-11 16:18:37 +05:30
191610c9b2 Localization Changes #8 2025-09-11 15:32:27 +05:30
b03e917d78 Localization Changes #7 2025-09-11 15:27:05 +05:30
asif
188fbe9bb1 made a flow for letting migrated users set their password 2025-09-11 02:08:56 +05:30
asif
817f3d75f5 fix api url for setting password 2025-09-11 00:21:09 +05:30
asif
c42b973bee API for otp validation and setting password 2025-09-11 00:09:49 +05:30
asif
def009003c navigate to different screen if the error message is different
(for migrated users)
2025-09-11 00:08:51 +05:30
52e9f59e6f Transaction Details Bug Fixed 2025-09-10 18:10:18 +05:30
e4e104837b Localization Changes #6 2025-09-10 11:43:15 +05:30
dd7b7a6f4c balance added in transaction 2025-09-09 17:20:01 +05:30
c322d1d2fd Change Password Implemented 2025-09-09 16:03:34 +05:30
b3fb387bdd Change PasswordIntegration #1 2025-09-09 12:34:45 +05:30
asif
b62b8a157d added account balance in statement. Fixed bank logo in statement pdf.
removed unwanted log generations
2025-09-08 20:50:24 +05:30
c729b775c9 Change Password UI #2 2025-09-08 18:12:57 +05:30
465065db57 Change Password UI 2025-09-08 17:58:15 +05:30
0a6dde9ead Download PDF #2 2025-09-08 17:57:11 +05:30
b513664a47 Download PDF #1 2025-09-08 16:10:56 +05:30
d8d87e8da4 Localization Changes #6 2025-09-08 13:07:35 +05:30
1204507375 Add Beneficiary bugs fixed #2 2025-09-05 18:15:19 +05:30
be9a6fc93f Add Beneficiary bugs fixed 2025-09-05 12:02:48 +05:30
b8337d1152 Theme UI #2 2025-09-04 17:58:16 +05:30
d3792a1a06 Theme UI According to Client 2025-09-04 17:57:12 +05:30
845ce1fe78 Logout Button Implemented 2025-09-04 15:40:18 +05:30
45dbf8464a Theme Mode-System Default, Minor Changes & Logout Button #1 2025-09-04 13:43:52 +05:30
asif
a1ea60841e changed minSdk to 29 and also turned off backup in android manifest 2025-09-04 02:17:19 +05:30
asif
2abc315af9 removed unwanted comments from themes.dart 2025-09-04 01:58:26 +05:30
asif
a44ac67842 removed print statements 2025-09-04 01:57:32 +05:30
asif
1b684af3a8 added root and frida detection 2025-09-04 01:56:59 +05:30
asif
aa5a7877e4 added proguard for code obfuscation in android 2025-09-04 01:24:44 +05:30
asif
d6a99ea295 removed the newline from title bar 2025-09-04 01:24:14 +05:30
asif
cf8964ca36 changed add beneficiary screen to to improve UI 2025-09-04 00:58:30 +05:30
asif
c489bf3895 added SafeArea and changed buttton color in Add Beneficiary Page 2025-09-04 00:33:33 +05:30
asif
ba819f51d7 added safearea to some screens. Also fixed some titlebars from wrap text 2025-09-04 00:16:02 +05:30
asif
48f169d10c applied animations and haptics to TPIN screen 2025-09-04 00:14:36 +05:30
asif
64e80148a3 changed primary color usage to fix the color bug in dark mode
Changed Theme.of(context).primaryColor to Theme.of(contex).colorScheme.primary to make dark mode work properly.
2025-09-03 23:49:30 +05:30
asif
85f58c1e25 added const modifier 2025-09-03 23:46:58 +05:30
asif
3b9b1e507c implemented correct colors and proper animations in mpin screen. 2025-09-03 23:45:52 +05:30
57439f2acb Theme Mode #3 2025-09-03 17:39:57 +05:30
52534f9210 Theme Mode #2 2025-09-03 15:51:44 +05:30
d9f78629d2 Theme Mode #1 2025-09-03 15:15:09 +05:30
3e3465d5e8 Optimization #2 2025-09-03 12:10:28 +05:30
be76465774 Optimization #1 2025-09-03 11:41:41 +05:30
117 changed files with 5196 additions and 1684 deletions

View File

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

@@ -45,7 +45,7 @@ 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 flutter.minSdkVersion minSdkVersion 29
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@@ -53,13 +53,13 @@ android {
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
'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.
} }
} }
} }

View File

@@ -1,77 +1,21 @@
# Keep Flutter embedding and plugin registrant (important)
-keep class io.flutter.embedding.** { *; }
-keep class io.flutter.plugins.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.app.** { *; } -keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
# Keep Application / Activity if you customized them (replace with your package name) -keep class io.flutter.util.** { *; }
# -keep class com.yourcompany.yourapp.MainActivity { *; } -keep class io.flutter.view.** { *; }
# -keep class com.yourcompany.yourapp.** { *; } -keep class io.flutter.embedding.** { *; }
-keep class io.flutter.embedding.engine.plugins.** { *; }
# 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.
# WorkManager (if used) # This is generated automatically by the Android Gradle plugin.
-keep class androidx.work.impl.background.systemjob.SystemJobService { *; } -dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
-keep class androidx.work.** { *; } -dontwarn com.google.android.play.core.splitinstall.SplitInstallException
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager
# Room/DB entities - if you use Room, keep annotations and entity classes -dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory
-keepclassmembers class * { -dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder
@androidx.room.* <fields>; -dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest
} -dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState
-keep class androidx.room.** { *; } -dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
-dontwarn com.google.android.play.core.tasks.OnFailureListener
# Keep classes loaded by reflection (e.g. through Class.forName) -dontwarn com.google.android.play.core.tasks.OnSuccessListener
-keepclassmembers,includedescriptorclasses class * { -dontwarn com.google.android.play.core.tasks.Task
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,10 +2,13 @@
<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"

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

BIN
assets/images/ipos_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
assets/images/uco_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 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 = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
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 = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
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: 11 KiB

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -47,5 +47,19 @@
<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

@@ -25,7 +25,8 @@ class AuthService {
print(e.toString()); print(e.toString());
} }
if (e.response?.statusCode == 401) { if (e.response?.statusCode == 401) {
throw AuthException('Invalid credentials'); throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
} }
throw NetworkException('Network error during login'); throw NetworkException('Network error during login');
} catch (e) { } catch (e) {
@@ -91,4 +92,53 @@ 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;
}
} }

View File

@@ -1,5 +1,6 @@
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';
@@ -39,9 +40,12 @@ 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) {
rethrow; throw UnexpectedException(
'Unexpected error during login: ${e.toString()}');
} }
return Ifsc.fromJson({}); return Ifsc.fromJson({});
} }
@@ -59,6 +63,10 @@ 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");
@@ -66,7 +74,7 @@ class BeneficiaryService {
return response.data['name']; return response.data['name'];
} }
// Send Data for Validation // Beneficiary Validate And ADD
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

@@ -0,0 +1,70 @@
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();
}
}

View File

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

View File

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

View File

@@ -2,6 +2,10 @@ 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/features/auth/screens/login_screen.dart';
//import 'package:kmobile/features/auth/screens/sms_verification_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart'; import './l10n/app_localizations.dart';
@@ -11,13 +15,12 @@ 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/card/screens/card_management_screen.dart'; import 'features/card/screens/card_management_screen.dart';
import 'features/auth/screens/splash_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});
@@ -32,25 +35,46 @@ class KMobile extends StatefulWidget {
} }
} }
class _KMobileState extends State<KMobile> { class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
bool showSplash = true; Timer? _backgroundTimer;
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(() {
showSplash = false; @override
}); void dispose() {
}); WidgetsBinding.instance.removeObserver(this);
_backgroundTimer?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
_backgroundTimer?.cancel();
break;
case AppLifecycleState.paused:
_backgroundTimer = Timer(const Duration(minutes: 2), () {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
});
break;
default:
break;
}
} }
Future<void> loadPreferences() async { 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(() {
@@ -78,29 +102,33 @@ class _KMobileState extends State<KMobile> {
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 MaterialApp( return BlocBuilder<ThemeModeCubit, ThemeModeState>(
debugShowCheckedModeBanner: false, builder: (context, modeState) {
locale: _locale ?? const Locale('en'), return MaterialApp(
supportedLocales: const [ debugShowCheckedModeBanner: false,
Locale('en'), locale: _locale ?? const Locale('en'),
Locale('hi'), supportedLocales: const [
], Locale('en'),
localizationsDelegates: const [ Locale('hi'),
AppLocalizations.delegate, ],
GlobalMaterialLocalizations.delegate, localizationsDelegates: const [
GlobalWidgetsLocalizations.delegate, AppLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalMaterialLocalizations.delegate,
], GlobalWidgetsLocalizations.delegate,
title: 'kMobile', GlobalCupertinoLocalizations.delegate,
theme: themeState.getThemeData(), ],
//darkTheme: themeState.getThemeData(), title: 'kMobile',
themeMode: ThemeMode.light, theme: themeState.getLightThemeData(),
onGenerateRoute: AppRoutes.generateRoute, darkTheme: themeState.getDarkThemeData(),
initialRoute: AppRoutes.splash, themeMode: context.watch<ThemeModeCubit>().state.mode,
home: showSplash ? const SplashScreen() : const AuthGate(), onGenerateRoute: AppRoutes.generateRoute,
home: const AuthGate(),
);
},
); );
}, },
), ),
@@ -110,7 +138,6 @@ class _KMobileState extends State<KMobile> {
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();
} }
@@ -171,9 +198,12 @@ class _AuthGateState extends State<AuthGate> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_checking) { if (_checking) {
return const SplashScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
if (_isLoggedIn) { if (_isLoggedIn) {
if (_hasMPin) { if (_hasMPin) {
if (_biometricEnabled) { if (_biometricEnabled) {
@@ -181,13 +211,15 @@ 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 SplashScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
if (snapshot.data == true) { if (snapshot.data == true) {
return const NavigationScaffold(); // Authenticated return const NavigationScaffold();
} }
return MPinScreen( return MPinScreen(
mode: MPinMode.enter, mode: MPinMode.enter,
onCompleted: (_) { onCompleted: (_) {
@@ -218,7 +250,6 @@ 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,
@@ -239,7 +270,6 @@ 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;
@@ -247,7 +277,6 @@ 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,
@@ -262,7 +291,6 @@ 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(
@@ -280,7 +308,6 @@ 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();
} }
@@ -288,7 +315,6 @@ 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(),
const CardManagementScreen(), const CardManagementScreen(),
@@ -335,10 +361,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
backgroundColor: Theme.of(context) backgroundColor: const Color(0XFF1E58AD),
.scaffoldBackgroundColor, // Light blue background selectedItemColor: Theme.of(context).colorScheme.onPrimary,
selectedItemColor: Theme.of(context).primaryColor, unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
unselectedItemColor: Colors.black54,
onTap: (index) { onTap: (index) {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
@@ -365,11 +390,9 @@ 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;
@@ -400,7 +423,11 @@ class BiometricPromptScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context)); Future.microtask(() => _showDialog(context));
return const SplashScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
Future<void> _showDialog(BuildContext context) async { Future<void> _showDialog(BuildContext context) async {

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/splash_screen.dart';
import '../app.dart'; import '../app.dart';
import '../features/auth/screens/login_screen.dart'; import '../features/auth/screens/login_screen.dart';
// import '../features/auth/screens/forgot_password_screen.dart'; // import '../features/auth/screens/forgot_password_screen.dart';
@@ -30,8 +29,6 @@ class AppRoutes {
// Route generator // Route generator
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) { switch (settings.name) {
case splash:
return MaterialPageRoute(builder: (_) => const SplashScreen());
case login: case login:
return MaterialPageRoute(builder: (_) => const LoginScreen()); return MaterialPageRoute(builder: (_) => const LoginScreen());

View File

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

View File

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

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

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

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

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

View File

@@ -6,6 +6,7 @@ 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,
@@ -15,6 +16,7 @@ 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() {
@@ -26,6 +28,7 @@ class ImpsTransaction {
'remitterName': remitterName, 'remitterName': remitterName,
'beneficiaryName': beneficiaryName, 'beneficiaryName': beneficiaryName,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -6,6 +6,7 @@ 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,
@@ -15,6 +16,7 @@ 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() {
@@ -26,6 +28,7 @@ class NeftTransaction {
'remitterName': remitterName, 'remitterName': remitterName,
'beneficiaryName': beneficiaryName, 'beneficiaryName': beneficiaryName,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -6,6 +6,7 @@ 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,
@@ -15,6 +16,7 @@ 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() {
@@ -26,6 +28,7 @@ class RtgsTransaction {
'remitterName': remitterName, 'remitterName': remitterName,
'beneficiaryName': beneficiaryName, 'beneficiaryName': beneficiaryName,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -4,8 +4,18 @@ 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,
@@ -13,16 +23,19 @@ class Transaction {
'date': date, 'date': date,
'amount': amount, 'amount': amount,
'type': type, 'type': type,
'balance': balance,
'balanceType': balanceType
}; };
} }
factory Transaction.fromJson(Map<String, dynamic> json) { factory Transaction.fromJson(Map<String, dynamic> json) {
return Transaction( return Transaction(
id: json['id'] as String?, id: json['id'] as String?,
name: json['name'] as String?, name: json['name'] as String?,
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,6 +4,7 @@ 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,
@@ -11,6 +12,7 @@ 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() {
@@ -20,6 +22,7 @@ class Transfer {
'toAccountType': toAccountType, 'toAccountType': toAccountType,
'amount': amount, 'amount': amount,
'tpin': tpin, 'tpin': tpin,
'remarks': remarks,
}; };
} }
} }

View File

@@ -49,6 +49,10 @@ class AuthRepository {
_tokenExpiryKey, token.expiresAt.toIso8601String()); _tokenExpiryKey, token.expiresAt.toIso8601String());
} }
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);

View File

@@ -1,5 +1,3 @@
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';
@@ -25,8 +23,6 @@ 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,3 +1,4 @@
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';
@@ -8,8 +9,10 @@ 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';
@@ -20,6 +23,7 @@ Future<void> setupDependencies() async {
//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());
@@ -43,9 +47,13 @@ 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.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(
@@ -61,13 +69,15 @@ 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', // 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://localhost:8081', //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
connectTimeout: const Duration(seconds: 5), 'https://kccbmbnk.net', //prod small
receiveTimeout: const Duration(seconds: 10), connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'X-Login-Type': 'MB',
}, },
), ),
); );

View File

@@ -30,9 +30,9 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
int selectedIndex = widget.selectedIndex; int selectedIndex = widget.selectedIndex;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(AppLocalizations.of(context)
AppLocalizations.of(context).accountInfo, .accountInfo
), .replaceFirst(RegExp('\n'), '')),
), ),
body: ListView( body: ListView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),

View File

@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart'; import 'package:flutter/services.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/models/transaction.dart';
@@ -7,14 +8,21 @@ import 'package:kmobile/data/repositories/transaction_repository.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'transaction_details_screen.dart'; import 'transaction_details_screen.dart';
import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
class AccountStatementScreen extends StatefulWidget { class AccountStatementScreen extends StatefulWidget {
final String accountNo; final String accountNo;
final String balance; final String balance;
final String accountType;
const AccountStatementScreen({ const AccountStatementScreen({
super.key, super.key,
required this.accountNo, required this.accountNo,
required this.balance, required this.balance,
required this.accountType,
}); });
@override @override
@@ -28,6 +36,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
List<Transaction> _transactions = []; List<Transaction> _transactions = [];
final _minAmountController = TextEditingController(); final _minAmountController = TextEditingController();
final _maxAmountController = TextEditingController(); final _maxAmountController = TextEditingController();
//Future<Map<String, dynamic>?>? accountStatementsFuture;
@override @override
void initState() { void initState() {
@@ -119,31 +128,10 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
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).accountStatement, AppLocalizations.of(context).accountStatement,
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: Padding( body: Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
@@ -210,19 +198,20 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
child: ElevatedButton( child: ElevatedButton(
onPressed: _loadTransactions, onPressed: _loadTransactions,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor, backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
), ),
child: Text( child: Text(
AppLocalizations.of(context).search, AppLocalizations.of(context).search,
style: TextStyle( style: TextStyle(
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 16, fontSize: 16,
), ),
), ),
), ),
), ),
const SizedBox(height: 35), const SizedBox(height: 15),
if (!_txLoading && if (!_txLoading &&
_transactions.isNotEmpty && _transactions.isNotEmpty &&
fromDate == null && fromDate == null &&
@@ -234,7 +223,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.grey[700], color: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -275,10 +264,11 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
: _transactions.isEmpty : _transactions.isEmpty
? Center( ? Center(
child: Text( child: Text(
AppLocalizations.of(context).noTransactions, AppLocalizations.of(context).noTransactions,
style: TextStyle( style: TextStyle(
fontSize: 16, color: Colors.grey[600]), fontSize: 16,
), color: Theme.of(context).colorScheme.onSurface,
)),
) )
: ListView.separated( : ListView.separated(
itemCount: _transactions.length, itemCount: _transactions.length,
@@ -289,8 +279,9 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
tx.type == 'CR' tx.type == 'CR'
? Symbols.call_received ? Symbols.call_received
: Symbols.call_made, : Symbols.call_made,
color: color: tx.type == 'CR'
tx.type == 'CR' ? Colors.green : Colors.red, ? Colors.green
: Theme.of(context).colorScheme.error,
), ),
title: Text( title: Text(
tx.date ?? '', tx.date ?? '',
@@ -298,15 +289,26 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
), ),
subtitle: Text( subtitle: Text(
tx.name != null tx.name != null
? (tx.name!.length > 18 ? (tx.name!.length > 22
? tx.name!.substring(0, 22) ? tx.name!.substring(0, 22)
: tx.name!) : tx.name!)
: '', : '',
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
trailing: Text( trailing: Column(
"${tx.amount}", crossAxisAlignment: CrossAxisAlignment.end,
style: const TextStyle(fontSize: 17), mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${tx.amount}",
style: const TextStyle(fontSize: 17),
),
Text(
"Bal: ₹${tx.balance}",
style: const TextStyle(
fontSize: 12), // Style matches tx.name
),
],
), ),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
@@ -327,14 +329,197 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
], ],
), ),
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
_exportToPdf();
},
child: const Icon(Icons.download),
),
); );
} }
Future<void> _exportToPdf() async {
// Step 1: Check if there are any transactions to export.
if (_transactions.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No transactions to export.'),
),
);
}
return;
}
var logo = await rootBundle.load('assets/images/logo.png');
var rubik = await rootBundle.load("assets/fonts/Rubik-Regular.ttf");
final pdf = pw.Document();
pdf.addPage(pw.MultiPage(
margin: const pw.EdgeInsets.all(20),
build: (pw.Context context) {
return [
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.start,
crossAxisAlignment: pw.CrossAxisAlignment.center,
children: [
pw.Image(pw.MemoryImage(logo.buffer.asUint8List()),
width: 50, height: 50),
pw.SizedBox(width: 20),
pw.Text('Account Statement - KCCB',
style: pw.TextStyle(
fontSize: 24, fontWeight: pw.FontWeight.bold)),
]),
pw.SizedBox(height: 20),
pw.Row(
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
children: [
pw.Text('Account Number: ${widget.accountNo}',
style:
pw.TextStyle(font: pw.Font.ttf(rubik), fontSize: 15)),
pw.Text('Account Type: ${widget.accountType}',
style: pw.TextStyle(
fontSize: 15,
font: pw.Font.ttf(rubik),
)),
]),
pw.SizedBox(height: 20),
pw.Table(border: pw.TableBorder.all(), columnWidths: {
0: const pw.FractionColumnWidth(0.2),
1: const pw.FractionColumnWidth(0.45),
2: const pw.FractionColumnWidth(0.15),
3: const pw.FractionColumnWidth(0.20),
}, children: [
pw.TableRow(
children: [
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('Date')),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('Description', softWrap: true)),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('Amount')),
pw.Padding(
padding: const pw.EdgeInsets.all(4),
child: pw.Text('Balance')),
],
),
..._transactions.map<pw.TableRow>((tx) {
return pw.TableRow(children: [
pw.Padding(
padding: const pw.EdgeInsets.all(10),
child: pw.Text(tx.date ?? '',
style: pw.TextStyle(
fontSize: 12,
font: pw.Font.ttf(rubik),
))),
pw.Padding(
padding: const pw.EdgeInsets.all(10),
child: pw.Text(tx.name ?? '',
style: pw.TextStyle(
fontSize: 12, font: pw.Font.ttf(rubik)))),
pw.Padding(
padding: const pw.EdgeInsets.all(10),
child: pw.Text("${tx.amount} ${tx.type}",
style: pw.TextStyle(
fontSize: 12,
font: pw.Font.ttf(rubik),
))),
pw.Padding(
padding: const pw.EdgeInsets.all(10),
child: pw.Text("${tx.balance} ${tx.balanceType}",
style: pw.TextStyle(
fontSize: 12,
font: pw.Font.ttf(rubik),
))),
]);
}),
])
];
},
footer: (pw.Context context) {
return pw.Container(
alignment: pw.Alignment.centerRight,
margin: const pw.EdgeInsets.only(top: 10),
child: pw.Text(
'Kangra Central Co-Operative Bank Pvt Ltd. ©. All rights reserved.',
style: pw.TextStyle(
font: pw.Font.ttf(rubik),
fontSize: 8,
),
),
);
}));
//Logic For all platforms
try {
final Uint8List pdfBytes = await pdf.save();
final String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final String fileName = 'account_statement_$timestamp.pdf';
// For Android
if (Platform.isAndroid) {
final androidInfo = await DeviceInfoPlugin().androidInfo;
if (androidInfo.version.sdkInt < 29) {
final status = await Permission.storage.status;
if (status.isDenied) {
final result = await Permission.storage.request();
if (result.isDenied) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Storage permission is required to save PDF'),
),
);
}
return;
}
}
}
final directory = Directory('/storage/emulated/0/Download');
final file = File('${directory.path}/$fileName');
await file.writeAsBytes(pdfBytes);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('PDF saved to: ${file.path}'),
),
);
}
}
// Add for IOS
else if (Platform.isIOS) {
// On iOS, we save to a temporary directory and then open the share sheet.
final tempDir = await getTemporaryDirectory();
final file = await File('${tempDir.path}/$fileName').create();
await file.writeAsBytes(pdfBytes);
// Use share_plus to open the iOS share dialog
await Share.shareXFiles(
[XFile(file.path)],
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error saving PDF: $e'),
),
);
}
}
}
Widget buildDateBox(String label, DateTime? date) { Widget buildDateBox(String label, DateTime? date) {
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: Colors.grey), border:
Border.all(color: Theme.of(context).colorScheme.onSurfaceVariant),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: Row( child: Row(
@@ -344,7 +529,9 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
date != null ? _formatDate(date) : label, date != null ? _formatDate(date) : label,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: date != null ? Colors.black : Colors.grey, color: date != null
? Theme.of(context).colorScheme.onSurface
: Theme.of(context).colorScheme.onSurfaceVariant,
), ),
), ),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),

View File

@@ -66,13 +66,15 @@ class TransactionDetailsScreen extends StatelessWidget {
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),
], ],
), ),
), ),

View File

@@ -12,6 +12,10 @@ class AuthCubit extends Cubit<AuthState> {
checkAuthStatus(); checkAuthStatus();
} }
void reset() {
emit(AuthInitial());
}
Future<void> checkAuthStatus() async { Future<void> checkAuthStatus() async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
@@ -27,6 +31,10 @@ class AuthCubit extends Cubit<AuthState> {
} }
} }
void startVerification() {
emit(AuthVerificationInProgress());
}
Future<void> refreshUserData() async { Future<void> refreshUserData() async {
try { try {
// emit(AuthLoading()); // emit(AuthLoading());

View File

@@ -10,6 +10,8 @@ class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {} class AuthLoading extends AuthState {}
class AuthVerificationInProgress extends AuthState {}
class Authenticated extends AuthState { class Authenticated extends AuthState {
final List<User> users; final List<User> users;

View File

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

@@ -0,0 +1,11 @@
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,6 +12,27 @@ 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

@@ -1,13 +1,8 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/l10n/app_localizations.dart';
import '../../../app.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@@ -22,7 +17,12 @@ 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
void initState() {
super.initState();
context.read<AuthCubit>().reset();
}
@override @override
void dispose() { void dispose() {
@@ -33,10 +33,14 @@ class LoginScreenState extends State<LoginScreen>
void _submitForm() { void _submitForm() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login( Navigator.of(context).push(
_customerNumberController.text.trim(), MaterialPageRoute(
_passwordController.text, builder: (_) => VerificationScreen(
); customerNo: _customerNumberController.text.trim(),
password: _passwordController.text,
),
),
);
} }
} }
@@ -44,199 +48,141 @@ class LoginScreenState extends State<LoginScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// appBar: AppBar(title: const Text('Login')), // appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>( body: Padding(
listener: (context, state) async { padding: const EdgeInsets.all(24.0),
if (state is Authenticated) { child: Form(
final storage = getIt<SecureStorage>(); key: _formKey,
final mpin = await storage.read('mpin'); child: Column(
if (!context.mounted) return; mainAxisAlignment: MainAxisAlignment.center,
if (mpin == null) { children: [
Navigator.of(context).pushReplacement( Image.asset(
MaterialPageRoute( 'assets/images/logo.png',
builder: (_) => MPinScreen( width: 150,
mode: MPinMode.set, height: 150,
onCompleted: (_) { errorBuilder: (context, error, stackTrace) {
Navigator.of( return Icon(
context, Icons.account_balance,
rootNavigator: true, size: 100,
).pushReplacement( color: Theme.of(context).primaryColor,
MaterialPageRoute( );
builder: (_) => const NavigationScaffold(), },
), ),
); const SizedBox(height: 16),
// Title
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,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterUsername;
}
return null;
},
),
const SizedBox(height: 24),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
}, },
), ),
), ),
); validator: (value) {
} else { if (value == null || value.isEmpty) {
Navigator.of(context).pushReplacement( return AppLocalizations.of(context).pleaseEnterPassword;
MaterialPageRoute(builder: (_) => const NavigationScaffold()), }
); return null;
} },
} else if (state is AuthError) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.message)));
}
},
builder: (context, state) {
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),
// Title
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,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterUsername;
}
return null;
},
),
const SizedBox(height: 24),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterPassword;
}
return null;
},
),
const SizedBox(height: 24),
//Login Button
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: 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: const BorderSide(color: Colors.black, width: 1),
elevation: 0,
),
child: state is AuthLoading
? const CircularProgressIndicator()
: Text(
AppLocalizations.of(context).login,
style: const TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 15),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(AppLocalizations.of(context).or),
),
const Expanded(child: Divider()),
],
),
),
const SizedBox(height: 25),
// Register Button
SizedBox(
width: 250,
child: ElevatedButton(
//disable until registration is implemented
onPressed: null,
style: OutlinedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColorLight,
foregroundColor: Colors.black,
),
child: Text(AppLocalizations.of(context).register),
),
),
],
), ),
), const SizedBox(height: 24),
); //Login Button
}, SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorDark,
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1),
elevation: 0,
),
child: Text(
AppLocalizations.of(context).login,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
),
const SizedBox(height: 15),
const SizedBox(height: 25),
],
),
),
), ),
); );
} }
} }

View File

@@ -1,12 +1,14 @@
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/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 }
@@ -26,22 +28,73 @@ class MPinScreen extends StatefulWidget {
State<MPinScreen> createState() => _MPinScreenState(); State<MPinScreen> createState() => _MPinScreenState();
} }
class _MPinScreenState extends State<MPinScreen> { class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
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
_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) { if (widget.mode == MPinMode.enter) {
_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) {
@@ -62,11 +115,13 @@ class _MPinScreenState extends State<MPinScreen> {
} }
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();
} }
@@ -74,6 +129,7 @@ class _MPinScreenState extends State<MPinScreen> {
} }
void deleteDigit() { void deleteDigit() {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.isNotEmpty) { if (mPin.isNotEmpty) {
setState(() { setState(() {
mPin.removeLast(); mPin.removeLast();
@@ -83,19 +139,36 @@ class _MPinScreenState extends State<MPinScreen> {
} }
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)) {
widget.onCompleted?.call(pin); // Correct PIN
} else {
setState(() { setState(() {
_isSuccess = true;
errorText = null;
});
await Future.delayed(const Duration(milliseconds: 100));
_waveController.forward(from: 0).whenComplete(() {
widget.onCompleted?.call(pin);
});
} else {
// Incorrect PIN
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;
@@ -126,28 +199,81 @@ class _MPinScreenState extends State<MPinScreen> {
} }
} 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() { Widget buildMPinDots(BuildContext context) {
return Row( return AnimatedBuilder(
mainAxisAlignment: MainAxisAlignment.center, animation: Listenable.merge(
children: List.generate(4, (index) { [_bounceController, _shakeController, _waveController]),
return Container( builder: (context, child) {
margin: const EdgeInsets.all(8), double shakeOffset = 0;
width: 15, if (_shakeController.isAnimating) {
height: 15, // 4 cycles of sine wave for shake
decoration: BoxDecoration( shakeOffset = sin(_shakeController.value * 4 * pi) * 12;
shape: BoxShape.circle, }
color: index < mPin.length ? Colors.black : Colors.grey[400],
return Transform.translate(
offset: Offset(shakeOffset, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
final isFilled = index < mPin.length;
// 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),
width: 25,
height: 25,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: dotColor,
),
),
);
}),
), ),
); );
}), },
); );
} }
@@ -165,9 +291,10 @@ class _MPinScreenState extends State<MPinScreen> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) { children: row.map((key) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(1.0),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
HapticFeedback.lightImpact();
if (key == '<') { if (key == '<') {
deleteDigit(); deleteDigit();
} else if (key == 'Enter') { } else if (key == 'Enter') {
@@ -185,11 +312,10 @@ class _MPinScreenState extends State<MPinScreen> {
} }
}, },
child: Container( child: Container(
width: 70, width: 80,
height: 70, height: 80,
decoration: BoxDecoration( decoration: const BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.grey[200],
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: key == 'Enter' child: key == 'Enter'
@@ -197,10 +323,10 @@ class _MPinScreenState extends State<MPinScreen> {
: Text( : Text(
key == '<' ? '' : key, key == '<' ? '' : key,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 30,
color: key == 'Enter' color: key == 'Enter'
? Theme.of(context).primaryColor ? Theme.of(context).primaryColor
: Colors.black, : Theme.of(context).colorScheme.onSurface,
), ),
), ),
), ),
@@ -229,7 +355,7 @@ class _MPinScreenState extends State<MPinScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
const Spacer(), const SizedBox(height: 50),
// 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),
@@ -237,19 +363,18 @@ class _MPinScreenState extends State<MPinScreen> {
getTitle(), getTitle(),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 20), const Spacer(),
buildMPinDots(), buildMPinDots(context),
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: const TextStyle(color: Colors.red), style: TextStyle(color: Theme.of(context).colorScheme.error),
), ),
), ),
const Spacer(), const Spacer(),
buildNumberPad(), buildNumberPad(),
const Spacer(),
], ],
), ),
), ),

View File

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

@@ -0,0 +1,131 @@
// lib/features/auth/screens/sms_verification_helper.dart
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> _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(),
),
TextButton(
child: const Text("Open Settings"),
onPressed: () {
openAppSettings();
Navigator.of(context).pop();
},
),
],
),
);
}
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
),
);
}
Future<void> initiateSmsSequence({
required BuildContext context,
}) async {
bool hasPermission = false;
// --- PERMISSION LOOP ---
while (!hasPermission) {
final status = await _smsService.handleSmsPermission();
switch (status) {
case PermissionStatusResult.granted:
_showSnackBar(context, "Permissions Granted! Proceeding...");
hasPermission = true; // This will break the loop
break;
case PermissionStatusResult.denied:
_showSnackBar(context, "SMS and Phone permissions are required. Please try again.");
await Future.delayed(const Duration(seconds: 3));
break;
case PermissionStatusResult.permanentlyDenied:
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();
},
),
],
),
);
// Wait for user to return from settings
await Future.delayed(const Duration(seconds: 5));
break;
case PermissionStatusResult.restricted:
await _showRestrictedSmsDialog(context);
// Wait for user to return from settings
await Future.delayed(const Duration(seconds: 10));
break;
}
}
// --- SMS SENDING LOOP ---
bool isSmsSent = false;
while (!isSmsSent) {
var uuid = const Uuid();
String uniqueId = uuid.v4();
String smsMessage = uniqueId;
_showSnackBar(context, "Attempting to send verification SMS...");
isSmsSent = await _smsService.sendVerificationSms(
context: context,
destinationNumber: '9580079717', // Replace with your number
message: smsMessage,
);
if (isSmsSent) {
_showSnackBar(context, "SMS sent successfully! Proceeding to login.");
break;
} else {
_showSnackBar(context, "SMS failed to send. Retrying in 5 seconds...");
await Future.delayed(const Duration(seconds: 5));
}
}
}
}

View File

@@ -1,62 +0,0 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/kconnect2.webp',
fit: BoxFit.cover,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).kconnect,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
),
),
const SizedBox(height: 12),
Text(
AppLocalizations.of(context).kccBankFull,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18,
color: Color(0xFFFFFFFF),
letterSpacing: 1.2,
),
),
],
),
),
Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: CircularProgressIndicator(
color: Theme.of(context).scaffoldBackgroundColor),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,146 @@
// lib/features/auth/screens/verification_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
import '../../../app.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> {
final SmsVerificationHelper _smsVerificationHelper = SmsVerificationHelper();
late Timer _timer;
int _start = 120;
String _message = "Attempting verification...";
String? _error;
@override
void initState() {
super.initState();
context.read<AuthCubit>().startVerification();
startTimer();
_verifySmsAndLogin();
}
void startTimer() {
const oneSec = Duration(seconds: 1);
_timer = Timer.periodic(
oneSec,
(Timer timer) {
if (_start == 0) {
timer.cancel();
if (mounted) {
setState(() {
_error = "Verification timed out.";
});
}
} else {
setState(() {
_start--;
});
}
},
);
}
Future<void> _verifySmsAndLogin() async {
await _smsVerificationHelper.initiateSmsSequence(context: context);
// After SMS sequence completes, proceed with login
_timer.cancel(); // Stop the timer
if (mounted) {
context.read<AuthCubit>().login(widget.customerNo, widget.password);
}
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocListener<AuthCubit, AuthState>(
listenWhen: (previous, current) {
return current is! AuthVerificationInProgress && current is! AuthInitial;
},
listener: (context, state) {
if (state is Authenticated) {
_timer.cancel();
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(
context,
rootNavigator: true,
).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
},
),
),
);
} else if (state is AuthError) {
_timer.cancel();
setState(() {
_error = state.message;
});
}
},
child: Center(
child: _error != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 80),
const SizedBox(height: 16),
Text(
_error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red, fontSize: 18),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Back to Login"),
)
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_message),
const SizedBox(height: 16),
Text("Time remaining: $_start seconds"),
],
),
),
),
);
}
}

View File

@@ -1,11 +1,10 @@
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 'beneficiary_result_page.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'beneficiary_result_page.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';
@@ -24,7 +23,9 @@ 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();
@@ -33,6 +34,7 @@ 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;
@@ -44,6 +46,11 @@ 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';
@@ -51,23 +58,54 @@ 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;
final result = await beneficiaryService.validateIFSC(ifsc); try {
final result = await beneficiaryService.validateIFSC(ifsc);
if (mounted) {
if (result.bankName.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
);
bankNameController.clear();
branchNameController.clear();
} else {
bankNameController.text = result.bankName;
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;
}
if (mounted) {
if (result.bankName == '') {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)), SnackBar(content: Text(snackbarMessage)),
); );
bankNameController.clear(); bankNameController.clear();
branchNameController.clear(); branchNameController.clear();
} else {
bankNameController.text = result.bankName;
branchNameController.text = result.branchName;
} }
} }
} }
@@ -220,33 +258,10 @@ 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: Form( child: Form(
@@ -261,6 +276,7 @@ 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(
@@ -269,18 +285,6 @@ 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,
@@ -303,6 +307,7 @@ 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(
@@ -311,18 +316,6 @@ 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,
@@ -341,8 +334,9 @@ 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: [
@@ -352,31 +346,18 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
labelText: AppLocalizations.of(context).ifscCode, labelText: 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) {
final trimmed = value.trim().toUpperCase(); setState(() {
if (trimmed.length < 11) { final trimmed = value.trim().toUpperCase();
// clear bank/branch if backspace or changed if (trimmed.length < 11) {
bankNameController.clear(); // clear bank/branch if backspace or changed
branchNameController.clear(); bankNameController.clear();
} branchNameController.clear();
}
});
}, },
validator: (value) { validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$'); final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
@@ -393,7 +374,7 @@ 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: false, // changed from readOnly to disabled enabled: false, // changed from readOnly to disabled
@@ -401,18 +382,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
labelText: AppLocalizations.of(context).bankName, labelText: 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),
@@ -424,19 +393,38 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
labelText: AppLocalizations.of(context).branchName, labelText: 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)
Padding( Padding(
@@ -444,18 +432,25 @@ 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
: () { : () {
if (confirmAccountNumberController final isAccountValid =
.text == _accountNumberFieldKey.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
@@ -471,30 +466,6 @@ 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,
@@ -502,18 +473,6 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
labelText: AppLocalizations.of(context).accountType, labelText: 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',
@@ -542,18 +501,6 @@ 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) => validator: (value) =>
@@ -568,16 +515,22 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox( child: SizedBox(
width: 250, width: 250,
child: ElevatedButton( child: ElevatedButton(
onPressed: validateAndAddBeneficiary, onPressed: validateAndAddBeneficiary,
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),
), ),
), ),
), ),

View File

@@ -65,7 +65,6 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
TextButton( TextButton(
child: const Text('Delete'), child: const Text('Delete'),
onPressed: () { onPressed: () {
//Navigator.of(context).pop();
_deleteBeneficiary(context); _deleteBeneficiary(context);
}, },
), ),
@@ -81,59 +80,61 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiarydetails), title: Text(AppLocalizations.of(context).beneficiarydetails),
), ),
body: Padding( body: SafeArea(
padding: const EdgeInsets.all(16.0), child: Padding(
child: Column( padding: const EdgeInsets.all(16.0),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Row( children: [
children: [ Row(
CircleAvatar( children: [
radius: 24, CircleAvatar(
backgroundColor: Colors.transparent, radius: 24,
child: getBankLogo(beneficiary.bankName), backgroundColor: Colors.transparent,
), child: getBankLogo(beneficiary.bankName, context),
const SizedBox(width: 16), ),
Text( const SizedBox(width: 16),
beneficiary.name, Text(
style: const TextStyle( beneficiary.name,
fontSize: 20, fontWeight: FontWeight.bold), style: const TextStyle(
), fontSize: 20, fontWeight: FontWeight.bold),
], ),
), ],
const SizedBox(height: 24), ),
_buildDetailRow('${AppLocalizations.of(context).bankName} ', const SizedBox(height: 24),
beneficiary.bankName ?? 'N/A'), _buildDetailRow('${AppLocalizations.of(context).bankName} ',
_buildDetailRow('${AppLocalizations.of(context).accountNumber} ', beneficiary.bankName ?? 'N/A'),
beneficiary.accountNo), _buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
_buildDetailRow('${AppLocalizations.of(context).accountType} ', beneficiary.accountNo),
beneficiary.accountType), _buildDetailRow('${AppLocalizations.of(context).accountType} ',
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ', beneficiary.accountType),
beneficiary.ifscCode), _buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
_buildDetailRow('${AppLocalizations.of(context).branchName} ', beneficiary.ifscCode),
beneficiary.branchName ?? 'N/A'), _buildDetailRow('${AppLocalizations.of(context).branchName} ',
const Spacer(), beneficiary.branchName ?? 'N/A'),
Row( const Spacer(),
mainAxisAlignment: MainAxisAlignment.spaceEvenly, Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// ElevatedButton.icon( children: [
// onPressed: () { // ElevatedButton.icon(
// // Set Transaction Limit for this beneficiary // onPressed: () {
// }, // // Set Transaction Limit for this beneficiary
// icon: const Icon(Icons.currency_rupee), // },
// label: const Text('Set Limit'), // icon: const Icon(Icons.currency_rupee),
// ), // label: const Text('Set Limit'),
ElevatedButton.icon( // ),
onPressed: () { ElevatedButton.icon(
// Delete beneficiary option onPressed: () {
_showDeleteConfirmationDialog(context); // Delete beneficiary option
}, _showDeleteConfirmationDialog(context);
icon: const Icon(Icons.delete), },
label: Text(AppLocalizations.of(context).delete), icon: const Icon(Icons.delete),
), label: Text(AppLocalizations.of(context).delete),
], ),
), ],
], ),
],
),
), ),
), ),
); );

View File

@@ -90,7 +90,10 @@ class _BeneficiaryResultPageState extends State<BeneficiaryResultPage> {
), ),
child: Text( child: Text(
AppLocalizations.of(context).done, AppLocalizations.of(context).done,
style: const TextStyle(fontSize: 18), // slightly bigger text style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer), // slightly bigger text
), ),
), ),
), ),

View File

@@ -75,7 +75,7 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
leading: CircleAvatar( leading: CircleAvatar(
radius: 24, radius: 24,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: getBankLogo(item.bankName), child: getBankLogo(item.bankName, context),
), ),
title: Text(item.name), title: Text(item.name),
subtitle: Column( subtitle: Column(
@@ -122,8 +122,6 @@ 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,8 +1,6 @@
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});
@@ -56,33 +54,12 @@ 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: style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), 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: Padding( body: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),

View File

@@ -1,5 +1,6 @@
// 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';
@@ -21,25 +22,8 @@ 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: ListView( body: ListView(
children: [ children: [
@@ -47,6 +31,7 @@ class _CardManagementScreen extends State<CardManagementScreen> {
icon: Symbols.add, icon: Symbols.add,
label: AppLocalizations.of(context).applyDebitCard, label: AppLocalizations.of(context).applyDebitCard,
onTap: () {}, onTap: () {},
disabled: true, // Add this
), ),
const Divider(height: 1), const Divider(height: 1),
CardManagementTile( CardManagementTile(
@@ -60,22 +45,24 @@ class _CardManagementScreen extends State<CardManagementScreen> {
), ),
); );
}, },
disabled: true,
), ),
const Divider(height: 1), 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,
), ),
const Divider(height: 1), const Divider(height: 1),
CardManagementTile( CardManagementTile(
icon: Symbols.payment_card, icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils, label: AppLocalizations.of(context).viewCardDeatils,
onTap: () { onTap: () {
@@ -86,6 +73,7 @@ class _CardManagementScreen extends State<CardManagementScreen> {
), ),
); );
}, },
disabled: true,
), ),
const Divider(height: 1), const Divider(height: 1),
], ],
@@ -94,26 +82,40 @@ class _CardManagementScreen extends State<CardManagementScreen> {
} }
} }
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(icon), leading: Icon(
title: Text(label), icon,
trailing: const Icon(Symbols.arrow_right, size: 20), color: disabled ? theme.disabledColor : null,
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,8 +1,6 @@
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 {
@@ -46,33 +44,12 @@ 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: style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), 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: Padding( body: Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),

View File

@@ -1,6 +1,4 @@
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 {
@@ -46,33 +44,12 @@ 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: style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), 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: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),

View File

@@ -1,5 +1,4 @@
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';
@@ -16,33 +15,12 @@ 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: style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), 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: ListView( body: ListView(
children: [ children: [

View File

@@ -13,13 +13,24 @@ 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;
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).customerInfo, AppLocalizations.of(context)
.customerInfo
.replaceFirst(RegExp('\n'), ''),
), ),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
@@ -83,7 +94,7 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
), // Replace with Aadhar if available ), // Replace with Aadhar if available
InfoField( InfoField(
label: AppLocalizations.of(context).primaryId, label: AppLocalizations.of(context).primaryId,
value: user.primaryId ?? 'N/A', value: _maskPrimaryId(user.primaryId),
), // Replace with PAN if available ), // Replace with PAN if available
], ],
), ),

View File

@@ -30,7 +30,8 @@ class DashboardScreen extends StatefulWidget {
State<DashboardScreen> createState() => _DashboardScreenState(); State<DashboardScreen> createState() => _DashboardScreenState();
} }
class _DashboardScreenState extends State<DashboardScreen> { class _DashboardScreenState extends State<DashboardScreen>
with SingleTickerProviderStateMixin {
int selectedAccountIndex = 0; int selectedAccountIndex = 0;
bool isVisible = false; bool isVisible = false;
bool isRefreshing = false; bool isRefreshing = false;
@@ -98,7 +99,7 @@ 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.primaryColor, baseColor: theme.colorScheme.primary,
highlightColor: theme.colorScheme.onPrimary, highlightColor: theme.colorScheme.onPrimary,
child: Container( child: Container(
width: 200, height: 42, color: theme.scaffoldBackgroundColor), width: 200, height: 42, color: theme.scaffoldBackgroundColor),
@@ -138,6 +139,8 @@ 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':
@@ -209,41 +212,51 @@ class _DashboardScreenState extends State<DashboardScreen> {
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
automaticallyImplyLeading: false, leading: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 &&
selectedAccountIndex < authState.users.length) {
mobileNumberToPass =
authState.users[selectedAccountIndex].mobileNo ?? '';
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileScreen(mobileNumber: mobileNumberToPass),
),
);
},
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
),
title: Text( title: Text(
AppLocalizations.of(context).kconnect, AppLocalizations.of(context).kccbMobile,
textAlign: TextAlign.left,
style: TextStyle( style: TextStyle(
color: theme.primaryColor, color: theme.colorScheme.primary,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
centerTitle: true, centerTitle: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfileScreen(),
),
);
},
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
),
],
), ),
body: BlocBuilder<AuthCubit, AuthState>( body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) { builder: (context, state) {
@@ -275,7 +288,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
"${AppLocalizations.of(context).hi} $firstName!", "${AppLocalizations.of(context).hi} $firstName!",
style: GoogleFonts.baumans().copyWith( style: GoogleFonts.baumans().copyWith(
fontSize: 20, fontSize: 20,
color: theme.primaryColor, color: theme.colorScheme.primary,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
@@ -289,7 +302,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
vertical: 10, vertical: 10,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: theme.primaryColor, color: Color(0xFF01A04C),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@@ -307,7 +320,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
DropdownButton<int>( DropdownButton<int>(
value: selectedAccountIndex, value: selectedAccountIndex,
dropdownColor: theme.primaryColor, dropdownColor: theme.colorScheme.primary,
underline: const SizedBox(), underline: const SizedBox(),
icon: const Icon(Icons.keyboard_arrow_down), icon: const Icon(Icons.keyboard_arrow_down),
iconEnabledColor: theme.colorScheme.onPrimary, iconEnabledColor: theme.colorScheme.onPrimary,
@@ -521,6 +534,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
.accountNo!, .accountNo!,
balance: users[selectedAccountIndex] balance: users[selectedAccountIndex]
.availableBalance!, .availableBalance!,
accountType:
users[selectedAccountIndex]
.accountType!,
))); )));
}), }),
_buildQuickLink(Symbols.checkbook, _buildQuickLink(Symbols.checkbook,
@@ -573,7 +589,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
subtitle: Text( subtitle: Text(
tx.name != null tx.name != null
? (tx.name!.length > 18 ? (tx.name!.length > 22
? tx.name!.substring(0, 22) ? tx.name!.substring(0, 22)
: tx.name!) : tx.name!)
: '', : '',
@@ -602,7 +618,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
AppLocalizations.of(context).noTransactions, AppLocalizations.of(context).noTransactions,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey[600], color: Theme.of(context).colorScheme.outline,
), ),
), ),
), ),
@@ -663,8 +679,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
icon, icon,
size: 30, size: 30,
color: disable color: disable
? theme.colorScheme.surfaceContainerHighest ? theme.colorScheme.onSurface.withOpacity(0.3)
: theme.primaryColor, : theme.colorScheme.primary,
grade: 200, grade: 200,
weight: 700, weight: 700,
), ),

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: Theme.of(context).primaryColor, color: Colors.white,
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).scaffoldBackgroundColor, color: Theme.of(context).colorScheme.onPrimary,
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).scaffoldBackgroundColor, color: Theme.of(context).colorScheme.onPrimary,
), ),
], ],
), ),
@@ -50,13 +50,13 @@ class AccountCard extends StatelessWidget {
Text( Text(
account.accountNumber, account.accountNumber,
style: TextStyle( style: TextStyle(
color: Theme.of(context).scaffoldBackgroundColor, fontSize: 16), color: Theme.of(context).colorScheme.onPrimary, 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).scaffoldBackgroundColor, color: Theme.of(context).colorScheme.onPrimary,
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).scaffoldBackgroundColor, fontSize: 16), color: Theme.of(context).colorScheme.onPrimary, fontSize: 16),
), ),
], ],
), ),

View File

@@ -1,8 +1,6 @@
// ignore_for_file: use_build_context_synchronously // 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';
@@ -32,16 +30,27 @@ class _EnquiryScreen extends State<EnquiryScreen> {
} }
} }
Future<void> _launchUrl(String url) async {
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri);
} else {
// Consider adding a 'urlLaunchError' key to your AppLocalizations
debugPrint('Could not launch $url');
}
}
Widget _buildContactItem(String role, String email, String phone) { Widget _buildContactItem(String role, String email, String phone) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(role, style: const TextStyle(color: Colors.grey)), Text(role,
style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
const SizedBox(height: 4), const SizedBox(height: 4),
GestureDetector( GestureDetector(
onTap: () => _launchEmailAddress(email), onTap: () => _launchEmailAddress(email),
child: Text(email, child: Text(email,
style: TextStyle(color: Theme.of(context).primaryColor)), style: TextStyle(color: Theme.of(context).colorScheme.primary)),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
GestureDetector( GestureDetector(
@@ -58,61 +67,43 @@ class _EnquiryScreen extends State<EnquiryScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( title: Text(AppLocalizations.of(context).enquiry),
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
AppLocalizations.of(context).enquiry,
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: Padding( 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: [
// … existing Mail us / Call us / Write to us …
const SizedBox(height: 20),
Text(
AppLocalizations.of(context).writeToUs,
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 4),
Text(
"complaint@kccb.in",
style: TextStyle(color: Theme.of(context).primaryColor),
),
const SizedBox(height: 20), const SizedBox(height: 20),
GestureDetector(
onTap: () => _launchUrl("https://kccb.in/complaint-form"),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(
"Complaint Form",
style: TextStyle(
fontSize: 17,
color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 4),
Icon(
Icons.open_in_new,
color: Theme.of(context).colorScheme.primary,
size: 16.0,
),
])),
const SizedBox(height: 40),
Text( Text(
AppLocalizations.of(context).keyContacts, AppLocalizations.of(context).keyContacts,
style: TextStyle( style: TextStyle(
fontSize: 17, fontSize: 17,
color: Theme.of(context).primaryColor, color: Theme.of(context).colorScheme.primary,
), ),
// horizontal line // horizontal line
), ),
Divider(color: Colors.grey[300]), Divider(color: Theme.of(context).colorScheme.outline),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildContactItem( _buildContactItem(
AppLocalizations.of(context).chairman, AppLocalizations.of(context).chairman,

View File

@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -17,6 +19,7 @@ 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 {
@@ -38,14 +41,70 @@ class FundTransferAmountScreen extends StatefulWidget {
_FundTransferAmountScreenState(); _FundTransferAmountScreenState();
} }
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();
} }
@@ -112,6 +171,7 @@ 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>();
@@ -179,6 +239,7 @@ 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>();
@@ -235,6 +296,7 @@ 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>();
@@ -295,140 +357,159 @@ 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), title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
), ),
body: Padding( body: SafeArea(
padding: const EdgeInsets.all(16.0), child: Padding(
child: Form( padding: const EdgeInsets.all(16.0),
key: _formKey, child: Form(
child: Column( key: _formKey,
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
// Debit Account (User) children: [
Text( // Debit Account (User)
loc.debitFrom,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: Image.asset(
'assets/images/logo.png',
width: 40,
height: 40,
),
title: Text(widget.remitterName),
subtitle: Text(widget.debitAccountNo),
),
),
const SizedBox(height: 24),
// Credit Account (Beneficiary)
Text(
AppLocalizations.of(context).creditedTo,
style: Theme.of(context).textTheme.titleSmall,
),
Card(
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading: getBankLogo(widget.creditBeneficiary.bankName),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
),
const SizedBox(height: 24),
if (!widget.isOwnBank) ...[
// Transaction Mode Selection
Text( Text(
AppLocalizations.of(context).selectTransactionType, loc.debitFrom,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleSmall,
), ),
const SizedBox(height: 12), Card(
Container( elevation: 0,
decoration: BoxDecoration( margin: const EdgeInsets.symmetric(vertical: 8.0),
color: Theme.of(context).cardColor, child: ListTile(
borderRadius: BorderRadius.circular(12), leading: Image.asset(
border: Border.all(color: Colors.grey.shade300), 'assets/images/logo.png',
), width: 40,
child: ToggleButtons( height: 40,
isSelected: [ ),
_selectedMode == TransactionMode.neft, title: Text(widget.remitterName),
_selectedMode == TransactionMode.rtgs, subtitle: Text(widget.debitAccountNo),
_selectedMode == TransactionMode.imps,
],
onPressed: (index) {
setState(() {
_selectedMode = TransactionMode.values[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor: Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).primaryColor,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent,
splashColor:
Theme.of(context).primaryColor,
highlightColor:
Theme.of(context).primaryColor,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).neft),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).rtgs),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).imps),
),
],
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
],
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return loc.amountRequired;
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return loc.validAmount;
}
return null;
},
),
const Spacer(),
// Proceed Button // Credit Account (Beneficiary)
SizedBox( Text(
width: double.infinity, AppLocalizations.of(context).creditedTo,
child: ElevatedButton( style: Theme.of(context).textTheme.titleSmall,
onPressed: _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).proceed),
), ),
), Card(
], elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading:
getBankLogo(widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
),
const SizedBox(height: 24),
if (!widget.isOwnBank) ...[
// Transaction Mode Selection
Text(
AppLocalizations.of(context).selectTransactionType,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 12),
Container(
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: ToggleButtons(
isSelected: [
_selectedMode == TransactionMode.neft,
_selectedMode == TransactionMode.rtgs,
_selectedMode == TransactionMode.imps,
],
onPressed: (index) {
setState(() {
_selectedMode = TransactionMode.values[index];
});
},
borderRadius: BorderRadius.circular(10),
selectedColor: Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
selectedBorderColor: Colors.transparent,
splashColor: Theme.of(context).colorScheme.primary,
highlightColor: Theme.of(context).colorScheme.primary,
children: [
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).neft),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).rtgs),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0, vertical: 12.0),
child: Text(AppLocalizations.of(context).imps),
),
],
),
),
const SizedBox(height: 24),
],
//Remarks
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
// Amount
TextFormField(
controller: _amountController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: loc.amount,
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.currency_rupee),
),
validator: (value) {
if (value == null || value.isEmpty) {
return loc.amountRequired;
}
if (double.tryParse(value) == null ||
double.parse(value) <= 0) {
return loc.validAmount;
}
return null;
},
),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).proceed),
),
),
const SizedBox(height: 10),
],
),
), ),
), ),
), ),

View File

@@ -39,9 +39,8 @@ class _FundTransferBeneficiaryScreenState
setState(() { setState(() {
_beneficiaries = data _beneficiaries = data
.where((b) => widget.isOwnBank .where((b) => widget.isOwnBank
? b.bankName == ? b.bankName!.toLowerCase().contains('kangra central')
'THE KANGRA CENTRAL CO-OP BANK LIMITED' // Assuming 'KCCB' is your bank's name : !b.bankName!.toLowerCase().contains('kangra central'))
: b.bankName != 'THE KANGRA CENTRAL CO-OP BANK LIMITED')
.toList(); .toList();
_isLoading = false; _isLoading = false;
}); });
@@ -86,7 +85,7 @@ class _FundTransferBeneficiaryScreenState
leading: CircleAvatar( leading: CircleAvatar(
radius: 24, radius: 24,
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName), child: getBankLogo(beneficiary.bankName, context),
), ),
title: Text(beneficiary.name), title: Text(beneficiary.name),
subtitle: Column( subtitle: Column(

View File

@@ -17,7 +17,9 @@ class FundTransferScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).fundTransfer), title: Text(AppLocalizations.of(context)
.fundTransfer
.replaceFirst(RegExp('\n'), '')),
), ),
body: ListView( body: ListView(
children: [ children: [

View File

@@ -209,26 +209,22 @@ class _PaymentAnimationScreenState extends State<PaymentAnimationScreen> {
onPressed: _shareScreenshot, onPressed: _shareScreenshot,
icon: Icon( icon: Icon(
Icons.share_rounded, Icons.share_rounded,
color: Theme.of(context).primaryColor, color: Theme.of(context).colorScheme.primary,
), ),
label: Text( label: Text(
AppLocalizations.of(context).share, AppLocalizations.of(context).share,
style: TextStyle(color: Theme.of(context).primaryColor), style: TextStyle(
color: Theme.of(context).colorScheme.primary),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 32, vertical: 12), horizontal: 45, vertical: 12),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide(
color: Theme.of(context).primaryColor, width: 1),
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
textStyle: const TextStyle( textStyle: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Colors.black,
), ),
), ),
), ),

View File

@@ -1,20 +1,26 @@
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
import 'package:kmobile/api/services/change_password_service.dart';
import 'package:kmobile/di/injection.dart';
class TpinOtpScreen extends StatefulWidget { class TpinOtpScreen extends StatefulWidget {
const TpinOtpScreen({super.key}); final String mobileNumber;
const TpinOtpScreen({super.key, required this.mobileNumber});
@override @override
State<TpinOtpScreen> createState() => _TpinOtpScreenState(); State<TpinOtpScreen> createState() => _TpinOtpScreenState();
} }
class _TpinOtpScreenState extends State<TpinOtpScreen> { class _TpinOtpScreenState extends State<TpinOtpScreen> {
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode()); final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
final List<TextEditingController> _controllers = List.generate( final List<TextEditingController> _controllers = List.generate(
4, 6,
(_) => TextEditingController(), (_) => TextEditingController(),
); );
bool _isLoading = false;
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
@override @override
void dispose() { void dispose() {
@@ -28,7 +34,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
} }
void _onOtpChanged(int idx, String value) { void _onOtpChanged(int idx, String value) {
if (value.length == 1 && idx < 3) { if (value.length == 1 && idx < 5) {
_focusNodes[idx + 1].requestFocus(); _focusNodes[idx + 1].requestFocus();
} }
if (value.isEmpty && idx > 0) { if (value.isEmpty && idx > 0) {
@@ -39,16 +45,35 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
String get _enteredOtp => _controllers.map((c) => c.text).join(); String get _enteredOtp => _controllers.map((c) => c.text).join();
void _verifyOtp() { void _verifyOtp() async {
if (_enteredOtp == '0000') { setState(() {
Navigator.pushReplacement( _isLoading = true;
context, });
MaterialPageRoute(builder: (_) => TpinSetScreen()),
); try {
} else { await _changePasswordService.validateOtp(
ScaffoldMessenger.of(context).showSnackBar( otp: _enteredOtp,
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)), mobileNumber: widget.mobileNumber,
); );
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const TpinSetScreen()),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
} }
} }
@@ -64,7 +89,6 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
// mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon( Icon(
Icons.lock_outline, Icons.lock_outline,
@@ -90,9 +114,9 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (i) { children: List.generate(6, (i) {
return Container( return Container(
width: 48, width: 32,
margin: const EdgeInsets.symmetric(horizontal: 8), margin: const EdgeInsets.symmetric(horizontal: 8),
child: TextField( child: TextField(
controller: _controllers[i], controller: _controllers[i],
@@ -101,7 +125,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLength: 1, maxLength: 1,
obscureText: true, obscureText: true,
obscuringCharacter: '', obscuringCharacter: '*',
decoration: InputDecoration( decoration: InputDecoration(
counterText: '', counterText: '',
filled: true, filled: true,
@@ -128,14 +152,23 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.verified_user_rounded), icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.verified_user_rounded),
label: Text( label: Text(
AppLocalizations.of(context).verifyOtp, AppLocalizations.of(context).verifyOtp,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600), fontSize: 18, fontWeight: FontWeight.w600),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary, backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 14, vertical: 14,
horizontal: 28, horizontal: 28,
@@ -144,12 +177,15 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
), ),
onPressed: _enteredOtp.length == 4 ? _verifyOtp : null, // Update onPressed to handle loading state
onPressed: (_enteredOtp.length == 6 && !_isLoading)
? _verifyOtp
: null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
onPressed: () { onPressed: () {
// Resend OTP logic here // You can also add a getOtp call here for resending
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context).otpResent), content: Text(AppLocalizations.of(context).otpResent),

View File

@@ -1,10 +1,65 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart';
import 'package:kmobile/api/services/change_password_service.dart'; // <-- Add this import
import 'package:kmobile/di/injection.dart'; // <-- Add this import
class TpinSetupPromptScreen extends StatelessWidget { class TpinSetupPromptScreen extends StatefulWidget {
const TpinSetupPromptScreen({super.key}); const TpinSetupPromptScreen({super.key});
@override
State<TpinSetupPromptScreen> createState() => _TpinSetupPromptScreenState();
}
class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> {
int selectedAccountIndex = 0;
bool _isLoading = false;
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
Future<void> _getOtp() async {
setState(() {
_isLoading = true;
});
try {
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 &&
selectedAccountIndex < authState.users.length) {
mobileNumberToPass =
authState.users[selectedAccountIndex].mobileNo ?? '';
}
}
await _changePasswordService.getOtpTpin(mobileNumber: mobileNumberToPass);
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => TpinOtpScreen(
mobileNumber: mobileNumberToPass,
)),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -38,14 +93,23 @@ class TpinSetupPromptScreen extends StatelessWidget {
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
icon: const Icon(Icons.arrow_forward_rounded), icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Icon(Icons.arrow_forward_rounded),
label: Text( label: Text(
AppLocalizations.of(context).setTpin, AppLocalizations.of(context).setTpin,
style: style:
const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary, backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 14, vertical: 14,
horizontal: 32, horizontal: 32,
@@ -54,12 +118,8 @@ class TpinSetupPromptScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
), ),
onPressed: () { onPressed:
Navigator.pushReplacement( _isLoading ? null : _getOtp, // <-- Use the new function
context,
MaterialPageRoute(builder: (_) => const TpinOtpScreen()),
);
},
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
Padding( Padding(

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:kmobile/api/services/auth_service.dart'; import 'package:kmobile/api/services/auth_service.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_prompt_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/tpin_prompt_screen.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -15,15 +15,40 @@ class TransactionPinScreen extends StatefulWidget {
State<TransactionPinScreen> createState() => _TransactionPinScreenState(); State<TransactionPinScreen> createState() => _TransactionPinScreenState();
} }
class _TransactionPinScreenState extends State<TransactionPinScreen> { class _TransactionPinScreenState extends State<TransactionPinScreen>
with SingleTickerProviderStateMixin {
final List<String> _pin = []; final List<String> _pin = [];
bool _loading = true; bool _loading = true;
bool _isSubmitting = false; bool _isSubmitting = false;
late final AnimationController _bounceController;
late final Animation<double> _bounceAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_checkIfTpinIsSet(); _checkIfTpinIsSet();
_bounceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 150),
);
_bounceAnimation = Tween<double>(begin: 1.0, end: 1.1).animate(
CurvedAnimation(
parent: _bounceController,
curve: Curves.elasticIn,
reverseCurve: Curves.elasticOut),
)..addStatusListener((status) {
if (status == AnimationStatus.completed) {
_bounceController.reverse();
}
});
}
@override
void dispose() {
_bounceController.dispose();
super.dispose();
} }
Future<void> _checkIfTpinIsSet() async { Future<void> _checkIfTpinIsSet() async {
@@ -58,6 +83,7 @@ class _TransactionPinScreenState extends State<TransactionPinScreen> {
if (_pin.isNotEmpty) _pin.removeLast(); if (_pin.isNotEmpty) _pin.removeLast();
} else if (_pin.length < 6) { } else if (_pin.length < 6) {
_pin.add(value); _pin.add(value);
_bounceController.forward(from: 0);
} }
}); });
} }
@@ -84,29 +110,46 @@ class _TransactionPinScreenState extends State<TransactionPinScreen> {
} }
Widget _buildPinIndicators() { Widget _buildPinIndicators() {
return Row( return AnimatedBuilder(
mainAxisAlignment: MainAxisAlignment.center, animation: _bounceAnimation,
children: List.generate(6, (index) { builder: (context, child) {
return Container( return Row(
margin: const EdgeInsets.symmetric(horizontal: 8), mainAxisAlignment: MainAxisAlignment.center,
width: 20, children: List.generate(6, (index) {
height: 20, final isAnimating =
decoration: BoxDecoration( index == _pin.length - 1 && _bounceController.isAnimating;
shape: BoxShape.circle, return Transform.scale(
border: Border.all(color: Theme.of(context).primaryColor, width: 2), scale: isAnimating ? _bounceAnimation.value : 1.0,
color: index < _pin.length child: Container(
? Theme.of(context).primaryColor margin: const EdgeInsets.symmetric(horizontal: 8),
: Colors.transparent, width: 30,
), height: 30,
); decoration: BoxDecoration(
}), shape: BoxShape.circle,
); color: index < _pin.length
? Theme.of(context).colorScheme.onSurfaceVariant
: Theme.of(context)
.colorScheme
.surfaceContainerHighest),
),
);
}),
);
});
} }
Widget _buildKey(String label, {IconData? icon}) { Widget _buildKey(String label, {IconData? icon}) {
return Expanded( return Expanded(
child: InkWell( child: InkWell(
onTap: () => label == 'done' ? _submitPin() : _onKeyPressed(label), onTap: () {
HapticFeedback.lightImpact();
if (label == 'done') {
_submitPin();
} else {
_onKeyPressed(label);
}
},
borderRadius: BorderRadius.circular(48),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 20), padding: const EdgeInsets.symmetric(vertical: 20),
child: Center( child: Center(
@@ -143,35 +186,27 @@ class _TransactionPinScreenState extends State<TransactionPinScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( title: Text(AppLocalizations.of(context).tpin),
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
AppLocalizations.of(context).tpin,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
centerTitle: false, centerTitle: false,
), ),
body: _loading body: _loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: Padding( : SafeArea(
padding: const EdgeInsets.only(bottom: 20.0), child: Padding(
child: Column( padding: const EdgeInsets.only(bottom: 20.0),
children: [ child: Column(
const Spacer(), children: [
Text( const Spacer(),
AppLocalizations.of(context).enterTpin, Text(
style: const TextStyle(fontSize: 18), AppLocalizations.of(context).enterTpin,
), style: const TextStyle(fontSize: 18),
const SizedBox(height: 20), ),
_buildPinIndicators(), const SizedBox(height: 50),
const Spacer(), _buildPinIndicators(),
_buildKeypad(), const Spacer(),
], _buildKeypad(),
],
),
), ),
), ),
); );

View File

@@ -67,13 +67,23 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
"On $transactionDate", "On $transactionDate",
style: const TextStyle(fontSize: 14, color: Colors.black54), style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6)),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
"${AppLocalizations.of(context).toAccountNumber}: $creditAccount", "${AppLocalizations.of(context).toAccountNumber}: $creditAccount",
style: const TextStyle(fontSize: 12, color: Colors.black87), style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.8)),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -91,15 +101,19 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _shareScreenshot, onPressed: _shareScreenshot,
icon: const Icon(Icons.share, size: 18), icon: const Icon(Icons.share, size: 18),
label: Text(AppLocalizations.of(context).share), label: Text(
AppLocalizations.of(context).share,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: const StadiumBorder(), shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: backgroundColor: Theme.of(context).primaryColorDark,
foregroundColor:
Theme.of(context).scaffoldBackgroundColor, Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorLight,
side: const BorderSide(color: Colors.black, width: 1),
elevation: 0,
), ),
), ),
), ),

View File

@@ -0,0 +1,110 @@
import 'package:flutter/material.dart';
import '../../../api/services/change_password_service.dart';
import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart';
class ChangePasswordOTPScreen extends StatefulWidget {
final String currentPassword;
final String newPassword;
final String confirmPassword;
final String mobileNumber;
// ignore: use_key_in_widget_constructors
const ChangePasswordOTPScreen({
required this.currentPassword,
required this.newPassword,
required this.confirmPassword,
required this.mobileNumber,
});
@override
State<ChangePasswordOTPScreen> createState() =>
_ChangePasswordOTPScreenState();
}
class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
bool _isLoading = true;
final TextEditingController otpController = TextEditingController();
@override
void initState() {
super.initState();
// Simulate OTP sending delay
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_isLoading = false;
});
});
}
final changePasswordService = getIt<ChangePasswordService>();
Future<void> _validateOTP() async {
try {
await changePasswordService.validateOtp(
otp: otpController.text,
mobileNumber: widget.mobileNumber,
);
// If OTP is valid, then change the password
await changePasswordService.validateChangePwd(
OldLPsw: widget.currentPassword,
newLPsw: widget.newPassword,
confirmLPsw: widget.confirmPassword,
);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).pwdchangeSuccess)),
);
// Navigate back to profile or login
Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
AppLocalizations.of(context).otpSent,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
TextFormField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterOTP,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateOTP,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).validateOTP),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,157 @@
// ignore_for_file: use_key_in_widget_constructors
import 'package:flutter/material.dart';
import '../../../api/services/change_password_service.dart';
import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart';
import 'change_password_otp_screen.dart';
class ChangePasswordScreen extends StatefulWidget {
final String mobileNumber;
const ChangePasswordScreen({super.key, required this.mobileNumber});
@override
State<ChangePasswordScreen> createState() => _ChangePasswordScreenState();
}
class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
final _formKey = GlobalKey<FormState>();
final currentPasswordController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
bool _showCurrentPassword = false;
bool _showNewPassword = false;
bool _showConfirmPassword = false;
final passwordRegex =
RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$');
String? validateCurrentPassword(String? value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).currentpwdrqd;
}
return null;
}
String? validateNewPassword(String? value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).newpwdrqd;
}
if (!passwordRegex.hasMatch(value)) {
return AppLocalizations.of(context).pwdFormat;
}
if (value == currentPasswordController.text) {
return AppLocalizations.of(context).newoldpwddiff;
}
return null;
}
String? validateConfirmPassword(String? value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).confirmpwdrqd;
}
if (value != newPasswordController.text) {
return AppLocalizations.of(context).pwdmismatch;
}
return null;
}
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
void _proceed() async {
if (_formKey.currentState!.validate()) {
try {
await _changePasswordService.getOtp(mobileNumber: widget.mobileNumber);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordOTPScreen(
currentPassword: currentPasswordController.text,
newPassword: newPasswordController.text,
confirmPassword: confirmPasswordController.text,
mobileNumber: widget.mobileNumber,
),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text('${AppLocalizations.of(context).failedtosentOTP}: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar:
AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)),
body: Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: currentPasswordController,
obscureText: !_showCurrentPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).currentpwd,
suffixIcon: IconButton(
icon: Icon(_showCurrentPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showCurrentPassword = !_showCurrentPassword),
),
),
validator: validateCurrentPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: newPasswordController,
obscureText: !_showNewPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).newpwd,
suffixIcon: IconButton(
icon: Icon(_showNewPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () =>
setState(() => _showNewPassword = !_showNewPassword),
),
),
validator: validateNewPassword,
),
const SizedBox(height: 16),
TextFormField(
controller: confirmPasswordController,
obscureText: !_showConfirmPassword,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmpwd,
suffixIcon: IconButton(
icon: Icon(_showConfirmPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () => setState(
() => _showConfirmPassword = !_showConfirmPassword),
),
),
validator: validateConfirmPassword,
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _proceed,
child: Text(AppLocalizations.of(context).proceed),
),
],
),
),
),
);
}
}

View File

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

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import '../../l10n/app_localizations.dart';
class LogoutDialog extends StatelessWidget {
const LogoutDialog({super.key});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).deregister),
content: Text(AppLocalizations.of(context).deregistercheck),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false), // dismiss
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.pop(context, true), // confirm
child: Text(AppLocalizations.of(context).yes),
),
],
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/profile/preferences/theme_mode_dialog.dart';
import 'language_dialog.dart'; import 'language_dialog.dart';
import 'color_theme_dialog.dart'; import 'color_theme_dialog.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -21,6 +22,12 @@ class PreferenceScreen extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return ListView( return ListView(
children: [ children: [
//Set Prefered Username
// ListTile(
// leading: const Icon(Icons.person),
// title: const Text("Set Prefered Username"),
// onTap: () {
// }),
// Language Selection // Language Selection
ListTile( ListTile(
leading: const Icon(Icons.language), leading: const Icon(Icons.language),
@@ -28,8 +35,7 @@ class PreferenceScreen extends StatelessWidget {
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (_) => builder: (_) => const LanguageDialog(),
const LanguageDialog(), // your custom language dialog
); );
}, },
), ),
@@ -37,19 +43,14 @@ class PreferenceScreen extends StatelessWidget {
ListTile( ListTile(
leading: const Icon(Icons.brightness_6), leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode), title: Text(AppLocalizations.of(context).themeMode),
enabled: false, onTap: () {
//trailing: Switch( showThemeModeDialog(context);
// value: state.isDarkMode, },
// onChanged: (val) {
// context.read<ThemeCubit>().toggleDarkMode(val);
// },
//),
), ),
//Color_Theme_Selection //Color_Theme_Selection
ListTile( ListTile(
leading: const Icon(Icons.color_lens), leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor), title: Text(AppLocalizations.of(context).themeColor),
//enabled: false,
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
import '../../../l10n/app_localizations.dart';
String _getThemeModeText(ThemeMode mode, AppLocalizations l10n) {
switch (mode) {
case ThemeMode.system:
return l10n.themeModeSystem;
case ThemeMode.light:
return l10n.themeModeLight;
case ThemeMode.dark:
return l10n.themeModeDark;
}
}
Future<void> showThemeModeDialog(BuildContext context) async {
final cubit = context.read<ThemeModeCubit>();
final currentMode = context.read<ThemeModeCubit>().state.mode;
final l10n = AppLocalizations.of(context);
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.selectThemeMode),
content: Column(
mainAxisSize: MainAxisSize.min,
children: ThemeMode.values.map((mode) {
return RadioListTile<ThemeMode>(
title: Text(_getThemeModeText(mode, l10n)),
value: mode,
groupValue: currentMode,
onChanged: (value) {
if (value != null) {
cubit.changeThemeMode(value);
Navigator.pop(context);
}
},
);
}).toList(),
),
);
},
);
}

View File

@@ -1,9 +1,139 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:kmobile/data/repositories/auth_repository.dart';
import 'package:kmobile/features/profile/change_password/change_password_screen.dart';
import 'package:kmobile/features/profile/daily_transaction_limit.dart';
import 'package:kmobile/features/profile/logout_dialog.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../di/injection.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import 'package:kmobile/features/profile/preferences/preference_screen.dart'; import 'package:kmobile/features/profile/preferences/preference_screen.dart';
class ProfileScreen extends StatelessWidget { class ProfileScreen extends StatefulWidget {
const ProfileScreen({super.key}); final String mobileNumber;
const ProfileScreen({super.key, required this.mobileNumber});
@override
State<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends State<ProfileScreen> {
bool _isBiometricEnabled = false;
@override
void initState() {
super.initState();
_loadBiometricStatus();
}
Future<String> _getAppVersion() async {
final PackageInfo info = await PackageInfo.fromPlatform();
return 'Version ${info.version} (${info.buildNumber})';
}
Future<void> _loadBiometricStatus() async {
final storage = getIt<SecureStorage>();
final isEnabled = await storage.read('biometric_enabled');
setState(() {
_isBiometricEnabled = isEnabled == true;
});
}
Future<void> _handleLogout(BuildContext context) async {
final auth = getIt<AuthRepository>();
final prefs = await SharedPreferences.getInstance();
await prefs.clear(); // clear saved session/token
await auth.clearAuthTokens();
// Navigate to login and remove all previous routes
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
}
Future<void> _handleBiometricToggle(bool enable) async {
final localAuth = LocalAuthentication();
final storage = getIt<SecureStorage>();
final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) {
// Optional: Show a snackbar or dialog if biometrics are not available
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).biometricsNotAvailable)),
);
return;
}
if (enable) {
// Show "Enable" dialog
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
content: Text(AppLocalizations.of(context).enableFingerprintMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (optIn == true) {
try {
final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).authenticateToEnable,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
if (didAuth) {
await storage.write('biometric_enabled', 'true');
setState(() {
_isBiometricEnabled = true;
});
}
} catch (e) {
// Handle authentication errors
}
}
} else {
// Show "Disable" dialog
final optOut = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context).disableFingerprintLogin),
content: Text(AppLocalizations.of(context).disableFingerprintMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (optOut == true) {
await storage.write('biometric_enabled', 'false');
setState(() {
_isBiometricEnabled = false;
});
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -17,7 +147,7 @@ class ProfileScreen extends StatelessWidget {
children: [ children: [
ListTile( ListTile(
leading: const Icon(Icons.settings), leading: const Icon(Icons.settings),
title: Text(loc.preferences), // Localized "Preferences" title: Text(loc.preferences),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -26,7 +156,115 @@ class ProfileScreen extends StatelessWidget {
); );
}, },
), ),
// You can add more profile options here later ListTile(
leading: const Icon(Icons.currency_rupee),
title: Text(AppLocalizations.of(context).dailylimit),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DailyLimitScreen()),
);
},
),
SwitchListTile(
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
),
ListTile(
leading: const Icon(Icons.password),
title: Text(loc.changeLoginPassword),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordScreen(
mobileNumber: widget.mobileNumber,
)),
);
},
),
// ListTile(
// leading: const Icon(Icons.password),
// title: const Text("Manage TPIN"),
// onTap: () async {
// },
// ),
// ListTile(
// leading: const Icon(Icons.password),
// title: const Text("Change Login MPIN"),
// onTap: () async {
// },
// ),
ListTile(
leading: const Icon(Icons.smartphone),
title: const Text("App Version"),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Show a loading indicator while waiting for the future to complete
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return const Text("Error");
} else {
// Display the version number once the future is complete
return Text(
snapshot.data ?? "N/A",
selectionColor: const Color(0xFFFFFFFF),
);
}
},
),
),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: Text(AppLocalizations.of(context).logout),
onTap: () async {
final shouldExit = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).logout),
content: Text(AppLocalizations.of(context).logoutCheck),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (shouldExit == true) {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
}
},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text(AppLocalizations.of(context).deregister),
onTap: () async {
final shouldLogout = await showDialog<bool>(
context: context,
builder: (_) => const LogoutDialog(),
);
if (shouldLogout == true) {
await _handleLogout(context);
}
},
),
], ],
), ),
); );

View File

@@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/data/models/imps_transaction.dart'; import 'package:kmobile/data/models/imps_transaction.dart';
@@ -13,9 +15,7 @@ import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart';
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart'; import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_swipe_button/flutter_swipe_button.dart'; import 'package:flutter_swipe_button/flutter_swipe_button.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../../di/injection.dart'; import '../../../../di/injection.dart';
import '../../../../api/services/beneficiary_service.dart'; import '../../../../api/services/beneficiary_service.dart';
@@ -30,7 +30,10 @@ class QuickPayOutsideBankScreen extends StatefulWidget {
class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> { class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
// Controllers // Controllers
final accountNumberController = TextEditingController(); final accountNumberController = TextEditingController();
final confirmAccountNumberController = TextEditingController(); final confirmAccountNumberController = TextEditingController();
@@ -40,7 +43,10 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final ifscController = TextEditingController(); final ifscController = TextEditingController();
final phoneController = TextEditingController(); final phoneController = TextEditingController();
final amountController = TextEditingController(); final amountController = TextEditingController();
final remarksController = TextEditingController();
final _ifscFocusNode = FocusNode();
final service = getIt<BeneficiaryService>(); final service = getIt<BeneficiaryService>();
bool _isAmountOverLimit = false;
late String accountType; late String accountType;
bool _isValidating = false; bool _isValidating = false;
@@ -50,29 +56,96 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit();
_ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
accountType = 'Savings'; accountType = 'Savings';
}); });
}); });
amountController.addListener(_checkAmountLimit);
} }
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
void _validateIFSC() async { void _validateIFSC() async {
final ifsc = ifscController.text.trim().toUpperCase(); final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return; if (ifsc.isEmpty) return;
final result = await service.validateIFSC(ifsc); try {
final result = await service.validateIFSC(ifsc);
if (mounted) {
if (result.bankName.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
);
bankNameController.clear();
branchNameController.clear();
} else {
bankNameController.text = result.bankName;
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;
}
if (mounted) {
if (result.bankName == '') {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)), SnackBar(content: Text(snackbarMessage)),
); );
bankNameController.clear(); bankNameController.clear();
branchNameController.clear(); branchNameController.clear();
} else {
bankNameController.text = result.bankName;
branchNameController.text = result.branchName;
} }
} }
} }
@@ -88,7 +161,6 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final String accountNo = accountNumberController.text.trim(); final String accountNo = accountNumberController.text.trim();
final String ifsc = ifscController.text.trim(); final String ifsc = ifscController.text.trim();
// ignore: prefer_const_declarations
final String remitter = "Unknown"; final String remitter = "Unknown";
final service = getIt<BeneficiaryService>(); final service = getIt<BeneficiaryService>();
@@ -139,7 +211,9 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
ifscController.dispose(); ifscController.dispose();
phoneController.dispose(); phoneController.dispose();
amountController.dispose(); amountController.dispose();
remarksController.dispose();
super.dispose(); super.dispose();
_ifscFocusNode.dispose();
} }
void _onProceedToPay() { void _onProceedToPay() {
@@ -191,6 +265,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
remitterName: "Unknown", remitterName: "Unknown",
beneficiaryName: nameController.text, beneficiaryName: nameController.text,
tpin: tpin, tpin: tpin,
remarks: remarksController.text,
); );
final neftService = getIt<NeftService>(); final neftService = getIt<NeftService>();
final completer = Completer<PaymentResponse>(); final completer = Completer<PaymentResponse>();
@@ -256,6 +331,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
remitterName: "Unknown", remitterName: "Unknown",
beneficiaryName: nameController.text, beneficiaryName: nameController.text,
tpin: tpin, tpin: tpin,
remarks: remarksController.text,
); );
final impsService = getIt<ImpsService>(); final impsService = getIt<ImpsService>();
final completer = Completer<PaymentResponse>(); final completer = Completer<PaymentResponse>();
@@ -315,6 +391,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
remitterName: "Unknown", remitterName: "Unknown",
beneficiaryName: nameController.text, beneficiaryName: nameController.text,
tpin: tpin, tpin: tpin,
remarks: remarksController.text,
); );
final rtgsService = getIt<RtgsService>(); final rtgsService = getIt<RtgsService>();
final completer = Completer<PaymentResponse>(); final completer = Completer<PaymentResponse>();
@@ -375,33 +452,10 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
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).quickPayOutsideBank, AppLocalizations.of(context).quickPayOutsideBank,
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: Padding( body: Padding(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@@ -435,11 +489,13 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
controller: accountNumberController, controller: accountNumberController,
@@ -471,11 +527,13 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -496,6 +554,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
children: [ children: [
Expanded( Expanded(
child: TextFormField( child: TextFormField(
focusNode: _ifscFocusNode,
maxLength: 11, maxLength: 11,
inputFormatters: [ inputFormatters: [
LengthLimitingTextInputFormatter(11), LengthLimitingTextInputFormatter(11),
@@ -506,25 +565,27 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
), ),
), ),
controller: ifscController, controller: ifscController,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_validateIFSC();
},
onChanged: (value) { onChanged: (value) {
final trimmed = value.trim().toUpperCase(); setState(() {
if (trimmed.length < 11) { final trimmed = value.trim().toUpperCase();
// clear bank/branch if backspace or changed if (trimmed.length < 11) {
bankNameController.clear(); // clear bank/branch if backspace or changed
branchNameController.clear(); bankNameController.clear();
} branchNameController.clear();
}
});
}, },
validator: (value) { validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$'); final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
@@ -553,11 +614,14 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
), ),
), ),
items: [ items: [
@@ -585,14 +649,13 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).dialogBackgroundColor, fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Colors.black, color: Theme.of(context).colorScheme.outline),
width: 2, ),
), focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
), ),
@@ -606,12 +669,13 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).dialogBackgroundColor, fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Colors.black, color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2, width: 2,
), ),
), ),
@@ -624,20 +688,21 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isValidating onPressed:
? null _isValidating || ifscController.text.length != 11
: () { ? null
if (confirmAccountNumberController.text == : () {
accountNumberController.text) { if (confirmAccountNumberController.text ==
_validateBeneficiary(); accountNumberController.text) {
} else { _validateBeneficiary();
setState(() { } else {
_validationError = setState(() {
AppLocalizations.of(context) _validationError =
.accountMismatch; AppLocalizations.of(context)
}); .accountMismatch;
} });
}, }
},
child: _isValidating child: _isValidating
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
@@ -654,7 +719,8 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
padding: const EdgeInsets.only(bottom: 24.0), padding: const EdgeInsets.only(bottom: 24.0),
child: Text( child: Text(
_validationError!, _validationError!,
style: const TextStyle(color: Colors.red), style:
TextStyle(color: Theme.of(context).colorScheme.error),
), ),
), ),
TextFormField( TextFormField(
@@ -666,11 +732,13 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).dialogBackgroundColor, fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
validator: (value) { validator: (value) {
@@ -681,6 +749,28 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
}, },
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
TextFormField(
controller: remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
),
const SizedBox(height: 25),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -694,11 +784,14 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
), ),
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
@@ -716,11 +809,14 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
), ),
), ),
controller: amountController, controller: amountController,
@@ -740,6 +836,22 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
), ),
], ],
), ),
],
),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Text('Fetching daily limit...'),
),
if (!_isLoadingLimit && _limit != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
),
const SizedBox(height: 30), const SizedBox(height: 30),
Row( Row(
children: [ children: [
@@ -752,24 +864,31 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
], ],
), ),
const SizedBox(height: 45), const SizedBox(height: 45),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SwipeButton.expand( child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward, thumb: Icon(Icons.arrow_forward,
color: Theme.of(context).dialogBackgroundColor), color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: Theme.of(context).primaryColor, activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
activeTrackColor: Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary.withAlpha(100), activeTrackColor: _isAmountOverLimit
borderRadius: BorderRadius.circular(30), ? Colors.grey.shade300
height: 56, : Theme.of(context).colorScheme.secondary.withAlpha(100),
onSwipe: _onProceedToPay, borderRadius: BorderRadius.circular(30),
child: Text( height: 56,
AppLocalizations.of(context).swipeToPay, onSwipe: () {
style: const TextStyle( if (_isAmountOverLimit) {
fontSize: 16, fontWeight: FontWeight.bold), return; // Do nothing if amount is over the limit
), }
), _onProceedToPay();
), },
child: Text(
AppLocalizations.of(context).swipeToPay,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
], ],
), ),
), ),
@@ -793,12 +912,12 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
padding: const EdgeInsets.symmetric(vertical: 5), padding: const EdgeInsets.symmetric(vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? Theme.of(context).primaryColor ? Theme.of(context).colorScheme.primary
: Theme.of(context).scaffoldBackgroundColor, : Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
border: Border.all( border: Border.all(
color: isSelected color: isSelected
? Theme.of(context).primaryColor ? Theme.of(context).colorScheme.primary
: Theme.of(context).scaffoldBackgroundColor, : Theme.of(context).scaffoldBackgroundColor,
width: isSelected ? 0 : 1.2, width: isSelected ? 0 : 1.2,
), ),
@@ -806,7 +925,8 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
transactionModes(context)[index], transactionModes(context)[index],
style: const TextStyle(color: Colors.black), style:
TextStyle(color: Theme.of(context).colorScheme.onSurface),
), ),
), ),
), ),

View File

@@ -1,13 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter_swipe_button/flutter_swipe_button.dart'; import 'package:flutter_swipe_button/flutter_swipe_button.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/beneficiary_service.dart'; import 'package:kmobile/api/services/beneficiary_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/payment_service.dart'; import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart'; import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import '../../fund_transfer/screens/transaction_pin_screen.dart'; import '../../fund_transfer/screens/transaction_pin_screen.dart';
@@ -21,13 +21,17 @@ class QuickPayWithinBankScreen extends StatefulWidget {
class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> { class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final TextEditingController accountNumberController = TextEditingController(); final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController = final TextEditingController confirmAccountNumberController =
TextEditingController(); TextEditingController();
final TextEditingController amountController = TextEditingController(); final TextEditingController amountController = TextEditingController();
final TextEditingController remarksController = TextEditingController();
String? _selectedAccountType; String? _selectedAccountType;
bool _isAmountOverLimit = false;
String? _beneficiaryName; String? _beneficiaryName;
bool _isValidating = false; bool _isValidating = false;
bool _isBeneficiaryValidated = false; bool _isBeneficiaryValidated = false;
@@ -36,10 +40,54 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit();
accountNumberController.addListener(_resetBeneficiaryValidation); accountNumberController.addListener(_resetBeneficiaryValidation);
confirmAccountNumberController.addListener(_resetBeneficiaryValidation); confirmAccountNumberController.addListener(_resetBeneficiaryValidation);
amountController.addListener(_checkAmountLimit);
} }
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
// Update state only if it changes to avoid unnecessary rebuilds
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
void _resetBeneficiaryValidation() { void _resetBeneficiaryValidation() {
if (_isBeneficiaryValidated || if (_isBeneficiaryValidated ||
_beneficiaryName != null || _beneficiaryName != null ||
@@ -54,11 +102,13 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
@override @override
void dispose() { void dispose() {
amountController.removeListener(_checkAmountLimit);
accountNumberController.removeListener(_resetBeneficiaryValidation); accountNumberController.removeListener(_resetBeneficiaryValidation);
confirmAccountNumberController.removeListener(_resetBeneficiaryValidation); confirmAccountNumberController.removeListener(_resetBeneficiaryValidation);
accountNumberController.dispose(); accountNumberController.dispose();
confirmAccountNumberController.dispose(); confirmAccountNumberController.dispose();
amountController.dispose(); amountController.dispose();
remarksController.dispose();
super.dispose(); super.dispose();
} }
@@ -90,39 +140,20 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
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).quickPayOwnBank, AppLocalizations.of(context).quickPayOwnBank,
style: style: TextStyle(
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500), color: Theme.of(context).colorScheme.onSurface,
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: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: SingleChildScrollView(
child: Column(
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
TextFormField( TextFormField(
@@ -147,11 +178,13 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
controller: accountNumberController, controller: accountNumberController,
@@ -177,11 +210,13 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -261,11 +296,13 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
value: _selectedAccountType, value: _selectedAccountType,
@@ -292,6 +329,26 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
}, },
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
TextFormField(
controller: remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
),
const SizedBox(height: 25),
TextFormField( TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).amount, labelText: AppLocalizations.of(context).amount,
@@ -299,11 +356,13 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
controller: amountController, controller: amountController,
@@ -320,65 +379,81 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
return null; return null;
}, },
), ),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 45), const SizedBox(height: 45),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SwipeButton.expand( child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward, thumb: Icon(Icons.arrow_forward,
color: Theme.of(context).dialogBackgroundColor), color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: Theme.of(context).primaryColor, activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
activeTrackColor: Theme.of( Theme.of(context).colorScheme.primary,
context, activeTrackColor: _isAmountOverLimit
).colorScheme.secondary.withAlpha(100), ? Colors.grey.shade300
borderRadius: BorderRadius.circular(30), : Theme.of(
height: 56, context,
child: Text( ).colorScheme.secondary.withAlpha(100),
AppLocalizations.of(context).swipeToPay, borderRadius: BorderRadius.circular(30),
style: const TextStyle(fontSize: 16), height: 56,
), child: Text(
onSwipe: () { AppLocalizations.of(context).swipeToPay,
if (_formKey.currentState!.validate()) { style: const TextStyle(fontSize: 16),
if (!_isBeneficiaryValidated) { ),
setState(() { onSwipe: () {
_validationError = AppLocalizations.of(context) if (_isAmountOverLimit) {
.validateBeneficiaryproceeding; return; // Do nothing if amount is over limit
}); }
return; if (_formKey.currentState!.validate()) {
} if (!_isBeneficiaryValidated) {
// Perform payment logic setState(() {
Navigator.push( _validationError = AppLocalizations.of(context)
context, .validateBeneficiaryproceeding;
MaterialPageRoute( });
builder: (context) => TransactionPinScreen( return;
onPinCompleted: (pinScreenContext, tpin) async { }
final transfer = Transfer( // Perform payment logic
fromAccount: widget.debitAccount, Navigator.push(
toAccount: accountNumberController.text, context,
toAccountType: _selectedAccountType!, MaterialPageRoute(
amount: amountController.text, builder: (context) => TransactionPinScreen(
tpin: tpin, onPinCompleted: (pinScreenContext, tpin) async {
); final transfer = Transfer(
fromAccount: widget.debitAccount,
toAccount: accountNumberController.text,
toAccountType: _selectedAccountType!,
amount: amountController.text,
tpin: tpin,
remarks: remarksController.text,
);
final paymentService = getIt<PaymentService>(); final paymentService = getIt<PaymentService>();
final paymentResponseFuture = paymentService final paymentResponseFuture = paymentService
.processQuickPayWithinBank(transfer); .processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement( Navigator.of(pinScreenContext).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => PaymentAnimationScreen( builder: (_) => PaymentAnimationScreen(
paymentResponse: paymentResponseFuture), paymentResponse: paymentResponseFuture),
), ),
); );
}, },
), ),
), ),
); );
} }
}, },
), ),
), ),
], ],
), ),
),
), ),
), ),
); );
@@ -400,11 +475,12 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).dialogBackgroundColor, fillColor: Theme.of(context).dialogBackgroundColor,
enabledBorder: const OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black), borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: const OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2), borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
); );

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