16 Commits

60 changed files with 2440 additions and 1384 deletions

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@@ -40,8 +40,7 @@ class BeneficiaryService {
} on DioException catch (e) { } on DioException catch (e) {
if (e.response?.statusCode == 404) { if (e.response?.statusCode == 404) {
throw Exception('INVALID IFSC CODE'); throw Exception('INVALID IFSC CODE');
} } else if (e.response?.statusCode == 401) {
else if (e.response?.statusCode == 401) {
throw Exception('INVALID IFSC CODE'); throw Exception('INVALID IFSC CODE');
} }
} catch (e) { } catch (e) {

View File

@@ -10,10 +10,7 @@ class ChangePasswordService {
}) async { }) async {
final response = await _dio.post( final response = await _dio.post(
'/api/otp/send', '/api/otp/send',
data: { data: {'mobileNumber': mobileNumber, 'type': "CHANGE_LPWORD"},
'mobileNumber': mobileNumber,
'type': "CHANGE_LPWORD"
},
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type"); throw Exception("Invalid Mobile Number/Type");
@@ -22,15 +19,12 @@ class ChangePasswordService {
return response.toString(); return response.toString();
} }
Future getOtpTpin({ Future getOtpTpin({
required String mobileNumber, required String mobileNumber,
}) async { }) async {
final response = await _dio.post( final response = await _dio.post(
'/api/otp/send', '/api/otp/send',
data: { data: {'mobileNumber': mobileNumber, 'type': "CHANGE_TPIN"},
'mobileNumber': mobileNumber,
'type': "CHANGE_TPIN"
},
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception("Invalid Mobile Number/Type"); throw Exception("Invalid Mobile Number/Type");
@@ -39,15 +33,14 @@ class ChangePasswordService {
return response.toString(); return response.toString();
} }
Future validateOtp({
Future validateOtp({
required String otp, required String otp,
required String mobileNumber, required String mobileNumber,
}) async { }) async {
final response = await _dio.post( final response = await _dio.post(
'/api/otp/verify?mobileNumber=$mobileNumber', '/api/otp/verify?mobileNumber=$mobileNumber',
data: { data: {
'otp' : otp, 'otp': otp,
}, },
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
@@ -56,22 +49,22 @@ class ChangePasswordService {
return response.toString(); return response.toString();
} }
Future validateChangePwd({ Future validateChangePwd({
required String OldLPsw, required String OldLPsw,
required String newLPsw, required String newLPsw,
required String confirmLPsw, required String confirmLPsw,
}) async { }) async {
final response = await _dio.post( final response = await _dio.post(
'/api/auth/change/login_password', '/api/auth/change/login_password',
data: { data: {
'OldLPsw': OldLPsw, 'OldLPsw': OldLPsw,
'newLPsw': newLPsw, 'newLPsw': newLPsw,
'confirmLPsw': confirmLPsw, 'confirmLPsw': confirmLPsw,
}, },
); );
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception("Wrong OTP"); throw Exception("Wrong OTP");
} }
return response.toString(); return response.toString();
} }
} }

View File

@@ -0,0 +1,57 @@
// ignore_for_file: collection_methods_unrelated_type
import 'dart:developer';
import 'package:dio/dio.dart';
class Limit {
final double dailyLimit;
final double usedLimit;
Limit({
required this.dailyLimit,
required this.usedLimit,
});
factory Limit.fromJson(Map<String, dynamic> json) {
return Limit(
dailyLimit: json['dailyLimit']!,
usedLimit: json['usedLimit']!,
);
}
}
class LimitService {
final Dio _dio;
LimitService(this._dio);
Future<Limit> getLimit() async {
try {
final response = await _dio.get('/api/customer/daily-limit');
if (response.statusCode == 200) {
log('Response: ${response.data}');
return Limit.fromJson(response.data);
} else {
throw Exception('Failed to load');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
void editLimit( double newLimit) async {
try {
final response = await _dio.post('/api/customer/daily-limit',
data: '{"amount": $newLimit}');
if (response.statusCode == 200) {
log('Response: ${response.data}');
} else {
throw Exception('Failed to load');
}
} on DioException catch (e) {
throw Exception('Network error: ${e.message}');
} catch (e) {
throw Exception('Unexpected error: ${e.toString()}');
}
}
}

View File

@@ -0,0 +1,247 @@
// // ignore_for_file: avoid_print
// import 'dart:io';
// import 'package:flutter/material.dart';
// import 'package:send_message/send_message.dart' show sendSMS;
// import 'package:simcards/sim_card.dart';
// import 'package:simcards/simcards.dart';
// import 'package:uuid/uuid.dart';
// class SmsService {
// final Simcards _simcards = Simcards();
// Future<void> sendVerificationSms({
// required BuildContext context,
// required String destinationNumber,
// required String message,
// }) async {
// try {
// await _simcards.requestPermission();
// bool permissionGranted = await _simcards.hasPermission();
// if (!permissionGranted) {
// print("Permission denied." );
// return;
// }
// List<SimCard> simCardList = await _simcards.getSimCards();
// if (simCardList.isEmpty) {
// print("No SIM detected." );
// return;
// }
// await _sendSms(destinationNumber, message, simCardList.first);
// } catch (e) {
// print("Error in SMS process: $e");
// }
// }
// Future<void> _sendSms(
// String destinationNumber, String message, SimCard selectedSim) async {
// if (Platform.isAndroid) {
// try {
// var uuid = const Uuid();
// String uniqueId = uuid.v4();
// String smsMessage = uniqueId;
// String result = await sendSMS(
// message: smsMessage,
// recipients: [destinationNumber],
// sendDirect: false,
// );
// print("SMS send result: $result. Sent via ${selectedSim.displayName} (Note: OS default SIM isused).");
// } catch (e) {
// print("Error sending SMS: $e");
// }
// } else {
// print("SMS sending is only supported on Android.");
// }
// }
// }
// import 'dart:io';
// import 'package:flutter/material.dart';
// import 'package:permission_handler/permission_handler.dart'; // Import permission_handler
// import 'package:send_message/send_message.dart' show sendSMS;
// import 'package:simcards/sim_card.dart';
// import 'package:simcards/simcards.dart';
// class SmsService {
// final Simcards _simcards = Simcards();
// Future<bool> sendVerificationSms({
// required BuildContext context,
// required String destinationNumber,
// required String message,
// }) async {
// try {
// // --- NEW PERMISSION LOGIC ---
// // 1. Request both Phone and SMS permissions
// Map<Permission, PermissionStatus> statuses = await [
// Permission.phone,
// Permission.sms,
// ].request();
// // 2. Check if both permissions were granted
// if (statuses[Permission.phone]!.isGranted && statuses[Permission.sms]!.isGranted) {
// print("Phone and SMS permissions are granted.");
// } else {
// print("Permission was denied. Phone status: ${statuses[Permission.phone]}, SMS status: ${statuses[Permission.sms]}");
// // Optionally, you can open app settings to let the user grant it manually
// // openAppSettings();
// return false;
// }
// // --- END OF NEW PERMISSION LOGIC ---
// // Check for SIM card (this part remains the same)
// List<SimCard> simCardList = await _simcards.getSimCards();
// if (simCardList.isEmpty) {
// print("No SIM card detected.");
// return false;
// }
// // Try sending the SMS and return the result
// return await _sendSms(destinationNumber, message, simCardList.first);
// } catch (e) {
// print("An error occurred in the SMS process: $e");
// return false;
// }
// }
// 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, // Still attempting direct send as requested
// );
// 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;
// }
// }
// }
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

@@ -4,6 +4,8 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart'; import 'package:kmobile/features/auth/controllers/theme_mode_cubit.dart';
import 'package:kmobile/features/auth/controllers/theme_mode_state.dart'; import 'package:kmobile/features/auth/controllers/theme_mode_state.dart';
import 'package:kmobile/features/auth/screens/login_screen.dart';
//import 'package:kmobile/features/auth/screens/sms_verification_screen.dart';
import 'package:kmobile/security/secure_storage.dart'; import 'package:kmobile/security/secure_storage.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import './l10n/app_localizations.dart'; import './l10n/app_localizations.dart';
@@ -13,13 +15,12 @@ import 'config/routes.dart';
import 'di/injection.dart'; import 'di/injection.dart';
import 'features/auth/controllers/auth_cubit.dart'; import 'features/auth/controllers/auth_cubit.dart';
import 'features/card/screens/card_management_screen.dart'; import 'features/card/screens/card_management_screen.dart';
import 'features/auth/screens/splash_screen.dart';
import 'features/auth/screens/login_screen.dart';
import 'features/service/screens/service_screen.dart'; import 'features/service/screens/service_screen.dart';
import 'features/dashboard/screens/dashboard_screen.dart'; import 'features/dashboard/screens/dashboard_screen.dart';
import 'features/auth/screens/mpin_screen.dart'; import 'features/auth/screens/mpin_screen.dart';
import 'package:local_auth/local_auth.dart'; import 'package:local_auth/local_auth.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'dart:async';
class KMobile extends StatefulWidget { class KMobile extends StatefulWidget {
const KMobile({super.key}); const KMobile({super.key});
@@ -34,25 +35,46 @@ class KMobile extends StatefulWidget {
} }
} }
class _KMobileState extends State<KMobile> { class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
bool showSplash = true; Timer? _backgroundTimer;
Locale? _locale; Locale? _locale;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addObserver(this);
loadPreferences(); loadPreferences();
Future.delayed(const Duration(seconds: 3), () { }
setState(() {
showSplash = false; @override
}); void dispose() {
}); WidgetsBinding.instance.removeObserver(this);
_backgroundTimer?.cancel();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
_backgroundTimer?.cancel();
break;
case AppLifecycleState.paused:
_backgroundTimer = Timer(const Duration(minutes: 2), () {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
});
break;
default:
break;
}
} }
Future<void> loadPreferences() async { Future<void> loadPreferences() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
// Load Locale
final String? langCode = prefs.getString('locale'); final String? langCode = prefs.getString('locale');
if (langCode != null) { if (langCode != null) {
setState(() { setState(() {
@@ -104,8 +126,7 @@ class _KMobileState extends State<KMobile> {
darkTheme: themeState.getDarkThemeData(), darkTheme: themeState.getDarkThemeData(),
themeMode: context.watch<ThemeModeCubit>().state.mode, themeMode: context.watch<ThemeModeCubit>().state.mode,
onGenerateRoute: AppRoutes.generateRoute, onGenerateRoute: AppRoutes.generateRoute,
initialRoute: AppRoutes.splash, home: const AuthGate(),
home: showSplash ? const SplashScreen() : const AuthGate(),
); );
}, },
); );
@@ -117,7 +138,6 @@ class _KMobileState extends State<KMobile> {
class AuthGate extends StatefulWidget { class AuthGate extends StatefulWidget {
const AuthGate({super.key}); const AuthGate({super.key});
@override @override
State<AuthGate> createState() => _AuthGateState(); State<AuthGate> createState() => _AuthGateState();
} }
@@ -178,9 +198,12 @@ class _AuthGateState extends State<AuthGate> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_checking) { if (_checking) {
return const SplashScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
if (_isLoggedIn) { if (_isLoggedIn) {
if (_hasMPin) { if (_hasMPin) {
if (_biometricEnabled) { if (_biometricEnabled) {
@@ -188,13 +211,15 @@ class _AuthGateState extends State<AuthGate> {
future: _tryBiometric(), future: _tryBiometric(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
if (snapshot.data == true) { if (snapshot.data == true) {
return const NavigationScaffold(); // Authenticated return const NavigationScaffold();
} }
return MPinScreen( return MPinScreen(
mode: MPinMode.enter, mode: MPinMode.enter,
onCompleted: (_) { onCompleted: (_) {
@@ -225,7 +250,6 @@ class _AuthGateState extends State<AuthGate> {
onCompleted: (_) async { onCompleted: (_) async {
final storage = getIt<SecureStorage>(); final storage = getIt<SecureStorage>();
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final optIn = await showDialog<bool>( final optIn = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
@@ -246,7 +270,6 @@ class _AuthGateState extends State<AuthGate> {
], ],
), ),
); );
if (optIn == true) { if (optIn == true) {
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
bool didAuth = false; bool didAuth = false;
@@ -254,7 +277,6 @@ class _AuthGateState extends State<AuthGate> {
if (context.mounted) { if (context.mounted) {
authEnable = AppLocalizations.of(context).authenticateToEnable; authEnable = AppLocalizations.of(context).authenticateToEnable;
} }
if (canCheck) { if (canCheck) {
didAuth = await localAuth.authenticate( didAuth = await localAuth.authenticate(
localizedReason: authEnable, localizedReason: authEnable,
@@ -269,7 +291,6 @@ class _AuthGateState extends State<AuthGate> {
await storage.write('biometric_enabled', 'false'); await storage.write('biometric_enabled', 'false');
} }
} }
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pushReplacement( Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
@@ -287,7 +308,6 @@ class _AuthGateState extends State<AuthGate> {
class NavigationScaffold extends StatefulWidget { class NavigationScaffold extends StatefulWidget {
const NavigationScaffold({super.key}); const NavigationScaffold({super.key});
@override @override
State<NavigationScaffold> createState() => _NavigationScaffoldState(); State<NavigationScaffold> createState() => _NavigationScaffoldState();
} }
@@ -295,7 +315,6 @@ class NavigationScaffold extends StatefulWidget {
class _NavigationScaffoldState extends State<NavigationScaffold> { class _NavigationScaffoldState extends State<NavigationScaffold> {
final PageController _pageController = PageController(); final PageController _pageController = PageController();
int _selectedIndex = 0; int _selectedIndex = 0;
final List<Widget> _pages = [ final List<Widget> _pages = [
const DashboardScreen(), const DashboardScreen(),
const CardManagementScreen(), const CardManagementScreen(),
@@ -344,8 +363,7 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
type: BottomNavigationBarType.fixed, type: BottomNavigationBarType.fixed,
backgroundColor: const Color(0XFF1E58AD), backgroundColor: const Color(0XFF1E58AD),
selectedItemColor: Theme.of(context).colorScheme.onPrimary, selectedItemColor: Theme.of(context).colorScheme.onPrimary,
unselectedItemColor: unselectedItemColor: Theme.of(context).colorScheme.onSecondary,
Theme.of(context).colorScheme.onSecondary,
onTap: (index) { onTap: (index) {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
@@ -372,11 +390,9 @@ class _NavigationScaffoldState extends State<NavigationScaffold> {
} }
} }
// Add this widget at the end of the file
class BiometricPromptScreen extends StatelessWidget { class BiometricPromptScreen extends StatelessWidget {
final VoidCallback onCompleted; final VoidCallback onCompleted;
const BiometricPromptScreen({super.key, required this.onCompleted}); const BiometricPromptScreen({super.key, required this.onCompleted});
Future<void> _handleBiometric(BuildContext context) async { Future<void> _handleBiometric(BuildContext context) async {
final localAuth = LocalAuthentication(); final localAuth = LocalAuthentication();
final canCheck = await localAuth.canCheckBiometrics; final canCheck = await localAuth.canCheckBiometrics;
@@ -407,7 +423,11 @@ class BiometricPromptScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Future.microtask(() => _showDialog(context)); Future.microtask(() => _showDialog(context));
return const SplashScreen(); return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
} }
Future<void> _showDialog(BuildContext context) async { Future<void> _showDialog(BuildContext context) async {

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/splash_screen.dart';
import '../app.dart'; import '../app.dart';
import '../features/auth/screens/login_screen.dart'; import '../features/auth/screens/login_screen.dart';
// import '../features/auth/screens/forgot_password_screen.dart'; // import '../features/auth/screens/forgot_password_screen.dart';
@@ -30,8 +29,6 @@ class AppRoutes {
// Route generator // Route generator
static Route<dynamic> generateRoute(RouteSettings settings) { static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) { switch (settings.name) {
case splash:
return MaterialPageRoute(builder: (_) => const SplashScreen());
case login: case login:
return MaterialPageRoute(builder: (_) => const LoginScreen()); return MaterialPageRoute(builder: (_) => const LoginScreen());

15
lib/core/logger.dart Normal file
View File

@@ -0,0 +1,15 @@
import 'package:kmobile/core/toast.dart';
class Logger {
static void info(String message) {
showToast('INFO: $message');
}
static void warning(String message) {
showToast('WARNING: $message');
}
static void error(String message) {
showToast('ERROR: $message');
}
}

14
lib/core/toast.dart Normal file
View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
void showToast(String message) {
Fluttertoast.showToast(
msg: message,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
timeInSecForIosWeb: 1,
backgroundColor: Colors.black,
textColor: Colors.white,
fontSize: 16.0,
);
}

View File

@@ -49,12 +49,10 @@ class AuthRepository {
_tokenExpiryKey, token.expiresAt.toIso8601String()); _tokenExpiryKey, token.expiresAt.toIso8601String());
} }
Future<void> clearAuthTokens() async { Future<void> clearAuthTokens() async {
await _secureStorage.deleteAll(); await _secureStorage.deleteAll();
} }
Future<AuthToken?> _getAuthToken() async { Future<AuthToken?> _getAuthToken() async {
final accessToken = await _secureStorage.read(_accessTokenKey); final accessToken = await _secureStorage.read(_accessTokenKey);
final expiryString = await _secureStorage.read(_tokenExpiryKey); final expiryString = await _secureStorage.read(_tokenExpiryKey);

View File

@@ -1,5 +1,3 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/models/transaction.dart';

View File

@@ -1,3 +1,4 @@
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -46,10 +47,13 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>())); getIt.registerSingleton<PaymentService>(PaymentService(getIt<Dio>()));
getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>())); getIt.registerSingleton<BeneficiaryService>(BeneficiaryService(getIt<Dio>()));
getIt.registerSingleton<LimitService>(LimitService(getIt<Dio>()));
getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>())); getIt.registerSingleton<NeftService>(NeftService(getIt<Dio>()));
getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>())); getIt.registerSingleton<RtgsService>(RtgsService(getIt<Dio>()));
getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>())); getIt.registerSingleton<ImpsService>(ImpsService(getIt<Dio>()));
getIt.registerLazySingleton<ChangePasswordService>(() => ChangePasswordService(getIt<Dio>()),); getIt.registerLazySingleton<ChangePasswordService>(
() => ChangePasswordService(getIt<Dio>()),
);
// Add auth interceptor after repository is available // Add auth interceptor after repository is available
getIt<Dio>().interceptors.add( getIt<Dio>().interceptors.add(
@@ -65,14 +69,15 @@ Dio _createDioClient() {
final dio = Dio( final dio = Dio(
BaseOptions( BaseOptions(
baseUrl: baseUrl:
'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test // 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod //'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
//'https://kccbmbnk.net', 'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60), connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60),
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json', 'Accept': 'application/json',
'X-Login-Type': 'MB',
}, },
), ),
); );

View File

@@ -11,8 +11,8 @@ import 'transaction_details_screen.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart'; import 'package:device_info_plus/device_info_plus.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
class AccountStatementScreen extends StatefulWidget { class AccountStatementScreen extends StatefulWidget {
final String accountNo; final String accountNo;
@@ -295,20 +295,21 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
: '', : '',
style: const TextStyle(fontSize: 12), style: const TextStyle(fontSize: 12),
), ),
trailing: Column( trailing: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"${tx.amount}", "${tx.amount}",
style: const TextStyle(fontSize: 17), style: const TextStyle(fontSize: 17),
), ),
Text( Text(
"Bal: ₹${tx.balance}", "Bal: ₹${tx.balance}",
style: const TextStyle(fontSize: 12), // Style matches tx.name style: const TextStyle(
), fontSize: 12), // Style matches tx.name
], ),
), ],
),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -491,19 +492,17 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
} }
} }
// Add for IOS // Add for IOS
else if (Platform.isIOS) { else if (Platform.isIOS) {
// On iOS, we save to a temporary directory and then open the share sheet. // On iOS, we save to a temporary directory and then open the share sheet.
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final file = await File('${tempDir.path}/$fileName').create(); final file = await File('${tempDir.path}/$fileName').create();
await file.writeAsBytes(pdfBytes); await file.writeAsBytes(pdfBytes);
// Use share_plus to open the iOS share dialog
await Share.shareXFiles(
[XFile(file.path)],
);
}
// Use share_plus to open the iOS share dialog
await Share.shareXFiles(
[XFile(file.path)],
);
}
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@@ -73,8 +73,8 @@ class TransactionDetailsScreen extends StatelessWidget {
// AppLocalizations.of(context).beneficiaryAccountNo, // AppLocalizations.of(context).beneficiaryAccountNo,
// transaction.name.split("A/C ").last ?? "") // transaction.name.split("A/C ").last ?? "")
// ] // ]
_buildDetailRow(AppLocalizations.of(context).details, _buildDetailRow(
transaction.name), AppLocalizations.of(context).details, transaction.name),
], ],
), ),
), ),

View File

@@ -12,6 +12,10 @@ class AuthCubit extends Cubit<AuthState> {
checkAuthStatus(); checkAuthStatus();
} }
void reset() {
emit(AuthInitial());
}
Future<void> checkAuthStatus() async { Future<void> checkAuthStatus() async {
emit(AuthLoading()); emit(AuthLoading());
try { try {
@@ -27,6 +31,10 @@ class AuthCubit extends Cubit<AuthState> {
} }
} }
void startVerification() {
emit(AuthVerificationInProgress());
}
Future<void> refreshUserData() async { Future<void> refreshUserData() async {
try { try {
// emit(AuthLoading()); // emit(AuthLoading());

View File

@@ -10,6 +10,8 @@ class AuthInitial extends AuthState {}
class AuthLoading extends AuthState {} class AuthLoading extends AuthState {}
class AuthVerificationInProgress extends AuthState {}
class Authenticated extends AuthState { class Authenticated extends AuthState {
final List<User> users; final List<User> users;

View File

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

View File

@@ -1,14 +1,8 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart'; import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/features/auth/screens/set_password_screen.dart'; import 'package:kmobile/l10n/app_localizations.dart';
import 'package:kmobile/security/secure_storage.dart';
import '../../../app.dart';
import '../controllers/auth_cubit.dart';
import '../controllers/auth_state.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); const LoginScreen({super.key});
@@ -23,7 +17,12 @@ class LoginScreenState extends State<LoginScreen>
final _customerNumberController = TextEditingController(); final _customerNumberController = TextEditingController();
final _passwordController = TextEditingController(); final _passwordController = TextEditingController();
bool _obscurePassword = true; bool _obscurePassword = true;
//bool _showWelcome = true;
@override
void initState() {
super.initState();
context.read<AuthCubit>().reset();
}
@override @override
void dispose() { void dispose() {
@@ -34,10 +33,14 @@ class LoginScreenState extends State<LoginScreen>
void _submitForm() { void _submitForm() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
context.read<AuthCubit>().login( Navigator.of(context).push(
_customerNumberController.text.trim(), MaterialPageRoute(
_passwordController.text, builder: (_) => VerificationScreen(
); customerNo: _customerNumberController.text.trim(),
password: _passwordController.text,
),
),
);
} }
} }
@@ -45,217 +48,141 @@ class LoginScreenState extends State<LoginScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
// appBar: AppBar(title: const Text('Login')), // appBar: AppBar(title: const Text('Login')),
body: BlocConsumer<AuthCubit, AuthState>( body: Padding(
listener: (context, state) async { padding: const EdgeInsets.all(24.0),
if (state is Authenticated) { child: Form(
final storage = getIt<SecureStorage>(); key: _formKey,
final mpin = await storage.read('mpin'); child: Column(
if (!context.mounted) return; mainAxisAlignment: MainAxisAlignment.center,
if (mpin == null) { children: [
Navigator.of(context).pushReplacement( Image.asset(
MaterialPageRoute( 'assets/images/logo.png',
builder: (_) => MPinScreen( width: 150,
mode: MPinMode.set, height: 150,
onCompleted: (_) { errorBuilder: (context, error, stackTrace) {
Navigator.of( return Icon(
context, Icons.account_balance,
rootNavigator: true, size: 100,
).pushReplacement( color: Theme.of(context).primaryColor,
MaterialPageRoute( );
builder: (_) => const NavigationScaffold(), },
), ),
); const SizedBox(height: 16),
// Title
Text(
AppLocalizations.of(context).kccb,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(height: 48),
TextFormField(
controller: _customerNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).customerNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterUsername;
}
return null;
},
),
const SizedBox(height: 24),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
}, },
), ),
), ),
); validator: (value) {
} else { if (value == null || value.isEmpty) {
Navigator.of(context).pushReplacement( return AppLocalizations.of(context).pleaseEnterPassword;
MaterialPageRoute(builder: (_) => const NavigationScaffold()), }
); return null;
} },
} else if (state is AuthError) {
if (state.message == 'MIGRATED_USER_HAS_NO_PASSWORD') {
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => SetPasswordScreen(
customerNo: _customerNumberController.text.trim(),
)));
} else {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
}
}
},
builder: (context, state) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
'assets/images/logo.png',
width: 150,
height: 150,
errorBuilder: (context, error, stackTrace) {
return Icon(
Icons.account_balance,
size: 100,
color: Theme.of(context).primaryColor,
);
},
),
const SizedBox(height: 16),
// Title
Text(
AppLocalizations.of(context).kccb,
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
const SizedBox(height: 48),
TextFormField(
controller: _customerNumberController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).customerNumber,
// prefixIcon: Icon(Icons.person),
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
),
keyboardType: TextInputType.number,
textInputAction: TextInputAction.next,
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterUsername;
}
return null;
},
),
const SizedBox(height: 24),
// Password
TextFormField(
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _submitForm(),
decoration: InputDecoration(
labelText: AppLocalizations.of(context).password,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2),
),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility
: Icons.visibility_off,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return AppLocalizations.of(context).pleaseEnterPassword;
}
return null;
},
),
const SizedBox(height: 24),
//Login Button
SizedBox(
width: 250,
child: ElevatedButton(
onPressed: state is AuthLoading ? null : _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorDark,
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1),
elevation: 0,
),
child: state is AuthLoading
? const CircularProgressIndicator()
: Text(
AppLocalizations.of(context).login,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
),
const SizedBox(height: 15),
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 16),
// child: Row(
// children: [
// const Expanded(child: Divider()),
// Padding(
// padding: const EdgeInsets.symmetric(horizontal: 8),
// child: Text(AppLocalizations.of(context).or),
// ),
// //const Expanded(child: Divider()),
// ],
// ),
// ),
const SizedBox(height: 25),
// Register Button
// SizedBox(
// width: 250,
// child: ElevatedButton(
// //disable until registration is implemented
// onPressed: null,
// style: OutlinedButton.styleFrom(
// shape: const StadiumBorder(),
// padding: const EdgeInsets.symmetric(vertical: 16),
// backgroundColor: Theme.of(context).colorScheme.primary,
// foregroundColor: Theme.of(context).colorScheme.onPrimary,
// ),
// child: Text(AppLocalizations.of(context).register,
// style: TextStyle(color: Theme.of(context).colorScheme.onPrimary),),
// ),
// ),
],
), ),
), const SizedBox(height: 24),
); //Login Button
}, SizedBox(
width: 250,
child: ElevatedButton(
onPressed: _submitForm,
style: ElevatedButton.styleFrom(
shape: const StadiumBorder(),
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor:
Theme.of(context).scaffoldBackgroundColor,
foregroundColor: Theme.of(context).primaryColorDark,
side: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1),
elevation: 0,
),
child: Text(
AppLocalizations.of(context).login,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
),
),
),
const SizedBox(height: 15),
const SizedBox(height: 25),
],
),
),
), ),
); );
} }
} }

