233 Commits

Author SHA1 Message Date
1c3a07bd66 Test APK with cheque #2 2025-12-31 13:52:37 +05:30
d44ee5590e Test APK with cheque 2025-12-31 13:45:19 +05:30
715162b713 Test Sim Binding APK 2025-12-11 12:18:25 +05:30
8149ef2a5b Sim Binding Done #1 2025-12-10 11:50:02 +05:30
1a2dea611b SMS integrated with new ui 2025-12-09 18:11:46 +05:30
72a2c56392 Code Formatting 2025-12-05 16:02:49 +05:30
aef82237ac OTP added to daily limit #2 2025-12-05 15:59:59 +05:30
974f42bf95 OTP added to daily limit 2025-12-05 12:37:38 +05:30
4a8c69bb1e Account Info card changes and snackbar in statement #3 2025-12-04 16:50:08 +05:30
86aaaa1f6d Account Info card changes and snackbar in statement #2 2025-12-04 15:56:22 +05:30
6796793aac Account Info card changes and snackbar in statement 2025-12-04 15:40:24 +05:30
fbf6df7181 Icon and logo issues fixed 2025-12-04 12:44:39 +05:30
c7111d518a PDF Edited 2025-12-03 18:05:34 +05:30
5d307607fd Download notification created and Profile added in quick links 2025-12-02 17:06:49 +05:30
992092052a Customerf Info page changed and landing page changed 2025-12-02 13:31:40 +05:30
64fedabd89 Subtitles added in payment tabs with Localizations 2025-12-01 16:28:42 +05:30
4fc6f54fcd Subtitles added in payment tabs 2025-12-01 16:13:27 +05:30
8c7e94759a APK Build #1 2025-12-01 12:58:17 +05:30
8aa5b170ca Account Statement UI Changed 2025-11-28 12:22:51 +05:30
04a1ce26ec Profile Changed and Customer Info 2025-11-28 11:28:01 +05:30
b19bc2e222 Extras removed 2025-11-27 11:47:39 +05:30
b9147b30d5 Enquiry_screen ui changed 2025-11-25 14:43:52 +05:30
3358ec7669 UI #1 2025-11-25 12:51:29 +05:30
18db360a45 Code Formatted 2025-11-24 18:18:36 +05:30
b7fe6a9d18 View All Created 2025-11-24 18:16:53 +05:30
adb9a5330b Account Info card UI changed 2025-11-24 15:50:01 +05:30
0075abc906 Bottom navigation changed 2025-11-24 13:26:33 +05:30
353ec63916 3 changes 2025-11-24 12:56:06 +05:30
71b52cfb43 dashboard Screen UI changed 2025-11-24 12:20:35 +05:30
c1df43e9b6 dashboard#1 2025-11-20 17:45:29 +05:30
f0d5233afc Beneficiary lists changed 2025-11-20 13:31:42 +05:30
4fe6af4098 dark Theme and 18 other changes done 2025-11-20 12:33:30 +05:30
fda5d075ff Profile Screen 2025-11-18 11:33:22 +05:30
71e0521dec kmobile logo changed 2025-11-17 16:57:46 +05:30
f6e851a9ee New KCCB logo 2025-11-17 16:06:07 +05:30
547f534037 localized Beneficiary delete 2025-11-17 15:24:58 +05:30
66b2e71140 Quick Links Screen Icon to List 2025-11-17 10:58:44 +05:30
43d92d799b Quick Links added 2025-11-14 15:24:02 +05:30
3135116f26 Branch and ATM Locator added 2025-11-14 14:36:06 +05:30
39165d631e Watermark added, Card commented out and account opening commented out 2025-11-12 15:59:41 +05:30
shital
ef481ec879 Security settings improvements 2025-11-11 14:18:37 +05:30
shital
36702b198f Fix daily limit parsing type error 2025-11-11 01:10:45 +05:30
shital
f0718e9d68 removed unused imports and blocks 2025-11-11 00:49:31 +05:30
shital
d2cce89efb Fix biometric switch and UI improvements 2025-11-11 00:44:43 +05:30
8cfca113bf dart format 2025-11-10 16:50:29 +05:30
d6f61ebb31 Cooldown Added in Beneficiary 2025-11-10 16:42:29 +05:30
078e715d20 T&C Finalized Test APK 2025-11-10 13:24:52 +05:30
5c8df8ace3 TNC Route Fixed 2025-11-10 12:39:31 +05:30
3e88aad43f T&C #1 2025-11-09 15:42:50 +05:30
5b7f3f0096 Change TPIn #5 2025-11-08 20:10:35 +05:30
b5b6c6ed49 Change TPIn #4 2025-11-08 20:07:04 +05:30
c26cc507a1 Change TPIn #3 2025-11-08 16:56:54 +05:30
87fd36b748 Change TPIn #2 2025-11-08 12:25:56 +05:30
3417c4b0e5 Change TPIN #1 2025-11-08 11:23:38 +05:30
151140d563 Fingerprint Toggle fixed 2025-11-07 17:40:56 +05:30
a8ee7833be Loan And TD funds transfer disabled 2025-11-04 15:24:57 +05:30
f73faaa635 Merge branch 'testing' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into testing 2025-11-04 14:56:16 +05:30
5ac977e903 Self-Transfer #1 2025-11-04 14:53:14 +05:30
f15b8ac3f7 ICICI logo fixed 2025-11-03 12:28:05 +05:30
8f8fdb70e6 Proceed or Swipe to pay disabled on over limit 2025-10-31 16:51:37 +05:30
d86ff2c427 Snackbar added in amount screens 2025-10-31 16:31:04 +05:30
527111c1de Limit loaded and less that 2 lakhs 2025-10-31 13:17:31 +05:30
dfbdb3238d Loading Spinner added in Limit 2025-10-31 12:37:04 +05:30
3d13edf676 Limit Loading Change #1 2025-10-31 12:15:49 +05:30
32e8b85cee APK #1 2025-10-30 12:23:56 +05:30
58e53d0aeb App Version added and dart format 2025-10-21 13:32:14 +05:30
06ef2ab36b Profile shifted to left 2025-10-19 21:27:47 +05:30
0362bf2013 Fingerprint option added in profile screen 2025-10-17 11:47:20 +05:30
73b96b82f7 Background Timer Feature#1 2025-10-16 17:54:48 +05:30
c78a90dbfe Screenshot Prevention and backgorund Screen blacking 2025-10-15 12:46:56 +05:30
df025babd5 App Functionality 2025-10-15 12:30:01 +05:30
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
3024ddef15 Manage Beneficiary Services 2 2025-08-10 11:18:00 +05:30
83609fb778 Manage Beneficiary Services 2025-08-09 22:10:26 +05:30
dbc61abf00 Manage Beneficiary UI 2025-08-08 23:37:16 +05:30
763c101f58 Manage Beneficiary 2025-08-08 15:43:08 +05:30
117e2d5786 Manage Beneficiary fetch 2025-08-08 08:40:38 +05:30
ae40f61c01 Add Beneficiary Animations and Localizations 2025-08-07 23:36:28 +05:30
a1365b19d5 Add Beneficiary Animations 2025-08-07 21:07:32 +05:30
2dd7f4079b Add Beneficiary Validation API integration 2025-08-07 20:32:04 +05:30
99e23bf21d IFSC Code Validation and Add Beneficiary Validation API integration 2025-08-07 18:12:31 +05:30
c4d4261afc Theme 2025-08-06 17:26:25 +05:30
Nilanjan Chakrabarti
2fdef7c850 Shared Preferences Update 2025-07-31 16:14:29 +05:30
Nilanjan Chakrabarti
618bd4a9b9 Shared Preferences implementation 2025-07-30 12:50:11 +05:30
179 changed files with 16218 additions and 7163 deletions

8
.gitignore vendored
View File

