25 Commits

Author SHA1 Message Date
71e0521dec kmobile logo changed 2025-11-17 16:57:46 +05:30
f6e851a9ee New KCCB logo 2025-11-17 16:06:07 +05:30
547f534037 localized Beneficiary delete 2025-11-17 15:24:58 +05:30
66b2e71140 Quick Links Screen Icon to List 2025-11-17 10:58:44 +05:30
43d92d799b Quick Links added 2025-11-14 15:24:02 +05:30
3135116f26 Branch and ATM Locator added 2025-11-14 14:36:06 +05:30
39165d631e Watermark added, Card commented out and account opening commented out 2025-11-12 15:59:41 +05:30
shital
ef481ec879 Security settings improvements 2025-11-11 14:18:37 +05:30
shital
36702b198f Fix daily limit parsing type error 2025-11-11 01:10:45 +05:30
shital
f0718e9d68 removed unused imports and blocks 2025-11-11 00:49:31 +05:30
shital
d2cce89efb Fix biometric switch and UI improvements 2025-11-11 00:44:43 +05:30
8cfca113bf dart format 2025-11-10 16:50:29 +05:30
d6f61ebb31 Cooldown Added in Beneficiary 2025-11-10 16:42:29 +05:30
078e715d20 T&C Finalized Test APK 2025-11-10 13:24:52 +05:30
5c8df8ace3 TNC Route Fixed 2025-11-10 12:39:31 +05:30
3e88aad43f T&C #1 2025-11-09 15:42:50 +05:30
5b7f3f0096 Change TPIn #5 2025-11-08 20:10:35 +05:30
b5b6c6ed49 Change TPIn #4 2025-11-08 20:07:04 +05:30
c26cc507a1 Change TPIn #3 2025-11-08 16:56:54 +05:30
87fd36b748 Change TPIn #2 2025-11-08 12:25:56 +05:30
3417c4b0e5 Change TPIN #1 2025-11-08 11:23:38 +05:30
151140d563 Fingerprint Toggle fixed 2025-11-07 17:40:56 +05:30
a8ee7833be Loan And TD funds transfer disabled 2025-11-04 15:24:57 +05:30
f73faaa635 Merge branch 'testing' of https://7o9o-lb-526275444.ap-south-1.elb.amazonaws.com/md.asif5/kmobile into testing 2025-11-04 14:56:16 +05:30
5ac977e903 Self-Transfer #1 2025-11-04 14:53:14 +05:30
95 changed files with 6167 additions and 3089 deletions

5
.gitignore vendored
View File

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

View File

@@ -22,6 +22,12 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace "com.example.kmobile"
compileSdk flutter.compileSdkVersion
@@ -51,15 +57,21 @@ android {
versionName flutterVersionName
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

View File

@@ -40,6 +40,20 @@
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. -->
<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>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/images/logo_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1006 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -141,4 +141,23 @@ class AuthService {
}
return;
}
Future setTncflag() async {
try {
final response = await _dio.post(
'/api/auth/tnc',
data: {"flag": 'Y'},
);
if (response.statusCode != 200) {
throw AuthException('Failed to proceed with T&C');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
throw NetworkException('Network error during T&C Setup');
} catch (e) {
throw UnexpectedException('Unexpected error: ${e.toString()}');
}
}
}

View File

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

View File

@@ -67,4 +67,21 @@ class ChangePasswordService {
}
return response.toString();
}
Future validateChangeTpin({
required String oldTpin,
required String newTpin,
}) async {
final response = await _dio.post(
'/api/auth/change/tpin',
data: {
'oldTpin': oldTpin,
'newTpin': newTpin,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
}

View File

@@ -13,8 +13,8 @@ class Limit {
factory Limit.fromJson(Map<String, dynamic> json) {
return Limit(
dailyLimit: json['dailyLimit']!,
usedLimit: json['usedLimit']!,
dailyLimit: (json['dailyLimit'] as num).toDouble(),
usedLimit: (json['usedLimit'] as num).toDouble(),
);
}
}

View File

@@ -316,7 +316,7 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
int _selectedIndex = 0;
final List<Widget> _pages = [
const DashboardScreen(),
const CardManagementScreen(),
// const CardManagementScreen(),
const ServiceScreen(),
];
@@ -374,10 +374,10 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
icon: const Icon(Icons.home_filled),
label: AppLocalizations.of(context).home,
),
BottomNavigationBarItem(
icon: const Icon(Icons.credit_card),
label: AppLocalizations.of(context).card,
),
// BottomNavigationBarItem(
// icon: const Icon(Icons.credit_card),
// label: AppLocalizations.of(context).card,
// ),
BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services),
label: AppLocalizations.of(context).services,

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
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/neft_service.dart';
@@ -51,6 +52,7 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
getIt.registerSingleton<BranchService>(BranchService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>(
() => ChangePasswordService(getIt<Dio>()),
);
@@ -61,8 +63,8 @@ Future<void> setupDependencies() async {
);
// Register controllers/cubits
getIt.registerFactory<AuthCubit>(
() => AuthCubit(getIt<AuthRepository>(), getIt<UserService>()));
getIt.registerFactory<AuthCubit>(() => AuthCubit(
getIt<AuthRepository>(), getIt<UserService>(), getIt<SecureStorage>()));
}
Dio _createDioClient() {

View File

@@ -34,12 +34,15 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
.accountInfo
.replaceFirst(RegExp('\n'), '')),
),
body: ListView(
body: Stack(
children: [
ListView(
padding: const EdgeInsets.all(16.0),
children: [
Text(
AppLocalizations.of(context).accountNumber,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
style:
const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
),
DropdownButton<User>(
@@ -89,6 +92,22 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
: 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
),
),
),
),
),
],
),
);
}
}

View File

@@ -133,7 +133,9 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -147,7 +149,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
fontWeight: FontWeight.bold,
),
),
Text(widget.accountNo, style: const TextStyle(fontSize: 17)),
Text(widget.accountNo,
style: const TextStyle(fontSize: 17)),
],
),
const SizedBox(height: 15),
@@ -247,7 +250,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
child: Container(
height: 10,
width: 100,
color: Theme.of(context).scaffoldBackgroundColor,
color:
Theme.of(context).scaffoldBackgroundColor,
),
),
subtitle: Shimmer.fromColors(
@@ -256,7 +260,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
child: Container(
height: 8,
width: 60,
color: Theme.of(context).scaffoldBackgroundColor,
color:
Theme.of(context).scaffoldBackgroundColor,
),
),
),
@@ -267,7 +272,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
AppLocalizations.of(context).noTransactions,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.onSurface,
color:
Theme.of(context).colorScheme.onSurface,
)),
)
: ListView.separated(
@@ -306,7 +312,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
Text(
"Bal: ₹${tx.balance}",
style: const TextStyle(
fontSize: 12), // Style matches tx.name
fontSize:
12), // Style matches tx.name
),
],
),
@@ -314,7 +321,8 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => TransactionDetailsScreen(
builder: (_) =>
TransactionDetailsScreen(
transaction: tx),
),
);
@@ -329,6 +337,22 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
],
),
),
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();

View File

@@ -14,7 +14,9 @@ class TransactionDetailsScreen extends StatelessWidget {
return Scaffold(
appBar:
AppBar(title: Text(AppLocalizations.of(context).transactionDetails)),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
@@ -37,7 +39,9 @@ class TransactionDetailsScreen extends StatelessWidget {
),
const SizedBox(width: 8),
Icon(
isCredit ? Symbols.call_received : Symbols.call_made,
isCredit
? Symbols.call_received
: Symbols.call_made,
color: isCredit ? Colors.green : Colors.red,
size: 28,
),
@@ -62,7 +66,8 @@ class TransactionDetailsScreen extends StatelessWidget {
flex: 5,
child: ListView(
children: [
_buildDetailRow(AppLocalizations.of(context).transactionType,
_buildDetailRow(
AppLocalizations.of(context).transactionType,
transaction.type ?? ""),
_buildDetailRow(AppLocalizations.of(context).transferType,
transaction.name.split("/").first ?? ""),
@@ -73,14 +78,30 @@ class TransactionDetailsScreen extends StatelessWidget {
// AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "")
// ]
_buildDetailRow(
AppLocalizations.of(context).details, transaction.name),
_buildDetailRow(AppLocalizations.of(context).details,
transaction.name),
],
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}

View File

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

View File

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

View File

@@ -6,16 +6,31 @@ import 'package:equatable/equatable.dart';
class AuthToken extends Equatable {
final String accessToken;
final DateTime expiresAt;
final bool tnc;
const AuthToken({
required this.accessToken,
required this.expiresAt,
required this.tnc,
});
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(
accessToken: json['token'],
expiresAt: _decodeExpiryFromToken(json['token']),
accessToken: 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);
@override
List<Object> get props => [accessToken, expiresAt];
List<Object> get props => [accessToken, expiresAt, tnc];
}

View File

@@ -1,12 +1,11 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/set_password_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../app.dart';
import 'package:kmobile/features/auth/screens/tnc_required_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_state.dart';
@@ -23,7 +22,6 @@ class LoginScreenState extends State<LoginScreen>
final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
//bool _showWelcome = true;
@override
void dispose() {
@@ -43,37 +41,238 @@ class LoginScreenState extends State<LoginScreen>
@override
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(
// appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) async {
if (state is Authenticated) {
final storage = getIt<SecureStorage>();
final mpin = await storage.read('mpin');
if (!context.mounted) return;
if (mpin == null) {
Navigator.of(context).pushReplacement(
listener: (context, state) {
if (state is ShowTncDialog) {
showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => TncDialog(
onProceed: () async {
// Pop the dialog before the cubit action
Navigator.of(dialogContext).pop();
await context
.read<AuthCubit>()
.onTncDialogResult(true, state.authToken, state.users);
},
),
);
} else if (state is NavigateToTncRequiredScreen) {
Navigator.of(context).pushNamed(TncRequiredScreen.routeName);
} else if (state is NavigateToMpinSetupScreen) {
Navigator.of(context).push(
// Use push, NOT pushReplacement
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(
context,
rootNavigator: true,
).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
// Call the cubit to signal MPIN setup is complete
context.read<AuthCubit>().mpinSetupCompleted();
},
),
),
);
} else {
Navigator.of(context).pushReplacement(
} else if (state is Authenticated) {
// This is the single source of truth for navigating to the dashboard
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
}
} else if (state is AuthError) {
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
Navigator.of(context).push(MaterialPageRoute(
@@ -87,6 +286,7 @@ class LoginScreenState extends State<LoginScreen>
}
},
builder: (context, state) {
// The builder part remains largely the same, focusing on UI display
return Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
@@ -107,7 +307,6 @@ class LoginScreenState extends State<LoginScreen>
},
),
const SizedBox(height: 16),
// Title
Text(
AppLocalizations.of(context).kccb,
style: TextStyle(
@@ -117,12 +316,10 @@ class LoginScreenState extends State<LoginScreen>
),
),
const SizedBox(height: 48),
TextFormField(
controller: _customerNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).customerNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
@@ -147,7 +344,6 @@ class LoginScreenState extends State<LoginScreen>
},
),
const SizedBox(height: 24),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
@@ -189,7 +385,6 @@ class LoginScreenState extends State<LoginScreen>
},
),
const SizedBox(height: 24),
//Login Button
SizedBox(
width: 250,
child: ElevatedButton(
@@ -216,40 +411,7 @@ class LoginScreenState extends State<LoginScreen>
),
),
),
const SizedBox(height: 15),
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 16),
// child: Row(
// children: [
// const Expanded(child: Divider()),
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 8),
// child: Text(AppLocalizations.of(context).or),
// ),
// //const Expanded(child: Divider()),
// ],
// ),
// ),
const SizedBox(height: 25),
// Register Button
// SizedBox(
// width: 250,
// child: ElevatedButton(
// //disable until registration is implemented
// onPressed: null,
// style: OutlinedButton.styleFrom(
// shape: const StadiumBorder(),
// padding: const EdgeInsets.symmetric(vertical: 16),
// backgroundColor: Theme.of(context).colorScheme.primary,
// foregroundColor: Theme.of(context).colorScheme.onPrimary,
// ),
// child: Text(AppLocalizations.of(context).register,
// style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),),
// ),
// ),
],
),
),