View File

@@ -140,9 +140,10 @@ class _SetPasswordScreenState extends State<SetPasswordScreen> {
if (_error != null) ...[ if (_error != null) ...[
Text( Text(
_error!, _error!,
style: const TextStyle(color: Colors.red, style: const TextStyle(
fontWeight: FontWeight.bold, color: Colors.red,
fontSize: 20), fontWeight: FontWeight.bold,
fontSize: 20),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),

View File

@@ -0,0 +1,131 @@
// lib/features/auth/screens/sms_verification_helper.dart
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> _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(),
),
TextButton(
child: const Text("Open Settings"),
onPressed: () {
openAppSettings();
Navigator.of(context).pop();
},
),
],
),
);
}
void _showSnackBar(BuildContext context, String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
),
);
}
Future<void> initiateSmsSequence({
required BuildContext context,
}) async {
bool hasPermission = false;
// --- PERMISSION LOOP ---
while (!hasPermission) {
final status = await _smsService.handleSmsPermission();
switch (status) {
case PermissionStatusResult.granted:
_showSnackBar(context, "Permissions Granted! Proceeding...");
hasPermission = true; // This will break the loop
break;
case PermissionStatusResult.denied:
_showSnackBar(context, "SMS and Phone permissions are required. Please try again.");
await Future.delayed(const Duration(seconds: 3));
break;
case PermissionStatusResult.permanentlyDenied:
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();
},
),
],
),
);
// Wait for user to return from settings
await Future.delayed(const Duration(seconds: 5));
break;
case PermissionStatusResult.restricted:
await _showRestrictedSmsDialog(context);
// Wait for user to return from settings
await Future.delayed(const Duration(seconds: 10));
break;
}
}
// --- SMS SENDING LOOP ---
bool isSmsSent = false;
while (!isSmsSent) {
var uuid = const Uuid();
String uniqueId = uuid.v4();
String smsMessage = uniqueId;
_showSnackBar(context, "Attempting to send verification SMS...");
isSmsSent = await _smsService.sendVerificationSms(
context: context,
destinationNumber: '9580079717', // Replace with your number
message: smsMessage,
);
if (isSmsSent) {
_showSnackBar(context, "SMS sent successfully! Proceeding to login.");
break;
} else {
_showSnackBar(context, "SMS failed to send. Retrying in 5 seconds...");
await Future.delayed(const Duration(seconds: 5));
}
}
}
}

