31 Commits

Author SHA1 Message Date
715162b713 Test Sim Binding APK 2025-12-11 12:18:25 +05:30
8149ef2a5b Sim Binding Done #1 2025-12-10 11:50:02 +05:30
1a2dea611b SMS integrated with new ui 2025-12-09 18:11:46 +05:30
72a2c56392 Code Formatting 2025-12-05 16:02:49 +05:30
aef82237ac OTP added to daily limit #2 2025-12-05 15:59:59 +05:30
974f42bf95 OTP added to daily limit 2025-12-05 12:37:38 +05:30
4a8c69bb1e Account Info card changes and snackbar in statement #3 2025-12-04 16:50:08 +05:30
86aaaa1f6d Account Info card changes and snackbar in statement #2 2025-12-04 15:56:22 +05:30
6796793aac Account Info card changes and snackbar in statement 2025-12-04 15:40:24 +05:30
fbf6df7181 Icon and logo issues fixed 2025-12-04 12:44:39 +05:30
c7111d518a PDF Edited 2025-12-03 18:05:34 +05:30
5d307607fd Download notification created and Profile added in quick links 2025-12-02 17:06:49 +05:30
992092052a Customerf Info page changed and landing page changed 2025-12-02 13:31:40 +05:30
64fedabd89 Subtitles added in payment tabs with Localizations 2025-12-01 16:28:42 +05:30
4fc6f54fcd Subtitles added in payment tabs 2025-12-01 16:13:27 +05:30
8c7e94759a APK Build #1 2025-12-01 12:58:17 +05:30
8aa5b170ca Account Statement UI Changed 2025-11-28 12:22:51 +05:30
04a1ce26ec Profile Changed and Customer Info 2025-11-28 11:28:01 +05:30
b19bc2e222 Extras removed 2025-11-27 11:47:39 +05:30
b9147b30d5 Enquiry_screen ui changed 2025-11-25 14:43:52 +05:30
3358ec7669 UI #1 2025-11-25 12:51:29 +05:30
18db360a45 Code Formatted 2025-11-24 18:18:36 +05:30
b7fe6a9d18 View All Created 2025-11-24 18:16:53 +05:30
adb9a5330b Account Info card UI changed 2025-11-24 15:50:01 +05:30
0075abc906 Bottom navigation changed 2025-11-24 13:26:33 +05:30
353ec63916 3 changes 2025-11-24 12:56:06 +05:30
71b52cfb43 dashboard Screen UI changed 2025-11-24 12:20:35 +05:30
c1df43e9b6 dashboard#1 2025-11-20 17:45:29 +05:30
f0d5233afc Beneficiary lists changed 2025-11-20 13:31:42 +05:30
4fe6af4098 dark Theme and 18 other changes done 2025-11-20 12:33:30 +05:30
fda5d075ff Profile Screen 2025-11-18 11:33:22 +05:30
54 changed files with 3752 additions and 1684 deletions

View File

@@ -34,6 +34,7 @@ android {
ndkVersion "27.0.12077973" ndkVersion "27.0.12077973"
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@@ -80,4 +81,6 @@ flutter {
source '../..' source '../..'
} }
dependencies {} dependencies {
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
}

View File