View File

@@ -4,7 +4,6 @@ import 'dart:math';
// import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:kmobile/app.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart';
@@ -16,12 +15,18 @@ class MPinScreen extends StatefulWidget {
final MPinMode mode;
final String? initialPin;
final void Function(String pin)? onCompleted;
final bool disableBiometric;
final String? customTitle;
final String? customConfirmTitle;
const MPinScreen({
super.key,
required this.mode,
this.initialPin,
this.onCompleted,
this.disableBiometric = false,
this.customTitle,
this.customConfirmTitle,
});
@override
@@ -78,7 +83,7 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
CurvedAnimation(parent: _waveController, curve: Curves.easeInOut),
);
if (widget.mode == MPinMode.enter) {
if (widget.mode == MPinMode.enter && !widget.disableBiometric) {
_tryBiometricBeforePin();
}
}
@@ -173,29 +178,36 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
}
break;
case MPinMode.set:
// propagate parent onCompleted into confirm step
Navigator.push(
// Navigate to confirm and wait for result
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.confirm,
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;
case MPinMode.confirm:
if (widget.initialPin == pin) {
// 1) persist the 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) {
Navigator.of(context, rootNavigator: true).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const NavigationScaffold()),
(route) => false,
);
widget.onCompleted?.call(pin);
}
} else {
setState(() {
@@ -339,6 +351,9 @@ class _MPinScreenState extends State<MPinScreen> with TickerProviderStateMixin {
}
String getTitle() {
if (widget.customTitle != null) {
return widget.customTitle!;
}
switch (widget.mode) {
case MPinMode.enter:
return AppLocalizations.of(context).enterMPIN;

View 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
),
),
),
),
),
],
),
);
}
}

View File

@@ -264,7 +264,9 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
centerTitle: false,
),
body: SafeArea(
child: Form(
child: Stack(
children: [
Form(
key: _formKey,
child: Column(
children: [
@@ -343,7 +345,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
LengthLimitingTextInputFormatter(11),
],
decoration: InputDecoration(
labelText: AppLocalizations.of(context).ifscCode,
labelText:
AppLocalizations.of(context).ifscCode,
border: const OutlineInputBorder(),
isDense: true,
),
@@ -360,7 +363,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
});
},
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) {
return AppLocalizations.of(context).enterIfsc;
} else if (!pattern.hasMatch(
@@ -377,9 +381,11 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
// Bank Name (Disabled)
TextFormField(
controller: bankNameController,
enabled: false, // changed from readOnly to disabled
enabled:
false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText: AppLocalizations.of(context).bankName,
labelText:
AppLocalizations.of(context).bankName,
border: const OutlineInputBorder(),
isDense: true,
),
@@ -388,9 +394,11 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
// 🔹 Branch Name (Disabled)
TextFormField(
controller: branchNameController,
enabled: false, // changed from readOnly to disabled
enabled:
false, // changed from readOnly to disabled
decoration: InputDecoration(
labelText: AppLocalizations.of(context).branchName,
labelText:
AppLocalizations.of(context).branchName,
border: const OutlineInputBorder(),
isDense: true,
),
@@ -418,9 +426,10 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
isDense: true,
),
textInputAction: TextInputAction.next,
validator: (value) => value == null ||
value.isEmpty
? AppLocalizations.of(context).nameRequired
validator: (value) =>
value == null || value.isEmpty
? AppLocalizations.of(context)
.nameRequired
: null,
),
],
@@ -437,7 +446,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
? null
: () {
final isAccountValid =
_accountNumberFieldKey.currentState!
_accountNumberFieldKey
.currentState!
.validate();
final isConfirmAccountValid =
_confirmAccountNumberFieldKey
@@ -470,7 +480,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
DropdownButtonFormField<String>(
value: accountType,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).accountType,
labelText:
AppLocalizations.of(context).accountType,
border: const OutlineInputBorder(),
isDense: true,
),
@@ -503,8 +514,8 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
isDense: true,
),
textInputAction: TextInputAction.done,
validator: (value) =>
value == null || value.length != 10
validator: (value) => value == null ||
value.length != 10
? AppLocalizations.of(context).enterValidPhone
: null,
),
@@ -525,8 +536,9 @@ class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer),
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer),
child: Text(
AppLocalizations.of(context).validateAndAdd,
style: const TextStyle(fontSize: 16),
@@ -537,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
),
),
),
),
),
],
),
),
);
}

View File

@@ -22,7 +22,7 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
_showSuccessDialog(context);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to delete beneficiary: $e')),
SnackBar(content: Text('${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
);
}
}
@@ -32,11 +32,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Success'),
content: const Text('Beneficiary deleted successfully.'),
title: Text(AppLocalizations.of(context).success),
content: Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[
TextButton(
child: const Text('OK'),
child: Text(AppLocalizations.of(context).ok),
onPressed: () {
Navigator.of(context).popUntil((route) => route.isFirst);
},
@@ -52,18 +52,18 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Delete Beneficiary'),
title: Text(AppLocalizations.of(context).deleteBeneficiary),
content:
const Text('Are you sure you want to delete this beneficiary?'),
Text(AppLocalizations.of(context).areYouSureYouWantToDeleteThisBeneficiary),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
child: Text(AppLocalizations.of(context).cancel),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Delete'),
child: Text(AppLocalizations.of(context).delete),
onPressed: () {
_deleteBeneficiary(context);
},
@@ -81,7 +81,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
title: Text(AppLocalizations.of(context).beneficiarydetails),
),
body: SafeArea(
child: Padding(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -104,9 +106,11 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
const SizedBox(height: 24),
_buildDetailRow('${AppLocalizations.of(context).bankName} ',
beneficiary.bankName ?? 'N/A'),
_buildDetailRow('${AppLocalizations.of(context).accountNumber} ',
_buildDetailRow(
'${AppLocalizations.of(context).accountNumber} ',
beneficiary.accountNo),
_buildDetailRow('${AppLocalizations.of(context).accountType} ',
_buildDetailRow(
'${AppLocalizations.of(context).accountType} ',
beneficiary.accountType),
_buildDetailRow('${AppLocalizations.of(context).ifscCode} ',
beneficiary.ifscCode),
@@ -136,6 +140,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
),
),
),
),
),
],
),
),
);
}

View File

@@ -109,7 +109,25 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries),
),
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
body: Stack(
children: [
_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(
padding: const EdgeInsets.only(bottom: 8.0),
child: FloatingActionButton(

View File

@@ -61,7 +61,9 @@ class _BlockCardScreen extends State<BlockCardScreen> {
),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
@@ -100,18 +102,21 @@ class _BlockCardScreen extends State<BlockCardScreen> {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
borderSide:
BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
obscureText: true,
validator: (value) => value != null && value.length == 3
validator: (value) =>
value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
),
@@ -128,15 +133,18 @@ class _BlockCardScreen extends State<BlockCardScreen> {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
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
: AppLocalizations.of(context).selectExpiryDate,
),
@@ -188,6 +196,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
),
),
),
),
),
],
),
);
}
}

View File

@@ -9,7 +9,9 @@ class CardDetailsScreen extends StatelessWidget {
appBar: AppBar(
title: const Text("My Cards"),
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: const [
@@ -31,6 +33,22 @@ class CardDetailsScreen extends StatelessWidget {
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -25,7 +25,9 @@ class _CardManagementScreen extends State<CardManagementScreen> {
),
centerTitle: false,
),
body: ListView(
body: Stack(
children: [
ListView(
children: [
CardManagementTile(
icon: Symbols.add,
@@ -78,6 +80,22 @@ class _CardManagementScreen extends State<CardManagementScreen> {
const Divider(height: 1),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -51,7 +51,9 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(10.0),
child: Form(
key: _formKey,
@@ -90,18 +92,21 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2),
borderSide:
BorderSide(color: Colors.black, width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
obscureText: true,
validator: (value) => value != null && value.length == 3
validator: (value) =>
value != null && value.length == 3
? null
: AppLocalizations.of(context).cvv3Digits,
),
@@ -118,15 +123,18 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black),
),
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
: AppLocalizations.of(context).selectExpiryDate,
),
@@ -178,6 +186,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
),
),
),
),
),
],
),
);
}
}

View File

@@ -51,7 +51,9 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
@@ -133,6 +135,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
),
),
),
),
),
],
),
);
}
}

View File

