166 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
de596aa138 Code Obfuscation #1 2025-09-02 16:05:50 +05:30
720c6ec03a View Cards static page 2025-09-02 15:48:23 +05:30
d081a5433c Localization Changes #5 2025-09-01 16:41:32 +05:30
90f4a6bb81 Removed bugs and duplicates 2025-09-01 12:32:17 +05:30
asif
f71c8d646b removed a bunch of print statements and added a few async checks 2025-08-31 21:44:56 +05:30
asif
6a8f091ac5 changed welcome_screen to splash_screen in app.dart 2025-08-31 21:44:56 +05:30
asif
596f0a96eb replaced print with log in theme_cubit.dart 2025-08-31 21:44:04 +05:30
asif
768d312066 refactored welcome string to splash screen 2025-08-31 21:44:04 +05:30
asif
4277ca9169 replaced print with log in neft_service.dart 2025-08-31 21:44:04 +05:30
asif
53caa5b7ae removed decorations from title bar and chaged constant colors to themes 2025-08-31 21:44:04 +05:30
asif
bfa0721e79 removed unnecessary bg color from avatar 2025-08-31 21:44:04 +05:30
asif
75bcce8201 removed unnecessary decorations from title bar 2025-08-31 21:44:04 +05:30
asif
99f4b1b735 UI changes in customer info screen
Removed unnecessary decorations from title bar.
2025-08-31 21:44:04 +05:30
asif
6ebc96d153 removed some unnecessary code and add some async guards 2025-08-31 21:44:04 +05:30
asif
6496aa95c0 changed greeting font and size 2025-08-31 21:44:04 +05:30
asif
e91efafa0e increased font weight of title bar 2025-08-31 21:44:04 +05:30
asif
61a6e356d9 increased font weight in dashboard card 2025-08-31 21:44:04 +05:30
asif
a48781ad1b changed some wordings in the card 2025-08-31 21:44:04 +05:30
asif
d425af184e updated android ndk version 2025-08-31 21:44:04 +05:30
asif
bc2c460f96 changed the font size of acc no in card 2025-08-31 21:44:04 +05:30
asif
ef3556f207 removed some print statements 2025-08-31 21:44:04 +05:30
asif
6724d8bed0 refactored the balacnce card to remove unnecessary things 2025-08-31 21:44:04 +05:30
asif
83ed973b44 fixed some async gaps warnings 2025-08-31 21:44:04 +05:30
asif
f7dd31fb93 added rubik text theme 2025-08-31 21:44:04 +05:30
asif
9c60dc1c1c changed debit and credit icons colors 2025-08-31 21:44:04 +05:30
asif
c369ad415d added weight and dim color to the dashboard QL icons 2025-08-31 21:44:04 +05:30
asif
f934f0bff6 fixed some warnings and colors in the dashboard screen 2025-08-31 21:44:04 +05:30
asif
da0287ef38 fixed some print, async context warnings in
fund_transfer_amount_screen.dart
2025-08-31 21:44:04 +05:30
asif
f65507a706 fixed a context used across async gap warning and removed comments 2025-08-31 21:41:09 +05:30
9f6b35d3eb Beneficiary Details alignment fixed 2025-08-29 15:08:14 +05:30
a33b4bc1e1 Localization changes #4 2025-08-28 13:28:21 +05:30
0d629226a8 Available Balance in Account Statement #2 2025-08-27 18:21:42 +05:30
6d502a0c81 Available Balance in Account Statement 2025-08-27 17:17:47 +05:30
96d1e37c42 Beneficiary Validation Changes #2 2025-08-27 16:13:49 +05:30
b0610ad84d Beneficiary Validation Changes 2025-08-27 16:08:47 +05:30
ceb29ee7fe Manage Beneficiary delete Confirm 2025-08-27 15:00:46 +05:30
57cb93c124 Fund Transfer page with Own Bank and Outside Bank modified 2025-08-27 14:39:51 +05:30
0ac9a1f319 Merge branch 'uat-2' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into uat-2 2025-08-27 13:18:30 +05:30
d3ad68b25c Fund Transfer page with Own Bank and Outside Bank modified 2025-08-27 13:16:26 +05:30
asif
eb0e1c120e Changed font size in transaction detail screen 2025-08-26 21:03:28 +05:30
a64e68d642 NEFT and RTGS transaction error resolved, 2025-08-26 16:42:30 +05:30
80faa06272 Merge branch 'uat-2' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into uat-2 2025-08-26 16:00:26 +05:30
0bab68a2e6 NEFT and RTGS transaction error resolved 2025-08-26 15:58:31 +05:30
asif
351ee84414 formatted some files 2025-08-25 19:47:19 +05:30
asif
f77549310a Changed some formatting issues 2025-08-25 19:45:20 +05:30
4b28a113d4 color Changes minor 2025-08-25 18:10:52 +05:30
bf35ec61d5 Merge branch 'uat-2' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into uat-2 2025-08-25 17:48:40 +05:30
180d3a3a60 Theme Colour bugs fixed 2025-08-25 17:47:18 +05:30
asif
737ce6cce6 Fixed some errors and delted some commented codes 2025-08-24 01:11:29 +05:30
asif
e75f48aed5 changed backend URL to point to load balancer 2025-08-24 01:04:38 +05:30
asif
5ee88403dd fixed context usage async gaps warnings and deleted some comments 2025-08-24 00:32:06 +05:30
asif
7fe8e8f084 fixed warnings related to colors and deleted some commented code 2025-08-24 00:31:25 +05:30
asif
59eb509dda formatted some files 2025-08-23 23:51:53 +05:30
asif
5f41c6e7f4 pubspec.lock 2025-08-23 23:49:51 +05:30
asif
36474786c1 generated localizations file to be ignored by git 2025-08-23 23:49:45 +05:30
asif
df51c4f30b resolved many context usage across async gaps warnings
also removed some unused code blocks
2025-08-23 23:41:52 +05:30
asif
cf8588b336 removed the logo blinking animation in login page 2025-08-23 23:41:52 +05:30
04c992c934 IMPS implementation 2025-08-22 16:36:58 +05:30
fc76528206 Branch Locator static Screen 2025-08-20 12:46:28 +05:30
7527db3d2f Merge branch 'uat-2' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into uat-2 2025-08-20 11:44:58 +05:30
19d3487552 FORMAT TESTING #! 2025-08-20 11:43:35 +05:30
asif
60a5703c35 formatted the whole codebase 2025-08-20 01:36:59 +05:30
09618db28e New file created for bank logos 2025-08-19 16:11:54 +05:30
712553e49f Localization Changes #3 2025-08-19 11:20:32 +05:30
842eef2cc7 Localization Chamges #2 2025-08-19 11:13:05 +05:30
asif
e6ea764785 formatted the whole codebase 2025-08-18 03:32:51 +05:30
e2a809d363 Beneficiary Localization Chamges 2025-08-14 16:15:35 +05:30
f48bfabb4a Transaction Details page Localizations 2025-08-13 18:06:53 +05:30
76d5874e19 Transaction Details localized to Hindi 2025-08-13 11:42:11 +05:30
913e31a6ab Transaction Details page 2025-08-12 18:10:02 +05:30
7f5258b5b7 Minor Changes of const and bank logos 2025-08-12 13:19:24 +05:30
asif
82951b7919 code reformatting and adding const everywhere 2025-08-12 11:47:10 +05:30
8ae26e2f18 Payment Error Message and Beneficiary Page 2025-08-11 17:03:02 +05:30
e7bbab13ba Bank Logos in beneficiary list 2025-08-11 13:13:28 +05:30
72862492c0 Changed Substring to full UTR No. 2025-08-11 12:34:41 +05:30
asif
f91d0f739b feat: Implement major features and fix theming bug
This commit introduces several new features and a critical bug fix.

- Implemented a full "Quick Pay" flow for both within and outside the bank, including IFSC validation, beneficiary verification, and a TPIN-based payment process.
- Added a date range filter to the Account Statement screen and streamlined the UI by removing the amount filters.
- Fixed a major bug that prevented dynamic theme changes from being applied. The app now correctly switches between color themes.
- Refactored and improved beneficiary management, transaction models, and the fund transfer flow to support NEFT/RTGS.
2025-08-11 04:06:05 +05:30
155 changed files with 7886 additions and 5925 deletions

3
.gitignore vendored
View File

@@ -41,3 +41,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
lib/l10n/app_localizations.dart
lib/l10n/app_localizations_en.dart
lib/l10n/app_localizations_hi.dart

View File

@@ -7,6 +7,10 @@
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
dead_code: ignore
non_constant_identifier_names: ignore
include: package:flutter_lints/flutter.yaml
linter:

View File