@@ -2,6 +2,8 @@
<uses-permission android:name="android.permission.USE_BIOMETRIC"/> <uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/> <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application <application
android:label="kmobile" android:label="kmobile"
android:name="${applicationName}" android:name="${applicationName}"

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
assets/images/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/images/profile.svg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -8,6 +8,41 @@ class AuthService {
final Dio _dio; final Dio _dio;
AuthService(this._dio); AuthService(this._dio);
Future<void> simVerify(String uuid, String cifNo) async {
try {
final response = await _dio.post('/api/sim-details-verify', data: {
'uuid': uuid,
'cifNo': cifNo,
});
if (response.statusCode == 200) {
final String message = response.data.toString().toUpperCase();
if (message.contains("VERIFIED")) {
return; // Success
} else {
throw AuthException(message); // Throw message received
}
} else {
throw AuthException('Verification Failed');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e.toString());
}
if (e.response?.statusCode == 401) {
throw AuthException(
e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
}
throw NetworkException('Network error during verification');
} catch (e) {
throw UnexpectedException(
'Unexpected error during verification: ${e.toString()}');
}
}
Future<AuthToken> login(AuthCredentials credentials) async { Future<AuthToken> login(AuthCredentials credentials) async {
try { try {
final response = await _dio.post( final response = await _dio.post(

View File

@@ -1,47 +1,47 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
class Branch{ class Branch {
final String branch_code; final String branch_code;
final String branch_name; final String branch_name;
final String zone; final String zone;
final String tehsil; final String tehsil;
final String block; final String block;
final String block_code; final String block_code;
final String distt_name; final String distt_name;
final String distt_code_slbc; final String distt_code_slbc;
final String date_of_opening; final String date_of_opening;
final String rbi_code_1; final String rbi_code_1;
final String rbi_code_2; final String rbi_code_2;
final String telephone_no; final String telephone_no;
final String type_of_branch; final String type_of_branch;
final String rtgs_acct_no; final String rtgs_acct_no;
final String br_lattitude; final String br_lattitude;
final String br_longitude; final String br_longitude;
final String pincode; final String pincode;
final String post_office; final String post_office;
Branch({ Branch({
required this.branch_code, required this.branch_code,
required this.branch_name, required this.branch_name,
required this.zone, required this.zone,
required this.tehsil, required this.tehsil,
required this.block, required this.block,
required this.block_code, required this.block_code,
required this.distt_name, required this.distt_name,
required this.distt_code_slbc, required this.distt_code_slbc,
required this.date_of_opening, required this.date_of_opening,
required this.rbi_code_1, required this.rbi_code_1,
required this.rbi_code_2, required this.rbi_code_2,
required this.telephone_no, required this.telephone_no,
required this.type_of_branch, required this.type_of_branch,
required this.rtgs_acct_no, required this.rtgs_acct_no,
required this.br_lattitude, required this.br_lattitude,
required this.br_longitude, required this.br_longitude,
required this.pincode, required this.pincode,
required this.post_office, required this.post_office,
}); });
factory Branch.fromJson(Map<String, dynamic> json) { factory Branch.fromJson(Map<String, dynamic> json) {
return Branch( return Branch(
branch_code: json['branch_code'] ?? json['branch_code'] ?? '', branch_code: json['branch_code'] ?? json['branch_code'] ?? '',
branch_name: json['branch_name'] ?? json['branch_name'] ?? '', branch_name: json['branch_name'] ?? json['branch_name'] ?? '',
@@ -60,33 +60,32 @@ factory Branch.fromJson(Map<String, dynamic> json) {
br_lattitude: json['br_lattitude'] ?? json['br_lattitude'] ?? '', br_lattitude: json['br_lattitude'] ?? json['br_lattitude'] ?? '',
br_longitude: json['br_longitude'] ?? json['br_longitude'] ?? '', br_longitude: json['br_longitude'] ?? json['br_longitude'] ?? '',
pincode: json['pincode'] ?? json['pincode'] ?? '', pincode: json['pincode'] ?? json['pincode'] ?? '',
post_office: json['post_office'] ?? json['post_office'] ?? '', post_office: json['post_office'] ?? json['post_office'] ?? '',
); );
} }
static List<Branch> listFromJson(List<dynamic> jsonList) { static List<Branch> listFromJson(List<dynamic> jsonList) {
final beneficiaryList = jsonList final beneficiaryList =
.map((beneficiary) => Branch.fromJson(beneficiary)) jsonList.map((beneficiary) => Branch.fromJson(beneficiary)).toList();
.toList();
return beneficiaryList; return beneficiaryList;
} }
} }
class Atm { class Atm {
final String name; final String name;
Atm({required this.name}); Atm({required this.name});
factory Atm.fromJson(Map<String, dynamic> json) { factory Atm.fromJson(Map<String, dynamic> json) {
return Atm( return Atm(
name: json['name'] ?? '', // Assuming the API returns a 'name' field name: json['name'] ?? '', // Assuming the API returns a 'name' field
); );
} }
static List<Atm> listFromJson(List<dynamic> jsonList) { static List<Atm> listFromJson(List<dynamic> jsonList) {
return jsonList.map((atm) => Atm.fromJson(atm)).toList(); return jsonList.map((atm) => Atm.fromJson(atm)).toList();
} }
} }
class BranchService { class BranchService {
final Dio _dio; final Dio _dio;
@@ -113,7 +112,7 @@ class BranchService {
} }
} }
Future<List<Atm>> fetchAtmList() async { Future<List<Atm>> fetchAtmList() async {
try { try {
final response = await _dio.get( final response = await _dio.get(
"/api/atm", "/api/atm",
@@ -134,4 +133,4 @@ class BranchService {
return []; return [];
} }
} }
} }

View File

@@ -54,4 +54,34 @@ class LimitService {
throw Exception('Unexpected error: ${e.toString()}'); throw Exception('Unexpected error: ${e.toString()}');
} }
} }
Future getOtpTLimit({
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/send',
data: {'mobileNumber': mobileNumber, 'type': "TLIMIT"},
);
if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type");
}
print(response.toString());
return response.toString();
}
Future validateOtp({
required String otp,
required String mobileNumber,
}) async {
final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber',
data: {
'otp': otp,
},
);
if (response.statusCode != 200) {
throw Exception("Wrong OTP");
}
return response.toString();
}
} }

View File

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

View File

@@ -12,8 +12,8 @@ import 'package:kmobile/features/auth/controllers/theme_state.dart';
import 'config/routes.dart'; import 'config/routes.dart';
import 'di/injection.dart'; import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart'; import 'features/auth/controllers/auth_cubit.dart';
import 'features/card/screens/card_management_screen.dart'; import 'features/accounts/screens/account_statement_screen.dart';
import 'features/auth/screens/splash_screen.dart'; import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'features/auth/screens/login_screen.dart'; import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart'; import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart'; import 'features/dashboard/screens/dashboard_screen.dart';
@@ -37,7 +37,6 @@ class KMobile extends StatefulWidget {
class _KMobileState extends State<KMobile> with WidgetsBindingObserver { class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
Timer? _backgroundTimer; Timer? _backgroundTimer;
bool showSplash = true;
Locale? _locale; Locale? _locale;
@override @override
@@ -45,11 +44,6 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
loadPreferences(); loadPreferences();
Future.delayed(const Duration(seconds: 3), () {
setState(() {
showSplash = false;
});
});
} }
@override @override
@@ -131,9 +125,12 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
theme: themeState.getLightThemeData(), theme: themeState.getLightThemeData(),
darkTheme: themeState.getDarkThemeData(), darkTheme: themeState.getDarkThemeData(),
themeMode: context.watch<ThemeModeCubit>().state.mode, themeMode: context.watch<ThemeModeCubit>().state.mode,
navigatorObservers: [
getIt<RouteObserver<ModalRoute<void>>>(),
],
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash, initialRoute: AppRoutes.splash,
home: showSplash ? const SplashScreen() : const AuthGate(), home: const AuthGate(),
); );
}, },
); );
@@ -205,7 +202,7 @@ class _AuthGateState extends State<AuthGate> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_checking) { if (_checking) {
return const SplashScreen(); return const LoginScreen();
} }
if (_isLoggedIn) { if (_isLoggedIn) {
if (_hasMPin) { if (_hasMPin) {
@@ -214,7 +211,7 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(), future: _tryBiometric(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen(); return const LoginScreen();
} }
if (snapshot.data == true) { if (snapshot.data == true) {
return const NavigationScaffold(); return const NavigationScaffold();
@@ -316,7 +313,21 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardScreen(), const DashboardScreen(),
// const CardManagementScreen(), BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
if (state is Authenticated) {
if (state.users.isNotEmpty) {
return AccountStatementScreen(
users: state.users,
selectedIndex: 0,
);
} else {
return const Center(child: Text("No accounts found."));
}
}
return const Center(child: CircularProgressIndicator());
},
),
const ServiceScreen(), const ServiceScreen(),
]; ];
@@ -374,10 +385,10 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
icon: const Icon(Icons.home_filled), icon: const Icon(Icons.home_filled),
label: AppLocalizations.of(context).home, label: AppLocalizations.of(context).home,
), ),
// BottomNavigationBarItem( BottomNavigationBarItem(
// icon: const Icon(Icons.credit_card), icon: const Icon(Icons.swap_vert_sharp),
// label: AppLocalizations.of(context).card, label: AppLocalizations.of(context).transactions,
// ), ),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: const Icon(Icons.miscellaneous_services), icon: const Icon(Icons.miscellaneous_services),
label: AppLocalizations.of(context).services, label: AppLocalizations.of(context).services,
@@ -422,7 +433,7 @@ class BiometricPromptScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context)); Future.microtask(() => _showDialog(context));
return const SplashScreen(); return const SizedBox.shrink();
} }
Future<void> _showDialog(BuildContext context) async { Future<void> _showDialog(BuildContext context) async {

View File

@@ -15,7 +15,17 @@ class AppThemes {
colorScheme: colorScheme, colorScheme: colorScheme,
useMaterial3: true, useMaterial3: true,
textTheme: GoogleFonts.rubikTextTheme(), textTheme: GoogleFonts.rubikTextTheme(),
); ).copyWith(
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF01A04C),
titleTextStyle: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w700,
fontSize: 20,
),
iconTheme: IconThemeData(color: colorScheme.onPrimary),
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
));
} }
static ThemeData getDarkTheme(ThemeType type) { static ThemeData getDarkTheme(ThemeType type) {
@@ -32,7 +42,17 @@ class AppThemes {
textTheme: GoogleFonts.rubikTextTheme( textTheme: GoogleFonts.rubikTextTheme(
ThemeData(brightness: Brightness.dark).textTheme, ThemeData(brightness: Brightness.dark).textTheme,
), ),
); ).copyWith(
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF01A04C),
titleTextStyle: TextStyle(
color: colorScheme.onPrimary,
fontWeight: FontWeight.w700,
fontSize: 20,
),
iconTheme: IconThemeData(color: colorScheme.onPrimary),
actionsIconTheme: IconThemeData(color: colorScheme.onPrimary),
));
} }
static Color _getSeedColor(ThemeType type) { static Color _getSeedColor(ThemeType type) {

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart'; import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/api/services/limit_service.dart'; import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
@@ -21,6 +22,8 @@ import '../security/secure_storage.dart';
final getIt = GetIt.instance; final getIt = GetIt.instance;
Future<void> setupDependencies() async { Future<void> setupDependencies() async {
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
RouteObserver<ModalRoute<void>>());
//getIt.registerLazySingleton<ThemeController>(() => ThemeController()); //getIt.registerLazySingleton<ThemeController>(() => ThemeController());
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController()); //getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
getIt.registerSingleton<ThemeCubit>(ThemeCubit()); getIt.registerSingleton<ThemeCubit>(ThemeCubit());
@@ -71,9 +74,9 @@ Dio _createDioClient() {
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: baseUrl:
// 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
'https://kccbmbnk.net', //prod small //'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60), connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60),
headers: { headers: {

View File

@@ -18,16 +18,45 @@ class AccountInfoScreen extends StatefulWidget {
class _AccountInfoScreen extends State<AccountInfoScreen> { class _AccountInfoScreen extends State<AccountInfoScreen> {
late User selectedUser; late User selectedUser;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
selectedUser = widget.users[widget.selectedIndex]; selectedUser = widget.users[widget.selectedIndex];
} }
String getFullAccountType(String? accountType) {
if (accountType == null || accountType.isEmpty) return 'N/A';
// Convert to title case
switch (accountType.toLowerCase()) {
case 'sa':
return AppLocalizations.of(context).savingsAccount;
case 'sb':
return AppLocalizations.of(context).savingsAccount;
case 'ln':
return AppLocalizations.of(context).loanAccount;
case 'td':
return AppLocalizations.of(context).termDeposit;
case 'rd':
return AppLocalizations.of(context).recurringDeposit;
case 'ca':
return "Current Account";
case 'cc':
return "Cash Credit Account";
case 'od':
return "Overdraft Account";
default:
return AppLocalizations.of(context).unknownAccount;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final users = widget.users; final users = widget.users;
int selectedIndex = widget.selectedIndex; int selectedIndex = widget.selectedIndex;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context) title: Text(AppLocalizations.of(context)
@@ -36,70 +65,106 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
ListView( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(8.0),
children: [ child: Column(
Text( children: [
AppLocalizations.of(context).accountNumber, Card(
style: elevation: 4,
const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), margin: const EdgeInsets.symmetric(vertical: 8.0),
), child: Padding(
padding: const EdgeInsets.all(16.0),
DropdownButton<User>( child: Column(
value: selectedUser, crossAxisAlignment: CrossAxisAlignment.start,
onChanged: (User? newUser) { children: [
if (newUser != null) { Text(
setState(() { AppLocalizations.of(context).accountNumber,
selectedUser = newUser; style: const TextStyle(
}); fontWeight: FontWeight.bold, fontSize: 18),
} ),
}, DropdownButton<User>(
items: widget.users.map((user) { value: selectedUser,
return DropdownMenuItem<User>( onChanged: (User? newUser) {
value: user, if (newUser != null) {
child: Text(user.accountNo.toString()), setState(() {
); selectedUser = newUser;
}).toList(), });
), }
},
InfoRow( items: widget.users.map((user) {
title: AppLocalizations.of(context).customerNumber, return DropdownMenuItem<User>(
value: selectedUser.cifNumber ?? 'N/A', value: user,
), child: Text(
InfoRow( user.accountNo.toString(),
title: AppLocalizations.of(context).productName, style: const TextStyle(
value: selectedUser.productType ?? 'N/A', fontSize: 20, fontWeight: FontWeight.bold),
), ),
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'), );
InfoRow( }).toList(),
title: AppLocalizations.of(context).accountStatus, isExpanded: true,
value: 'OPEN', ),
), ],
InfoRow( ),
title: AppLocalizations.of(context).availableBalance, ),
value: selectedUser.availableBalance ?? 'N/A', ),
), Expanded(
InfoRow( child: Card(
title: AppLocalizations.of(context).currentBalance, elevation: 4,
value: selectedUser.currentBalance ?? 'N/A', margin: const EdgeInsets.symmetric(vertical: 8.0),
), child: Padding(
padding: const EdgeInsets.all(16.0),
users[selectedIndex].approvedAmount != null child: ListView(
? InfoRow( children: [
title: AppLocalizations.of(context).approvedAmount, InfoRow(
value: selectedUser.approvedAmount ?? 'N/A', title: AppLocalizations.of(context).customerNumber,
) value: selectedUser.cifNumber ?? 'N/A',
: const SizedBox.shrink(), ),
], InfoRow(
title: AppLocalizations.of(context).accountType,
value: getFullAccountType(selectedUser.accountType),
),
InfoRow(
title: AppLocalizations.of(context).productName,
value: selectedUser.productType ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).accountStatus,
value: 'OPEN',
),
InfoRow(
title:
AppLocalizations.of(context).availableBalance,
value: selectedUser.availableBalance ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).currentBalance,
value: selectedUser.currentBalance ?? 'N/A',
),
if (users[selectedIndex].approvedAmount != null)
InfoRow(
title:
AppLocalizations.of(context).approvedAmount,
value: selectedUser.approvedAmount ?? 'N/A',
),
],
),
),
),
),
],
),
), ),
IgnorePointer( IgnorePointer(
child: Center( child: Center(
child: Opacity( child: Opacity(
opacity: 0.07, // Reduced opacity opacity: 0.07, // Reduced opacity
child: ClipOval( child: ClipOval(
child: Image.asset( child: Image.asset(
'assets/images/logo.png', 'assets/images/logo.png',
width: 200, // Adjust size as needed width: 200, // Adjust size as needed
height: 200, // Adjust size as needed height: 200, // Adjust size as needed
), ),
), ),
@@ -130,15 +195,18 @@ class InfoRow extends StatelessWidget {
Text( Text(
title, title,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 18,
fontWeight: FontWeight.w500, fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
Text( Text(
value, value,
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface), style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurface),
), ),
], ],
), ),

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -42,7 +42,9 @@ class TransactionDetailsScreen extends StatelessWidget {
isCredit isCredit
? Symbols.call_received ? Symbols.call_received
: Symbols.call_made, : Symbols.call_made,
color: isCredit ? Colors.green : Colors.red, color: isCredit
? const Color(0xFF10BB10)
: Theme.of(context).colorScheme.error,
size: 28, size: 28,
), ),
], ],
@@ -51,9 +53,9 @@ class TransactionDetailsScreen extends StatelessWidget {
// Date centered // Date centered
Text( Text(
transaction.date ?? "", transaction.date ?? "",
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
color: Colors.grey, color: Theme.of(context).textTheme.bodySmall?.color,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -61,7 +63,7 @@ class TransactionDetailsScreen extends StatelessWidget {
), ),
), ),
), ),
const Divider(), Divider(color: Theme.of(context).dividerColor),
Expanded( Expanded(
flex: 5, flex: 5,
child: ListView( child: ListView(

View File

@@ -3,6 +3,7 @@ import 'package:kmobile/app.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/set_password_screen.dart'; import 'package:kmobile/features/auth/screens/set_password_screen.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart'; import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/widgets/tnc_dialog.dart'; import 'package:kmobile/widgets/tnc_dialog.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -30,12 +31,23 @@ class LoginScreenState extends State<LoginScreen>
super.dispose(); super.dispose();
} }
void _submitForm() { void _submitForm() async {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login( final bool? verificationSuccess = await Navigator.of(context).push(
_customerNumberController.text.trim(), MaterialPageRoute(
_passwordController.text, builder: (_) => VerificationScreen(
); customerNo: _customerNumberController.text.trim(),
password: _passwordController.text,
),
),
);
if (verificationSuccess == true && mounted) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
}
} }
} }

View File

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

View File

@@ -0,0 +1,165 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/auth_service.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
class VerificationScreen extends StatefulWidget {
final String customerNo;
final String password;
const VerificationScreen({
super.key,
required this.customerNo,
required this.password,
});
@override
State<VerificationScreen> createState() => _VerificationScreenState();
}
class _VerificationScreenState extends State<VerificationScreen> {
String _statusMessage = "Starting verification...";
Timer? _timer;
int _countdown = 120;
bool _isVerifying = false;
bool _verificationFailed = false;
@override
void initState() {
super.initState();
_startVerificationProcess();
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _startTimer() {
_timer?.cancel(); // Cancel any existing timer
_countdown = 120;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (_countdown > 0) {
setState(() {
_countdown--;
});
} else {
_timer?.cancel();
if (mounted) {
setState(() {
_statusMessage = "Verification timed out. Please try again.";
_isVerifying = false;
_verificationFailed = true;
});
}
}
});
}
Future<void> _startVerificationProcess() async {
setState(() {
_isVerifying = true;
_verificationFailed = false;
_statusMessage = "Starting verification...";
});
_startTimer();
// 1. Send SMS
setState(() {
_statusMessage = "SMS sending...";
});
final smsHelper = SmsVerificationHelper();
final uuid = await smsHelper.initiateSmsSequence(context: context);
if (uuid != null && mounted) {
// SMS sending was successful, now wait before verifying.
setState(() {
_statusMessage = "SMS sent. Waiting for network delivery...";
});
// Adding a 10-second delay to account for SMS network latency.
await Future.delayed(const Duration(seconds: 10));
if (!mounted) return;
// 2. Verify SIM
setState(() {
_statusMessage = "Verifying with server...";
});
final authService = getIt<AuthService>();
try {
await authService.simVerify(uuid, widget.customerNo);
setState(() {
_statusMessage = "Verification successful!";
_isVerifying = false;
});
_timer?.cancel();
// Pop with success result
Navigator.of(context).pop(true);
} catch (e) {
setState(() {
_statusMessage = e.toString();
_isVerifying = false;
_verificationFailed = true;
});
}
} else if (mounted) {
setState(() {
_statusMessage =
"SMS sending failed. Please check permissions and try again.";
_isVerifying = false;
_verificationFailed = true;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Device Verification"),
automaticallyImplyLeading: !_isVerifying,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_isVerifying) const CircularProgressIndicator(),
if (!_isVerifying && _verificationFailed)
const Icon(Icons.error_outline, color: Colors.red, size: 50),
if (!_isVerifying && !_verificationFailed)
const Icon(Icons.check_circle_outline,
color: Colors.green, size: 50),
const SizedBox(height: 32),
Text(
_statusMessage,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 18),
),
const SizedBox(height: 16),
if (_isVerifying)
Text(
"Time remaining: $_countdown seconds",
style: const TextStyle(fontSize: 16, color: Colors.grey),
),
if (_verificationFailed && !_isVerifying) ...[
const SizedBox(height: 20),
ElevatedButton(
onPressed: _startVerificationProcess,
child: const Text('Retry'),
),
]
],
),
),
),
);
}
}

View File

@@ -22,7 +22,9 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
_showSuccessDialog(context); _showSuccessDialog(context);
} catch (e) { } catch (e) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')), SnackBar(
content: Text(
'${AppLocalizations.of(context).failedToDeleteBeneficiary} : $e')),
); );
} }
} }
@@ -33,7 +35,8 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).success), title: Text(AppLocalizations.of(context).success),
content: Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully), content:
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(AppLocalizations.of(context).ok), child: Text(AppLocalizations.of(context).ok),
@@ -53,8 +56,8 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).deleteBeneficiary), title: Text(AppLocalizations.of(context).deleteBeneficiary),
content: content: Text(AppLocalizations.of(context)
Text(AppLocalizations.of(context).areYouSureYouWantToDeleteThisBeneficiary), .areYouSureYouWantToDeleteThisBeneficiary),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
child: Text(AppLocalizations.of(context).cancel), child: Text(AppLocalizations.of(context).cancel),

View File

