Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 715162b713 | |||
| 8149ef2a5b | |||
| 1a2dea611b | |||
| 72a2c56392 | |||
| aef82237ac | |||
| 974f42bf95 | |||
| 4a8c69bb1e | |||
| 86aaaa1f6d | |||
| 6796793aac | |||
| fbf6df7181 | |||
| c7111d518a | |||
| 5d307607fd | |||
| 992092052a | |||
| 64fedabd89 | |||
| 4fc6f54fcd | |||
| 8c7e94759a | |||
| 8aa5b170ca | |||
| 04a1ce26ec | |||
| b19bc2e222 | |||
| b9147b30d5 | |||
| 3358ec7669 | |||
| 18db360a45 | |||
| b7fe6a9d18 | |||
| adb9a5330b | |||
| 0075abc906 | |||
| 353ec63916 | |||
| 71b52cfb43 | |||
| c1df43e9b6 | |||
| f0d5233afc | |||
| 4fe6af4098 | |||
| fda5d075ff | |||
| 71e0521dec | |||
| f6e851a9ee | |||
| 547f534037 | |||
| 66b2e71140 | |||
| 43d92d799b | |||
| 3135116f26 | |||
| 39165d631e | |||
|
|
ef481ec879 | ||
|
|
36702b198f | ||
|
|
f0718e9d68 | ||
|
|
d2cce89efb | ||
| 8cfca113bf | |||
| d6f61ebb31 | |||
| 078e715d20 | |||
| 5c8df8ace3 | |||
| 3e88aad43f | |||
| 5b7f3f0096 | |||
| b5b6c6ed49 | |||
| c26cc507a1 | |||
| 87fd36b748 | |||
| 3417c4b0e5 | |||
| 151140d563 | |||
| a8ee7833be | |||
| f73faaa635 | |||
| 5ac977e903 | |||
| f15b8ac3f7 | |||
| 8f8fdb70e6 | |||
| d86ff2c427 | |||
| 527111c1de | |||
| dfbdb3238d | |||
| 3d13edf676 | |||
| 32e8b85cee | |||
| 58e53d0aeb | |||
| 06ef2ab36b | |||
| 0362bf2013 | |||
| 73b96b82f7 | |||
| c78a90dbfe | |||
| df025babd5 | |||
| 4d19bf6146 | |||
| d36cad31c1 | |||
| 39e7a02ca5 | |||
| 0c7470d74b | |||
| 32463680e8 | |||
| 00cb98ae83 | |||
| 18844495c2 | |||
| 3f7869677c | |||
| 0e4072fe8f | |||
| b5acae85d5 | |||
| cc7c7a8042 | |||
| 475f30a4bb | |||
| b00bc6c8c0 | |||
| c47862bd60 | |||
| 8ef4f1327f | |||
| 87e00d540f | |||
| 44a6307995 | |||
| f6b24e4c6f | |||
| 9cf4c44bb0 | |||
| b3b51d423d | |||
| b1f4d380c6 | |||
| 17ebf8626a | |||
| e466dff424 | |||
| 8f2b981b5b | |||
| ba1ef0ed24 | |||
| d2044d49b5 | |||
| db39cfbcc9 | |||
| bf23627e0a | |||
| eba38c2e42 | |||
|
|
d4bba6dc49 | ||
| b56bf0d7df | |||
| 82e057d804 | |||
| 0f205873a9 | |||
| 191610c9b2 | |||
| b03e917d78 | |||
|
|
188fbe9bb1 | ||
|
|
817f3d75f5 | ||
|
|
c42b973bee | ||
|
|
def009003c | ||
| 52e9f59e6f | |||
| e4e104837b | |||
| dd7b7a6f4c | |||
| c322d1d2fd | |||
| b3fb387bdd | |||
|
|
b62b8a157d | ||
| c729b775c9 | |||
| 465065db57 | |||
| 0a6dde9ead | |||
| b513664a47 | |||
| d8d87e8da4 | |||
| 1204507375 | |||
| be9a6fc93f | |||
| b8337d1152 | |||
| d3792a1a06 |
5
.gitignore
vendored
@@ -44,3 +44,8 @@ app.*.map.json
|
|||||||
lib/l10n/app_localizations.dart
|
lib/l10n/app_localizations.dart
|
||||||
lib/l10n/app_localizations_en.dart
|
lib/l10n/app_localizations_en.dart
|
||||||
lib/l10n/app_localizations_hi.dart
|
lib/l10n/app_localizations_hi.dart
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
android/key.properties
|
||||||
|
android/*.jks
|
||||||
|
android/*.keystore
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
errors:
|
||||||
dead_code: ignore
|
dead_code: ignore
|
||||||
|
non_constant_identifier_names: ignore
|
||||||
include: package:flutter_lints/flutter.yaml
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
|
|||||||
@@ -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 "27.0.12077973"
|
ndkVersion "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -51,15 +58,21 @@ android {
|
|||||||
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.
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,4 +81,6 @@ flutter {
|
|||||||
source '../..'
|
source '../..'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {}
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
<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}"
|
||||||
@@ -40,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"/>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
android/app/src/main/res/drawable/notification_icon.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 26 KiB |
BIN
assets/images/ipos_logo.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 90 KiB |
BIN
assets/images/logo_2.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/images/profile.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/images/profile.svg
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
assets/images/uco_logo.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/yes_bank_logo.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
flutter_01.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
@@ -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++";
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 21 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -8,6 +8,41 @@ 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(
|
||||||
@@ -25,7 +60,8 @@ class AuthService {
|
|||||||
print(e.toString());
|
print(e.toString());
|
||||||
}
|
}
|
||||||
if (e.response?.statusCode == 401) {
|
if (e.response?.statusCode == 401) {
|
||||||
throw AuthException('Invalid credentials');
|
throw AuthException(
|
||||||
|
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
|
||||||
}
|
}
|
||||||
throw NetworkException('Network error during login');
|
throw NetworkException('Network error during login');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -91,4 +127,72 @@ class AuthService {
|
|||||||
'Unexpected error during TPIN setup: ${e.toString()}');
|
'Unexpected error during TPIN setup: ${e.toString()}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> sendOtpForSettingPassword(String customerNo) async {
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await _dio.get('/api/otp/send/set-password', queryParameters: {
|
||||||
|
'customerNo': customerNo,
|
||||||
|
});
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception(response.data['error'] ?? 'Failed to send OTP');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> verifyOtpForSettingPassword(
|
||||||
|
String customerNo, String otp) async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.get(
|
||||||
|
'/api/otp/verify/set-password',
|
||||||
|
queryParameters: {'customerNo': customerNo, 'otp': otp},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return response.data['token'];
|
||||||
|
} else {
|
||||||
|
throw Exception(response.data['error'] ?? 'Failed to verify OTP');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw Exception('Network error: ${e.message}');
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future changePassword(String newPassword, String token) async {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/auth/login_password',
|
||||||
|
data: {'login_password': newPassword},
|
||||||
|
options: Options(headers: {'Authorization': 'Bearer $token'}),
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Error setting password');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future setTncflag() async {
|
||||||
|
try {
|
||||||
|
final response = await _dio.post(
|
||||||
|
'/api/auth/tnc',
|
||||||
|
data: {"flag": 'Y'},
|
||||||
|
);
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw AuthException('Failed to proceed with T&C');
|
||||||
|
}
|
||||||
|
} on DioException catch (e) {
|
||||||
|
if (kDebugMode) {
|
||||||
|
print(e.toString());
|
||||||
|
}
|
||||||
|
throw NetworkException('Network error during T&C Setup');
|
||||||
|
} catch (e) {
|
||||||
|
throw UnexpectedException('Unexpected error: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:kmobile/core/errors/exceptions.dart';
|
||||||
import 'package:kmobile/data/models/ifsc.dart';
|
import 'package:kmobile/data/models/ifsc.dart';
|
||||||
import 'package:kmobile/data/models/beneficiary.dart';
|
import 'package:kmobile/data/models/beneficiary.dart';
|
||||||
|
|
||||||
@@ -39,9 +40,12 @@ class BeneficiaryService {
|
|||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == 404) {
|
if (e.response?.statusCode == 404) {
|
||||||
throw Exception('INVALID IFSC CODE');
|
throw Exception('INVALID IFSC CODE');
|
||||||
|
} else if (e.response?.statusCode == 401) {
|
||||||
|
throw Exception('INVALID IFSC CODE');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
rethrow;
|
throw UnexpectedException(
|
||||||
|
'Unexpected error during login: ${e.toString()}');
|
||||||
}
|
}
|
||||||
return Ifsc.fromJson({});
|
return Ifsc.fromJson({});
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,10 @@ class BeneficiaryService {
|
|||||||
'ifscCode': ifscCode,
|
'ifscCode': ifscCode,
|
||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
},
|
},
|
||||||
|
options: Options(
|
||||||
|
sendTimeout: const Duration(seconds: 60),
|
||||||
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Invalid Beneficiary Details");
|
throw Exception("Invalid Beneficiary Details");
|
||||||
@@ -66,7 +74,7 @@ class BeneficiaryService {
|
|||||||
return response.data['name'];
|
return response.data['name'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Data for Validation
|
// Beneficiary Validate And ADD
|
||||||
Future<bool> sendForValidation(Beneficiary beneficiary) async {
|
Future<bool> sendForValidation(Beneficiary beneficiary) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
|
|||||||
136
lib/api/services/branch_service.dart
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/api/services/change_password_service.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/api/services/limit_service.dart
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
102
lib/api/services/send_sms_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
94
lib/app.dart
@@ -12,14 +12,15 @@ 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/splash_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 '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});
|
||||||
@@ -34,25 +35,46 @@ class KMobile extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _KMobileState extends State<KMobile> {
|
class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
|
||||||
bool showSplash = true;
|
Timer? _backgroundTimer;
|
||||||
Locale? _locale;
|
Locale? _locale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addObserver(this);
|
||||||
loadPreferences();
|
loadPreferences();
|
||||||
Future.delayed(const Duration(seconds: 3), () {
|
}
|
||||||
setState(() {
|
|
||||||
showSplash = false;
|
@override
|
||||||
});
|
void dispose() {
|
||||||
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
_backgroundTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
|
super.didChangeAppLifecycleState(state);
|
||||||
|
switch (state) {
|
||||||
|
case AppLifecycleState.resumed:
|
||||||
|
_backgroundTimer?.cancel();
|
||||||
|
break;
|
||||||
|
case AppLifecycleState.paused:
|
||||||
|
_backgroundTimer = Timer(const Duration(minutes: 2), () {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
SystemNavigator.pop();
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
});
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadPreferences() async {
|
Future<void> loadPreferences() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
// Load Locale
|
|
||||||
final String? langCode = prefs.getString('locale');
|
final String? langCode = prefs.getString('locale');
|
||||||
if (langCode != null) {
|
if (langCode != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -103,9 +125,12 @@ class _KMobileState extends State<KMobile> {
|
|||||||
theme: themeState.getLightThemeData(),
|
theme: themeState.getLightThemeData(),
|
||||||
darkTheme: themeState.getDarkThemeData(),
|
darkTheme: themeState.getDarkThemeData(),
|
||||||
themeMode: context.watch<ThemeModeCubit>().state.mode,
|
themeMode: context.watch<ThemeModeCubit>().state.mode,
|
||||||
|
navigatorObservers: [
|
||||||
|
getIt<RouteObserver<ModalRoute<void>>>(),
|
||||||
|
],
|
||||||
onGenerateRoute: AppRoutes.generateRoute,
|
onGenerateRoute: AppRoutes.generateRoute,
|
||||||
initialRoute: AppRoutes.splash,
|
initialRoute: AppRoutes.splash,
|
||||||
home: showSplash ? const SplashScreen() : const AuthGate(),
|
home: const AuthGate(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -117,7 +142,6 @@ class _KMobileState extends State<KMobile> {
|
|||||||
|
|
||||||
class AuthGate extends StatefulWidget {
|
class AuthGate extends StatefulWidget {
|
||||||
const AuthGate({super.key});
|
const AuthGate({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AuthGate> createState() => _AuthGateState();
|
State<AuthGate> createState() => _AuthGateState();
|
||||||
}
|
}
|
||||||
@@ -178,9 +202,8 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_checking) {
|
if (_checking) {
|
||||||
return const SplashScreen();
|
return const LoginScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isLoggedIn) {
|
if (_isLoggedIn) {
|
||||||
if (_hasMPin) {
|
if (_hasMPin) {
|
||||||
if (_biometricEnabled) {
|
if (_biometricEnabled) {
|
||||||
@@ -188,13 +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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return MPinScreen(
|
return MPinScreen(
|
||||||
mode: MPinMode.enter,
|
mode: MPinMode.enter,
|
||||||
onCompleted: (_) {
|
onCompleted: (_) {
|
||||||
@@ -225,7 +246,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
onCompleted: (_) async {
|
onCompleted: (_) async {
|
||||||
final storage = getIt<SecureStorage>();
|
final storage = getIt<SecureStorage>();
|
||||||
final localAuth = LocalAuthentication();
|
final localAuth = LocalAuthentication();
|
||||||
|
|
||||||
final optIn = await showDialog<bool>(
|
final optIn = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
@@ -246,7 +266,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (optIn == true) {
|
if (optIn == true) {
|
||||||
final canCheck = await localAuth.canCheckBiometrics;
|
final canCheck = await localAuth.canCheckBiometrics;
|
||||||
bool didAuth = false;
|
bool didAuth = false;
|
||||||
@@ -254,7 +273,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
authEnable = AppLocalizations.of(context).authenticateToEnable;
|
authEnable = AppLocalizations.of(context).authenticateToEnable;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canCheck) {
|
if (canCheck) {
|
||||||
didAuth = await localAuth.authenticate(
|
didAuth = await localAuth.authenticate(
|
||||||
localizedReason: authEnable,
|
localizedReason: authEnable,
|
||||||
@@ -269,7 +287,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
await storage.write('biometric_enabled', 'false');
|
await storage.write('biometric_enabled', 'false');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -287,7 +304,6 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
|
|
||||||
class NavigationScaffold extends StatefulWidget {
|
class NavigationScaffold extends StatefulWidget {
|
||||||
const NavigationScaffold({super.key});
|
const NavigationScaffold({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<NavigationScaffold> createState() => _NavigationScaffoldState();
|
State<NavigationScaffold> createState() => _NavigationScaffoldState();
|
||||||
}
|
}
|
||||||
@@ -295,10 +311,23 @@ 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(),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -342,10 +371,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
|||||||
bottomNavigationBar: BottomNavigationBar(
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
currentIndex: _selectedIndex,
|
currentIndex: _selectedIndex,
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: const Color(0XFF1E58AD),
|
||||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
selectedItemColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
unselectedItemColor:
|
unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
|
||||||
onTap: (index) {
|
onTap: (index) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedIndex = index;
|
_selectedIndex = index;
|
||||||
@@ -358,8 +386,8 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
|||||||
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),
|
||||||
@@ -372,11 +400,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this widget at the end of the file
|
|
||||||
class BiometricPromptScreen extends StatelessWidget {
|
class BiometricPromptScreen extends StatelessWidget {
|
||||||
final VoidCallback onCompleted;
|
final VoidCallback onCompleted;
|
||||||
const BiometricPromptScreen({super.key, required this.onCompleted});
|
const BiometricPromptScreen({super.key, required this.onCompleted});
|
||||||
|
|
||||||
Future<void> _handleBiometric(BuildContext context) async {
|
Future<void> _handleBiometric(BuildContext context) async {
|
||||||
final localAuth = LocalAuthentication();
|
final localAuth = LocalAuthentication();
|
||||||
final canCheck = await localAuth.canCheckBiometrics;
|
final canCheck = await localAuth.canCheckBiometrics;
|
||||||
@@ -407,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 {
|
||||||
|
|||||||
@@ -10,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
|
||||||
@@ -34,7 +35,9 @@ class AppRoutes {
|
|||||||
return MaterialPageRoute(builder: (_) => const SplashScreen());
|
return MaterialPageRoute(builder: (_) => const SplashScreen());
|
||||||
case login:
|
case login:
|
||||||
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
return MaterialPageRoute(builder: (_) => const LoginScreen());
|
||||||
|
case TncRequiredScreen.routeName: // Renamed class
|
||||||
|
return MaterialPageRoute(
|
||||||
|
builder: (_) => const TncRequiredScreen()); // Renamed class
|
||||||
case mPin:
|
case mPin:
|
||||||
return MaterialPageRoute(
|
return MaterialPageRoute(
|
||||||
builder: (_) => const MPinScreen(
|
builder: (_) => const MPinScreen(
|
||||||
|
|||||||
@@ -15,7 +15,17 @@ class AppThemes {
|
|||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
textTheme: GoogleFonts.rubikTextTheme(),
|
textTheme: GoogleFonts.rubikTextTheme(),
|
||||||
);
|
).copyWith(
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: const Color(0xFF01A04C),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
iconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static ThemeData getDarkTheme(ThemeType type) {
|
static ThemeData getDarkTheme(ThemeType type) {
|
||||||
@@ -32,7 +42,17 @@ class AppThemes {
|
|||||||
textTheme: GoogleFonts.rubikTextTheme(
|
textTheme: GoogleFonts.rubikTextTheme(
|
||||||
ThemeData(brightness: Brightness.dark).textTheme,
|
ThemeData(brightness: Brightness.dark).textTheme,
|
||||||
),
|
),
|
||||||
);
|
).copyWith(
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
backgroundColor: const Color(0xFF01A04C),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: colorScheme.onPrimary,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
iconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
static Color _getSeedColor(ThemeType type) {
|
static Color _getSeedColor(ThemeType type) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class Beneficiary {
|
|||||||
final String accountNo;
|
final String accountNo;
|
||||||
final String accountType;
|
final String accountType;
|
||||||
final String name;
|
final String name;
|
||||||
|
final DateTime? createdAt;
|
||||||
final String ifscCode;
|
final String ifscCode;
|
||||||
final String? bankName;
|
final String? bankName;
|
||||||
final String? branchName;
|
final String? branchName;
|
||||||
@@ -11,6 +12,7 @@ class Beneficiary {
|
|||||||
required this.accountNo,
|
required this.accountNo,
|
||||||
required this.accountType,
|
required this.accountType,
|
||||||
required this.name,
|
required this.name,
|
||||||
|
this.createdAt,
|
||||||
required this.ifscCode,
|
required this.ifscCode,
|
||||||
this.bankName,
|
this.bankName,
|
||||||
this.branchName,
|
this.branchName,
|
||||||
@@ -21,6 +23,9 @@ class Beneficiary {
|
|||||||
return Beneficiary(
|
return Beneficiary(
|
||||||
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
|
accountNo: json['account_no'] ?? json['accountNo'] ?? '',
|
||||||
accountType: json['account_type'] ?? json['accountType'] ?? '',
|
accountType: json['account_type'] ?? json['accountType'] ?? '',
|
||||||
|
createdAt: json['createdAt'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.tryParse(json['createdAt']),
|
||||||
name: json['name'] ?? '',
|
name: json['name'] ?? '',
|
||||||
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
|
ifscCode: json['ifsc_code'] ?? json['ifscCode'] ?? '',
|
||||||
bankName: json['bank_name'] ?? json['bankName'] ?? '',
|
bankName: json['bank_name'] ?? json['bankName'] ?? '',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class ImpsTransaction {
|
|||||||
final String? remitterName;
|
final String? remitterName;
|
||||||
final String beneficiaryName;
|
final String beneficiaryName;
|
||||||
final String tpin;
|
final String tpin;
|
||||||
|
final String? remarks;
|
||||||
|
|
||||||
ImpsTransaction({
|
ImpsTransaction({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -15,6 +16,7 @@ class ImpsTransaction {
|
|||||||
this.remitterName,
|
this.remitterName,
|
||||||
required this.beneficiaryName,
|
required this.beneficiaryName,
|
||||||
required this.tpin,
|
required this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -26,6 +28,7 @@ class ImpsTransaction {
|
|||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
'beneficiaryName': beneficiaryName,
|
'beneficiaryName': beneficiaryName,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class NeftTransaction {
|
|||||||
final String remitterName;
|
final String remitterName;
|
||||||
final String beneficiaryName;
|
final String beneficiaryName;
|
||||||
final String tpin;
|
final String tpin;
|
||||||
|
final String? remarks;
|
||||||
|
|
||||||
NeftTransaction({
|
NeftTransaction({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -15,6 +16,7 @@ class NeftTransaction {
|
|||||||
required this.remitterName,
|
required this.remitterName,
|
||||||
required this.beneficiaryName,
|
required this.beneficiaryName,
|
||||||
required this.tpin,
|
required this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -26,6 +28,7 @@ class NeftTransaction {
|
|||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
'beneficiaryName': beneficiaryName,
|
'beneficiaryName': beneficiaryName,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class RtgsTransaction {
|
|||||||
final String remitterName;
|
final String remitterName;
|
||||||
final String beneficiaryName;
|
final String beneficiaryName;
|
||||||
final String tpin;
|
final String tpin;
|
||||||
|
final String? remarks;
|
||||||
|
|
||||||
RtgsTransaction({
|
RtgsTransaction({
|
||||||
required this.fromAccount,
|
required this.fromAccount,
|
||||||
@@ -15,6 +16,7 @@ class RtgsTransaction {
|
|||||||
required this.remitterName,
|
required this.remitterName,
|
||||||
required this.beneficiaryName,
|
required this.beneficiaryName,
|
||||||
required this.tpin,
|
required this.tpin,
|
||||||
|
this.remarks,
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -26,6 +28,7 @@ class RtgsTransaction {
|
|||||||
'remitterName': remitterName,
|
'remitterName': remitterName,
|
||||||
'beneficiaryName': beneficiaryName,
|
'beneficiaryName': beneficiaryName,
|
||||||
'tpin': tpin,
|
'tpin': tpin,
|
||||||
|
'remarks': remarks,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,12 @@ 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 =
|
final credentials =
|
||||||
AuthCredentials(customerNo: customerNo, password: password);
|
AuthCredentials(customerNo: customerNo, password: password);
|
||||||
@@ -27,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 {
|
||||||
@@ -47,24 +49,38 @@ class AuthRepository {
|
|||||||
await _secureStorage.write(_accessTokenKey, token.accessToken);
|
await _secureStorage.write(_accessTokenKey, token.accessToken);
|
||||||
await _secureStorage.write(
|
await _secureStorage.write(
|
||||||
_tokenExpiryKey, token.expiresAt.toIso8601String());
|
_tokenExpiryKey, token.expiresAt.toIso8601String());
|
||||||
|
await _secureStorage.write(_tncKey, token.tnc.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearAuthTokens() async {
|
Future<void> clearAuthTokens() async {
|
||||||
await _secureStorage.deleteAll();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:developer';
|
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:kmobile/data/models/transaction.dart';
|
import 'package:kmobile/data/models/transaction.dart';
|
||||||
@@ -25,8 +23,6 @@ class TransactionRepositoryImpl implements TransactionRepository {
|
|||||||
queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate);
|
queryParameters['toDate'] = DateFormat('ddMMyyyy').format(toDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
log('query params below');
|
|
||||||
log(queryParameters.toString());
|
|
||||||
final resp = await _dio.get(
|
final resp = await _dio.get(
|
||||||
'/api/transactions/account/$accountNo',
|
'/api/transactions/account/$accountNo',
|
||||||
queryParameters: queryParameters.isNotEmpty ? queryParameters : null,
|
queryParameters: queryParameters.isNotEmpty ? queryParameters : null,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/branch_service.dart';
|
||||||
|
import 'package:kmobile/api/services/limit_service.dart';
|
||||||
import 'package:kmobile/api/services/rtgs_service.dart';
|
import 'package:kmobile/api/services/rtgs_service.dart';
|
||||||
import 'package:kmobile/api/services/neft_service.dart';
|
import 'package:kmobile/api/services/neft_service.dart';
|
||||||
import 'package:kmobile/api/services/imps_service.dart';
|
import 'package:kmobile/api/services/imps_service.dart';
|
||||||
@@ -11,6 +14,7 @@ import 'package:kmobile/features/auth/controllers/theme_cubit.dart';
|
|||||||
import 'package:kmobile/features/auth/controllers/theme_mode_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';
|
||||||
@@ -18,6 +22,8 @@ import '../security/secure_storage.dart';
|
|||||||
final getIt = GetIt.instance;
|
final getIt = GetIt.instance;
|
||||||
|
|
||||||
Future<void> setupDependencies() async {
|
Future<void> setupDependencies() async {
|
||||||
|
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
|
||||||
|
RouteObserver<ModalRoute<void>>());
|
||||||
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
|
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
|
||||||
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
|
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
|
||||||
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
|
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
|
||||||
@@ -45,9 +51,14 @@ Future<void> setupDependencies() async {
|
|||||||
|
|
||||||
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
|
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
|
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
|
||||||
|
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
|
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
|
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
|
||||||
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
|
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
|
||||||
|
getIt.registerSingleton<BranchService>(BranchService(getIt<Dio>()));
|
||||||
|
getIt.registerLazySingleton<ChangePasswordService>(
|
||||||
|
() => ChangePasswordService(getIt<Dio>()),
|
||||||
|
);
|
||||||
|
|
||||||
// Add auth interceptor after repository is available
|
// Add auth interceptor after repository is available
|
||||||
getIt<Dio>().interceptors.add(
|
getIt<Dio>().interceptors.add(
|
||||||
@@ -55,21 +66,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:8080', //test
|
||||||
//'http://localhost:8081',
|
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
|
||||||
connectTimeout: const Duration(seconds: 5),
|
//'https://kccbmbnk.net', //prod small
|
||||||
receiveTimeout: const Duration(seconds: 10),
|
connectTimeout: const Duration(seconds: 60),
|
||||||
|
receiveTimeout: const Duration(seconds: 60),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
|
'X-Login-Type': 'MB',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,30 +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(
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)
|
||||||
.accountInfo
|
.accountInfo
|
||||||
.replaceFirst(RegExp('\n'), '')),
|
.replaceFirst(RegExp('\n'), '')),
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).accountNumber,
|
AppLocalizations.of(context).accountNumber,
|
||||||
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 18),
|
||||||
),
|
),
|
||||||
|
|
||||||
DropdownButton<User>(
|
DropdownButton<User>(
|
||||||
value: selectedUser,
|
value: selectedUser,
|
||||||
onChanged: (User? newUser) {
|
onChanged: (User? newUser) {
|
||||||
@@ -54,39 +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,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).customerNumber,
|
title: AppLocalizations.of(context).customerNumber,
|
||||||
value: selectedUser.cifNumber ?? 'N/A',
|
value: selectedUser.cifNumber ?? 'N/A',
|
||||||
),
|
),
|
||||||
|
InfoRow(
|
||||||
|
title: AppLocalizations.of(context).accountType,
|
||||||
|
value: getFullAccountType(selectedUser.accountType),
|
||||||
|
),
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).productName,
|
title: AppLocalizations.of(context).productName,
|
||||||
value: selectedUser.productType ?? 'N/A',
|
value: selectedUser.productType ?? 'N/A',
|
||||||
),
|
),
|
||||||
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'),
|
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: AppLocalizations.of(context).accountStatus,
|
title: AppLocalizations.of(context).accountStatus,
|
||||||
value: 'OPEN',
|
value: 'OPEN',
|
||||||
),
|
),
|
||||||
InfoRow(
|
InfoRow(
|
||||||
title: 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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -111,15 +195,18 @@ class InfoRow extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value,
|
||||||
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: theme.colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,19 +1,30 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:kmobile/data/models/transaction.dart';
|
import 'package:kmobile/data/models/transaction.dart';
|
||||||
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
||||||
import 'package:kmobile/di/injection.dart';
|
import 'package:kmobile/di/injection.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import 'transaction_details_screen.dart';
|
import 'transaction_details_screen.dart';
|
||||||
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:open_filex/open_filex.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
|
||||||
class AccountStatementScreen extends StatefulWidget {
|
class AccountStatementScreen extends StatefulWidget {
|
||||||
final String accountNo;
|
final List<User> users;
|
||||||
final String balance;
|
final int selectedIndex;
|
||||||
|
|
||||||
const AccountStatementScreen({
|
const AccountStatementScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.accountNo,
|
required this.users,
|
||||||
required this.balance,
|
required this.selectedIndex,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -21,17 +32,57 @@ class AccountStatementScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AccountStatementScreen extends State<AccountStatementScreen> {
|
class _AccountStatementScreen extends State<AccountStatementScreen> {
|
||||||
|
late User selectedUser;
|
||||||
DateTime? fromDate;
|
DateTime? fromDate;
|
||||||
DateTime? toDate;
|
DateTime? toDate;
|
||||||
bool _txLoading = true;
|
bool _txLoading = true;
|
||||||
List<Transaction> _transactions = [];
|
List<Transaction> _transactions = [];
|
||||||
final _minAmountController = TextEditingController();
|
final _minAmountController = TextEditingController();
|
||||||
final _maxAmountController = TextEditingController();
|
final _maxAmountController = TextEditingController();
|
||||||
|
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
selectedUser = widget.users[widget.selectedIndex];
|
||||||
_loadTransactions();
|
_loadTransactions();
|
||||||
|
_initializeNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initializeNotifications() async {
|
||||||
|
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
||||||
|
|
||||||
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||||
|
AndroidInitializationSettings('notification_icon');
|
||||||
|
|
||||||
|
const InitializationSettings initializationSettings =
|
||||||
|
InitializationSettings(android: initializationSettingsAndroid);
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.initialize(
|
||||||
|
initializationSettings,
|
||||||
|
onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
|
||||||
|
);
|
||||||
|
_requestNotificationPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDidReceiveNotificationResponse(
|
||||||
|
NotificationResponse notificationResponse) async {
|
||||||
|
final String? payload = notificationResponse.payload;
|
||||||
|
if (payload != null && payload.isNotEmpty) {
|
||||||
|
await OpenFilex.open(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requestNotificationPermission() async {
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
|
||||||
|
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||||
|
AndroidFlutterLocalNotificationsPlugin>();
|
||||||
|
|
||||||
|
if (androidImplementation != null) {
|
||||||
|
await androidImplementation.requestNotificationsPermission();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadTransactions() async {
|
Future<void> _loadTransactions() async {
|
||||||
@@ -42,7 +93,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
try {
|
try {
|
||||||
final repo = getIt<TransactionRepository>();
|
final repo = getIt<TransactionRepository>();
|
||||||
final txs = await repo.fetchTransactions(
|
final txs = await repo.fetchTransactions(
|
||||||
widget.accountNo,
|
selectedUser.accountNo ?? '',
|
||||||
fromDate: fromDate,
|
fromDate: fromDate,
|
||||||
toDate: toDate,
|
toDate: toDate,
|
||||||
);
|
);
|
||||||
@@ -87,12 +138,15 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final maxToDate = fromDate!.add(const Duration(days: 31)).isBefore(now)
|
final maxToDate = fromDate!.add(const Duration(days: 183)).isBefore(now)
|
||||||
? fromDate!.add(const Duration(days: 31))
|
? fromDate!.add(const Duration(days: 183))
|
||||||
: now;
|
: now;
|
||||||
|
final initialToDate = toDate ?? now;
|
||||||
|
final clampedInitialToDate =
|
||||||
|
initialToDate.isBefore(fromDate!) ? fromDate! : initialToDate;
|
||||||
final picked = await showDatePicker(
|
final picked = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: toDate ?? fromDate!,
|
initialDate: clampedInitialToDate,
|
||||||
firstDate: fromDate!,
|
firstDate: fromDate!,
|
||||||
lastDate: maxToDate,
|
lastDate: maxToDate,
|
||||||
);
|
);
|
||||||
@@ -123,25 +177,59 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${AppLocalizations.of(context).accountNumber}: ",
|
AppLocalizations.of(context).accountNumber,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 17,
|
fontWeight: FontWeight.w500, fontSize: 17),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
|
const VerticalDivider(
|
||||||
|
width: 20,
|
||||||
|
thickness: 1,
|
||||||
|
indent: 5,
|
||||||
|
endIndent: 5,
|
||||||
|
color: Colors.grey),
|
||||||
|
DropdownButton<User>(
|
||||||
|
value: selectedUser,
|
||||||
|
onChanged: (User? newUser) {
|
||||||
|
if (newUser != null) {
|
||||||
|
setState(() {
|
||||||
|
selectedUser = newUser;
|
||||||
|
});
|
||||||
|
_loadTransactions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: widget.users.map((user) {
|
||||||
|
return DropdownMenuItem<User>(
|
||||||
|
value: user,
|
||||||
|
child: Text(user.accountNo.toString()),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
underline: Container(), // Remove the underline
|
||||||
),
|
),
|
||||||
Text(widget.accountNo, style: const TextStyle(fontSize: 17)),
|
Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
),
|
||||||
Row(
|
),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 10),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${AppLocalizations.of(context).availableBalance}: ",
|
"${AppLocalizations.of(context).availableBalance}: ",
|
||||||
@@ -149,11 +237,12 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(' ₹ ${widget.balance}',
|
Text(' ₹ ${selectedUser.availableBalance}',
|
||||||
style: const TextStyle(fontSize: 17)),
|
style: const TextStyle(fontSize: 17)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).filters,
|
AppLocalizations.of(context).filters,
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
@@ -223,8 +312,11 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
itemBuilder: (_, __) => ListTile(
|
itemBuilder: (_, __) => ListTile(
|
||||||
leading: Shimmer.fromColors(
|
leading: Shimmer.fromColors(
|
||||||
baseColor: Colors.grey[300]!,
|
baseColor:
|
||||||
highlightColor: Colors.grey[100]!,
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
highlightColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
child: CircleAvatar(
|
child: CircleAvatar(
|
||||||
radius: 12,
|
radius: 12,
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@@ -232,21 +324,29 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Shimmer.fromColors(
|
title: Shimmer.fromColors(
|
||||||
baseColor: Colors.grey[300]!,
|
baseColor:
|
||||||
highlightColor: Colors.grey[100]!,
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
highlightColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 10,
|
height: 10,
|
||||||
width: 100,
|
width: 100,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Shimmer.fromColors(
|
subtitle: Shimmer.fromColors(
|
||||||
baseColor: Colors.grey[300]!,
|
baseColor:
|
||||||
highlightColor: Colors.grey[100]!,
|
Theme.of(context).colorScheme.surfaceVariant,
|
||||||
|
highlightColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurfaceVariant,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 8,
|
height: 8,
|
||||||
width: 60,
|
width: 60,
|
||||||
color: Theme.of(context).scaffoldBackgroundColor,
|
color:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -257,20 +357,24 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
AppLocalizations.of(context).noTransactions,
|
AppLocalizations.of(context).noTransactions,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color:
|
||||||
|
Theme.of(context).colorScheme.onSurface,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
: ListView.separated(
|
: ListView.builder(
|
||||||
itemCount: _transactions.length,
|
itemCount: _transactions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final tx = _transactions[index];
|
final tx = _transactions[index];
|
||||||
return ListTile(
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 0, vertical: 4),
|
||||||
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
tx.type == 'CR'
|
tx.type == 'CR'
|
||||||
? Symbols.call_received
|
? Symbols.call_received
|
||||||
: Symbols.call_made,
|
: Symbols.call_made,
|
||||||
color: tx.type == 'CR'
|
color: tx.type == 'CR'
|
||||||
? Colors.green
|
? const Color(0xFF10BB10)
|
||||||
: Theme.of(context).colorScheme.error,
|
: Theme.of(context).colorScheme.error,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -279,38 +383,670 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
tx.name != null
|
tx.name != null
|
||||||
? (tx.name!.length > 18
|
? (tx.name!.length > 22
|
||||||
? tx.name!.substring(0, 22)
|
? tx.name!.substring(0, 22)
|
||||||
: tx.name!)
|
: tx.name!)
|
||||||
: '',
|
: '',
|
||||||
style: const TextStyle(fontSize: 12),
|
style: const TextStyle(fontSize: 12),
|
||||||
),
|
),
|
||||||
trailing: Text(
|
trailing: Column(
|
||||||
|
crossAxisAlignment:
|
||||||
|
CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
"₹${tx.amount}",
|
"₹${tx.amount}",
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
"Bal: ₹${tx.balance}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize:
|
||||||
|
12), // Style matches tx.name
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => TransactionDetailsScreen(
|
builder: (_) =>
|
||||||
|
TransactionDetailsScreen(
|
||||||
transaction: tx),
|
transaction: tx),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (context, index) {
|
|
||||||
return const Divider();
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () {
|
||||||
|
_exportToPdf();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.download),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _exportToPdf() async {
|
||||||
|
if (_transactions.isEmpty) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('No transactions to export.')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
'Downloading PDF',
|
||||||
|
'Your account statement is being downloaded...',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'download_channel',
|
||||||
|
'Download Notifications',
|
||||||
|
channelDescription: 'Notifications for PDF downloads',
|
||||||
|
importance: Importance.low,
|
||||||
|
priority: Priority.low,
|
||||||
|
showProgress: true,
|
||||||
|
maxProgress: 0,
|
||||||
|
ongoing: true,
|
||||||
|
icon: 'notification_icon',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 1. LOAD ASSETS ---
|
||||||
|
final logoImage = pw.MemoryImage(
|
||||||
|
(await rootBundle.load('assets/images/logo.png')).buffer.asUint8List());
|
||||||
|
final timesFont = await rootBundle.load("assets/fonts/Rubik-Regular.ttf");
|
||||||
|
final timesBoldFont = await rootBundle.load("assets/fonts/Rubik-Bold.ttf");
|
||||||
|
final ttf = pw.Font.ttf(timesFont);
|
||||||
|
final ttfBold = pw.Font.ttf(timesBoldFont);
|
||||||
|
|
||||||
|
// --- 2. DEFINE COLORS ---
|
||||||
|
final primaryColor = PdfColor.fromHex("#1a5f3a");
|
||||||
|
final secondaryColor = PdfColor.fromHex("#2e7d32");
|
||||||
|
final debitColor = PdfColor.fromHex("#d32f2f");
|
||||||
|
final lightGreyColor = PdfColor.fromHex("#666");
|
||||||
|
final tableBorderColor = PdfColor.fromHex("#d0d0d0");
|
||||||
|
final lightBgColor = PdfColor.fromHex("#f9f9f9");
|
||||||
|
final warningBgColor = PdfColor.fromHex("#f8d7da");
|
||||||
|
final warningBorderColor = PdfColor.fromHex("#f5c6cb");
|
||||||
|
final warningTextColor = PdfColor.fromHex("#721c24");
|
||||||
|
|
||||||
|
// --- 3. CREATE PDF ---
|
||||||
|
final pdf = pw.Document(
|
||||||
|
theme: pw.ThemeData.withFont(base: ttf, bold: ttfBold),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 4. BUILD PAGES ---
|
||||||
|
pdf.addPage(
|
||||||
|
pw.MultiPage(
|
||||||
|
pageFormat: PdfPageFormat.a4.copyWith(
|
||||||
|
marginTop: 15 * PdfPageFormat.mm,
|
||||||
|
marginLeft: 10 * PdfPageFormat.mm,
|
||||||
|
marginRight: 10 * PdfPageFormat.mm,
|
||||||
|
marginBottom: 20 * PdfPageFormat.mm,
|
||||||
|
),
|
||||||
|
header: (context) =>
|
||||||
|
_buildHeader(logoImage, primaryColor, lightGreyColor),
|
||||||
|
footer: (context) {
|
||||||
|
return pw.Center(
|
||||||
|
child: pw.Text(
|
||||||
|
'** This is only for information purpose and not for legal use **',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
color: lightGreyColor,
|
||||||
|
fontStyle: pw.FontStyle.italic)));
|
||||||
|
},
|
||||||
|
build: (context) => [
|
||||||
|
_buildAccountDetails(
|
||||||
|
customerName: selectedUser.name ?? '',
|
||||||
|
branchCode: selectedUser.branchId ?? '',
|
||||||
|
accountNo: selectedUser.accountNo ?? '',
|
||||||
|
cifNumber: selectedUser.cifNumber ?? '',
|
||||||
|
address: selectedUser.address ?? '',
|
||||||
|
lightGreyColor: lightGreyColor,
|
||||||
|
tableBorderColor: tableBorderColor,
|
||||||
|
lightBgColor: lightBgColor,
|
||||||
|
),
|
||||||
|
_buildWarning(
|
||||||
|
warningBgColor, warningBorderColor, debitColor, warningTextColor),
|
||||||
|
_buildPeriodHeader(
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
fromDate: fromDate,
|
||||||
|
toDate: toDate,
|
||||||
|
),
|
||||||
|
_buildTransactionsTable(
|
||||||
|
transactions: _transactions,
|
||||||
|
primaryColor: primaryColor,
|
||||||
|
secondaryColor: secondaryColor,
|
||||||
|
debitColor: debitColor,
|
||||||
|
tableBorderColor: tableBorderColor,
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
pw.Text('END OF STATEMENT', style: const pw.TextStyle(fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
pdf.addPage(
|
||||||
|
pw.Page(
|
||||||
|
pageFormat: PdfPageFormat.a4.copyWith(
|
||||||
|
marginTop: 15 * PdfPageFormat.mm,
|
||||||
|
marginLeft: 10 * PdfPageFormat.mm,
|
||||||
|
marginRight: 10 * PdfPageFormat.mm,
|
||||||
|
marginBottom: 20 * PdfPageFormat.mm,
|
||||||
|
),
|
||||||
|
build: (context) => _buildLastPage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- 5. SAVE AND NOTIFY ---
|
||||||
|
try {
|
||||||
|
final Uint8List pdfBytes = await pdf.save();
|
||||||
|
final String timestamp =
|
||||||
|
DateFormat("ddMMyyyy_HHmm").format(DateTime.now());
|
||||||
|
final String fileName =
|
||||||
|
'Statement_${selectedUser.accountNo}_$timestamp.pdf';
|
||||||
|
|
||||||
|
String? filePath;
|
||||||
|
|
||||||
|
if (Platform.isAndroid) {
|
||||||
|
final directory = Directory('/storage/emulated/0/Download');
|
||||||
|
if (!await directory.exists()) {
|
||||||
|
await directory.create(recursive: true);
|
||||||
|
}
|
||||||
|
final file = File('${directory.path}/$fileName');
|
||||||
|
await file.writeAsBytes(pdfBytes);
|
||||||
|
filePath = file.path;
|
||||||
|
} else {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final file = await File('${tempDir.path}/$fileName').create();
|
||||||
|
await file.writeAsBytes(pdfBytes);
|
||||||
|
filePath = file.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
'PDF Download Complete',
|
||||||
|
'Your account statement has been saved.',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'download_channel',
|
||||||
|
'Download Notifications',
|
||||||
|
channelDescription: 'Notifications for PDF downloads',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
showProgress: false,
|
||||||
|
ongoing: false,
|
||||||
|
autoCancel: true,
|
||||||
|
icon: 'notification_icon',
|
||||||
|
actions: [
|
||||||
|
AndroidNotificationAction('open_pdf', 'Open PDF',
|
||||||
|
showsUserInterface: true)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
payload: filePath,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('PDF saved to: $filePath'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
await flutterLocalNotificationsPlugin.show(
|
||||||
|
0,
|
||||||
|
'PDF Download Failed',
|
||||||
|
'Error saving PDF: $e',
|
||||||
|
const NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'download_channel', 'Download Notifications',
|
||||||
|
channelDescription: 'Notifications for PDF downloads',
|
||||||
|
importance: Importance.high,
|
||||||
|
priority: Priority.high,
|
||||||
|
showProgress: false,
|
||||||
|
ongoing: false,
|
||||||
|
icon: 'notification_icon'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Error saving PDF: $e'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildHeader(pw.MemoryImage logoImage, PdfColor primaryColor,
|
||||||
|
PdfColor lightGreyColor) {
|
||||||
|
return pw.Container(
|
||||||
|
margin: const pw.EdgeInsets.only(bottom: 15),
|
||||||
|
padding: const pw.EdgeInsets.only(bottom: 12),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border(bottom: pw.BorderSide(color: primaryColor, width: 2)),
|
||||||
|
),
|
||||||
|
child: pw.Row(
|
||||||
|
children: [
|
||||||
|
pw.Image(logoImage, height: 55, width: 55),
|
||||||
|
pw.SizedBox(width: 12),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: primaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
letterSpacing: 0.3),
|
||||||
|
),
|
||||||
|
pw.Text(
|
||||||
|
'Head Office: Dharmsala, District Kangra (H.P.), Pin. 176215',
|
||||||
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'e-Statement Service',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: primaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 2),
|
||||||
|
pw.Text(
|
||||||
|
'Generated: ${DateFormat("dd/MM/yyyy HH:mm").format(DateTime.now())}',
|
||||||
|
style: pw.TextStyle(fontSize: 9, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildAccountDetails({
|
||||||
|
required String customerName,
|
||||||
|
required String branchCode,
|
||||||
|
required String accountNo,
|
||||||
|
required String cifNumber,
|
||||||
|
required String address,
|
||||||
|
required PdfColor lightGreyColor,
|
||||||
|
required PdfColor tableBorderColor,
|
||||||
|
required PdfColor lightBgColor,
|
||||||
|
}) {
|
||||||
|
const cellPadding = pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8);
|
||||||
|
final border = pw.BorderSide(color: tableBorderColor, width: 1);
|
||||||
|
|
||||||
|
return pw.Table(
|
||||||
|
border: pw.TableBorder(
|
||||||
|
top: border,
|
||||||
|
bottom: border,
|
||||||
|
left: border,
|
||||||
|
right: border,
|
||||||
|
horizontalInside: border,
|
||||||
|
verticalInside: border,
|
||||||
|
),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(1),
|
||||||
|
1: const pw.FlexColumnWidth(1),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
children: [
|
||||||
|
_buildDetailCell(
|
||||||
|
'Customer Name', customerName, cellPadding, lightGreyColor),
|
||||||
|
_buildDetailCell(
|
||||||
|
'CIF Number', cifNumber, cellPadding, lightGreyColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.TableRow(
|
||||||
|
children: [
|
||||||
|
_buildDetailCell(
|
||||||
|
'Account Number', accountNo, cellPadding, lightGreyColor),
|
||||||
|
_buildDetailCell(
|
||||||
|
'Branch Code', branchCode, cellPadding, lightGreyColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.TableRow(
|
||||||
|
children: [
|
||||||
|
pw.Container(
|
||||||
|
padding: cellPadding,
|
||||||
|
// Using a Column inside a single cell to potentially wrap long address
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'Customer Address',
|
||||||
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 2),
|
||||||
|
pw.Text(
|
||||||
|
address,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11, fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Empty container for the second column in this row, as it's a single spanning column
|
||||||
|
pw.Container(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildDetailCell(String label, String value, pw.EdgeInsets padding,
|
||||||
|
PdfColor lightGreyColor,
|
||||||
|
{bool vertical = false}) {
|
||||||
|
final children = [
|
||||||
|
pw.Text(
|
||||||
|
label,
|
||||||
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
||||||
|
),
|
||||||
|
if (vertical) pw.SizedBox(height: 2),
|
||||||
|
pw.Text(
|
||||||
|
value,
|
||||||
|
style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
return pw.Padding(
|
||||||
|
padding: padding,
|
||||||
|
child: vertical
|
||||||
|
? pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.start,
|
||||||
|
children: children)
|
||||||
|
: pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: children),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildWarning(PdfColor warningBgColor, PdfColor warningBorderColor,
|
||||||
|
PdfColor debitColor, PdfColor warningTextColor) {
|
||||||
|
return pw.Container(
|
||||||
|
margin: const pw.EdgeInsets.symmetric(vertical: 15),
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
color: warningBgColor,
|
||||||
|
border: pw.Border.all(color: warningBorderColor, width: 1),
|
||||||
|
borderRadius: pw.BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: pw.Row(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.SizedBox(width: 10),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.RichText(
|
||||||
|
text: pw.TextSpan(
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 10, color: warningTextColor, lineSpacing: 1.5),
|
||||||
|
children: [
|
||||||
|
pw.TextSpan(
|
||||||
|
text: 'NEVER SHARE ',
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
||||||
|
const pw.TextSpan(
|
||||||
|
text:
|
||||||
|
'your Card number, CVV, PIN, OTP, Internet Banking User ID, Password or URB with anyone even if the caller claims to be a bank employee. Sharing these details can lead to unauthorized access to your account.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildPeriodHeader(
|
||||||
|
{required PdfColor primaryColor, DateTime? fromDate, DateTime? toDate}) {
|
||||||
|
String from = fromDate != null
|
||||||
|
? DateFormat('dd/MM/yyyy').format(fromDate)
|
||||||
|
: 'the beginning';
|
||||||
|
String to =
|
||||||
|
toDate != null ? DateFormat('dd/MM/yyyy').format(toDate) : 'today';
|
||||||
|
|
||||||
|
return pw.Container(
|
||||||
|
margin: const pw.EdgeInsets.only(bottom: 12),
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border.symmetric(
|
||||||
|
horizontal: pw.BorderSide(color: primaryColor, width: 2)),
|
||||||
|
color: PdfColor.fromHex("#f5f5f5"),
|
||||||
|
),
|
||||||
|
alignment: pw.Alignment.center,
|
||||||
|
child: pw.RichText(
|
||||||
|
text: pw.TextSpan(
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: primaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold),
|
||||||
|
children: [
|
||||||
|
const pw.TextSpan(text: 'Account statement from '),
|
||||||
|
pw.TextSpan(
|
||||||
|
text: from,
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
||||||
|
const pw.TextSpan(text: ' to '),
|
||||||
|
pw.TextSpan(
|
||||||
|
text: to, style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildTransactionsTable({
|
||||||
|
required List<Transaction> transactions,
|
||||||
|
required PdfColor primaryColor,
|
||||||
|
required PdfColor secondaryColor,
|
||||||
|
required PdfColor debitColor,
|
||||||
|
required PdfColor tableBorderColor,
|
||||||
|
}) {
|
||||||
|
//final border = pw.BorderSide(color: tableBorderColor, width: 1);
|
||||||
|
|
||||||
|
return pw.Table(
|
||||||
|
border: pw.TableBorder.all(color: tableBorderColor, width: 1),
|
||||||
|
columnWidths: {
|
||||||
|
0: const pw.FlexColumnWidth(1.5),
|
||||||
|
1: const pw.FlexColumnWidth(4),
|
||||||
|
2: const pw.FlexColumnWidth(2.2),
|
||||||
|
3: const pw.FlexColumnWidth(2.2),
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
pw.TableRow(
|
||||||
|
decoration: pw.BoxDecoration(color: secondaryColor),
|
||||||
|
children: [
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Date',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Mode / Particulars',
|
||||||
|
textAlign: pw.TextAlign.left,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Withdrawals / Deposits',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(10),
|
||||||
|
child: pw.Text('Balance',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
...transactions.map((tx) {
|
||||||
|
final amount = double.tryParse(tx.amount ?? '0') ?? 0;
|
||||||
|
final isDebit = tx.type == 'DR';
|
||||||
|
return pw.TableRow(
|
||||||
|
decoration: const pw.BoxDecoration(color: PdfColors.white),
|
||||||
|
children: [
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(tx.date ?? '',
|
||||||
|
textAlign: pw.TextAlign.center,
|
||||||
|
style: const pw.TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(tx.name ?? '',
|
||||||
|
textAlign: pw.TextAlign.left,
|
||||||
|
style: const pw.TextStyle(fontSize: 11)),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(
|
||||||
|
'${NumberFormat.currency(locale: 'en_IN', symbol: '₹').format(amount)} ${tx.type}',
|
||||||
|
textAlign: pw.TextAlign.right,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: isDebit ? debitColor : secondaryColor,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
child: pw.Text(
|
||||||
|
'${NumberFormat.currency(locale: 'en_IN', symbol: '₹').format(double.tryParse(tx.balance ?? '0') ?? 0)} ${tx.balanceType ?? ''}',
|
||||||
|
textAlign: pw.TextAlign.right,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11, fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pw.Widget _buildLastPage() {
|
||||||
|
return pw.Container(
|
||||||
|
padding: const pw.EdgeInsets.all(20),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border.all(color: PdfColors.black, width: 2),
|
||||||
|
color: PdfColor.fromHex("#fafafa"),
|
||||||
|
borderRadius: pw.BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'IMPORTANT INFORMATION:',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 15),
|
||||||
|
..._buildInfoPoints(PdfColors.black, [
|
||||||
|
'The Kangra Central Cooperative Bank Officials or representatives will NEVER ask you for your personal information i.e. your card details, passwords, PIN, CVV, OTP etc. Do not share such details with anyone over phone, SMS or email.',
|
||||||
|
'Always stay vigilant to suspicious emails. Do not open any suspicious emails.',
|
||||||
|
'Always stay vigilant when giving out sensitive personal or account information.',
|
||||||
|
'Beware of messages that instill a sense of urgency (e.g., account will expire unless you "verify" your information). Contact the Bank directly if unsure.',
|
||||||
|
'Always log out of secondary devices and reset your passwords frequently.',
|
||||||
|
'Use strong passwords: Create strong passwords that are difficult for hackers to guess.',
|
||||||
|
'Use public Wi-Fi with caution: Be careful when using public Wi-Fi networks.',
|
||||||
|
'Back up your data regularly to a secure, encrypted, off-site location.',
|
||||||
|
'Follow corporate security policies: Adhere to your company\'s security guidelines.',
|
||||||
|
'Assess third-party app permissions carefully before granting access.',
|
||||||
|
'Lock your computer and mobile phone when not in use.',
|
||||||
|
'Don\'t leave devices unattended. Keep all mobile devices, such as laptops and cell phones, physically secured.',
|
||||||
|
'Don\'t leave Bluetooth / Wireless turned on when not in use. Enable them only when needed and in a safe environment.',
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<pw.Widget> _buildInfoPoints(PdfColor primaryColor, List<String> points) {
|
||||||
|
return points.map((point) {
|
||||||
|
return pw.Padding(
|
||||||
|
padding: const pw.EdgeInsets.only(bottom: 10),
|
||||||
|
child: pw.Row(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Container(
|
||||||
|
width: 15,
|
||||||
|
child: pw.Text('*',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
color: primaryColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: pw.FontWeight.bold))),
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Text(
|
||||||
|
point,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
lineSpacing: 1.6,
|
||||||
|
fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
Widget buildDateBox(String label, DateTime? date) {
|
Widget buildDateBox(String label, DateTime? date) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||||
|
|||||||
155
lib/features/accounts/screens/all_accounts_screen.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,9 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar:
|
appBar:
|
||||||
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
|
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -37,8 +39,12 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(
|
Icon(
|
||||||
isCredit ? Symbols.call_received : Symbols.call_made,
|
isCredit
|
||||||
color: isCredit ? Colors.green : Colors.red,
|
? Symbols.call_received
|
||||||
|
: Symbols.call_made,
|
||||||
|
color: isCredit
|
||||||
|
? const Color(0xFF10BB10)
|
||||||
|
: Theme.of(context).colorScheme.error,
|
||||||
size: 28,
|
size: 28,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -47,9 +53,9 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
// Date centered
|
// Date centered
|
||||||
Text(
|
Text(
|
||||||
transaction.date ?? "",
|
transaction.date ?? "",
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.grey,
|
color: Theme.of(context).textTheme.bodySmall?.color,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -57,28 +63,47 @@ class TransactionDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(),
|
Divider(color: Theme.of(context).dividerColor),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 5,
|
flex: 5,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
_buildDetailRow(AppLocalizations.of(context).transactionType,
|
_buildDetailRow(
|
||||||
|
AppLocalizations.of(context).transactionType,
|
||||||
transaction.type ?? ""),
|
transaction.type ?? ""),
|
||||||
_buildDetailRow(AppLocalizations.of(context).transferType,
|
_buildDetailRow(AppLocalizations.of(context).transferType,
|
||||||
transaction.name.split("/").first ?? ""),
|
transaction.name.split("/").first ?? ""),
|
||||||
if (transaction.name.length > 12) ...[
|
// if (transaction.name.length > 12) ...[
|
||||||
_buildDetailRow(AppLocalizations.of(context).utrNo,
|
// _buildDetailRow(AppLocalizations.of(context).utrNo,
|
||||||
transaction.name.split("= ")[1].split(" ")[0] ?? ""),
|
// transaction.name.split("= ")[1].split(" ")[0] ?? ""),
|
||||||
_buildDetailRow(
|
// _buildDetailRow(
|
||||||
AppLocalizations.of(context).beneficiaryAccountNo,
|
// AppLocalizations.of(context).beneficiaryAccountNo,
|
||||||
transaction.name.split("A/C ").last ?? "")
|
// transaction.name.split("A/C ").last ?? "")
|
||||||
]
|
// ]
|
||||||
|
_buildDetailRow(AppLocalizations.of(context).details,
|
||||||
|
transaction.name),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ class ThemeState extends Equatable {
|
|||||||
List<Object?> get props => [themeType];
|
List<Object?> get props => [themeType];
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/config/theme_type.dart';
|
import 'package:kmobile/config/theme_type.dart';
|
||||||
import 'package:kmobile/config/themes.dart';
|
import 'package:kmobile/config/themes.dart';
|
||||||
|
|
||||||
class ThemeState extends Equatable {
|
class ThemeState extends Equatable {
|
||||||
final ThemeType themeType;
|
final ThemeType themeType;
|
||||||
const ThemeState({required this.themeType});
|
const ThemeState({required this.themeType});
|
||||||
@@ -35,7 +35,4 @@ class ThemeState extends Equatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [themeType];
|
List<Object?> get props => [themeType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +57,45 @@ class AuthToken extends Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static bool _decodeTncFromToken(String token) {
|
||||||
|
// try {
|
||||||
|
// final parts = token.split('.');
|
||||||
|
// if (parts.length != 3) {
|
||||||
|
// throw Exception('Invalid JWT format for TNC decoding');
|
||||||
|
// }
|
||||||
|
// final payload = parts[1];
|
||||||
|
// String normalized = base64Url.normalize(payload);
|
||||||
|
// final payloadMap = json.decode(utf8.decode(base64Url.decode(normalized)));
|
||||||
|
|
||||||
|
// if (payloadMap is! Map<String, dynamic> || !payloadMap.containsKey('tnc')) {
|
||||||
|
// // If 'tnc' is not present in the payload, default to false
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// final tncValue = payloadMap['tnc'];
|
||||||
|
|
||||||
|
// // Handle different representations of 'true'
|
||||||
|
// if (tncValue is bool) {
|
||||||
|
// return tncValue;
|
||||||
|
// }
|
||||||
|
// if (tncValue is String) {
|
||||||
|
// return tncValue.toLowerCase() == 'true';
|
||||||
|
// }
|
||||||
|
// if (tncValue is int) {
|
||||||
|
// return tncValue == 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Default to false for any other case
|
||||||
|
// return false;
|
||||||
|
// } catch (e) {
|
||||||
|
// log('Error decoding tnc from token: $e');
|
||||||
|
// // Default to false if decoding fails or 'tnc' is not found/invalid
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
bool get isExpired => DateTime.now().isAfter(expiresAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [accessToken, expiresAt];
|
List<Object> get props => [accessToken, expiresAt, tnc];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +23,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
final _customerNumberController = TextEditingController();
|
final _customerNumberController = TextEditingController();
|
||||||
final _passwordController = TextEditingController();
|
final _passwordController = TextEditingController();
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
//bool _showWelcome = true;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -31,55 +31,274 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
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 (!context.mounted) return;
|
barrierDismissible: false,
|
||||||
if (mpin == null) {
|
builder: (dialogContext) => TncDialog(
|
||||||
Navigator.of(context).pushReplacement(
|
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,7 +319,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Title
|
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).kccb,
|
AppLocalizations.of(context).kccb,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -110,21 +328,22 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _customerNumberController,
|
controller: _customerNumberController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).customerNumber,
|
labelText: AppLocalizations.of(context).customerNumber,
|
||||||
// prefixIcon: Icon(Icons.person),
|
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
@@ -137,7 +356,6 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Password
|
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
obscureText: _obscurePassword,
|
obscureText: _obscurePassword,
|
||||||
@@ -150,10 +368,13 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.outline),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.outline),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary, width: 2),
|
borderSide: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2),
|
||||||
),
|
),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -176,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(
|
||||||
@@ -187,51 +407,23 @@ class LoginScreenState extends State<LoginScreen>
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
foregroundColor: Theme.of(context).primaryColorDark,
|
foregroundColor: Theme.of(context).primaryColorDark,
|
||||||
side: BorderSide(color: Theme.of(context).colorScheme.outline, width: 1),
|
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(color: Theme.of(context).colorScheme.onPrimaryContainer),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
|
||||||
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
||||||
child: Text(AppLocalizations.of(context).or),
|
|
||||||
),
|
|
||||||
const Expanded(child: Divider()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 25),
|
const SizedBox(height: 25),
|
||||||
|
|
||||||
// Register Button
|
|
||||||
SizedBox(
|
|
||||||
width: 250,
|
|
||||||
child: ElevatedButton(
|
|
||||||
//disable until registration is implemented
|
|
||||||
onPressed: null,
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
shape: const StadiumBorder(),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
||||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
child: Text(AppLocalizations.of(context).register,
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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';
|
||||||
@@ -16,12 +15,18 @@ 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
|
||||||
@@ -78,7 +83,7 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
|
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.mode == MPinMode.enter) {
|
if (widget.mode == MPinMode.enter && !widget.disableBiometric) {
|
||||||
_tryBiometricBeforePin();
|
_tryBiometricBeforePin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,29 +178,36 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
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(() {
|
||||||
@@ -339,6 +351,9 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String getTitle() {
|
String getTitle() {
|
||||||
|
if (widget.customTitle != null) {
|
||||||
|
return widget.customTitle!;
|
||||||
|
}
|
||||||
switch (widget.mode) {
|
switch (widget.mode) {
|
||||||
case MPinMode.enter:
|
case MPinMode.enter:
|
||||||
return AppLocalizations.of(context).enterMPIN;
|
return AppLocalizations.of(context).enterMPIN;
|
||||||
|
|||||||
296
lib/features/auth/screens/set_password_screen.dart
Normal 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'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
177
lib/features/auth/screens/sms_verification_helper.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -10,11 +12,29 @@ class SplashScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SplashScreenState extends State<SplashScreen> {
|
class _SplashScreenState extends State<SplashScreen> {
|
||||||
|
String _version = '';
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadVersion() async {
|
||||||
|
final PackageInfo info = await PackageInfo.fromPlatform();
|
||||||
|
if (mounted) {
|
||||||
|
// Check if the widget is still in the tree
|
||||||
|
setState(() {
|
||||||
|
_version = 'Version ${info.version} (${info.buildNumber})';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
fit: StackFit.expand,
|
||||||
|
children: <Widget>[
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/kconnect2.webp',
|
'assets/images/kconnect2.webp',
|
||||||
@@ -26,7 +46,7 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).kconnect,
|
AppLocalizations.of(context).kccbMobile,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 36,
|
fontSize: 36,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -51,8 +71,20 @@ class _SplashScreenState extends State<SplashScreen> {
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: Color(0xFFFFFFFF)),
|
||||||
color: Color(0xFFFFFFFF)),
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 90,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Text(
|
||||||
|
_version,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFFFFFFFF),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
58
lib/features/auth/screens/tnc_required_screen.dart
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class TncRequiredScreen extends StatelessWidget {
|
||||||
|
// Renamed class
|
||||||
|
const TncRequiredScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
static const routeName = '/tnc-required';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Terms and Conditions'),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'You must accept the Terms and Conditions to use the application.',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// This will take the user back to the previous screen
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Go Back'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
165
lib/features/auth/screens/verification_screen.dart
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/api/services/auth_service.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
|
||||||
|
|
||||||
|
class VerificationScreen extends StatefulWidget {
|
||||||
|
final String customerNo;
|
||||||
|
final String password;
|
||||||
|
|
||||||
|
const VerificationScreen({
|
||||||
|
super.key,
|
||||||
|
required this.customerNo,
|
||||||
|
required this.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<VerificationScreen> createState() => _VerificationScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VerificationScreenState extends State<VerificationScreen> {
|
||||||
|
String _statusMessage = "Starting verification...";
|
||||||
|
Timer? _timer;
|
||||||
|
int _countdown = 120;
|
||||||
|
bool _isVerifying = false;
|
||||||
|
bool _verificationFailed = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_startVerificationProcess();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startTimer() {
|
||||||
|
_timer?.cancel(); // Cancel any existing timer
|
||||||
|
_countdown = 120;
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (_countdown > 0) {
|
||||||
|
setState(() {
|
||||||
|
_countdown--;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_timer?.cancel();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "Verification timed out. Please try again.";
|
||||||
|
_isVerifying = false;
|
||||||
|
_verificationFailed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _startVerificationProcess() async {
|
||||||
|
setState(() {
|
||||||
|
_isVerifying = true;
|
||||||
|
_verificationFailed = false;
|
||||||
|
_statusMessage = "Starting verification...";
|
||||||
|
});
|
||||||
|
_startTimer();
|
||||||
|
|
||||||
|
// 1. Send SMS
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "SMS sending...";
|
||||||
|
});
|
||||||
|
final smsHelper = SmsVerificationHelper();
|
||||||
|
final uuid = await smsHelper.initiateSmsSequence(context: context);
|
||||||
|
|
||||||
|
if (uuid != null && mounted) {
|
||||||
|
// SMS sending was successful, now wait before verifying.
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "SMS sent. Waiting for network delivery...";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adding a 10-second delay to account for SMS network latency.
|
||||||
|
await Future.delayed(const Duration(seconds: 10));
|
||||||
|
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
// 2. Verify SIM
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "Verifying with server...";
|
||||||
|
});
|
||||||
|
|
||||||
|
final authService = getIt<AuthService>();
|
||||||
|
try {
|
||||||
|
await authService.simVerify(uuid, widget.customerNo);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = "Verification successful!";
|
||||||
|
_isVerifying = false;
|
||||||
|
});
|
||||||
|
_timer?.cancel();
|
||||||
|
|
||||||
|
// Pop with success result
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_statusMessage = e.toString();
|
||||||
|
_isVerifying = false;
|
||||||
|
_verificationFailed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_statusMessage =
|
||||||
|
"SMS sending failed. Please check permissions and try again.";
|
||||||
|
_isVerifying = false;
|
||||||
|
_verificationFailed = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Device Verification"),
|
||||||
|
automaticallyImplyLeading: !_isVerifying,
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
if (_isVerifying) const CircularProgressIndicator(),
|
||||||
|
if (!_isVerifying && _verificationFailed)
|
||||||
|
const Icon(Icons.error_outline, color: Colors.red, size: 50),
|
||||||
|
if (!_isVerifying && !_verificationFailed)
|
||||||
|
const Icon(Icons.check_circle_outline,
|
||||||
|
color: Colors.green, size: 50),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
_statusMessage,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 18),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (_isVerifying)
|
||||||
|
Text(
|
||||||
|
"Time remaining: $_countdown seconds",
|
||||||
|
style: const TextStyle(fontSize: 16, color: Colors.grey),
|
||||||
|
),
|
||||||
|
if (_verificationFailed && !_isVerifying) ...[
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _startVerificationProcess,
|
||||||
|
child: const Text('Retry'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ class AddBeneficiaryScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _accountNumberFieldKey = GlobalKey<FormFieldState>();
|
||||||
|
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
|
||||||
|
final _ifscFieldKey = GlobalKey<FormFieldState>();
|
||||||
final TextEditingController accountNumberController = TextEditingController();
|
final TextEditingController accountNumberController = TextEditingController();
|
||||||
final TextEditingController confirmAccountNumberController =
|
final TextEditingController confirmAccountNumberController =
|
||||||
TextEditingController();
|
TextEditingController();
|
||||||
@@ -32,6 +34,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
final TextEditingController branchNameController = TextEditingController();
|
final TextEditingController branchNameController = TextEditingController();
|
||||||
final TextEditingController ifscController = TextEditingController();
|
final TextEditingController ifscController = TextEditingController();
|
||||||
final TextEditingController phoneController = TextEditingController();
|
final TextEditingController phoneController = TextEditingController();
|
||||||
|
final _ifscFocusNode = FocusNode();
|
||||||
final service = getIt<BeneficiaryService>();
|
final service = getIt<BeneficiaryService>();
|
||||||
|
|
||||||
bool _isValidating = false;
|
bool _isValidating = false;
|
||||||
@@ -43,6 +46,11 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_ifscFocusNode.addListener(() {
|
||||||
|
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
|
||||||
|
_validateIFSC();
|
||||||
|
}
|
||||||
|
});
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
accountType = 'Savings';
|
accountType = 'Savings';
|
||||||
@@ -50,15 +58,29 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
accountNumberController.dispose();
|
||||||
|
confirmAccountNumberController.dispose();
|
||||||
|
nameController.dispose();
|
||||||
|
bankNameController.dispose();
|
||||||
|
branchNameController.dispose();
|
||||||
|
ifscController.dispose();
|
||||||
|
phoneController.dispose();
|
||||||
|
_ifscFocusNode.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _validateIFSC() async {
|
void _validateIFSC() async {
|
||||||
var beneficiaryService = getIt<BeneficiaryService>();
|
var beneficiaryService = getIt<BeneficiaryService>();
|
||||||
final ifsc = ifscController.text.trim().toUpperCase();
|
final ifsc = ifscController.text.trim().toUpperCase();
|
||||||
if (ifsc.isEmpty) return;
|
if (ifsc.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
final result = await beneficiaryService.validateIFSC(ifsc);
|
final result = await beneficiaryService.validateIFSC(ifsc);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
if (result.bankName == '') {
|
if (result.bankName.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
|
SnackBar(content: Text(AppLocalizations.of(context).invalidIfsc)),
|
||||||
);
|
);
|
||||||
@@ -69,6 +91,23 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
branchNameController.text = result.branchName;
|
branchNameController.text = result.branchName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
final errorMessage = e.toString().toUpperCase();
|
||||||
|
String snackbarMessage =
|
||||||
|
AppLocalizations.of(context).somethingWentWrong;
|
||||||
|
|
||||||
|
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
|
||||||
|
snackbarMessage = AppLocalizations.of(context).invalidIfsc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(snackbarMessage)),
|
||||||
|
);
|
||||||
|
bankNameController.clear();
|
||||||
|
branchNameController.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _validateBeneficiary() async {
|
void _validateBeneficiary() async {
|
||||||
@@ -225,7 +264,9 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Form(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -237,6 +278,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
key: _accountNumberFieldKey,
|
||||||
controller: accountNumberController,
|
controller: accountNumberController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(
|
labelText: AppLocalizations.of(
|
||||||
@@ -267,6 +309,7 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// Confirm Account Number
|
// Confirm Account Number
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
key: _confirmAccountNumberFieldKey,
|
||||||
controller: confirmAccountNumberController,
|
controller: confirmAccountNumberController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(
|
labelText: AppLocalizations.of(
|
||||||
@@ -294,31 +337,34 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
|
focusNode: _ifscFocusNode,
|
||||||
|
key: _ifscFieldKey,
|
||||||
controller: ifscController,
|
controller: ifscController,
|
||||||
maxLength: 11,
|
maxLength: 11,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
LengthLimitingTextInputFormatter(11),
|
LengthLimitingTextInputFormatter(11),
|
||||||
],
|
],
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).ifscCode,
|
labelText:
|
||||||
|
AppLocalizations.of(context).ifscCode,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
textCapitalization: TextCapitalization.characters,
|
textCapitalization: TextCapitalization.characters,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onFieldSubmitted: (_) {
|
|
||||||
_validateIFSC();
|
|
||||||
},
|
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
final trimmed = value.trim().toUpperCase();
|
final trimmed = value.trim().toUpperCase();
|
||||||
if (trimmed.length < 11) {
|
if (trimmed.length < 11) {
|
||||||
// clear bank/branch if backspace or changed
|
// clear bank/branch if backspace or changed
|
||||||
bankNameController.clear();
|
bankNameController.clear();
|
||||||
branchNameController.clear();
|
branchNameController.clear();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
final pattern =
|
||||||
|
RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
|
||||||
if (value == null || value.trim().isEmpty) {
|
if (value == null || value.trim().isEmpty) {
|
||||||
return AppLocalizations.of(context).enterIfsc;
|
return AppLocalizations.of(context).enterIfsc;
|
||||||
} else if (!pattern.hasMatch(
|
} else if (!pattern.hasMatch(
|
||||||
@@ -332,12 +378,14 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
// 🔹 Bank Name (Disabled)
|
// Bank Name (Disabled)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: bankNameController,
|
controller: bankNameController,
|
||||||
enabled: false, // changed from readOnly to disabled
|
enabled:
|
||||||
|
false, // changed from readOnly to disabled
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).bankName,
|
labelText:
|
||||||
|
AppLocalizations.of(context).bankName,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
@@ -346,9 +394,11 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
// 🔹 Branch Name (Disabled)
|
// 🔹 Branch Name (Disabled)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: branchNameController,
|
controller: branchNameController,
|
||||||
enabled: false, // changed from readOnly to disabled
|
enabled:
|
||||||
|
false, // changed from readOnly to disabled
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).branchName,
|
labelText:
|
||||||
|
AppLocalizations.of(context).branchName,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
@@ -376,9 +426,10 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
validator: (value) => value == null ||
|
validator: (value) =>
|
||||||
value.isEmpty
|
value == null || value.isEmpty
|
||||||
? AppLocalizations.of(context).nameRequired
|
? AppLocalizations.of(context)
|
||||||
|
.nameRequired
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -390,18 +441,26 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isValidating
|
onPressed: _isValidating ||
|
||||||
|
ifscController.text.length != 11
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
if (confirmAccountNumberController
|
final isAccountValid =
|
||||||
.text ==
|
_accountNumberFieldKey
|
||||||
accountNumberController.text) {
|
.currentState!
|
||||||
|
.validate();
|
||||||
|
final isConfirmAccountValid =
|
||||||
|
_confirmAccountNumberFieldKey
|
||||||
|
.currentState!
|
||||||
|
.validate();
|
||||||
|
final isIfscValid = _ifscFieldKey
|
||||||
|
.currentState!
|
||||||
|
.validate();
|
||||||
|
|
||||||
|
if (isAccountValid &&
|
||||||
|
isConfirmAccountValid &&
|
||||||
|
isIfscValid) {
|
||||||
_validateBeneficiary();
|
_validateBeneficiary();
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
_validationError =
|
|
||||||
'Please enter a valid and matching account number.';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: _isValidating
|
child: _isValidating
|
||||||
@@ -421,7 +480,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
value: accountType,
|
value: accountType,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: AppLocalizations.of(context).accountType,
|
labelText:
|
||||||
|
AppLocalizations.of(context).accountType,
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
@@ -454,8 +514,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
validator: (value) =>
|
validator: (value) => value == null ||
|
||||||
value == null || value.length != 10
|
value.length != 10
|
||||||
? AppLocalizations.of(context).enterValidPhone
|
? AppLocalizations.of(context).enterValidPhone
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -476,8 +536,9 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
Theme.of(context).colorScheme.primaryContainer,
|
Theme.of(context).colorScheme.primaryContainer,
|
||||||
foregroundColor:
|
foregroundColor: Theme.of(context)
|
||||||
Theme.of(context).colorScheme.onPrimaryContainer),
|
.colorScheme
|
||||||
|
.onPrimaryContainer),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).validateAndAdd,
|
AppLocalizations.of(context).validateAndAdd,
|
||||||
style: const TextStyle(fontSize: 16),
|
style: const TextStyle(fontSize: 16),
|
||||||
@@ -488,6 +549,22 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
_showSuccessDialog(context);
|
_showSuccessDialog(context);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Failed to delete beneficiary: $e')),
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,11 +34,12 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Success'),
|
title: Text(AppLocalizations.of(context).success),
|
||||||
content: const Text('Beneficiary deleted successfully.'),
|
content:
|
||||||
|
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('OK'),
|
child: Text(AppLocalizations.of(context).ok),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||||
},
|
},
|
||||||
@@ -52,20 +55,19 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
title: const Text('Delete Beneficiary'),
|
title: Text(AppLocalizations.of(context).deleteBeneficiary),
|
||||||
content:
|
content: Text(AppLocalizations.of(context)
|
||||||
const Text('Are you sure you want to delete this beneficiary?'),
|
.areYouSureYouWantToDeleteThisBeneficiary),
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Cancel'),
|
child: Text(AppLocalizations.of(context).cancel),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('Delete'),
|
child: Text(AppLocalizations.of(context).delete),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//Navigator.of(context).pop();
|
|
||||||
_deleteBeneficiary(context);
|
_deleteBeneficiary(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -82,7 +84,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
title: Text(AppLocalizations.of(context).beneficiarydetails),
|
title: Text(AppLocalizations.of(context).beneficiarydetails),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -105,9 +109,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
|
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
|
||||||
beneficiary.bankName ?? 'N/A'),
|
beneficiary.bankName ?? 'N/A'),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
|
_buildDetailRow(
|
||||||
|
'${AppLocalizations.of(context).accountNumber} ',
|
||||||
beneficiary.accountNo),
|
beneficiary.accountNo),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).accountType} ',
|
_buildDetailRow(
|
||||||
|
'${AppLocalizations.of(context).accountType} ',
|
||||||
beneficiary.accountType),
|
beneficiary.accountType),
|
||||||
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
|
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
|
||||||
beneficiary.ifscCode),
|
beneficiary.ifscCode),
|
||||||
@@ -137,6 +143,22 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,10 @@ class _BeneficiaryResultPageState extends State<BeneficiaryResultPage> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
AppLocalizations.of(context).done,
|
AppLocalizations.of(context).done,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer), // slightly bigger text
|
style: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimaryContainer), // slightly bigger text
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,21 +21,47 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
var service = getIt<BeneficiaryService>();
|
var service = getIt<BeneficiaryService>();
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
List<Beneficiary> _beneficiaries = [];
|
List<Beneficiary> _beneficiaries = [];
|
||||||
|
List<Beneficiary> _filteredBeneficiaries = [];
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadBeneficiaries();
|
_loadBeneficiaries();
|
||||||
|
_searchController.addListener(() {
|
||||||
|
_filterBeneficiaries(_searchController.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBeneficiaries() async {
|
Future<void> _loadBeneficiaries() async {
|
||||||
final data = await service.fetchBeneficiaryList();
|
final data = await service.fetchBeneficiaryList();
|
||||||
setState(() {
|
setState(() {
|
||||||
_beneficiaries = data;
|
_beneficiaries = data;
|
||||||
|
_filteredBeneficiaries = data;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _filterBeneficiaries(String query) {
|
||||||
|
setState(() {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
_filteredBeneficiaries = _beneficiaries;
|
||||||
|
} else {
|
||||||
|
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
|
||||||
|
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildShimmerList() {
|
Widget _buildShimmerList() {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
@@ -63,15 +89,17 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBeneficiaryList() {
|
Widget _buildBeneficiaryList() {
|
||||||
if (_beneficiaries.isEmpty) {
|
if (_filteredBeneficiaries.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
|
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: _beneficiaries.length,
|
itemCount: _filteredBeneficiaries.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = _beneficiaries[index];
|
final item = _filteredBeneficiaries[index];
|
||||||
return ListTile(
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
child: ListTile(
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -97,6 +125,7 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -109,7 +138,45 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).beneficiaries),
|
title: Text(AppLocalizations.of(context).beneficiaries),
|
||||||
),
|
),
|
||||||
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search by name or account number",
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
floatingActionButton: Padding(
|
floatingActionButton: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
|
|||||||
@@ -56,12 +56,12 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).blockCard,
|
AppLocalizations.of(context).blockCard,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -100,18 +100,21 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black),
|
borderSide: BorderSide(color: Colors.black),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
borderSide:
|
||||||
|
BorderSide(color: Colors.black, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
validator: (value) => value != null && value.length == 3
|
validator: (value) =>
|
||||||
|
value != null && value.length == 3
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).cvv3Digits,
|
: AppLocalizations.of(context).cvv3Digits,
|
||||||
),
|
),
|
||||||
@@ -128,15 +131,18 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black),
|
borderSide: BorderSide(color: Colors.black),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
borderSide:
|
||||||
|
BorderSide(color: Colors.black, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: (value) => value != null && value.isNotEmpty
|
validator: (value) => value != null &&
|
||||||
|
value.isNotEmpty
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).selectExpiryDate,
|
: AppLocalizations.of(context).selectExpiryDate,
|
||||||
),
|
),
|
||||||
@@ -188,6 +194,22 @@ class _BlockCardScreen extends State<BlockCardScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ class CardDetailsScreen extends StatelessWidget {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("My Cards"),
|
title: const Text("My Cards"),
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: const [
|
children: const [
|
||||||
@@ -31,6 +33,8 @@ class CardDetailsScreen extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +91,7 @@ class CardTile extends StatelessWidget {
|
|||||||
"Kangra Central Co-operative Bank",
|
"Kangra Central Co-operative Bank",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 15.5,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// ignore_for_file: unused_import
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/features/card/screens/block_card_screen.dart';
|
import 'package:kmobile/features/card/screens/block_card_screen.dart';
|
||||||
import 'package:kmobile/features/card/screens/card_details_screen.dart';
|
import 'package:kmobile/features/card/screens/card_details_screen.dart';
|
||||||
@@ -23,14 +25,17 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.add,
|
icon: Symbols.add,
|
||||||
label: AppLocalizations.of(context).applyDebitCard,
|
label: AppLocalizations.of(context).applyDebitCard,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
|
disabled: true, // Add this
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.remove_moderator,
|
icon: Symbols.remove_moderator,
|
||||||
label: AppLocalizations.of(context).blockUnblockCard,
|
label: AppLocalizations.of(context).blockUnblockCard,
|
||||||
@@ -42,8 +47,9 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disabled: true,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.password_2,
|
icon: Symbols.password_2,
|
||||||
label: AppLocalizations.of(context).changeCardPin,
|
label: AppLocalizations.of(context).changeCardPin,
|
||||||
@@ -55,8 +61,9 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disabled: true,
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
CardManagementTile(
|
CardManagementTile(
|
||||||
icon: Symbols.payment_card,
|
icon: Symbols.payment_card,
|
||||||
label: AppLocalizations.of(context).viewCardDeatils,
|
label: AppLocalizations.of(context).viewCardDeatils,
|
||||||
@@ -68,8 +75,25 @@ class _CardManagementScreen extends State<CardManagementScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disabled: false,
|
||||||
|
),
|
||||||
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -80,21 +104,36 @@ class CardManagementTile extends StatelessWidget {
|
|||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool disabled;
|
||||||
|
|
||||||
const CardManagementTile({
|
const CardManagementTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.disabled = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(icon),
|
leading: Icon(
|
||||||
title: Text(label),
|
icon,
|
||||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
color: disabled ? theme.disabledColor : null,
|
||||||
onTap: onTap,
|
),
|
||||||
|
title: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: disabled ? theme.disabledColor : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
Symbols.arrow_right,
|
||||||
|
size: 20,
|
||||||
|
color: disabled ? theme.disabledColor : null,
|
||||||
|
),
|
||||||
|
onTap: disabled ? null : onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).cardDetails,
|
AppLocalizations.of(context).cardDetails,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -90,18 +90,21 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black),
|
borderSide: BorderSide(color: Colors.black),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
borderSide:
|
||||||
|
BorderSide(color: Colors.black, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
validator: (value) => value != null && value.length == 3
|
validator: (value) =>
|
||||||
|
value != null && value.length == 3
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).cvv3Digits,
|
: AppLocalizations.of(context).cvv3Digits,
|
||||||
),
|
),
|
||||||
@@ -118,15 +121,18 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).scaffoldBackgroundColor,
|
fillColor:
|
||||||
|
Theme.of(context).scaffoldBackgroundColor,
|
||||||
enabledBorder: const OutlineInputBorder(
|
enabledBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black),
|
borderSide: BorderSide(color: Colors.black),
|
||||||
),
|
),
|
||||||
focusedBorder: const OutlineInputBorder(
|
focusedBorder: const OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Colors.black, width: 2),
|
borderSide:
|
||||||
|
BorderSide(color: Colors.black, width: 2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
validator: (value) => value != null && value.isNotEmpty
|
validator: (value) => value != null &&
|
||||||
|
value.isNotEmpty
|
||||||
? null
|
? null
|
||||||
: AppLocalizations.of(context).selectExpiryDate,
|
: AppLocalizations.of(context).selectExpiryDate,
|
||||||
),
|
),
|
||||||
@@ -178,6 +184,22 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,12 +46,12 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).cardPin,
|
AppLocalizations.of(context).cardPin,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -133,6 +133,22 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).chequeManagement,
|
AppLocalizations.of(context).chequeManagement,
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
|
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: ListView(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 15),
|
||||||
ChequeManagementTile(
|
ChequeManagementTile(
|
||||||
@@ -30,42 +30,59 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
|
|||||||
label: AppLocalizations.of(context).requestChequeBook,
|
label: AppLocalizations.of(context).requestChequeBook,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
ChequeManagementTile(
|
ChequeManagementTile(
|
||||||
icon: Symbols.data_alert,
|
icon: Symbols.data_alert,
|
||||||
label: AppLocalizations.of(context).enquiry,
|
label: AppLocalizations.of(context).enquiry,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (context) => const EnquiryScreen()),
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const EnquiryScreen()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
ChequeManagementTile(
|
ChequeManagementTile(
|
||||||
icon: Symbols.approval_delegation,
|
icon: Symbols.approval_delegation,
|
||||||
label: AppLocalizations.of(context).chequeDeposit,
|
label: AppLocalizations.of(context).chequeDeposit,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
ChequeManagementTile(
|
ChequeManagementTile(
|
||||||
icon: Symbols.front_hand,
|
icon: Symbols.front_hand,
|
||||||
label: AppLocalizations.of(context).stopCheque,
|
label: AppLocalizations.of(context).stopCheque,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
ChequeManagementTile(
|
ChequeManagementTile(
|
||||||
icon: Symbols.cancel_presentation,
|
icon: Symbols.cancel_presentation,
|
||||||
label: AppLocalizations.of(context).revokeStop,
|
label: AppLocalizations.of(context).revokeStop,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
ChequeManagementTile(
|
ChequeManagementTile(
|
||||||
icon: Symbols.payments,
|
icon: Symbols.payments,
|
||||||
label: AppLocalizations.of(context).positivePay,
|
label: AppLocalizations.of(context).positivePay,
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
|
||||||
import 'package:kmobile/data/models/user.dart';
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class CustomerInfoScreen extends StatefulWidget {
|
class CustomerInfoScreen extends StatefulWidget {
|
||||||
@@ -13,6 +14,16 @@ class CustomerInfoScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
|
class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
|
||||||
late final User user = widget.user;
|
late final User user = widget.user;
|
||||||
|
int _selectedCard = 0; // 0 for Personal Info, 1 for KYC
|
||||||
|
|
||||||
|
String _maskPrimaryId(String? primaryId) {
|
||||||
|
if (primaryId == null || primaryId.length <= 4) {
|
||||||
|
return primaryId ?? 'N/A';
|
||||||
|
}
|
||||||
|
final lastFour = primaryId.substring(primaryId.length - 4);
|
||||||
|
return '*' * (primaryId.length - 4) + lastFour;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -24,45 +35,160 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
|
|||||||
.replaceFirst(RegExp('\n'), ''),
|
.replaceFirst(RegExp('\n'), ''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: SingleChildScrollView(
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: SafeArea(
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 30),
|
Card(
|
||||||
CircleAvatar(
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(
|
||||||
|
color: theme.colorScheme.outline.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
child: CircleAvatar(
|
||||||
radius: 50,
|
radius: 50,
|
||||||
child: SvgPicture.asset(
|
child: Icon(
|
||||||
'assets/images/avatar_male.svg',
|
Symbols.person,
|
||||||
width: 150,
|
size: 56,
|
||||||
height: 150,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 10.0),
|
|
||||||
child: Text(
|
|
||||||
user.name ?? '',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// Name + mobile
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${AppLocalizations.of(context).cif}: ${user.cifNumber ?? 'N/A'}',
|
// If you want to show the user's name instead, replace below.
|
||||||
style: TextStyle(
|
user.name ?? '',
|
||||||
fontSize: 16,
|
style:
|
||||||
color: theme.colorScheme.onSurfaceVariant),
|
theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
user.cifNumber ?? '',
|
||||||
|
style:
|
||||||
|
theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface
|
||||||
|
.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Toggle Buttons for Personal Info and KYC
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: CupertinoSlidingSegmentedControl<int>(
|
||||||
|
groupValue: _selectedCard,
|
||||||
|
thumbColor: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onPrimary, // Set selected switch color to theme primary color
|
||||||
|
onValueChanged: (int? newValue) {
|
||||||
|
if (newValue != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedCard = newValue;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
0: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20, vertical: 10),
|
||||||
|
child: Text(
|
||||||
|
AppLocalizations.of(context).personaldetails),
|
||||||
|
),
|
||||||
|
1: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20, vertical: 10),
|
||||||
|
child:
|
||||||
|
Text(AppLocalizations.of(context).kycdetails),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Card that shows content based on the toggle
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
side: BorderSide(
|
||||||
|
color: theme.colorScheme.outline.withOpacity(0.2),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
child: _selectedCard == 0
|
||||||
|
? _buildPersonalInfo(theme)
|
||||||
|
: _buildKycDetails(theme),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: 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 _buildPersonalInfo(ThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
key: const ValueKey('personal_info'),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).personaldetails,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
InfoField(
|
InfoField(
|
||||||
label: AppLocalizations.of(context).activeAccounts,
|
label: AppLocalizations.of(context).activeAccounts,
|
||||||
value: user.activeAccounts?.toString() ?? '6',
|
value: user.activeAccounts?.toString() ?? 'N/A',
|
||||||
),
|
),
|
||||||
InfoField(
|
InfoField(
|
||||||
label: AppLocalizations.of(context).mobileNumber,
|
label: AppLocalizations.of(context).mobileNumber,
|
||||||
@@ -70,29 +196,39 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
|
|||||||
),
|
),
|
||||||
InfoField(
|
InfoField(
|
||||||
label: AppLocalizations.of(context).dateOfBirth,
|
label: AppLocalizations.of(context).dateOfBirth,
|
||||||
value: (user.dateOfBirth != null &&
|
value: (user.dateOfBirth != null && user.dateOfBirth!.length == 8)
|
||||||
user.dateOfBirth!.length == 8)
|
|
||||||
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
|
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
|
||||||
: 'N/A',
|
: 'N/A',
|
||||||
), // Replace with DOB if available
|
),
|
||||||
InfoField(
|
InfoField(
|
||||||
label: AppLocalizations.of(context).branchCode,
|
label: AppLocalizations.of(context).branchCode,
|
||||||
value: user.branchId ?? 'N/A',
|
value: user.branchId ?? 'N/A',
|
||||||
),
|
),
|
||||||
InfoField(
|
InfoField(
|
||||||
label: AppLocalizations.of(context).branchAddress,
|
label: AppLocalizations.of(context).address,
|
||||||
value: user.address ?? 'N/A',
|
value: user.address ?? 'N/A',
|
||||||
), // Replace with Aadhar if available
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildKycDetails(ThemeData theme) {
|
||||||
|
return Column(
|
||||||
|
key: const ValueKey('kyc_details'),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).kycdetails,
|
||||||
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
InfoField(
|
InfoField(
|
||||||
label: AppLocalizations.of(context).primaryId,
|
label: AppLocalizations.of(context).primaryId,
|
||||||
value: user.primaryId ?? 'N/A',
|
value: _maskPrimaryId(user.primaryId),
|
||||||
), // Replace with PAN if available
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,16 +250,16 @@ class InfoField extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
fontSize: 15,
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value.isEmpty ? 'N/A' : value,
|
||||||
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
|
||||||
import 'package:kmobile/data/repositories/transaction_repository.dart';
|
import 'package:kmobile/data/models/user.dart';
|
||||||
import 'package:kmobile/di/injection.dart';
|
import 'package:kmobile/di/injection.dart';
|
||||||
import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
|
import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
|
||||||
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
|
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
|
||||||
import 'package:kmobile/features/accounts/screens/transaction_details_screen.dart';
|
import 'package:kmobile/features/accounts/screens/all_accounts_screen.dart';
|
||||||
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||||
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
||||||
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
|
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
|
||||||
@@ -15,12 +14,12 @@ import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
|
|||||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_screen.dart';
|
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_screen.dart';
|
||||||
import 'package:kmobile/features/profile/profile_screen.dart';
|
import 'package:kmobile/features/profile/profile_screen.dart';
|
||||||
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
|
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
|
||||||
|
import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
|
||||||
import 'package:kmobile/security/secure_storage.dart';
|
import 'package:kmobile/security/secure_storage.dart';
|
||||||
import 'package:local_auth/local_auth.dart';
|
import 'package:local_auth/local_auth.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:kmobile/data/models/transaction.dart';
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class DashboardScreen extends StatefulWidget {
|
class DashboardScreen extends StatefulWidget {
|
||||||
@@ -30,47 +29,190 @@ class DashboardScreen extends StatefulWidget {
|
|||||||
State<DashboardScreen> createState() => _DashboardScreenState();
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DashboardScreenState extends State<DashboardScreen> {
|
class _DashboardScreenState extends State<DashboardScreen>
|
||||||
|
with SingleTickerProviderStateMixin, RouteAware {
|
||||||
int selectedAccountIndex = 0;
|
int selectedAccountIndex = 0;
|
||||||
bool isVisible = false;
|
Map<String, bool> _visibilityMap = {};
|
||||||
bool isRefreshing = false;
|
bool isRefreshing = false;
|
||||||
bool isBalanceLoading = false;
|
|
||||||
bool _biometricPromptShown = false;
|
|
||||||
bool _txLoading = false;
|
|
||||||
List<Transaction> _transactions = [];
|
|
||||||
bool _txInitialized = false;
|
|
||||||
|
|
||||||
Future<void> _loadTransactions(String accountNo) async {
|
bool _biometricPromptShown = false;
|
||||||
setState(() {
|
bool _txInitialized = false;
|
||||||
_txLoading = true;
|
PageController? _pageController;
|
||||||
_transactions = [];
|
final routeObserver = getIt<RouteObserver<ModalRoute<void>>>();
|
||||||
});
|
|
||||||
try {
|
@override
|
||||||
final repo = getIt<TransactionRepository>();
|
void didChangeDependencies() {
|
||||||
final txs = await repo.fetchTransactions(accountNo);
|
super.didChangeDependencies();
|
||||||
var fiveTxns = <Transaction>[];
|
routeObserver.subscribe(this, ModalRoute.of(context)!);
|
||||||
//only take the first 5 transactions
|
|
||||||
if (txs.length > 5) {
|
|
||||||
fiveTxns = txs.sublist(0, 5);
|
|
||||||
} else {
|
|
||||||
fiveTxns = txs;
|
|
||||||
}
|
}
|
||||||
setState(() => _transactions = fiveTxns);
|
|
||||||
} catch (e) {
|
@override
|
||||||
log(accountNo, error: e);
|
void dispose() {
|
||||||
if (!mounted) return;
|
routeObserver.unsubscribe(this);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
_pageController?.dispose();
|
||||||
SnackBar(
|
_visibilityMap.clear();
|
||||||
content: Text(
|
super.dispose();
|
||||||
AppLocalizations.of(context).failedToLoad(e.toString()),
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPushNext() {
|
||||||
|
// This method is called when another route is pushed on top of this one.
|
||||||
|
// We clear the map and call setState to ensure the UI is updated
|
||||||
|
// if the user navigates back.
|
||||||
|
setState(() {
|
||||||
|
_visibilityMap.clear();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAccountCard(User user, bool isSelected) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final bool isCardVisible = _visibilityMap[user.accountNo] ?? false;
|
||||||
|
// Animated scale for the selected card
|
||||||
|
final scale = isSelected ? 1.02 : 0.9;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 3.0),
|
||||||
|
child: AnimatedScale(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
scale: scale,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 18,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF01A04C),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Top section with account type and number (no refresh button here)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
getFullAccountType(user.accountType),
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
user.accountNo ?? 'N/A',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isSelected) // Show logo only if card is selected
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 20,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(2.0),
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
// Bottom section with balance and combined toggle/refresh
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (isRefreshing && isSelected)
|
||||||
|
Expanded(child: _buildBalanceShimmer())
|
||||||
|
else
|
||||||
|
Expanded(
|
||||||
|
child: FittedBox(
|
||||||
|
fit: BoxFit.scaleDown,
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"₹ ",
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
isCardVisible
|
||||||
|
? user.currentBalance ?? '0.00'
|
||||||
|
: '*****',
|
||||||
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onPrimary,
|
||||||
|
fontSize: 40,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10), // A steady space
|
||||||
|
if (isSelected) // Only show toggle for selected card
|
||||||
|
InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
if (isRefreshing)
|
||||||
|
return; // Prevent taps while refreshing
|
||||||
|
final accountNo = user.accountNo;
|
||||||
|
if (accountNo == null) return;
|
||||||
|
|
||||||
|
final bool currentVisibility =
|
||||||
|
_visibilityMap[accountNo] ?? false;
|
||||||
|
|
||||||
|
if (!currentVisibility) {
|
||||||
|
// If hidden, refresh data and then show the balance
|
||||||
|
await _refreshAccountData(context);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_visibilityMap[accountNo] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If visible, just hide it
|
||||||
|
setState(() {
|
||||||
|
_visibilityMap[accountNo] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
isCardVisible
|
||||||
|
? Symbols.visibility_lock
|
||||||
|
: Symbols.visibility,
|
||||||
|
color: theme.scaffoldBackgroundColor,
|
||||||
|
weight: 800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
if (mounted) {
|
|
||||||
setState(() => _txLoading = false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshAccountData(BuildContext context) async {
|
Future<void> _refreshAccountData(BuildContext context) async {
|
||||||
@@ -100,8 +242,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
return Shimmer.fromColors(
|
return Shimmer.fromColors(
|
||||||
baseColor: theme.colorScheme.primary,
|
baseColor: theme.colorScheme.primary,
|
||||||
highlightColor: theme.colorScheme.onPrimary,
|
highlightColor: theme.colorScheme.onPrimary,
|
||||||
child: Container(
|
child: Container(height: 36, color: theme.scaffoldBackgroundColor),
|
||||||
width: 200, height: 42, color: theme.scaffoldBackgroundColor),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,12 +279,20 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
switch (accountType.toLowerCase()) {
|
switch (accountType.toLowerCase()) {
|
||||||
case 'sa':
|
case 'sa':
|
||||||
return AppLocalizations.of(context).savingsAccount;
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
|
case 'sb':
|
||||||
|
return AppLocalizations.of(context).savingsAccount;
|
||||||
case 'ln':
|
case 'ln':
|
||||||
return AppLocalizations.of(context).loanAccount;
|
return AppLocalizations.of(context).loanAccount;
|
||||||
case 'td':
|
case 'td':
|
||||||
return AppLocalizations.of(context).termDeposit;
|
return AppLocalizations.of(context).termDeposit;
|
||||||
case 'rd':
|
case 'rd':
|
||||||
return AppLocalizations.of(context).recurringDeposit;
|
return AppLocalizations.of(context).recurringDeposit;
|
||||||
|
case 'ca':
|
||||||
|
return "Current Account";
|
||||||
|
case 'cc':
|
||||||
|
return "Cash Credit Account";
|
||||||
|
case 'od':
|
||||||
|
return "Overdraft Account";
|
||||||
default:
|
default:
|
||||||
return AppLocalizations.of(context).unknownAccount;
|
return AppLocalizations.of(context).unknownAccount;
|
||||||
}
|
}
|
||||||
@@ -194,6 +343,19 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final authState = context.read<AuthCubit>().state;
|
||||||
|
String mobileNumberToPass = '';
|
||||||
|
String customerNo = '';
|
||||||
|
String customerName = '';
|
||||||
|
if (authState is Authenticated) {
|
||||||
|
if (selectedAccountIndex >= 0 &&
|
||||||
|
selectedAccountIndex < authState.users.length) {
|
||||||
|
mobileNumberToPass =
|
||||||
|
authState.users[selectedAccountIndex].mobileNo ?? '';
|
||||||
|
customerNo = authState.users[selectedAccountIndex].cifNumber ?? '';
|
||||||
|
customerName = authState.users[selectedAccountIndex].name ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
return BlocListener<AuthCubit, AuthState>(
|
return BlocListener<AuthCubit, AuthState>(
|
||||||
listener: (context, state) async {
|
listener: (context, state) async {
|
||||||
if (state is Authenticated && !_biometricPromptShown) {
|
if (state is Authenticated && !_biometricPromptShown) {
|
||||||
@@ -208,16 +370,41 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
leading: Padding(
|
||||||
automaticallyImplyLeading: false,
|
padding: const EdgeInsets.only(left: 10.0),
|
||||||
|
child: Material(
|
||||||
|
elevation: 4.0,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: const BorderSide(color: Colors.white, width: 1.5),
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
AppLocalizations.of(context).kconnect,
|
AppLocalizations.of(context).kccBankFull.replaceAll('-', '\u2011'),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
softWrap: true, // Explicitly allow wrapping
|
||||||
|
maxLines: 2, // Allow text to wrap to a maximum of 2 lines
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.onPrimary,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
fontSize: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
// Removed centerTitle: true to give more space for text wrapping
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 10.0),
|
padding: const EdgeInsets.only(right: 10.0),
|
||||||
@@ -227,18 +414,18 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const ProfileScreen(),
|
builder: (context) => ProfileScreen(
|
||||||
|
mobileNumber: mobileNumberToPass,
|
||||||
|
customerNo: customerNo,
|
||||||
|
customerName: customerName),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: CircleAvatar(
|
child: const CircleAvatar(
|
||||||
backgroundColor: Colors.grey[200],
|
radius: 21,
|
||||||
radius: 20,
|
child: Icon(
|
||||||
child: SvgPicture.asset(
|
Symbols.person,
|
||||||
'assets/images/avatar_male.svg',
|
size: 30,
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -253,24 +440,30 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
if (state is Authenticated) {
|
if (state is Authenticated) {
|
||||||
final users = state.users;
|
final users = state.users;
|
||||||
final currAccount = users[selectedAccountIndex];
|
final currAccount = users[selectedAccountIndex];
|
||||||
|
final accountType = currAccount.accountType?.toLowerCase();
|
||||||
|
final isPaymentDisabled = accountType != 'sa' &&
|
||||||
|
accountType != 'sb' &&
|
||||||
|
accountType != 'ca';
|
||||||
// first‐time load
|
// first‐time load
|
||||||
if (!_txInitialized) {
|
if (!_txInitialized) {
|
||||||
_txInitialized = true;
|
_txInitialized = true;
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {});
|
||||||
_loadTransactions(currAccount.accountNo!);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
_pageController ??= PageController(
|
||||||
|
initialPage: selectedAccountIndex,
|
||||||
|
viewportFraction: 0.75,
|
||||||
|
);
|
||||||
final firstName = getProcessedFirstName(currAccount.name);
|
final firstName = getProcessedFirstName(currAccount.name);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(height: 16), // Added spacing
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 8.0),
|
padding: const EdgeInsets.only(left: 4.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
"${AppLocalizations.of(context).hi} $firstName!",
|
"${AppLocalizations.of(context).hi} $firstName!",
|
||||||
style: GoogleFonts.baumans().copyWith(
|
style: GoogleFonts.baumans().copyWith(
|
||||||
@@ -282,176 +475,76 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Account Info Card
|
// Account Info Cards
|
||||||
Container(
|
SizedBox(
|
||||||
padding: const EdgeInsets.symmetric(
|
height: 160,
|
||||||
horizontal: 18,
|
child: PageView.builder(
|
||||||
vertical: 10,
|
clipBehavior: Clip.none,
|
||||||
|
controller: _pageController,
|
||||||
|
itemCount:
|
||||||
|
users.length, // Keep this to show adjacent cards
|
||||||
|
|
||||||
|
onPageChanged: (int newIndex) async {
|
||||||
|
if (newIndex == selectedAccountIndex) return;
|
||||||
|
|
||||||
|
// Hide the balance of the old card when scrolling away
|
||||||
|
final oldAccountNo =
|
||||||
|
users[selectedAccountIndex].accountNo;
|
||||||
|
if (oldAccountNo != null) {
|
||||||
|
_visibilityMap[oldAccountNo] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
selectedAccountIndex = newIndex;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final user = users[index];
|
||||||
|
final isSelected = index == selectedAccountIndex;
|
||||||
|
return _buildAccountCard(user, isSelected);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
),
|
||||||
child: Column(
|
const SizedBox(height: 8),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
GestureDetector(
|
||||||
"${getFullAccountType(currAccount.accountType)}: ",
|
onTap: () {
|
||||||
style: TextStyle(
|
Navigator.push(
|
||||||
color: theme.colorScheme.onPrimary,
|
context,
|
||||||
fontSize: 18,
|
MaterialPageRoute(
|
||||||
fontWeight: FontWeight.w700,
|
builder: (context) =>
|
||||||
|
AllAccountsScreen(users: users),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
DropdownButton<int>(
|
},
|
||||||
value: selectedAccountIndex,
|
|
||||||
dropdownColor: theme.colorScheme.primary,
|
|
||||||
underline: const SizedBox(),
|
|
||||||
icon: const Icon(Icons.keyboard_arrow_down),
|
|
||||||
iconEnabledColor: theme.colorScheme.onPrimary,
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
fontSize: 18,
|
|
||||||
),
|
|
||||||
items: List.generate(users.length, (index) {
|
|
||||||
return DropdownMenuItem<int>(
|
|
||||||
value: index,
|
|
||||||
child: Text(
|
child: Text(
|
||||||
users[index].accountNo ?? 'N/A',
|
AppLocalizations.of(context).viewall,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: theme.colorScheme.onPrimary,
|
fontSize: 14,
|
||||||
fontSize: 18,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.w700,
|
color: theme.colorScheme.primary,
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
onChanged: (int? newIndex) async {
|
|
||||||
if (newIndex == null ||
|
|
||||||
newIndex == selectedAccountIndex) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isBalanceLoading) return;
|
|
||||||
if (isVisible) {
|
|
||||||
setState(() {
|
|
||||||
isBalanceLoading = true;
|
|
||||||
selectedAccountIndex = newIndex;
|
|
||||||
});
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(milliseconds: 200),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
isBalanceLoading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
selectedAccountIndex = newIndex;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await _loadTransactions(
|
|
||||||
users[newIndex].accountNo!,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: isRefreshing
|
|
||||||
? SizedBox(
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Symbols.refresh,
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
),
|
|
||||||
onPressed: isRefreshing
|
|
||||||
? null
|
|
||||||
: () => _refreshAccountData(context),
|
|
||||||
tooltip: 'Refresh',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"₹ ",
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
isRefreshing || isBalanceLoading
|
|
||||||
? _buildBalanceShimmer()
|
|
||||||
: Text(
|
|
||||||
isVisible
|
|
||||||
? currAccount.currentBalance ??
|
|
||||||
'0.00'
|
|
||||||
: '*****',
|
|
||||||
style: TextStyle(
|
|
||||||
color: theme.colorScheme.onPrimary,
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
InkWell(
|
|
||||||
onTap: () async {
|
|
||||||
if (isBalanceLoading) return;
|
|
||||||
if (!isVisible) {
|
|
||||||
setState(() {
|
|
||||||
isBalanceLoading = true;
|
|
||||||
});
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(seconds: 1),
|
|
||||||
);
|
|
||||||
setState(() {
|
|
||||||
isVisible = true;
|
|
||||||
isBalanceLoading = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setState(() {
|
|
||||||
isVisible = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Icon(
|
|
||||||
isVisible
|
|
||||||
? Symbols.visibility_lock
|
|
||||||
: Symbols.visibility,
|
|
||||||
color: theme.scaffoldBackgroundColor,
|
|
||||||
weight: 800,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).quickLinks,
|
AppLocalizations.of(context).quickLinks,
|
||||||
style: const TextStyle(fontSize: 17),
|
style: const TextStyle(fontSize: 17),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Quick Links
|
// Quick Links
|
||||||
GridView.count(
|
GridView.count(
|
||||||
crossAxisCount: 4,
|
crossAxisCount: 3,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
childAspectRatio: 1,
|
||||||
children: [
|
children: [
|
||||||
_buildQuickLink(
|
_buildQuickLink(
|
||||||
Symbols.id_card,
|
Symbols.id_card,
|
||||||
@@ -480,6 +573,7 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disable: isPaymentDisabled,
|
||||||
),
|
),
|
||||||
_buildQuickLink(Symbols.send_money,
|
_buildQuickLink(Symbols.send_money,
|
||||||
AppLocalizations.of(context).fundTransfer, () {
|
AppLocalizations.of(context).fundTransfer, () {
|
||||||
@@ -491,9 +585,10 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
users[selectedAccountIndex]
|
users[selectedAccountIndex]
|
||||||
.accountNo!,
|
.accountNo!,
|
||||||
remitterName:
|
remitterName:
|
||||||
users[selectedAccountIndex]
|
users[selectedAccountIndex].name!,
|
||||||
.name!)));
|
// Pass the full list of accounts
|
||||||
}, disable: false),
|
accounts: users)));
|
||||||
|
}, disable: isPaymentDisabled),
|
||||||
_buildQuickLink(
|
_buildQuickLink(
|
||||||
Symbols.server_person,
|
Symbols.server_person,
|
||||||
AppLocalizations.of(context).accountInfo,
|
AppLocalizations.of(context).accountInfo,
|
||||||
@@ -517,15 +612,18 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
AccountStatementScreen(
|
AccountStatementScreen(
|
||||||
accountNo: users[selectedAccountIndex]
|
users: users,
|
||||||
.accountNo!,
|
selectedIndex: selectedAccountIndex,
|
||||||
balance: users[selectedAccountIndex]
|
|
||||||
.availableBalance!,
|
|
||||||
)));
|
)));
|
||||||
}),
|
}),
|
||||||
_buildQuickLink(Symbols.checkbook,
|
_buildQuickLink(Icons.location_pin,
|
||||||
AppLocalizations.of(context).handleCheque, () {},
|
AppLocalizations.of(context).branchlocator, () {
|
||||||
disable: true),
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
const BranchLocatorScreen()));
|
||||||
|
}, disable: false),
|
||||||
_buildQuickLink(Icons.group,
|
_buildQuickLink(Icons.group,
|
||||||
AppLocalizations.of(context).manageBeneficiary,
|
AppLocalizations.of(context).manageBeneficiary,
|
||||||
() {
|
() {
|
||||||
@@ -544,68 +642,23 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
const EnquiryScreen()));
|
const EnquiryScreen()));
|
||||||
}),
|
}),
|
||||||
],
|
_buildQuickLink(
|
||||||
),
|
Symbols.person,
|
||||||
const SizedBox(height: 5),
|
AppLocalizations.of(context).profile,
|
||||||
|
() {
|
||||||
// Recent Transactions
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context).recentTransactions,
|
|
||||||
style: const TextStyle(fontSize: 17),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (_txLoading)
|
|
||||||
..._buildTransactionShimmer()
|
|
||||||
else if (_transactions.isNotEmpty)
|
|
||||||
..._transactions.map(
|
|
||||||
(tx) => ListTile(
|
|
||||||
leading: Icon(
|
|
||||||
tx.type == 'CR'
|
|
||||||
? Symbols.call_received
|
|
||||||
: Symbols.call_made,
|
|
||||||
color: tx.type == 'CR'
|
|
||||||
? const Color(0xFF10BB10)
|
|
||||||
: theme.colorScheme.error,
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
tx.date ?? '',
|
|
||||||
style: const TextStyle(fontSize: 15),
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
tx.name != null
|
|
||||||
? (tx.name!.length > 18
|
|
||||||
? tx.name!.substring(0, 22)
|
|
||||||
: tx.name!)
|
|
||||||
: '',
|
|
||||||
style: const TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
trailing: Text(
|
|
||||||
"₹${tx.amount}",
|
|
||||||
style: const TextStyle(fontSize: 17),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) =>
|
builder: (context) => ProfileScreen(
|
||||||
TransactionDetailsScreen(transaction: tx),
|
mobileNumber: mobileNumberToPass,
|
||||||
|
customerNo: customerNo,
|
||||||
|
customerName: customerName),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
disable: false,
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
else
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
AppLocalizations.of(context).noTransactions,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Theme.of(context).colorScheme.outline,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -621,32 +674,6 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildTransactionShimmer() {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
return List.generate(3, (i) {
|
|
||||||
return ListTile(
|
|
||||||
leading: Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 12, backgroundColor: theme.scaffoldBackgroundColor),
|
|
||||||
),
|
|
||||||
title: Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: Container(
|
|
||||||
height: 10, width: 100, color: theme.scaffoldBackgroundColor),
|
|
||||||
),
|
|
||||||
subtitle: Shimmer.fromColors(
|
|
||||||
baseColor: Colors.grey[300]!,
|
|
||||||
highlightColor: Colors.grey[100]!,
|
|
||||||
child: Container(
|
|
||||||
height: 8, width: 60, color: theme.scaffoldBackgroundColor),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildQuickLink(
|
Widget _buildQuickLink(
|
||||||
IconData icon,
|
IconData icon,
|
||||||
String label,
|
String label,
|
||||||
@@ -654,28 +681,36 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
|||||||
bool disable = false,
|
bool disable = false,
|
||||||
}) {
|
}) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
return InkWell(
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
onTap: disable ? null : onTap,
|
onTap: disable ? null : onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 30,
|
size: 30,
|
||||||
color: disable
|
color: disable ? theme.disabledColor : theme.colorScheme.primary,
|
||||||
? theme.colorScheme.onSurface.withOpacity(0.3)
|
|
||||||
: theme.colorScheme.primary,
|
|
||||||
grade: 200,
|
|
||||||
weight: 700,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(fontSize: 13),
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
color:
|
||||||
|
disable ? theme.disabledColor : theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,4 +72,3 @@ class AccountCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
// ignore_for_file: use_build_context_synchronously
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
@@ -12,44 +10,97 @@ class EnquiryScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EnquiryScreen extends State<EnquiryScreen> {
|
class _EnquiryScreen extends State<EnquiryScreen> {
|
||||||
|
// Updated to launch externally and pre-fill the subject
|
||||||
Future<void> _launchEmailAddress(String email) async {
|
Future<void> _launchEmailAddress(String email) async {
|
||||||
final Uri emailUri = Uri(scheme: 'mailto', path: email);
|
final Uri emailUri = Uri(
|
||||||
|
scheme: 'mailto',
|
||||||
|
path: email,
|
||||||
|
query: 'subject=Enquiry', // Pre-fills the subject line
|
||||||
|
);
|
||||||
if (await canLaunchUrl(emailUri)) {
|
if (await canLaunchUrl(emailUri)) {
|
||||||
await launchUrl(emailUri);
|
// Use external application mode
|
||||||
|
await launchUrl(emailUri, mode: LaunchMode.externalApplication);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('${AppLocalizations.of(context).emailLaunchError} $email');
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Could not open email app for $email')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated with better error handling
|
||||||
Future<void> _launchPhoneNumber(String phone) async {
|
Future<void> _launchPhoneNumber(String phone) async {
|
||||||
final Uri phoneUri = Uri(scheme: 'tel', path: phone);
|
final Uri phoneUri = Uri(scheme: 'tel', path: phone);
|
||||||
if (await canLaunchUrl(phoneUri)) {
|
if (await canLaunchUrl(phoneUri)) {
|
||||||
await launchUrl(phoneUri);
|
await launchUrl(phoneUri);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('${AppLocalizations.of(context).dialerLaunchError} $phone');
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Could not open dialer for $phone')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated to launch externally
|
||||||
|
Future<void> _launchUrl(String url) async {
|
||||||
|
final Uri uri = Uri.parse(url);
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
// Use external application mode
|
||||||
|
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Could not launch $url')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContactItem(String role, String email, String phone) {
|
Widget _buildContactItem(String role, String email, String phone) {
|
||||||
return Column(
|
return Card(
|
||||||
|
elevation: 4,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(role,
|
Text(role,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface)),
|
style: TextStyle(
|
||||||
const SizedBox(height: 4),
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
|
const SizedBox(height: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _launchEmailAddress(email),
|
onTap: () => _launchEmailAddress(email),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.email),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
child: Text(email,
|
child: Text(email,
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary)),
|
style: TextStyle(
|
||||||
),
|
color: Theme.of(context).colorScheme.primary,
|
||||||
const SizedBox(height: 4),
|
fontSize: 14)),
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _launchPhoneNumber(phone),
|
|
||||||
child: Text(phone,
|
|
||||||
style:
|
|
||||||
TextStyle(color: Theme.of(context).scaffoldBackgroundColor)),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _launchPhoneNumber(phone),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.phone),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(phone,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
fontSize: 14)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,51 +111,67 @@ class _EnquiryScreen extends State<EnquiryScreen> {
|
|||||||
title: Text(AppLocalizations.of(context).enquiry),
|
title: Text(AppLocalizations.of(context).enquiry),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Padding(
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// … existing Mail us / Call us / Write to us …
|
Card(
|
||||||
const SizedBox(height: 20),
|
elevation: 4,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () =>
|
||||||
|
_launchUrl("https://kccbhp.bank.in/complaint-form/"),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
Text(
|
Text(
|
||||||
AppLocalizations.of(context).writeToUs,
|
"Complaint Form",
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
"complaint@kccb.in",
|
|
||||||
style: TextStyle(color: Theme.of(context).colorScheme.primary),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Text(
|
|
||||||
AppLocalizations.of(context).keyContacts,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 17,
|
fontSize: 15,
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
// horizontal line
|
|
||||||
),
|
),
|
||||||
Divider(color: Theme.of(context).colorScheme.outline),
|
Icon(
|
||||||
|
Icons.open_in_new,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
size: 16.0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
Text(
|
||||||
|
AppLocalizations.of(context).keyContacts,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
_buildContactItem(
|
_buildContactItem(
|
||||||
AppLocalizations.of(context).chairman,
|
AppLocalizations.of(context).chairman,
|
||||||
"chairman@kccb.in",
|
"chairman@kccb.in",
|
||||||
"01892-222677",
|
"01892-222677",
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildContactItem(
|
_buildContactItem(
|
||||||
AppLocalizations.of(context).managingDirector,
|
AppLocalizations.of(context).managingDirector,
|
||||||
"md@kccb.in",
|
"md@kccb.in",
|
||||||
"01892-224969",
|
"01892-224969",
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildContactItem(
|
_buildContactItem(
|
||||||
AppLocalizations.of(context).gmWest,
|
AppLocalizations.of(context).gmWest,
|
||||||
"gmw@kccb.in",
|
"gmw@kccb.in",
|
||||||
"01892-223280",
|
"01892-223280",
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildContactItem(
|
_buildContactItem(
|
||||||
AppLocalizations.of(context).gmNorth,
|
AppLocalizations.of(context).gmNorth,
|
||||||
"gmn@kccb.in",
|
"gmn@kccb.in",
|
||||||
@@ -113,6 +180,25 @@ class _EnquiryScreen extends State<EnquiryScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
lib/features/fund_transfer/screens/cooldown.dart
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CooldownTimer extends StatefulWidget {
|
||||||
|
final DateTime createdAt;
|
||||||
|
final VoidCallback onTimerFinish;
|
||||||
|
|
||||||
|
const CooldownTimer({
|
||||||
|
Key? key,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.onTimerFinish,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_CooldownTimerState createState() => _CooldownTimerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CooldownTimerState extends State<CooldownTimer> {
|
||||||
|
late Timer _timer;
|
||||||
|
late Duration _timeRemaining;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_updateRemainingTime();
|
||||||
|
// Update the timer every second
|
||||||
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
_updateRemainingTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateRemainingTime() {
|
||||||
|
final cooldownEnd = widget.createdAt.add(const Duration(minutes: 60));
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
if (now.isAfter(cooldownEnd)) {
|
||||||
|
_timeRemaining = Duration.zero;
|
||||||
|
_timer.cancel();
|
||||||
|
// Notify the parent widget that the timer is done
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
widget.onTimerFinish();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_timeRemaining = cooldownEnd.difference(now);
|
||||||
|
}
|
||||||
|
// Trigger a rebuild
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_timer.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatDuration(Duration duration) {
|
||||||
|
final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
|
||||||
|
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
|
||||||
|
return '$minutes:$seconds';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_timeRemaining == Duration.zero) {
|
||||||
|
return const SizedBox
|
||||||
|
.shrink(); // Or some other widget indicating it's enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.timer,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
_formatDuration(_timeRemaining),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontFeatures: const [FontFeature.tabularFigures()],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:kmobile/api/services/limit_service.dart';
|
||||||
import 'package:kmobile/api/services/neft_service.dart';
|
import 'package:kmobile/api/services/neft_service.dart';
|
||||||
import 'package:kmobile/api/services/rtgs_service.dart';
|
import 'package:kmobile/api/services/rtgs_service.dart';
|
||||||
import 'package:kmobile/api/services/imps_service.dart';
|
import 'package:kmobile/api/services/imps_service.dart';
|
||||||
@@ -40,13 +42,71 @@ class FundTransferAmountScreen extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
||||||
|
final _limitService = getIt<LimitService>();
|
||||||
|
Limit? _limit;
|
||||||
|
bool _isLoadingLimit = true;
|
||||||
|
bool _isAmountOverLimit = false;
|
||||||
|
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '₹');
|
||||||
final _amountController = TextEditingController();
|
final _amountController = TextEditingController();
|
||||||
|
final _remarksController = TextEditingController();
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
TransactionMode _selectedMode = TransactionMode.neft;
|
TransactionMode _selectedMode = TransactionMode.neft;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadLimit(); // Call the new method
|
||||||
|
_amountController.addListener(_checkAmountLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLimit() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingLimit = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final limitData = await _limitService.getLimit();
|
||||||
|
setState(() {
|
||||||
|
_limit = limitData;
|
||||||
|
_isLoadingLimit = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Handle error if needed
|
||||||
|
setState(() {
|
||||||
|
_isLoadingLimit = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add this method to check the amount against the limit
|
||||||
|
void _checkAmountLimit() {
|
||||||
|
if (_limit == null) return;
|
||||||
|
|
||||||
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
|
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
|
||||||
|
final bool isOverLimit = amount > remainingLimit;
|
||||||
|
|
||||||
|
if (isOverLimit) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isAmountOverLimit != isOverLimit) {
|
||||||
|
setState(() {
|
||||||
|
_isAmountOverLimit = isOverLimit;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_amountController.removeListener(_checkAmountLimit);
|
||||||
_amountController.dispose();
|
_amountController.dispose();
|
||||||
|
_remarksController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +173,7 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
remitterName: widget.remitterName,
|
remitterName: widget.remitterName,
|
||||||
beneficiaryName: widget.creditBeneficiary.name,
|
beneficiaryName: widget.creditBeneficiary.name,
|
||||||
tpin: tpin,
|
tpin: tpin,
|
||||||
|
remarks: _remarksController.text,
|
||||||
);
|
);
|
||||||
final neftService = getIt<NeftService>();
|
final neftService = getIt<NeftService>();
|
||||||
final completer = Completer<PaymentResponse>();
|
final completer = Completer<PaymentResponse>();
|
||||||
@@ -180,6 +241,7 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
remitterName: widget.remitterName,
|
remitterName: widget.remitterName,
|
||||||
beneficiaryName: widget.creditBeneficiary.name,
|
beneficiaryName: widget.creditBeneficiary.name,
|
||||||
tpin: tpin,
|
tpin: tpin,
|
||||||
|
remarks: _remarksController.text,
|
||||||
);
|
);
|
||||||
final impsService = getIt<ImpsService>();
|
final impsService = getIt<ImpsService>();
|
||||||
final completer = Completer<PaymentResponse>();
|
final completer = Completer<PaymentResponse>();
|
||||||
@@ -236,6 +298,7 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
remitterName: widget.remitterName,
|
remitterName: widget.remitterName,
|
||||||
beneficiaryName: widget.creditBeneficiary.name,
|
beneficiaryName: widget.creditBeneficiary.name,
|
||||||
tpin: tpin,
|
tpin: tpin,
|
||||||
|
remarks: _remarksController.text,
|
||||||
);
|
);
|
||||||
final rtgsService = getIt<RtgsService>();
|
final rtgsService = getIt<RtgsService>();
|
||||||
final completer = Completer<PaymentResponse>();
|
final completer = Completer<PaymentResponse>();
|
||||||
@@ -299,7 +362,9 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
|
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Padding(
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -335,8 +400,8 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading:
|
leading: getBankLogo(
|
||||||
getBankLogo(widget.creditBeneficiary.bankName, context),
|
widget.creditBeneficiary.bankName, context),
|
||||||
title: Text(widget.creditBeneficiary.name),
|
title: Text(widget.creditBeneficiary.name),
|
||||||
subtitle: Text(widget.creditBeneficiary.accountNo),
|
subtitle: Text(widget.creditBeneficiary.accountNo),
|
||||||
),
|
),
|
||||||
@@ -368,7 +433,8 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
selectedColor: Theme.of(context).colorScheme.onPrimary,
|
selectedColor:
|
||||||
|
Theme.of(context).colorScheme.onPrimary,
|
||||||
fillColor: Theme.of(context).colorScheme.primary,
|
fillColor: Theme.of(context).colorScheme.primary,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
borderColor: Colors.transparent,
|
borderColor: Colors.transparent,
|
||||||
@@ -396,6 +462,15 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
],
|
],
|
||||||
|
//Remarks
|
||||||
|
TextFormField(
|
||||||
|
controller: _remarksController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: AppLocalizations.of(context).remarks,
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
// Amount
|
// Amount
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _amountController,
|
controller: _amountController,
|
||||||
@@ -416,13 +491,21 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
if (_isLoadingLimit)
|
||||||
|
Text(AppLocalizations.of(context).fetchingDailyLimit),
|
||||||
|
if (!_isLoadingLimit && _limit != null)
|
||||||
|
Text(
|
||||||
|
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
||||||
// Proceed Button
|
// Proceed Button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _onProceed,
|
onPressed: _isAmountOverLimit ? null : _onProceed,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
),
|
),
|
||||||
@@ -434,6 +517,22 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
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
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/cooldown.dart';
|
||||||
import 'package:kmobile/widgets/bank_logos.dart';
|
import 'package:kmobile/widgets/bank_logos.dart';
|
||||||
import 'package:kmobile/data/models/beneficiary.dart';
|
import 'package:kmobile/data/models/beneficiary.dart';
|
||||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart';
|
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart';
|
||||||
@@ -27,11 +28,22 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
var service = getIt<BeneficiaryService>();
|
var service = getIt<BeneficiaryService>();
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
List<Beneficiary> _beneficiaries = [];
|
List<Beneficiary> _beneficiaries = [];
|
||||||
|
List<Beneficiary> _filteredBeneficiaries = [];
|
||||||
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadBeneficiaries();
|
_loadBeneficiaries();
|
||||||
|
_searchController.addListener(() {
|
||||||
|
_filterBeneficiaries(_searchController.text);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_searchController.dispose();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadBeneficiaries() async {
|
Future<void> _loadBeneficiaries() async {
|
||||||
@@ -39,14 +51,28 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
setState(() {
|
setState(() {
|
||||||
_beneficiaries = data
|
_beneficiaries = data
|
||||||
.where((b) => widget.isOwnBank
|
.where((b) => widget.isOwnBank
|
||||||
? b.bankName ==
|
? b.bankName!.toLowerCase().contains('kangra central')
|
||||||
'THE KANGRA CENTRAL CO-OP BANK LIMITED' // Assuming 'KCCB' is your bank's name
|
: !b.bankName!.toLowerCase().contains('kangra central'))
|
||||||
: b.bankName != 'THE KANGRA CENTRAL CO-OP BANK LIMITED')
|
|
||||||
.toList();
|
.toList();
|
||||||
|
_filteredBeneficiaries = _beneficiaries;
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _filterBeneficiaries(String query) {
|
||||||
|
setState(() {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
_filteredBeneficiaries = _beneficiaries;
|
||||||
|
} else {
|
||||||
|
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
|
||||||
|
final lowerQuery = query.toLowerCase();
|
||||||
|
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
|
||||||
|
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildShimmerList() {
|
Widget _buildShimmerList() {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: 6,
|
itemCount: 6,
|
||||||
@@ -74,15 +100,32 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBeneficiaryList() {
|
Widget _buildBeneficiaryList() {
|
||||||
if (_beneficiaries.isEmpty) {
|
if (_filteredBeneficiaries.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
|
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
|
||||||
}
|
}
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
itemCount: _beneficiaries.length,
|
itemCount: _filteredBeneficiaries.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final beneficiary = _beneficiaries[index];
|
final beneficiary = _filteredBeneficiaries[index];
|
||||||
return ListTile(
|
|
||||||
|
// --- Cooldown Logic ---
|
||||||
|
bool isCoolingDown = false;
|
||||||
|
if (beneficiary.createdAt != null) {
|
||||||
|
final sixtyMinutesAgo =
|
||||||
|
DateTime.now().subtract(const Duration(minutes: 60));
|
||||||
|
isCoolingDown = beneficiary.createdAt!.isAfter(sixtyMinutesAgo);
|
||||||
|
}
|
||||||
|
// --- End of Cooldown Logic ---
|
||||||
|
|
||||||
|
// By wrapping the ListTile in an Opacity widget, we can make it look
|
||||||
|
// disabled while ensuring the onTap callback still works.
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: isCoolingDown ? 0.5 : 1.0,
|
||||||
|
child: ListTile(
|
||||||
|
// REMOVED the 'enabled' property from here.
|
||||||
leading: CircleAvatar(
|
leading: CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
@@ -101,7 +144,25 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
trailing: isCoolingDown
|
||||||
|
? CooldownTimer(
|
||||||
|
createdAt: beneficiary.createdAt!,
|
||||||
|
onTimerFinish: () {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
if (isCoolingDown) {
|
||||||
|
// This will now execute correctly on tap
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Beneficiary will be enabled after the cooldown period.'),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -113,7 +174,10 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -125,7 +189,45 @@ class _FundTransferBeneficiaryScreenState
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(AppLocalizations.of(context).beneficiaries),
|
title: Text(AppLocalizations.of(context).beneficiaries),
|
||||||
),
|
),
|
||||||
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: TextField(
|
||||||
|
controller: _searchController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: "Search by name or account number",
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,78 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||||
|
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
||||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
|
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_accounts_screen.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart'; // Keep localizations
|
||||||
|
|
||||||
class FundTransferScreen extends StatelessWidget {
|
class FundTransferScreen extends StatelessWidget {
|
||||||
final String creditAccountNo;
|
final String creditAccountNo;
|
||||||
final String remitterName;
|
final String remitterName;
|
||||||
|
final List<User> accounts; // Continue to accept the list of accounts
|
||||||
|
|
||||||
const FundTransferScreen({
|
const FundTransferScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.creditAccountNo,
|
required this.creditAccountNo,
|
||||||
required this.remitterName,
|
required this.remitterName,
|
||||||
|
required this.accounts, // It is passed from the dashboard
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
// Restore localization for the title
|
||||||
title: Text(AppLocalizations.of(context)
|
title: Text(AppLocalizations.of(context)
|
||||||
.fundTransfer
|
.fundTransfer
|
||||||
.replaceFirst(RegExp('\n'), '')),
|
.replaceFirst(RegExp('\n'), '')),
|
||||||
),
|
),
|
||||||
body: ListView(
|
// Wrap with BlocBuilder to check the authentication state
|
||||||
|
body: BlocBuilder<AuthCubit, AuthState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
FundTransferManagementTile(
|
Padding(
|
||||||
icon: Symbols.input_circle,
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
label: AppLocalizations.of(context).ownBank,
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FundTransferManagementTile(
|
||||||
|
icon: Symbols.person,
|
||||||
|
label: "Self Pay",
|
||||||
|
subtitle:
|
||||||
|
AppLocalizations.of(context).ftselfpaysubtitle,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => FundTransferBeneficiaryScreen(
|
builder: (context) =>
|
||||||
|
FundTransferSelfAccountsScreen(
|
||||||
|
debitAccountNo: creditAccountNo,
|
||||||
|
remitterName: remitterName,
|
||||||
|
accounts: accounts,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
disable: state is! Authenticated,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: FundTransferManagementTile(
|
||||||
|
icon: Symbols.input_circle,
|
||||||
|
label: AppLocalizations.of(context).ownBank,
|
||||||
|
subtitle: AppLocalizations.of(context).ftownsubtitle,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
FundTransferBeneficiaryScreen(
|
||||||
creditAccountNo: creditAccountNo,
|
creditAccountNo: creditAccountNo,
|
||||||
remitterName: remitterName,
|
remitterName: remitterName,
|
||||||
isOwnBank: true,
|
isOwnBank: true,
|
||||||
@@ -39,15 +81,20 @@ class FundTransferScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
FundTransferManagementTile(
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: FundTransferManagementTile(
|
||||||
icon: Symbols.output_circle,
|
icon: Symbols.output_circle,
|
||||||
label: AppLocalizations.of(context).outsideBank,
|
label: AppLocalizations.of(context).outsideBank,
|
||||||
|
subtitle:
|
||||||
|
AppLocalizations.of(context).ftoutsidesubtitle,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => FundTransferBeneficiaryScreen(
|
builder: (context) =>
|
||||||
|
FundTransferBeneficiaryScreen(
|
||||||
creditAccountNo: creditAccountNo,
|
creditAccountNo: creditAccountNo,
|
||||||
remitterName: remitterName,
|
remitterName: remitterName,
|
||||||
isOwnBank: false,
|
isOwnBank: false,
|
||||||
@@ -56,9 +103,28 @@ class FundTransferScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +132,7 @@ class FundTransferScreen extends StatelessWidget {
|
|||||||
class FundTransferManagementTile extends StatelessWidget {
|
class FundTransferManagementTile extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final String label;
|
final String label;
|
||||||
|
final String? subtitle;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
final bool disable;
|
final bool disable;
|
||||||
|
|
||||||
@@ -73,18 +140,65 @@ class FundTransferManagementTile extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
required this.label,
|
||||||
|
this.subtitle,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
this.disable = false,
|
this.disable = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
final theme = Theme.of(context);
|
||||||
leading: Icon(icon),
|
return Card(
|
||||||
title: Text(label),
|
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||||
trailing: const Icon(Symbols.arrow_right, size: 20),
|
shape: RoundedRectangleBorder(
|
||||||
onTap: onTap,
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
enabled: !disable,
|
),
|
||||||
|
elevation: 4, // Add some elevation for better visual separation
|
||||||
|
child: InkWell(
|
||||||
|
onTap:
|
||||||
|
disable ? null : onTap, // Disable InkWell if the tile is disabled
|
||||||
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 48, // Make icon larger
|
||||||
|
color:
|
||||||
|
disable ? theme.disabledColor : theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: disable
|
||||||
|
? theme.disabledColor
|
||||||
|
: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (subtitle != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
subtitle!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: disable
|
||||||
|
? theme.disabledColor
|
||||||
|
: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_self_amount_screen.dart';
|
||||||
|
import 'package:kmobile/widgets/bank_logos.dart';
|
||||||
|
|
||||||
|
class FundTransferSelfAccountsScreen extends StatelessWidget {
|
||||||
|
final String debitAccountNo;
|
||||||
|
final String remitterName;
|
||||||
|
final List<User> accounts;
|
||||||
|
|
||||||
|
const FundTransferSelfAccountsScreen({
|
||||||
|
super.key,
|
||||||
|
required this.debitAccountNo,
|
||||||
|
required this.remitterName,
|
||||||
|
required this.accounts,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper function to get the full account type name from the short code
|
||||||
|
String _getFullAccountType(String? accountType) {
|
||||||
|
if (accountType == null || accountType.isEmpty) return 'N/A';
|
||||||
|
switch (accountType.toLowerCase()) {
|
||||||
|
case 'sa':
|
||||||
|
case 'sb':
|
||||||
|
return "Savings Account";
|
||||||
|
case 'ln':
|
||||||
|
return "Loan Account";
|
||||||
|
case 'td':
|
||||||
|
return "Term Deposit";
|
||||||
|
case 'rd':
|
||||||
|
return "Recurring Deposit";
|
||||||
|
case 'ca':
|
||||||
|
return "Current Account";
|
||||||
|
default:
|
||||||
|
return "Unknown Account";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Filter out the account from which the transfer is being made
|
||||||
|
final filteredAccounts =
|
||||||
|
accounts.where((acc) => acc.accountNo != debitAccountNo).toList();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Select Account"),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
filteredAccounts.isEmpty
|
||||||
|
? const Center(
|
||||||
|
child: Text("No other accounts found"),
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
itemCount: filteredAccounts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final account = filteredAccounts[index];
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 6),
|
||||||
|
child: ListTile(
|
||||||
|
leading: CircleAvatar(
|
||||||
|
radius: 24,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: getBankLogo(
|
||||||
|
'Kangra Central Co-operative Bank', context),
|
||||||
|
),
|
||||||
|
title: Text(account.name ?? 'N/A'),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(account.accountNo ?? 'N/A'),
|
||||||
|
Text(
|
||||||
|
_getFullAccountType(account.accountType),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
// Navigate to the amount screen, passing the selected User object directly.
|
||||||
|
// No Beneficiary object is created.
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) =>
|
||||||
|
FundTransferSelfAmountScreen(
|
||||||
|
debitAccountNo: debitAccountNo,
|
||||||
|
creditAccount: account, // Pass the User object
|
||||||
|
remitterName: remitterName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:kmobile/api/services/limit_service.dart';
|
||||||
|
import 'package:kmobile/api/services/payment_service.dart';
|
||||||
|
import 'package:kmobile/data/models/transfer.dart';
|
||||||
|
import 'package:kmobile/data/models/user.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/payment_animation.dart';
|
||||||
|
import 'package:kmobile/features/fund_transfer/screens/transaction_pin_screen.dart';
|
||||||
|
import 'package:kmobile/widgets/bank_logos.dart';
|
||||||
|
|
||||||
|
class FundTransferSelfAmountScreen extends StatefulWidget {
|
||||||
|
final String debitAccountNo;
|
||||||
|
final User creditAccount;
|
||||||
|
final String remitterName;
|
||||||
|
|
||||||
|
const FundTransferSelfAmountScreen({
|
||||||
|
super.key,
|
||||||
|
required this.debitAccountNo,
|
||||||
|
required this.creditAccount,
|
||||||
|
required this.remitterName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FundTransferSelfAmountScreen> createState() =>
|
||||||
|
_FundTransferSelfAmountScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FundTransferSelfAmountScreenState
|
||||||
|
extends State<FundTransferSelfAmountScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _amountController = TextEditingController();
|
||||||
|
final _remarksController = TextEditingController();
|
||||||
|
|
||||||
|
// --- Limit Checking Variables ---
|
||||||
|
final _limitService = getIt<LimitService>();
|
||||||
|
Limit? _limit;
|
||||||
|
bool _isLoadingLimit = true;
|
||||||
|
bool _isAmountOverLimit = false;
|
||||||
|
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '₹');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadLimit(); // Fetch the daily limit
|
||||||
|
_amountController
|
||||||
|
.addListener(_checkAmountLimit); // Listen for amount changes
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_amountController.removeListener(_checkAmountLimit);
|
||||||
|
_amountController.dispose();
|
||||||
|
_remarksController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadLimit() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingLimit = true;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
final limitData = await _limitService.getLimit();
|
||||||
|
setState(() {
|
||||||
|
_limit = limitData;
|
||||||
|
_isLoadingLimit = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingLimit = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkAmountLimit() {
|
||||||
|
if (_limit == null) return;
|
||||||
|
|
||||||
|
final amount = double.tryParse(_amountController.text) ?? 0;
|
||||||
|
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
|
||||||
|
final bool isOverLimit = amount > remainingLimit;
|
||||||
|
|
||||||
|
if (isOverLimit) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isAmountOverLimit != isOverLimit) {
|
||||||
|
setState(() {
|
||||||
|
_isAmountOverLimit = isOverLimit;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onProceed() {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => TransactionPinScreen(
|
||||||
|
onPinCompleted: (pinScreenContext, tpin) async {
|
||||||
|
final transfer = Transfer(
|
||||||
|
fromAccount: widget.debitAccountNo,
|
||||||
|
toAccount: widget.creditAccount.accountNo!,
|
||||||
|
toAccountType: 'Savings', // Assuming 'SB' for savings
|
||||||
|
amount: _amountController.text,
|
||||||
|
tpin: tpin,
|
||||||
|
);
|
||||||
|
|
||||||
|
final paymentService = getIt<PaymentService>();
|
||||||
|
final paymentResponseFuture =
|
||||||
|
paymentService.processQuickPayWithinBank(transfer);
|
||||||
|
|
||||||
|
Navigator.of(pinScreenContext).pushReplacement(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => PaymentAnimationScreen(
|
||||||
|
paymentResponse: paymentResponseFuture),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Fund Transfer"),
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Debit Account (User)
|
||||||
|
Text(
|
||||||
|
"Debit From",
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
|
title: Text(widget.remitterName),
|
||||||
|
subtitle: Text(widget.debitAccountNo),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Credit Account (Self)
|
||||||
|
Text(
|
||||||
|
"Credited To",
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
Card(
|
||||||
|
elevation: 0,
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: ListTile(
|
||||||
|
leading: getBankLogo(
|
||||||
|
'Kangra Central Co-operative Bank', context),
|
||||||
|
title: Text(widget.creditAccount.name ?? 'N/A'),
|
||||||
|
subtitle: Text(widget.creditAccount.accountNo ?? 'N/A'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Remarks
|
||||||
|
TextFormField(
|
||||||
|
controller: _remarksController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Remarks (Optional)",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
TextFormField(
|
||||||
|
controller: _amountController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Amount",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.currency_rupee),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return "Amount is required";
|
||||||
|
}
|
||||||
|
if (double.tryParse(value) == null ||
|
||||||
|
double.parse(value) <= 0) {
|
||||||
|
return "Please enter a valid amount";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Daily Limit Display
|
||||||
|
if (_isLoadingLimit) const Text('Fetching daily limit...'),
|
||||||
|
if (!_isLoadingLimit && _limit != null)
|
||||||
|
Text(
|
||||||
|
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
// Proceed Button
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: _isAmountOverLimit ? null : _onProceed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
),
|
||||||
|
child: const Text("Proceed"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IgnorePointer(
|
||||||
|
child: Center(
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.07, // Reduced opacity
|
||||||
|
child: ClipOval(
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/images/logo.png',
|
||||||
|
width: 200, // Adjust size as needed
|
||||||
|
height: 200, // Adjust size as needed
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -209,26 +209,22 @@ class _PaymentAnimationScreenState extends State<PaymentAnimationScreen> {
|
|||||||
onPressed: _shareScreenshot,
|
onPressed: _shareScreenshot,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.share_rounded,
|
Icons.share_rounded,
|
||||||
color: Theme.of(context).primaryColor,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
AppLocalizations.of(context).share,
|
AppLocalizations.of(context).share,
|
||||||
style: TextStyle(color: Theme.of(context).primaryColor),
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor:
|
|
||||||
Theme.of(context).scaffoldBackgroundColor,
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 32, vertical: 12),
|
horizontal: 45, vertical: 12),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(
|
|
||||||
color: Theme.of(context).primaryColor, width: 1),
|
|
||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
),
|
),
|
||||||
textStyle: const TextStyle(
|
textStyle: const TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.black,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
|
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
|
||||||
|
import 'package:kmobile/api/services/change_password_service.dart';
|
||||||
|
import 'package:kmobile/di/injection.dart';
|
||||||
|
|
||||||
class TpinOtpScreen extends StatefulWidget {
|
class TpinOtpScreen extends StatefulWidget {
|
||||||
const TpinOtpScreen({super.key});
|
final String mobileNumber;
|
||||||
|
const TpinOtpScreen({super.key, required this.mobileNumber});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TpinOtpScreen> createState() => _TpinOtpScreenState();
|
State<TpinOtpScreen> createState() => _TpinOtpScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
||||||
final List<FocusNode> _focusNodes = List.generate(4, (_) => FocusNode());
|
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
|
||||||
final List<TextEditingController> _controllers = List.generate(
|
final List<TextEditingController> _controllers = List.generate(
|
||||||
4,
|
6,
|
||||||
(_) => TextEditingController(),
|
(_) => TextEditingController(),
|
||||||
);
|
);
|
||||||
|
bool _isLoading = false;
|
||||||
|
final ChangePasswordService _changePasswordService =
|
||||||
|
getIt<ChangePasswordService>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -28,7 +34,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onOtpChanged(int idx, String value) {
|
void _onOtpChanged(int idx, String value) {
|
||||||
if (value.length == 1 && idx < 3) {
|
if (value.length == 1 && idx < 5) {
|
||||||
_focusNodes[idx + 1].requestFocus();
|
_focusNodes[idx + 1].requestFocus();
|
||||||
}
|
}
|
||||||
if (value.isEmpty && idx > 0) {
|
if (value.isEmpty && idx > 0) {
|
||||||
@@ -39,17 +45,36 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
|||||||
|
|
||||||
String get _enteredOtp => _controllers.map((c) => c.text).join();
|
String get _enteredOtp => _controllers.map((c) => c.text).join();
|
||||||
|
|
||||||
void _verifyOtp() {
|
void _verifyOtp() async {
|
||||||
if (_enteredOtp == '0000') {
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _changePasswordService.validateOtp(
|
||||||
|
otp: _enteredOtp,
|
||||||
|
mobileNumber: widget.mobileNumber,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(builder: (_) => TpinSetScreen()),
|
MaterialPageRoute(builder: (_) => const TpinSetScreen()),
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
|
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -64,7 +89,6 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
|||||||
body: Center(
|
body: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
// mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.lock_outline,
|
Icons.lock_outline,
|
||||||
@@ -90,26 +114,41 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
|||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: List.generate(4, (i) {
|
children: List.generate(6, (i) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 48,
|
width: 50,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
height: 60,
|
||||||
child: TextField(
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
controller: _controllers[i],
|
controller: _controllers[i],
|
||||||
focusNode: _focusNodes[i],
|
focusNode: _focusNodes[i],
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
maxLength: 1,
|
maxLength: 1,
|
||||||
obscureText: true,
|
style: const TextStyle(
|
||||||
obscuringCharacter: '⬤',
|
color: Colors.transparent,
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
counterText: '',
|
counterText: '',
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Theme.of(context).primaryColorLight,
|
fillColor: Colors.grey[200],
|
||||||
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: theme.colorScheme.primary,
|
color: Colors.grey[400]!,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: Colors.grey[400]!,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -123,19 +162,39 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
|||||||
),
|
),
|
||||||
onChanged: (val) => _onOtpChanged(i, val),
|
onChanged: (val) => _onOtpChanged(i, val),
|
||||||
),
|
),
|
||||||
|
if (_controllers[i].text.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black87,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.verified_user_rounded),
|
icon: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.verified_user_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
AppLocalizations.of(context).verifyOtp,
|
AppLocalizations.of(context).verifyOtp,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 18, fontWeight: FontWeight.w600),
|
fontSize: 18, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.primary,
|
backgroundColor: theme.colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 14,
|
vertical: 14,
|
||||||
horizontal: 28,
|
horizontal: 28,
|
||||||
@@ -144,12 +203,15 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
|
|||||||
borderRadius: BorderRadius.circular(30),
|
borderRadius: BorderRadius.circular(30),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: _enteredOtp.length == 4 ? _verifyOtp : null,
|
// Update onPressed to handle loading state
|
||||||
|
onPressed: (_enteredOtp.length == 6 && !_isLoading)
|
||||||
|
? _verifyOtp
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// Resend OTP logic here
|
// You can also add a getOtp call here for resending
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(AppLocalizations.of(context).otpResent),
|
content: Text(AppLocalizations.of(context).otpResent),
|
||||||
|
|||||||
@@ -1,10 +1,65 @@
|
|||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
||||||
|
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
||||||
|
|
||||||
import '../../../l10n/app_localizations.dart';
|
import '../../../l10n/app_localizations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart';
|
import 'package:kmobile/features/fund_transfer/screens/tpin_otp_screen.dart';
|
||||||
|
import 'package:kmobile/api/services/change_password_service.dart'; // <-- Add this import
|
||||||
|
import 'package:kmobile/di/injection.dart'; // <-- Add this import
|
||||||
|
|
||||||
class TpinSetupPromptScreen extends StatelessWidget {
|
class TpinSetupPromptScreen extends StatefulWidget {
|
||||||
const TpinSetupPromptScreen({super.key});
|
const TpinSetupPromptScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TpinSetupPromptScreen> createState() => _TpinSetupPromptScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> {
|
||||||
|
int selectedAccountIndex = 0;
|
||||||
|
bool _isLoading = false;
|
||||||
|
final ChangePasswordService _changePasswordService =
|
||||||
|
getIt<ChangePasswordService>();
|
||||||
|
Future<void> _getOtp() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final authState = context.read<AuthCubit>().state;
|
||||||
|
String mobileNumberToPass = '';
|
||||||
|
if (authState is Authenticated) {
|
||||||
|
if (selectedAccountIndex >= 0 &&
|
||||||
|
selectedAccountIndex < authState.users.length) {
|
||||||
|
mobileNumberToPass =
|
||||||
|
authState.users[selectedAccountIndex].mobileNo ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await _changePasswordService.getOtpTpin(mobileNumber: mobileNumberToPass);
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.pushReplacement(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => TpinOtpScreen(
|
||||||
|
mobileNumber: mobileNumberToPass,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error: ${e.toString()}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
@@ -38,14 +93,23 @@ class TpinSetupPromptScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
icon: const Icon(Icons.arrow_forward_rounded),
|
icon: _isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.arrow_forward_rounded),
|
||||||
label: Text(
|
label: Text(
|
||||||
AppLocalizations.of(context).setTpin,
|
AppLocalizations.of(context).setTpin,
|
||||||
style:
|
style:
|
||||||
const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: theme.colorScheme.primary,
|
backgroundColor: theme.colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 14,
|
vertical: 14,
|
||||||
horizontal: 32,
|
horizontal: 32,
|
||||||
@@ -54,12 +118,8 @@ class TpinSetupPromptScreen extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed:
|
||||||
Navigator.pushReplacement(
|
_isLoading ? null : _getOtp, // <-- Use the new function
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (_) => const TpinOtpScreen()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -212,4 +212,3 @@ class _TransactionPinScreenState extends State<TransactionPinScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||