@@ -25,7 +25,7 @@ if (flutterVersionName == null) {
android {
namespace "com.example.kmobile"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
ndkVersion "27.0.12077973"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@@ -45,7 +45,7 @@ android {
applicationId "com.example.kmobile"
// 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.
minSdkVersion flutter.minSdkVersion
minSdkVersion 29
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -56,6 +56,10 @@ android {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

21
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.embedding.** { *; }
-keep class io.flutter.embedding.engine.plugins.** { *; }
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn com.google.android.play.core.splitcompat.SplitCompatApplication
-dontwarn com.google.android.play.core.splitinstall.SplitInstallException
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManager
-dontwarn com.google.android.play.core.splitinstall.SplitInstallManagerFactory
-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest$Builder
-dontwarn com.google.android.play.core.splitinstall.SplitInstallRequest
-dontwarn com.google.android.play.core.splitinstall.SplitInstallSessionState
-dontwarn com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
-dontwarn com.google.android.play.core.tasks.OnFailureListener
-dontwarn com.google.android.play.core.tasks.OnSuccessListener
-dontwarn com.google.android.play.core.tasks.Task

View File

@@ -2,10 +2,13 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:label="kmobile"
android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"

View File

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

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
assets/images/boi_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
assets/images/hdfc_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
assets/images/ipos_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
assets/images/pnb_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/images/sbi_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 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;
buildSettings = {
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
@@ -484,7 +484,7 @@
isa = XCBuildConfiguration;
buildSettings = {
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_CXX_LANGUAGE_STANDARD = "gnu++0x";
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/>
<key>NSFaceIDUsageDescription</key>
<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>
</plist>

View File

@@ -1,3 +1,5 @@
// ignore_for_file: unused_local_variable
import 'package:dio/dio.dart';
import '../../data/repositories/auth_repository.dart';

View File

@@ -4,7 +4,6 @@ import '../../features/auth/models/auth_token.dart';
import '../../features/auth/models/auth_credentials.dart';
import '../../core/errors/exceptions.dart';
class AuthService {
final Dio _dio;
AuthService(this._dio);
@@ -15,7 +14,7 @@ class AuthService {
'/api/auth/login',
data: credentials.toJson(),
);
if (response.statusCode == 200) {
return AuthToken.fromJson(response.data);
} else {
@@ -26,11 +25,13 @@ class AuthService {
print(e.toString());
}
if (e.response?.statusCode == 401) {
throw AuthException('Invalid credentials');
throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
}
throw NetworkException('Network error during login');
} catch (e) {
throw UnexpectedException('Unexpected error during login: ${e.toString()}');
throw UnexpectedException(
'Unexpected error during login: ${e.toString()}');
}
}
@@ -40,7 +41,7 @@ class AuthService {
'/auth/refresh',
data: {'refresh_token': refreshToken},
);
if (response.statusCode == 200) {
return AuthToken.fromJson(response.data);
} else {
@@ -54,7 +55,7 @@ class AuthService {
Future<bool> checkTpin() async {
try {
final response = await _dio.get('/api/auth/tpin');
if (response.statusCode == 200) {
return response.data['tpinSet'] as bool;
} else {
@@ -66,7 +67,8 @@ class AuthService {
}
throw NetworkException('Network error during TPIN check');
} catch (e) {
throw UnexpectedException('Unexpected error during TPIN check: ${e.toString()}');
throw UnexpectedException(
'Unexpected error during TPIN check: ${e.toString()}');
}
}
@@ -76,7 +78,7 @@ class AuthService {
'/api/auth/tpin',
data: {'tpin': tpin},
);
if (response.statusCode != 200) {
throw AuthException('Failed to set TPIN');
}
@@ -86,8 +88,57 @@ class AuthService {
}
throw NetworkException('Network error during TPIN setup');
} catch (e) {
throw UnexpectedException('Unexpected error during TPIN setup: ${e.toString()}');
throw UnexpectedException(
'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,4 +1,6 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:kmobile/core/errors/exceptions.dart';
import 'package:kmobile/data/models/ifsc.dart';
import 'package:kmobile/data/models/beneficiary.dart';
@@ -9,14 +11,16 @@ class BeneficiaryService {
Future<String> validateBeneficiaryWithinBank(String accountNumber) async {
try {
final response = await _dio.get('/api/beneficiary/validate/within-bank', queryParameters: {
final response = await _dio
.get('/api/beneficiary/validate/within-bank', queryParameters: {
'accountNumber': accountNumber,
});
if (response.statusCode == 200) {
return response.data['name'];
} else {
throw Exception(response.data['error'] ?? 'Failed to validate beneficiary');
throw Exception(
response.data['error'] ?? 'Failed to validate beneficiary');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
@@ -24,156 +28,102 @@ class BeneficiaryService {
throw Exception('Unexpected error: ${e.toString()}');
}
}
Future<ifsc?> validateIFSC(String ifscCode) async {
try {
final response = await _dio.get('/api/beneficiary/ifsc-details', queryParameters: {
"ifscCode": ifscCode
}
);
if (response.statusCode == 200) {
return ifsc.fromJson(response.data);
Future<Ifsc> validateIFSC(String ifscCode) async {
try {
final response = await _dio.get('/api/beneficiary/ifsc-details',
queryParameters: {"ifscCode": ifscCode});
if (response.statusCode == 200) {
return Ifsc.fromJson(response.data);
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('INVALID IFSC CODE');
} else if (e.response?.statusCode == 401) {
throw Exception('INVALID IFSC CODE');
}
} catch (e) {
throw UnexpectedException(
'Unexpected error during login: ${e.toString()}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
print('Invalid IFSC code.');
} else {
print('API error: ${e.message}');
}
} catch (e) {
print('Unexpected error: $e');
return Ifsc.fromJson({});
}
return null;
}
///Step 1: Validate beneficiary (returns refNo)
Future<String?> validateBeneficiary({
Future<String> validateBeneficiary({
required String accountNo,
required String ifscCode,
required String remitterName,
}) async {
try {
final response = await _dio.post(
'/api/beneficiary/validate/outside_bank',
queryParameters: {
'accountNo': accountNo,
'ifscCode': ifscCode,
'remitterName': remitterName,
},
);
if (response.statusCode == 200) {
return response.data['refNo'];
} else {
return null;
}
} catch (e) {
print("Error validating beneficiary: $e");
return null;
log('inside validate beneficiary service');
final response = await _dio.get(
'/api/beneficiary/validate/outside-bank',
queryParameters: {
'accountNo': accountNo,
'ifscCode': ifscCode,
'remitterName': remitterName,
},
options: Options(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
),
);
if (response.statusCode != 200) {
throw Exception("Invalid Beneficiary Details");
}
return response.data['name'];
}
/// Step 2: Check validation status (returns Beneficiary name if success)
Future<String?> checkValidationStatus(String refNo) async {
// Beneficiary Validate And ADD
Future<bool> sendForValidation(Beneficiary beneficiary) async {
try {
final response = await _dio.get(
'/api/beneficiary/check',
queryParameters: {
'refNo': refNo,
},
);
if (response.statusCode == 200 && response.data != null) {
return Beneficiary.fromJson(response.data).name;
} else {
return null;
}
} catch (e) {
print("Error checking validation status: $e");
return null;
}
}
// Send Data for Validation
Future<void> sendForValidation(Beneficiary beneficiary) async {
try {
print(beneficiary.toJson());
final response = await _dio.post(
'/api/beneficiary/add',
'/api/beneficiary',
data: beneficiary.toJson(),
);
if (response.statusCode == 200) {
print("SENT FOR VALIDATION");
return true;
} else {
print("VALIDATION REQUEST FAILED: ${response.statusCode}");
return false;
}
} catch (e) {
print("ERROR IN SENDING REQUEST: $e");
rethrow;
}
}
// Poll to check if beneficiary is found
Future<bool> checkIfFound(String accountNo) async {
const int timeoutInSeconds = 30;
const int intervalInSeconds = 2;
const int maxTries = timeoutInSeconds ~/ intervalInSeconds;
int attempts = 0;
while (attempts < maxTries) {
try {
final response = await _dio.get(
'/api/beneficiary/check?',
queryParameters: {
'accountNo': accountNo
}
);
if (response.statusCode == 200) {
print("FOUND");
return true;
} else if (response.statusCode == 404) {
print("NOT FOUND");
}
} catch (e) {
print("ERROR DURING STATUS: $e");
}
attempts++;
}
print("Beneficiary not found within timeout.");
return false;
}
Future<List<Beneficiary>> fetchBeneficiaryList() async{
Future<List<Beneficiary>> fetchBeneficiaryList() async {
try {
final response = await _dio.get(
"/api/beneficiary/get",
options: Options(
"/api/beneficiary",
options: Options(
headers: {
"Content-Type": "application/json",
},
),
);
if (response.statusCode == 200) {
return Beneficiary.listFromJson(response.data);
if (response.statusCode == 200) {
return Beneficiary.listFromJson(response.data);
} else {
throw Exception("Failed to fetch beneficiaries");
}
} catch (e) {
print("Error fetching beneficiaries: $e");
return [];
}
}
}
Future<void> deleteBeneficiary(String accountNo) async {
try {
final response = await _dio.delete('/api/beneficiary/$accountNo');
if (response.statusCode != 204) {
throw Exception('Failed to delete beneficiary');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
}

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,31 @@
import 'package:dio/dio.dart';
import 'package:kmobile/data/models/imps_response.dart';
import 'package:kmobile/data/models/imps_transaction.dart';
class ImpsService {
final Dio _dio;
ImpsService(this._dio);
Future<ImpsResponse> processImpsTransaction(
ImpsTransaction transaction) async {
try {
await Future.delayed(const Duration(seconds: 3));
final response = await _dio.post(
'/api/payment/imps',
data: transaction.toJson(),
);
if (response.statusCode == 200) {
return ImpsResponse.fromJson(response.data);
} else {
throw Exception(
'IMPS transaction failed with status code: ${response.statusCode}');
}
} on DioException {
rethrow;
} catch (e) {
throw Exception('An unexpected error occurred: ${e.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,34 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:kmobile/data/models/neft_response.dart';
import 'package:kmobile/data/models/neft_transaction.dart';
class NeftService {
final Dio _dio;
NeftService(this._dio);
Future<NeftResponse> processNeftTransaction(
NeftTransaction transaction) async {
try {
await Future.delayed(const Duration(seconds: 3));
final response = await _dio.post(
'/api/payment/neft',
data: transaction.toJson(),
);
if (response.statusCode == 200) {
return NeftResponse.fromJson(response.data);
} else {
throw Exception(
'NEFT transaction failed with status code: ${response.statusCode}');
}
} on DioException {
log('DioException Occured');
rethrow;
} catch (e) {
throw Exception('An unexpected error occurred: ${e.toString()}');
}
}
}

View File

@@ -1,5 +1,4 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:kmobile/data/models/payment_response.dart';
import 'package:kmobile/data/models/transfer.dart';
@@ -11,7 +10,7 @@ class PaymentService {
Future<PaymentResponse> processQuickPayWithinBank(Transfer transfer) async {
try {
await Future.delayed(const Duration(seconds: 3)); // Simulate delay
await _dio.post('/api/payment/transfer', data: transfer.toJson());
return PaymentResponse(

View File

@@ -0,0 +1,31 @@
import 'package:dio/dio.dart';
import 'package:kmobile/data/models/rtgs_response.dart';
import 'package:kmobile/data/models/rtgs_transaction.dart';
class RtgsService {
final Dio _dio;
RtgsService(this._dio);
Future<RtgsResponse> processRtgsTransaction(
RtgsTransaction transaction) async {
try {
await Future.delayed(const Duration(seconds: 3));
final response = await _dio.post(
'/api/payment/rtgs',
data: transaction.toJson(),
);
if (response.statusCode == 200) {
return RtgsResponse.fromJson(response.data);
} else {
throw Exception(
'RTGS transaction failed with status code: ${response.statusCode}');
}
} on DioException {
rethrow;
} catch (e) {
throw Exception('An unexpected error occurred: ${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

@@ -28,4 +28,4 @@ class UserService {
throw Exception('Unexpected error: ${e.toString()}');
}
}
}
}

View File

@@ -2,6 +2,10 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.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:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart';
@@ -11,13 +15,12 @@ import 'config/routes.dart';
import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart';
import 'features/card/screens/card_management_screen.dart';
import 'features/auth/screens/welcome_screen.dart';
import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart';
import 'features/auth/screens/mpin_screen.dart';
import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
class KMobile extends StatefulWidget {
const KMobile({super.key});
@@ -26,38 +29,58 @@ class KMobile extends StatefulWidget {
State<KMobile> createState() => _KMobileState();
static void setLocale(BuildContext context, Locale newLocale) {
final _KMobileState? state = context.findAncestorStateOfType<_KMobileState>();
final _KMobileState? state =
context.findAncestorStateOfType<_KMobileState>();
state?.setLocale(newLocale);
}
}
class _KMobileState extends State<KMobile> {
bool showSplash = true;
class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
Timer? _backgroundTimer;
Locale? _locale;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
loadPreferences();
Future.delayed(const Duration(seconds: 2), () {
setState(() {
showSplash = false;
});
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_backgroundTimer?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
_backgroundTimer?.cancel();
break;
case AppLifecycleState.paused:
_backgroundTimer = Timer(const Duration(minutes: 2), () {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
});
break;
default:
break;
}
}
Future<void> loadPreferences() async {
final prefs = await SharedPreferences.getInstance();
// Load Locale
final String? langCode = prefs.getString('locale');
if (langCode != null) {
setState(() {
_locale = Locale(langCode);
});
}
}
void setLocale(Locale locale) {
@@ -66,57 +89,55 @@ class _KMobileState extends State<KMobile> {
});
}
@override
Widget build(BuildContext context) {
// Set status bar color and brightness
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
@override
Widget build(BuildContext context) {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
BlocProvider<ThemeCubit>(create: (_) => getIt<ThemeCubit>()),
],
child: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, themeState) {
print('global theme state changed');
print(themeState);
return MaterialApp(
debugShowCheckedModeBanner: false,
locale: _locale ?? const Locale('en'),
supportedLocales: const [
Locale('en'),
Locale('hi'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
title: 'kMobile',
//theme: AppThemes.getLightTheme(themeState.themeType),
theme: themeState.getThemeData(),
// darkTheme: AppThemes.getDarkTheme(themeState.themeType),
themeMode: ThemeMode.system,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash,
home: showSplash ? const SplashScreen() : const AuthGate(),
);
},
),
);
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
BlocProvider<ThemeCubit>(create: (_) => ThemeCubit()),
BlocProvider<ThemeModeCubit>(create: (_) => ThemeModeCubit()),
],
child: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, themeState) {
return BlocBuilder<ThemeModeCubit, ThemeModeState>(
builder: (context, modeState) {
return MaterialApp(
debugShowCheckedModeBanner: false,
locale: _locale ?? const Locale('en'),
supportedLocales: const [
Locale('en'),
Locale('hi'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
title: 'kMobile',
theme: themeState.getLightThemeData(),
darkTheme: themeState.getDarkThemeData(),
themeMode: context.watch<ThemeModeCubit>().state.mode,
onGenerateRoute: AppRoutes.generateRoute,
home: const AuthGate(),
);
},
);
},
),
);
}
}
}
class AuthGate extends StatefulWidget {
const AuthGate({super.key});
@override
State<AuthGate> createState() => _AuthGateState();
}
@@ -124,7 +145,6 @@ class AuthGate extends StatefulWidget {
class _AuthGateState extends State<AuthGate> {
bool _checking = true;
bool _isLoggedIn = false;
bool _showWelcome = true;
bool _hasMPin = false;
bool _biometricEnabled = false;
bool _biometricTried = false;
@@ -142,8 +162,7 @@ class _AuthGateState extends State<AuthGate> {
final mpin = await storage.read('mpin');
final biometric = await storage.read('biometric_enabled');
setState(() {
_isLoggedIn =
accessToken != null &&
_isLoggedIn = accessToken != null &&
accessTokenExpiry != null &&
DateTime.parse(accessTokenExpiry).isAfter(DateTime.now());
_hasMPin = mpin != null;
@@ -158,9 +177,13 @@ class _AuthGateState extends State<AuthGate> {
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) return false;
String localizedReason = "";
if (mounted) {
localizedReason = AppLocalizations.of(context).authenticateToAccess;
}
try {
final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).authenticateToAccess,
localizedReason: localizedReason,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
@@ -172,24 +195,15 @@ class _AuthGateState extends State<AuthGate> {
}
}
@override
Widget build(BuildContext context) {
if (_checking) {
return const SplashScreen();
}
// ✅ Step 1: Show welcome screen first, only once
if (_showWelcome) {
return WelcomeScreen(
onContinue: () {
setState(() {
_showWelcome = false;
});
},
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
// ✅ Step 2: Check login status
if (_isLoggedIn) {
if (_hasMPin) {
if (_biometricEnabled) {
@@ -197,14 +211,15 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (snapshot.data == true) {
return const NavigationScaffold(); // Authenticated
return const NavigationScaffold();
}
// ❌ Biometric failed → Show MPIN screen
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
@@ -230,13 +245,11 @@ class _AuthGateState extends State<AuthGate> {
);
}
} else {
// No MPIN set → show MPIN set screen + biometric dialog
return MPinScreen(
mode: MPinMode.set,
onCompleted: (_) async {
final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication();
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -257,15 +270,16 @@ class _AuthGateState extends State<AuthGate> {
],
),
);
if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false;
String authEnable = "";
if (context.mounted) {
authEnable = AppLocalizations.of(context).authenticateToEnable;
}
if (canCheck) {
didAuth = await localAuth.authenticate(
localizedReason:
AppLocalizations.of(context).authenticateToEnable,
localizedReason: authEnable,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
@@ -277,25 +291,23 @@ class _AuthGateState extends State<AuthGate> {
await storage.write('biometric_enabled', 'false');
}
}
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
if (context.mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
}
},
);
}
}
// ✅ Step 3: If not logged in, show login screen
return const LoginScreen();
}
}
class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key});
@override
State<NavigationScaffold> createState() => _NavigationScaffoldState();
}
@@ -303,20 +315,12 @@ class NavigationScaffold extends StatefulWidget {
class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController();
int _selectedIndex = 0;
final List<Widget> _pages = [
const DashboardScreen(),
const CardManagementScreen(),
const ServiceScreen(),
];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
}
@override
Widget build(BuildContext context) {
return PopScope(
@@ -357,10 +361,15 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed,
backgroundColor: Theme.of(context).scaffoldBackgroundColor, // Light blue background
selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: Colors.black54,
onTap: _onItemTapped,
backgroundColor: const Color(0XFF1E58AD),
selectedItemColor: Theme.of(context).colorScheme.onPrimary,
unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
},
items: [
BottomNavigationBarItem(
icon: const Icon(Icons.home_filled),
@@ -381,34 +390,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
}
}
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text(
AppLocalizations.of(context).loading,
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
);
}
}
// Add this widget at the end of the file
class BiometricPromptScreen extends StatelessWidget {
final VoidCallback onCompleted;
const BiometricPromptScreen({super.key, required this.onCompleted});
Future<void> _handleBiometric(BuildContext context) async {
final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics;
@@ -416,8 +400,12 @@ class BiometricPromptScreen extends StatelessWidget {
onCompleted();
return;
}
String localizedReason = "";
if (context.mounted) {
localizedReason = AppLocalizations.of(context).enableFingerprintQuick;
}
final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).enableFingerprintQuick,
localizedReason: localizedReason,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
@@ -435,7 +423,11 @@ class BiometricPromptScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context));
return const SplashScreen();
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
Future<void> _showDialog(BuildContext context) async {
@@ -461,6 +453,9 @@ class BiometricPromptScreen extends StatelessWidget {
],
),
);
if (!context.mounted) {
return;
}
if (result == true) {
await _handleBiometric(context);
} else {

View File

@@ -13,7 +13,7 @@ import '../features/dashboard/screens/dashboard_screen.dart';
class AppRoutes {
// Private constructor to prevent instantiation
AppRoutes._();
// Route names
static const String splash = '/';
static const String login = '/login';
@@ -25,23 +25,24 @@ class AppRoutes {
static const String accounts = '/accounts';
static const String transactions = '/transactions';
static const String payments = '/payments';
// Route generator
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case splash:
return MaterialPageRoute(builder: (_) => const SplashScreen());
case login:
return MaterialPageRoute(builder: (_) => const LoginScreen());
case mPin:
return MaterialPageRoute(builder: (_) => const MPinScreen(mode: MPinMode.enter,));
return MaterialPageRoute(
builder: (_) => const MPinScreen(
mode: MPinMode.enter,
));
case register:
// Placeholder - create the RegisterScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const RegisterScreen());
return _errorRoute();
case forgotPassword:
// Placeholder - create the ForgotPasswordScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const ForgotPasswordScreen());
@@ -49,31 +50,30 @@ class AppRoutes {
case navigationScaffold:
return MaterialPageRoute(builder: (_) => const NavigationScaffold());
case dashboard:
return MaterialPageRoute(builder: (_) => const DashboardScreen());
case accounts:
// Placeholder - create the AccountsScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const AccountsScreen());
return _errorRoute();
case transactions:
// Placeholder - create the TransactionsScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const TransactionsScreen());
return _errorRoute();
case payments:
// Placeholder - create the PaymentsScreen class and uncomment
// return MaterialPageRoute(builder: (_) => const PaymentsScreen());
return _errorRoute();
default:
return _errorRoute();
}
}
// Error route
static Route<dynamic> _errorRoute() {
return MaterialPageRoute(builder: (_) {
@@ -87,4 +87,4 @@ class AppRoutes {
);
});
}
}
}

View File

@@ -1,6 +1,7 @@
enum ThemeType {
violet,
green,
orange,
blue,
}
violet,
green,
orange,
blue,
yellow,
}

View File

@@ -1,32 +1,52 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'theme_type.dart';
import 'package:google_fonts/google_fonts.dart';
class AppThemes {
static ThemeData getLightTheme(ThemeType type) {
switch (type) {
case ThemeType.green:
return ThemeData(primarySwatch: Colors.green);
case ThemeType.orange:
return ThemeData(primarySwatch: Colors.orange);
case ThemeType.blue:
return ThemeData(primarySwatch: Colors.blue);
case ThemeType.violet:
default:
return ThemeData(primarySwatch: Colors.deepPurple);
}
}
final Color seedColor = _getSeedColor(type);
final colorScheme = ColorScheme.fromSeed(
seedColor: seedColor,
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(
colorScheme: colorScheme,
useMaterial3: true,
textTheme: GoogleFonts.rubikTextTheme(
ThemeData(brightness: Brightness.dark).textTheme,
),
);
}
static Color _getSeedColor(ThemeType type) {
switch (type) {
case ThemeType.green:
return ThemeData.dark().copyWith(primaryColor: Colors.green);
return Colors.green;
case ThemeType.orange:
return ThemeData.dark().copyWith(primaryColor: Colors.orange);
return Colors.orange;
case ThemeType.blue:
return ThemeData.dark().copyWith(primaryColor: Colors.blue);
return Colors.blue;
case ThemeType.violet:
default:
return ThemeData.dark().copyWith(primaryColor: Colors.deepPurple);
return Colors.deepPurple;
case ThemeType.yellow:
return Colors.yellow;
}
}
}
}

View File

@@ -1,8 +1,8 @@
class AppException implements Exception {
final String message;
AppException(this.message);
@override
String toString() => message;
}
@@ -11,7 +11,7 @@ class NetworkException extends AppException {
NetworkException(super.message);
}
class AuthException implements Exception{
class AuthException implements Exception {
final String message;
AuthException(this.message);
@@ -21,4 +21,4 @@ class AuthException implements Exception{
class UnexpectedException extends AppException {
UnexpectedException(super.message);
}
}

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

@@ -1,7 +1,3 @@
// ignore_for_file: non_constant_identifier_names
class Beneficiary {
final String accountNo;
final String accountType;
@@ -9,6 +5,7 @@ class Beneficiary {
final String ifscCode;
final String? bankName;
final String? branchName;
final String? tpin;
Beneficiary({
required this.accountNo,
@@ -17,31 +14,43 @@ class Beneficiary {
required this.ifscCode,
this.bankName,
this.branchName,
this.tpin,
});
factory Beneficiary.fromJson(Map<String, dynamic> json) {
return Beneficiary(
accountNo: json['accountNo'] ?? '',
accountType: json['accountType'] ?? '',
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
accountType: json['account_type'] ?? json['accountType'] ?? '',
name: json['name'] ?? '',
ifscCode: json['ifscCode'] ?? '',
bankName: json['bankName'] ?? '',
branchName: json['branchName'] ?? '',
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
bankName: json['bank_name'] ?? json['bankName'] ?? '',
branchName: json['branch_name'] ?? json['branchName'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
final Map<String, dynamic> json = {
'accountNo': accountNo,
'accountType': accountType,
'name': name,
'ifscCode' : ifscCode,
'ifscCode': ifscCode,
};
if (bankName != null && bankName!.isNotEmpty) {
json['bankName'] = bankName;
}
if (branchName != null && branchName!.isNotEmpty) {
json['branchName'] = branchName;
}
if (tpin != null && tpin!.isNotEmpty) {
json['tpin'] = tpin;
}
return json;
}
static List<Beneficiary> listFromJson(List<dynamic> jsonList) {
final beneficiaryList = jsonList.map((beneficiary) => Beneficiary.fromJson(beneficiary)).toList();
print(beneficiaryList);
final beneficiaryList = jsonList
.map((beneficiary) => Beneficiary.fromJson(beneficiary))
.toList();
return beneficiaryList;
}
@@ -49,4 +58,4 @@ class Beneficiary {
String toString() {
return 'Beneficiary(accountNo: $accountNo, accountType: $accountType, ifscCode: $ifscCode, name: $name)';
}
}
}

View File

@@ -1,45 +0,0 @@
class BeneficiaryRecieve {
final String accountNo;
final String accountType;
final String name;
final String ifscCode;
final String bankName;
final String branchName;
BeneficiaryRecieve({
required this.accountNo,
required this.accountType,
required this.name,
required this.ifscCode,
required this.bankName,
required this.branchName,
});
factory BeneficiaryRecieve.fromJson(Map<String, dynamic> json) {
return BeneficiaryRecieve(
accountNo: json['account_no'] ?? '',
accountType: json['account_type'] ?? '',
name: json['name'] ?? '',
ifscCode: json['ifsc_code'] ?? '',
bankName: json['bank_name'] ?? '',
branchName: json['branch_name'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'account_no': accountNo,
'account_type': accountType,
'name': name,
'ifsc_code' : ifscCode,
'bank_name' : bankName,
'branch_name' : branchName
};
}
@override
String toString() {
return 'ListBeneficiary(accountNo: $accountNo, accountType: $accountType, ifscCode: $ifscCode, name: $name, bankName: $bankName, branchName: $branchName)';
}
}

View File

@@ -1,16 +1,16 @@
class ifsc {
class Ifsc {
final String ifscCode;
final String bankName;
final String branchName;
ifsc({
Ifsc({
required this.ifscCode,
required this.bankName,
required this.branchName,
});
factory ifsc.fromJson(Map<String, dynamic> json) {
return ifsc(
factory Ifsc.fromJson(Map<String, dynamic> json) {
return Ifsc(
ifscCode: json['ifsc_code'] ?? '',
bankName: json['bank_name'] ?? '',
branchName: json['branch_name'] ?? '',
@@ -29,4 +29,4 @@ class ifsc {
String toString() {
return 'IFSC(ifscCode: $ifscCode, bankName: $bankName, branchName: $branchName)';
}
}
}

View File

@@ -0,0 +1,16 @@
class ImpsResponse {
final String message;
final String utr;
ImpsResponse({
required this.message,
required this.utr,
});
factory ImpsResponse.fromJson(Map<String, dynamic> json) {
return ImpsResponse(
message: json['message'] ?? '',
utr: json['utr'] ?? '',
);
}
}

View File

@@ -0,0 +1,34 @@
class ImpsTransaction {
final String fromAccount;
final String toAccount;
final String amount;
final String ifscCode;
final String? remitterName;
final String beneficiaryName;
final String tpin;
final String? remarks;
ImpsTransaction({
required this.fromAccount,
required this.toAccount,
required this.amount,
required this.ifscCode,
this.remitterName,
required this.beneficiaryName,
required this.tpin,
this.remarks,
});
Map<String, dynamic> toJson() {
return {
'fromAccount': fromAccount,
'toAccount': toAccount,
'amount': amount,
'ifscCode': ifscCode,
'remitterName': remitterName,
'beneficiaryName': beneficiaryName,
'tpin': tpin,
'remarks': remarks,
};
}
}

View File

@@ -0,0 +1,16 @@
class NeftResponse {
final String message;
final String utr;
NeftResponse({
required this.message,
required this.utr,
});
factory NeftResponse.fromJson(Map<String, dynamic> json) {
return NeftResponse(
message: json['message'] ?? '',
utr: json['utr'] ?? '',
);
}
}

View File

@@ -0,0 +1,34 @@
class NeftTransaction {
final String fromAccount;
final String toAccount;
final String amount;
final String ifscCode;
final String remitterName;
final String beneficiaryName;
final String tpin;
final String? remarks;
NeftTransaction({
required this.fromAccount,
required this.toAccount,
required this.amount,
required this.ifscCode,
required this.remitterName,
required this.beneficiaryName,
required this.tpin,
this.remarks,
});
Map<String, dynamic> toJson() {
return {
'fromAccount': fromAccount,
'toAccount': toAccount,
'amount': amount,
'ifscCode': ifscCode,
'remitterName': remitterName,
'beneficiaryName': beneficiaryName,
'tpin': tpin,
'remarks': remarks,
};
}
}

View File

@@ -6,6 +6,7 @@ class PaymentResponse {
final String? currency;
final String? errorMessage;
final String? errorCode;
final String? utr;
PaymentResponse({
required this.isSuccess,
@@ -15,5 +16,6 @@ class PaymentResponse {
this.currency,
this.errorMessage,
this.errorCode,
this.utr,
});
}

View File

@@ -0,0 +1,16 @@
class RtgsResponse {
final String message;
final String utr;
RtgsResponse({
required this.message,
required this.utr,
});
factory RtgsResponse.fromJson(Map<String, dynamic> json) {
return RtgsResponse(
message: json['message'] ?? '',
utr: json['utr'] ?? '',
);
}
}

View File

@@ -0,0 +1,34 @@
class RtgsTransaction {
final String fromAccount;
final String toAccount;
final String amount;
final String ifscCode;
final String remitterName;
final String beneficiaryName;
final String tpin;
final String? remarks;
RtgsTransaction({
required this.fromAccount,
required this.toAccount,
required this.amount,
required this.ifscCode,
required this.remitterName,
required this.beneficiaryName,
required this.tpin,
this.remarks,
});
Map<String, dynamic> toJson() {
return {
'fromAccount': fromAccount,
'toAccount': toAccount,
'amount': amount,
'ifscCode': ifscCode,
'remitterName': remitterName,
'beneficiaryName': beneficiaryName,
'tpin': tpin,
'remarks': remarks,
};
}
}

View File

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

View File

@@ -10,7 +10,7 @@ class AuthRepository {
final AuthService _authService;
final UserService _userService;
final SecureStorage _secureStorage;
static const _accessTokenKey = 'access_token';
static const _tokenExpiryKey = 'token_expiry';
@@ -18,35 +18,41 @@ class AuthRepository {
Future<List<User>> login(String customerNo, String password) async {
// Create credentials and call service
final credentials = AuthCredentials(customerNo: customerNo, password: password);
final credentials =
AuthCredentials(customerNo: customerNo, password: password);
final authToken = await _authService.login(credentials);
// Save token securely
await _saveAuthToken(authToken);
// Get and save user profile
final users = await _userService.getUserDetails();
return users;
}
Future<bool> isLoggedIn() async {
final token = await _getAuthToken();
return token != null && !token.isExpired;
}
Future<String?> getAccessToken() async {
final token = await _getAuthToken();
if (token == null) return null;
return token.accessToken;
}
// Private helper methods
Future<void> _saveAuthToken(AuthToken token) async {
await _secureStorage.write(_accessTokenKey, token.accessToken);
await _secureStorage.write(_tokenExpiryKey, token.expiresAt.toIso8601String());
await _secureStorage.write(
_tokenExpiryKey, token.expiresAt.toIso8601String());
}
Future<void> clearAuthTokens() async {
await _secureStorage.deleteAll();
}
Future<AuthToken?> _getAuthToken() async {
final accessToken = await _secureStorage.read(_accessTokenKey);
final expiryString = await _secureStorage.read(_tokenExpiryKey);
@@ -59,4 +65,4 @@ class AuthRepository {
}
return null;
}
}
}

View File

@@ -1,8 +1,10 @@
import 'package:dio/dio.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/data/models/transaction.dart';
abstract class TransactionRepository {
Future<List<Transaction>> fetchTransactions(String accountNo);
Future<List<Transaction>> fetchTransactions(String accountNo,
{DateTime? fromDate, DateTime? toDate});
}
class TransactionRepositoryImpl implements TransactionRepository {
@@ -10,9 +12,21 @@ class TransactionRepositoryImpl implements TransactionRepository {
TransactionRepositoryImpl(this._dio);
@override
Future<List<Transaction>> fetchTransactions(String accountNo) async {
Future<List<Transaction>> fetchTransactions(String accountNo,
{DateTime? fromDate, DateTime? toDate}) async {
final queryParameters = <String, String>{};
final resp = await _dio.get('/api/transactions/account/$accountNo');
if (fromDate != null) {
queryParameters['fromDate'] = DateFormat('ddMMyyyy').format(fromDate);
}
if (toDate != null) {
queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate);
}
final resp = await _dio.get(
'/api/transactions/account/$accountNo',
queryParameters: queryParameters.isNotEmpty ? queryParameters : null,
);
if (resp.statusCode != 200) {
throw Exception(
@@ -25,4 +39,4 @@ class TransactionRepositoryImpl implements TransactionRepository {
.map((e) => Transaction.fromJson(e as Map<String, dynamic>))
.toList();
}
}
}

View File

@@ -1,7 +1,6 @@
import 'package:kmobile/data/models/transfer.dart';
import 'package:dio/dio.dart';
abstract class TransferRepository {
Future<TransferResponse> transfer(Transfer transfer);
}
@@ -25,4 +24,4 @@ class TransferRepositoryImpl implements TransferRepository {
return TransferResponse.fromJson(resp.data as Map<String, dynamic>);
}
}
}

View File

@@ -1,3 +1,7 @@
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/imps_service.dart';
import 'package:get_it/get_it.dart';
import 'package:dio/dio.dart';
import 'package:kmobile/api/services/beneficiary_service.dart';
@@ -5,20 +9,21 @@ import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/api/services/user_service.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_mode_cubit.dart';
import '../api/services/auth_service.dart';
import '../api/interceptors/auth_interceptor.dart';
import '../api/services/change_password_service.dart';
import '../data/repositories/auth_repository.dart';
import '../features/auth/controllers/auth_cubit.dart';
import '../security/secure_storage.dart';
final getIt = GetIt.instance;
Future<void> setupDependencies() async {
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
getIt.registerSingleton<ThemeCubit>( ThemeCubit());
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
getIt.registerSingleton<ThemeModeCubit>(ThemeModeCubit());
// Register Dio client
getIt.registerSingleton<Dio>(_createDioClient());
@@ -42,6 +47,13 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>(
() => ChangePasswordService(getIt<Dio>()),
);
// Add auth interceptor after repository is available
getIt<Dio>().interceptors.add(
@@ -51,20 +63,21 @@ Future<void> setupDependencies() async {
// Register controllers/cubits
getIt.registerFactory<AuthCubit>(
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
}
Dio _createDioClient() {
final dio = Dio(
BaseOptions(
baseUrl:
//'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080',
'http://localhost:8081',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
// 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Login-Type': 'MB',
},
),
);

View File

@@ -4,7 +4,7 @@ class Account {
final String accountType;
final double balance;
final String currency;
Account({
required this.id,
required this.accountNumber,
@@ -12,4 +12,4 @@ class Account {
required this.balance,
required this.currency,
});
}
}

View File

@@ -1,7 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart';
class AccountInfoScreen extends StatefulWidget {
@@ -32,35 +30,9 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
int selectedIndex = widget.selectedIndex;
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
AppLocalizations.of(context).accountInfo,
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
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,
),
),
),
],
title: Text(AppLocalizations.of(context)
.accountInfo
.replaceFirst(RegExp('\n'), '')),
),
body: ListView(
padding: const EdgeInsets.all(16.0),
@@ -70,7 +42,6 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
),
/// Dropdown to change account
DropdownButton<User>(
value: selectedUser,
onChanged: (User? newUser) {
@@ -88,12 +59,6 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
}).toList(),
),
/*InfoRow(
title: AppLocalizations.of(context).accountNumber,
value: users[selectedIndex].accountNo ?? 'N/A'),
// InfoRow(title: 'Nominee Customer No', value: user.nomineeCustomerNo),
// InfoRow(title: 'SMS Service', value: user.smsService),
// InfoRow(title: 'Missed Call Service', value: user.missedCallService),*/
InfoRow(
title: AppLocalizations.of(context).customerNumber,
value: selectedUser.cifNumber ?? 'N/A',
@@ -136,6 +101,7 @@ class InfoRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 8),
@@ -144,16 +110,16 @@ class InfoRow extends StatelessWidget {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black87,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 3),
Text(
value,
style: const TextStyle(fontSize: 16, color: Colors.black),
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
),
],
),

View File

@@ -1,15 +1,29 @@
import 'dart:io';
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:shimmer/shimmer.dart';
import 'package:kmobile/data/models/transaction.dart';
import 'package:kmobile/data/repositories/transaction_repository.dart';
import 'package:kmobile/di/injection.dart';
import '../../../l10n/app_localizations.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 {
final String accountNo;
const AccountStatementScreen({super.key, required this.accountNo});
final String balance;
final String accountType;
const AccountStatementScreen({
super.key,
required this.accountNo,
required this.balance,
required this.accountType,
});
@override
State<AccountStatementScreen> createState() => _AccountStatementScreen();
@@ -22,6 +36,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
List<Transaction> _transactions = [];
final _minAmountController = TextEditingController();
final _maxAmountController = TextEditingController();
//Future<Map<String, dynamic>?>? accountStatementsFuture;
@override
void initState() {
@@ -36,7 +51,11 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
});
try {
final repo = getIt<TransactionRepository>();
final txs = await repo.fetchTransactions(widget.accountNo);
final txs = await repo.fetchTransactions(
widget.accountNo,
fromDate: fromDate,
toDate: toDate,
);
setState(() => _transactions = txs);
} catch (e) {
if (!mounted) return;
@@ -109,30 +128,10 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppLocalizations.of(context).accountStatement,
style: const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
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(
padding: const EdgeInsets.all(12.0),
@@ -144,7 +143,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
Text(
"${AppLocalizations.of(context).accountNumber}: ",
style: const TextStyle(
fontSize: 19,
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
@@ -152,6 +151,19 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
],
),
const SizedBox(height: 15),
Row(
children: [
Text(
"${AppLocalizations.of(context).availableBalance}: ",
style: const TextStyle(
fontSize: 17,
),
),
Text('${widget.balance}',
style: const TextStyle(fontSize: 17)),
],
),
const SizedBox(height: 15),
Text(
AppLocalizations.of(context).filters,
style: const TextStyle(fontSize: 17),
@@ -181,56 +193,29 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: TextFormField(
controller: _minAmountController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).minAmount,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _loadTransactions,
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(
AppLocalizations.of(context).search,
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontSize: 16,
),
),
const SizedBox(width: 8),
Expanded(
child: TextFormField(
controller: _maxAmountController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).maxAmount,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
),
),
const SizedBox(width: 8),
SizedBox(
width: 70,
child: ElevatedButton(
onPressed: _loadTransactions,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
),
child: Icon(
Symbols.arrow_forward,
color: Theme.of(context).scaffoldBackgroundColor,
size: 30,
),
),
),
],
),
),
const SizedBox(height: 35),
if (!_txLoading && _transactions.isNotEmpty)
const SizedBox(height: 15),
if (!_txLoading &&
_transactions.isNotEmpty &&
fromDate == null &&
toDate == null)
Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
@@ -238,7 +223,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
@@ -252,7 +237,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
highlightColor: Colors.grey[100]!,
child: CircleAvatar(
radius: 12,
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
),
),
title: Shimmer.fromColors(
@@ -278,12 +264,13 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
: _transactions.isEmpty
? Center(
child: Text(
AppLocalizations.of(context).noTransactions,
style: TextStyle(
fontSize: 16, color: Colors.grey[600]),
),
AppLocalizations.of(context).noTransactions,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
)),
)
: ListView.builder(
: ListView.separated(
itemCount: _transactions.length,
itemBuilder: (context, index) {
final tx = _transactions[index];
@@ -292,40 +279,247 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
tx.type == 'CR'
? Symbols.call_received
: Symbols.call_made,
color:
tx.type == 'CR' ? Colors.green : Colors.red,
color: tx.type == 'CR'
? Colors.green
: Theme.of(context).colorScheme.error,
),
title: Text(
tx.name != null
? (tx.name!.length > 18
? tx.name!.substring(0, 20)
: tx.name!)
: '',
style: const TextStyle(fontSize: 14),
tx.date ?? '',
style: const TextStyle(fontSize: 15),
),
subtitle: Text(
tx.date ?? '',
tx.name != null
? (tx.name!.length > 22
? tx.name!.substring(0, 22)
: tx.name!)
: '',
style: const TextStyle(fontSize: 12),
),
trailing: Text(
"${tx.amount}",
style: const TextStyle(fontSize: 16),
trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"${tx.amount}",
style: const TextStyle(fontSize: 17),
),
Text(
"Bal: ₹${tx.balance}",
style: const TextStyle(
fontSize: 12), // Style matches tx.name
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => TransactionDetailsScreen(
transaction: tx),
),
);
},
);
},
separatorBuilder: (context, index) {
return const Divider();
},
),
),
],
),
),
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) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
border:
Border.all(color: Theme.of(context).colorScheme.onSurfaceVariant),
borderRadius: BorderRadius.circular(6),
),
child: Row(
@@ -335,7 +529,9 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
date != null ? _formatDate(date) : label,
style: TextStyle(
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),

View File

@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:kmobile/l10n/app_localizations.dart';
import 'package:material_symbols_icons/symbols.dart';
class TransactionDetailsScreen extends StatelessWidget {
final dynamic transaction;
const TransactionDetailsScreen({super.key, required this.transaction});
@override
Widget build(BuildContext context) {
final bool isCredit = transaction.type?.toUpperCase() == 'CR';
return Scaffold(
appBar:
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
flex: 3,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Amount + icon + Share Button
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"${transaction.amount}",
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 8),
Icon(
isCredit ? Symbols.call_received : Symbols.call_made,
color: isCredit ? Colors.green : Colors.red,
size: 28,
),
],
),
const SizedBox(height: 8),
// Date centered
Text(
transaction.date ?? "",
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
),
),
),
const Divider(),
Expanded(
flex: 5,
child: ListView(
children: [
_buildDetailRow(AppLocalizations.of(context).transactionType,
transaction.type ?? ""),
_buildDetailRow(AppLocalizations.of(context).transferType,
transaction.name.split("/").first ?? ""),
// if (transaction.name.length > 12) ...[
// _buildDetailRow(AppLocalizations.of(context).utrNo,
// transaction.name.split("= ")[1].split(" ")[0] ?? ""),
// _buildDetailRow(
// AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "")
// ]
_buildDetailRow(
AppLocalizations.of(context).details, transaction.name),
],
),
),
],
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"$label: ",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17,
),
),
const SizedBox(height: 30),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 16),
),
),
],
),
);
}
}

View File

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

View File

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

View File

@@ -1,48 +1,27 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme_state.dart';
import 'package:kmobile/config/theme_type.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'theme_state.dart';
class ThemeCubit extends Cubit<ThemeState> {
ThemeCubit(): super(ThemeViolet()) {
ThemeCubit() : super(const ThemeState(themeType: ThemeType.violet)) {
loadTheme();
}
Future<void> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
final themeIndex = prefs.getInt('theme_type') ?? 0;
// final isDark = prefs.getBool('is_dark_mode') ?? false;
final type = ThemeType.values[themeIndex];
switch(type) {
case ThemeType.blue:
emit(ThemeBlue());
case ThemeType.violet:
emit(ThemeViolet());
default:
emit(ThemeViolet());
}
emit(ThemeState(themeType: type));
}
Future<void> changeTheme(ThemeType type) async {
log("Attempting to change theme...");
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('theme_type', type.index);
log("Mode Change");
print("mode changed");
switch(type) {
case ThemeType.blue:
emit(ThemeBlue());
print('blue matched');
break;
case ThemeType.violet:
emit(ThemeViolet());
print('violet matched');
break;
default:
emit(ThemeBlue());
print('default macthed');
}
emit(ThemeState(themeType: type));
}
}

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,27 +1,38 @@
import 'package:equatable/equatable.dart';
/*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;
abstract class ThemeState extends Equatable {
getThemeData();
@override
List<Object?> get props => [];
}
const ThemeState({required this.themeType});
class ThemeBlue extends ThemeState {
@override
getThemeData() {
print('returning blue theme');
return AppThemes.getLightTheme(ThemeType.blue);
ThemeData getThemeData() {
return AppThemes.getLightTheme(themeType);
}
}
class ThemeViolet extends ThemeState {
@override
getThemeData() {
print('returning violet theme');
return AppThemes.getLightTheme(ThemeType.violet);
List<Object?> get props => [themeType];
}*/
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:kmobile/config/theme_type.dart';
import 'package:kmobile/config/themes.dart';
class ThemeState extends Equatable {
final ThemeType themeType;
const ThemeState({required this.themeType});
ThemeData getLightThemeData() {
return AppThemes.getLightTheme(themeType);
}
}
ThemeData getDarkThemeData() {
return AppThemes.getDarkTheme(themeType);
}
@override
List<Object?> get props => [themeType];
}

View File

@@ -6,12 +6,12 @@ import 'package:equatable/equatable.dart';
class AuthToken extends Equatable {
final String accessToken;
final DateTime expiresAt;
const AuthToken({
required this.accessToken,
required this.expiresAt,
});
factory AuthToken.fromJson(Map<String, dynamic> json) {
return AuthToken(
accessToken: json['token'],
@@ -29,7 +29,8 @@ class AuthToken extends Equatable {
// Pad the payload if necessary
String normalized = base64Url.normalize(payload);
final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized)));
if (payloadMap is! Map<String, dynamic> || !payloadMap.containsKey('exp')) {
if (payloadMap is! Map<String, dynamic> ||
!payloadMap.containsKey('exp')) {
throw Exception('Invalid payload');
}
final exp = payloadMap['exp'];
@@ -40,9 +41,9 @@ class AuthToken extends Equatable {
return DateTime.now().add(const Duration(hours: 1));
}
}
bool get isExpired => DateTime.now().isAfter(expiresAt);
@override
List<Object> get props => [accessToken, expiresAt];
}

View File

@@ -1,13 +1,8 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../app.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/l10n/app_localizations.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@@ -22,24 +17,15 @@ class LoginScreenState extends State<LoginScreen>
final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
//bool _showWelcome = true;
late AnimationController _logoController;
late Animation<double> _logoAnimation;
@override
void initState() {
super.initState();
_logoController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(reverse: true);
_logoAnimation = Tween<double>(begin: 0.2, end: 1).animate(_logoController);
context.read<AuthCubit>().reset();
}
@override
void dispose() {
_logoController.dispose();
_customerNumberController.dispose();
_passwordController.dispose();
super.dispose();
@@ -47,9 +33,13 @@ class LoginScreenState extends State<LoginScreen>
void _submitForm() {
if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => VerificationScreen(
customerNo: _customerNumberController.text.trim(),
password: _passwordController.text,
),
),
);
}
}
@@ -58,426 +48,141 @@ class LoginScreenState extends State<LoginScreen>
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) async {
if (state is Authenticated) {
final storage = getIt<SecureStorage>();
final mpin = await storage.read('mpin');
if (mpin == null) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(
context,
rootNavigator: true,
).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
body: 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: 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;
});
},
),
),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
}
} 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: [
// 🔁 Animated Blinking Logo
FadeTransition(
opacity: _logoAnimation,
child: 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: OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: 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(), // ⌨️ Enter key submits
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: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 15),
Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text(AppLocalizations.of(context).or),
),
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),
),
),
],
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterPassword;
}
return null;
},
),
),
);
},
const SizedBox(height: 24),
//Login Button
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorDark,
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1),
elevation: 0,
),
child: Text(
AppLocalizations.of(context).login,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
),
const SizedBox(height: 15),
const SizedBox(height: 25),
],
),
),
),
);
}
}
/*import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../app.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
LoginScreenState createState() => LoginScreenState();
}
class LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_customerNumberController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submitForm() {
if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) async {
if (state is Authenticated) {
final storage = getIt<SecureStorage>();
final mpin = await storage.read('mpin');
if (mpin == null) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(context, rootNavigator: true)
.pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold()),
);
},
),
),
);
} else {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
}
} 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 const Icon(Icons.account_balance,
size: 100, color: Colors.blue);
}),
const SizedBox(height: 16),
// Title
const Text(
'KCCB',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue),
),
const SizedBox(height: 48),
TextFormField(
controller: _customerNumberController,
decoration: const InputDecoration(
labelText: 'Customer Number',
// prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Colors.white,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your username';
}
return null;
},
),
const SizedBox(height: 24),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
// prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Colors.white,
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;
});
},
),
),
textInputAction: TextInputAction.done,
obscureText: _obscurePassword,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter your password';
}
return null;
},
),
const SizedBox(height: 24),
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: state is AuthLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Colors.white,
foregroundColor: Colors.blueAccent,
side: const BorderSide(color: Colors.black, width: 1),
elevation: 0),
child: state is AuthLoading
? const CircularProgressIndicator()
: const Text(
'Login',
style: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 15),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Row(
children: [
Expanded(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('OR'),
),
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: Colors.lightBlue[100],
foregroundColor: Colors.black),
child: const Text('Register'),
),
),
],
),
),
);
},
),
);
}
}
*/