@@ -22,7 +22,9 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
),
centerTitle: false,
),
body: ListView(
body: Stack(
children: [
ListView(
children: [
const SizedBox(height: 15),
ChequeManagementTile(
@@ -37,7 +39,8 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const EnquiryScreen()),
MaterialPageRoute(
builder: (context) => const EnquiryScreen()),
);
},
),
@@ -68,6 +71,22 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
const Divider(height: 1),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -33,7 +33,9 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
.replaceFirst(RegExp('\n'), ''),
),
),
body: SingleChildScrollView(
body: Stack(
children: [
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
@@ -102,6 +104,22 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -15,6 +15,7 @@ import 'package:kmobile/features/enquiry/screens/enquiry_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/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:local_auth/local_auth.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
@@ -147,6 +148,8 @@ class _DashboardScreenState extends State<DashboardScreen>
return AppLocalizations.of(context).termDeposit;
case 'rd':
return AppLocalizations.of(context).recurringDeposit;
case 'ca':
return "Current Account";
default:
return AppLocalizations.of(context).unknownAccount;
}
@@ -266,6 +269,10 @@ class _DashboardScreenState extends State<DashboardScreen>
if (state is Authenticated) {
final users = state.users;
final currAccount = users[selectedAccountIndex];
final accountType = currAccount.accountType?.toLowerCase();
final isPaymentDisabled = accountType != 'sa' &&
accountType != 'sb' &&
accountType != 'ca';
// firsttime load
if (!_txInitialized) {
_txInitialized = true;
@@ -493,6 +500,7 @@ class _DashboardScreenState extends State<DashboardScreen>
),
);
},
disable: isPaymentDisabled,
),
_buildQuickLink(Symbols.send_money,
AppLocalizations.of(context).fundTransfer, () {
@@ -504,9 +512,10 @@ class _DashboardScreenState extends State<DashboardScreen>
users[selectedAccountIndex]
.accountNo!,
remitterName:
users[selectedAccountIndex]
.name!)));
}, disable: false),
users[selectedAccountIndex].name!,
// Pass the full list of accounts
accounts: users)));
}, disable: isPaymentDisabled),
_buildQuickLink(
Symbols.server_person,
AppLocalizations.of(context).accountInfo,
@@ -539,9 +548,14 @@ class _DashboardScreenState extends State<DashboardScreen>
.accountType!,
)));
}),
_buildQuickLink(Symbols.checkbook,
AppLocalizations.of(context).handleCheque, () {},
disable: true),
_buildQuickLink(Icons.location_pin, "Branch Locator",
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
const BranchLocatorScreen()));
}, disable: false),
_buildQuickLink(Icons.group,
AppLocalizations.of(context).manageBeneficiary,
() {

View File

@@ -1,5 +1,3 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../l10n/app_localizations.dart';
@@ -12,31 +10,45 @@ class EnquiryScreen extends StatefulWidget {
}
class _EnquiryScreen extends State<EnquiryScreen> {
// Updated to launch externally and pre-fill the subject
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)) {
await launchUrl(emailUri);
// Use external application mode
await launchUrl(emailUri, mode: LaunchMode.externalApplication);
} 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 {
final Uri phoneUri = Uri(scheme: 'tel', path: phone);
if (await canLaunchUrl(phoneUri)) {
await launchUrl(phoneUri);
} 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)) {
await launchUrl(uri);
// Use external application mode
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
// Consider adding a 'urlLaunchError' key to your AppLocalizations
debugPrint('Could not launch $url');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not launch $url')),
);
}
}
@@ -57,7 +69,7 @@ class _EnquiryScreen extends State<EnquiryScreen> {
onTap: () => _launchPhoneNumber(phone),
child: Text(phone,
style:
TextStyle(color: Theme.of(context).scaffoldBackgroundColor)),
TextStyle(color: Theme.of(context).colorScheme.primary)), // Changed color for visibility
),
],
);
@@ -70,21 +82,25 @@ class _EnquiryScreen extends State<EnquiryScreen> {
title: Text(AppLocalizations.of(context).enquiry),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
GestureDetector(
onTap: () => _launchUrl("https://kccb.in/complaint-form"),
onTap: () => _launchUrl("https://kccbhp.bank.in/complaint-form/"),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(
"Complaint Form",
style: TextStyle(
fontSize: 17,
color: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary,
decoration: TextDecoration.underline, // Added underline for link clarity
decorationColor:
Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 4),
@@ -131,6 +147,22 @@ class _EnquiryScreen extends State<EnquiryScreen> {
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -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()],
),
),
],
);
}
}

View File

@@ -58,6 +58,7 @@ void initState() {
_loadLimit(); // Call the new method
_amountController.addListener(_checkAmountLimit);
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
@@ -87,7 +88,8 @@ void _checkAmountLimit() {
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
@@ -360,7 +362,9 @@ void _checkAmountLimit() {
title: Text(loc.fundTransfer.replaceFirst(RegExp('\n'), '')),
),
body: SafeArea(
child: Padding(
child: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
@@ -396,8 +400,8 @@ void _checkAmountLimit() {
elevation: 0,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: ListTile(
leading:
getBankLogo(widget.creditBeneficiary.bankName, context),
leading: getBankLogo(
widget.creditBeneficiary.bankName, context),
title: Text(widget.creditBeneficiary.name),
subtitle: Text(widget.creditBeneficiary.accountNo),
),
@@ -429,7 +433,8 @@ void _checkAmountLimit() {
});
},
borderRadius: BorderRadius.circular(10),
selectedColor: Theme.of(context).colorScheme.onPrimary,
selectedColor:
Theme.of(context).colorScheme.onPrimary,
fillColor: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.onSurface,
borderColor: Colors.transparent,
@@ -487,8 +492,7 @@ void _checkAmountLimit() {
},
),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (_isLoadingLimit) Text(AppLocalizations.of(context).fetchingDailyLimit),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
@@ -512,6 +516,22 @@ void _checkAmountLimit() {
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
),
);
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:kmobile/features/fund_transfer/screens/cooldown.dart';
import 'package:kmobile/widgets/bank_logos.dart';
import 'package:kmobile/data/models/beneficiary.dart';
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_amount_screen.dart';
@@ -81,7 +82,22 @@ class _FundTransferBeneficiaryScreenState
itemCount: _beneficiaries.length,
itemBuilder: (context, index) {
final beneficiary = _beneficiaries[index];
return ListTile(
// --- Cooldown Logic ---
bool isCoolingDown = false;
if (beneficiary.createdAt != null) {
final sixtyMinutesAgo =
DateTime.now().subtract(const Duration(minutes: 60));
isCoolingDown = beneficiary.createdAt!.isAfter(sixtyMinutesAgo);
}
// --- End of Cooldown Logic ---
// By wrapping the ListTile in an Opacity widget, we can make it look
// disabled while ensuring the onTap callback still works.
return Opacity(
opacity: isCoolingDown ? 0.5 : 1.0,
child: ListTile(
// REMOVED the 'enabled' property from here.
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
@@ -100,7 +116,25 @@ class _FundTransferBeneficiaryScreenState
),
],
),
trailing: isCoolingDown
? CooldownTimer(
createdAt: beneficiary.createdAt!,
onTimerFinish: () {
setState(() {});
},
)
: null,
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(
context,
MaterialPageRoute(
@@ -112,7 +146,9 @@ class _FundTransferBeneficiaryScreenState
),
),
);
}
},
),
);
},
);
@@ -124,7 +160,25 @@ class _FundTransferBeneficiaryScreenState
appBar: AppBar(
title: Text(AppLocalizations.of(context).beneficiaries),
),
body: _isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
body: Stack(
children: [
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -1,30 +1,65 @@
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_self_accounts_screen.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../l10n/app_localizations.dart';
import '../../../l10n/app_localizations.dart'; // Keep localizations
class FundTransferScreen extends StatelessWidget {
final String creditAccountNo;
final String remitterName;
final List<User> accounts; // Continue to accept the list of accounts
const FundTransferScreen({
super.key,
required this.creditAccountNo,
required this.remitterName,
required this.accounts, // It is passed from the dashboard
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Restore localization for the title
title: Text(AppLocalizations.of(context)
.fundTransfer
.replaceFirst(RegExp('\n'), '')),
),
body: ListView(
// Wrap with BlocBuilder to check the authentication state
body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
return Stack(
children: [
ListView(
children: [
FundTransferManagementTile(
icon: Symbols.person,
// Restore localization for the label
label: "Self Pay",
onTap: () {
// The accounts list is passed directly from the constructor
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferSelfAccountsScreen(
debitAccountNo: creditAccountNo,
remitterName: remitterName,
accounts: accounts,
),
),
);
},
// Disable the tile if the state is not Authenticated
disable: state is! Authenticated,
),
const Divider(height: 1),
FundTransferManagementTile(
icon: Symbols.input_circle,
// Restore localization for the label
label: AppLocalizations.of(context).ownBank,
onTap: () {
Navigator.push(
@@ -42,6 +77,7 @@ class FundTransferScreen extends StatelessWidget {
const Divider(height: 1),
FundTransferManagementTile(
icon: Symbols.output_circle,
// Restore localization for the label
label: AppLocalizations.of(context).outsideBank,
onTap: () {
Navigator.push(
@@ -59,6 +95,24 @@ 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
),
),
),
),
),
],
);
},
),
);
}
}

View File

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

View File

@@ -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
),
),
),
),
),
],
),
),
);
}
}

View File

@@ -116,24 +116,39 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(6, (i) {
return Container(
width: 32,
margin: const EdgeInsets.symmetric(horizontal: 8),
child: TextField(
width: 50,
height: 60,
margin: const EdgeInsets.symmetric(horizontal: 6),
child: Stack(
alignment: Alignment.center,
children: [
TextField(
controller: _controllers[i],
focusNode: _focusNodes[i],
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
maxLength: 1,
obscureText: true,
obscuringCharacter: '*',
style: const TextStyle(
color: Colors.transparent,
fontSize: 24,
),
decoration: InputDecoration(
counterText: '',
filled: true,
fillColor: Theme.of(context).primaryColorLight,
fillColor: Colors.grey[200],
contentPadding:
const EdgeInsets.symmetric(vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
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,
),
),
@@ -147,6 +162,17 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
),
onChanged: (val) => _onOtpChanged(i, val),
),
if (_controllers[i].text.isNotEmpty)
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.black87,
shape: BoxShape.circle,
),
),
],
),
);
}),
),

View File