View File

@@ -1,62 +0,0 @@
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
Positioned.fill(
child: Image.asset(
'assets/images/kconnect2.webp',
fit: BoxFit.cover,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
AppLocalizations.of(context).kccbMobile,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Color(0xFFFFFFFF),
),
),
const SizedBox(height: 12),
Text(
AppLocalizations.of(context).kccBankFull,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 18,
color: Color(0xFFFFFFFF),
letterSpacing: 1.2,
),
),
],
),
),
const Positioned(
bottom: 40,
left: 0,
right: 0,
child: Center(
child: CircularProgressIndicator(
color: Color(0xFFFFFFFF)),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,146 @@
// lib/features/auth/screens/verification_screen.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
import 'package:kmobile/features/auth/controllers/auth_state.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
import '../../../app.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> {
final SmsVerificationHelper _smsVerificationHelper = SmsVerificationHelper();
late Timer _timer;
int _start = 120;
String _message = "Attempting verification...";
String? _error;
@override
void initState() {
super.initState();
context.read<AuthCubit>().startVerification();
startTimer();
_verifySmsAndLogin();
}
void startTimer() {
const oneSec = Duration(seconds: 1);
_timer = Timer.periodic(
oneSec,
(Timer timer) {
if (_start == 0) {
timer.cancel();
if (mounted) {
setState(() {
_error = "Verification timed out.";
});
}
} else {
setState(() {
_start--;
});
}
},
);
}
Future<void> _verifySmsAndLogin() async {
await _smsVerificationHelper.initiateSmsSequence(context: context);
// After SMS sequence completes, proceed with login
_timer.cancel(); // Stop the timer
if (mounted) {
context.read<AuthCubit>().login(widget.customerNo, widget.password);
}
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocListener<AuthCubit, AuthState>(
listenWhen: (previous, current) {
return current is! AuthVerificationInProgress && current is! AuthInitial;
},
listener: (context, state) {
if (state is Authenticated) {
_timer.cancel();
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (_) => MPinScreen(
mode: MPinMode.set,
onCompleted: (_) {
Navigator.of(
context,
rootNavigator: true,
).pushReplacement(
MaterialPageRoute(
builder: (_) => const NavigationScaffold(),
),
);
},
),
),
);
} else if (state is AuthError) {
_timer.cancel();
setState(() {
_error = state.message;
});
}
},
child: Center(
child: _error != null
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 80),
const SizedBox(height: 16),
Text(
_error!,
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red, fontSize: 18),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text("Back to Login"),
)
],
)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_message),
const SizedBox(height: 16),
Text("Time remaining: $_start seconds"),
],
),
),
),
);
}
}

View File

@@ -24,8 +24,8 @@ class AddBeneficiaryScreen extends StatefulWidget {
class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> { class _AddBeneficiaryScreen extends State<AddBeneficiaryScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _accountNumberFieldKey = GlobalKey<FormFieldState>(); final _accountNumberFieldKey = GlobalKey<FormFieldState>();
final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>(); final _confirmAccountNumberFieldKey = GlobalKey<FormFieldState>();
final _ifscFieldKey = GlobalKey<FormFieldState>(); final _ifscFieldKey = GlobalKey<FormFieldState>();
final TextEditingController accountNumberController = TextEditingController(); final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController = final TextEditingController confirmAccountNumberController =
TextEditingController(); TextEditingController();
@@ -47,10 +47,10 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
void initState() { void initState() {
super.initState(); super.initState();
_ifscFocusNode.addListener(() { _ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) { if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC(); _validateIFSC();
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
accountType = 'Savings'; accountType = 'Savings';
@@ -58,25 +58,25 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
}); });
} }
@override @override
void dispose() { void dispose() {
accountNumberController.dispose(); accountNumberController.dispose();
confirmAccountNumberController.dispose(); confirmAccountNumberController.dispose();
nameController.dispose(); nameController.dispose();
bankNameController.dispose(); bankNameController.dispose();
branchNameController.dispose(); branchNameController.dispose();
ifscController.dispose(); ifscController.dispose();
phoneController.dispose(); phoneController.dispose();
_ifscFocusNode.dispose(); _ifscFocusNode.dispose();
super.dispose(); super.dispose();
} }
void _validateIFSC() async { void _validateIFSC() async {
var beneficiaryService = getIt<BeneficiaryService>(); var beneficiaryService = getIt<BeneficiaryService>();
final ifsc = ifscController.text.trim().toUpperCase(); final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return; if (ifsc.isEmpty) return;
try { try {
final result = await beneficiaryService.validateIFSC(ifsc); final result = await beneficiaryService.validateIFSC(ifsc);
if (mounted) { if (mounted) {
@@ -94,7 +94,8 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
final errorMessage = e.toString().toUpperCase(); final errorMessage = e.toString().toUpperCase();
String snackbarMessage = AppLocalizations.of(context).somethingWentWrong; String snackbarMessage =
AppLocalizations.of(context).somethingWentWrong;
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) { if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
snackbarMessage = AppLocalizations.of(context).invalidIfsc; snackbarMessage = AppLocalizations.of(context).invalidIfsc;
@@ -107,7 +108,7 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
branchNameController.clear(); branchNameController.clear();
} }
} }
} }
void _validateBeneficiary() async { void _validateBeneficiary() async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@@ -325,7 +326,9 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
).reenterAccountNumber; ).reenterAccountNumber;
} }
if (value != accountNumberController.text) { if (value != accountNumberController.text) {
return AppLocalizations.of(context,).accountMismatch; return AppLocalizations.of(
context,
).accountMismatch;
} }
return null; return null;
}, },
@@ -346,16 +349,16 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
), ),
textCapitalization: TextCapitalization.characters, textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
final trimmed = value.trim().toUpperCase(); final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) { if (trimmed.length < 11) {
// clear bank/branch if backspace or changed // clear bank/branch if backspace or changed
bankNameController.clear(); bankNameController.clear();
branchNameController.clear(); branchNameController.clear();
} }
}); });
}, },
validator: (value) { validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$'); final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
@@ -429,20 +432,27 @@ final _ifscFieldKey = GlobalKey<FormFieldState>();
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isValidating || ifscController.text.length != 11 onPressed: _isValidating ||
? null ifscController.text.length != 11
: () { ? null
final isAccountValid = : () {
_accountNumberFieldKey.currentState!.validate(); final isAccountValid =
final isConfirmAccountValid = _accountNumberFieldKey.currentState!
_confirmAccountNumberFieldKey.currentState!.validate(); .validate();
final isIfscValid = _ifscFieldKey.currentState!.validate(); final isConfirmAccountValid =
_confirmAccountNumberFieldKey
if (isAccountValid && isConfirmAccountValid && isIfscValid) { .currentState!
_validateBeneficiary(); .validate();
} final isIfscValid = _ifscFieldKey
}, .currentState!
.validate();
if (isAccountValid &&
isConfirmAccountValid &&
isIfscValid) {
_validateBeneficiary();
}
},
child: _isValidating child: _isValidating
? const SizedBox( ? const SizedBox(
width: 20, width: 20,

View File

@@ -90,7 +90,10 @@ class _BeneficiaryResultPageState extends State<BeneficiaryResultPage> {
), ),
child: Text( child: Text(
AppLocalizations.of(context).done, AppLocalizations.of(context).done,
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer), // slightly bigger text style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer), // slightly bigger text
), ),
), ),
), ),

View File