View File

@@ -1,12 +1,14 @@
import '../../../l10n/app_localizations.dart';
import 'dart:math';
import 'dart:developer';
// import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart';
enum MPinMode { enter, set, confirm }
@@ -26,26 +28,78 @@ class MPinScreen extends StatefulWidget {
State<MPinScreen> createState() => _MPinScreenState();
}
class _MPinScreenState extends State<MPinScreen> {
class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
List<String> mPin = [];
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
void 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) {
_tryBiometricBeforePin();
}
}
@override
void dispose() {
_bounceController.dispose();
_shakeController.dispose();
_waveController.dispose();
super.dispose();
}
Future<void> _tryBiometricBeforePin() async {
final storage = getIt<SecureStorage>();
final enabled = await storage.read('biometric_enabled');
log('biometric_enabled: $enabled');
// log('biometric_enabled: $enabled');
if (enabled != null && enabled) {
final auth = LocalAuthentication();
if (await auth.canCheckBiometrics) {
final didAuth = await auth.authenticate(
// ignore: use_build_context_synchronously
localizedReason: AppLocalizations.of(context).authenticateToAccess,
options: const AuthenticationOptions(biometricOnly: true),
);
@@ -61,11 +115,13 @@ class _MPinScreenState extends State<MPinScreen> {
}
void addDigit(String digit) {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.length < 4) {
setState(() {
mPin.add(digit);
errorText = null;
});
_bounceController.forward(from: 0);
if (mPin.length == 4) {
_handleComplete();
}
@@ -73,6 +129,7 @@ class _MPinScreenState extends State<MPinScreen> {
}
void deleteDigit() {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.isNotEmpty) {
setState(() {
mPin.removeLast();
@@ -82,19 +139,36 @@ class _MPinScreenState extends State<MPinScreen> {
}
Future<void> _handleComplete() async {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
final pin = mPin.join();
final storage = SecureStorage();
switch (widget.mode) {
case MPinMode.enter:
final storedPin = await storage.read('mpin');
log('storedPin: $storedPin');
// log('storedPin: $storedPin');
if (storedPin == int.tryParse(pin)) {
widget.onCompleted?.call(pin);
} else {
// Correct PIN
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;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear();
_isError = false;
// Keep error text until next digit is entered
});
}
break;
@@ -125,28 +199,81 @@ class _MPinScreenState extends State<MPinScreen> {
}
} else {
setState(() {
_isError = true;
errorText = AppLocalizations.of(context).pinsDoNotMatch;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear();
_isError = false;
});
}
break;
}
}
Widget buildMPinDots() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
return Container(
margin: const EdgeInsets.all(8),
width: 15,
height: 15,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: index < mPin.length ? Colors.black : Colors.grey[400],
Widget buildMPinDots(BuildContext context) {
return AnimatedBuilder(
animation: Listenable.merge(
[_bounceController, _shakeController, _waveController]),
builder: (context, child) {
double shakeOffset = 0;
if (_shakeController.isAnimating) {
// 4 cycles of sine wave for shake
shakeOffset = sin(_shakeController.value * 4 * pi) * 12;
}
return Transform.translate(
offset: Offset(shakeOffset, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
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,
),
),
);
}),
),
);
}),
},
);
}
@@ -164,9 +291,10 @@ class _MPinScreenState extends State<MPinScreen> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) {
return Padding(
padding: const EdgeInsets.all(8.0),
padding: const EdgeInsets.all(1.0),
child: GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
if (key == '<') {
deleteDigit();
} else if (key == 'Enter') {
@@ -184,11 +312,10 @@ class _MPinScreenState extends State<MPinScreen> {
}
},
child: Container(
width: 70,
height: 70,
decoration: BoxDecoration(
width: 80,
height: 80,
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey[200],
),
alignment: Alignment.center,
child: key == 'Enter'
@@ -196,8 +323,10 @@ class _MPinScreenState extends State<MPinScreen> {
: Text(
key == '<' ? '' : key,
style: TextStyle(
fontSize: 20,
color: key == 'Enter' ? Theme.of(context).primaryColor : Colors.black,
fontSize: 30,
color: key == 'Enter'
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.onSurface,
),
),
),
@@ -226,7 +355,7 @@ class _MPinScreenState extends State<MPinScreen> {
body: SafeArea(
child: Column(
children: [
const Spacer(),
const SizedBox(height: 50),
// Logo
Image.asset('assets/images/logo.png', height: 100),
const SizedBox(height: 20),
@@ -234,19 +363,18 @@ class _MPinScreenState extends State<MPinScreen> {
getTitle(),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
),
const SizedBox(height: 20),
buildMPinDots(),
const Spacer(),
buildMPinDots(context),
if (errorText != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
errorText!,
style: const TextStyle(color: Colors.red),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const Spacer(),
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

@@ -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,141 +0,0 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'dart:async';
class WelcomeScreen extends StatefulWidget {
final VoidCallback onContinue;
const WelcomeScreen({super.key, required this.onContinue});
@override
State<WelcomeScreen> createState() => _WelcomeScreenState();
}
class _WelcomeScreenState extends State<WelcomeScreen> {
@override
void initState() {
super.initState();
// Automatically go to logizn after 4 seconds
Timer(const Duration(seconds: 4), () {
widget.onContinue();
}
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
/// 🔹 Background Image
Positioned.fill(
child: Image.asset(
'assets/images/kconnect2.webp',
fit: BoxFit.cover,
),
),
/// 🔹 Centered Text Overlay
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).kconnect,
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Theme.of(context).dialogBackgroundColor,
letterSpacing: 1.5,
),
),
const SizedBox(height: 12),
Text(
AppLocalizations.of(context).kccBankFull,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
color: Theme.of(context).dialogBackgroundColor,
letterSpacing: 1.2,
),
),
],
),
),
/// 🔹 Loading Spinner at Bottom
Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: CircularProgressIndicator(color: Theme.of(context).scaffoldBackgroundColor),
),
),
],
),
);
}
}
/*import 'package:flutter/material.dart';
class WelcomeScreen extends StatelessWidget {
final VoidCallback onContinue;
const WelcomeScreen({super.key, required this.onContinue});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Spacer(),
const Text(
'Welcome to',
style: TextStyle(fontSize: 28, color: Colors.black87),
),
const SizedBox(height: 10),
const Text(
'KCCB',
style: TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Colors.indigo,
),
),
const SizedBox(height: 40),
Image.asset(
'assets/images/logo.png',
width: 150,
height: 150,
),
const Spacer(),
ElevatedButton(
onPressed: onContinue,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 28, vertical: 20),
),
child: const Text('Proceed to Login'),
),
const SizedBox(height: 24),
],
),
),
),
);
}
}
*/