@@ -143,6 +143,20 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
),

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:kmobile/di/injection.dart';
import '../../l10n/app_localizations.dart';
class ChangeMpinScreen extends StatefulWidget {
const ChangeMpinScreen({super.key});
@override
State<ChangeMpinScreen> createState() => _ChangeMpinScreenState();
}
class _ChangeMpinScreenState extends State<ChangeMpinScreen> {
@override
void initState() {
super.initState();
// Start the flow after the widget is built
WidgetsBinding.instance.addPostFrameCallback((_) {
_startChangeMpin();
});
}
void _startChangeMpin() async {
final loc = AppLocalizations.of(context);
// Step 1: Verify old PIN
final oldPinVerified = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.enter,
disableBiometric: true,
customTitle: loc.enterOldMpin,
onCompleted: (oldPin) => _verifyOldPin(oldPin),
),
),
);
if (oldPinVerified != true) {
if (mounted) Navigator.of(context).pop(false);
return;
}
// Step 2 & 3: Set new PIN (which will internally navigate to confirm)
// The onCompleted will be called after both set and confirm succeed
final success = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
customTitle: loc.enterNewMpin,
customConfirmTitle: loc.confirmNewMpin,
onCompleted: (newPin) async {
// This is called after confirm succeeds and PIN is saved
if (context.mounted) {
Navigator.of(context).pop(true);
}
},
),
),
);
if (mounted) {
Navigator.of(context).pop(success == true);
}
}
Future<void> _verifyOldPin(String oldPin) async {
final storage = getIt<SecureStorage>();
final storedPin = await storage.read('mpin');
if (storedPin == int.tryParse(oldPin)) {
// Old PIN is correct
if (mounted) {
Navigator.of(context).pop(true);
}
} else {
// This shouldn't happen as MPinScreen handles validation
if (mounted) {
Navigator.of(context).pop(false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).changeMpin),
centerTitle: true,
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
}

View File

@@ -70,7 +70,9 @@ class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
@@ -105,6 +107,22 @@ class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -90,7 +90,9 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
return Scaffold(
appBar:
AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Form(
key: _formKey,
@@ -121,8 +123,8 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
icon: Icon(_showNewPassword
? Icons.visibility
: Icons.visibility_off),
onPressed: () =>
setState(() => _showNewPassword = !_showNewPassword),
onPressed: () => setState(
() => _showNewPassword = !_showNewPassword),
),
),
validator: validateNewPassword,
@@ -152,6 +154,22 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -81,7 +81,8 @@ Future<void> _showAddOrEditLimitDialog() async {
if (value > 200000) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("Limit To be Set must be less than 200000"),
content: const Text(
"Limit To be Set must be less than 200000"),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.error,
),
@@ -110,13 +111,6 @@ Future<void> _showAddOrEditLimitDialog() async {
}
}
void _removeLimit() {
setState(() {
_currentLimit = null;
});
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
@@ -135,7 +129,8 @@ Future<void> _showAddOrEditLimitDialog() async {
final localizations = AppLocalizations.of(context);
final theme = Theme.of(context);
final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final remainingLimit = _currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
final remainingLimit =
_currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
return Scaffold(
appBar: AppBar(

View File

@@ -20,7 +20,9 @@ class PreferenceScreen extends StatelessWidget {
),
body: BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) {
return ListView(
return Stack(
children: [
ListView(
children: [
//Set Prefered Username
// ListTile(
@@ -58,6 +60,22 @@ class PreferenceScreen extends StatelessWidget {
);
}),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
);
},
),

View File

@@ -2,9 +2,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:kmobile/data/repositories/auth_repository.dart';
import 'package:kmobile/features/profile/change_password/change_password_screen.dart';
import 'package:kmobile/features/profile/daily_transaction_limit.dart';
import 'package:kmobile/features/profile/logout_dialog.dart';
import 'package:kmobile/features/profile/security_settings_screen.dart';
import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -36,9 +36,9 @@ class _ProfileScreenState extends State<ProfileScreen> {
Future<void> _loadBiometricStatus() async {
final storage = getIt<SecureStorage>();
final isEnabled = await storage.read('biometric_enabled');
final enabled = await storage.read('biometric_enabled');
setState(() {
_isBiometricEnabled = isEnabled == 'true';
_isBiometricEnabled = enabled ?? false;
});
}
@@ -57,16 +57,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) {
// Optional: Show a snackbar or dialog if biometrics are not available
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).biometricsNotAvailable)),
content:
Text(AppLocalizations.of(context).biometricsNotAvailable)),
);
}
return;
}
if (enable) {
// Show "Enable" dialog
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -96,17 +97,31 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
);
if (didAuth) {
await storage.write('biometric_enabled', 'true');
await storage.write('biometric_enabled', true);
if (mounted) {
setState(() {
_isBiometricEnabled = true;
});
}
} else {
// Authentication failed, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
}
} catch (e) {
// Handle authentication errors
// Handle exceptions, reload state to ensure consistency
if (mounted) {
await _loadBiometricStatus();
}
}
} else {
// User cancelled, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
}
} else {
// Show "Disable" dialog
final optOut = await showDialog<bool>(
context: context,
barrierDismissible: false,
@@ -127,11 +142,18 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
if (optOut == true) {
await storage.write('biometric_enabled', 'false');
await storage.write('biometric_enabled', false);
if (mounted) {
setState(() {
_isBiometricEnabled = false;
});
}
} else {
// User cancelled, reload state to refresh UI
if (mounted) {
await _loadBiometricStatus();
}
}
}
}
@@ -143,11 +165,14 @@ class _ProfileScreenState extends State<ProfileScreen> {
appBar: AppBar(
title: Text(loc.profile), // Localized "Profile"
),
body: ListView(
body: Stack(
children: [
ListView(
children: [
ListTile(
leading: const Icon(Icons.settings),
title: Text(loc.preferences),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
@@ -156,6 +181,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
},
),
ListTile(
leading: const Icon(Icons.security),
title: Text(loc.securitySettings),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SecuritySettingsScreen(
mobileNumber: widget.mobileNumber,
),
),
);
},
),
ListTile(
leading: const Icon(Icons.currency_rupee),
title: Text(AppLocalizations.of(context).dailylimit),
@@ -168,44 +208,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
},
),
SwitchListTile(
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
title:
Text(AppLocalizations.of(context).enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
// The state is now managed within _handleBiometricToggle
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
),
ListTile(
leading: const Icon(Icons.password),
title: Text(loc.changeLoginPassword),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordScreen(
mobileNumber: widget.mobileNumber,
)),
);
},
),
// ListTile(
// leading: const Icon(Icons.password),
// title: const Text("Manage TPIN"),
// onTap: () async {
// },
// ),
// ListTile(
// leading: const Icon(Icons.password),
// title: const Text("Change Login MPIN"),
// onTap: () async {
// },
// ),
ListTile(
leading: const Icon(Icons.smartphone),
title: const Text("App Version"),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Show a loading indicator while waiting for the future to complete
return const CircularProgressIndicator();
@@ -267,6 +285,22 @@ class _ProfileScreenState extends State<ProfileScreen> {
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:kmobile/features/profile/change_password/change_password_screen.dart';
import 'package:kmobile/features/profile/tpin/change_tpin_screen.dart';
import 'package:kmobile/features/profile/change_mpin_screen.dart';
import 'package:kmobile/api/services/auth_service.dart';
import 'package:kmobile/features/fund_transfer/screens/tpin_set_screen.dart';
import 'package:kmobile/di/injection.dart';
import '../../l10n/app_localizations.dart';
class SecuritySettingsScreen extends StatelessWidget {
final String mobileNumber;
const SecuritySettingsScreen({super.key, required this.mobileNumber});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.securitySettings),
centerTitle: true,
),
body: Stack(
children: [
ListView(
children: [
ListTile(
leading: const Icon(Icons.lock_outline),
title: Text(loc.changeLoginPassword),
trailing: const Icon(Icons.chevron_right),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChangePasswordScreen(
mobileNumber: mobileNumber,
),
),
);
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.pin),
title: Text(loc.changeMpin),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChangeMpinScreen(),
),
);
if (result == true && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(loc.mpinChangedSuccessfully),
backgroundColor: Colors.green,
),
);
}
},
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.password),
title: const Text('Change TPIN'),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final authService = getIt<AuthService>();
final isTpinSet = await authService.checkTpin();
if (!isTpinSet) {
if (context.mounted) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('TPIN Not Set'),
content: const Text(
'You have not set a TPIN yet. Please set a TPIN to proceed.'),
actions: <Widget>[
TextButton(
child: const Text('Back'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Proceed'),
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const TpinSetScreen(),
),
);
},
),
],
);
},
);
}
} else {
if (context.mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeTpinScreen(mobileNumber: mobileNumber),
),
);
}
}
},
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/widgets/pin_input_field.dart';
import '../../../api/services/change_password_service.dart';
class ChangeTpinOtpScreen extends StatefulWidget {
final String oldTpin;
final String newTpin;
final String mobileNumber;
const ChangeTpinOtpScreen({
super.key,
required this.oldTpin,
required this.newTpin,
required this.mobileNumber,
});
@override
State<ChangeTpinOtpScreen> createState() => _ChangeTpinOtpScreenState();
}
class _ChangeTpinOtpScreenState extends State<ChangeTpinOtpScreen> {
final _otpController = TextEditingController();
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
bool _isLoading = false;
void _handleVerifyOtp() async {
if (_otpController.text.length != 6) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please enter a valid 6-digit OTP')),
);
return;
}
setState(() {
_isLoading = true;
});
try {
// 1. Validate the OTP first.
await _changePasswordService.validateOtp(
otp: _otpController.text,
mobileNumber: widget.mobileNumber,
);
// 2. If OTP is valid, then call validateChangeTpin.
await _changePasswordService.validateChangeTpin(
oldTpin: widget.oldTpin,
newTpin: widget.newTpin,
);
// 3. Show success message.
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('TPIN changed successfully!'),
backgroundColor: Colors.green,
),
);
// 4. Navigate back to the profile screen or home.
Navigator.of(context).popUntil((route) => route.isFirst);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('An error occurred: $e'),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Verify OTP'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 24),
const Text(
'Enter the OTP sent to your registered mobile number.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16),
),
const SizedBox(height: 32),
PinInputField(
controller: _otpController,
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleVerifyOtp,
child: _isLoading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: const Text('Verify & Change TPIN'),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/profile/tpin/change_tpin_otp_screen.dart';
import 'package:kmobile/widgets/pin_input_field.dart';
import '../../../api/services/change_password_service.dart';
class ChangeTpinScreen extends StatefulWidget {
final String mobileNumber;
const ChangeTpinScreen({super.key, required this.mobileNumber});
@override
State<ChangeTpinScreen> createState() => _ChangeTpinScreenState();
}
class _ChangeTpinScreenState extends State<ChangeTpinScreen> {
final _formKey = GlobalKey<FormState>();
final _oldTpinController = TextEditingController();
final _newTpinController = TextEditingController();
final _confirmTpinController = TextEditingController();
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
bool _isLoading = false;
@override
void dispose() {
_oldTpinController.dispose();
_newTpinController.dispose();
_confirmTpinController.dispose();
super.dispose();
}
void _handleChangeTpin() async {
if (_formKey.currentState!.validate()) {
setState(() {
_isLoading = true;
});
try {
// 1. Get OTP for TPIN change.
await _changePasswordService.getOtpTpin(
mobileNumber: widget.mobileNumber);
// 2. Navigate to the OTP screen on success.
if (mounted) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeTpinOtpScreen(
oldTpin: _oldTpinController.text,
newTpin: _newTpinController.text,
mobileNumber: widget.mobileNumber,
),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to send OTP: $e')),
);
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Change TPIN'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Current TPIN'),
const SizedBox(height: 8),
PinInputField(
controller: _oldTpinController,
validator: (value) {
if (value == null || value.length != 6) {
return 'Please enter your 6-digit old TPIN';
}
return null;
},
),
const SizedBox(height: 24),
const Text('New TPIN'),
const SizedBox(height: 8),
PinInputField(
controller: _newTpinController,
validator: (value) {
if (value == null || value.length != 6) {
return 'Please enter a 6-digit new TPIN';
}
if (value == _oldTpinController.text) {
return 'New TPIN must be different from the old one.';
}
return null;
},
),
const SizedBox(height: 24),
const Text('Confirm New TPIN'),
const SizedBox(height: 8),
PinInputField(
controller: _confirmTpinController,
validator: (value) {
if (value == null || value.length != 6) {
return 'Please confirm your new TPIN';
}
if (value != _newTpinController.text) {
return 'TPINs do not match';
}
return null;
},
),
const SizedBox(height: 32),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _handleChangeTpin,
child: _isLoading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2.5,
),
)
: const Text('Proceed'),
),
),
],
),
),
),
);
}
}

View File