@@ -17,69 +17,69 @@ class CardManagementScreen extends StatefulWidget {
class _CardManagementScreen extends State<CardManagementScreen> { class _CardManagementScreen extends State<CardManagementScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Text( title: Text(
AppLocalizations.of(context).cardManagement, AppLocalizations.of(context).cardManagement,
),
centerTitle: false,
),
body: ListView(
children: [
CardManagementTile(
icon: Symbols.add,
label: AppLocalizations.of(context).applyDebitCard,
onTap: () {},
disabled: true, // Add this
), ),
const Divider(height: 1), centerTitle: false,
CardManagementTile( ),
icon: Symbols.remove_moderator, body: ListView(
label: AppLocalizations.of(context).blockUnblockCard, children: [
onTap: () { CardManagementTile(
Navigator.push( icon: Symbols.add,
label: AppLocalizations.of(context).applyDebitCard,
onTap: () {},
disabled: true, // Add this
),
const Divider(height: 1),
CardManagementTile(
icon: Symbols.remove_moderator,
label: AppLocalizations.of(context).blockUnblockCard,
onTap: () {
Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const BlockCardScreen(), builder: (context) => const BlockCardScreen(),
), ),
); );
}, },
disabled: true, disabled: true,
), ),
const Divider(height: 1), const Divider(height: 1),
CardManagementTile( CardManagementTile(
icon: Symbols.password_2, icon: Symbols.password_2,
label: AppLocalizations.of(context).changeCardPin, label: AppLocalizations.of(context).changeCardPin,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const CardPinChangeDetailsScreen(), builder: (context) => const CardPinChangeDetailsScreen(),
), ),
); );
}, },
disabled: true, disabled: true,
), ),
const Divider(height: 1), const Divider(height: 1),
CardManagementTile( CardManagementTile(
icon: Symbols.payment_card, icon: Symbols.payment_card,
label: AppLocalizations.of(context).viewCardDeatils, label: AppLocalizations.of(context).viewCardDeatils,
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const CardDetailsScreen(), builder: (context) => const CardDetailsScreen(),
), ),
); );
}, },
disabled: true, disabled: true,
), ),
const Divider(height: 1), const Divider(height: 1),
], ],
), ),
); );
} }
} }
class CardManagementTile extends StatelessWidget { class CardManagementTile extends StatelessWidget {

View File

@@ -14,13 +14,13 @@ class CustomerInfoScreen extends StatefulWidget {
class _CustomerInfoScreenState extends State<CustomerInfoScreen> { class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
late final User user = widget.user; late final User user = widget.user;
String _maskPrimaryId(String? primaryId) { String _maskPrimaryId(String? primaryId) {
if (primaryId == null || primaryId.length <= 4) { if (primaryId == null || primaryId.length <= 4) {
return primaryId ?? 'N/A'; return primaryId ?? 'N/A';
} }
final lastFour = primaryId.substring(primaryId.length - 4); final lastFour = primaryId.substring(primaryId.length - 4);
return '*' * (primaryId.length - 4) + lastFour; return '*' * (primaryId.length - 4) + lastFour;
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -92,10 +92,10 @@ class _CustomerInfoScreenState extends State<CustomerInfoScreen> {
label: AppLocalizations.of(context).branchAddress, label: AppLocalizations.of(context).branchAddress,
value: user.address ?? 'N/A', value: user.address ?? 'N/A',
), // Replace with Aadhar if available ), // Replace with Aadhar if available
InfoField( InfoField(
label: AppLocalizations.of(context).primaryId, label: AppLocalizations.of(context).primaryId,
value: _maskPrimaryId(user.primaryId), value: _maskPrimaryId(user.primaryId),
), // Replace with PAN if available ), // Replace with PAN if available
], ],
), ),
), ),

View File

@@ -30,7 +30,8 @@ class DashboardScreen extends StatefulWidget {
State<DashboardScreen> createState() => _DashboardScreenState(); State<DashboardScreen> createState() => _DashboardScreenState();
} }
class _DashboardScreenState extends State<DashboardScreen> { class _DashboardScreenState extends State<DashboardScreen>
with SingleTickerProviderStateMixin {
int selectedAccountIndex = 0; int selectedAccountIndex = 0;
bool isVisible = false; bool isVisible = false;
bool isRefreshing = false; bool isRefreshing = false;
@@ -211,7 +212,42 @@ class _DashboardScreenState extends State<DashboardScreen> {
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
backgroundColor: theme.scaffoldBackgroundColor, backgroundColor: theme.scaffoldBackgroundColor,
automaticallyImplyLeading: false, 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: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
),
title: Text( title: Text(
AppLocalizations.of(context).kccbMobile, AppLocalizations.of(context).kccbMobile,
textAlign: TextAlign.left, textAlign: TextAlign.left,
@@ -221,41 +257,6 @@ class _DashboardScreenState extends State<DashboardScreen> {
), ),
), ),
centerTitle: true, centerTitle: true,
actions: [
Padding(
padding: const EdgeInsets.only(right: 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: CircleAvatar(
backgroundColor: Colors.grey[200],
radius: 20,
child: SvgPicture.asset(
'assets/images/avatar_male.svg',
width: 40,
height: 40,
fit: BoxFit.cover,
),
),
),
),
],
), ),
body: BlocBuilder<AuthCubit, AuthState>( body: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) { builder: (context, state) {
@@ -533,8 +534,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
.accountNo!, .accountNo!,
balance: users[selectedAccountIndex] balance: users[selectedAccountIndex]
.availableBalance!, .availableBalance!,
accountType: users[selectedAccountIndex] accountType:
.accountType!, users[selectedAccountIndex]
.accountType!,
))); )));
}), }),
_buildQuickLink(Symbols.checkbook, _buildQuickLink(Symbols.checkbook,

View File

@@ -72,4 +72,3 @@ class AccountCard extends StatelessWidget {
); );
} }
} }

View File

@@ -30,15 +30,15 @@ class _EnquiryScreen extends State<EnquiryScreen> {
} }
} }
Future<void> _launchUrl(String url) async { Future<void> _launchUrl(String url) async {
final Uri uri = Uri.parse(url); final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) { if (await canLaunchUrl(uri)) {
await launchUrl(uri); await launchUrl(uri);
} else { } else {
// Consider adding a 'urlLaunchError' key to your AppLocalizations // Consider adding a 'urlLaunchError' key to your AppLocalizations
debugPrint('Could not launch $url'); debugPrint('Could not launch $url');
} }
} }
Widget _buildContactItem(String role, String email, String phone) { Widget _buildContactItem(String role, String email, String phone) {
return Column( return Column(
@@ -75,29 +75,25 @@ class _EnquiryScreen extends State<EnquiryScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 20), const SizedBox(height: 20),
GestureDetector( GestureDetector(
onTap: () => _launchUrl("https://kccb.in/complaint-form"), onTap: () => _launchUrl("https://kccb.in/complaint-form"),
child: Row( child: Row(mainAxisSize: MainAxisSize.min, children: [
mainAxisSize: MainAxisSize.min, Text(
children: [ "Complaint Form",
Text("Complaint Form", style: TextStyle(
style: TextStyle( fontSize: 17,
fontSize: 17, color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary, decorationColor: Theme.of(context).colorScheme.primary,
decorationColor: Theme.of(context).colorScheme.primary, ),
), ),
), const SizedBox(width: 4),
const SizedBox(width: 4), Icon(
Icon( Icons.open_in_new,
Icons.open_in_new, color: Theme.of(context).colorScheme.primary,
color: Theme.of(context).colorScheme.primary, size: 16.0,
size: 16.0, ),
), ])),
]
)
),
const SizedBox(height: 40), const SizedBox(height: 40),
Text( Text(
AppLocalizations.of(context).keyContacts, AppLocalizations.of(context).keyContacts,

View File

@@ -2,6 +2,8 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
@@ -40,13 +42,67 @@ class FundTransferAmountScreen extends StatefulWidget {
} }
class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> { class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
bool _isAmountOverLimit = false;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final _amountController = TextEditingController(); final _amountController = TextEditingController();
final _remarksController = TextEditingController(); final _remarksController = TextEditingController();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
TransactionMode _selectedMode = TransactionMode.neft; TransactionMode _selectedMode = TransactionMode.neft;
@override
void initState() {
super.initState();
_loadLimit(); // Call the new method
_amountController.addListener(_checkAmountLimit);
}
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(_amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
@override @override
void dispose() { void dispose() {
_amountController.removeListener(_checkAmountLimit);
_amountController.dispose(); _amountController.dispose();
_remarksController.dispose(); _remarksController.dispose();
super.dispose(); super.dispose();
@@ -402,14 +458,14 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
//Remarks //Remarks
TextFormField( TextFormField(
controller: _remarksController, controller: _remarksController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks, labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
),
), ),
const SizedBox(height: 24), ),
const SizedBox(height: 24),
// Amount // Amount
TextFormField( TextFormField(
controller: _amountController, controller: _amountController,
@@ -430,19 +486,27 @@ class _FundTransferAmountScreenState extends State<FundTransferAmountScreen> {
return null; return null;
}, },
), ),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(), const Spacer(),
// Proceed Button // Proceed Button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _onProceed, onPressed: _isAmountOverLimit ? null : _onProceed,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
), ),
child: Text(AppLocalizations.of(context).proceed), child: Text(AppLocalizations.of(context).proceed),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
], ],
), ),

View File

@@ -213,7 +213,8 @@ class _PaymentAnimationScreenState extends State<PaymentAnimationScreen> {
), ),
label: Text( label: Text(
AppLocalizations.of(context).share, AppLocalizations.of(context).share,
style: TextStyle(color: Theme.of(context).colorScheme.primary), style: TextStyle(
color: Theme.of(context).colorScheme.primary),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(

View File

@@ -18,8 +18,9 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
6, 6,
(_) => TextEditingController(), (_) => TextEditingController(),
); );
bool _isLoading = false; bool _isLoading = false;
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>(); final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
@override @override
void dispose() { void dispose() {
@@ -33,7 +34,7 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
} }
void _onOtpChanged(int idx, String value) { void _onOtpChanged(int idx, String value) {
if (value.length == 1 && idx <5) { if (value.length == 1 && idx < 5) {
_focusNodes[idx + 1].requestFocus(); _focusNodes[idx + 1].requestFocus();
} }
if (value.isEmpty && idx > 0) { if (value.isEmpty && idx > 0) {
@@ -44,160 +45,160 @@ class _TpinOtpScreenState extends State<TpinOtpScreen> {
String get _enteredOtp => _controllers.map((c) => c.text).join(); String get _enteredOtp => _controllers.map((c) => c.text).join();
void _verifyOtp() async { void _verifyOtp() async {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
}); });
try { try {
await _changePasswordService.validateOtp( await _changePasswordService.validateOtp(
otp: _enteredOtp, otp: _enteredOtp,
mobileNumber: widget.mobileNumber, mobileNumber: widget.mobileNumber,
); );
if (mounted) { if (mounted) {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
MaterialPageRoute(builder: (_) => const TpinSetScreen()), MaterialPageRoute(builder: (_) => const TpinSetScreen()),
); );
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)), SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
); );
} }
} finally { } finally {
if (mounted) { if (mounted) {
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
}
} }
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).enterOtp), title: Text(AppLocalizations.of(context).enterOtp),
centerTitle: true, centerTitle: true,
elevation: 0, elevation: 0,
), ),
body: Center( body: Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
Icon( Icon(
Icons.lock_outline, Icons.lock_outline,
size: 48, size: 48,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
AppLocalizations.of(context).otpVerification, AppLocalizations.of(context).otpVerification,
style: theme.textTheme.titleLarge?.copyWith( style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
AppLocalizations.of(context).otpSentMessage, AppLocalizations.of(context).otpSentMessage,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey[700], color: Colors.grey[700],
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(6, (i) { children: List.generate(6, (i) {
return Container( return Container(
width: 32, width: 32,
margin: const EdgeInsets.symmetric(horizontal: 8), margin: const EdgeInsets.symmetric(horizontal: 8),
child: TextField( child: TextField(
controller: _controllers[i], controller: _controllers[i],
focusNode: _focusNodes[i], focusNode: _focusNodes[i],
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLength: 1, maxLength: 1,
obscureText: true, obscureText: true,
obscuringCharacter: '*', obscuringCharacter: '*',
decoration: InputDecoration( decoration: InputDecoration(
counterText: '', counterText: '',
filled: true, filled: true,
fillColor: Theme.of(context).primaryColorLight, fillColor: Theme.of(context).primaryColorLight,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide( borderSide: BorderSide(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
width: 2, width: 2,
), ),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide( borderSide: BorderSide(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
width: 2.5, width: 2.5,
), ),
), ),
), ),
onChanged: (val) => _onOtpChanged(i, val), onChanged: (val) => _onOtpChanged(i, val),
), ),
); );
}), }),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
icon: _isLoading icon: _isLoading
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2, strokeWidth: 2,
), ),
) )
: const Icon(Icons.verified_user_rounded), : const Icon(Icons.verified_user_rounded),
label: Text( label: Text(
AppLocalizations.of(context).verifyOtp, AppLocalizations.of(context).verifyOtp,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600), fontSize: 18, fontWeight: FontWeight.w600),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.onPrimary, backgroundColor: theme.colorScheme.onPrimary,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 14, vertical: 14,
horizontal: 28, horizontal: 28,
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
), ),
// Update onPressed to handle loading state // Update onPressed to handle loading state
onPressed: (_enteredOtp.length == 6 && !_isLoading) onPressed: (_enteredOtp.length == 6 && !_isLoading)
? _verifyOtp ? _verifyOtp
: null, : null,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton( TextButton(
onPressed: () { onPressed: () {
// You can also add a getOtp call here for resending // You can also add a getOtp call here for resending
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(AppLocalizations.of(context).otpResent), content: Text(AppLocalizations.of(context).otpResent),
), ),
); );
}, },
child: Text(AppLocalizations.of(context).resendOtp), child: Text(AppLocalizations.of(context).resendOtp),
), ),
const SizedBox(height: 60), const SizedBox(height: 60),
], ],
), ),
), ),
), ),
); );
} }
} }