@@ -21,21 +21,47 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
var service = getIt<BeneficiaryService>(); var service = getIt<BeneficiaryService>();
bool _isLoading = true; bool _isLoading = true;
List<Beneficiary> _beneficiaries = []; List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBeneficiaries(); _loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
} }
Future<void> _loadBeneficiaries() async { Future<void> _loadBeneficiaries() async {
final data = await service.fetchBeneficiaryList(); final data = await service.fetchBeneficiaryList();
setState(() { setState(() {
_beneficiaries = data; _beneficiaries = data;
_filteredBeneficiaries = data;
_isLoading = false; _isLoading = false;
}); });
} }
void _filterBeneficiaries(String query) {
setState(() {
if (query.isEmpty) {
_filteredBeneficiaries = _beneficiaries;
} else {
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
final lowerQuery = query.toLowerCase();
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
Widget _buildShimmerList() { Widget _buildShimmerList() {
return ListView.builder( return ListView.builder(
itemCount: 6, itemCount: 6,
@@ -63,40 +89,43 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
} }
Widget _buildBeneficiaryList() { Widget _buildBeneficiaryList() {
if (_beneficiaries.isEmpty) { if (_filteredBeneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
} }
return ListView.builder( return ListView.builder(
itemCount: _beneficiaries.length, itemCount: _filteredBeneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = _beneficiaries[index]; final item = _filteredBeneficiaries[index];
return ListTile( return Card(
leading: CircleAvatar( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
radius: 24, child: ListTile(
backgroundColor: Colors.transparent, leading: CircleAvatar(
child: getBankLogo(item.bankName, context), radius: 24,
), backgroundColor: Colors.transparent,
title: Text(item.name), child: getBankLogo(item.bankName, context),
subtitle: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, title: Text(item.name),
children: [ subtitle: Column(
Text(item.accountNo), crossAxisAlignment: CrossAxisAlignment.start,
if (item.bankName != null && item.bankName!.isNotEmpty) children: [
Text( Text(item.accountNo),
item.bankName!, if (item.bankName != null && item.bankName!.isNotEmpty)
style: TextStyle(fontSize: 12, color: Colors.grey[600]), Text(
item.bankName!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BeneficiaryDetailsScreen(beneficiary: item),
), ),
], );
},
), ),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BeneficiaryDetailsScreen(beneficiary: item),
),
);
},
); );
}, },
); );
@@ -111,7 +140,27 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(), Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer( IgnorePointer(
child: Center( child: Center(
child: Opacity( child: Opacity(

View File

@@ -56,8 +56,6 @@ class _BlockCardScreen extends State<BlockCardScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).blockCard, AppLocalizations.of(context).blockCard,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),

View File

@@ -33,20 +33,6 @@ 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
),
),
),
),
),
], ],
), ),
); );
@@ -105,7 +91,7 @@ class CardTile extends StatelessWidget {
"Kangra Central Co-operative Bank", "Kangra Central Co-operative Bank",
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 15.5,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,

View File

@@ -35,7 +35,7 @@ class _CardManagementScreen extends State<CardManagementScreen> {
onTap: () {}, onTap: () {},
disabled: true, // Add this disabled: true, // Add this
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile( CardManagementTile(
icon: Symbols.remove_moderator, icon: Symbols.remove_moderator,
label: AppLocalizations.of(context).blockUnblockCard, label: AppLocalizations.of(context).blockUnblockCard,
@@ -49,7 +49,7 @@ class _CardManagementScreen extends State<CardManagementScreen> {
}, },
disabled: true, disabled: true,
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile( CardManagementTile(
icon: Symbols.password_2, icon: Symbols.password_2,
label: AppLocalizations.of(context).changeCardPin, label: AppLocalizations.of(context).changeCardPin,
@@ -63,7 +63,7 @@ class _CardManagementScreen extends State<CardManagementScreen> {
}, },
disabled: true, disabled: true,
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
CardManagementTile( CardManagementTile(
icon: Symbols.payment_card, icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils, label: AppLocalizations.of(context).viewCardDeatils,
@@ -75,9 +75,9 @@ class _CardManagementScreen extends State<CardManagementScreen> {
), ),
); );
}, },
disabled: true, disabled: false,
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
], ],
), ),
IgnorePointer( IgnorePointer(

View File

@@ -46,8 +46,6 @@ class _CardPinChangeDetailsScreen extends State<CardPinChangeDetailsScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).cardDetails, AppLocalizations.of(context).cardDetails,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),

View File

@@ -46,8 +46,6 @@ class _CardPinSetScreen extends State<CardPinSetScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).cardPin, AppLocalizations.of(context).cardPin,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),

View File