@@ -99,7 +99,8 @@ Future<void> _loadLimit() async {
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
@@ -457,7 +458,9 @@ Future<void> _loadLimit() async {
),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Form(
key: _formKey,
@@ -495,7 +498,8 @@ Future<void> _loadLimit() async {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
controller: accountNumberController,
@@ -510,7 +514,8 @@ Future<void> _loadLimit() async {
},
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).accountNumberRequired;
return AppLocalizations.of(context)
.accountNumberRequired;
} else if (value.length < 7 || value.length > 20) {
return AppLocalizations.of(context).accno7to20;
}
@@ -521,7 +526,8 @@ Future<void> _loadLimit() async {
TextFormField(
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmAccountNumber,
labelText:
AppLocalizations.of(context).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
@@ -533,14 +539,16 @@ Future<void> _loadLimit() async {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
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).reenterAccountNumber;
return AppLocalizations.of(context)
.reenterAccountNumber;
}
if (value != accountNumberController.text) {
return AppLocalizations.of(context).accountMismatch;
@@ -564,7 +572,8 @@ Future<void> _loadLimit() async {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
@@ -613,7 +622,8 @@ Future<void> _loadLimit() async {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
@@ -629,7 +639,8 @@ Future<void> _loadLimit() async {
'Current',
]
.map(
(e) => DropdownMenuItem(value: e, child: Text(e)),
(e) =>
DropdownMenuItem(value: e, child: Text(e)),
)
.toList(),
onChanged: (value) => setState(() {
@@ -655,7 +666,8 @@ Future<void> _loadLimit() async {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
),
@@ -707,10 +719,11 @@ Future<void> _loadLimit() async {
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
child:
CircularProgressIndicator(strokeWidth: 2),
)
: Text(
AppLocalizations.of(context).validateBeneficiary),
: Text(AppLocalizations.of(context)
.validateBeneficiary),
),
),
),
@@ -719,8 +732,8 @@ Future<void> _loadLimit() async {
padding: const EdgeInsets.only(bottom: 24.0),
child: Text(
_validationError!,
style:
TextStyle(color: Theme.of(context).colorScheme.error),
style: TextStyle(
color: Theme.of(context).colorScheme.error),
),
),
TextFormField(
@@ -738,7 +751,8 @@ Future<void> _loadLimit() async {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
validator: (value) {
@@ -763,7 +777,8 @@ Future<void> _loadLimit() async {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
),
@@ -783,19 +798,24 @@ Future<void> _loadLimit() async {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
color: Theme.of(context)
.colorScheme
.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
color:
Theme.of(context).colorScheme.primary,
width: 2),
),
),
textInputAction: TextInputAction.next,
validator: (value) => value == null || value.isEmpty
validator: (value) => value == null ||
value.isEmpty
? AppLocalizations.of(context).phoneRequired
: null,
),
@@ -808,14 +828,18 @@ Future<void> _loadLimit() async {
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
fillColor:
Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
color: Theme.of(context)
.colorScheme
.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
color:
Theme.of(context).colorScheme.primary,
width: 2),
),
),
@@ -824,11 +848,13 @@ Future<void> _loadLimit() async {
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).amountRequired;
return AppLocalizations.of(context)
.amountRequired;
}
final amount = double.tryParse(value);
if (amount == null || amount <= 0) {
return AppLocalizations.of(context).validAmount;
return AppLocalizations.of(context)
.validAmount;
}
return null;
},
@@ -868,12 +894,18 @@ if (!_isLoadingLimit && _limit != null)
alignment: Alignment.center,
child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward,
color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
Theme.of(context).colorScheme.primary,
color: _isAmountOverLimit
? Colors.grey
: Theme.of(context).dialogBackgroundColor),
activeThumbColor: _isAmountOverLimit
? Colors.grey.shade700
: Theme.of(context).colorScheme.primary,
activeTrackColor: _isAmountOverLimit
? Colors.grey.shade300
: Theme.of(context).colorScheme.secondary.withAlpha(100),
: Theme.of(context)
.colorScheme
.secondary
.withAlpha(100),
borderRadius: BorderRadius.circular(30),
height: 56,
onSwipe: () {
@@ -893,6 +925,22 @@ if (!_isLoadingLimit && _limit != null)
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}

View File