View File

@@ -15,44 +15,50 @@ class TpinSetupPromptScreen extends StatefulWidget {
State<TpinSetupPromptScreen> createState() => _TpinSetupPromptScreenState(); State<TpinSetupPromptScreen> createState() => _TpinSetupPromptScreenState();
} }
class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> { class _TpinSetupPromptScreenState extends State<TpinSetupPromptScreen> {
int selectedAccountIndex = 0; int selectedAccountIndex = 0;
bool _isLoading = false; bool _isLoading = false;
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>(); final ChangePasswordService _changePasswordService =
Future<void> _getOtp() async { getIt<ChangePasswordService>();
setState(() { Future<void> _getOtp() async {
_isLoading = true; setState(() {
}); _isLoading = true;
});
try { try {
final authState = context.read<AuthCubit>().state; final authState = context.read<AuthCubit>().state;
String mobileNumberToPass = ''; String mobileNumberToPass = '';
if (authState is Authenticated) { if (authState is Authenticated) {
if (selectedAccountIndex >= 0 && selectedAccountIndex < authState.users.length) { if (selectedAccountIndex >= 0 &&
mobileNumberToPass = authState.users[selectedAccountIndex].mobileNo ?? ''; selectedAccountIndex < authState.users.length) {
} mobileNumberToPass =
} authState.users[selectedAccountIndex].mobileNo ?? '';
await _changePasswordService.getOtpTpin(mobileNumber: mobileNumberToPass); }
if (mounted) { }
Navigator.pushReplacement( await _changePasswordService.getOtpTpin(mobileNumber: mobileNumberToPass);
context, if (mounted) {
MaterialPageRoute(builder: (_) => TpinOtpScreen(mobileNumber: mobileNumberToPass,)), Navigator.pushReplacement(
); context,
} MaterialPageRoute(
} catch (e) { builder: (_) => TpinOtpScreen(
if (mounted) { mobileNumber: mobileNumberToPass,
ScaffoldMessenger.of(context).showSnackBar( )),
SnackBar(content: Text('Error: ${e.toString()}')), );
); }
} } catch (e) {
} finally { if (mounted) {
if (mounted) { ScaffoldMessenger.of(context).showSnackBar(
setState(() { SnackBar(content: Text('Error: ${e.toString()}')),
_isLoading = false; );
}); }
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
} }
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -86,33 +92,35 @@ class TpinSetupPromptScreen extends StatefulWidget {
), ),
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
ElevatedButton.icon( ElevatedButton.icon(
icon: _isLoading icon: _isLoading
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
height: 20, height: 20,
child: CircularProgressIndicator( child: CircularProgressIndicator(
color: Colors.white, color: Colors.white,
strokeWidth: 2, strokeWidth: 2,
), ),
) )
: const Icon(Icons.arrow_forward_rounded), : const Icon(Icons.arrow_forward_rounded),
label: Text( label: Text(
AppLocalizations.of(context).setTpin, AppLocalizations.of(context).setTpin,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), style:
), const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
style: ElevatedButton.styleFrom( ),
backgroundColor: theme.colorScheme.onPrimary, style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric( backgroundColor: theme.colorScheme.onPrimary,
vertical: 14, padding: const EdgeInsets.symmetric(
horizontal: 32, vertical: 14,
), horizontal: 32,
shape: RoundedRectangleBorder( ),
borderRadius: BorderRadius.circular(10), shape: RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(10),
), ),
onPressed: _isLoading ? null : _getOtp, // <-- Use the new function ),
), onPressed:
_isLoading ? null : _getOtp, // <-- Use the new function
),
const SizedBox(height: 18), const SizedBox(height: 18),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0), padding: const EdgeInsets.symmetric(horizontal: 18.0),

View File

@@ -212,4 +212,3 @@ class _TransactionPinScreenState extends State<TransactionPinScreen>
); );
} }
} }

View File

@@ -67,13 +67,23 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
"On $transactionDate", "On $transactionDate",
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6)), style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.6)),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
"${AppLocalizations.of(context).toAccountNumber}: $creditAccount", "${AppLocalizations.of(context).toAccountNumber}: $creditAccount",
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8)), style: TextStyle(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(0.8)),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
], ],
@@ -91,8 +101,12 @@ class _TransactionSuccessScreen extends State<TransactionSuccessScreen> {
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: _shareScreenshot, onPressed: _shareScreenshot,
icon: const Icon(Icons.share, size: 18), icon: const Icon(Icons.share, size: 18),
label: Text(AppLocalizations.of(context).share, label: Text(
style: TextStyle(color: Theme.of(context).colorScheme.onPrimaryContainer), AppLocalizations.of(context).share,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer),
), ),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: const StadiumBorder(), shape: const StadiumBorder(),

View File

@@ -19,7 +19,8 @@ class ChangePasswordOTPScreen extends StatefulWidget {
}); });
@override @override
State<ChangePasswordOTPScreen> createState() => _ChangePasswordOTPScreenState(); State<ChangePasswordOTPScreen> createState() =>
_ChangePasswordOTPScreenState();
} }
class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> { class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
@@ -36,33 +37,33 @@ class _ChangePasswordOTPScreenState extends State<ChangePasswordOTPScreen> {
}); });
}); });
} }
final changePasswordService = getIt<ChangePasswordService>(); final changePasswordService = getIt<ChangePasswordService>();
Future<void> _validateOTP() async { Future<void> _validateOTP() async {
try { try {
await changePasswordService.validateOtp( await changePasswordService.validateOtp(
otp: otpController.text, otp: otpController.text,
mobileNumber: widget.mobileNumber, mobileNumber: widget.mobileNumber,
); );
// If OTP is valid, then change the password // If OTP is valid, then change the password
await changePasswordService.validateChangePwd( await changePasswordService.validateChangePwd(
OldLPsw: widget.currentPassword, OldLPsw: widget.currentPassword,
newLPsw: widget.newPassword, newLPsw: widget.newPassword,
confirmLPsw: widget.confirmPassword, confirmLPsw: widget.confirmPassword,
); );
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(AppLocalizations.of(context).pwdchangeSuccess)), SnackBar(content: Text(AppLocalizations.of(context).pwdchangeSuccess)),
); );
// Navigate back to profile or login // Navigate back to profile or login
Navigator.of(context).popUntil((route) => route.isFirst); Navigator.of(context).popUntil((route) => route.isFirst);
} catch (e) {
} catch (e) { ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)),
SnackBar(content: Text(AppLocalizations.of(context).invalidOtp)), );
); }
}
} }
@override @override

View File

@@ -56,38 +56,40 @@ class _ChangePasswordScreenState extends State<ChangePasswordScreen> {
} }
return null; return null;
} }
final ChangePasswordService _changePasswordService = getIt<ChangePasswordService>();
void _proceed() async {
if (_formKey.currentState!.validate()) {
final ChangePasswordService _changePasswordService =
getIt<ChangePasswordService>();
void _proceed() async {
if (_formKey.currentState!.validate()) {
try {
await _changePasswordService.getOtp(mobileNumber: widget.mobileNumber);
try { Navigator.push(
await _changePasswordService.getOtp(mobileNumber: widget.mobileNumber); context,
MaterialPageRoute(
builder: (context) => ChangePasswordOTPScreen(
Navigator.push( currentPassword: currentPasswordController.text,
context, newPassword: newPasswordController.text,
MaterialPageRoute( confirmPassword: confirmPasswordController.text,
builder: (context) => ChangePasswordOTPScreen( mobileNumber: widget.mobileNumber,
currentPassword: currentPasswordController.text, ),
newPassword: newPasswordController.text,
confirmPassword: confirmPasswordController.text,
mobileNumber: widget.mobileNumber,
), ),
), );
); } catch (e) {
} catch (e) { ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar( SnackBar(
SnackBar(content: Text('${AppLocalizations.of(context).failedtosentOTP}: $e')), content:
); Text('${AppLocalizations.of(context).failedtosentOTP}: $e')),
);
}
} }
} }
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)), appBar:
AppBar(title: Text(AppLocalizations.of(context).changeLoginPassword)),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Form( child: Form(
@@ -103,8 +105,8 @@ void _proceed() async {
icon: Icon(_showCurrentPassword icon: Icon(_showCurrentPassword
? Icons.visibility ? Icons.visibility
: Icons.visibility_off), : Icons.visibility_off),
onPressed: () => onPressed: () => setState(
setState(() => _showCurrentPassword = !_showCurrentPassword), () => _showCurrentPassword = !_showCurrentPassword),
), ),
), ),
validator: validateCurrentPassword, validator: validateCurrentPassword,
@@ -135,8 +137,8 @@ void _proceed() async {
icon: Icon(_showConfirmPassword icon: Icon(_showConfirmPassword
? Icons.visibility ? Icons.visibility
: Icons.visibility_off), : Icons.visibility_off),
onPressed: () => onPressed: () => setState(
setState(() => _showConfirmPassword = !_showConfirmPassword), () => _showConfirmPassword = !_showConfirmPassword),
), ),
), ),
validator: validateConfirmPassword, validator: validateConfirmPassword,

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/di/injection.dart';
import 'package:kmobile/l10n/app_localizations.dart'; import 'package:kmobile/l10n/app_localizations.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -11,27 +13,42 @@ class DailyLimitScreen extends StatefulWidget {
class _DailyLimitScreenState extends State<DailyLimitScreen> { class _DailyLimitScreenState extends State<DailyLimitScreen> {
double? _currentLimit; double? _currentLimit;
double? _spentAmount = 0.0;
final _limitController = TextEditingController(); final _limitController = TextEditingController();
var service = getIt<LimitService>();
Limit? limit;
bool _isLoading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Now just taking null, but for real time limit will be fetched using API call _loadlimits();
_currentLimit = null;
} }
@override Future<void> _loadlimits() async {
void dispose() { setState(() {
_limitController.dispose(); _isLoading = true;
super.dispose(); });
} final limit_data = await service.getLimit();
setState(() {
limit = limit_data;
_isLoading = false;
});
}
Future<void> _showAddOrEditLimitDialog() async { @override
void dispose() {
_limitController.dispose();
super.dispose();
}
Future<void> _showAddOrEditLimitDialog() async {
_limitController.text = _currentLimit?.toStringAsFixed(0) ?? ''; _limitController.text = _currentLimit?.toStringAsFixed(0) ?? '';
final newLimit = await showDialog<double>( final newLimit = await showDialog<double>(
context: context, context: context,
builder: (context) { builder: (dialogContext) {
final localizations = AppLocalizations.of(context); final localizations = AppLocalizations.of(dialogContext);
final theme = Theme.of(dialogContext);
return AlertDialog( return AlertDialog(
title: Text( title: Text(
_currentLimit == null _currentLimit == null
@@ -53,14 +70,25 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(dialogContext).pop(),
child: Text(localizations.cancel), child: Text(localizations.cancel),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
final value = double.tryParse(_limitController.text); final value = double.tryParse(_limitController.text);
if (value != null && value > 0) { if (value == null || value <= 0) return;
Navigator.of(context).pop(value);
if (value > 200000) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text("Limit To be Set must be less than 200000"),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.error,
),
);
} else {
service.editLimit(value);
Navigator.of(dialogContext).pop(value);
} }
}, },
child: Text(localizations.save), child: Text(localizations.save),
@@ -69,24 +97,46 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
); );
}, },
); );
if (newLimit != null) { if (newLimit != null) {
setState(() { _loadlimits();
_currentLimit = newLimit; if (!mounted) return;
}); ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Limit Updated"),
behavior: SnackBarBehavior.floating,
),
);
} }
} }
void _removeLimit() {
setState(() {
_currentLimit = null;
});
}
@override void _removeLimit() {
setState(() {
_currentLimit = null;
});
}
@override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isLoading) {
final localizations = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(localizations.dailylimit),
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
_currentLimit = limit?.dailyLimit;
_spentAmount = limit?.usedLimit;
final localizations = AppLocalizations.of(context); final localizations = AppLocalizations.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: ''); final formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final remainingLimit = _currentLimit != null ? _currentLimit! - _spentAmount! : 0.0;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(localizations.dailylimit), title: Text(localizations.dailylimit),
@@ -116,6 +166,23 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
: theme.colorScheme.primary, : theme.colorScheme.primary,
), ),
), ),
if (_currentLimit != null) ...[
const SizedBox(height: 24),
Text(
"Remaining Limit Today", // This should be localized
style: theme.textTheme.titleMedium,
),
const SizedBox(height: 4),
Text(
formatCurrency.format(remainingLimit),
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: remainingLimit > 0
? Colors.green
: theme.colorScheme.error,
),
),
],
const SizedBox(height: 48), const SizedBox(height: 48),
if (_currentLimit == null) if (_currentLimit == null)
ElevatedButton.icon( ElevatedButton.icon(
@@ -142,14 +209,14 @@ class _DailyLimitScreenState extends State<DailyLimitScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextButton.icon( // TextButton.icon(
onPressed: _removeLimit, // onPressed: _removeLimit,
icon: const Icon(Icons.remove_circle_outline), // icon: const Icon(Icons.remove_circle_outline),
label: Text(localizations.removeLimit), // label: Text(localizations.removeLimit),
style: TextButton.styleFrom( // style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.error, // foregroundColor: theme.colorScheme.error,
), // ),
), // ),
], ],
), ),
], ],