View File

@@ -1,22 +1,20 @@
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:flutter/services.dart';
import 'package:kmobile/api/services/beneficiary_service.dart';
import 'package:kmobile/data/models/ifsc.dart';
import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/data/models/user.dart';
import 'beneficiary_result_page.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'beneficiary_result_page.dart';
import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
class AddBeneficiaryScreen extends StatefulWidget {
final List<User>? users;
final int? selectedIndex;
final String customerName;
const AddBeneficiaryScreen({
super.key,
this.users,
this.selectedIndex,
required this.customerName,
});
@override
@@ -25,8 +23,9 @@ class AddBeneficiaryScreen extends StatefulWidget {
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final _formKey = GlobalKey<FormState>();
late User selectedUser = (widget.users ?? [])[widget.selectedIndex!];
final _accountNumberFieldKey = GlobalKey<FormFieldState>();
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
final _ifscFieldKey = GlobalKey<FormFieldState>();
final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController =
TextEditingController();
@@ -35,216 +34,234 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final TextEditingController branchNameController = TextEditingController();
final TextEditingController ifscController = TextEditingController();
final TextEditingController phoneController = TextEditingController();
String? _beneficiaryName;
final _ifscFocusNode = FocusNode();
final service = getIt<BeneficiaryService>();
bool _isValidating = false;
bool _isBeneficiaryValidated = false;
// ignore: unused_field
String? _validationError;
late String accountType;
@override
void initState() {
super.initState();
_ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
accountType = AppLocalizations.of(context).savings;
accountType = 'Savings';
});
});
}
ifsc? _ifscData;
bool _isLoading = false; //for validateIFSC()
void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return;
setState(() {
_isLoading = true;
_ifscData = null;
});
final result = await beneficiaryService.validateIFSC(ifsc);
setState(() {
_isLoading = false;
_ifscData = result;
});
if (result == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
);
bankNameController.clear();
branchNameController.clear();
} else {
print("${AppLocalizations.of(context).validIfsc}: ${result.bankName}, ${result.branchName}");
bankNameController.text = result.bankName;
branchNameController.text = result.branchName;
@override
void dispose() {
accountNumberController.dispose();
confirmAccountNumberController.dispose();
nameController.dispose();
bankNameController.dispose();
branchNameController.dispose();
ifscController.dispose();
phoneController.dispose();
_ifscFocusNode.dispose();
super.dispose();
}
}
Future<void> _validateBeneficiary() async {
// start spinner / disable button
setState(() {
_isValidating = true;
_validationError = null;
_isBeneficiaryValidated = false;
nameController.text = ''; // clear previous name
});
void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return;
final String accountNo = accountNumberController.text.trim();
final String ifsc = ifscController.text.trim();
final String remitter = selectedUser.name ?? '';
try {
final result = await beneficiaryService.validateIFSC(ifsc);
final service = getIt<BeneficiaryService>();
try {
// Step 1: call validate API -> get refNo
final String? refNo = await service.validateBeneficiary(
accountNo: accountNo,
ifscCode: ifsc,
remitterName: remitter,
);
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 (refNo == null || refNo.isEmpty) {
setState(() {
_validationError = 'Validation request failed. Please check details.';
_isBeneficiaryValidated = false;
});
return;
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
snackbarMessage = AppLocalizations.of(context).invalidIfsc;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(snackbarMessage)),
);
bankNameController.clear();
branchNameController.clear();
}
}
}
// Step 2: poll checkValidationStatus for up to 30 seconds
const int timeoutSeconds = 30;
const int intervalSeconds = 2;
int elapsed = 0;
String? foundName;
void _validateBeneficiary() async {
FocusScope.of(context).unfocus();
setState(() {
_isValidating = true;
_validationError = null;
_isBeneficiaryValidated = false;
nameController.text = ''; // clear previous name
});
while (elapsed < timeoutSeconds) {
final String? name = await service.checkValidationStatus(refNo);
final String accountNo = accountNumberController.text.trim();
final String ifsc = ifscController.text.trim();
final String remitter = widget.customerName;
if (name != null && name.trim().isNotEmpty) {
foundName = name.trim();
break;
final service = getIt<BeneficiaryService>();
try {
String beneficiaryName;
if (ifsc.toLowerCase().contains('kace')) {
beneficiaryName =
await service.validateBeneficiaryWithinBank(accountNo);
} else {
beneficiaryName = await service.validateBeneficiary(
accountNo: accountNo,
ifscCode: ifsc,
remitterName: remitter,
);
}
await Future.delayed(const Duration(seconds: intervalSeconds));
elapsed += intervalSeconds;
}
if (foundName != null) {
setState(() {
nameController.text = foundName!;
nameController.text = beneficiaryName;
_isBeneficiaryValidated = true;
_validationError = null;
});
} else {
} catch (e) {
setState(() {
_validationError = 'Beneficiary not found within timeout.';
_validationError = e.toString();
_isBeneficiaryValidated = false;
});
}
} catch (e, st) {
// handle unexpected errors
// print or log if you want
debugPrint('Error validating beneficiary: $e\n$st');
setState(() {
_validationError = 'Something went wrong. Please try again.';
_isBeneficiaryValidated = false;
});
} finally {
if (mounted) {
setState(() {
_isValidating = false;
});
} finally {
if (mounted) {
setState(() {
_isValidating = false;
});
}
}
}
}
String _selectedAccountType = 'Savings'; // default value
void validateAndAddBeneficiary() async {
// Show spinner and disable UI
showDialog(
context: context,
barrierDismissible: false, // Prevent dismiss on tap outside
builder: (BuildContext context) {
return WillPopScope(
onWillPop: () async => false, // Disable back button
child: const Center(
child: CircularProgressIndicator(),
),
);
},
);
final beneficiary = Beneficiary(
accountNo: accountNumberController.text.trim(),
accountType: _selectedAccountType,
name: nameController.text.trim(),
ifscCode: ifscController.text.trim(),
);
var service = getIt<BeneficiaryService>();
try {
await service.sendForValidation(beneficiary);
bool isFound = await service.checkIfFound(beneficiary.accountNo);
if (context.mounted) {
Navigator.pop(context); // Close the spinner
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BeneficiaryResultPage(isSuccess: isFound),
),
);
void validateAndAddBeneficiary() async {
// First, validate the form fields (account number, confirm account, ifsc, etc.)
if (!_formKey.currentState!.validate()) {
return;
}
} catch (e) {
Navigator.pop(context); // Close the spinner
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).somethingWentWrong)),
// Ensure beneficiary is validated before proceeding to TPIN
if (!_isBeneficiaryValidated) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context)
.pleaseValidateBeneficiaryDetailsFirst)),
);
return;
}
// Create the beneficiary object without TPIN for now
final beneficiaryWithoutTpin = Beneficiary(
accountNo: accountNumberController.text.trim(),
accountType: accountType,
name: nameController.text.trim(),
ifscCode: ifscController.text.trim(),
bankName: bankNameController.text.trim(),
branchName: branchNameController.text.trim(),
);
// Navigate to TPIN screen
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TransactionPinScreen(
onPinCompleted: (pinScreenContext, tpin) async {
// Show loading spinner while processing
showDialog(
context: pinScreenContext,
barrierDismissible: false,
builder: (BuildContext ctx) {
// ignore: deprecated_member_use
return WillPopScope(
onWillPop: () async => false,
child: const Center(
child: CircularProgressIndicator(),
),
);
},
);
// Create a new Beneficiary object with the TPIN
final beneficiaryWithTpin = Beneficiary(
accountNo: beneficiaryWithoutTpin.accountNo,
accountType: beneficiaryWithoutTpin.accountType,
name: beneficiaryWithoutTpin.name,
ifscCode: beneficiaryWithoutTpin.ifscCode,
bankName: beneficiaryWithoutTpin.bankName,
branchName: beneficiaryWithoutTpin.branchName,
tpin: tpin,
);
try {
final isSuccess =
await service.sendForValidation(beneficiaryWithTpin);
if (pinScreenContext.mounted) {
Navigator.pop(pinScreenContext); // Close the spinner
Navigator.pushReplacement(
pinScreenContext,
MaterialPageRoute(
builder: (ctx) =>
BeneficiaryResultPage(isSuccess: isSuccess),
),
);
}
} on DioException catch (e) {
if (pinScreenContext.mounted) {
Navigator.pop(pinScreenContext); // Close the spinner
ScaffoldMessenger.of(pinScreenContext).showSnackBar(
SnackBar(
content: Text(e.response?.statusCode == 409
? 'Beneficiary already exists'
: 'Something went Wrong')),
);
}
} catch (e) {
if (pinScreenContext.mounted) {
Navigator.pop(pinScreenContext); // Close the spinner
ScaffoldMessenger.of(pinScreenContext).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).somethingWentWrong)),
);
}
}
},
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Symbols.arrow_back_ios_new),
onPressed: () {
Navigator.pop(context);
},
),
title: Text(
AppLocalizations.of(context).addBeneficiary,
style: TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
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(
child: Form(
@@ -259,29 +276,25 @@ void validateAndAddBeneficiary() async {
child: Column(
children: [
TextFormField(
key: _accountNumberFieldKey,
controller: accountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).accountNumber,
// prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
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,
),
),
),
obscureText: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
onChanged: (value) {
nameController.clear();
setState(() {
_isBeneficiaryValidated = false;
});
},
validator: (value) {
if (value == null || value.length < 10) {
return AppLocalizations.of(
@@ -294,25 +307,15 @@ void validateAndAddBeneficiary() async {
const SizedBox(height: 24),
// Confirm Account Number
TextFormField(
key: _confirmAccountNumberFieldKey,
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(
context,
).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
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,
@@ -331,37 +334,30 @@ void validateAndAddBeneficiary() async {
},
),
const SizedBox(height: 24),
// 🔹 IFSC Code Field
TextFormField(
focusNode: _ifscFocusNode,
key: _ifscFieldKey,
controller: ifscController,
maxLength: 11,
inputFormatters: [
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black,
width: 2,
),
),
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) {
_validateIFSC();
},
onChanged: (value) {
final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) {
// clear bank/branch if backspace or changed
bankNameController.clear();
branchNameController.clear();
}
setState(() {
final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) {
// clear bank/branch if backspace or changed
bankNameController.clear();
branchNameController.clear();
}
});
},
validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
@@ -377,9 +373,8 @@ void validateAndAddBeneficiary() async {
return null;
},
),
const SizedBox(height: 24),
// 🔹 Bank Name (Disabled)
// Bank Name (Disabled)
TextFormField(
controller: bankNameController,
enabled: false, // changed from readOnly to disabled
@@ -387,20 +382,8 @@ void validateAndAddBeneficiary() async {
labelText: AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
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),
// 🔹 Branch Name (Disabled)
TextFormField(
@@ -410,73 +393,79 @@ void validateAndAddBeneficiary() async {
labelText: AppLocalizations.of(context).branchName,
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,
),
),
if (_isBeneficiaryValidated)
Column(
children: [
const SizedBox(height: 24),
TextFormField(
controller: nameController,
enabled: false,
decoration: InputDecoration(
suffixIcon: _isBeneficiaryValidated
? const Icon(
Symbols.verified,
size: 25,
fill: 1,
)
: null,
suffixIconColor:
Theme.of(context).colorScheme.primary,
labelText: AppLocalizations.of(context)
.beneficiaryName,
border: const OutlineInputBorder(),
isDense: true,
),
textInputAction: TextInputAction.next,
validator: (value) => value == null ||
value.isEmpty
? AppLocalizations.of(context).nameRequired
: null,
),
],
),
const SizedBox(height: 24),
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(bottom: 24),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating ||
ifscController.text.length != 11
? null
: () {
final isAccountValid =
_accountNumberFieldKey.currentState!
.validate();
final isConfirmAccountValid =
_confirmAccountNumberFieldKey
.currentState!
.validate();
final isIfscValid = _ifscFieldKey
.currentState!
.validate();
if (isAccountValid &&
isConfirmAccountValid &&
isIfscValid) {
_validateBeneficiary();
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2),
)
: Text(AppLocalizations.of(context)
.validateBeneficiary),
),
),
),
),
//Validate Beneficiary Name
if (!_isBeneficiaryValidated)
Padding(
padding: const EdgeInsets.only(top: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isValidating
? null
: () {
if (
confirmAccountNumberController.text ==
accountNumberController.text) {
_validateBeneficiary();
} else {
setState(() {
_validationError =
'Please enter a valid and matching account number.';
});
}
},
child: _isValidating
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Validate Beneficiary'),
),
),
),
//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
DropdownButtonFormField<String>(
value: accountType,
@@ -484,30 +473,18 @@ if (!_isBeneficiaryValidated)
labelText: AppLocalizations.of(context).accountType,
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,
),
),
),
items:
[
AppLocalizations.of(context).savings,
AppLocalizations.of(context).current,
]
.map(
(type) => DropdownMenuItem(
value: type,
child: Text(type),
),
)
.toList(),
items: [
'Savings',
'Current',
]
.map(
(type) => DropdownMenuItem(
value: type,
child: Text(type),
),
)
.toList(),
onChanged: (value) {
setState(() {
accountType = value!;
@@ -524,23 +501,12 @@ if (!_isBeneficiaryValidated)
prefixIcon: const Icon(Icons.phone),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Colors.black,
width: 2,
),
),
),
textInputAction: TextInputAction.done,
validator: (value) =>
value == null || value.length != 10
? AppLocalizations.of(context).enterValidPhone
: null,
? AppLocalizations.of(context).enterValidPhone
: null,
),
const SizedBox(height: 35),
],
@@ -549,19 +515,22 @@ if (!_isBeneficiaryValidated)
),
),
Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: 250,
child: ElevatedButton(
onPressed:
validateAndAddBeneficiary,
onPressed: validateAndAddBeneficiary,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(context).primaryColorDark,
foregroundColor: Theme.of(context).scaffoldBackgroundColor,
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer),
child: Text(
AppLocalizations.of(context).validateAndAdd,
style: const TextStyle(fontSize: 16),
),
child: Text(AppLocalizations.of(context).validateAndAdd),
),
),
),