@@ -21,7 +21,9 @@ class _QuickPayScreen extends State<QuickPayScreen> {
AppLocalizations.of(context).quickPay.replaceAll('\n', ''),
),
),
body: ListView(
body: Stack(
children: [
ListView(
children: [
QuickPayManagementTile(
icon: Symbols.input_circle,
@@ -55,6 +57,22 @@ class _QuickPayScreen extends State<QuickPayScreen> {
const Divider(height: 1),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -74,7 +74,8 @@ void _checkAmountLimit() {
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
content: Text(
'Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
@@ -148,7 +149,9 @@ void _checkAmountLimit() {
),
centerTitle: false,
),
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
@@ -158,14 +161,16 @@ void _checkAmountLimit() {
const SizedBox(height: 10),
TextFormField(
decoration: InputDecoration(
labelText: AppLocalizations.of(context).debitAccountNumber,
labelText:
AppLocalizations.of(context).debitAccountNumber,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
),
readOnly: true,
controller: TextEditingController(text: widget.debitAccount),
controller:
TextEditingController(text: widget.debitAccount),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
enabled: false,
@@ -184,7 +189,8 @@ void _checkAmountLimit() {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
controller: accountNumberController,
@@ -193,9 +199,11 @@ void _checkAmountLimit() {
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).accountNumberRequired;
return AppLocalizations.of(context)
.accountNumberRequired;
} else if (value.length != 11) {
return AppLocalizations.of(context).validAccountNumber;
return AppLocalizations.of(context)
.validAccountNumber;
}
return null;
},
@@ -204,7 +212,8 @@ void _checkAmountLimit() {
TextFormField(
controller: confirmAccountNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).confirmAccountNumber,
labelText:
AppLocalizations.of(context).confirmAccountNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
@@ -216,14 +225,16 @@ void _checkAmountLimit() {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
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).reenterAccountNumber;
return AppLocalizations.of(context)
.reenterAccountNumber;
}
if (value != accountNumberController.text) {
return AppLocalizations.of(context).accountMismatch;
@@ -240,7 +251,8 @@ void _checkAmountLimit() {
onPressed: _isValidating
? null
: () {
if (accountNumberController.text.length == 11 &&
if (accountNumberController.text.length ==
11 &&
confirmAccountNumberController.text ==
accountNumberController.text) {
_validateBeneficiary();
@@ -256,10 +268,11 @@ void _checkAmountLimit() {
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
child: CircularProgressIndicator(
strokeWidth: 2),
)
: Text(
AppLocalizations.of(context).validateBeneficiary),
: Text(AppLocalizations.of(context)
.validateBeneficiary),
),
),
),
@@ -273,7 +286,8 @@ void _checkAmountLimit() {
Text(
'${AppLocalizations.of(context).beneficiaryName}: $_beneficiaryName',
style: const TextStyle(
color: Colors.green, fontWeight: FontWeight.bold),
color: Colors.green,
fontWeight: FontWeight.bold),
),
],
),
@@ -302,7 +316,8 @@ void _checkAmountLimit() {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
value: _selectedAccountType,
@@ -343,12 +358,12 @@ void _checkAmountLimit() {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
),
const SizedBox(height: 25),
TextFormField(
decoration: InputDecoration(
labelText: AppLocalizations.of(context).amount,
@@ -362,7 +377,8 @@ void _checkAmountLimit() {
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
controller: amountController,
@@ -380,8 +396,7 @@ void _checkAmountLimit() {
},
),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (_isLoadingLimit) const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
@@ -392,9 +407,12 @@ if (!_isLoadingLimit && _limit != null)
alignment: Alignment.center,
child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward,
color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
Theme.of(context).colorScheme.primary,
color: _isAmountOverLimit
? Colors.grey
: Theme.of(context).dialogBackgroundColor),
activeThumbColor: _isAmountOverLimit
? Colors.grey.shade700
: Theme.of(context).colorScheme.primary,
activeTrackColor: _isAmountOverLimit
? Colors.grey.shade300
: Theme.of(
@@ -423,7 +441,8 @@ if (!_isLoadingLimit && _limit != null)
context,
MaterialPageRoute(
builder: (context) => TransactionPinScreen(
onPinCompleted: (pinScreenContext, tpin) async {
onPinCompleted:
(pinScreenContext, tpin) async {
final transfer = Transfer(
fromAccount: widget.debitAccount,
toAccount: accountNumberController.text,
@@ -433,14 +452,17 @@ if (!_isLoadingLimit && _limit != null)
remarks: remarksController.text,
);
final paymentService = getIt<PaymentService>();
final paymentService =
getIt<PaymentService>();
final paymentResponseFuture = paymentService
.processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement(
Navigator.of(pinScreenContext)
.pushReplacement(
MaterialPageRoute(
builder: (_) => PaymentAnimationScreen(
paymentResponse: paymentResponseFuture),
paymentResponse:
paymentResponseFuture),
),
);
},
@@ -456,6 +478,22 @@ if (!_isLoadingLimit && _limit != null)
),
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}

View File

@@ -11,7 +11,9 @@ class SecurityErrorScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -21,17 +23,34 @@ class SecurityErrorScreen extends StatelessWidget {
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () =>
SystemChannels.platform.invokeMethod('SystemNavigator.pop'),
onPressed: () => SystemChannels.platform
.invokeMethod('SystemNavigator.pop'),
child: const Text('Okay'),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,151 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/api/services/branch_service.dart'; // Added: Import BranchService for Atm model and API calls
import 'package:kmobile/di/injection.dart'; // Added: Import for dependency injection (getIt)
import 'package:shimmer/shimmer.dart'; // Added: Import for shimmer loading effect
// Removed: The local 'Location' class is no longer needed as we use the 'Atm' model from branch_service.dart
class ATMLocatorScreen extends StatefulWidget {
const ATMLocatorScreen({super.key});
@override
State<ATMLocatorScreen> createState() => _ATMLocatorScreenState();
}
class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
final TextEditingController _searchController = TextEditingController();
var service = getIt<BranchService>(); // Added: Instance of BranchService for API calls
bool _isLoading = true; // State variable to manage loading status
List<Atm> _allAtms = []; // Changed: List to hold all fetched Atm objects
List<Atm> _filteredAtms = []; // Changed: List to hold filtered Atm objects for display
@override
void initState() {
super.initState();
_loadAtms(); // Changed: Call _loadAtms to fetch data on initialization
}
/// Fetches the list of ATMs from the API using BranchService.
Future<void> _loadAtms() async {
final data = await service.fetchAtmList(); // Call the new fetchAtmList method
setState(() {
_allAtms = data; // Update the list of all ATMs
_filteredAtms = data; // Initialize filtered list with all ATMs
_isLoading = false; // Set loading to false once data is fetched
});
}
/// Filters the list of ATMs based on the search query.
void _filterAtms(String query) { // Changed: Renamed from _filterLocations
setState(() {
if (query.isEmpty) {
_filteredAtms = _allAtms; // If query is empty, show all ATMs
} else {
_filteredAtms = _allAtms.where((atm) { // Changed: Filter based on Atm object
final lowerQuery = query.toLowerCase();
return atm.name.toLowerCase().contains(lowerQuery); // Filter by atm.name
}).toList();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("ATM Locator"), // Title for the app bar
),
body: Stack(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
onChanged: _filterAtms, // Updated: Call _filterAtms on text change
decoration: InputDecoration(
hintText: "Name/Address", // Hint text for the search bar
prefixIcon: const Icon(Icons.search), // Search icon
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
// Content area: Displays loading shimmer, "no ATMs found", or the list of ATMs
Expanded(
child: _isLoading
? _buildShimmerList() // Display shimmer while loading
: _filteredAtms.isEmpty
? const Center(
child: Text("No matching ATMs found")) // Message if no ATMs found
: ListView.builder(
itemCount: _filteredAtms.length, // Number of items in the filtered list
itemBuilder: (context, index) {
final atm = _filteredAtms[index]; // Get the current Atm object
return _buildAtmItem(atm); // Build the ATM list item
},
),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png', // Background logo image
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
/// Helper widget to build a single ATM list item.
Widget _buildAtmItem(Atm atm) { // Changed: Takes an Atm object
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: const Icon(Icons.currency_rupee), // Icon for ATM
title: Text(atm.name, // Display the ATM's name
style: const TextStyle(fontWeight: FontWeight.bold)),
onTap: () {
// No action on tap as per user request
},
),
);
}
/// Helper widget to display a shimmer loading effect.
Widget _buildShimmerList() {
return ListView.builder(
itemCount: 10, // Number of shimmer items to display
itemBuilder: (context, index) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: ListTile(
leading: const CircleAvatar(
radius: 24,
backgroundColor: Colors.white,
),
title: Container(
height: 16,
color: Colors.white,
margin: const EdgeInsets.symmetric(vertical: 4),
),
),
),
);
}
}

View File

@@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:url_launcher/url_launcher.dart';
class BranchDetailsScreen extends StatelessWidget {
final Branch branch;
const BranchDetailsScreen({super.key, required this.branch});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(branch.branch_name),
),
body: Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow("Branch Name", branch.branch_name),
_buildDetailRow("Branch Code", branch.branch_code),
_buildDetailRow("Zone", branch.zone),
_buildDetailRow("Tehsil", branch.tehsil),
_buildDetailRow("Block", branch.block),
_buildDetailRow("District", branch.distt_name),
_buildDetailRow("Pincode", branch.pincode),
// _buildDetailRow("Post Office", branch.post_office),
// _buildDetailRow("Date of Opening", branch.date_of_opening),
// _buildDetailRow("Branch Type", branch.type_of_branch),
_buildDetailRow("Telephone No.", branch.telephone_no),
// _buildDetailRow("RTGS Account No.", branch.rtgs_acct_no),
// _buildDetailRow("RBI Code 1", branch.rbi_code_1),
// _buildDetailRow("RBI Code 2", branch.rbi_code_2),
// _buildDetailRow("Latitude", branch.br_lattitude),
// _buildDetailRow("Longitude", branch.br_longitude),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
label == "Telephone No."
? InkWell(
onTap: () => _launchUrl('tel:$value'),
child: Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.blue, // Indicate it's clickable
decoration: TextDecoration.underline,
),
),
)
: Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const Divider(height: 16),
],
),
);
}
Future<void> _launchUrl(String urlString) async {
final Uri url = Uri.parse(urlString);
if (!await launchUrl(url)) {
throw 'Could not launch $urlString';
}
}
}

View File

@@ -2,25 +2,11 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/di/injection.dart';
import 'package:shimmer/shimmer.dart';
import 'package:kmobile/features/service/screens/branch_details_screen.dart';
// Enum to define the type of location
enum LocationType { branch, atm }
class Location {
final String name;
final String? code; // Nullable for ATMs
final String? ifsc; // Nullable for ATMs
final String address;
final LocationType type;
Location({
required this.name,
this.code,
this.ifsc,
required this.address,
required this.type,
});
}
class BranchLocatorScreen extends StatefulWidget {
const BranchLocatorScreen({super.key});
@@ -31,96 +17,58 @@ class BranchLocatorScreen extends StatefulWidget {
class _BranchLocatorScreenState extends State<BranchLocatorScreen> {
final TextEditingController _searchController = TextEditingController();
final List<Location> _allLocations = [
Location(
name: "Dharamsala - Head Office",
code: "002",
ifsc: "KACE0000002",
address: "Civil Lines Dharmashala, Kangra, HP - 176215",
type: LocationType.branch,
),
Location(
name: "Kangra",
code: "033",
ifsc: "KACE0000033",
address: "Rajput Bhawankangrapo, Kangra, HP ",
type: LocationType.branch,
),
Location(
name: "Dharamsala ATM",
address: "Near Main Square, Dharamsala",
type: LocationType.atm,
),
Location(
name: "Kangra ATM",
address: "Opposite Bus Stand, Kangra",
type: LocationType.atm,
),
];
List<Location> _filteredLocations = [];
bool _isLoading = false;
var service = getIt<BranchService>();
bool _isLoading = true;
List<Branch> _allBranches = [];
List<Branch> _filteredBranches = [];
@override
void initState() {
super.initState();
// _fetchAndSetLocations();
_filteredLocations = _allLocations;
_loadBranches();
}
// Example of a future API fetching function
/*
Future<void> _fetchAndSetLocations() async {
setState(() {
_isLoading = true;
});
try {
// final locations = await yourApiService.getLocations();
// setState(() {
// _allLocations = locations;
// _filteredLocations = locations;
// });
} catch (e) {
// Handle error
} finally {
Future<void> _loadBranches() async {
final data = await service.fetchBranchList();
setState(() {
_allBranches = data;
_filteredBranches = data;
_isLoading = false;
});
}
}
*/
void _filterLocations(String query) {
void _filterBranches(String query) {
setState(() {
if (query.isEmpty) {
_filteredLocations = _allLocations;
_filteredBranches = _allBranches;
} else {
_filteredLocations = _allLocations.where((location) {
_filteredBranches = _allBranches.where((branch) {
final lowerQuery = query.toLowerCase();
return location.name.toLowerCase().contains(lowerQuery) ||
(location.code?.toLowerCase().contains(lowerQuery) ?? false) ||
(location.ifsc?.toLowerCase().contains(lowerQuery) ?? false) ||
location.address.toLowerCase().contains(lowerQuery);
return branch.branch_name.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).branchLocator),
title: const Text("Branch Locator"),
),
body: Column(
body: Stack(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
onChanged: _filterLocations,
onChanged: _filterBranches, // Updated
decoration: InputDecoration(
hintText: AppLocalizations.of(context).searchbranchby,
hintText: "Branch Name",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
@@ -132,19 +80,36 @@ Future<void> _fetchAndSetLocations() async {
// Content area
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredLocations.isEmpty
? const Center(child: Text("No matching locations found"))
? _buildShimmerList() // Changed to shimmer
: _filteredBranches.isEmpty
? const Center(
child: Text("No matching branches found")) // Updated tex
: ListView.builder(
itemCount: _filteredLocations.length,
itemCount: _filteredBranches.length,
itemBuilder: (context, index) {
final location = _filteredLocations[index];
return _buildLocationItem(location);
final branch = _filteredBranches[index]; // Changed to
return _buildBranchItem(branch); // Updated
},
),
),
],
),
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
),
),
),
),
),
],
),
);
}
@@ -161,28 +126,50 @@ Future<void> _fetchAndSetLocations() async {
);
}
// Helper widget to build a single location item
Widget _buildLocationItem(Location location) {
final isBranch = location.type == LocationType.branch;
// Helper widget to build a single branch item
Widget _buildBranchItem(Branch branch) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: CircleAvatar(
child: Icon(isBranch ? Icons.location_city : Icons.currency_rupee),
leading: const CircleAvatar(
child: Icon(Icons.location_city),
),
title: Text(location.name,
title: Text(branch.branch_name,
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
isBranch
? "Code: ${location.code} | IFSC: ${location.ifsc}\nAddress: ${location.address}"
: "Address: ${location.address}",
),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Selected ${location.name}")),
// This is the updated part
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BranchDetailsScreen(branch: branch),
),
);
},
),
);
}
// Shimmer loading list
Widget _buildShimmerList() {
return ListView.builder(
itemCount: 10, // Number of shimmer items
itemBuilder: (context, index) => Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: ListTile(
leading: const CircleAvatar(
radius: 24,
backgroundColor: Colors.white,
),
title: Container(
height: 16,
color: Colors.white,
margin: const EdgeInsets.symmetric(vertical: 4),
),
),
),
);
}
}

View File

@@ -1,6 +1,14 @@
import 'package:flutter/material.dart';
import 'package:kmobile/l10n/app_localizations.dart';
// Data model for a single FAQ item
class FaqItem {
final String question;
final String answer;
FaqItem({required this.question, required this.answer});
}
class FaqsScreen extends StatefulWidget {
const FaqsScreen({super.key});
@@ -9,38 +17,96 @@ class FaqsScreen extends StatefulWidget {
}
class _FaqsScreenState extends State<FaqsScreen> {
@override
void initState() {
super.initState();
_getFaqs();
}
// A placeholder for your future API call
Future<void> _getFaqs() async {
// TODO: Implement API call to fetch FAQs data
// For now, simulating a network call with a delay
await Future.delayed(const Duration(seconds: 1));
// In a real implementation, you would process the API response here
}
// List of FAQs
final List<FaqItem> _faqs = [
FaqItem(
question: "How do I log in to the mobile banking app?",
answer:
"You can log in using your customer number and password. Biometric login (fingerprint) and MPIN is also available for supported devices.",
),
FaqItem(
question: "Is my banking information secure on this app?",
answer:
"Yes. We use industry-standard encryption and multi-factor authentication to ensure your data is safe.",
),
FaqItem(
question: "How can I check my account balance?",
answer:
"Once logged in, your account balance will be displayed on the home screen. You can also view detailed balances under the “Accounts” section.",
),
FaqItem(
question: "Can I transfer money to other bank accounts?",
answer:
"Yes. You can use NEFT, RTGS or IMPS to transfer funds to any bank account in India.",
),
FaqItem(
question: "How do I view my transaction history?",
answer:
"Click on the “Account Statement” icon under the Home Screen to view recent and past transactions.",
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Flexible(
child: Text(
AppLocalizations.of(context).faq,
softWrap: true,
style: const TextStyle(
fontSize: 16.5,
title: Text(AppLocalizations.of(context).faq),
),
body: Stack(
children: [
// Background logo
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07,
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
),
),
),
),
),
// FAQ List
ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: _faqs.length,
itemBuilder: (context, index) {
final faq = _faqs[index];
return Card(
margin:
const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
elevation: 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: ExpansionTile(
title: Text(
faq.question,
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 0, 16.0, 16.0),
child: Text(
faq.answer,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
height: 1.5,
),
textAlign: TextAlign.left,
),
),
],
),
);
},
),
],
),
);
}

View File