View File

@@ -8,8 +8,8 @@ class LogoutDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context).logout), title: Text(AppLocalizations.of(context).deregister),
content: Text(AppLocalizations.of(context).logoutCheck), content: Text(AppLocalizations.of(context).deregistercheck),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(context, false), // dismiss onPressed: () => Navigator.pop(context, false), // dismiss

View File

@@ -1,7 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:kmobile/data/repositories/auth_repository.dart'; import 'package:kmobile/data/repositories/auth_repository.dart';
import 'package:kmobile/features/profile/change_password/change_password_screen.dart'; import 'package:kmobile/features/profile/change_password/change_password_screen.dart';
import 'package:kmobile/features/profile/daily_transaction_limit.dart';
import 'package:kmobile/features/profile/logout_dialog.dart'; import 'package:kmobile/features/profile/logout_dialog.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:shared_preferences/shared_preferences.dart';
import '../../di/injection.dart'; import '../../di/injection.dart';
import '../../l10n/app_localizations.dart'; import '../../l10n/app_localizations.dart';
@@ -16,7 +22,27 @@ class ProfileScreen extends StatefulWidget {
} }
class _ProfileScreenState extends State<ProfileScreen> { class _ProfileScreenState extends State<ProfileScreen> {
Future<void> _handleLogout(BuildContext context) async { bool _isBiometricEnabled = false;
@override
void initState() {
super.initState();
_loadBiometricStatus();
}
Future<String> _getAppVersion() async {
final PackageInfo info = await PackageInfo.fromPlatform();
return 'Version ${info.version} (${info.buildNumber})';
}
Future<void> _loadBiometricStatus() async {
final storage = getIt<SecureStorage>();
final isEnabled = await storage.read('biometric_enabled');
setState(() {
_isBiometricEnabled = isEnabled == true;
});
}
Future<void> _handleLogout(BuildContext context) async {
final auth = getIt<AuthRepository>(); final auth = getIt<AuthRepository>();
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await prefs.clear(); // clear saved session/token await prefs.clear(); // clear saved session/token
@@ -25,6 +51,90 @@ class _ProfileScreenState extends State<ProfileScreen> {
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false); Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
} }
Future<void> _handleBiometricToggle(bool enable) async {
final localAuth = LocalAuthentication();
final storage = getIt<SecureStorage>();
final canCheck = await localAuth.canCheckBiometrics;
if (!canCheck) {
// Optional: Show a snackbar or dialog if biometrics are not available
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalizations.of(context).biometricsNotAvailable)),
);
return;
}
if (enable) {
// Show "Enable" dialog
final optIn = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
content: Text(AppLocalizations.of(context).enableFingerprintMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (optIn == true) {
try {
final didAuth = await localAuth.authenticate(
localizedReason: AppLocalizations.of(context).authenticateToEnable,
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
if (didAuth) {
await storage.write('biometric_enabled', 'true');
setState(() {
_isBiometricEnabled = true;
});
}
} catch (e) {
// Handle authentication errors
}
}
} else {
// Show "Disable" dialog
final optOut = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context).disableFingerprintLogin),
content: Text(AppLocalizations.of(context).disableFingerprintMessage),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (optOut == true) {
await storage.write('biometric_enabled', 'false');
setState(() {
_isBiometricEnabled = false;
});
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -46,16 +156,36 @@ class _ProfileScreenState extends State<ProfileScreen> {
); );
}, },
), ),
ListTile(
leading: const Icon(Icons.currency_rupee),
title: Text(AppLocalizations.of(context).dailylimit),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DailyLimitScreen()),
);
},
),
SwitchListTile(
title: Text(AppLocalizations.of(context).enableFingerprintLogin),
value: _isBiometricEnabled,
onChanged: (bool value) {
_handleBiometricToggle(value);
},
secondary: const Icon(Icons.fingerprint),
),
ListTile( ListTile(
leading: const Icon(Icons.password), leading: const Icon(Icons.password),
title: Text(loc.changeLoginPassword), title: Text(loc.changeLoginPassword),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => ChangePasswordScreen( MaterialPageRoute(
mobileNumber: widget.mobileNumber, builder: (context) => ChangePasswordScreen(
)), mobileNumber: widget.mobileNumber,
); )),
);
}, },
), ),
// ListTile( // ListTile(
@@ -70,9 +200,60 @@ class _ProfileScreenState extends State<ProfileScreen> {
// onTap: () async { // onTap: () async {
// }, // },
// ), // ),
ListTile( ListTile(
leading: const Icon(Icons.logout), leading: const Icon(Icons.smartphone),
title: const Text("App Version"),
trailing: FutureBuilder<String>(
future: _getAppVersion(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
// Show a loading indicator while waiting for the future to complete
return const CircularProgressIndicator();
} else if (snapshot.hasError) {
return const Text("Error");
} else {
// Display the version number once the future is complete
return Text(
snapshot.data ?? "N/A",
selectionColor: const Color(0xFFFFFFFF),
);
}
},
),
),
ListTile(
leading: const Icon(Icons.exit_to_app),
title: Text(AppLocalizations.of(context).logout), title: Text(AppLocalizations.of(context).logout),
onTap: () async {
final shouldExit = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(AppLocalizations.of(context).logout),
content: Text(AppLocalizations.of(context).logoutCheck),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(AppLocalizations.of(context).no),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(AppLocalizations.of(context).yes),
),
],
),
);
if (shouldExit == true) {
if (Platform.isAndroid) {
SystemNavigator.pop();
}
exit(0);
}
},
),
ListTile(
leading: const Icon(Icons.logout),
title: Text(AppLocalizations.of(context).deregister),
onTap: () async { onTap: () async {
final shouldLogout = await showDialog<bool>( final shouldLogout = await showDialog<bool>(
context: context, context: context,

View File

@@ -2,7 +2,9 @@ import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/imps_service.dart'; import 'package:kmobile/api/services/imps_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/neft_service.dart'; import 'package:kmobile/api/services/neft_service.dart';
import 'package:kmobile/api/services/rtgs_service.dart'; import 'package:kmobile/api/services/rtgs_service.dart';
import 'package:kmobile/data/models/imps_transaction.dart'; import 'package:kmobile/data/models/imps_transaction.dart';
@@ -28,7 +30,10 @@ class QuickPayOutsideBankScreen extends StatefulWidget {
class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> { class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
// Controllers // Controllers
final accountNumberController = TextEditingController(); final accountNumberController = TextEditingController();
final confirmAccountNumberController = TextEditingController(); final confirmAccountNumberController = TextEditingController();
@@ -41,6 +46,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
final remarksController = TextEditingController(); final remarksController = TextEditingController();
final _ifscFocusNode = FocusNode(); final _ifscFocusNode = FocusNode();
final service = getIt<BeneficiaryService>(); final service = getIt<BeneficiaryService>();
bool _isAmountOverLimit = false;
late String accountType; late String accountType;
bool _isValidating = false; bool _isValidating = false;
@@ -50,18 +56,62 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit();
_ifscFocusNode.addListener(() { _ifscFocusNode.addListener(() {
if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) { if (!_ifscFocusNode.hasFocus && ifscController.text.trim().length == 11) {
_validateIFSC(); _validateIFSC();
} }
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() { setState(() {
accountType = 'Savings'; accountType = 'Savings';
}); });
}); });
amountController.addListener(_checkAmountLimit);
} }
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
// Add this method to check the amount against the limit
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
void _validateIFSC() async { void _validateIFSC() async {
final ifsc = ifscController.text.trim().toUpperCase(); final ifsc = ifscController.text.trim().toUpperCase();
if (ifsc.isEmpty) return; if (ifsc.isEmpty) return;
@@ -84,7 +134,8 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
final errorMessage = e.toString().toUpperCase(); final errorMessage = e.toString().toUpperCase();
String snackbarMessage = AppLocalizations.of(context).somethingWentWrong; String snackbarMessage =
AppLocalizations.of(context).somethingWentWrong;
if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) { if (errorMessage.contains('INVALID') && errorMessage.contains('IFSC')) {
snackbarMessage = AppLocalizations.of(context).invalidIfsc; snackbarMessage = AppLocalizations.of(context).invalidIfsc;
@@ -97,7 +148,7 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
branchNameController.clear(); branchNameController.clear();
} }
} }
} }
void _validateBeneficiary() async { void _validateBeneficiary() async {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@@ -527,15 +578,15 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
controller: ifscController, controller: ifscController,
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
final trimmed = value.trim().toUpperCase(); final trimmed = value.trim().toUpperCase();
if (trimmed.length < 11) { if (trimmed.length < 11) {
// clear bank/branch if backspace or changed // clear bank/branch if backspace or changed
bankNameController.clear(); bankNameController.clear();
branchNameController.clear(); branchNameController.clear();
} }
}); });
}, },
validator: (value) { validator: (value) {
final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$'); final pattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$');
if (value == null || value.trim().isEmpty) { if (value == null || value.trim().isEmpty) {
@@ -637,20 +688,21 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: _isValidating || ifscController.text.length != 11 onPressed:
? null _isValidating || ifscController.text.length != 11
: () { ? null
if (confirmAccountNumberController.text == : () {
accountNumberController.text) { if (confirmAccountNumberController.text ==
_validateBeneficiary(); accountNumberController.text) {
} else { _validateBeneficiary();
setState(() { } else {
_validationError = setState(() {
AppLocalizations.of(context) _validationError =
.accountMismatch; AppLocalizations.of(context)
}); .accountMismatch;
} });
}, }
},
child: _isValidating child: _isValidating
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
@@ -696,26 +748,29 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
return null; return null;
}, },
), ),
const SizedBox(height: 25),
TextFormField(
controller: remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
),
const SizedBox(height: 25), const SizedBox(height: 25),
TextFormField(
controller: remarksController,
decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(),
isDense: true,
filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2),
),
),
),
const SizedBox(height: 25),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row( Row(
children: [ children: [
Expanded( Expanded(
@@ -781,6 +836,22 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
), ),
], ],
), ),
],
),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Padding(
padding: EdgeInsets.only(left: 8.0),
child: Text('Fetching daily limit...'),
),
if (!_isLoadingLimit && _limit != null)
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
),
const SizedBox(height: 30), const SizedBox(height: 30),
Row( Row(
children: [ children: [
@@ -793,24 +864,31 @@ class _QuickPayOutsideBankScreen extends State<QuickPayOutsideBankScreen> {
], ],
), ),
const SizedBox(height: 45), const SizedBox(height: 45),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SwipeButton.expand( child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward, thumb: Icon(Icons.arrow_forward,
color: Theme.of(context).dialogBackgroundColor), color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: Theme.of(context).colorScheme.primary, activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
activeTrackColor: Theme.of(context).colorScheme.primary,
Theme.of(context).colorScheme.secondary.withAlpha(100), activeTrackColor: _isAmountOverLimit
borderRadius: BorderRadius.circular(30), ? Colors.grey.shade300
height: 56, : Theme.of(context).colorScheme.secondary.withAlpha(100),
onSwipe: _onProceedToPay, borderRadius: BorderRadius.circular(30),
child: Text( height: 56,
AppLocalizations.of(context).swipeToPay, onSwipe: () {
style: const TextStyle( if (_isAmountOverLimit) {
fontSize: 16, fontWeight: FontWeight.bold), return; // Do nothing if amount is over the limit
), }
), _onProceedToPay();
), },
child: Text(
AppLocalizations.of(context).swipeToPay,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
], ],
), ),
), ),

View File

