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"
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@@ -80,4 +81,6 @@ flutter {
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_FINGERPRINT"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:label="kmobile"
android:name="${applicationName}"

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;
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 {
try {
final response = await _dio.post(

View File

@@ -65,9 +65,8 @@ factory Branch.fromJson(Map<String, dynamic> json) {
}
static List<Branch> listFromJson(List<dynamic> jsonList) {
final beneficiaryList = jsonList
.map((beneficiary) => Branch.fromJson(beneficiary))
.toList();
final beneficiaryList =
jsonList.map((beneficiary) => Branch.fromJson(beneficiary)).toList();
return beneficiaryList;
}
}

View File

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

View File

@@ -15,7 +15,17 @@ class AppThemes {
colorScheme: colorScheme,
useMaterial3: true,
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) {
@@ -32,7 +42,17 @@ class AppThemes {
textTheme: GoogleFonts.rubikTextTheme(
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) {

View File

@@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart';
@@ -21,6 +22,8 @@ import '../security/secure_storage.dart';
final getIt = GetIt.instance;
Future<void> setupDependencies() async {
getIt.registerSingleton<RouteObserver<ModalRoute<void>>>(
RouteObserver<ModalRoute<void>>());
//getIt.registerLazySingleton<ThemeController>(() => ThemeController());
//getIt.registerLazySingleton<ThemeModeController>(() => ThemeModeController());
getIt.registerSingleton<ThemeCubit>(ThemeCubit());
@@ -71,9 +74,9 @@ Dio _createDioClient() {
final dio = Dio(
BaseOptions(
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
'https://kccbmbnk.net', //prod small
//'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
headers: {

View File

@@ -18,16 +18,45 @@ class AccountInfoScreen extends StatefulWidget {
class _AccountInfoScreen extends State<AccountInfoScreen> {
late User selectedUser;
@override
void initState() {
super.initState();
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
Widget build(BuildContext context) {
final users = widget.users;
int selectedIndex = widget.selectedIndex;
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context)
@@ -36,15 +65,23 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
),
body: Stack(
children: [
ListView(
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppLocalizations.of(context).accountNumber,
style:
const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
style: const TextStyle(
fontWeight: FontWeight.bold, fontSize: 18),
),
DropdownButton<User>(
value: selectedUser,
onChanged: (User? newUser) {
@@ -57,49 +94,77 @@ class _AccountInfoScreen extends State<AccountInfoScreen> {
items: widget.users.map((user) {
return DropdownMenuItem<User>(
value: user,
child: Text(user.accountNo.toString()),
child: Text(
user.accountNo.toString(),
style: const TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
),
);
}).toList(),
isExpanded: true,
),
],
),
),
),
Expanded(
child: Card(
elevation: 4,
margin: const EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ListView(
children: [
InfoRow(
title: AppLocalizations.of(context).customerNumber,
value: selectedUser.cifNumber ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).accountType,
value: getFullAccountType(selectedUser.accountType),
),
InfoRow(
title: AppLocalizations.of(context).productName,
value: selectedUser.productType ?? 'N/A',
),
// InfoRow(title: 'Account Opening Date', value: users[selectedIndex].accountOpeningDate ?? 'N/A'),
InfoRow(
title: AppLocalizations.of(context).accountStatus,
value: 'OPEN',
),
InfoRow(
title: AppLocalizations.of(context).availableBalance,
title:
AppLocalizations.of(context).availableBalance,
value: selectedUser.availableBalance ?? 'N/A',
),
InfoRow(
title: AppLocalizations.of(context).currentBalance,
value: selectedUser.currentBalance ?? 'N/A',
),
users[selectedIndex].approvedAmount != null
? InfoRow(
title: AppLocalizations.of(context).approvedAmount,
if (users[selectedIndex].approvedAmount != null)
InfoRow(
title:
AppLocalizations.of(context).approvedAmount,
value: selectedUser.approvedAmount ?? 'N/A',
)
: const SizedBox.shrink(),
),
],
),
),
),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
opacity: 0.07, // Reduced opacity
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 200, // Adjust size as needed
height: 200, // Adjust size as needed
),
),
@@ -130,15 +195,18 @@ class InfoRow extends StatelessWidget {
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
fontSize: 18,
fontWeight: FontWeight.bold,
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 3),
Text(
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
? Symbols.call_received
: Symbols.call_made,
color: isCredit ? Colors.green : Colors.red,
color: isCredit
? const Color(0xFF10BB10)
: Theme.of(context).colorScheme.error,
size: 28,
),
],
@@ -51,9 +53,9 @@ class TransactionDetailsScreen extends StatelessWidget {
// Date centered
Text(
transaction.date ?? "",
style: const TextStyle(
style: TextStyle(
fontSize: 16,
color: Colors.grey,
color: Theme.of(context).textTheme.bodySmall?.color,
),
textAlign: TextAlign.center,
),
@@ -61,7 +63,7 @@ class TransactionDetailsScreen extends StatelessWidget {
),
),
),
const Divider(),
Divider(color: Theme.of(context).dividerColor),
Expanded(
flex: 5,
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/set_password_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 '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
@@ -30,14 +31,25 @@ class LoginScreenState extends State<LoginScreen>
super.dispose();
}
void _submitForm() {
void _submitForm() async {
if (_formKey.currentState!.validate()) {
final bool? verificationSuccess = await Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => VerificationScreen(
customerNo: _customerNumberController.text.trim(),
password: _passwordController.text,
),
),
);
if (verificationSuccess == true && mounted) {
context.read<AuthCubit>().login(
_customerNumberController.text.trim(),
_passwordController.text,
);
}
}
}
@override
Widget build(BuildContext context) {

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);
} catch (e) {
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) {
return AlertDialog(
title: Text(AppLocalizations.of(context).success),
content: Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
content:
Text(AppLocalizations.of(context).beneficiaryDeletedSuccessfully),
actions: <Widget>[
TextButton(
child: Text(AppLocalizations.of(context).ok),
@@ -53,8 +56,8 @@ class BeneficiaryDetailsScreen extends StatelessWidget {
builder: (BuildContext context) {
return AlertDialog(
title: Text(AppLocalizations.of(context).deleteBeneficiary),
content:
Text(AppLocalizations.of(context).areYouSureYouWantToDeleteThisBeneficiary),
content: Text(AppLocalizations.of(context)
.areYouSureYouWantToDeleteThisBeneficiary),
actions: <Widget>[
TextButton(
child: Text(AppLocalizations.of(context).cancel),

View File

@@ -21,21 +21,47 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
var service = getIt<BeneficiaryService>();
bool _isLoading = true;
List<Beneficiary> _beneficiaries = [];
List<Beneficiary> _filteredBeneficiaries = [];
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
_loadBeneficiaries();
_searchController.addListener(() {
_filterBeneficiaries(_searchController.text);
});
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _loadBeneficiaries() async {
final data = await service.fetchBeneficiaryList();
setState(() {
_beneficiaries = data;
_filteredBeneficiaries = data;
_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() {
return ListView.builder(
itemCount: 6,
@@ -63,15 +89,17 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
}
Widget _buildBeneficiaryList() {
if (_beneficiaries.isEmpty) {
if (_filteredBeneficiaries.isEmpty) {
return Center(
child: Text(AppLocalizations.of(context).noBeneficiaryFound));
}
return ListView.builder(
itemCount: _beneficiaries.length,
itemCount: _filteredBeneficiaries.length,
itemBuilder: (context, index) {
final item = _beneficiaries[index];
return ListTile(
final item = _filteredBeneficiaries[index];
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
@@ -97,6 +125,7 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
),
);
},
),
);
},
);
@@ -111,7 +140,27 @@ class _ManageBeneficiariesScreen extends State<ManageBeneficiariesScreen> {
),
body: Stack(
children: [
Column(
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Search by name or account number",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
Expanded(
child:
_isLoading ? _buildShimmerList() : _buildBeneficiaryList(),
),
],
),
IgnorePointer(
child: Center(
child: Opacity(

View File

@@ -56,8 +56,6 @@ class _BlockCardScreen extends State<BlockCardScreen> {
appBar: AppBar(
title: Text(
AppLocalizations.of(context).blockCard,
style:
const TextStyle(color: Colors.black, fontWeight: FontWeight.w500),
),
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",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontSize: 15.5,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:kmobile/data/models/user.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
import '../../../l10n/app_localizations.dart';
class CustomerInfoScreen extends StatefulWidget {
@@ -13,6 +14,7 @@ class CustomerInfoScreen extends StatefulWidget {
class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
late final User user = widget.user;
int _selectedCard = 0; // 0 for Personal Info, 1 for KYC
String _maskPrimaryId(String? primaryId) {
if (primaryId == null || primaryId.length <= 4) {
@@ -33,74 +35,123 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
.replaceFirst(RegExp('\n'), ''),
),
),
body: Stack(
body: SafeArea(
child: Stack(
children: [
SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SafeArea(
child: Center(
child: Column(
children: [
const SizedBox(height: 30),
CircleAvatar(
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
const SizedBox(
width: 56,
height: 56,
child: CircleAvatar(
radius: 50,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 150,
height: 150,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.only(top: 10.0),
child: Text(
user.name ?? '',
style: TextStyle(
fontSize: 20,
color: theme.colorScheme.onSurface,
fontWeight: FontWeight.w500,
child: Icon(
Symbols.person,
size: 56,
),
),
),
const SizedBox(width: 12),
// Name + mobile
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${AppLocalizations.of(context).cif}: ${user.cifNumber ?? 'N/A'}',
style: TextStyle(
fontSize: 16,
color: theme.colorScheme.onSurfaceVariant),
// 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: 30),
InfoField(
label: AppLocalizations.of(context).activeAccounts,
value: user.activeAccounts?.toString() ?? '6',
),
InfoField(
label: AppLocalizations.of(context).mobileNumber,
value: user.mobileNo ?? 'N/A',
const SizedBox(height: 4),
Text(
user.cifNumber ?? '',
style:
theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface
.withOpacity(0.7),
),
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
],
),
),
],
),
),
),
const SizedBox(height: 16),
// Toggle Buttons for Personal Info and KYC
SizedBox(
width: double.infinity,
child: CupertinoSlidingSegmentedControl<int>(
groupValue: _selectedCard,
thumbColor: Theme.of(context)
.colorScheme
.onPrimary, // Set selected switch color to theme primary color
onValueChanged: (int? newValue) {
if (newValue != null) {
setState(() {
_selectedCard = newValue;
});
}
},
children: {
0: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child: Text(
AppLocalizations.of(context).personaldetails),
),
1: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 10),
child:
Text(AppLocalizations.of(context).kycdetails),
),
},
),
),
const SizedBox(height: 16),
// Card that shows content based on the toggle
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: theme.colorScheme.outline.withOpacity(0.2),
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: _selectedCard == 0
? _buildPersonalInfo(theme)
: _buildKycDetails(theme),
),
),
),
],
),
),
),
@@ -120,6 +171,64 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
),
],
),
));
}
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: [
Text(
label,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurfaceVariant,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.6),
),
),
const SizedBox(height: 3),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(fontSize: 16, color: theme.colorScheme.onSurface),
value.isEmpty ? 'N/A' : value,
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_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/features/accounts/screens/account_info_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_state.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:google_fonts/google_fonts.dart';
import 'package:shimmer/shimmer.dart';
import 'package:kmobile/data/models/transaction.dart';
import '../../../l10n/app_localizations.dart';
class DashboardScreen extends StatefulWidget {
@@ -32,47 +30,189 @@ class DashboardScreen extends StatefulWidget {
}
class _DashboardScreenState extends State<DashboardScreen>
with SingleTickerProviderStateMixin {
with SingleTickerProviderStateMixin, RouteAware {
int selectedAccountIndex = 0;
bool isVisible = false;
Map<String, bool> _visibilityMap = {};
bool isRefreshing = false;
bool isBalanceLoading = false;
bool _biometricPromptShown = false;
bool _txLoading = false;
List<Transaction> _transactions = [];
bool _txInitialized = false;
Future<void> _loadTransactions(String accountNo) async {
setState(() {
_txLoading = true;
_transactions = [];
});
try {
final repo = getIt<TransactionRepository>();
final txs = await repo.fetchTransactions(accountNo);
var fiveTxns = <Transaction>[];
//only take the first 5 transactions
if (txs.length > 5) {
fiveTxns = txs.sublist(0, 5);
} else {
fiveTxns = txs;
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)!);
}
setState(() => _transactions = fiveTxns);
} catch (e) {
log(accountNo, error: e);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).failedToLoad(e.toString()),
@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(() {
_visibilityMap.clear();
});
}
Widget _buildAccountCard(User user, bool isSelected) {
final theme = Theme.of(context);
final bool isCardVisible = _visibilityMap[user.accountNo] ?? false;
// Animated scale for the selected card
final scale = isSelected ? 1.02 : 0.9;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 3.0),
child: AnimatedScale(
duration: const Duration(milliseconds: 200),
scale: scale,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 10,
),
decoration: BoxDecoration(
color: const Color(0xFF01A04C),
borderRadius: BorderRadius.circular(20),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Top section with account type and number (no refresh button here)
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
getFullAccountType(user.accountType),
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 16,
fontWeight: FontWeight.w700,
),
),
Text(
user.accountNo ?? 'N/A',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 14,
fontWeight: FontWeight.w700,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
if (isSelected) // Show logo only if card is selected
CircleAvatar(
radius: 20,
backgroundColor: Colors.white,
child: Padding(
padding: const EdgeInsets.all(2.0),
child: ClipOval(
child: Image.asset(
'assets/images/logo.png',
width: 30,
height: 30,
fit: BoxFit.cover,
),
),
),
),
],
),
const Spacer(),
// Bottom section with balance and combined toggle/refresh
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (isRefreshing && isSelected)
Expanded(child: _buildBalanceShimmer())
else
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Row(
children: [
Text(
"",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
Text(
isCardVisible
? user.currentBalance ?? '0.00'
: '*****',
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 40,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
const SizedBox(width: 10), // A steady space
if (isSelected) // Only show toggle for selected card
InkWell(
onTap: () async {
if (isRefreshing)
return; // Prevent taps while refreshing
final accountNo = user.accountNo;
if (accountNo == null) return;
final bool currentVisibility =
_visibilityMap[accountNo] ?? false;
if (!currentVisibility) {
// If hidden, refresh data and then show the balance
await _refreshAccountData(context);
if (mounted) {
setState(() {
_visibilityMap[accountNo] = true;
});
}
} else {
// If visible, just hide it
setState(() {
_visibilityMap[accountNo] = false;
});
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isCardVisible
? Symbols.visibility_lock
: Symbols.visibility,
color: theme.scaffoldBackgroundColor,
weight: 800,
),
),
),
],
),
const Spacer(),
],
),
),
),
);
} finally {
if (mounted) {
setState(() => _txLoading = false);
}
}
}
Future<void> _refreshAccountData(BuildContext context) async {
@@ -102,8 +242,7 @@ class _DashboardScreenState extends State<DashboardScreen>
return Shimmer.fromColors(
baseColor: theme.colorScheme.primary,
highlightColor: theme.colorScheme.onPrimary,
child: Container(
width: 200, height: 42, color: theme.scaffoldBackgroundColor),
child: Container(height: 36, color: theme.scaffoldBackgroundColor),
);
}
@@ -150,6 +289,10 @@ class _DashboardScreenState extends State<DashboardScreen>
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;
}
@@ -200,6 +343,19 @@ class _DashboardScreenState extends State<DashboardScreen>
@override
Widget build(BuildContext 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>(
listener: (context, state) async {
if (state is Authenticated && !_biometricPromptShown) {
@@ -214,52 +370,67 @@ class _DashboardScreenState extends State<DashboardScreen>
child: Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor,
leading: Padding(
padding: const EdgeInsets.only(left: 10.0),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = '';
if (authState is Authenticated) {
if (selectedAccountIndex >= 0 &&
selectedAccountIndex < authState.users.length) {
mobileNumberToPass =
authState.users[selectedAccountIndex].mobileNo ?? '';
}
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
ProfileScreen(mobileNumber: mobileNumberToPass),
child: Material(
elevation: 4.0,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
side: const BorderSide(color: Colors.white, width: 1.5),
borderRadius: BorderRadius.circular(12.0),
),
);
},
child: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
child: Container(
width: 40,
height: 40,
fit: BoxFit.cover,
decoration: const BoxDecoration(
color: Colors.white,
),
child: Image.asset(
'assets/images/logo.png',
fit: BoxFit.fill,
),
),
),
),
title: Text(
AppLocalizations.of(context).kccbMobile,
textAlign: TextAlign.left,
AppLocalizations.of(context).kccBankFull.replaceAll('-', '\u2011'),
textAlign: TextAlign.center,
softWrap: true, // Explicitly allow wrapping
maxLines: 2, // Allow text to wrap to a maximum of 2 lines
style: TextStyle(
color: theme.colorScheme.primary,
color: theme.colorScheme.onPrimary,
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>(
builder: (context, state) {
@@ -276,21 +447,23 @@ class _DashboardScreenState extends State<DashboardScreen>
// firsttime load
if (!_txInitialized) {
_txInitialized = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadTransactions(currAccount.accountNo!);
});
WidgetsBinding.instance.addPostFrameCallback((_) {});
}
_pageController ??= PageController(
initialPage: selectedAccountIndex,
viewportFraction: 0.75,
);
final firstName = getProcessedFirstName(currAccount.name);
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 16), // Added spacing
Padding(
padding: const EdgeInsets.only(left: 8.0),
padding: const EdgeInsets.only(left: 4.0),
child: Text(
"${AppLocalizations.of(context).hi} $firstName!",
style: GoogleFonts.baumans().copyWith(
@@ -302,176 +475,76 @@ class _DashboardScreenState extends State<DashboardScreen>
),
const SizedBox(height: 16),
// Account Info Card
Container(
padding: const EdgeInsets.symmetric(
horizontal: 18,
vertical: 10,
// Account Info Cards
SizedBox(
height: 160,
child: PageView.builder(
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),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"${getFullAccountType(currAccount.accountType)}: ",
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 18,
fontWeight: FontWeight.w700,
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
AllAccountsScreen(users: users),
),
),
DropdownButton<int>(
value: selectedAccountIndex,
dropdownColor: theme.colorScheme.primary,
underline: const SizedBox(),
icon: const Icon(Icons.keyboard_arrow_down),
iconEnabledColor: theme.colorScheme.onPrimary,
style: TextStyle(
color: theme.colorScheme.onPrimary,
fontSize: 18,
),
items: List.generate(users.length, (index) {
return DropdownMenuItem<int>(
value: index,
);
},
child: Text(
users[index].accountNo ?? 'N/A',
AppLocalizations.of(context).viewall,
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,
fontSize: 14,
fontWeight: FontWeight.bold,
color: theme.colorScheme.primary,
),
),
),
],
),
const SizedBox(height: 15),
],
),
),
const SizedBox(height: 18),
Text(
AppLocalizations.of(context).quickLinks,
style: const TextStyle(fontSize: 17),
),
const SizedBox(height: 16),
const SizedBox(height: 24),
// Quick Links
GridView.count(
crossAxisCount: 4,
crossAxisCount: 3,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1,
children: [
_buildQuickLink(
Symbols.id_card,
@@ -539,17 +612,12 @@ class _DashboardScreenState extends State<DashboardScreen>
MaterialPageRoute(
builder: (context) =>
AccountStatementScreen(
accountNo: users[selectedAccountIndex]
.accountNo!,
balance: users[selectedAccountIndex]
.availableBalance!,
accountType:
users[selectedAccountIndex]
.accountType!,
users: users,
selectedIndex: selectedAccountIndex,
)));
}),
_buildQuickLink(Icons.location_pin, "Branch Locator",
() {
_buildQuickLink(Icons.location_pin,
AppLocalizations.of(context).branchlocator, () {
Navigator.push(
context,
MaterialPageRoute(
@@ -574,68 +642,23 @@ class _DashboardScreenState extends State<DashboardScreen>
builder: (context) =>
const EnquiryScreen()));
}),
],
),
const SizedBox(height: 5),
// 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: () {
_buildQuickLink(
Symbols.person,
AppLocalizations.of(context).profile,
() {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
TransactionDetailsScreen(transaction: tx),
builder: (context) => ProfileScreen(
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(
IconData icon,
String label,
@@ -684,28 +681,36 @@ class _DashboardScreenState extends State<DashboardScreen>
bool disable = false,
}) {
final theme = Theme.of(context);
return InkWell(
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: InkWell(
onTap: disable ? null : onTap,
borderRadius: BorderRadius.circular(12.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 30,
color: disable
? theme.colorScheme.onSurface.withOpacity(0.3)
: theme.colorScheme.primary,
grade: 200,
weight: 700,
color: disable ? theme.disabledColor : theme.colorScheme.primary,
),
const SizedBox(height: 4),
Text(
label,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 13),
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 12,
color:
disable ? theme.disabledColor : theme.colorScheme.onSurface,
),
),
],
),
),
);
}
}

View File

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

View File

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

View File

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

View File

@@ -34,18 +34,23 @@ class FundTransferScreen extends StatelessWidget {
builder: (context, state) {
return Stack(
children: [
ListView(
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FundTransferManagementTile(
Expanded(
child: FundTransferManagementTile(
icon: Symbols.person,
// Restore localization for the label
label: "Self Pay",
subtitle:
AppLocalizations.of(context).ftselfpaysubtitle,
onTap: () {
// The accounts list is passed directly from the constructor
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferSelfAccountsScreen(
builder: (context) =>
FundTransferSelfAccountsScreen(
debitAccountNo: creditAccountNo,
remitterName: remitterName,
accounts: accounts,
@@ -53,19 +58,21 @@ class FundTransferScreen extends StatelessWidget {
),
);
},
// Disable the tile if the state is not Authenticated
disable: state is! Authenticated,
),
const Divider(height: 1),
FundTransferManagementTile(
),
const SizedBox(height: 16),
Expanded(
child: FundTransferManagementTile(
icon: Symbols.input_circle,
// Restore localization for the label
label: AppLocalizations.of(context).ownBank,
subtitle: AppLocalizations.of(context).ftownsubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen(
builder: (context) =>
FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: true,
@@ -74,16 +81,20 @@ class FundTransferScreen extends StatelessWidget {
);
},
),
const Divider(height: 1),
FundTransferManagementTile(
),
const SizedBox(height: 16),
Expanded(
child: FundTransferManagementTile(
icon: Symbols.output_circle,
// Restore localization for the label
label: AppLocalizations.of(context).outsideBank,
subtitle:
AppLocalizations.of(context).ftoutsidesubtitle,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferBeneficiaryScreen(
builder: (context) =>
FundTransferBeneficiaryScreen(
creditAccountNo: creditAccountNo,
remitterName: remitterName,
isOwnBank: false,
@@ -92,9 +103,10 @@ class FundTransferScreen extends StatelessWidget {
);
},
),
const Divider(height: 1),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
@@ -120,6 +132,7 @@ class FundTransferScreen extends StatelessWidget {
class FundTransferManagementTile extends StatelessWidget {
final IconData icon;
final String label;
final String? subtitle;
final VoidCallback onTap;
final bool disable;
@@ -127,18 +140,65 @@ class FundTransferManagementTile extends StatelessWidget {
super.key,
required this.icon,
required this.label,
this.subtitle,
required this.onTap,
this.disable = false,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(label),
trailing: const Icon(Symbols.arrow_right, size: 20),
onTap: onTap,
enabled: !disable,
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
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,7 +55,10 @@ class FundTransferSelfAccountsScreen extends StatelessWidget {
itemCount: filteredAccounts.length,
itemBuilder: (context, index) {
final account = filteredAccounts[index];
return ListTile(
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
child: ListTile(
leading: CircleAvatar(
radius: 24,
backgroundColor: Colors.transparent,
@@ -80,7 +83,8 @@ class FundTransferSelfAccountsScreen extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => FundTransferSelfAmountScreen(
builder: (context) =>
FundTransferSelfAmountScreen(
debitAccountNo: debitAccountNo,
creditAccount: account, // Pass the User object
remitterName: remitterName,
@@ -88,6 +92,7 @@ class FundTransferSelfAccountsScreen extends StatelessWidget {
),
);
},
),
);
},
),

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:kmobile/api/services/limit_service.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:intl/intl.dart';
class DailyLimitScreen extends StatefulWidget {
const DailyLimitScreen({super.key});
final String mobileNumber;
const DailyLimitScreen({super.key, required this.mobileNumber});
@override
State<DailyLimitScreen> createState() => _DailyLimitScreenState();
}
@@ -74,7 +76,7 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
child: Text(localizations.cancel),
),
ElevatedButton(
onPressed: () {
onPressed: () async {
final value = double.tryParse(_limitController.text);
if (value == null || value <= 0) return;
@@ -88,8 +90,26 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
),
);
} else {
service.editLimit(value);
Navigator.of(dialogContext).pop(value);
try {
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),
@@ -204,14 +224,6 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
),
),
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,17 +23,16 @@ class PreferenceScreen extends StatelessWidget {
return Stack(
children: [
ListView(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
//Set Prefered Username
// ListTile(
// leading: const Icon(Icons.person),
// title: const Text("Set Prefered Username"),
// onTap: () {
// }),
// Language Selection
ListTile(
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.language),
title: Text(loc.language),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
@@ -41,24 +40,33 @@ class PreferenceScreen extends StatelessWidget {
);
},
),
),
//Theme Mode Switch (Light/Dark)
ListTile(
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
ListTile(
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(

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/security/secure_storage.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:material_symbols_icons/material_symbols_icons.dart';
import '../../di/injection.dart';
import '../../l10n/app_localizations.dart';
import 'package:kmobile/features/profile/preferences/preference_screen.dart';
class ProfileScreen extends StatefulWidget {
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
State<ProfileScreen> createState() => _ProfileScreenState();
@@ -30,8 +36,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
}
Future<String> _getAppVersion() async {
final PackageInfo info = await PackageInfo.fromPlatform();
return 'Version ${info.version} (${info.buildNumber})';
return 'Version 1.0.1 (1))';
}
Future<void> _loadBiometricStatus() async {
@@ -160,31 +165,95 @@ class _ProfileScreenState extends State<ProfileScreen> {
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.profile), // Localized "Profile"
title: Text(loc.profile),
elevation: 0,
),
body: Stack(
children: [
ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
ListTile(
leading: const Icon(Icons.settings),
title: Text(loc.preferences),
trailing: const Icon(Icons.chevron_right),
// ===== Profile Header =====
Card(
child: Padding(
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: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const PreferenceScreen()),
builder: (context) => const PreferenceScreen(),
),
);
},
),
ListTile(
leading: const Icon(Icons.security),
title: Text(loc.securitySettings),
trailing: const Icon(Icons.chevron_right),
_SectionTile(
leadingIcon: Icons.security,
title: loc.securitySettings,
onTap: () {
Navigator.push(
context,
@@ -196,66 +265,114 @@ class _ProfileScreenState extends State<ProfileScreen> {
);
},
),
ListTile(
leading: const Icon(Icons.currency_rupee),
title: Text(AppLocalizations.of(context).dailylimit),
_SectionTile(
leadingIcon: Icons.currency_rupee,
title: loc.dailylimit,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DailyLimitScreen()),
builder: (context) =>
DailyLimitScreen(mobileNumber: widget.mobileNumber),
),
);
},
),
SwitchListTile(
title:
Text(AppLocalizations.of(context).enableFingerprintLogin),
Card(
child: SwitchListTile(
title: Text(loc.enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
// The state is now managed within _handleBiometricToggle
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
),
ListTile(
),
const SizedBox(height: 16),
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: const Text("App Version"),
title: Text(loc.appVersion),
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();
return const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
);
} else if (snapshot.hasError) {
return const Text("Error");
return Text(loc.error);
} else {
// Display the version number once the future is complete
return Text(
snapshot.data ?? "N/A",
selectionColor: const Color(0xFFFFFFFF),
);
}
},
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: Text(AppLocalizations.of(context).logout),
),
),
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 {
final shouldExit = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).logout),
content: Text(AppLocalizations.of(context).logoutCheck),
title: Text(loc.logout),
content: Text(loc.logoutCheck),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).no),
child: Text(loc.no),
),
TextButton(
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(
leading: const Icon(Icons.logout),
title: Text(AppLocalizations.of(context).deregister),
_SectionTile(
leadingIcon: Icons.logout,
title: loc.deregister,
trailChevron: false,
onTap: () async {
final shouldLogout = await showDialog<bool>(
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,8 +24,11 @@ class SecuritySettingsScreen extends StatelessWidget {
body: Stack(
children: [
ListView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
children: [
ListTile(
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.lock_outline),
title: Text(loc.changeLoginPassword),
trailing: const Icon(Icons.chevron_right),
@@ -40,8 +43,10 @@ class SecuritySettingsScreen extends StatelessWidget {
);
},
),
const Divider(height: 1),
ListTile(
),
Card(
margin: const EdgeInsets.only(bottom: 10),
child: ListTile(
leading: const Icon(Icons.pin),
title: Text(loc.changeMpin),
trailing: const Icon(Icons.chevron_right),
@@ -57,14 +62,17 @@ class SecuritySettingsScreen extends StatelessWidget {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(loc.mpinChangedSuccessfully),
backgroundColor: Colors.green,
backgroundColor:
Theme.of(context).colorScheme.secondary,
),
);
}
},
),
const Divider(height: 1),
ListTile(
),
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),
@@ -117,6 +125,7 @@ class SecuritySettingsScreen extends StatelessWidget {
}
},
),
),
],
),
IgnorePointer(

View File

@@ -23,11 +23,16 @@ class _QuickPayScreen extends State<QuickPayScreen> {
),
body: Stack(
children: [
ListView(
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
QuickPayManagementTile(
Expanded(
child: QuickPayManagementTile(
icon: Symbols.input_circle,
label: AppLocalizations.of(context).ownBank,
subtitle: AppLocalizations.of(context).quickownsubtitle,
onTap: () {
Navigator.push(
context,
@@ -39,10 +44,13 @@ class _QuickPayScreen extends State<QuickPayScreen> {
);
},
),
const Divider(height: 1),
QuickPayManagementTile(
),
const SizedBox(height: 16),
Expanded(
child: QuickPayManagementTile(
icon: Symbols.output_circle,
label: AppLocalizations.of(context).outsideBank,
subtitle: AppLocalizations.of(context).quickoutsidesubtitle,
onTap: () {
Navigator.push(
context,
@@ -54,9 +62,10 @@ class _QuickPayScreen extends State<QuickPayScreen> {
);
},
),
const Divider(height: 1),
),
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
@@ -80,6 +89,7 @@ class _QuickPayScreen extends State<QuickPayScreen> {
class QuickPayManagementTile extends StatelessWidget {
final IconData icon;
final String label;
final String? subtitle;
final VoidCallback onTap;
final bool disable;
@@ -87,18 +97,63 @@ class QuickPayManagementTile extends StatelessWidget {
super.key,
required this.icon,
required this.label,
this.subtitle,
required this.onTap,
this.disable = false,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Icon(icon),
title: Text(label),
trailing: const Icon(Symbols.arrow_right, size: 20),
onTap: onTap,
enabled: !disable,
final theme = Theme.of(context);
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
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(
title: Text(
AppLocalizations.of(context).quickPayOwnBank,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500),
),
centerTitle: false,
),

View File

@@ -1,8 +1,9 @@
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/api/services/branch_service.dart'; // Added: Import BranchService for Atm model and API calls
import 'package:kmobile/di/injection.dart'; // Added: Import for dependency injection (getIt)
import 'package:shimmer/shimmer.dart'; // Added: Import for shimmer loading effect
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
@@ -15,10 +16,12 @@ class ATMLocatorScreen extends StatefulWidget {
class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
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
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
void initState() {
@@ -28,7 +31,8 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
/// Fetches the list of ATMs from the API using BranchService.
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(() {
_allAtms = data; // Update the list of 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.
void _filterAtms(String query) { // Changed: Renamed from _filterLocations
void _filterAtms(String query) {
// Changed: Renamed from _filterLocations
setState(() {
if (query.isEmpty) {
_filteredAtms = _allAtms; // If query is empty, show all ATMs
} else {
_filteredAtms = _allAtms.where((atm) { // Changed: Filter based on Atm object
_filteredAtms = _allAtms.where((atm) {
// Changed: Filter based on Atm object
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();
}
});
@@ -54,7 +62,8 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
Widget build(BuildContext context) {
return Scaffold(
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(
children: [
@@ -64,7 +73,8 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
padding: const EdgeInsets.all(12.0),
child: TextField(
controller: _searchController,
onChanged: _filterAtms, // Updated: Call _filterAtms on text change
onChanged:
_filterAtms, // Updated: Call _filterAtms on text change
decoration: InputDecoration(
hintText: "Name/Address", // Hint text for the search bar
prefixIcon: const Icon(Icons.search), // Search icon
@@ -81,12 +91,16 @@ class _ATMLocatorScreenState extends State<ATMLocatorScreen> {
? _buildShimmerList() // Display shimmer while loading
: _filteredAtms.isEmpty
? 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(
itemCount: _filteredAtms.length, // Number of items in the filtered list
itemCount: _filteredAtms
.length, // Number of items in the filtered list
itemBuilder: (context, index) {
final atm = _filteredAtms[index]; // Get the current Atm object
return _buildAtmItem(atm); // Build the ATM list item
final atm = _filteredAtms[
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.
Widget _buildAtmItem(Atm atm) { // Changed: Takes an Atm object
Widget _buildAtmItem(Atm atm) {
// Changed: Takes an Atm object
return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: const Icon(Icons.currency_rupee), // Icon for ATM
leading: const Icon(Icons.credit_card), // Icon for ATM
title: Text(atm.name, // Display the ATM's name
style: const TextStyle(fontWeight: FontWeight.bold)),
onTap: () {

View File

@@ -20,17 +20,17 @@
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailRow("Branch Name", branch.branch_name),
_buildDetailRow("Branch Code", branch.branch_code),
_buildDetailRow("Zone", branch.zone),
_buildDetailRow("Tehsil", branch.tehsil),
_buildDetailRow("Block", branch.block),
_buildDetailRow("District", branch.distt_name),
_buildDetailRow("Pincode", branch.pincode),
// _buildDetailRow("Post Office", branch.post_office),
// _buildDetailRow("Date of Opening", branch.date_of_opening),
// _buildDetailRow("Branch Type", branch.type_of_branch),
_buildDetailRow("Telephone No.", branch.telephone_no),
_buildDetailRow(context, "Branch Name", branch.branch_name),
_buildDetailRow(context, "Branch Code", branch.branch_code),
_buildDetailRow(context, "Zone", branch.zone),
_buildDetailRow(context, "Tehsil", branch.tehsil),
_buildDetailRow(context, "Block", branch.block),
_buildDetailRow(context, "District", branch.distt_name),
_buildDetailRow(context, "Pincode", branch.pincode),
// _buildDetailRow(context, "Post Office", branch.post_office),
// _buildDetailRow(context, "Date of Opening", branch.date_of_opening),
// _buildDetailRow(context, "Branch Type", branch.type_of_branch),
_buildDetailRow(context, "Telephone No.", branch.telephone_no),
// _buildDetailRow("RTGS Account No.", branch.rtgs_acct_no),
// _buildDetailRow("RBI Code 1", branch.rbi_code_1),
// _buildDetailRow("RBI Code 2", branch.rbi_code_2),
@@ -58,7 +58,8 @@
);
}
Widget _buildDetailRow(String label, String value) {
Widget _buildDetailRow(BuildContext context, String label, String value) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
@@ -68,7 +69,7 @@
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
color: theme.textTheme.bodySmall?.color,
),
),
const SizedBox(height: 4),
@@ -77,10 +78,11 @@
onTap: () => _launchUrl('tel:$value'),
child: Text(
value,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.blue, // Indicate it's clickable
color:
theme.colorScheme.primary, // Indicate it's clickable
decoration: TextDecoration.underline,
),
),
@@ -92,7 +94,7 @@
fontWeight: FontWeight.w500,
),
),
const Divider(height: 16),
Divider(height: 16, color: theme.dividerColor),
],
),
);

View File

@@ -1,12 +1,12 @@
// ignore_for_file: unused_element
import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart';
import 'package:kmobile/api/services/branch_service.dart';
import 'package:kmobile/di/injection.dart';
import 'package:shimmer/shimmer.dart';
import 'package:kmobile/features/service/screens/branch_details_screen.dart';
import '../../../l10n/app_localizations.dart';
class BranchLocatorScreen extends StatefulWidget {
const BranchLocatorScreen({super.key});
@@ -51,12 +51,13 @@ class BranchLocatorScreen extends StatefulWidget {
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Branch Locator"),
title: Text(
AppLocalizations.of(context).branchlocator,
),
),
body: Stack(
children: [
@@ -83,11 +84,13 @@ class BranchLocatorScreen extends StatefulWidget {
? _buildShimmerList() // Changed to shimmer
: _filteredBranches.isEmpty
? const Center(
child: Text("No matching branches found")) // Updated tex
child: Text(
"No matching branches found")) // Updated tex
: ListView.builder(
itemCount: _filteredBranches.length,
itemBuilder: (context, index) {
final branch = _filteredBranches[index]; // Changed to
final branch =
_filteredBranches[index]; // Changed to
return _buildBranchItem(branch); // Updated
},
),
@@ -133,7 +136,7 @@ class BranchLocatorScreen extends StatefulWidget {
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile(
leading: const CircleAvatar(
child: Icon(Icons.location_city),
child: Icon(Icons.account_balance),
),
title: Text(branch.branch_name,
style: const TextStyle(fontWeight: FontWeight.bold)),
@@ -150,7 +153,6 @@ class BranchLocatorScreen extends StatefulWidget {
);
}
// Shimmer loading list
Widget _buildShimmerList() {
return ListView.builder(

View File

@@ -1,6 +1,4 @@
import 'package:kmobile/features/service/screens/atm_locator_screen.dart';
import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart';
@@ -27,23 +25,13 @@ class _ServiceScreen extends State<ServiceScreen> {
),
body: Stack(
children: [
ListView(
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// ServiceManagementTile(
// icon: Symbols.add,
// label: AppLocalizations.of(context).accountOpeningDeposit,
// onTap: () {},
// disabled: true,
// ),
// const Divider(height: 1),
// ServiceManagementTile(
// icon: Symbols.add,
// label: AppLocalizations.of(context).accountOpeningLoan,
// onTap: () {},
// disabled: true,
// ),
// const Divider(height: 1),
ServiceManagementTile(
Expanded(
child: ServiceManagementTile(
icon: Symbols.captive_portal,
label: AppLocalizations.of(context).quickLinks,
onTap: () {
@@ -54,21 +42,26 @@ class _ServiceScreen extends State<ServiceScreen> {
},
disabled: false,
),
const Divider(height: 1),
ServiceManagementTile(
),
const SizedBox(height: 16),
Expanded(
child: ServiceManagementTile(
icon: Symbols.question_mark,
label: AppLocalizations.of(context).faq,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const FaqsScreen()),
MaterialPageRoute(
builder: (context) => const FaqsScreen()),
);
},
disabled: false,
),
const Divider(height: 1),
ServiceManagementTile(
),
const SizedBox(height: 16),
Expanded(
child: ServiceManagementTile(
icon: Symbols.location_pin,
label: "ATM Locator",
label: AppLocalizations.of(context).atmlocator,
onTap: () {
Navigator.push(
context,
@@ -77,9 +70,11 @@ class _ServiceScreen extends State<ServiceScreen> {
},
disabled: false,
),
const Divider(height: 1),
),
// No Spacer() needed here as Expanded children will fill space
],
),
),
IgnorePointer(
child: Center(
child: Opacity(
@@ -117,23 +112,42 @@ class ServiceManagementTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListTile(
leading: Icon(
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
elevation: 4, // Add some elevation for better visual separation
child: InkWell(
onTap:
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,
color: disabled ? theme.disabledColor : null,
size: 48, // Make icon larger
color:
disabled ? theme.disabledColor : theme.colorScheme.primary,
),
title: Text(
const SizedBox(height: 12),
Text(
label,
style: TextStyle(
color: disabled ? theme.disabledColor : null,
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",
"securitySettings": "Security Settings",
"kconnect": "Kconnect",
"kccBankFull": "Kangra Central Co-operative Bank",
"kccBankFull": "The Kangra Central Co-operative Bank Ltd.",
"themeColor": "Theme Color",
"selectThemeColor": "Select Theme Color",
"violet": "Violet",
@@ -323,7 +323,7 @@
"details": "Details",
"remarks": "Remarks (Optional)",
"kccbMobile": "KCCB Mobile",
"faq": "Frequently Asked Questions(FAQs)",
"faq": "Frequently Asked Questions",
"branches": "Branches",
"atms": "ATMs",
"dailylimit": "Daily Transaction Limit",
@@ -404,5 +404,17 @@
"postOffice": "Post Office",
"rbiCode1": "RBI Code 1",
"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": "डाकघर",
"rbiCode1": "आरबीआई कोड 1",
"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
final compromisedMessage = await SecurityService.deviceCompromisedMessage;
if (compromisedMessage != null) {
runApp(MaterialApp(
home: SecurityErrorScreen(message: compromisedMessage),
));
return;
}
// final compromisedMessage = await SecurityService.deviceCompromisedMessage;
// if (compromisedMessage != null) {
// runApp(MaterialApp(
// home: SecurityErrorScreen(message: compromisedMessage),
// ));
// return;
// }
await setupDependencies();
runApp(const KMobile());
}

View File

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

View File

@@ -137,6 +137,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -238,6 +246,38 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description: flutter
@@ -541,6 +581,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -725,6 +773,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct main"
description:
@@ -813,6 +869,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description: flutter
@@ -874,6 +938,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.6"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
typed_data:
dependency: transitive
description:
@@ -947,7 +1019,7 @@ packages:
source: hosted
version: "3.1.4"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff

View File

@@ -34,7 +34,7 @@ dependencies:
cupertino_icons: ^1.0.6
jailbreak_root_detection: ^1.1.6
equatable: ^2.0.7
dio: ^5.8.0+1
dio: ^5.9.0
flutter_secure_storage: ^9.2.4
bloc: ^9.0.0
flutter_bloc: ^9.1.0
@@ -61,6 +61,11 @@ dependencies:
device_info_plus: ^11.3.0
showcaseview: ^2.0.3
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"
@@ -114,6 +119,8 @@ flutter:
- assets/images/yes_bank_logo.png
- assets/images/uco_logo.png
- assets/images/ipos_logo.png
- assets/images/profile.svg
- assets/images/profile.png
- assets/animations/rupee.json
- assets/animations/error.json
- assets/animations/done.json

View File

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