@@ -17,8 +17,6 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).chequeManagement, AppLocalizations.of(context).chequeManagement,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),
@@ -32,7 +30,7 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
label: AppLocalizations.of(context).requestChequeBook, label: AppLocalizations.of(context).requestChequeBook,
onTap: () {}, onTap: () {},
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.data_alert, icon: Symbols.data_alert,
label: AppLocalizations.of(context).enquiry, label: AppLocalizations.of(context).enquiry,
@@ -44,31 +42,31 @@ class _ChequeManagementScreen extends State<ChequeManagementScreen> {
); );
}, },
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.approval_delegation, icon: Symbols.approval_delegation,
label: AppLocalizations.of(context).chequeDeposit, label: AppLocalizations.of(context).chequeDeposit,
onTap: () {}, onTap: () {},
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.front_hand, icon: Symbols.front_hand,
label: AppLocalizations.of(context).stopCheque, label: AppLocalizations.of(context).stopCheque,
onTap: () {}, onTap: () {},
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.cancel_presentation, icon: Symbols.cancel_presentation,
label: AppLocalizations.of(context).revokeStop, label: AppLocalizations.of(context).revokeStop,
onTap: () {}, onTap: () {},
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
ChequeManagementTile( ChequeManagementTile(
icon: Symbols.payments, icon: Symbols.payments,
label: AppLocalizations.of(context).positivePay, label: AppLocalizations.of(context).positivePay,
onTap: () {}, onTap: () {},
), ),
const Divider(height: 1), Divider(height: 1, color: Theme.of(context).dividerColor),
], ],
), ),
IgnorePointer( IgnorePointer(

View File

@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class CustomerInfoScreen extends StatefulWidget { class CustomerInfoScreen extends StatefulWidget {
@@ -13,6 +14,7 @@ class CustomerInfoScreen extends StatefulWidget {
class _CustomerInfoScreenState extends State<CustomerInfoScreen> { class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
late final User user = widget.user; late final User user = widget.user;
int _selectedCard = 0; // 0 for Personal Info, 1 for KYC
String _maskPrimaryId(String? primaryId) { String _maskPrimaryId(String? primaryId) {
if (primaryId == null || primaryId.length <= 4) { if (primaryId == null || primaryId.length <= 4) {
@@ -26,100 +28,207 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context) AppLocalizations.of(context)
.customerInfo .customerInfo
.replaceFirst(RegExp('\n'), ''), .replaceFirst(RegExp('\n'), ''),
),
), ),
), body: SafeArea(
body: Stack( child: Stack(
children: [ children: [
SingleChildScrollView( SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SafeArea(
child: Center(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 30), Card(
CircleAvatar( elevation: 0,
radius: 50, shape: RoundedRectangleBorder(
child: SvgPicture.asset( borderRadius: BorderRadius.circular(12),
'assets/images/avatar_male.svg', side: BorderSide(
width: 150, color: theme.colorScheme.outline.withOpacity(0.2),
height: 150, width: 1,
fit: BoxFit.cover, ),
), ),
), child: Padding(
Padding( padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.only(top: 10.0), child: Row(
child: Text( children: [
user.name ?? '', const SizedBox(
style: TextStyle( width: 56,
fontSize: 20, height: 56,
color: theme.colorScheme.onSurface, child: CircleAvatar(
fontWeight: FontWeight.w500, radius: 50,
child: Icon(
Symbols.person,
size: 56,
),
),
),
const SizedBox(width: 12),
// Name + mobile
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
// If you want to show the user's name instead, replace below.
user.name ?? '',
style:
theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
user.cifNumber ?? '',
style:
theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withOpacity(0.7),
),
),
],
),
),
],
), ),
), ),
), ),
Text( const SizedBox(height: 16),
'${AppLocalizations.of(context).cif}: ${user.cifNumber ?? 'N/A'}', // Toggle Buttons for Personal Info and KYC
style: TextStyle( SizedBox(
fontSize: 16, width: double.infinity,
color: theme.colorScheme.onSurfaceVariant), child: CupertinoSlidingSegmentedControl<int>(
groupValue: _selectedCard,
thumbColor: Theme.of(context)
.colorScheme
.onPrimary, // Set selected switch color to theme primary color
onValueChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_selectedCard = newValue;
});
}
},
children: {
0: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child: Text(
AppLocalizations.of(context).personaldetails),
),
1: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child:
Text(AppLocalizations.of(context).kycdetails),
),
},
),
), ),
const SizedBox(height: 30), const SizedBox(height: 16),
InfoField( // Card that shows content based on the toggle
label: AppLocalizations.of(context).activeAccounts, Card(
value: user.activeAccounts?.toString() ?? '6', elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _selectedCard == 0
? _buildPersonalInfo(theme)
: _buildKycDetails(theme),
),
),
), ),
InfoField(
label: AppLocalizations.of(context).mobileNumber,
value: user.mobileNo ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).dateOfBirth,
value: (user.dateOfBirth != null &&
user.dateOfBirth!.length == 8)
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
: 'N/A',
), // Replace with DOB if available
InfoField(
label: AppLocalizations.of(context).branchCode,
value: user.branchId ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).branchAddress,
value: user.address ?? 'N/A',
), // Replace with Aadhar if available
InfoField(
label: AppLocalizations.of(context).primaryId,
value: _maskPrimaryId(user.primaryId),
), // Replace with PAN if available
], ],
), ),
), ),
), ),
), IgnorePointer(
), child: Center(
IgnorePointer( child: Opacity(
child: Center( opacity: 0.07, // Reduced opacity
child: Opacity( child: ClipOval(
opacity: 0.07, // Reduced opacity child: Image.asset(
child: ClipOval( 'assets/images/logo.png',
child: Image.asset( width: 200, // Adjust size as needed
'assets/images/logo.png', height: 200, // Adjust size as needed
width: 200, // Adjust size as needed ),
height: 200, // Adjust size as needed ),
), ),
), ),
), ),
), ],
), ),
], ));
), }
Widget _buildPersonalInfo(ThemeData theme) {
return Column(
key: const ValueKey('personal_info'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).personaldetails,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
InfoField(
label: AppLocalizations.of(context).activeAccounts,
value: user.activeAccounts?.toString() ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).mobileNumber,
value: user.mobileNo ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).dateOfBirth,
value: (user.dateOfBirth != null && user.dateOfBirth!.length == 8)
? '${user.dateOfBirth!.substring(0, 2)}-${user.dateOfBirth!.substring(2, 4)}-${user.dateOfBirth!.substring(4, 8)}'
: 'N/A',
),
InfoField(
label: AppLocalizations.of(context).branchCode,
value: user.branchId ?? 'N/A',
),
InfoField(
label: AppLocalizations.of(context).address,
value: user.address ?? 'N/A',
),
],
);
}
Widget _buildKycDetails(ThemeData theme) {
return Column(
key: const ValueKey('kyc_details'),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).kycdetails,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
InfoField(
label: AppLocalizations.of(context).primaryId,
value: _maskPrimaryId(user.primaryId),
),
],
); );
} }
} }
@@ -141,16 +250,16 @@ class InfoField extends StatelessWidget {
children: [ children: [
Text( Text(
label, label,
style: TextStyle( style: theme.textTheme.bodySmall?.copyWith(
fontSize: 15, color: theme.colorScheme.onSurface.withOpacity(0.6),
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 3), const SizedBox(height: 4),
Text( Text(
value, value.isEmpty ? 'N/A' : value,
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface), style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
), ),
], ],
), ),

View File

@@ -1,12 +1,11 @@
import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/repositories/transaction_repository.dart'; import 'package:kmobile/data/models/user.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/accounts/screens/account_info_screen.dart'; import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart'; import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
import 'package:kmobile/features/accounts/screens/transaction_details_screen.dart'; import 'package:kmobile/features/accounts/screens/all_accounts_screen.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart'; import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart'; import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart'; import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
@@ -21,7 +20,6 @@ import 'package:local_auth/local_auth.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:kmobile/data/models/transaction.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
class DashboardScreen extends StatefulWidget { class DashboardScreen extends StatefulWidget {
@@ -32,47 +30,189 @@ class DashboardScreen extends StatefulWidget {
} }
class _DashboardScreenState extends State<DashboardScreen> class _DashboardScreenState extends State<DashboardScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin, RouteAware {
int selectedAccountIndex = 0; int selectedAccountIndex = 0;
bool isVisible = false; Map<String, bool> _visibilityMap = {};
bool isRefreshing = false; bool isRefreshing = false;
bool isBalanceLoading = false;
bool _biometricPromptShown = false;
bool _txLoading = false;
List<Transaction> _transactions = [];
bool _txInitialized = false;
Future<void> _loadTransactions(String accountNo) async { bool _biometricPromptShown = false;
bool _txInitialized = false;
PageController? _pageController;
final routeObserver = getIt<RouteObserver<ModalRoute<void>>>();
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void dispose() {
routeObserver.unsubscribe(this);
_pageController?.dispose();
_visibilityMap.clear();
super.dispose();
}
@override
void didPushNext() {
// This method is called when another route is pushed on top of this one.
// We clear the map and call setState to ensure the UI is updated
// if the user navigates back.
setState(() { setState(() {
_txLoading = true; _visibilityMap.clear();
_transactions = [];
}); });
try { }
final repo = getIt<TransactionRepository>();
final txs = await repo.fetchTransactions(accountNo); Widget _buildAccountCard(User user, bool isSelected) {
var fiveTxns = <Transaction>[]; final theme = Theme.of(context);
//only take the first 5 transactions final bool isCardVisible = _visibilityMap[user.accountNo] ?? false;
if (txs.length > 5) { // Animated scale for the selected card
fiveTxns = txs.sublist(0, 5); final scale = isSelected ? 1.02 : 0.9;
} else { return Padding(
fiveTxns = txs; padding: const EdgeInsets.symmetric(horizontal: 3.0),
} child: AnimatedScale(
setState(() => _transactions = fiveTxns); duration: const Duration(milliseconds: 200),
} catch (e) { scale: scale,
log(accountNo, error: e); child: Container(
if (!mounted) return; padding: const EdgeInsets.symmetric(
ScaffoldMessenger.of(context).showSnackBar( horizontal: 18,
SnackBar( vertical: 10,
content: Text( ),
AppLocalizations.of(context).failedToLoad(e.toString()), decoration: BoxDecoration(
color: const Color(0xFF01A04C),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top section with account type and number (no refresh button here)
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getFullAccountType(user.accountType),
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
Text(
user.accountNo ?? 'N/A',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 14,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
if (isSelected) // Show logo only if card is selected
CircleAvatar(
radius: 20,
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 30,
height: 30,
fit: BoxFit.cover,
),
),
),
),
],
),
const Spacer(),
// Bottom section with balance and combined toggle/refresh
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isRefreshing && isSelected)
Expanded(child: _buildBalanceShimmer())
else
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Row(
children: [
Text(
"",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
Text(
isCardVisible
? user.currentBalance ?? '0.00'
: '*****',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
const SizedBox(width: 10), // A steady space
if (isSelected) // Only show toggle for selected card
InkWell(
onTap: () async {
if (isRefreshing)
return; // Prevent taps while refreshing
final accountNo = user.accountNo;
if (accountNo == null) return;
final bool currentVisibility =
_visibilityMap[accountNo] ?? false;
if (!currentVisibility) {
// If hidden, refresh data and then show the balance
await _refreshAccountData(context);
if (mounted) {
setState(() {
_visibilityMap[accountNo] = true;
});
}
} else {
// If visible, just hide it
setState(() {
_visibilityMap[accountNo] = false;
});
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isCardVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: theme.scaffoldBackgroundColor,
weight: 800,
),
),
),
],
),
const Spacer(),
],
), ),
), ),
); ),
} finally { );
if (mounted) {
setState(() => _txLoading = false);
}
}
} }
Future<void> _refreshAccountData(BuildContext context) async { Future<void> _refreshAccountData(BuildContext context) async {
@@ -102,8 +242,7 @@ class _DashboardScreenState extends State<DashboardScreen>
return Shimmer.fromColors( return Shimmer.fromColors(
baseColor: theme.colorScheme.primary, baseColor: theme.colorScheme.primary,
highlightColor: theme.colorScheme.onPrimary, highlightColor: theme.colorScheme.onPrimary,
child: Container( child: Container(height: 36, color: theme.scaffoldBackgroundColor),
width: 200, height: 42, color: theme.scaffoldBackgroundColor),
); );
} }
@@ -150,6 +289,10 @@ class _DashboardScreenState extends State<DashboardScreen>
return AppLocalizations.of(context).recurringDeposit; return AppLocalizations.of(context).recurringDeposit;
case 'ca': case 'ca':
return "Current Account"; return "Current Account";
case 'cc':
return "Cash Credit Account";
case 'od':
return "Overdraft Account";
default: default:
return AppLocalizations.of(context).unknownAccount; return AppLocalizations.of(context).unknownAccount;
} }
@@ -200,6 +343,19 @@ class _DashboardScreenState extends State<DashboardScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
String customerNo = '';
String customerName = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 &&
selectedAccountIndex < authState.users.length) {
mobileNumberToPass =
authState.users[selectedAccountIndex].mobileNo ?? '';
customerNo = authState.users[selectedAccountIndex].cifNumber ?? '';
customerName = authState.users[selectedAccountIndex].name ?? '';
}
}
return BlocListener<AuthCubit, AuthState>( return BlocListener<AuthCubit, AuthState>(
listener: (context, state) async { listener: (context, state) async {
if (state is Authenticated && !_biometricPromptShown) { if (state is Authenticated && !_biometricPromptShown) {
@@ -214,52 +370,67 @@ class _DashboardScreenState extends State<DashboardScreen>
child: Scaffold( child: Scaffold(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor,
leading: Padding( leading: Padding(
padding: const EdgeInsets.only(left: 10.0), padding: const EdgeInsets.only(left: 10.0),
child: InkWell( child: Material(
borderRadius: BorderRadius.circular(20), elevation: 4.0,
onTap: () { clipBehavior: Clip.antiAlias,
final authState = context.read<AuthCubit>().state; shape: RoundedRectangleBorder(
String mobileNumberToPass = ''; side: const BorderSide(color: Colors.white, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
if (authState is Authenticated) { ),
if (selectedAccountIndex >= 0 && child: Container(
selectedAccountIndex < authState.users.length) { width: 40,
mobileNumberToPass = height: 40,
authState.users[selectedAccountIndex].mobileNo ?? ''; decoration: const BoxDecoration(
} color: Colors.white,
} ),
child: Image.asset(
Navigator.push( 'assets/images/logo.png',
context, fit: BoxFit.fill,
MaterialPageRoute(
builder: (context) =>
ProfileScreen(mobileNumber: mobileNumberToPass),
),
);
},
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
), ),
), ),
), ),
), ),
title: Text( title: Text(
AppLocalizations.of(context).kccbMobile, AppLocalizations.of(context).kccBankFull.replaceAll('-', '\u2011'),
textAlign: TextAlign.left, textAlign: TextAlign.center,
softWrap: true, // Explicitly allow wrapping
maxLines: 2, // Allow text to wrap to a maximum of 2 lines
style: TextStyle( style: TextStyle(
color: theme.colorScheme.primary, color: theme.colorScheme.onPrimary,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
fontSize: 20,
), ),
), ),
centerTitle: true, // Removed centerTitle: true to give more space for text wrapping
actions: [
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfileScreen(
mobileNumber: mobileNumberToPass,
customerNo: customerNo,
customerName: customerName),
),
);
},
child: const CircleAvatar(
radius: 21,
child: Icon(
Symbols.person,
size: 30,
),
),
),
),
],
), ),
body: BlocBuilder<AuthCubit, AuthState>( body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) { builder: (context, state) {
@@ -276,21 +447,23 @@ class _DashboardScreenState extends State<DashboardScreen>
// firsttime load // firsttime load
if (!_txInitialized) { if (!_txInitialized) {
_txInitialized = true; _txInitialized = true;
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {});
_loadTransactions(currAccount.accountNo!);
});
} }
_pageController ??= PageController(
initialPage: selectedAccountIndex,
viewportFraction: 0.75,
);
final firstName = getProcessedFirstName(currAccount.name); final firstName = getProcessedFirstName(currAccount.name);
return SingleChildScrollView( return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 16), // Added spacing
Padding( Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 4.0),
child: Text( child: Text(
"${AppLocalizations.of(context).hi} $firstName!", "${AppLocalizations.of(context).hi} $firstName!",
style: GoogleFonts.baumans().copyWith( style: GoogleFonts.baumans().copyWith(
@@ -302,176 +475,76 @@ class _DashboardScreenState extends State<DashboardScreen>
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Account Info Card // Account Info Cards
Container( SizedBox(
padding: const EdgeInsets.symmetric( height: 160,
horizontal: 18, child: PageView.builder(
vertical: 10, clipBehavior: Clip.none,
controller: _pageController,
itemCount:
users.length, // Keep this to show adjacent cards
onPageChanged: (int newIndex) async {
if (newIndex == selectedAccountIndex) return;
// Hide the balance of the old card when scrolling away
final oldAccountNo =
users[selectedAccountIndex].accountNo;
if (oldAccountNo != null) {
_visibilityMap[oldAccountNo] = false;
}
setState(() {
selectedAccountIndex = newIndex;
});
},
itemBuilder: (context, index) {
final user = users[index];
final isSelected = index == selectedAccountIndex;
return _buildAccountCard(user, isSelected);
},
), ),
decoration: BoxDecoration( ),
color: Color(0xFF01A04C), const SizedBox(height: 8),
borderRadius: BorderRadius.circular(16), Row(
), mainAxisAlignment: MainAxisAlignment.end,
child: Column( children: [
crossAxisAlignment: CrossAxisAlignment.start, GestureDetector(
children: [ onTap: () {
Row( Navigator.push(
children: [ context,
Text( MaterialPageRoute(
"${getFullAccountType(currAccount.accountType)}: ", builder: (context) =>
style: TextStyle( AllAccountsScreen(users: users),
color: theme.colorScheme.onPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
), ),
DropdownButton<int>( );
value: selectedAccountIndex, },
dropdownColor: theme.colorScheme.primary, child: Text(
underline: const SizedBox(), AppLocalizations.of(context).viewall,
icon: const Icon(Icons.keyboard_arrow_down), style: TextStyle(
iconEnabledColor: theme.colorScheme.onPrimary, fontSize: 14,
style: TextStyle( fontWeight: FontWeight.bold,
color: theme.colorScheme.onPrimary, color: theme.colorScheme.primary,
fontSize: 18, ),
),
items: List.generate(users.length, (index) {
return DropdownMenuItem<int>(
value: index,
child: Text(
users[index].accountNo ?? 'N/A',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
),
),
);
}),
onChanged: (int? newIndex) async {
if (newIndex == null ||
newIndex == selectedAccountIndex) {
return;
}
if (isBalanceLoading) return;
if (isVisible) {
setState(() {
isBalanceLoading = true;
selectedAccountIndex = newIndex;
});
await Future.delayed(
const Duration(milliseconds: 200),
);
setState(() {
isBalanceLoading = false;
});
} else {
setState(() {
selectedAccountIndex = newIndex;
});
}
await _loadTransactions(
users[newIndex].accountNo!,
);
},
),
const Spacer(),
IconButton(
icon: isRefreshing
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: theme.colorScheme.onPrimary,
strokeWidth: 2,
),
)
: Icon(
Symbols.refresh,
color: theme.colorScheme.onPrimary,
),
onPressed: isRefreshing
? null
: () => _refreshAccountData(context),
tooltip: 'Refresh',
),
],
), ),
const SizedBox(height: 15), ),
Row( ],
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(
"",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
isRefreshing || isBalanceLoading
? _buildBalanceShimmer()
: Text(
isVisible
? currAccount.currentBalance ??
'0.00'
: '*****',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
const Spacer(),
InkWell(
onTap: () async {
if (isBalanceLoading) return;
if (!isVisible) {
setState(() {
isBalanceLoading = true;
});
await Future.delayed(
const Duration(seconds: 1),
);
setState(() {
isVisible = true;
isBalanceLoading = false;
});
} else {
setState(() {
isVisible = false;
});
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: theme.scaffoldBackgroundColor,
weight: 800,
),
),
),
],
),
const SizedBox(height: 15),
],
),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
Text( Text(
AppLocalizations.of(context).quickLinks, AppLocalizations.of(context).quickLinks,
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
const SizedBox(height: 16), const SizedBox(height: 24),
// Quick Links // Quick Links
GridView.count( GridView.count(
crossAxisCount: 4, crossAxisCount: 3,
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1,
children: [ children: [
_buildQuickLink( _buildQuickLink(
Symbols.id_card, Symbols.id_card,
@@ -539,17 +612,12 @@ class _DashboardScreenState extends State<DashboardScreen>
MaterialPageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
AccountStatementScreen( AccountStatementScreen(
accountNo: users[selectedAccountIndex] users: users,
.accountNo!, selectedIndex: selectedAccountIndex,
balance: users[selectedAccountIndex]
.availableBalance!,
accountType:
users[selectedAccountIndex]
.accountType!,
))); )));
}), }),
_buildQuickLink(Icons.location_pin, "Branch Locator", _buildQuickLink(Icons.location_pin,
() { AppLocalizations.of(context).branchlocator, () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@@ -574,69 +642,24 @@ class _DashboardScreenState extends State<DashboardScreen>
builder: (context) => builder: (context) =>
const EnquiryScreen())); const EnquiryScreen()));
}), }),
], _buildQuickLink(
), Symbols.person,
const SizedBox(height: 5), AppLocalizations.of(context).profile,
() {
// Recent Transactions
Text(
AppLocalizations.of(context).recentTransactions,
style: const TextStyle(fontSize: 17),
),
const SizedBox(height: 16),
if (_txLoading)
..._buildTransactionShimmer()
else if (_transactions.isNotEmpty)
..._transactions.map(
(tx) => ListTile(
leading: Icon(
tx.type == 'CR'
? Symbols.call_received
: Symbols.call_made,
color: tx.type == 'CR'
? const Color(0xFF10BB10)
: theme.colorScheme.error,
),
title: Text(
tx.date ?? '',
style: const TextStyle(fontSize: 15),
),
subtitle: Text(
tx.name != null
? (tx.name!.length > 22
? tx.name!.substring(0, 22)
: tx.name!)
: '',
style: const TextStyle(fontSize: 12),
),
trailing: Text(
"${tx.amount}",
style: const TextStyle(fontSize: 17),
),
onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (_) => builder: (context) => ProfileScreen(
TransactionDetailsScreen(transaction: tx), mobileNumber: mobileNumberToPass,
customerNo: customerNo,
customerName: customerName),
), ),
); );
}, },
disable: false,
), ),
) ],
else ),
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Center(
child: Text(
AppLocalizations.of(context).noTransactions,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).colorScheme.outline,
),
),
),
),
], ],
), ),
), ),
@@ -651,32 +674,6 @@ class _DashboardScreenState extends State<DashboardScreen>
); );
} }
List<Widget> _buildTransactionShimmer() {
final theme = Theme.of(context);
return List.generate(3, (i) {
return ListTile(
leading: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: CircleAvatar(
radius: 12, backgroundColor: theme.scaffoldBackgroundColor),
),
title: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 10, width: 100, color: theme.scaffoldBackgroundColor),
),
subtitle: Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 8, width: 60, color: theme.scaffoldBackgroundColor),
),
);
});
}
Widget _buildQuickLink( Widget _buildQuickLink(
IconData icon, IconData icon,
String label, String label,
@@ -684,27 +681,35 @@ class _DashboardScreenState extends State<DashboardScreen>
bool disable = false, bool disable = false,
}) { }) {
final theme = Theme.of(context); final theme = Theme.of(context);
return InkWell( return Card(
onTap: disable ? null : onTap, elevation: 2,
child: Column( shape: RoundedRectangleBorder(
mainAxisSize: MainAxisSize.min, borderRadius: BorderRadius.circular(12.0),
children: [ ),
Icon( child: InkWell(
icon, onTap: disable ? null : onTap,
size: 30, borderRadius: BorderRadius.circular(12.0),
color: disable child: Column(
? theme.colorScheme.onSurface.withOpacity(0.3) mainAxisAlignment: MainAxisAlignment.center,
: theme.colorScheme.primary, children: [
grade: 200, Icon(
weight: 700, icon,
), size: 30,
const SizedBox(height: 4), color: disable ? theme.disabledColor : theme.colorScheme.primary,
Text( ),
label, const SizedBox(height: 4),
textAlign: TextAlign.center, Text(
style: const TextStyle(fontSize: 13), label,
), textAlign: TextAlign.center,
], style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 12,
color:
disable ? theme.disabledColor : theme.colorScheme.onSurface,
),
),
],
),
), ),
); );
} }