@@ -1,5 +1,18 @@
//
import 'package:flutter/material.dart';
import 'package:kmobile/l10n/app_localizations.dart';
import 'package:url_launcher/url_launcher.dart';
// Data model for a single Quick Link item
class QuickLink {
final String title;
final String url;
// The icon is no longer used in the UI but is kept in the model for potential future use.
final IconData icon;
QuickLink({required this.title, required this.url, required this.icon});
}
class QuickLinksScreen extends StatefulWidget {
const QuickLinksScreen({super.key});
@@ -9,18 +22,48 @@ class QuickLinksScreen extends StatefulWidget {
}
class _QuickLinksScreenState extends State<QuickLinksScreen> {
@override
void initState() {
super.initState();
_getQuickLinks();
}
// List of Quick Links
final List<QuickLink> _quickLinks = [
QuickLink(
title: "National Bank of Agriculture & Rural Development",
url: "http://www.nabard.org/",
icon: Icons.account_balance),
QuickLink(
title: "Reserve Bank of India",
url: "http://www.rbi.org.in/home.aspx",
icon: Icons.account_balance_wallet),
QuickLink(
title: "Indian Institute of Banking & Finance",
url: "http://www.iibf.org.in/",
icon: Icons.school),
QuickLink(
title: "Indian Bank Association",
url: "http://www.iba.org.in/",
icon: Icons.group_work),
QuickLink(
title: "Ministry of Finance",
url: "http://www.finmin.nic.in/",
icon: Icons.business),
QuickLink(
title: "Securities Exchange Board of India",
url: "http://www.sebi.gov.in/",
icon: Icons.show_chart),
QuickLink(
title: "Insurance Regulatory & Development Authority",
url: "https://www.irdai.gov.in/",
icon: Icons.shield_outlined),
];
// A placeholder for your future API call
Future<void> _getQuickLinks() async {
// TODO: Implement API call to fetch quick links data
// For now, simulating a network call with a delay
await Future.delayed(const Duration(seconds: 1));
// In a real implementation, you would process the API response here
// Function to launch a URL
Future<void> _launchURL(String url, BuildContext context) async {
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Could not launch $url')),
);
}
}
@override
@@ -29,6 +72,42 @@ class _QuickLinksScreenState extends State<QuickLinksScreen> {
appBar: AppBar(
title: Text(AppLocalizations.of(context).quickLinks),
),
body: Stack(
children: [
// Background logo
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07,
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200,
height: 200,
),
),
),
),
),
// UPDATED: List of Quick Links
ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: _quickLinks.length,
itemBuilder: (context, index) {
final link = _quickLinks[index];
return Card(
margin:
const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
child: ListTile(
title: Text(link.title),
trailing: const Icon(Icons.open_in_new, size: 20),
onTap: () => _launchURL(link.url, context),
),
);
},
),
],
),
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:kmobile/features/service/screens/atm_locator_screen.dart';
import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
import '../../../l10n/app_localizations.dart';
@@ -24,22 +25,24 @@ class _ServiceScreen extends State<ServiceScreen> {
),
centerTitle: false,
),
body: ListView(
body: Stack(
children: [
ServiceManagementTile(
icon: Symbols.add,
label: AppLocalizations.of(context).accountOpeningDeposit,
onTap: () {},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.add,
label: AppLocalizations.of(context).accountOpeningLoan,
onTap: () {},
disabled: true,
),
const Divider(height: 1),
ListView(
children: [
// ServiceManagementTile(
// icon: Symbols.add,
// label: AppLocalizations.of(context).accountOpeningDeposit,
// onTap: () {},
// disabled: true,
// ),
// const Divider(height: 1),
// ServiceManagementTile(
// icon: Symbols.add,
// label: AppLocalizations.of(context).accountOpeningLoan,
// onTap: () {},
// disabled: true,
// ),
// const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.captive_portal,
label: AppLocalizations.of(context).quickLinks,
@@ -49,7 +52,7 @@ class _ServiceScreen extends State<ServiceScreen> {
builder: (context) => const QuickLinksScreen()),
);
},
disabled: true,
disabled: false,
),
const Divider(height: 1),
ServiceManagementTile(
@@ -60,23 +63,39 @@ class _ServiceScreen extends State<ServiceScreen> {
MaterialPageRoute(builder: (context) => const FaqsScreen()),
);
},
disabled: true,
disabled: false,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.location_pin,
label: AppLocalizations.of(context).branchLocator,
label: "ATM Locator",
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BranchLocatorScreen()));
builder: (context) => const ATMLocatorScreen()));
},
disabled: true,
disabled: false,
),
const Divider(height: 1),
],
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
),
),
),
],
),
);
}
}

View File

@@ -217,6 +217,13 @@
"enterMPIN": "Enter your mPIN",
"setMPIN": "Set your mPIN",
"confirmMPIN": "Confirm your mPIN",
"changeMpin": "Change mPIN",
"enterOldMpin": "Enter your old mPIN",
"enterNewMpin": "Enter your new mPIN",
"confirmNewMpin": "Confirm your new mPIN",
"mpinChangedSuccessfully": "mPIN changed successfully",
"pinMismatch": "PINs do not match",
"securitySettings": "Security Settings",
"kconnect": "Kconnect",
"kccBankFull": "Kangra Central Co-operative Bank",
"themeColor": "Theme Color",
@@ -331,5 +338,71 @@
"deregistercheck": "Are you sure you want to De-Register?",
"biometricsNotAvailable": "Biometrics not available on this device",
"disableFingerprintLogin": "Disable Fingerprint Login",
"disableFingerprintMessage": "Are you sure you want to disable fingerprint login?"
"disableFingerprintMessage": "Are you sure you want to disable fingerprint login?",
"selectAccount": "Select Account",
"noOtherAccountsFound": "No other accounts found",
"selfPay": "Self Pay",
"fundTransfer": "Fund Transfer",
"fetchingDailyLimit": "Fetching daily limit...",
"okay": "Okay",
"limitUpdated": "Limit Updated",
"changeTpin": "Change TPIN",
"currentTpin": "Current TPIN",
"newTpin": "New TPIN",
"confirmNewTpin": "Confirm New TPIN",
"pleaseEnterAValid6DigitOtp": "Please enter a valid 6-digit OTP",
"tpinChangedSuccessfully": "TPIN changed successfully!",
"verifyChangeTpin": "Verify & Change TPIN",
"tpinNotSet": "TPIN Not Set",
"back": "Back",
"appVersion": "App Version",
"error": "Error",
"atmLocator": "ATM Locator",
"nationalBankOfAgricultureRuralDevelopment": "National Bank of Agriculture & Rural Development",
"reserveBankOfIndia": "Reserve Bank of India",
"indianInstituteOfBankingFinance": "Indian Institute of Banking & Finance",
"indianBankAssociation": "Indian Bank Association",
"ministryOfFinance": "Ministry of Finance",
"securitiesExchangeBoardOfIndia": "Securities Exchange Board of India",
"insuranceRegulatoryDevelopmentAuthority": "Insurance Regulatory & Development Authority",
"noMatchingBranchesFound": "No matching branches found",
"nameAddress": "Name/Address",
"noMatchingAtmsFound": "No matching ATMs found",
"myCards": "My Cards",
"validFrom": "VALID FROM",
"validUpto": "VALID UPTO",
"termsAndConditions": "Terms and Conditions",
"goBack": "Go Back",
"noTransactionsToExport": "No transactions to export.",
"accountStatementKccb": "Account Statement - KCCB",
"description": "Description",
"balance": "Balance",
"storagePermissionIsRequiredToSavePdf": "Storage permission is required to save PDF",
"setNewPassword": "Set New Password",
"setPassword": "Set Password",
"backToLogin": "Back to Login",
"enterDigitPin": "Enter {length}-digit PIN",
"couldNotLaunch": "Could not launch {url}",
"selected": "Selected {atmName}",
"currencySymbol": "₹",
"iAgreeToTheTermsAndConditions": "I agree to the Terms and Conditions",
"disagree": "Disagree",
"couldNotOpenEmailAppFor": "Could not open email app for {email}",
"couldNotOpenDialerFor": "Could not open dialer for {phone}",
"failedToSendOtp": "Failed to send OTP: {error}",
"anErrorOccurred": "An error occurred: {error}",
"pdfSavedTo": "PDF saved to: {filePath}",
"errorSavingPdf": "Error saving PDF: {error}",
"limitToBeSetMustBeLessThan200000": "Limit To be Set must be less than 200000",
"remainingLimitToday": "Remaining Limit Today",
"hindiLanguage": "हनद",
"pleaseFillBothPasswordFields": "Please fill both password fields",
"enter": "Enter",
"setLimit": "Set Limit",
"tehsil": "Tehsil",
"district": "District",
"postOffice": "Post Office",
"rbiCode1": "RBI Code 1",
"rbiCode2": "RBI Code 2",
"latitude": "Latitude"
}

View File

@@ -218,6 +218,13 @@
"enterMPIN": "अपना mPIN दर्ज करें",
"setMPIN": "अपना mPIN सेट करें",
"confirmMPIN": "अपना mPIN की पुष्टि करें",
"changeMpin": "mPIN बदलें",
"enterOldMpin": "अपना पुराना mPIN दर्ज करें",
"enterNewMpin": "अपना नया mPIN दर्ज करें",
"confirmNewMpin": "अपना नया mPIN की पुष्टि करें",
"mpinChangedSuccessfully": "mPIN सफलतापूर्वक बदल गया",
"pinMismatch": "PIN मेल नहीं खा रहे हैं",
"securitySettings": "सुरक्षा सेटिंग्स",
"kconnect": "के-कनेक्ट",
"kccBankFull": "कांगड़ा सेंट्रल को-ऑपरेटिव बैंक",
"themeColor": "थीम रंग",
@@ -332,5 +339,71 @@
"deregistercheck": "क्या आप वाकई पंजीकरण रद्द करना चाहते हैं??",
"biometricsNotAvailable": "इस डिवाइस पर बायोमेट्रिक्स उपलब्ध नहीं है",
"disableFingerprintLogin": "फ़िंगरप्रिंट लॉगिन अक्षम करें",
"disableFingerprintMessage": "क्या आप फ़िंगरप्रिंट लॉगिन अक्षम करना चाहते हैं?"
"disableFingerprintMessage": "क्या आप फ़िंगरप्रिंट लॉगिन अक्षम करना चाहते हैं?",
"selectAccount": "खाता चुनें",
"noOtherAccountsFound": "कोई अन्य खाता नहीं मिला",
"selfPay": "स्वयं भुगतान",
"fundTransfer": "धन हस्तांतरण",
"fetchingDailyLimit": "दैनिक सीमा प्राप्त की जा रही है...",
"okay": "ठीक है",
"limitUpdated": "सीमा अपडेट की गई",
"changeTpin": "टी-पिन बदलें",
"currentTpin": "वर्तमान टी-पिन",
"newTpin": "नया टी-पिन",
"confirmNewTpin": "नए टी-पिन की पुष्टि करें",
"pleaseEnterAValid6DigitOtp": "कृपया एक मान्य 6-अंकीय ओटीपी दर्ज करें",
"tpinChangedSuccessfully": "टी-पिन सफलतापूर्वक बदला गया!",
"verifyChangeTpin": "सत्यापित करें और टी-पिन बदलें",
"tpinNotSet": "टी-पिन सेट नहीं है",
"back": "वापस",
"appVersion": "ऐप संस्करण",
"error": "त्रुटि",
"atmLocator": "एटीएम लोकेटर",
"nationalBankOfAgricultureRuralDevelopment": "राष्ट्रीय कृषि और ग्रामीण विकास बैंक",
"reserveBankOfIndia": "भारतीय रिज़र्व बैंक",
"indianInstituteOfBankingFinance": "भारतीय बैंकिंग और वित्त संस्थान",
"indianBankAssociation": "भारतीय बैंक संघ",
"ministryOfFinance": "वित्त मंत्रालय",
"securitiesExchangeBoardOfIndia": "भारतीय प्रतिभूति और विनिमय बोर्ड",
"insuranceRegulatoryDevelopmentAuthority": "बीमा नियामक और विकास प्राधिकरण",
"noMatchingBranchesFound": "कोई मिलती-जुलती शाखा नहीं मिली",
"nameAddress": "नाम/पता",
"noMatchingAtmsFound": "कोई मिलते-जुलते एटीएम नहीं मिले",
"myCards": "मेरे कार्ड",
"validFrom": "से मान्य",
"validUpto": "तक मान्य",
"termsAndConditions": "नियम और शर्तें",
"goBack": "वापस जाएं",
"noTransactionsToExport": "निर्यात करने के लिए कोई लेन-देन नहीं है।",
"accountStatementKccb": "खाता विवरण - KCCB",
"description": "विवरण",
"balance": "शेष राशि",
"storagePermissionIsRequiredToSavePdf": "पीडीएफ सहेजने के लिए स्टोरेज अनुमति आवश्यक है",
"setNewPassword": "नया पासवर्ड सेट करें",
"setPassword": "पासवर्ड सेट करें",
"backToLogin": "लॉगिन पर वापस जाएं",
"enterDigitPin": "{length}-अंकीय पिन दर्ज करें",
"couldNotLaunch": "{url} लॉन्च नहीं किया जा सका",
"selected": "{atmName} चयनित",
"currencySymbol": "₹",
"iAgreeToTheTermsAndConditions": "मैं नियम और शर्तों से सहमत हूं",
"disagree": "असहमत",
"couldNotOpenEmailAppFor": "{email} के लिए ईमेल ऐप नहीं खोला जा सका",
"couldNotOpenDialerFor": "{phone} के लिए डायलर नहीं खोला जा सका",
"failedToSendOtp": "ओटीपी भेजने में विफल: {error}",
"anErrorOccurred": "एक त्रुटि हुई: {error}",
"pdfSavedTo": "पीडीएफ यहां सहेजा गया: {filePath}",
"errorSavingPdf": "पीडीएफ सहेजने में त्रुटि: {error}",
"limitToBeSetMustBeLessThan200000": "सेट की जाने वाली सीमा 200000 से कम होनी चाहिए",
"remainingLimitToday": "आज की शेष सीमा",
"hindiLanguage": "हिन्दी",
"pleaseFillBothPasswordFields": "कृपया दोनों पासवर्ड फ़ील्ड भरें",
"enter": "दर्ज करें",
"setLimit": "सीमा सेट करें",
"tehsil": "तहसील",
"district": "जिला",
"postOffice": "डाकघर",
"rbiCode1": "आरबीआई कोड 1",
"rbiCode2": "आरबीआई कोड 2",
"latitude": "अक्षांश"
}