@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_swipe_button/flutter_swipe_button.dart'; import 'package:flutter_swipe_button/flutter_swipe_button.dart';
import 'package:intl/intl.dart';
import 'package:kmobile/api/services/beneficiary_service.dart'; import 'package:kmobile/api/services/beneficiary_service.dart';
import 'package:kmobile/api/services/limit_service.dart';
import 'package:kmobile/api/services/payment_service.dart'; import 'package:kmobile/api/services/payment_service.dart';
import 'package:kmobile/data/models/transfer.dart'; import 'package:kmobile/data/models/transfer.dart';
import 'package:kmobile/di/injection.dart'; import 'package:kmobile/di/injection.dart';
@@ -19,14 +21,17 @@ class QuickPayWithinBankScreen extends StatefulWidget {
class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> { class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _limitService = getIt<LimitService>();
Limit? _limit;
bool _isLoadingLimit = true;
final _formatCurrency = NumberFormat.currency(locale: 'en_IN', symbol: '');
final TextEditingController accountNumberController = TextEditingController(); final TextEditingController accountNumberController = TextEditingController();
final TextEditingController confirmAccountNumberController = final TextEditingController confirmAccountNumberController =
TextEditingController(); TextEditingController();
final TextEditingController amountController = TextEditingController(); final TextEditingController amountController = TextEditingController();
final TextEditingController remarksController = TextEditingController(); final TextEditingController remarksController = TextEditingController();
String? _selectedAccountType; String? _selectedAccountType;
bool _isAmountOverLimit = false;
String? _beneficiaryName; String? _beneficiaryName;
bool _isValidating = false; bool _isValidating = false;
bool _isBeneficiaryValidated = false; bool _isBeneficiaryValidated = false;
@@ -35,10 +40,54 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadLimit();
accountNumberController.addListener(_resetBeneficiaryValidation); accountNumberController.addListener(_resetBeneficiaryValidation);
confirmAccountNumberController.addListener(_resetBeneficiaryValidation); confirmAccountNumberController.addListener(_resetBeneficiaryValidation);
amountController.addListener(_checkAmountLimit);
} }
Future<void> _loadLimit() async {
setState(() {
_isLoadingLimit = true;
});
try {
final limitData = await _limitService.getLimit();
setState(() {
_limit = limitData;
_isLoadingLimit = false;
});
} catch (e) {
// Handle error if needed
setState(() {
_isLoadingLimit = false;
});
}
}
void _checkAmountLimit() {
if (_limit == null) return;
final amount = double.tryParse(amountController.text) ?? 0;
final remainingLimit = _limit!.dailyLimit - _limit!.usedLimit;
final bool isOverLimit = amount > remainingLimit;
if (isOverLimit) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Amount exceeds remaining daily limit of ${_formatCurrency.format(remainingLimit)}'),
backgroundColor: Colors.red,
),
);
}
// Update state only if it changes to avoid unnecessary rebuilds
if (_isAmountOverLimit != isOverLimit) {
setState(() {
_isAmountOverLimit = isOverLimit;
});
}
}
void _resetBeneficiaryValidation() { void _resetBeneficiaryValidation() {
if (_isBeneficiaryValidated || if (_isBeneficiaryValidated ||
_beneficiaryName != null || _beneficiaryName != null ||
@@ -53,6 +102,7 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
@override @override
void dispose() { void dispose() {
amountController.removeListener(_checkAmountLimit);
accountNumberController.removeListener(_resetBeneficiaryValidation); accountNumberController.removeListener(_resetBeneficiaryValidation);
confirmAccountNumberController.removeListener(_resetBeneficiaryValidation); confirmAccountNumberController.removeListener(_resetBeneficiaryValidation);
accountNumberController.dispose(); accountNumberController.dispose();
@@ -102,7 +152,8 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: SingleChildScrollView(
child: Column(
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
TextFormField( TextFormField(
@@ -278,25 +329,26 @@ class _QuickPayWithinBankScreen extends State<QuickPayWithinBankScreen> {
}, },
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
TextFormField( TextFormField(
controller: remarksController, controller: remarksController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).remarks, labelText: AppLocalizations.of(context).remarks,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
isDense: true, isDense: true,
filled: true, filled: true,
fillColor: Theme.of(context).scaffoldBackgroundColor, fillColor: Theme.of(context).scaffoldBackgroundColor,
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline), color: Theme.of(context).colorScheme.outline),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, width: 2), color: Theme.of(context).colorScheme.primary, width: 2),
), ),
), ),
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
TextFormField( TextFormField(
decoration: InputDecoration( decoration: InputDecoration(
labelText: AppLocalizations.of(context).amount, labelText: AppLocalizations.of(context).amount,
@@ -327,66 +379,81 @@ TextFormField(
return null; return null;
}, },
), ),
const SizedBox(height: 8),
if (_isLoadingLimit)
const Text('Fetching daily limit...'),
if (!_isLoadingLimit && _limit != null)
Text(
'Remaining Daily Limit: ${_formatCurrency.format(_limit!.dailyLimit - _limit!.usedLimit)}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 45), const SizedBox(height: 45),
Align( Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SwipeButton.expand( child: SwipeButton.expand(
thumb: Icon(Icons.arrow_forward, thumb: Icon(Icons.arrow_forward,
color: Theme.of(context).dialogBackgroundColor), color: _isAmountOverLimit ? Colors.grey : Theme.of(context).dialogBackgroundColor),
activeThumbColor: Theme.of(context).colorScheme.primary, activeThumbColor: _isAmountOverLimit ? Colors.grey.shade700 :
activeTrackColor: Theme.of( Theme.of(context).colorScheme.primary,
context, activeTrackColor: _isAmountOverLimit
).colorScheme.secondary.withAlpha(100), ? Colors.grey.shade300
borderRadius: BorderRadius.circular(30), : Theme.of(
height: 56, context,
child: Text( ).colorScheme.secondary.withAlpha(100),
AppLocalizations.of(context).swipeToPay, borderRadius: BorderRadius.circular(30),
style: const TextStyle(fontSize: 16), height: 56,
), child: Text(
onSwipe: () { AppLocalizations.of(context).swipeToPay,
if (_formKey.currentState!.validate()) { style: const TextStyle(fontSize: 16),
if (!_isBeneficiaryValidated) { ),
setState(() { onSwipe: () {
_validationError = AppLocalizations.of(context) if (_isAmountOverLimit) {
.validateBeneficiaryproceeding; return; // Do nothing if amount is over limit
}); }
return; if (_formKey.currentState!.validate()) {
} if (!_isBeneficiaryValidated) {
// Perform payment logic setState(() {
Navigator.push( _validationError = AppLocalizations.of(context)
context, .validateBeneficiaryproceeding;
MaterialPageRoute( });
builder: (context) => TransactionPinScreen( return;
onPinCompleted: (pinScreenContext, tpin) async { }
final transfer = Transfer( // Perform payment logic
fromAccount: widget.debitAccount, Navigator.push(
toAccount: accountNumberController.text, context,
toAccountType: _selectedAccountType!, MaterialPageRoute(
amount: amountController.text, builder: (context) => TransactionPinScreen(
tpin: tpin, onPinCompleted: (pinScreenContext, tpin) async {
remarks: remarksController.text, final transfer = Transfer(
); fromAccount: widget.debitAccount,
toAccount: accountNumberController.text,
toAccountType: _selectedAccountType!,
amount: amountController.text,
tpin: tpin,
remarks: remarksController.text,
);
final paymentService = getIt<PaymentService>(); final paymentService = getIt<PaymentService>();
final paymentResponseFuture = paymentService final paymentResponseFuture = paymentService
.processQuickPayWithinBank(transfer); .processQuickPayWithinBank(transfer);
Navigator.of(pinScreenContext).pushReplacement( Navigator.of(pinScreenContext).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => PaymentAnimationScreen( builder: (_) => PaymentAnimationScreen(
paymentResponse: paymentResponseFuture), paymentResponse: paymentResponseFuture),
), ),
); );
}, },
), ),
), ),
); );
} }
}, },
), ),
), ),
], ],
), ),
),
), ),
), ),
); );

View File

@@ -5,7 +5,8 @@ import 'package:lottie/lottie.dart';
class SecurityErrorScreen extends StatelessWidget { class SecurityErrorScreen extends StatelessWidget {
final String message; final String message;
const SecurityErrorScreen({Key? key, required this.message}) : super(key: key); const SecurityErrorScreen({Key? key, required this.message})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -24,7 +25,8 @@ class SecurityErrorScreen extends StatelessWidget {
), ),
const SizedBox(height: 40), const SizedBox(height: 40),
ElevatedButton( ElevatedButton(
onPressed: () => SystemChannels.platform.invokeMethod('SystemNavigator.pop'), onPressed: () =>
SystemChannels.platform.invokeMethod('SystemNavigator.pop'),
child: const Text('Okay'), child: const Text('Okay'),
), ),
], ],

View File

@@ -1,3 +1,5 @@
// ignore_for_file: unused_element
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
@@ -27,45 +29,45 @@ class BranchLocatorScreen extends StatefulWidget {
State<BranchLocatorScreen> createState() => _BranchLocatorScreenState(); State<BranchLocatorScreen> createState() => _BranchLocatorScreenState();
} }
class _BranchLocatorScreenState extends State<BranchLocatorScreen> { class _BranchLocatorScreenState extends State<BranchLocatorScreen> {
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
final List<Location> _allLocations = [ final List<Location> _allLocations = [
Location( Location(
name: "Dharamsala - Head Office", name: "Dharamsala - Head Office",
code: "002", code: "002",
ifsc: "KACE0000002", ifsc: "KACE0000002",
address: "Civil Lines Dharmashala, Kangra, HP - 176215", address: "Civil Lines Dharmashala, Kangra, HP - 176215",
type: LocationType.branch, type: LocationType.branch,
), ),
Location( Location(
name: "Kangra", name: "Kangra",
code: "033", code: "033",
ifsc: "KACE0000033", ifsc: "KACE0000033",
address: "Rajput Bhawankangrapo, Kangra, HP ", address: "Rajput Bhawankangrapo, Kangra, HP ",
type: LocationType.branch, type: LocationType.branch,
), ),
Location( Location(
name: "Dharamsala ATM", name: "Dharamsala ATM",
address: "Near Main Square, Dharamsala", address: "Near Main Square, Dharamsala",
type: LocationType.atm, type: LocationType.atm,
), ),
Location( Location(
name: "Kangra ATM", name: "Kangra ATM",
address: "Opposite Bus Stand, Kangra", address: "Opposite Bus Stand, Kangra",
type: LocationType.atm, type: LocationType.atm,
), ),
]; ];
List<Location> _filteredLocations = []; List<Location> _filteredLocations = [];
bool _isLoading = false; bool _isLoading = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// _fetchAndSetLocations(); // _fetchAndSetLocations();
_filteredLocations = _allLocations; _filteredLocations = _allLocations;
} }
// Example of a future API fetching function // Example of a future API fetching function
/* /*
@@ -88,99 +90,99 @@ Future<void> _fetchAndSetLocations() async {
} }
} }
*/ */
void _filterLocations(String query) { void _filterLocations(String query) {
setState(() { setState(() {
if (query.isEmpty) { if (query.isEmpty) {
_filteredLocations = _allLocations; _filteredLocations = _allLocations;
} else { } else {
_filteredLocations = _allLocations.where((location) { _filteredLocations = _allLocations.where((location) {
final lowerQuery = query.toLowerCase(); final lowerQuery = query.toLowerCase();
return location.name.toLowerCase().contains(lowerQuery) || return location.name.toLowerCase().contains(lowerQuery) ||
(location.code?.toLowerCase().contains(lowerQuery) ?? false) || (location.code?.toLowerCase().contains(lowerQuery) ?? false) ||
(location.ifsc?.toLowerCase().contains(lowerQuery) ?? false) || (location.ifsc?.toLowerCase().contains(lowerQuery) ?? false) ||
location.address.toLowerCase().contains(lowerQuery); location.address.toLowerCase().contains(lowerQuery);
}).toList(); }).toList();
} }
}); });
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(AppLocalizations.of(context).branchLocator), title: Text(AppLocalizations.of(context).branchLocator),
), ),
body: Column( body: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(12.0), padding: const EdgeInsets.all(12.0),
child: TextField( child: TextField(
controller: _searchController, controller: _searchController,
onChanged: _filterLocations, onChanged: _filterLocations,
decoration: InputDecoration( decoration: InputDecoration(
hintText: AppLocalizations.of(context).searchbranchby, hintText: AppLocalizations.of(context).searchbranchby,
prefixIcon: const Icon(Icons.search), prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
), ),
),
),
// Content area
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredLocations.isEmpty
? const Center(child: Text("No matching locations found"))
: ListView.builder(
itemCount: _filteredLocations.length,
itemBuilder: (context, index) {
final location = _filteredLocations[index];
return _buildLocationItem(location);
},
), ),
),
],
),
);
}
Widget _buildHeader(String title) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
), ),
),
); // Content area
} Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: _filteredLocations.isEmpty
? const Center(child: Text("No matching locations found"))
: ListView.builder(
itemCount: _filteredLocations.length,
itemBuilder: (context, index) {
final location = _filteredLocations[index];
return _buildLocationItem(location);
},
),
),
],
),
);
}
Widget _buildHeader(String title) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
// Helper widget to build a single location item // Helper widget to build a single location item
Widget _buildLocationItem(Location location) { Widget _buildLocationItem(Location location) {
final isBranch = location.type == LocationType.branch; final isBranch = location.type == LocationType.branch;
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: ListTile( child: ListTile(
leading: CircleAvatar( leading: CircleAvatar(
child: Icon(isBranch ? Icons.location_city : Icons.currency_rupee), child: Icon(isBranch ? Icons.location_city : Icons.currency_rupee),
),
title: Text(location.name,
style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(
isBranch
? "Code: ${location.code} | IFSC: ${location.ifsc}\nAddress: ${location.address}"
: "Address: ${location.address}",
),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Selected ${location.name}")),
);
},
), ),
title: Text(location.name, );
style: const TextStyle(fontWeight: FontWeight.bold)), }
subtitle: Text(
isBranch
? "Code: ${location.code} | IFSC: ${location.ifsc}\nAddress: ${location.address}"
: "Address: ${location.address}",
),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("Selected ${location.name}")),
);
},
),
);
}
} }