@@ -41,3 +41,11 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
lib/l10n/app_localizations.dart
lib/l10n/app_localizations_en.dart
lib/l10n/app_localizations_hi.dart
# Keystore files
android/key.properties
android/*.jks
android/*.keystore

View File

@@ -7,6 +7,10 @@
# The following line activates a set of recommended lints for Flutter apps, # The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices. # 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 include: package:flutter_lints/flutter.yaml
linter: linter:

View File

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

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/images/logo_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/images/pnb_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
assets/images/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/images/profile.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
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

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

View File

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

View File

@@ -4,11 +4,45 @@ import '../../features/auth/models/auth_token.dart';
import '../../features/auth/models/auth_credentials.dart'; import '../../features/auth/models/auth_credentials.dart';
import '../../core/errors/exceptions.dart'; import '../../core/errors/exceptions.dart';
class AuthService { class AuthService {
final Dio _dio; final Dio _dio;
AuthService(this._dio); AuthService(this._dio);
Future<void> simVerify(String uuid, String cifNo) async {
try {
final response = await _dio.post('/api/sim-details-verify', data: {
'uuid': uuid,
'cifNo': cifNo,
});
if (response.statusCode == 200) {
final String message = response.data.toString().toUpperCase();
if (message.contains("VERIFIED")) {
return; // Success
} else {
throw AuthException(message); // Throw message received
}
} else {
throw AuthException('Verification Failed');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
if (e.response?.statusCode == 401) {
throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
}
throw NetworkException('Network error during verification');
} catch (e) {
throw UnexpectedException(
'Unexpected error during verification: ${e.toString()}');
}
}
Future<AuthToken> login(AuthCredentials credentials) async { Future<AuthToken> login(AuthCredentials credentials) async {
try { try {
final response = await _dio.post( final response = await _dio.post(
@@ -26,11 +60,13 @@ class AuthService {
print(e.toString()); print(e.toString());
} }
if (e.response?.statusCode == 401) { if (e.response?.statusCode == 401) {
throw AuthException('Invalid credentials'); throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
} }
throw NetworkException('Network error during login'); throw NetworkException('Network error during login');
} catch (e) { } catch (e) {
throw UnexpectedException('Unexpected error during login: ${e.toString()}'); throw UnexpectedException(
'Unexpected error during login: ${e.toString()}');
} }
} }
@@ -66,7 +102,8 @@ class AuthService {
} }
throw NetworkException('Network error during TPIN check'); throw NetworkException('Network error during TPIN check');
} catch (e) { } catch (e) {
throw UnexpectedException('Unexpected error during TPIN check: ${e.toString()}'); throw UnexpectedException(
'Unexpected error during TPIN check: ${e.toString()}');
} }
} }
@@ -86,8 +123,76 @@ class AuthService {
} }
throw NetworkException('Network error during TPIN setup'); throw NetworkException('Network error during TPIN setup');
} catch (e) { } 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;
}
Future setTncflag() async {
try {
final response = await _dio.post(
'/api/auth/tnc',
data: {"flag": 'Y'},
);
if (response.statusCode != 200) {
throw AuthException('Failed to proceed with T&C');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
throw NetworkException('Network error during T&C Setup');
} catch (e) {
throw UnexpectedException('Unexpected error: ${e.toString()}');
}
}
} }

View File

@@ -1,4 +1,8 @@
import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:kmobile/core/errors/exceptions.dart';
import 'package:kmobile/data/models/ifsc.dart';
import 'package:kmobile/data/models/beneficiary.dart';
class BeneficiaryService { class BeneficiaryService {
final Dio _dio; final Dio _dio;
@@ -7,14 +11,114 @@ class BeneficiaryService {
Future<String> validateBeneficiaryWithinBank(String accountNumber) async { Future<String> validateBeneficiaryWithinBank(String accountNumber) async {
try { 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, 'accountNumber': accountNumber,
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
return response.data['name']; return response.data['name'];
} else { } 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}');
} catch (e) {
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);
}
} 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()}');
}
return Ifsc.fromJson({});
}
Future<String> validateBeneficiary({
required String accountNo,
required String ifscCode,
required String remitterName,
}) async {
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'];
}
// Beneficiary Validate And ADD
Future<bool> sendForValidation(Beneficiary beneficiary) async {
try {
final response = await _dio.post(
'/api/beneficiary',
data: beneficiary.toJson(),
);
if (response.statusCode == 200) {
return true;
} else {
return false;
}
} catch (e) {
rethrow;
}
}
Future<List<Beneficiary>> fetchBeneficiaryList() async {
try {
final response = await _dio.get(
"/api/beneficiary",
options: Options(
headers: {
"Content-Type": "application/json",
},
),
);
if (response.statusCode == 200) {
return Beneficiary.listFromJson(response.data);
} else {
throw Exception("Failed to fetch beneficiaries");
}
} catch (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) { } on DioException catch (e) {
throw Exception('Network error: ${e.message}'); throw Exception('Network error: ${e.message}');

View File

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

View File

@@ -0,0 +1,87 @@
import 'package:dio/dio.dart';
class ChangePasswordService {
final Dio _dio;
ChangePasswordService(this._dio);
Future getOtp({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_LPWORD"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future getOtpTpin({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "CHANGE_TPIN"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future validateOtp({
required String otp,
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber',
data: {
'otp': otp,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
Future validateChangePwd({
required String OldLPsw,
required String newLPsw,
required String confirmLPsw,
}) async {
final response = await _dio.post(
'/api/auth/change/login_password',
data: {
'OldLPsw': OldLPsw,
'newLPsw': newLPsw,
'confirmLPsw': confirmLPsw,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
Future validateChangeTpin({
required String oldTpin,
required String newTpin,
}) async {
final response = await _dio.post(
'/api/auth/change/tpin',
data: {
'oldTpin': oldTpin,
'newTpin': newTpin,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
}

View File

@@ -0,0 +1,138 @@
import 'dart:convert';
import 'package:dio/dio.dart';
class Cheque {
final String? type;
final String? InstrType;
final String? Date;
final String? branchCode;
final String? fromCheque;
final String? toCheque;
final String? Chequescount;
final String? ChequeNumber;
final String? transactionCode;
final int? amount;
final String? status;
final String? stopIssueDate;
final String? StopExpiryDate;
Cheque({
this.type,
this.InstrType,
this.Date,
this.branchCode,
this.fromCheque,
this.toCheque,
this.Chequescount,
this.ChequeNumber,
this.transactionCode,
this.amount,
this.status,
this.stopIssueDate,
this.StopExpiryDate,
});
factory Cheque.fromJson(Map<String, dynamic> json) {
return Cheque(
type: json['type'] ?? '',
InstrType: json['InstrType'] ?? '',
Date: json['Date'] ?? '',
branchCode: json['branchCode'] ?? '',
fromCheque: json['fromCheque'] ?? '',
toCheque: json['toCheque'] ?? '',
Chequescount: json['Chequescount'] ?? '',
ChequeNumber: json['ChequeNumber'] ?? '',
transactionCode: json['transactionCode'] ?? '',
amount: json['amount'],
status: json['status'] ?? '',
stopIssueDate: json['stopIssueDate'] ?? '',
StopExpiryDate: json['StopExpiryDate'] ?? '',
);
}
static List<Cheque> listFromJson(List<dynamic> jsonList) {
final chequeList =
jsonList.map((cheque) => Cheque.fromJson(cheque)).toList();
return chequeList;
}
}
class ChequeService {
final Dio _dio;
ChequeService(this._dio);
Future<List<Cheque>> ChequeEnquiry({
required String accountNumber,
required String instrType,
}) async {
try {
final response = await _dio.get(
"/api/cheque/enquiry",
queryParameters: {
'accountNumber': accountNumber,
'instrumentType': instrType,
},
options: Options(
headers: {
"Content-Type": "application/json",
},
),
);
if (response.statusCode == 200) {
if (response.data is Map<String, dynamic> &&
response.data.containsKey('records')) {
final records = response.data['records'];
if (records is List) {
return Cheque.listFromJson(records);
}
}
throw Exception(
"Unexpected API response format: 'records' list not found or malformed");
} else {
throw Exception("Failed to fetch");
}
} catch (e) {
print('Error in ChequeEnquiry: $e');
throw e;
}
}
Future stopCheque({
required String accountno,
required String stopFromChequeNo,
required String instrType,
String? stopToChequeNo,
String? stopIssueDate,
String? stopExpiryDate,
String? stopAmount,
String? stopComment,
String? chequeIssueDate,
required String tpin,
}) async {
final response = await _dio.post(
'/api/cheque/stop',
options: Options(
validateStatus: (int? status) => true,
receiveDataWhenStatusError: true,
),
data: {
'accountNumber': accountno,
'stopFromChequeNo': stopFromChequeNo,
'instrumentType': instrType,
'stopToChequeNo': stopToChequeNo,
'stopIssueDate': stopIssueDate,
'stopExpiryDate': stopExpiryDate,
'stopAmount': stopAmount,
'stopComment': stopComment,
'chqIssueDate': chequeIssueDate,
'tpin': tpin,
},
);
if (response.statusCode != 200) {
throw Exception(jsonEncode(response.data));
}
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,87 @@
// ignore_for_file: collection_methods_unrelated_type
import 'dart:developer';
import 'package:dio/dio.dart';
class Limit {
final double dailyLimit;
final double usedLimit;
Limit({
required this.dailyLimit,
required this.usedLimit,
});
factory Limit.fromJson(Map<String, dynamic> json) {
return Limit(
dailyLimit: (json['dailyLimit'] as num).toDouble(),
usedLimit: (json['usedLimit'] as num).toDouble(),
);
}
}
class LimitService {
final Dio _dio;
LimitService(this._dio);
Future<Limit> getLimit() async {
try {
final response = await _dio.get('/api/customer/daily-limit');
if (response.statusCode == 200) {
log('Response: ${response.data}');
return Limit.fromJson(response.data);
} else {
throw Exception('Failed to load');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
void editLimit(double newLimit) async {
try {
final response = await _dio.post('/api/customer/daily-limit',
data: '{"amount": $newLimit}');
if (response.statusCode == 200) {
log('Response: ${response.data}');
} else {
throw Exception('Failed to load');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
Future getOtpTLimit({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "TLIMIT"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future validateOtp({
required String otp,
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber',
data: {
'otp': otp,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
}

View File

@@ -0,0 +1,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 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:kmobile/data/models/payment_response.dart'; import 'package:kmobile/data/models/payment_response.dart';
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';

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

View File

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

View File

@@ -2,20 +2,25 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_state.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart'; import './l10n/app_localizations.dart';
import 'config/themes.dart'; import 'package:kmobile/features/auth/controllers/theme_cubit.dart';
import 'package:kmobile/features/auth/controllers/theme_state.dart';
import 'config/routes.dart'; import 'config/routes.dart';
import 'di/injection.dart'; import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart'; import 'features/auth/controllers/auth_cubit.dart';
import 'features/card/screens/card_management_screen.dart'; import 'features/accounts/screens/account_statement_screen.dart';
import 'features/auth/screens/welcome_screen.dart'; import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'features/auth/screens/login_screen.dart'; import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart'; import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart'; import 'features/dashboard/screens/dashboard_screen.dart';
import 'features/auth/screens/mpin_screen.dart'; import 'features/auth/screens/mpin_screen.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
class KMobile extends StatefulWidget { class KMobile extends StatefulWidget {
const KMobile({super.key}); const KMobile({super.key});
@@ -24,27 +29,59 @@ class KMobile extends StatefulWidget {
State<KMobile> createState() => _KMobileState(); State<KMobile> createState() => _KMobileState();
static void setLocale(BuildContext context, Locale newLocale) { static void setLocale(BuildContext context, Locale newLocale) {
final _KMobileState? state = context final _KMobileState? state =
.findAncestorStateOfType<_KMobileState>(); context.findAncestorStateOfType<_KMobileState>();
state?.setLocale(newLocale); state?.setLocale(newLocale);
} }
} }
class _KMobileState extends State<KMobile> { class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
bool _showSplash = false; Timer? _backgroundTimer;
Locale? _locale;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Simulate a splash screen delay WidgetsBinding.instance.addObserver(this);
Future.delayed(const Duration(seconds: 2), () { loadPreferences();
setState(() {
_showSplash = false;
});
});
} }
Locale? _locale; @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();
final String? langCode = prefs.getString('locale');
if (langCode != null) {
setState(() {
_locale = Locale(langCode);
});
}
}
void setLocale(Locale locale) { void setLocale(Locale locale) {
setState(() { setState(() {
@@ -52,10 +89,8 @@ class _KMobileState extends State<KMobile> {
}); });
} }
/*
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Set status bar color
SystemChrome.setSystemUIOverlayStyle( SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle( const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, statusBarColor: Colors.transparent,
@@ -63,10 +98,19 @@ class _KMobileState extends State<KMobile> {
), ),
); );
if (_showSplash) { 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( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
locale: _locale, locale: _locale ?? const Locale('en'),
supportedLocales: const [ supportedLocales: const [
Locale('en'), Locale('en'),
Locale('hi'), Locale('hi'),
@@ -77,59 +121,20 @@ class _KMobileState extends State<KMobile> {
GlobalWidgetsLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
], ],
home: const SplashScreen(),
);
}
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>()),
],
child: MaterialApp(
title: 'kMobile', title: 'kMobile',
// debugShowCheckedModeBanner: false, theme: themeState.getLightThemeData(),
theme: AppThemes.lightTheme, darkTheme: themeState.getDarkThemeData(),
// darkTheme: AppThemes.darkTheme, themeMode: context.watch<ThemeModeCubit>().state.mode,
themeMode: ThemeMode.system, // Use system theme by default navigatorObservers: [
getIt<RouteObserver<ModalRoute<void>>>(),
],
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash, initialRoute: AppRoutes.splash,
home: const AuthGate(), home: const AuthGate(),
),
); );
} },
*/
@override
Widget build(BuildContext context) {
// Set status bar color
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
); );
},
return MultiBlocProvider(
providers: [BlocProvider<AuthCubit>(create: (_) => getIt<AuthCubit>())],
child: MaterialApp(
debugShowCheckedModeBanner: false,
locale: _locale, // Use your existing locale variable
supportedLocales: const [Locale('en'), Locale('hi')],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
title: 'kMobile',
theme: AppThemes.lightTheme,
// darkTheme: AppThemes.darkTheme,
themeMode: ThemeMode.system, // Use system theme by default
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash,
home: _showSplash ? const SplashScreen() : const AuthGate(),
), ),
); );
} }
@@ -137,7 +142,6 @@ class _KMobileState extends State<KMobile> {
class AuthGate extends StatefulWidget { class AuthGate extends StatefulWidget {
const AuthGate({super.key}); const AuthGate({super.key});
@override @override
State<AuthGate> createState() => _AuthGateState(); State<AuthGate> createState() => _AuthGateState();
} }
@@ -145,7 +149,6 @@ class AuthGate extends StatefulWidget {
class _AuthGateState extends State<AuthGate> { class _AuthGateState extends State<AuthGate> {
bool _checking = true; bool _checking = true;
bool _isLoggedIn = false; bool _isLoggedIn = false;
bool _showWelcome = true;
bool _hasMPin = false; bool _hasMPin = false;
bool _biometricEnabled = false; bool _biometricEnabled = false;
bool _biometricTried = false; bool _biometricTried = false;
@@ -163,8 +166,7 @@ class _AuthGateState extends State<AuthGate> {
final mpin = await storage.read('mpin'); final mpin = await storage.read('mpin');
final biometric = await storage.read('biometric_enabled'); final biometric = await storage.read('biometric_enabled');
setState(() { setState(() {
_isLoggedIn = _isLoggedIn = accessToken != null &&
accessToken != null &&
accessTokenExpiry != null && accessTokenExpiry != null &&
DateTime.parse(accessTokenExpiry).isAfter(DateTime.now()); DateTime.parse(accessTokenExpiry).isAfter(DateTime.now());
_hasMPin = mpin != null; _hasMPin = mpin != null;
@@ -179,9 +181,13 @@ class _AuthGateState extends State<AuthGate> {
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) return false; if (!canCheck) return false;
String localizedReason = "";
if (mounted) {
localizedReason = AppLocalizations.of(context).authenticateToAccess;
}
try { try {
final didAuth = await localAuth.authenticate( final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).authenticateToAccess, localizedReason: localizedReason,
options: const AuthenticationOptions( options: const AuthenticationOptions(
stickyAuth: true, stickyAuth: true,
biometricOnly: true, biometricOnly: true,
@@ -193,123 +199,11 @@ class _AuthGateState extends State<AuthGate> {
} }
} }
/* @override
Widget build(BuildContext context) {
if (_checking) {
return const SplashScreen();
}
if (_isLoggedIn) {
if (_hasMPin) {
if (_biometricEnabled) {
return FutureBuilder<bool>(
future: _tryBiometric(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
}
if (snapshot.data == true) {
// Authenticated with biometrics, go to dashboard
return const NavigationScaffold();
}
// If not authenticated or user dismissed, show mPIN screen
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold()),
);
},
);
},
);
} else {
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
},
);
}
} else {
return MPinScreen(
mode: MPinMode.set,
onCompleted: (_) async {
final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication();
// 1) Prompt user to optin for biometric
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false, // force choice
builder: (ctx) => AlertDialog(
title: const Text('Enable Fingerprint Login?'),
content: const Text(
'Would you like to enable fingerprint authentication for faster login?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Yes'),
),
],
),
);
// 2) If opted in, perform biometric auth
if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false;
if (canCheck) {
didAuth = await localAuth.authenticate(
localizedReason: 'Authenticate to enable fingerprint login',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
}
await storage.write(
'biometric_enabled', didAuth ? 'true' : 'false');
} else {
await storage.write('biometric_enabled', 'false');
}
// 3) Finally go to your main scaffold
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
},
);
}
}
return const LoginScreen();
}
}*/
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_checking) { if (_checking) {
return const SplashScreen(); return const LoginScreen();
} }
// ✅ Step 1: Show welcome screen first, only once
if (_showWelcome) {
return WelcomeScreen(
onContinue: () {
setState(() {
_showWelcome = false;
});
},
);
}
// ✅ Step 2: Check login status
if (_isLoggedIn) { if (_isLoggedIn) {
if (_hasMPin) { if (_hasMPin) {
if (_biometricEnabled) { if (_biometricEnabled) {
@@ -317,14 +211,11 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(), future: _tryBiometric(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen(); return const LoginScreen();
} }
if (snapshot.data == true) { if (snapshot.data == true) {
return const NavigationScaffold(); // Authenticated return const NavigationScaffold();
} }
// ❌ Biometric failed → Show MPIN screen
return MPinScreen( return MPinScreen(
mode: MPinMode.enter, mode: MPinMode.enter,
onCompleted: (_) { onCompleted: (_) {
@@ -350,13 +241,11 @@ class _AuthGateState extends State<AuthGate> {
); );
} }
} else { } else {
// No MPIN set → show MPIN set screen + biometric dialog
return MPinScreen( return MPinScreen(
mode: MPinMode.set, mode: MPinMode.set,
onCompleted: (_) async { onCompleted: (_) async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final optIn = await showDialog<bool>( final optIn = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -377,15 +266,16 @@ class _AuthGateState extends State<AuthGate> {
], ],
), ),
); );
if (optIn == true) { if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false; bool didAuth = false;
String authEnable = "";
if (context.mounted) {
authEnable = AppLocalizations.of(context).authenticateToEnable;
}
if (canCheck) { if (canCheck) {
didAuth = await localAuth.authenticate( didAuth = await localAuth.authenticate(
localizedReason: localizedReason: authEnable,
AppLocalizations.of(context).authenticateToEnable,
options: const AuthenticationOptions( options: const AuthenticationOptions(
stickyAuth: true, stickyAuth: true,
biometricOnly: true, biometricOnly: true,
@@ -397,143 +287,23 @@ class _AuthGateState extends State<AuthGate> {
await storage.write('biometric_enabled', 'false'); await storage.write('biometric_enabled', 'false');
} }
} }
if (context.mounted) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => const NavigationScaffold(), builder: (_) => const NavigationScaffold(),
), ),
); );
}
}, },
); );
} }
} }
// ✅ Step 3: If not logged in, show login screen
return const LoginScreen(); return const LoginScreen();
} }
} }
/*@override
Widget build(BuildContext context) {
if (_checking) {
return const SplashScreen();
}
if (_isLoggedIn) {
if (_hasMPin) {
if (_biometricEnabled) {
return FutureBuilder<bool>(
future: _tryBiometric(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
}
if (snapshot.data == true) {
return const NavigationScaffold(); // Authenticated
}
// Failed or dismissed biometric → Show MPIN
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
},
);
},
);
} else {
return MPinScreen(
mode: MPinMode.enter,
onCompleted: (_) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
},
);
}
} else {
return MPinScreen(
mode: MPinMode.set,
onCompleted: (_) async {
final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication();
final optin = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(
AppLocalizations.of(context).enableFingerprintLogin,
),
content: Text(
AppLocalizations.of(context).enableFingerprintMessage,
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (optin == true) {
final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false;
if (canCheck) {
didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(
context,
).authenticateToEnable,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
}
await storage.write(
'biometric_enabled',
didAuth ? 'true' : 'false',
);
} else {
await storage.write('biometric_enabled', 'false');
}
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
);
},
);
}
}
// 🔻 Show Welcome screen before login if not logged in
if (_showWelcome) {
return WelcomeScreen(
onContinue: () {
setState(() {
_showWelcome = false;
});
},
);
}
return const LoginScreen();
}
}*/
class NavigationScaffold extends StatefulWidget { class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key}); const NavigationScaffold({super.key});
@override @override
State<NavigationScaffold> createState() => _NavigationScaffoldState(); State<NavigationScaffold> createState() => _NavigationScaffoldState();
} }
@@ -541,20 +311,26 @@ class NavigationScaffold extends StatefulWidget {
class _NavigationScaffoldState extends State<NavigationScaffold> { class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController(); final PageController _pageController = PageController();
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardScreen(), const DashboardScreen(),
const CardManagementScreen(), BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
if (state.users.isNotEmpty) {
return AccountStatementScreen(
users: state.users,
selectedIndex: 0,
);
} else {
return const Center(child: Text("No accounts found."));
}
}
return const Center(child: CircularProgressIndicator());
},
),
const ServiceScreen(), const ServiceScreen(),
]; ];
void _onItemTapped(int index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return PopScope(
@@ -595,18 +371,23 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
currentIndex: _selectedIndex, currentIndex: _selectedIndex,
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
backgroundColor: const Color(0xFFE0F7FA), // Light blue background backgroundColor: const Color(0XFF1E58AD),
selectedItemColor: Colors.blue[800], selectedItemColor: Theme.of(context).colorScheme.onPrimary,
unselectedItemColor: Colors.black54, unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
onTap: _onItemTapped, onTap: (index) {
setState(() {
_selectedIndex = index;
});
_pageController.jumpToPage(index);
},
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.home_filled), icon: const Icon(Icons.home_filled),
label: AppLocalizations.of(context).home, label: AppLocalizations.of(context).home,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.credit_card), icon: const Icon(Icons.swap_vert_sharp),
label: AppLocalizations.of(context).card, label: AppLocalizations.of(context).transactions,
), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services), icon: const Icon(Icons.miscellaneous_services),
@@ -619,34 +400,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 { class BiometricPromptScreen extends StatelessWidget {
final VoidCallback onCompleted; final VoidCallback onCompleted;
const BiometricPromptScreen({super.key, required this.onCompleted}); const BiometricPromptScreen({super.key, required this.onCompleted});
Future<void> _handleBiometric(BuildContext context) async { Future<void> _handleBiometric(BuildContext context) async {
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
@@ -654,8 +410,12 @@ class BiometricPromptScreen extends StatelessWidget {
onCompleted(); onCompleted();
return; return;
} }
String localizedReason = "";
if (context.mounted) {
localizedReason = AppLocalizations.of(context).enableFingerprintQuick;
}
final didAuth = await localAuth.authenticate( final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).enableFingerprintQuick, localizedReason: localizedReason,
options: const AuthenticationOptions( options: const AuthenticationOptions(
stickyAuth: true, stickyAuth: true,
biometricOnly: true, biometricOnly: true,
@@ -673,7 +433,7 @@ class BiometricPromptScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context)); Future.microtask(() => _showDialog(context));
return const SplashScreen(); return const SizedBox.shrink();
} }
Future<void> _showDialog(BuildContext context) async { Future<void> _showDialog(BuildContext context) async {
@@ -699,6 +459,9 @@ class BiometricPromptScreen extends StatelessWidget {
], ],
), ),
); );
if (!context.mounted) {
return;
}
if (result == true) { if (result == true) {
await _handleBiometric(context); await _handleBiometric(context);
} else { } else {

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/splash_screen.dart';
import '../app.dart'; import '../app.dart';
import '../features/auth/screens/login_screen.dart'; import '../features/auth/screens/login_screen.dart';
// import '../features/auth/screens/forgot_password_screen.dart'; // import '../features/auth/screens/forgot_password_screen.dart';
@@ -9,6 +10,7 @@ import '../features/dashboard/screens/dashboard_screen.dart';
// import '../features/transactions/screens/transactions_screen.dart'; // import '../features/transactions/screens/transactions_screen.dart';
// import '../features/payments/screens/payments_screen.dart'; // import '../features/payments/screens/payments_screen.dart';
// import '../features/settings/screens/settings_screen.dart'; // import '../features/settings/screens/settings_screen.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
class AppRoutes { class AppRoutes {
// Private constructor to prevent instantiation // Private constructor to prevent instantiation
@@ -33,9 +35,14 @@ class AppRoutes {
return MaterialPageRoute(builder: (_) => const SplashScreen()); return MaterialPageRoute(builder: (_) => const SplashScreen());
case login: case login:
return MaterialPageRoute(builder: (_) => const LoginScreen()); return MaterialPageRoute(builder: (_) => const LoginScreen());
case TncRequiredScreen.routeName: // Renamed class
return MaterialPageRoute(
builder: (_) => const TncRequiredScreen()); // Renamed class
case mPin: case mPin:
return MaterialPageRoute(builder: (_) => const MPinScreen(mode: MPinMode.enter,)); return MaterialPageRoute(
builder: (_) => const MPinScreen(
mode: MPinMode.enter,
));
case register: case register:
// Placeholder - create the RegisterScreen class and uncomment // Placeholder - create the RegisterScreen class and uncomment
@@ -68,7 +75,6 @@ class AppRoutes {
// return MaterialPageRoute(builder: (_) => const PaymentsScreen()); // return MaterialPageRoute(builder: (_) => const PaymentsScreen());
return _errorRoute(); return _errorRoute();
default: default:
return _errorRoute(); return _errorRoute();
} }

View File

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

View File

@@ -1,266 +1,72 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'theme_type.dart';
import 'package:google_fonts/google_fonts.dart';
class AppThemes { class AppThemes {
// Private constructor to prevent instantiation static ThemeData getLightTheme(ThemeType type) {
AppThemes._(); final Color seedColor = _getSeedColor(type);
final colorScheme = ColorScheme.fromSeed(
// Light theme colors seedColor: seedColor,
static const Color _primaryColorLight = Color(0xFF1E88E5); // Blue 600
static const Color _secondaryColorLight = Color(0xFF26A69A); // Teal 400
static const Color _errorColorLight = Color(0xFFE53935); // Red 600
static const Color _surfaceColorLight = Colors.white;
// Dark theme colors
static const Color _primaryColorDark = Color(0xFF42A5F5); // Blue 400
static const Color _secondaryColorDark = Color(0xFF4DB6AC); // Teal 300
static const Color _errorColorDark = Color(0xFFEF5350); // Red 400
static const Color _surfaceColorDark = Color(0xFF1E1E1E);
// Text themes
static const TextTheme _textThemeLight = TextTheme(
displayLarge: TextStyle(
fontSize: 96,
fontWeight: FontWeight.w300,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
displayMedium: TextStyle(
fontSize: 60,
fontWeight: FontWeight.w300,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
displaySmall: TextStyle(
fontSize: 48,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
headlineMedium: TextStyle(
fontSize: 34,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
headlineSmall: TextStyle(
fontSize: 24,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
titleLarge: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w500,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
bodyLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
bodyMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
bodySmall: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFF757575),
fontFamily: 'Rubik',
),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF212121),
fontFamily: 'Rubik',
),
);
static final TextTheme _textThemeDark = _textThemeLight.copyWith(
displayLarge: _textThemeLight.displayLarge?.copyWith(color: Colors.white),
displayMedium: _textThemeLight.displayMedium?.copyWith(color: Colors.white),
displaySmall: _textThemeLight.displaySmall?.copyWith(color: Colors.white),
headlineMedium:
_textThemeLight.headlineMedium?.copyWith(color: Colors.white),
headlineSmall: _textThemeLight.headlineSmall?.copyWith(color: Colors.white),
titleLarge: _textThemeLight.titleLarge?.copyWith(color: Colors.white),
bodyLarge: _textThemeLight.bodyLarge?.copyWith(color: Colors.white),
bodyMedium: _textThemeLight.bodyMedium?.copyWith(color: Colors.white),
bodySmall: _textThemeLight.bodySmall?.copyWith(color: Colors.white70),
labelLarge: _textThemeLight.labelLarge?.copyWith(color: Colors.white),
);
// Light theme
static final ThemeData lightTheme = ThemeData(
useMaterial3: true,
fontFamily: 'Rubik',
colorScheme: const ColorScheme.light(
primary: _primaryColorLight,
secondary: _secondaryColorLight,
error: _errorColorLight,
surface: _surfaceColorLight,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: Colors.black87,
onError: Colors.white,
brightness: Brightness.light, brightness: Brightness.light,
);
return ThemeData.from(
colorScheme: colorScheme,
useMaterial3: true,
textTheme: GoogleFonts.rubikTextTheme(),
).copyWith(
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF01A04C),
titleTextStyle: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w700,
fontSize: 20,
), ),
textTheme: _textThemeLight, iconTheme: IconThemeData(color: colorScheme.onPrimary),
appBarTheme: const AppBarTheme( actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
elevation: 0, ));
backgroundColor: _surfaceColorLight, }
foregroundColor: Color(0xFF212121),
centerTitle: true, static ThemeData getDarkTheme(ThemeType type) {
), final Color seedColor = _getSeedColor(type);
cardTheme: CardThemeData( log(seedColor.toString());
//Earlier CardThemeData final colorScheme = ColorScheme.fromSeed(
elevation: 2, seedColor: seedColor,
shape: RoundedRectangleBorder( brightness: Brightness.dark,
borderRadius: BorderRadius.circular(12),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor: _primaryColorLight,
elevation: 2,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: _primaryColorLight,
side: const BorderSide(color: _primaryColorLight),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: _primaryColorLight,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey[100],
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _primaryColorLight, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _errorColorLight, width: 2),
),
labelStyle: const TextStyle(color: Colors.grey),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
selectedItemColor: _primaryColorLight,
unselectedItemColor: Colors.grey,
),
); );
// Dark theme return ThemeData.from(
static final ThemeData darkTheme = ThemeData( colorScheme: colorScheme,
fontFamily: 'Rubik',
useMaterial3: true, useMaterial3: true,
colorScheme: const ColorScheme.dark( textTheme: GoogleFonts.rubikTextTheme(
primary: _primaryColorDark, ThemeData(brightness: Brightness.dark).textTheme,
secondary: _secondaryColorDark,
error: _errorColorDark,
surface: _surfaceColorDark,
onPrimary: Colors.black,
onSecondary: Colors.black,
onSurface: Colors.white,
onError: Colors.black,
brightness: Brightness.dark,
), ),
textTheme: _textThemeDark, ).copyWith(
appBarTheme: const AppBarTheme( appBarTheme: AppBarTheme(
elevation: 0, backgroundColor: const Color(0xFF01A04C),
backgroundColor: _surfaceColorDark, titleTextStyle: TextStyle(
foregroundColor: Colors.white, color: colorScheme.onPrimary,
centerTitle: true, fontWeight: FontWeight.w700,
fontSize: 20,
), ),
cardTheme: CardThemeData( iconTheme: IconThemeData(color: colorScheme.onPrimary),
//Earlier was CardThemeData actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
elevation: 2, ));
shape: RoundedRectangleBorder( }
borderRadius: BorderRadius.circular(12),
), static Color _getSeedColor(ThemeType type) {
color: const Color(0xFF2C2C2C), switch (type) {
), case ThemeType.green:
elevatedButtonTheme: ElevatedButtonThemeData( return Colors.green;
style: ElevatedButton.styleFrom( case ThemeType.orange:
foregroundColor: Colors.black, return Colors.orange;
backgroundColor: _primaryColorDark, case ThemeType.blue:
elevation: 2, return Colors.blue;
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), case ThemeType.violet:
shape: RoundedRectangleBorder( return Colors.deepPurple;
borderRadius: BorderRadius.circular(8), case ThemeType.yellow:
), return Colors.yellow;
), }
), }
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: _primaryColorDark,
side: const BorderSide(color: _primaryColorDark),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: _primaryColorDark,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF2A2A2A),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF3A3A3A)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _primaryColorDark, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _errorColorDark, width: 2),
),
labelStyle: const TextStyle(color: Colors.grey),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
selectedItemColor: _primaryColorDark,
unselectedItemColor: Colors.grey,
backgroundColor: _surfaceColorDark,
),
);
} }