View File

@@ -15,13 +15,13 @@ void main() async {
]);
// Check for device compromise
// final compromisedMessage = await SecurityService.deviceCompromisedMessage;
// if (compromisedMessage != null) {
// runApp(MaterialApp(
// home: SecurityErrorScreen(message: compromisedMessage),
// ));
// return;
// }
final compromisedMessage = await SecurityService.deviceCompromisedMessage;
if (compromisedMessage != null) {
runApp(MaterialApp(
home: SecurityErrorScreen(message: compromisedMessage),
));
return;
}
await setupDependencies();
runApp(const KMobile());
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PinInputField extends StatelessWidget {
final TextEditingController controller;
final int length;
final FormFieldValidator<String>? validator;
const PinInputField({
super.key,
required this.controller,
this.length = 6,
this.validator,
});
@override
Widget build(BuildContext context) {
return FormField<String>(
validator: validator,
builder: (FormFieldState<String> state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: controller,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(length),
],
obscureText: true,
obscuringCharacter: '*',
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: 'Enter $length-digit PIN',
counterText: '', // Hide the counter
),
onChanged: (value) {
state.didChange(value);
},
),
if (state.hasError)
Padding(
padding: const EdgeInsets.only(top: 8.0, left: 12.0),
child: Text(
state.errorText!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 12,
),
),
),
],
);
},
);
}
}

211
lib/widgets/tnc_dialog.dart Normal file
View File

@@ -0,0 +1,211 @@
import 'package:flutter/material.dart';
class TncDialog extends StatefulWidget {
final Future<void> Function() onProceed;
const TncDialog({Key? key, required this.onProceed}) : super(key: key);
@override
_TncDialogState createState() => _TncDialogState();
}
class _TncDialogState extends State<TncDialog> {
bool _isAgreed = false;
bool _isLoading = false;
// --- NEW: ScrollController for the TNC text ---
final ScrollController _scrollController = ScrollController();
final String _termsAndConditionsText = """
Effective Date: November 10, 2025
These Terms and Conditions ("Terms") govern your access to and use of The Bank mobile banking application (the "App") and the services
provided through it (the "Services").
By downloading, installing, accessing, or using the App, you agree to be bound by these Terms and our Privacy Policy. If you do not
agree to these Terms, you must not download, install, access, or use the App.
1. Definitions
- App: Refers to The Bank mobile banking application.
- Bank/We/Us/Our: Refers to The Bank.
- User/You/Your: Refers to the individual using the App.
- Device: Refers to any compatible mobile phone, tablet, or other device on which you install and use the App.
- Security Credentials: Refers to your username, password, PIN, biometric data (e.g., fingerprint, facial recognition), and any other
authentication methods used to access the App and Services.
2. Acceptance of Terms
Your use of the App constitutes your acceptance of these Terms. We recommend that you print or save a copy of these Terms for your
records.
3. License to Use
We grant you a limited, non-exclusive, non-transferable, revocable license to install and use the App on a Device that you own or
control, solely for your personal, non-commercial use in connection with your accounts at The Bank. This license does not permit you to
use the App on any Device that you do not own or control.
4. User Responsibilities
You agree to:
- Use the App only for lawful purposes and in accordance with these Terms.
- Keep your Device and Security Credentials secure and confidential.
- Notify us immediately if you suspect any unauthorized use of your Security Credentials or Device, or if your Device is lost or stolen.
- Ensure that any information you provide to us through the App is accurate and up-to-date.
- Comply with all reasonable instructions we issue regarding the safe use of your Device and the App.
- Not use the App in any unlawful manner, for any unlawful purpose, or in any manner inconsistent with these Terms, or act fraudulently
or maliciously (e.g., by hacking into or inserting malicious code into the App or your Device's operating system).
- Not download the App from anywhere other than an app store approved by us (e.g., Apple App Store, Google Play Store) or install or use
it on a jail-broken or rooted device.
- Delete the App if you change or dispose of a Device that you use to access the Services.
5. Security
We employ reasonable security measures to protect your information and transactions conducted through the App. However, you acknowledge
that no system is entirely secure. You are responsible for maintaining the security of your Device and Security Credentials. We are not
liable for damages arising from virus contamination in your IT system or if the parameters of your browser are different from the
required technical conditions.
6. Privacy
Your privacy is important to us. Our Privacy Policy explains how we collect, use, and protect your personal information in connection
with your use of the App and Services. By using the App, you consent to such collection, use, and protection as described in our Privacy
Policy.
7. Limitations of Liability and Disclaimer of Warranty
THE APP AND SERVICES ARE PROVIDED "AS IS" AND "AS AVAILABLE" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. We do not
guarantee continuous, uninterrupted, or secure access to any part of our Service, and operation of the App or the Services may be
interfered with by numerous factors outside of our control.
IN NO EVENT SHALL WE OR OUR AFFILIATES, LICENSORS, OR CONTRACTORS BE LIABLE FOR ANY CLAIM, ARISING FROM OR RELATED TO THE MOBILE BANKING
APP OR THE SERVICES, THAT YOU DO NOT STATE IN WRITING IN A COMPLAINT FILED IN A COURT OR ARBITRATION PROCEEDING WITHIN TWO (2) YEARS OF
THE DATE THAT THE EVENT GIVING RISE TO THE CLAIM OCCURRED. THESE LIMITATIONS WILL APPLY TO ALL CAUSES OF ACTION, WHETHER ARISING FROM
BREACH OF CONTRACT, TORT (INCLUDING NEGLIGENCE) OR ANY OTHER LEGAL THEORY.
8. Intellectual Property
All intellectual property rights in the App and its content (excluding user-generated content) are owned by The Bank or its licensors.
You are granted a limited license to use the App as set forth in these Terms, but no ownership rights are transferred to you. You must
not remove or tamper with any copyright notice attached to or contained within the App.
9. Termination
We may terminate or suspend your access to the App and Services immediately, without prior notice or liability, for any reason
whatsoever, including without limitation if you breach these Terms. You may stop using the App at any time. If you wish to deregister
your digital banking access, you need to notify us.
10. Changes to Terms
We reserve the right to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice
prior to any new terms taking effect. Your continued use of the App after any such changes constitutes your acceptance of the new Terms.
11. Governing Law and Dispute Resolution
These Terms shall be governed and construed in accordance with the laws of your local jurisdiction, without regard to its conflict of
law provisions. Any dispute arising under these Terms shall be resolved in the courts located in your local jurisdiction.
12. Contact Information
If you have any questions about these Terms, please contact us through the channels provided on our official website or within the App.
13. Electronic Communications
By using the App, you consent to receive electronic communications from us. These communications may include notices about your account,
transactional information, and marketing materials.
14. Third-Party Services
The App may integrate with or provide links to third-party services. We are not responsible for the content, privacy policies, or
practices of any third-party websites or services.
15. Indemnification
You agree to indemnify and hold harmless The Bank, its affiliates, officers, directors, employees, and agents from any and all claims,
liabilities, damages, losses, and expenses, including reasonable attorneys' fees, arising out of or in any way connected with your
access to or use of the App and Services.
""";
void _handleProceed() async {
if (_isLoading) return;
setState(() {
_isLoading = true;
});
await widget.onProceed();
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
@override
void dispose() {
_scrollController.dispose(); // --- NEW: Dispose the ScrollController ---
super.dispose();
}
@override
Widget build(BuildContext context) {
final screenSize = MediaQuery.of(context).size;
return AlertDialog(
title: const Text('Terms and Conditions'),
content: SizedBox(
height: screenSize.height * 0.5, // 50% of screen height
width: screenSize.width * 0.9, // 90% of screen width
// --- MODIFIED: Use a Column to separate scrollable text from fixed checkbox ---
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// --- NEW: Expanded Scrollbar for the TNC text ---
Expanded(
child: Scrollbar(
controller: _scrollController,
thumbVisibility: true, // Always show the scrollbar thumb
// To place the scrollbar on the left, you might need to wrap
// this in a Directionality widget or use a custom scrollbar.
// For now, it will appear on the right as is standard.
child: SingleChildScrollView(
controller: _scrollController,
child: _isLoading
? const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
)
: Text(_termsAndConditionsText),
),
),
),
const SizedBox(height: 16), // Space between text and checkbox
// --- MODIFIED: Checkbox Row is now outside the SingleChildScrollView ---
Row(
children: [
Checkbox(
value: _isAgreed,
onChanged: (bool? value) {
setState(() {
_isAgreed = value ?? false;
});
},
),
const Flexible(
child: Text('I agree to the Terms and Conditions')),
],
),
],
),
),
actions: [
TextButton(
onPressed: _isLoading
? null
: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'You must agree to the terms and conditions to proceed.'),
behavior: SnackBarBehavior.floating,
),
);
},
child: const Text('Disagree'),
),
ElevatedButton(
onPressed: _isAgreed && !_isLoading ? _handleProceed : null,
child: const Text('Proceed'),
),
],
);
}
}

View File

@@ -144,4 +144,4 @@ flutter:
flutter_icons:
android: true
ios: true
image_path: assets/images/icon.png
image_path: assets/images/logo.png