View File

@@ -53,25 +53,54 @@ class _EnquiryScreen extends State<EnquiryScreen> {
} }
Widget _buildContactItem(String role, String email, String phone) { Widget _buildContactItem(String role, String email, String phone) {
return Column( return Card(
crossAxisAlignment: CrossAxisAlignment.start, elevation: 4,
children: [ margin: const EdgeInsets.symmetric(vertical: 8),
Text(role, child: Padding(
style: TextStyle(color: Theme.of(context).colorScheme.onSurface)), padding: const EdgeInsets.all(16.0),
const SizedBox(height: 4), child: Column(
GestureDetector( crossAxisAlignment: CrossAxisAlignment.start,
onTap: () => _launchEmailAddress(email), children: [
child: Text(email, Text(role,
style: TextStyle(color: Theme.of(context).colorScheme.primary)), style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 15,
fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _launchEmailAddress(email),
child: Row(
children: [
const Icon(Icons.email),
const SizedBox(width: 8),
Expanded(
child: Text(email,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14)),
),
],
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _launchPhoneNumber(phone),
child: Row(
children: [
const Icon(Icons.phone),
const SizedBox(width: 8),
Expanded(
child: Text(phone,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 14)),
),
],
),
),
],
), ),
const SizedBox(height: 4), ),
GestureDetector(
onTap: () => _launchPhoneNumber(phone),
child: Text(phone,
style:
TextStyle(color: Theme.of(context).colorScheme.primary)), // Changed color for visibility
),
],
); );
} }
@@ -89,60 +118,67 @@ class _EnquiryScreen extends State<EnquiryScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 20), Card(
GestureDetector( elevation: 4,
onTap: () => _launchUrl("https://kccbhp.bank.in/complaint-form/"), child: InkWell(
child: Row(mainAxisSize: MainAxisSize.min, children: [ onTap: () =>
Text( _launchUrl("https://kccbhp.bank.in/complaint-form/"),
"Complaint Form", child: Padding(
style: TextStyle( padding: const EdgeInsets.all(16.0),
fontSize: 17, child: Row(
color: Theme.of(context).colorScheme.primary, mainAxisAlignment: MainAxisAlignment.spaceBetween,
decoration: TextDecoration.underline, // Added underline for link clarity children: [
decorationColor: Text(
Theme.of(context).colorScheme.primary, "Complaint Form",
), style: TextStyle(
fontSize: 15,
color: Theme.of(context).colorScheme.primary,
),
),
Icon(
Icons.open_in_new,
color: Theme.of(context).colorScheme.primary,
size: 16.0,
),
],
), ),
const SizedBox(width: 4), ),
Icon( ),
Icons.open_in_new, ),
color: Theme.of(context).colorScheme.primary, const Divider(height: 32),
size: 16.0,
),
])),
const SizedBox(height: 40),
Text( Text(
AppLocalizations.of(context).keyContacts, AppLocalizations.of(context).keyContacts,
style: TextStyle( style: const TextStyle(
fontSize: 17, fontSize: 15,
color: Theme.of(context).colorScheme.primary, fontWeight: FontWeight.bold,
), ),
// horizontal line
),
Divider(color: Theme.of(context).colorScheme.outline),
const SizedBox(height: 16),
_buildContactItem(
AppLocalizations.of(context).chairman,
"chairman@kccb.in",
"01892-222677",
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildContactItem( Expanded(
AppLocalizations.of(context).managingDirector, child: ListView(
"md@kccb.in", children: [
"01892-224969", _buildContactItem(
), AppLocalizations.of(context).chairman,
const SizedBox(height: 16), "chairman@kccb.in",
_buildContactItem( "01892-222677",
AppLocalizations.of(context).gmWest, ),
"gmw@kccb.in", _buildContactItem(
"01892-223280", AppLocalizations.of(context).managingDirector,
), "md@kccb.in",
const SizedBox(height: 16), "01892-224969",
_buildContactItem( ),
AppLocalizations.of(context).gmNorth, _buildContactItem(
"gmn@kccb.in", AppLocalizations.of(context).gmWest,
"01892-224607", "gmw@kccb.in",
"01892-223280",
),
_buildContactItem(
AppLocalizations.of(context).gmNorth,
"gmn@kccb.in",
"01892-224607",
),
],
),
), ),
], ],
), ),
@@ -165,4 +201,4 @@ class _EnquiryScreen extends State<EnquiryScreen> {
), ),
); );
} }
} }

View File

@@ -492,7 +492,8 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
if (_isLoadingLimit) Text(AppLocalizations.of(context).fetchingDailyLimit), if (_isLoadingLimit)
Text(AppLocalizations.of(context).fetchingDailyLimit),
if (!_isLoadingLimit && _limit != null) if (!_isLoadingLimit && _limit != null)
Text( Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}', 'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',

View File