View File

@@ -1,47 +1,47 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:kmobile/l10n/app_localizations.dart'; import 'package:kmobile/l10n/app_localizations.dart';
class FaqsScreen extends StatefulWidget { class FaqsScreen extends StatefulWidget {
const FaqsScreen({super.key}); const FaqsScreen({super.key});
@override @override
State<FaqsScreen> createState() => _FaqsScreenState(); State<FaqsScreen> createState() => _FaqsScreenState();
} }
class _FaqsScreenState extends State<FaqsScreen> { class _FaqsScreenState extends State<FaqsScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_getFaqs(); _getFaqs();
} }
// A placeholder for your future API call // A placeholder for your future API call
Future<void> _getFaqs() async { Future<void> _getFaqs() async {
// TODO: Implement API call to fetch FAQs data // TODO: Implement API call to fetch FAQs data
// For now, simulating a network call with a delay // For now, simulating a network call with a delay
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
// In a real implementation, you would process the API response here // In a real implementation, you would process the API response here
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row( title: Row(
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
AppLocalizations.of(context).faq, AppLocalizations.of(context).faq,
softWrap: true, softWrap: true,
style: const TextStyle( style: const TextStyle(
fontSize: 16.5, fontSize: 16.5,
), ),
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
), ),
], ],
), ),
), ),
); );
} }
} }

View File

@@ -1,10 +1,11 @@
import 'package:kmobile/features/service/screens/branch_locator_screen.dart'; import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
import 'package:kmobile/features/service/screens/daily_transaction_limit.dart'; import 'package:kmobile/features/profile/daily_transaction_limit.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:kmobile/features/service/screens/quick_links_screen.dart'; import 'package:kmobile/features/service/screens/quick_links_screen.dart';
import 'package:kmobile/features/service/screens/faqs_screen.dart'; import 'package:kmobile/features/service/screens/faqs_screen.dart';
class ServiceScreen extends StatefulWidget { class ServiceScreen extends StatefulWidget {
const ServiceScreen({super.key}); const ServiceScreen({super.key});
@@ -13,81 +14,71 @@ class ServiceScreen extends StatefulWidget {
} }
class _ServiceScreen extends State<ServiceScreen> { class _ServiceScreen extends State<ServiceScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
title: Text( title: Text(
AppLocalizations.of(context).services, AppLocalizations.of(context).services,
),
centerTitle: false,
), ),
centerTitle: false, body: ListView(
), children: [
body: ListView( ServiceManagementTile(
children: [ icon: Symbols.add,
ServiceManagementTile( label: AppLocalizations.of(context).accountOpeningDeposit,
icon: Symbols.add, onTap: () {},
label: AppLocalizations.of(context).accountOpeningDeposit, disabled: true,
onTap: () {}, ),
disabled: true, const Divider(height: 1),
), ServiceManagementTile(
const Divider(height: 1), icon: Symbols.add,
ServiceManagementTile( label: AppLocalizations.of(context).accountOpeningLoan,
icon: Symbols.add, onTap: () {},
label: AppLocalizations.of(context).accountOpeningLoan, disabled: true,
onTap: () {}, ),
disabled: true, const Divider(height: 1),
), ServiceManagementTile(
const Divider(height: 1), icon: Symbols.captive_portal,
ServiceManagementTile( label: AppLocalizations.of(context).quickLinks,
icon: Symbols.currency_rupee, onTap: () {
label: AppLocalizations.of(context).dailylimit, Navigator.of(context).push(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const DailyLimitScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.captive_portal,
label: AppLocalizations.of(context).quickLinks,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const QuickLinksScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.question_mark,
label: AppLocalizations.of(context).faq,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const FaqsScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.location_pin,
label: AppLocalizations.of(context).branchLocator,
onTap: () {
Navigator.push(
context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const BranchLocatorScreen())); builder: (context) => const QuickLinksScreen()),
}, );
disabled: true, },
), disabled: true,
const Divider(height: 1), ),
], const Divider(height: 1),
), ServiceManagementTile(
); icon: Symbols.question_mark,
} label: AppLocalizations.of(context).faq,
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const FaqsScreen()),
);
},
disabled: true,
),
const Divider(height: 1),
ServiceManagementTile(
icon: Symbols.location_pin,
label: AppLocalizations.of(context).branchLocator,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const BranchLocatorScreen()));
},
disabled: true,
),
const Divider(height: 1),
],
),
);
}
} }
class ServiceManagementTile extends StatelessWidget { class ServiceManagementTile extends StatelessWidget {

View File

@@ -326,5 +326,10 @@
"editLimit": "Edit Limit", "editLimit": "Edit Limit",
"removeLimit": "Remove Limit", "removeLimit": "Remove Limit",
"limitAmount": "Limit Amount", "limitAmount": "Limit Amount",
"save": "Save" "save": "Save",
"deregister": "De-Register",
"deregistercheck": "Are you sure you want to De-Register?",
"biometricsNotAvailable": "Biometrics not available on this device",
"disableFingerprintLogin": "Disable Fingerprint Login",
"disableFingerprintMessage": "Are you sure you want to disable fingerprint login?"
} }

View File

@@ -327,5 +327,10 @@
"editLimit": "सीमा संपादित करें", "editLimit": "सीमा संपादित करें",
"removeLimit": "सीमा हटाएँ", "removeLimit": "सीमा हटाएँ",
"limitAmount": "सीमा राशि", "limitAmount": "सीमा राशि",
"save": "जमा करें" "save": "जमा करें",
"deregister": "अपंजीकृत",
"deregistercheck": "क्या आप वाकई पंजीकरण रद्द करना चाहते हैं??",
"biometricsNotAvailable": "इस डिवाइस पर बायोमेट्रिक्स उपलब्ध नहीं है",
"disableFingerprintLogin": "फ़िंगरप्रिंट लॉगिन अक्षम करें",
"disableFingerprintMessage": "क्या आप फ़िंगरप्रिंट लॉगिन अक्षम करना चाहते हैं?"
} }

View File

@@ -1,6 +1,7 @@
// ignore_for_file: unused_import // ignore_for_file: unused_import
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:kmobile/core/logger.dart';
import 'package:kmobile/features/security/security_error_screen.dart'; import 'package:kmobile/features/security/security_error_screen.dart';
import 'package:kmobile/security/security_service.dart'; import 'package:kmobile/security/security_service.dart';
import 'di/injection.dart'; import 'di/injection.dart';
@@ -8,6 +9,7 @@ import 'app.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
Logger.info("App starting...");
await SystemChrome.setPreferredOrientations([ await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp, DeviceOrientation.portraitUp,
@@ -15,13 +17,16 @@ void main() async {
]); ]);
// Check for device compromise // Check for device compromise
// final compromisedMessage = await SecurityService.deviceCompromisedMessage; final compromisedMessage = await SecurityService.deviceCompromisedMessage;
// if (compromisedMessage != null) { if (compromisedMessage != null) {
// runApp(MaterialApp( Logger.error("Device compromised: $compromisedMessage");
// home: SecurityErrorScreen(message: compromisedMessage), runApp(MaterialApp(
// )); home: SecurityErrorScreen(message: compromisedMessage),
// return; ));
// } return;
}
Logger.info("Setting up dependencies...");
await setupDependencies(); await setupDependencies();
Logger.info("Dependencies set up.");
runApp(const KMobile()); runApp(const KMobile());
} }

View File

@@ -22,7 +22,7 @@ Widget getBankLogo(String? bankName, BuildContext context) {
height: 40, height: 40,
); );
} }
if (bankName != null && bankName.toLowerCase().contains('icici bank ltd')) { if (bankName != null && bankName.toLowerCase().contains('icici')) {
return Image.asset( return Image.asset(
'assets/images/icici_logo.png', 'assets/images/icici_logo.png',
width: 40, width: 40,
@@ -86,14 +86,14 @@ Widget getBankLogo(String? bankName, BuildContext context) {
height: 40, height: 40,
); );
} }
if (bankName != null && bankName.toLowerCase().contains('ipsbank') || bankName != null && bankName.toLowerCase().contains('india post') ) { if (bankName != null && bankName.toLowerCase().contains('ipsbank') ||
bankName != null && bankName.toLowerCase().contains('india post')) {
return Image.asset( return Image.asset(
'assets/images/ipos_logo.png', 'assets/images/ipos_logo.png',
width: 40, width: 40,
height: 40, height: 40,
); );
} } else {
else {
return Icon( return Icon(
Icons.account_balance, Icons.account_balance,
size: 40, size: 40,

View File

@@ -8,6 +8,7 @@ import Foundation
import device_info_plus import device_info_plus
import flutter_secure_storage_macos import flutter_secure_storage_macos
import local_auth_darwin import local_auth_darwin
import package_info_plus
import path_provider_foundation import path_provider_foundation
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View File

@@ -69,10 +69,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: characters name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -93,18 +93,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: clock name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.19.1"
confetti: confetti:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -181,10 +181,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: fake_async name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.1" version: "1.3.3"
ffi: ffi:
dependency: transitive dependency: transitive
description: description:
@@ -333,6 +333,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
sha256: "90778fe0497fe3a09166e8cf2e0867310ff434b794526589e77ec03cf08ba8e8"
url: "https://pub.dev"
source: hosted
version: "8.2.14"
get_it: get_it:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -385,10 +393,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: intl name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.20.2"
jailbreak_root_detection: jailbreak_root_detection:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -417,26 +425,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.5" version: "11.0.2"
leak_tracker_flutter_testing: leak_tracker_flutter_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_flutter_testing name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.10"
leak_tracker_testing: leak_tracker_testing:
dependency: transitive dependency: transitive
description: description:
name: leak_tracker_testing name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@@ -497,10 +505,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.16+1" version: "0.12.17"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -521,10 +529,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.15.0" version: "1.16.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -541,14 +549,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
path: path:
dependency: transitive dependency: transitive
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
@@ -617,18 +641,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: permission_handler name: permission_handler
sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "12.0.1" version: "11.4.0"
permission_handler_android: permission_handler_android:
dependency: transitive dependency: transitive
description: description:
name: permission_handler_android name: permission_handler_android
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "13.0.1" version: "12.1.0"
permission_handler_apple: permission_handler_apple:
dependency: transitive dependency: transitive
description: description:
@@ -709,6 +733,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
send_message:
dependency: "direct main"
description:
name: send_message
sha256: "79b5f69fd3ab0b9e6265f8d972800d7989b3082a0523c7f4b8e38bf4e1c71235"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -789,11 +821,27 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
showcaseview:
dependency: "direct main"
description:
name: showcaseview
sha256: "3929adfcff53a8a9bc6b501914d67e4b7eae40451db7e654f76f34b0b30a185a"
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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.99" version: "0.0.0"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -814,18 +862,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.11.1" version: "1.12.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.4"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@@ -846,10 +894,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.2" version: "0.7.6"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -923,7 +971,7 @@ packages:
source: hosted source: hosted
version: "3.1.4" version: "3.1.4"
uuid: uuid:
dependency: transitive dependency: "direct main"
description: description:
name: uuid name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
@@ -958,10 +1006,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_math name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.2.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@@ -1019,5 +1067,5 @@ packages:
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.5.0 <4.0.0" dart: ">=3.8.0-0 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.24.0"

View File

@@ -31,10 +31,6 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_neumorphic : 3.2.0 flutter_neumorphic : 3.2.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
jailbreak_root_detection: ^1.1.6 jailbreak_root_detection: ^1.1.6
equatable: ^2.0.7 equatable: ^2.0.7
@@ -61,8 +57,14 @@ dependencies:
share_plus: ^7.2.1 share_plus: ^7.2.1
confetti: ^0.7.0 confetti: ^0.7.0
pdf: ^3.11.3 pdf: ^3.11.3
permission_handler: ^12.0.1 permission_handler: ^11.3.1
device_info_plus: ^11.3.0 device_info_plus: ^11.3.0
showcaseview: ^2.0.3
package_info_plus: ^4.2.0
simcards: ^0.0.1
uuid: ^4.5.1
send_message: ^1.0.0
fluttertoast: ^8.2.6
# jailbreak_root_detection: "^1.1.6" # jailbreak_root_detection: "^1.1.6"