View File

@@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/widgets/bank_logos.dart';
import 'package:kmobile/api/services/beneficiary_service.dart';
import '../../../l10n/app_localizations.dart';
class BeneficiaryDetailsScreen extends StatelessWidget {
final Beneficiary beneficiary;
BeneficiaryDetailsScreen({super.key, required this.beneficiary});
final service = getIt<BeneficiaryService>();
void _deleteBeneficiary(BuildContext context) async {
try {
await service.deleteBeneficiary(beneficiary.accountNo);
if (!context.mounted) {
return;
}
_showSuccessDialog(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete beneficiary: $e')),
);
}
}
void _showSuccessDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Success'),
content: const Text('Beneficiary deleted successfully.'),
actions: <Widget>[
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
),
],
);
},
);
}
void _showDeleteConfirmationDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete Beneficiary'),
content:
const Text('Are you sure you want to delete this beneficiary?'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Delete'),
onPressed: () {
_deleteBeneficiary(context);
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiarydetails),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
child: getBankLogo(beneficiary.bankName, context),
),
const SizedBox(width: 16),
Text(
beneficiary.name,
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 24),
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
beneficiary.bankName ?? 'N/A'),
_buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
beneficiary.accountNo),
_buildDetailRow('${AppLocalizations.of(context).accountType} ',
beneficiary.accountType),
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
beneficiary.ifscCode),
_buildDetailRow('${AppLocalizations.of(context).branchName} ',
beneficiary.branchName ?? 'N/A'),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// ElevatedButton.icon(
// onPressed: () {
// // Set Transaction Limit for this beneficiary
// },
// icon: const Icon(Icons.currency_rupee),
// label: const Text('Set Limit'),
// ),
ElevatedButton.icon(
onPressed: () {
// Delete beneficiary option
_showDeleteConfirmationDialog(context);
},
icon: const Icon(Icons.delete),
label: Text(AppLocalizations.of(context).delete),
),
],
),
],
),
),
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 16),
Flexible(
child: Text(
value,
textAlign: TextAlign.end,
),
),
],
),
);
}
}

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