View File

@@ -0,0 +1,66 @@
class Beneficiary {
final String accountNo;
final String accountType;
final String name;
final DateTime? createdAt;
final String ifscCode;
final String? bankName;
final String? branchName;
final String? tpin;
Beneficiary({
required this.accountNo,
required this.accountType,
required this.name,
this.createdAt,
required this.ifscCode,
this.bankName,
this.branchName,
this.tpin,
});
factory Beneficiary.fromJson(Map<String, dynamic> json) {
return Beneficiary(
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
accountType: json['account_type'] ?? json['accountType'] ?? '',
createdAt: json['createdAt'] == null
? null
: DateTime.tryParse(json['createdAt']),
name: json['name'] ?? '',
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
bankName: json['bank_name'] ?? json['bankName'] ?? '',
branchName: json['branch_name'] ?? json['branchName'] ?? '',
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = {
'accountNo': accountNo,
'accountType': accountType,
'name': name,
'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();
return beneficiaryList;
}
@override
String toString() {
return 'Beneficiary(accountNo: $accountNo, accountType: $accountType, ifscCode: $ifscCode, name: $name)';
}
}

32
lib/data/models/ifsc.dart Normal file
View File

@@ -0,0 +1,32 @@
class Ifsc {
final String ifscCode;
final String bankName;
final String branchName;
Ifsc({
required this.ifscCode,
required this.bankName,
required this.branchName,
});
factory Ifsc.fromJson(Map<String, dynamic> json) {
return Ifsc(
ifscCode: json['ifsc_code'] ?? '',
bankName: json['bank_name'] ?? '',
branchName: json['branch_name'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'ifsc_code': ifscCode,
'bank_name': bankName,
'branch_name': branchName,
};
}
@override
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? currency;
final String? errorMessage; final String? errorMessage;
final String? errorCode; final String? errorCode;
final String? utr;
PaymentResponse({ PaymentResponse({
required this.isSuccess, required this.isSuccess,
@@ -15,5 +16,6 @@ class PaymentResponse {
this.currency, this.currency,
this.errorMessage, this.errorMessage,
this.errorCode, 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? date;
final String? amount; final String? amount;
final String? type; final String? type;
final String? balance;
final String? balanceType;
Transaction(
{this.id,
this.name,
this.date,
this.amount,
this.type,
this.balance,
this.balanceType});
Transaction({this.id, this.name, this.date, this.amount, this.type});
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
'id': id, 'id': id,
@@ -13,6 +23,8 @@ class Transaction {
'date': date, 'date': date,
'amount': amount, 'amount': amount,
'type': type, 'type': type,
'balance': balance,
'balanceType': balanceType
}; };
} }
@@ -23,6 +35,7 @@ class Transaction {
date: json['date'] as String?, date: json['date'] as String?,
amount: json['amount'] as String?, amount: json['amount'] as String?,
type: json['type'] as String?, type: json['type'] as String?,
); balance: json['balance'] as String?,
balanceType: json['balanceType'] as String?);
} }
} }

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/models/transaction.dart';
abstract class TransactionRepository { abstract class TransactionRepository {
Future<List<Transaction>> fetchTransactions(String accountNo); Future<List<Transaction>> fetchTransactions(String accountNo,
{DateTime? fromDate, DateTime? toDate});
} }
class TransactionRepositoryImpl implements TransactionRepository { class TransactionRepositoryImpl implements TransactionRepository {
@@ -10,9 +12,21 @@ class TransactionRepositoryImpl implements TransactionRepository {
TransactionRepositoryImpl(this._dio); TransactionRepositoryImpl(this._dio);
@override @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) { if (resp.statusCode != 200) {
throw Exception( throw Exception(

View File

@@ -1,7 +1,6 @@
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
abstract class TransferRepository { abstract class TransferRepository {
Future<TransferResponse> transfer(Transfer transfer); Future<TransferResponse> transfer(Transfer transfer);
} }

View File

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

View File

@@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class AccountInfoScreen extends StatefulWidget { class AccountInfoScreen extends StatefulWidget {
@@ -20,57 +18,70 @@ class AccountInfoScreen extends StatefulWidget {
class _AccountInfoScreen extends State<AccountInfoScreen> { class _AccountInfoScreen extends State<AccountInfoScreen> {
late User selectedUser; late User selectedUser;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedUser = widget.users[widget.selectedIndex]; selectedUser = widget.users[widget.selectedIndex];
} }
String getFullAccountType(String? accountType) {
if (accountType == null || accountType.isEmpty) return 'N/A';
// Convert to title case
switch (accountType.toLowerCase()) {
case 'sa':
return AppLocalizations.of(context).savingsAccount;
case 'sb':
return AppLocalizations.of(context).savingsAccount;
case 'ln':
return AppLocalizations.of(context).loanAccount;
case 'td':
return AppLocalizations.of(context).termDeposit;
case 'rd':
return AppLocalizations.of(context).recurringDeposit;
case 'ca':
return "Current Account";
case 'cc':
return "Cash Credit Account";
case 'od':
return "Overdraft Account";
default:
return AppLocalizations.of(context).unknownAccount;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final users = widget.users; final users = widget.users;
int selectedIndex = widget.selectedIndex; int selectedIndex = widget.selectedIndex;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( title: Text(AppLocalizations.of(context)
icon: const Icon(Symbols.arrow_back_ios_new), .accountInfo
onPressed: () { .replaceFirst(RegExp('\n'), '')),
Navigator.pop(context);
},
), ),
title: Text( body: Stack(
AppLocalizations.of(context).accountInfo, children: [
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.w500,
),
),
centerTitle: false,
actions: [
Padding( Padding(
padding: const EdgeInsets.only(right: 10.0), padding: const EdgeInsets.all(8.0),
child: CircleAvatar( child: Column(
backgroundColor: Colors.grey[200], children: [
radius: 20, Card(
child: SvgPicture.asset( elevation: 4,
'assets/images/avatar_male.svg', margin: const EdgeInsets.symmetric(vertical: 8.0),
width: 40, child: Padding(
height: 40,
fit: BoxFit.cover,
),
),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
AppLocalizations.of(context).accountNumber, AppLocalizations.of(context).accountNumber,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
), ),
/// Dropdown to change account
DropdownButton<User>( DropdownButton<User>(
value: selectedUser, value: selectedUser,
onChanged: (User? newUser) { onChanged: (User? newUser) {
@@ -83,45 +94,83 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
items: widget.users.map((user) { items: widget.users.map((user) {
return DropdownMenuItem<User>( return DropdownMenuItem<User>(
value: user, value: user,
child: Text(user.accountNo.toString()), child: Text(
user.accountNo.toString(),
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
); );
}).toList(), }).toList(),
isExpanded: true,
), ),
],
/*InfoRow( ),
title: AppLocalizations.of(context).accountNumber, ),
value: users[selectedIndex].accountNo ?? 'N/A'), ),
// InfoRow(title: 'Nominee Customer No', value: user.nomineeCustomerNo), Expanded(
// InfoRow(title: 'SMS Service', value: user.smsService), child: Card(
// InfoRow(title: 'Missed Call Service', value: user.missedCallService),*/ elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
InfoRow( InfoRow(
title: AppLocalizations.of(context).customerNumber, title: AppLocalizations.of(context).customerNumber,
value: selectedUser.cifNumber ?? 'N/A', value: selectedUser.cifNumber ?? 'N/A',
), ),
InfoRow(
title: AppLocalizations.of(context).accountType,
value: getFullAccountType(selectedUser.accountType),
),
InfoRow( InfoRow(
title: AppLocalizations.of(context).productName, title: AppLocalizations.of(context).productName,
value: selectedUser.productType ?? 'N/A', value: selectedUser.productType ?? 'N/A',
), ),
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'),
InfoRow( InfoRow(
title: AppLocalizations.of(context).accountStatus, title: AppLocalizations.of(context).accountStatus,
value: 'OPEN', value: 'OPEN',
), ),
InfoRow( InfoRow(
title: AppLocalizations.of(context).availableBalance, title:
AppLocalizations.of(context).availableBalance,
value: selectedUser.availableBalance ?? 'N/A', value: selectedUser.availableBalance ?? 'N/A',
), ),
InfoRow( InfoRow(
title: AppLocalizations.of(context).currentBalance, title: AppLocalizations.of(context).currentBalance,
value: selectedUser.currentBalance ?? 'N/A', value: selectedUser.currentBalance ?? 'N/A',
), ),
if (users[selectedIndex].approvedAmount != null)
users[selectedIndex].approvedAmount != null InfoRow(
? InfoRow( title:
title: AppLocalizations.of(context).approvedAmount, AppLocalizations.of(context).approvedAmount,
value: selectedUser.approvedAmount ?? 'N/A', value: selectedUser.approvedAmount ?? 'N/A',
) ),
: const SizedBox.shrink(), ],
),
),
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
], ],
), ),
); );
@@ -136,6 +185,7 @@ class InfoRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container( return Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 8), margin: const EdgeInsets.symmetric(vertical: 8),
@@ -144,16 +194,19 @@ class InfoRow extends StatelessWidget {
children: [ children: [
Text( Text(
title, title,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontSize: 18,
fontWeight: FontWeight.w500, fontWeight: FontWeight.bold,
color: Colors.black87, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(
value, value,
style: const TextStyle(fontSize: 16, color: Colors.black), style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface),
), ),
], ],
), ),

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,135 @@
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: Stack(
children: [
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
? const Color(0xFF10BB10)
: Theme.of(context).colorScheme.error,
size: 28,
),
],
),
const SizedBox(height: 8),
// Date centered
Text(
transaction.date ?? "",
style: TextStyle(
fontSize: 16,
color: Theme.of(context).textTheme.bodySmall?.color,
),
textAlign: TextAlign.center,
),
],
),
),
),
Divider(color: Theme.of(context).dividerColor),
Expanded(
flex: 5,
child: ListView(
children: [
_buildDetailRow(
AppLocalizations.of(context).transactionType,
transaction.type ?? ""),
_buildDetailRow(AppLocalizations.of(context).transferType,
transaction.name.split("/").first ?? ""),
// if (transaction.name.length > 12) ...[
// _buildDetailRow(AppLocalizations.of(context).utrNo,
// transaction.name.split("= ")[1].split(" ")[0] ?? ""),
// _buildDetailRow(
// AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "")
// ]
_buildDetailRow(AppLocalizations.of(context).details,
transaction.name),
],
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
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

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

View File

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

View File

@@ -0,0 +1,27 @@
import 'dart:developer';
import 'package:flutter_bloc/flutter_bloc.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(const ThemeState(themeType: ThemeType.violet)) {
loadTheme();
}
Future<void> loadTheme() async {
final prefs = await SharedPreferences.getInstance();
final themeIndex = prefs.getInt('theme_type') ?? 0;
final type = ThemeType.values[themeIndex];
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);
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

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'dart:math';
import 'dart:developer'; // import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:flutter/services.dart';
enum MPinMode { enter, set, confirm } enum MPinMode { enter, set, confirm }
@@ -14,38 +15,96 @@ class MPinScreen extends StatefulWidget {
final MPinMode mode; final MPinMode mode;
final String? initialPin; final String? initialPin;
final void Function(String pin)? onCompleted; final void Function(String pin)? onCompleted;
final bool disableBiometric;
final String? customTitle;
final String? customConfirmTitle;
const MPinScreen({ const MPinScreen({
super.key, super.key,
required this.mode, required this.mode,
this.initialPin, this.initialPin,
this.onCompleted, this.onCompleted,
this.disableBiometric = false,
this.customTitle,
this.customConfirmTitle,
}); });
@override @override
State<MPinScreen> createState() => _MPinScreenState(); State<MPinScreen> createState() => _MPinScreenState();
} }
class _MPinScreenState extends State<MPinScreen> { class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
List<String> mPin = []; List<String> mPin = [];
String? errorText; String? errorText;
// Animation controllers
late final AnimationController _bounceController;
late final Animation<double> _bounceAnimation;
late final AnimationController _shakeController;
late final AnimationController _waveController;
late final Animation<double> _waveAnimation;
// State flags for animations
bool _isError = false;
bool _isSuccess = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.mode == MPinMode.enter) { // 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 && !widget.disableBiometric) {
_tryBiometricBeforePin(); _tryBiometricBeforePin();
} }
} }
@override
void dispose() {
_bounceController.dispose();
_shakeController.dispose();
_waveController.dispose();
super.dispose();
}
Future<void> _tryBiometricBeforePin() async { Future<void> _tryBiometricBeforePin() async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final enabled = await storage.read('biometric_enabled'); final enabled = await storage.read('biometric_enabled');
log('biometric_enabled: $enabled'); // log('biometric_enabled: $enabled');
if (enabled != null && enabled) { if (enabled != null && enabled) {
final auth = LocalAuthentication(); final auth = LocalAuthentication();
if (await auth.canCheckBiometrics) { if (await auth.canCheckBiometrics) {
final didAuth = await auth.authenticate( final didAuth = await auth.authenticate(
// ignore: use_build_context_synchronously
localizedReason: AppLocalizations.of(context).authenticateToAccess, localizedReason: AppLocalizations.of(context).authenticateToAccess,
options: const AuthenticationOptions(biometricOnly: true), options: const AuthenticationOptions(biometricOnly: true),
); );
@@ -61,11 +120,13 @@ class _MPinScreenState extends State<MPinScreen> {
} }
void addDigit(String digit) { void addDigit(String digit) {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.length < 4) { if (mPin.length < 4) {
setState(() { setState(() {
mPin.add(digit); mPin.add(digit);
errorText = null; errorText = null;
}); });
_bounceController.forward(from: 0);
if (mPin.length == 4) { if (mPin.length == 4) {
_handleComplete(); _handleComplete();
} }
@@ -73,6 +134,7 @@ class _MPinScreenState extends State<MPinScreen> {
} }
void deleteDigit() { void deleteDigit() {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
if (mPin.isNotEmpty) { if (mPin.isNotEmpty) {
setState(() { setState(() {
mPin.removeLast(); mPin.removeLast();
@@ -82,71 +144,148 @@ class _MPinScreenState extends State<MPinScreen> {
} }
Future<void> _handleComplete() async { Future<void> _handleComplete() async {
if (_shakeController.isAnimating || _waveController.isAnimating) return;
final pin = mPin.join(); final pin = mPin.join();
final storage = SecureStorage(); final storage = SecureStorage();
switch (widget.mode) { switch (widget.mode) {
case MPinMode.enter: case MPinMode.enter:
final storedPin = await storage.read('mpin'); final storedPin = await storage.read('mpin');
log('storedPin: $storedPin'); // log('storedPin: $storedPin');
if (storedPin == int.tryParse(pin)) { if (storedPin == int.tryParse(pin)) {
widget.onCompleted?.call(pin); // Correct PIN
} else {
setState(() { setState(() {
_isSuccess = true;
errorText = null;
});
await Future.delayed(const Duration(milliseconds: 100));
_waveController.forward(from: 0).whenComplete(() {
widget.onCompleted?.call(pin);
});
} else {
// Incorrect PIN
setState(() {
_isError = true;
errorText = AppLocalizations.of(context).incorrectMPIN; errorText = AppLocalizations.of(context).incorrectMPIN;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear(); mPin.clear();
_isError = false;
// Keep error text until next digit is entered
}); });
} }
break; break;
case MPinMode.set: case MPinMode.set:
// propagate parent onCompleted into confirm step // Navigate to confirm and wait for result
Navigator.push( final result = await Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => MPinScreen( builder: (_) => MPinScreen(
mode: MPinMode.confirm, mode: MPinMode.confirm,
initialPin: pin, initialPin: pin,
onCompleted: widget.onCompleted, // <-- use parent callback onCompleted: (confirmedPin) {
// Just pop with the pin, don't call parent callback yet
Navigator.of(context).pop(confirmedPin);
},
disableBiometric: widget.disableBiometric,
customTitle: widget.customConfirmTitle,
), ),
), ),
); );
// If confirm succeeded, call parent callback
if (result != null && mounted) {
widget.onCompleted?.call(result);
}
break; break;
case MPinMode.confirm: case MPinMode.confirm:
if (widget.initialPin == pin) { if (widget.initialPin == pin) {
// 1) persist the pin // 1) persist the pin
await storage.write('mpin', pin); await storage.write('mpin', pin);
// 3) now clear the entire navigation stack and go to your main scaffold // 2) Call the onCompleted callback to let the parent handle navigation
if (mounted) { if (mounted) {
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil( widget.onCompleted?.call(pin);
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
} }
} else { } else {
setState(() { setState(() {
_isError = true;
errorText = AppLocalizations.of(context).pinsDoNotMatch; errorText = AppLocalizations.of(context).pinsDoNotMatch;
});
await _shakeController.forward(from: 0);
setState(() {
mPin.clear(); mPin.clear();
_isError = false;
}); });
} }
break; break;
} }
} }
Widget buildMPinDots() { Widget buildMPinDots(BuildContext context) {
return Row( return AnimatedBuilder(
animation: Listenable.merge(
[_bounceController, _shakeController, _waveController]),
builder: (context, child) {
double shakeOffset = 0;
if (_shakeController.isAnimating) {
// 4 cycles of sine wave for shake
shakeOffset = sin(_shakeController.value * 4 * pi) * 12;
}
return Transform.translate(
offset: Offset(shakeOffset, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) { children: List.generate(4, (index) {
return Container( 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), margin: const EdgeInsets.all(8),
width: 15, width: 25,
height: 15, height: 25,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: index < mPin.length ? Colors.black : Colors.grey[400], color: dotColor,
),
), ),
); );
}), }),
),
);
},
); );
} }
@@ -164,9 +303,10 @@ class _MPinScreenState extends State<MPinScreen> {
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: row.map((key) { children: row.map((key) {
return Padding( return Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(1.0),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
HapticFeedback.lightImpact();
if (key == '<') { if (key == '<') {
deleteDigit(); deleteDigit();
} else if (key == 'Enter') { } else if (key == 'Enter') {
@@ -184,11 +324,10 @@ class _MPinScreenState extends State<MPinScreen> {
} }
}, },
child: Container( child: Container(
width: 70, width: 80,
height: 70, height: 80,
decoration: BoxDecoration( decoration: const BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: Colors.grey[200],
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: key == 'Enter' child: key == 'Enter'
@@ -196,8 +335,10 @@ class _MPinScreenState extends State<MPinScreen> {
: Text( : Text(
key == '<' ? '' : key, key == '<' ? '' : key,
style: TextStyle( style: TextStyle(
fontSize: 20, fontSize: 30,
color: key == 'Enter' ? Colors.blue : Colors.black, color: key == 'Enter'
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.onSurface,
), ),
), ),
), ),
@@ -210,6 +351,9 @@ class _MPinScreenState extends State<MPinScreen> {
} }
String getTitle() { String getTitle() {
if (widget.customTitle != null) {
return widget.customTitle!;
}
switch (widget.mode) { switch (widget.mode) {
case MPinMode.enter: case MPinMode.enter:
return AppLocalizations.of(context).enterMPIN; return AppLocalizations.of(context).enterMPIN;
@@ -226,7 +370,7 @@ class _MPinScreenState extends State<MPinScreen> {
body: SafeArea( body: SafeArea(
child: Column( child: Column(
children: [ children: [
const Spacer(), const SizedBox(height: 50),
// Logo // Logo
Image.asset('assets/images/logo.png', height: 100), Image.asset('assets/images/logo.png', height: 100),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -234,19 +378,18 @@ class _MPinScreenState extends State<MPinScreen> {
getTitle(), getTitle(),
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w500),
), ),
const SizedBox(height: 20), const Spacer(),
buildMPinDots(), buildMPinDots(context),
if (errorText != null) if (errorText != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 8.0), padding: const EdgeInsets.only(top: 8.0),
child: Text( child: Text(
errorText!, errorText!,
style: const TextStyle(color: Colors.red), style: TextStyle(color: Theme.of(context).colorScheme.error),
), ),
), ),
const Spacer(), const Spacer(),
buildNumberPad(), buildNumberPad(),
const Spacer(),
], ],
), ),
), ),

View File

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

View File

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

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