@@ -28,11 +28,22 @@ class _FundTransferBeneficiaryScreenState
var service = getIt<BeneficiaryService>(); var service = getIt<BeneficiaryService>();
bool _isLoading = true; bool _isLoading = true;
List<Beneficiary> _beneficiaries = []; List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadBeneficiaries(); _loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
} }
Future<void> _loadBeneficiaries() async { Future<void> _loadBeneficiaries() async {
@@ -43,10 +54,25 @@ class _FundTransferBeneficiaryScreenState
? b.bankName!.toLowerCase().contains('kangra central') ? b.bankName!.toLowerCase().contains('kangra central')
: !b.bankName!.toLowerCase().contains('kangra central')) : !b.bankName!.toLowerCase().contains('kangra central'))
.toList(); .toList();
_filteredBeneficiaries = _beneficiaries;
_isLoading = false; _isLoading = false;
}); });
} }
void _filterBeneficiaries(String query) {
setState(() {
if (query.isEmpty) {
_filteredBeneficiaries = _beneficiaries;
} else {
_filteredBeneficiaries = _beneficiaries.where((beneficiary) {
final lowerQuery = query.toLowerCase();
return beneficiary.name.toLowerCase().contains(lowerQuery) ||
beneficiary.accountNo.toLowerCase().contains(lowerQuery);
}).toList();
}
});
}
Widget _buildShimmerList() { Widget _buildShimmerList() {
return ListView.builder( return ListView.builder(
itemCount: 6, itemCount: 6,
@@ -74,14 +100,14 @@ class _FundTransferBeneficiaryScreenState
} }
Widget _buildBeneficiaryList() { Widget _buildBeneficiaryList() {
if (_beneficiaries.isEmpty) { if (_filteredBeneficiaries.isEmpty) {
return Center( return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound)); child: Text(AppLocalizations.of(context).noBeneficiaryFound));
} }
return ListView.builder( return ListView.builder(
itemCount: _beneficiaries.length, itemCount: _filteredBeneficiaries.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final beneficiary = _beneficiaries[index]; final beneficiary = _filteredBeneficiaries[index];
// --- Cooldown Logic --- // --- Cooldown Logic ---
bool isCoolingDown = false; bool isCoolingDown = false;
@@ -94,60 +120,63 @@ class _FundTransferBeneficiaryScreenState
// By wrapping the ListTile in an Opacity widget, we can make it look // By wrapping the ListTile in an Opacity widget, we can make it look
// disabled while ensuring the onTap callback still works. // disabled while ensuring the onTap callback still works.
return Opacity( return Card(
opacity: isCoolingDown ? 0.5 : 1.0, margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile( child: Opacity(
// REMOVED the 'enabled' property from here. opacity: isCoolingDown ? 0.5 : 1.0,
leading: CircleAvatar( child: ListTile(
radius: 24, // REMOVED the 'enabled' property from here.
backgroundColor: Colors.transparent, leading: CircleAvatar(
child: getBankLogo(beneficiary.bankName, context), radius: 24,
), backgroundColor: Colors.transparent,
title: Text(beneficiary.name), child: getBankLogo(beneficiary.bankName, context),
subtitle: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, title: Text(beneficiary.name),
children: [ subtitle: Column(
Text(beneficiary.accountNo), crossAxisAlignment: CrossAxisAlignment.start,
if (beneficiary.bankName != null && children: [
beneficiary.bankName!.isNotEmpty) Text(beneficiary.accountNo),
Text( if (beneficiary.bankName != null &&
beneficiary.bankName!, beneficiary.bankName!.isNotEmpty)
style: TextStyle(fontSize: 12, color: Colors.grey[600]), Text(
), beneficiary.bankName!,
], style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
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(
builder: (context) => FundTransferAmountScreen(
debitAccountNo: widget.creditAccountNo,
creditBeneficiary: beneficiary,
remitterName: widget.remitterName,
isOwnBank: widget.isOwnBank,
), ),
), ],
); ),
} 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(
builder: (context) => FundTransferAmountScreen(
debitAccountNo: widget.creditAccountNo,
creditBeneficiary: beneficiary,
remitterName: widget.remitterName,
isOwnBank: widget.isOwnBank,
),
),
);
}
},
),
), ),
); );
}, },
@@ -162,7 +191,27 @@ class _FundTransferBeneficiaryScreenState
), ),
body: Stack( body: Stack(
children: [ children: [
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(), Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer( IgnorePointer(
child: Center( child: Center(
child: Opacity( child: Opacity(

View File

@@ -34,66 +34,78 @@ class FundTransferScreen extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
return Stack( return Stack(
children: [ children: [
ListView( Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 16.0),
FundTransferManagementTile( child: Column(
icon: Symbols.person, crossAxisAlignment: CrossAxisAlignment.stretch,
// Restore localization for the label children: [
label: "Self Pay", Expanded(
onTap: () { child: FundTransferManagementTile(
// The accounts list is passed directly from the constructor icon: Symbols.person,
Navigator.push( label: "Self Pay",
context, subtitle:
MaterialPageRoute( AppLocalizations.of(context).ftselfpaysubtitle,
builder: (context) => FundTransferSelfAccountsScreen( onTap: () {
debitAccountNo: creditAccountNo, Navigator.push(
remitterName: remitterName, context,
accounts: accounts, MaterialPageRoute(
), builder: (context) =>
), FundTransferSelfAccountsScreen(
); debitAccountNo: creditAccountNo,
}, remitterName: remitterName,
// Disable the tile if the state is not Authenticated accounts: accounts,
disable: state is! Authenticated, ),
), ),
const Divider(height: 1), );
FundTransferManagementTile( },
icon: Symbols.input_circle, disable: state is! Authenticated,
// Restore localization for the label ),
label: AppLocalizations.of(context).ownBank, ),
onTap: () { const SizedBox(height: 16),
Navigator.push( Expanded(
context, child: FundTransferManagementTile(
MaterialPageRoute( icon: Symbols.input_circle,
builder: (context) => FundTransferBeneficiaryScreen( label: AppLocalizations.of(context).ownBank,
creditAccountNo: creditAccountNo, subtitle: AppLocalizations.of(context).ftownsubtitle,
remitterName: remitterName, onTap: () {
isOwnBank: true, Navigator.push(
), context,
), MaterialPageRoute(
); builder: (context) =>
}, FundTransferBeneficiaryScreen(
), creditAccountNo: creditAccountNo,
const Divider(height: 1), remitterName: remitterName,
FundTransferManagementTile( isOwnBank: true,
icon: Symbols.output_circle, ),
// Restore localization for the label ),
label: AppLocalizations.of(context).outsideBank, );
onTap: () { },
Navigator.push( ),
context, ),
MaterialPageRoute( const SizedBox(height: 16),
builder: (context) => FundTransferBeneficiaryScreen( Expanded(
creditAccountNo: creditAccountNo, child: FundTransferManagementTile(
remitterName: remitterName, icon: Symbols.output_circle,
isOwnBank: false, label: AppLocalizations.of(context).outsideBank,
), subtitle:
), AppLocalizations.of(context).ftoutsidesubtitle,
); onTap: () {
}, Navigator.push(
), context,
const Divider(height: 1), MaterialPageRoute(
], builder: (context) =>
FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: false,
),
),
);
},
),
),
],
),
), ),
IgnorePointer( IgnorePointer(
child: Center( child: Center(
@@ -120,6 +132,7 @@ class FundTransferScreen extends StatelessWidget {
class FundTransferManagementTile extends StatelessWidget { class FundTransferManagementTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final String? subtitle;
final VoidCallback onTap; final VoidCallback onTap;
final bool disable; final bool disable;
@@ -127,18 +140,65 @@ class FundTransferManagementTile extends StatelessWidget {
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
this.subtitle,
required this.onTap, required this.onTap,
this.disable = false, this.disable = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( final theme = Theme.of(context);
leading: Icon(icon), return Card(
title: Text(label), margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
trailing: const Icon(Symbols.arrow_right, size: 20), shape: RoundedRectangleBorder(
onTap: onTap, borderRadius: BorderRadius.circular(12.0),
enabled: !disable, ),
elevation: 4, // Add some elevation for better visual separation
child: InkWell(
onTap:
disable ? null : onTap, // Disable InkWell if the tile is disabled
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 48, // Make icon larger
color:
disable ? theme.disabledColor : theme.colorScheme.primary,
),
const SizedBox(height: 12),
Text(
label,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: disable
? theme.disabledColor
: theme.colorScheme.onSurface,
),
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
subtitle!,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: disable
? theme.disabledColor
: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
),
),
); );
} }
} }

View File

@@ -55,39 +55,44 @@ class FundTransferSelfAccountsScreen extends StatelessWidget {
itemCount: filteredAccounts.length, itemCount: filteredAccounts.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final account = filteredAccounts[index]; final account = filteredAccounts[index];
return ListTile( return Card(
leading: CircleAvatar( margin: const EdgeInsets.symmetric(
radius: 24, horizontal: 12, vertical: 6),
backgroundColor: Colors.transparent, child: ListTile(
child: getBankLogo( leading: CircleAvatar(
'Kangra Central Co-operative Bank', context), radius: 24,
), backgroundColor: Colors.transparent,
title: Text(account.name ?? 'N/A'), child: getBankLogo(
subtitle: Column( 'Kangra Central Co-operative Bank', context),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ title: Text(account.name ?? 'N/A'),
Text(account.accountNo ?? 'N/A'), subtitle: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
_getFullAccountType(account.accountType), children: [
style: TextStyle( Text(account.accountNo ?? 'N/A'),
fontSize: 12, color: Colors.grey[600]), 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,
), ),
), ],
); ),
}, 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,
),
),
);
},
),
); );
}, },
), ),

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/limit_service.dart';
import '../../../di/injection.dart';
import '../../../l10n/app_localizations.dart';
class ChangeLimitOTPScreen extends StatefulWidget {
final String newLimit;
final String mobileNumber;
// ignore: use_key_in_widget_constructors
const ChangeLimitOTPScreen({
required this.newLimit,
required this.mobileNumber,
});
@override
State<ChangeLimitOTPScreen> createState() => _ChangeLimitOTPScreenState();
}
class _ChangeLimitOTPScreenState extends State<ChangeLimitOTPScreen> {
bool _isLoading = true;
final TextEditingController otpController = TextEditingController();
@override
void initState() {
super.initState();
// Simulate OTP sending delay
Future.delayed(const Duration(seconds: 2), () {
setState(() {
_isLoading = false;
});
});
}
final limitService = getIt<LimitService>();
Future<void> _validateOTP() async {
try {
await limitService.validateOtp(
otp: otpController.text,
mobileNumber: widget.mobileNumber,
);
// If OTP is valid, then change the limit
limitService.editLimit(
double.parse(widget.newLimit),
);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Limit has been Changed"),
));
// Navigate back to profile or login
Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).otpVerification)),
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
AppLocalizations.of(context).otpSent,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 24),
TextFormField(
controller: otpController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).enterOTP,
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _validateOTP,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: Text(AppLocalizations.of(context).validateOTP),
),
),
],
),
),
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,11 +2,13 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:kmobile/api/services/limit_service.dart'; import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:kmobile/features/profile/change_limit_otp_screen.dart';
import 'package:kmobile/l10n/app_localizations.dart'; import 'package:kmobile/l10n/app_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class DailyLimitScreen extends StatefulWidget { class DailyLimitScreen extends StatefulWidget {
const DailyLimitScreen({super.key}); final String mobileNumber;
const DailyLimitScreen({super.key, required this.mobileNumber});
@override @override
State<DailyLimitScreen> createState() => _DailyLimitScreenState(); State<DailyLimitScreen> createState() => _DailyLimitScreenState();
} }
@@ -74,7 +76,7 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
child: Text(localizations.cancel), child: Text(localizations.cancel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () async {
final value = double.tryParse(_limitController.text); final value = double.tryParse(_limitController.text);
if (value == null || value <= 0) return; if (value == null || value <= 0) return;
@@ -88,8 +90,26 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
), ),
); );
} else { } else {
service.editLimit(value); try {
Navigator.of(dialogContext).pop(value); await service.getOtpTLimit(
mobileNumber: widget.mobileNumber);
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChangeLimitOTPScreen(
newLimit: value.toString(),
mobileNumber: widget.mobileNumber,
),
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Error: $e"),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.error,
),
);
}
} }
}, },
child: Text(localizations.save), child: Text(localizations.save),
@@ -204,14 +224,6 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// TextButton.icon(
// onPressed: _removeLimit,
// icon: const Icon(Icons.remove_circle_outline),
// label: Text(localizations.removeLimit),
// style: TextButton.styleFrom(
// foregroundColor: theme.colorScheme.error,
// ),
// ),
], ],
), ),
], ],

View File

@@ -23,42 +23,50 @@ class PreferenceScreen extends StatelessWidget {
return Stack( return Stack(
children: [ children: [
ListView( ListView(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [ children: [
//Set Prefered Username
// ListTile(
// leading: const Icon(Icons.person),
// title: const Text("Set Prefered Username"),
// onTap: () {
// }),
// Language Selection // Language Selection
ListTile( Card(
leading: const Icon(Icons.language), margin: const EdgeInsets.only(bottom: 10),
title: Text(loc.language), child: ListTile(
onTap: () { leading: const Icon(Icons.language),
showDialog( title: Text(loc.language),
context: context, trailing: const Icon(Icons.chevron_right),
builder: (_) => const LanguageDialog(),
);
},
),
//Theme Mode Switch (Light/Dark)
ListTile(
leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode),
onTap: () {
showThemeModeDialog(context);
},
),
//Color_Theme_Selection
ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (_) => const ColorThemeDialog(), builder: (_) => const LanguageDialog(),
); );
}), },
),
),
//Theme Mode Switch (Light/Dark)
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.brightness_6),
title: Text(AppLocalizations.of(context).themeMode),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showThemeModeDialog(context);
},
),
),
//Color_Theme_Selection
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.color_lens),
title: Text(AppLocalizations.of(context).themeColor),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (_) => const ColorThemeDialog(),
);
}),
),
], ],
), ),
IgnorePointer( IgnorePointer(

View File

@@ -7,15 +7,21 @@ import 'package:kmobile/features/profile/logout_dialog.dart';
import 'package:kmobile/features/profile/security_settings_screen.dart'; import 'package:kmobile/features/profile/security_settings_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../di/injection.dart'; import '../../di/injection.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
import 'package:kmobile/features/profile/preferences/preference_screen.dart'; import 'package:kmobile/features/profile/preferences/preference_screen.dart';
class ProfileScreen extends StatefulWidget { class ProfileScreen extends StatefulWidget {
final String mobileNumber; final String mobileNumber;
const ProfileScreen({super.key, required this.mobileNumber}); final String customerNo;
final String customerName;
const ProfileScreen(
{super.key,
required this.mobileNumber,
required this.customerNo,
required this.customerName});
@override @override
State<ProfileScreen> createState() => _ProfileScreenState(); State<ProfileScreen> createState() => _ProfileScreenState();
@@ -30,8 +36,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
Future<String> _getAppVersion() async { Future<String> _getAppVersion() async {
final PackageInfo info = await PackageInfo.fromPlatform(); return 'Version 1.0.1 (1))';
return 'Version ${info.version} (${info.buildNumber})';
} }
Future<void> _loadBiometricStatus() async { Future<void> _loadBiometricStatus() async {
@@ -160,31 +165,95 @@ class _ProfileScreenState extends State<ProfileScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(loc.profile), // Localized "Profile" title: Text(loc.profile),
elevation: 0,
), ),
body: Stack( body: Stack(
children: [ children: [
ListView( ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [ children: [
ListTile( // ===== Profile Header =====
leading: const Icon(Icons.settings), Card(
title: Text(loc.preferences), child: Padding(
trailing: const Icon(Icons.chevron_right), padding: const EdgeInsets.all(16.0),
child: Row(
children: [
// Avatar
Container(
width: 56,
height: 56,
child: const CircleAvatar(
radius: 50,
child: Icon(
Symbols.person,
size: 56,
),
),
),
const SizedBox(width: 12),
// Name + mobile
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
// If you want to show the user's name instead, replace below.
widget.customerName,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 4),
Text(
widget.customerNo,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withOpacity(0.7),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 16),
// ===== Section: Settings =====
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
"Settings",
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
),
const SizedBox(height: 8),
_SectionTile(
leadingIcon: Icons.settings,
title: loc.preferences,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const PreferenceScreen()), builder: (context) => const PreferenceScreen(),
),
); );
}, },
), ),
ListTile( _SectionTile(
leading: const Icon(Icons.security), leadingIcon: Icons.security,
title: Text(loc.securitySettings), title: loc.securitySettings,
trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -196,66 +265,114 @@ class _ProfileScreenState extends State<ProfileScreen> {
); );
}, },
), ),
ListTile( _SectionTile(
leading: const Icon(Icons.currency_rupee), leadingIcon: Icons.currency_rupee,
title: Text(AppLocalizations.of(context).dailylimit), title: loc.dailylimit,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const DailyLimitScreen()), builder: (context) =>
DailyLimitScreen(mobileNumber: widget.mobileNumber),
),
); );
}, },
), ),
SwitchListTile( Card(
title: child: SwitchListTile(
Text(AppLocalizations.of(context).enableFingerprintLogin), title: Text(loc.enableFingerprintLogin),
value: _isBiometricEnabled, value: _isBiometricEnabled,
onChanged: (bool value) { onChanged: (bool value) {
// The state is now managed within _handleBiometricToggle _handleBiometricToggle(value);
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
),
ListTile(
leading: const Icon(Icons.smartphone),
title: const Text("App Version"),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Show a loading indicator while waiting for the future to complete
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return const Text("Error");
} else {
// Display the version number once the future is complete
return Text(
snapshot.data ?? "N/A",
selectionColor: const Color(0xFFFFFFFF),
);
}
}, },
secondary: const Icon(Icons.fingerprint),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
), ),
), ),
ListTile(
leading: const Icon(Icons.exit_to_app), const SizedBox(height: 16),
title: Text(AppLocalizations.of(context).logout), const Divider(height: 24),
// ===== Section: Security & App =====
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
loc.appVersion,
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
),
const SizedBox(height: 8),
// Fingerprint toggle inside a styled container
Card(
child: ListTile(
leading: const Icon(Icons.smartphone),
title: Text(loc.appVersion),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder:
(BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
);
} else if (snapshot.hasError) {
return Text(loc.error);
} else {
return Text(
snapshot.data ?? "N/A",
);
}
},
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
),
const SizedBox(height: 16),
const Divider(height: 24),
// ===== Section: Actions =====
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
"Exit",
style: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
),
const SizedBox(height: 8),
_SectionTile(
leadingIcon: Icons.exit_to_app,
title: loc.logout,
trailChevron: false, // action tile, no chevron
onTap: () async { onTap: () async {
final shouldExit = await showDialog<bool>( final shouldExit = await showDialog<bool>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).logout), title: Text(loc.logout),
content: Text(AppLocalizations.of(context).logoutCheck), content: Text(loc.logoutCheck),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).no), child: Text(loc.no),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context).yes), child: Text(loc.yes),
), ),
], ],
), ),
@@ -269,9 +386,10 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
}, },
), ),
ListTile( _SectionTile(
leading: const Icon(Icons.logout), leadingIcon: Icons.logout,
title: Text(AppLocalizations.of(context).deregister), title: loc.deregister,
trailChevron: false,
onTap: () async { onTap: () async {
final shouldLogout = await showDialog<bool>( final shouldLogout = await showDialog<bool>(
context: context, context: context,
@@ -283,24 +401,47 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
}, },
), ),
const SizedBox(height: 24),
], ],
), ),
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
),
),
),
),
),
], ],
), ),
); );
} }
} }
class _SectionTile extends StatelessWidget {
const _SectionTile({
required this.leadingIcon,
required this.title,
this.onTap,
this.trailChevron = true,
});
final IconData leadingIcon;
final String title;
final VoidCallback? onTap;
final bool trailChevron;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: Icon(leadingIcon, color: theme.colorScheme.onSurface),
title: Text(title, style: theme.textTheme.bodyLarge),
trailing: trailChevron
? Icon(Icons.chevron_right, color: theme.colorScheme.onSurface)
: null,
onTap: onTap,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
);
}
}

View File

@@ -24,98 +24,107 @@ class SecuritySettingsScreen extends StatelessWidget {
body: Stack( body: Stack(
children: [ children: [
ListView( ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [ children: [
ListTile( Card(
leading: const Icon(Icons.lock_outline), margin: const EdgeInsets.only(bottom: 10),
title: Text(loc.changeLoginPassword), child: ListTile(
trailing: const Icon(Icons.chevron_right), leading: const Icon(Icons.lock_outline),
onTap: () { title: Text(loc.changeLoginPassword),
Navigator.push( trailing: const Icon(Icons.chevron_right),
context, onTap: () {
MaterialPageRoute( Navigator.push(
builder: (context) => ChangePasswordScreen( context,
mobileNumber: mobileNumber, 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), Card(
ListTile( margin: const EdgeInsets.only(bottom: 10),
leading: const Icon(Icons.password), child: ListTile(
title: const Text('Change TPIN'), leading: const Icon(Icons.pin),
trailing: const Icon(Icons.chevron_right), title: Text(loc.changeMpin),
onTap: () async { trailing: const Icon(Icons.chevron_right),
final authService = getIt<AuthService>(); onTap: () async {
final isTpinSet = await authService.checkTpin(); final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ChangeMpinScreen(),
),
);
if (!isTpinSet) { if (result == true && context.mounted) {
if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar(
showDialog( SnackBar(
context: context, content: Text(loc.mpinChangedSuccessfully),
builder: (BuildContext context) { backgroundColor:
return AlertDialog( Theme.of(context).colorScheme.secondary,
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),
), ),
); );
} }
} },
}, ),
),
Card(
margin: const EdgeInsets.only(bottom: 10),
child: 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),
),
);
}
}
},
),
), ),
], ],
), ),

View File

@@ -23,39 +23,48 @@ class _QuickPayScreen extends State<QuickPayScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
ListView( Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 16.0),
QuickPayManagementTile( child: Column(
icon: Symbols.input_circle, crossAxisAlignment: CrossAxisAlignment.stretch,
label: AppLocalizations.of(context).ownBank, children: [
onTap: () { Expanded(
Navigator.push( child: QuickPayManagementTile(
context, icon: Symbols.input_circle,
MaterialPageRoute( label: AppLocalizations.of(context).ownBank,
builder: (context) => QuickPayWithinBankScreen( subtitle: AppLocalizations.of(context).quickownsubtitle,
debitAccount: widget.debitAccount, onTap: () {
), Navigator.push(
), context,
); MaterialPageRoute(
}, builder: (context) => QuickPayWithinBankScreen(
), debitAccount: widget.debitAccount,
const Divider(height: 1), ),
QuickPayManagementTile( ),
icon: Symbols.output_circle, );
label: AppLocalizations.of(context).outsideBank, },
onTap: () { ),
Navigator.push( ),
context, const SizedBox(height: 16),
MaterialPageRoute( Expanded(
builder: (context) => QuickPayOutsideBankScreen( child: QuickPayManagementTile(
debitAccount: widget.debitAccount, icon: Symbols.output_circle,
), label: AppLocalizations.of(context).outsideBank,
), subtitle: AppLocalizations.of(context).quickoutsidesubtitle,
); onTap: () {
}, Navigator.push(
), context,
const Divider(height: 1), MaterialPageRoute(
], builder: (context) => QuickPayOutsideBankScreen(
debitAccount: widget.debitAccount,
),
),
);
},
),
),
],
),
), ),
IgnorePointer( IgnorePointer(
child: Center( child: Center(
@@ -80,6 +89,7 @@ class _QuickPayScreen extends State<QuickPayScreen> {
class QuickPayManagementTile extends StatelessWidget { class QuickPayManagementTile extends StatelessWidget {
final IconData icon; final IconData icon;
final String label; final String label;
final String? subtitle;
final VoidCallback onTap; final VoidCallback onTap;
final bool disable; final bool disable;
@@ -87,18 +97,63 @@ class QuickPayManagementTile extends StatelessWidget {
super.key, super.key,
required this.icon, required this.icon,
required this.label, required this.label,
this.subtitle,
required this.onTap, required this.onTap,
this.disable = false, this.disable = false,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( final theme = Theme.of(context);
leading: Icon(icon), return Card(
title: Text(label), margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
trailing: const Icon(Symbols.arrow_right, size: 20), shape: RoundedRectangleBorder(
onTap: onTap, borderRadius: BorderRadius.circular(12.0),
enabled: !disable, ),
elevation: 4, // Add some elevation for better visual separation
child: InkWell(
onTap:
disable ? null : onTap, // Disable InkWell if the tile is disabled
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 36.0, horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64, // Make icon larger for two cards
color:
disable ? theme.disabledColor : theme.colorScheme.primary,
),
const SizedBox(height: 16),
Text(
label,
textAlign: TextAlign.center,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: disable
? theme.disabledColor
: theme.colorScheme.onSurface,
),
),
if (subtitle != null) // Conditionally display subtitle
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
subtitle!,
textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith(
color: disable
? theme.disabledColor
: theme.colorScheme.onSurfaceVariant,
),
),
),
],
),
),
),
); );
} }
} }

View File

@@ -143,9 +143,6 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
AppLocalizations.of(context).quickPayOwnBank, AppLocalizations.of(context).quickPayOwnBank,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
), ),
centerTitle: false, centerTitle: false,
), ),

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; 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/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:kmobile/di/injection.dart'; // Added: Import for dependency injection (getIt)
import 'package:shimmer/shimmer.dart'; // Added: Import for shimmer loading effect import 'package:shimmer/shimmer.dart';
import '../../../l10n/app_localizations.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 // Removed: The local 'Location' class is no longer needed as we use the 'Atm' model from branch_service.dart
@@ -15,10 +16,12 @@ class ATMLocatorScreen extends StatefulWidget {
class _ATMLocatorScreenState extends State<ATMLocatorScreen> { class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
var service = getIt<BranchService>(); // Added: Instance of BranchService for API calls var service =
getIt<BranchService>(); // Added: Instance of BranchService for API calls
bool _isLoading = true; // State variable to manage loading status bool _isLoading = true; // State variable to manage loading status
List<Atm> _allAtms = []; // Changed: List to hold all fetched Atm objects List<Atm> _allAtms = []; // Changed: List to hold all fetched Atm objects
List<Atm> _filteredAtms = []; // Changed: List to hold filtered Atm objects for display List<Atm> _filteredAtms =
[]; // Changed: List to hold filtered Atm objects for display
@override @override
void initState() { void initState() {
@@ -28,7 +31,8 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
/// Fetches the list of ATMs from the API using BranchService. /// Fetches the list of ATMs from the API using BranchService.
Future<void> _loadAtms() async { Future<void> _loadAtms() async {
final data = await service.fetchAtmList(); // Call the new fetchAtmList method final data =
await service.fetchAtmList(); // Call the new fetchAtmList method
setState(() { setState(() {
_allAtms = data; // Update the list of all ATMs _allAtms = data; // Update the list of all ATMs
_filteredAtms = data; // Initialize filtered list with all ATMs _filteredAtms = data; // Initialize filtered list with all ATMs
@@ -37,14 +41,18 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
} }
/// Filters the list of ATMs based on the search query. /// Filters the list of ATMs based on the search query.
void _filterAtms(String query) { // Changed: Renamed from _filterLocations void _filterAtms(String query) {
// Changed: Renamed from _filterLocations
setState(() { setState(() {
if (query.isEmpty) { if (query.isEmpty) {
_filteredAtms = _allAtms; // If query is empty, show all ATMs _filteredAtms = _allAtms; // If query is empty, show all ATMs
} else { } else {
_filteredAtms = _allAtms.where((atm) { // Changed: Filter based on Atm object _filteredAtms = _allAtms.where((atm) {
// Changed: Filter based on Atm object
final lowerQuery = query.toLowerCase(); final lowerQuery = query.toLowerCase();
return atm.name.toLowerCase().contains(lowerQuery); // Filter by atm.name return atm.name
.toLowerCase()
.contains(lowerQuery); // Filter by atm.name
}).toList(); }).toList();
} }
}); });
@@ -54,7 +62,8 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("ATM Locator"), // Title for the app bar title: Text(
AppLocalizations.of(context).atmlocator), // Title for the app bar
), ),
body: Stack( body: Stack(
children: [ children: [
@@ -64,7 +73,8 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: _filterAtms, // Updated: Call _filterAtms on text change onChanged:
_filterAtms, // Updated: Call _filterAtms on text change
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Name/Address", // Hint text for the search bar hintText: "Name/Address", // Hint text for the search bar
prefixIcon: const Icon(Icons.search), // Search icon prefixIcon: const Icon(Icons.search), // Search icon
@@ -81,12 +91,16 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
? _buildShimmerList() // Display shimmer while loading ? _buildShimmerList() // Display shimmer while loading
: _filteredAtms.isEmpty : _filteredAtms.isEmpty
? const Center( ? const Center(
child: Text("No matching ATMs found")) // Message if no ATMs found child: Text(
"No matching ATMs found")) // Message if no ATMs found
: ListView.builder( : ListView.builder(
itemCount: _filteredAtms.length, // Number of items in the filtered list itemCount: _filteredAtms
.length, // Number of items in the filtered list
itemBuilder: (context, index) { itemBuilder: (context, index) {
final atm = _filteredAtms[index]; // Get the current Atm object final atm = _filteredAtms[
return _buildAtmItem(atm); // Build the ATM list item index]; // Get the current Atm object
return _buildAtmItem(
atm); // Build the ATM list item
}, },
), ),
), ),
@@ -111,13 +125,13 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
); );
} }
/// Helper widget to build a single ATM list item. /// Helper widget to build a single ATM list item.
Widget _buildAtmItem(Atm atm) { // Changed: Takes an Atm object Widget _buildAtmItem(Atm atm) {
// Changed: Takes an Atm object
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile( child: ListTile(
leading: const Icon(Icons.currency_rupee), // Icon for ATM leading: const Icon(Icons.credit_card), // Icon for ATM
title: Text(atm.name, // Display the ATM's name title: Text(atm.name, // Display the ATM's name
style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
onTap: () { onTap: () {

View File

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

View File

@@ -1,12 +1,12 @@
// ignore_for_file: unused_element // ignore_for_file: unused_element
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/api/services/branch_service.dart'; import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:kmobile/features/service/screens/branch_details_screen.dart'; import 'package:kmobile/features/service/screens/branch_details_screen.dart';
import '../../../l10n/app_localizations.dart';
class BranchLocatorScreen extends StatefulWidget { class BranchLocatorScreen extends StatefulWidget {
const BranchLocatorScreen({super.key}); const BranchLocatorScreen({super.key});
@@ -15,48 +15,49 @@ class BranchLocatorScreen extends StatefulWidget {
State<BranchLocatorScreen> createState() => _BranchLocatorScreenState(); State<BranchLocatorScreen> createState() => _BranchLocatorScreenState();
} }
class _BranchLocatorScreenState extends State<BranchLocatorScreen> { class _BranchLocatorScreenState extends State<BranchLocatorScreen> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
var service = getIt<BranchService>(); var service = getIt<BranchService>();
bool _isLoading = true; bool _isLoading = true;
List<Branch> _allBranches = []; List<Branch> _allBranches = [];
List<Branch> _filteredBranches = []; List<Branch> _filteredBranches = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// _fetchAndSetLocations(); // _fetchAndSetLocations();
_loadBranches(); _loadBranches();
} }
Future<void> _loadBranches() async { Future<void> _loadBranches() async {
final data = await service.fetchBranchList(); final data = await service.fetchBranchList();
setState(() { setState(() {
_allBranches = data; _allBranches = data;
_filteredBranches = data; _filteredBranches = data;
_isLoading = false; _isLoading = false;
}); });
} }
void _filterBranches(String query) { void _filterBranches(String query) {
setState(() { setState(() {
if (query.isEmpty) { if (query.isEmpty) {
_filteredBranches = _allBranches; _filteredBranches = _allBranches;
} else { } else {
_filteredBranches = _allBranches.where((branch) { _filteredBranches = _allBranches.where((branch) {
final lowerQuery = query.toLowerCase(); final lowerQuery = query.toLowerCase();
return branch.branch_name.toLowerCase().contains(lowerQuery); return branch.branch_name.toLowerCase().contains(lowerQuery);
}).toList(); }).toList();
} }
}); });
} }
@override
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text("Branch Locator"), title: Text(
AppLocalizations.of(context).branchlocator,
),
), ),
body: Stack( body: Stack(
children: [ children: [
@@ -83,11 +84,13 @@ class BranchLocatorScreen extends StatefulWidget {
? _buildShimmerList() // Changed to shimmer ? _buildShimmerList() // Changed to shimmer
: _filteredBranches.isEmpty : _filteredBranches.isEmpty
? const Center( ? const Center(
child: Text("No matching branches found")) // Updated tex child: Text(
"No matching branches found")) // Updated tex
: ListView.builder( : ListView.builder(
itemCount: _filteredBranches.length, itemCount: _filteredBranches.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final branch = _filteredBranches[index]; // Changed to final branch =
_filteredBranches[index]; // Changed to
return _buildBranchItem(branch); // Updated return _buildBranchItem(branch); // Updated
}, },
), ),
@@ -127,13 +130,13 @@ class BranchLocatorScreen extends StatefulWidget {
} }
// Helper widget to build a single branch item // Helper widget to build a single branch item
Widget _buildBranchItem(Branch branch) { Widget _buildBranchItem(Branch branch) {
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile( child: ListTile(
leading: const CircleAvatar( leading: const CircleAvatar(
child: Icon(Icons.location_city), child: Icon(Icons.account_balance),
), ),
title: Text(branch.branch_name, title: Text(branch.branch_name,
style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(fontWeight: FontWeight.bold)),
@@ -150,7 +153,6 @@ class BranchLocatorScreen extends StatefulWidget {
); );
} }
// Shimmer loading list // Shimmer loading list
Widget _buildShimmerList() { Widget _buildShimmerList() {
return ListView.builder( return ListView.builder(

View File

@@ -110,4 +110,4 @@ class _FaqsScreenState extends State<FaqsScreen> {
), ),
); );
} }
} }

View File

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

View File

@@ -1,6 +1,4 @@
import 'package:kmobile/features/service/screens/atm_locator_screen.dart'; 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'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
@@ -27,58 +25,55 @@ class _ServiceScreen extends State<ServiceScreen> {
), ),
body: Stack( body: Stack(
children: [ children: [
ListView( Padding(
children: [ padding: const EdgeInsets.symmetric(vertical: 16.0),
// ServiceManagementTile( child: Column(
// icon: Symbols.add, crossAxisAlignment: CrossAxisAlignment.stretch,
// label: AppLocalizations.of(context).accountOpeningDeposit, children: [
// onTap: () {}, Expanded(
// disabled: true, child: ServiceManagementTile(
// ), icon: Symbols.captive_portal,
// const Divider(height: 1), label: AppLocalizations.of(context).quickLinks,
// ServiceManagementTile( onTap: () {
// icon: Symbols.add, Navigator.of(context).push(
// label: AppLocalizations.of(context).accountOpeningLoan, MaterialPageRoute(
// onTap: () {}, builder: (context) => const QuickLinksScreen()),
// disabled: true, );
// ), },
// const Divider(height: 1), disabled: false,
ServiceManagementTile( ),
icon: Symbols.captive_portal, ),
label: AppLocalizations.of(context).quickLinks, const SizedBox(height: 16),
onTap: () { Expanded(
Navigator.of(context).push( child: ServiceManagementTile(
MaterialPageRoute( icon: Symbols.question_mark,
builder: (context) => const QuickLinksScreen()), label: AppLocalizations.of(context).faq,
); onTap: () {
}, Navigator.of(context).push(
disabled: false, MaterialPageRoute(
), builder: (context) => const FaqsScreen()),
const Divider(height: 1), );
ServiceManagementTile( },
icon: Symbols.question_mark, disabled: false,
label: AppLocalizations.of(context).faq, ),
onTap: () { ),
Navigator.of(context).push( const SizedBox(height: 16),
MaterialPageRoute(builder: (context) => const FaqsScreen()), Expanded(
); child: ServiceManagementTile(
}, icon: Symbols.location_pin,
disabled: false, label: AppLocalizations.of(context).atmlocator,
), onTap: () {
const Divider(height: 1), Navigator.push(
ServiceManagementTile( context,
icon: Symbols.location_pin, MaterialPageRoute(
label: "ATM Locator", builder: (context) => const ATMLocatorScreen()));
onTap: () { },
Navigator.push( disabled: false,
context, ),
MaterialPageRoute( ),
builder: (context) => const ATMLocatorScreen())); // No Spacer() needed here as Expanded children will fill space
}, ],
disabled: false, ),
),
const Divider(height: 1),
],
), ),
IgnorePointer( IgnorePointer(
child: Center( child: Center(
@@ -117,23 +112,42 @@ class ServiceManagementTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return ListTile( return Card(
leading: Icon( margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
icon, shape: RoundedRectangleBorder(
color: disabled ? theme.disabledColor : null, borderRadius: BorderRadius.circular(12.0),
), ),
title: Text( elevation: 4, // Add some elevation for better visual separation
label, child: InkWell(
style: TextStyle( onTap:
color: disabled ? theme.disabledColor : null, disabled ? null : onTap, // Disable InkWell if the tile is disabled
borderRadius: BorderRadius.circular(12.0),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0, horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 48, // Make icon larger
color:
disabled ? theme.disabledColor : theme.colorScheme.primary,
),
const SizedBox(height: 12),
Text(
label,
textAlign: TextAlign.center,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: disabled
? theme.disabledColor
: theme.colorScheme.onSurface,
),
),
],
),
), ),
), ),
trailing: Icon(
Symbols.arrow_right,
size: 20,
color: disabled ? theme.disabledColor : null,
),
onTap: disabled ? null : onTap,
); );
} }
} }

View File

@@ -225,7 +225,7 @@
"pinMismatch": "PINs do not match", "pinMismatch": "PINs do not match",
"securitySettings": "Security Settings", "securitySettings": "Security Settings",
"kconnect": "Kconnect", "kconnect": "Kconnect",
"kccBankFull": "Kangra Central Co-operative Bank", "kccBankFull": "The Kangra Central Co-operative Bank Ltd.",
"themeColor": "Theme Color", "themeColor": "Theme Color",
"selectThemeColor": "Select Theme Color", "selectThemeColor": "Select Theme Color",
"violet": "Violet", "violet": "Violet",
@@ -323,7 +323,7 @@
"details": "Details", "details": "Details",
"remarks": "Remarks (Optional)", "remarks": "Remarks (Optional)",
"kccbMobile": "KCCB Mobile", "kccbMobile": "KCCB Mobile",
"faq": "Frequently Asked Questions(FAQs)", "faq": "Frequently Asked Questions",
"branches": "Branches", "branches": "Branches",
"atms": "ATMs", "atms": "ATMs",
"dailylimit": "Daily Transaction Limit", "dailylimit": "Daily Transaction Limit",
@@ -404,5 +404,17 @@
"postOffice": "Post Office", "postOffice": "Post Office",
"rbiCode1": "RBI Code 1", "rbiCode1": "RBI Code 1",
"rbiCode2": "RBI Code 2", "rbiCode2": "RBI Code 2",
"latitude": "Latitude" "latitude": "Latitude",
"address": "Customer Address",
"transactions": "Transactions",
"quickownsubtitle": "Seamlessly send money to your loved ones within Kangra Bank. Fast, secure, and always at your fingertips",
"quickoutsidesubtitle": "Transfer funds to any bank across India with ease. Your transactions are secure and processed quickly",
"ftselfpaysubtitle": "Move money between your own accounts with ease. Your funds, your way",
"ftownsubtitle": "Send money to your saved beneficiaries within Kangra Bank",
"ftoutsidesubtitle": "Transfer funds to your saved beneficiaries in any other bank across India",
"personaldetails": "Personal Details",
"kycdetails": "KYC Details",
"viewall": "View All",
"branchlocator": "Branch Locator",
"atmlocator": "ATM Locator"
} }

View File

@@ -405,5 +405,17 @@
"postOffice": "डाकघर", "postOffice": "डाकघर",
"rbiCode1": "आरबीआई कोड 1", "rbiCode1": "आरबीआई कोड 1",
"rbiCode2": "आरबीआई कोड 2", "rbiCode2": "आरबीआई कोड 2",
"latitude": "अक्षांश" "latitude": "अक्षांश",
"address": "ग्राहक का पता",
"transactions": "लेनदेन",
"quickownsubtitle": "कांगड़ा बैंक के ज़रिए अपने प्रियजनों को आसानी से पैसे भेजें। तेज़, सुरक्षित और हमेशा आपकी उंगलियों पर",
"quickoutsidesubtitle": "भारत भर में किसी भी बैंक में आसानी से धनराशि स्थानांतरित करें। आपके लेन-देन सुरक्षित और शीघ्रता से संसाधित होते हैं",
"ftselfpaysubtitle": "अपने खातों के बीच आसानी से पैसे ट्रांसफर करें। आपका पैसा, आपकी सुविधानुसार",
"ftownsubtitle": "कांगड़ा बैंक के माध्यम से अपने बचत लाभार्थियों को पैसे भेजें",
"ftoutsidesubtitle": "भारत भर में किसी भी अन्य बैंक में अपने सहेजे गए लाभार्थियों को धनराशि हस्तांतरित करें",
"personaldetails": "व्यक्तिगत विवरण",
"kycdetails": "केवाईसी विवरण",
"viewall": "सभी देखें",
"branchlocator": "शाखा लोकेटर",
"atmlocator": "एटीएम लोकेटर"
} }

View File

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

View File

@@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import device_info_plus import device_info_plus
import flutter_local_notifications
import flutter_secure_storage_macos import flutter_secure_storage_macos
import local_auth_darwin import local_auth_darwin
import package_info_plus import package_info_plus
@@ -16,6 +17,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))

View File

@@ -137,6 +137,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -238,6 +246,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "19ffb0a8bb7407875555e5e98d7343a633bb73707bae6c6a5f37c90014077875"
url: "https://pub.dev"
source: hosted
version: "19.5.0"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "277d25d960c15674ce78ca97f57d0bae2ee401c844b6ac80fcd972a9c99d09fe"
url: "https://pub.dev"
source: hosted
version: "9.1.0"
flutter_local_notifications_windows:
dependency: transitive
description:
name: flutter_local_notifications_windows
sha256: "8d658f0d367c48bd420e7cf2d26655e2d1130147bca1eea917e576ca76668aaf"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -541,6 +581,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
open_filex:
dependency: "direct main"
description:
name: open_filex
sha256: "9976da61b6a72302cf3b1efbce259200cd40232643a467aac7370addf94d6900"
url: "https://pub.dev"
source: hosted
version: "4.7.0"
package_info_plus: package_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -725,6 +773,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
send_message:
dependency: "direct main"
description:
name: send_message
sha256: "79b5f69fd3ab0b9e6265f8d972800d7989b3082a0523c7f4b8e38bf4e1c71235"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -813,6 +869,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
simcards:
dependency: "direct main"
description:
name: simcards
sha256: b621cc265ebbb3e11009ca9be67063efbc011396c4224aff8b08edaba30fa5ae
url: "https://pub.dev"
source: hosted
version: "0.0.1"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@@ -874,6 +938,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.6"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -947,7 +1019,7 @@ packages:
source: hosted source: hosted
version: "3.1.4" version: "3.1.4"
uuid: uuid:
dependency: transitive dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff

View File

@@ -34,7 +34,7 @@ dependencies:
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
jailbreak_root_detection: ^1.1.6 jailbreak_root_detection: ^1.1.6
equatable: ^2.0.7 equatable: ^2.0.7
dio: ^5.8.0+1 dio: ^5.9.0
flutter_secure_storage: ^9.2.4 flutter_secure_storage: ^9.2.4
bloc: ^9.0.0 bloc: ^9.0.0
flutter_bloc: ^9.1.0 flutter_bloc: ^9.1.0
@@ -61,6 +61,11 @@ dependencies:
device_info_plus: ^11.3.0 device_info_plus: ^11.3.0
showcaseview: ^2.0.3 showcaseview: ^2.0.3
package_info_plus: ^4.2.0 package_info_plus: ^4.2.0
flutter_local_notifications: ^19.5.0
open_filex: ^4.7.0
simcards: ^0.0.1
uuid: ^4.5.1
send_message: ^1.0.0
# jailbreak_root_detection: "^1.1.6" # jailbreak_root_detection: "^1.1.6"
@@ -114,6 +119,8 @@ flutter:
- assets/images/yes_bank_logo.png - assets/images/yes_bank_logo.png
- assets/images/uco_logo.png - assets/images/uco_logo.png
- assets/images/ipos_logo.png - assets/images/ipos_logo.png
- assets/images/profile.svg
- assets/images/profile.png
- assets/animations/rupee.json - assets/animations/rupee.json
- assets/animations/error.json - assets/animations/error.json
- assets/animations/done.json - assets/animations/done.json

View File

@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_local_notifications_windows
) )
set(PLUGIN_BUNDLED_LIBRARIES) set(PLUGIN_BUNDLED_LIBRARIES)