Compare commits
10 Commits
testing
...
sms-testin
| Author | SHA1 | Date | |
|---|---|---|---|
| 5823eaede8 | |||
| 1ae3e7c0a6 | |||
| 530e5c0493 | |||
| e1c1a58086 | |||
| dd3e94a69e | |||
| 2743f92283 | |||
| 72a9d5711a | |||
| 1edb2804f1 | |||
| c9c52b39fa | |||
| 7a0265ad8d |
@@ -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}"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
247
lib/api/services/send_sms_service.dart
Normal file
247
lib/api/services/send_sms_service.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ class UserService {
|
|||||||
|
|
||||||
Future<List<User>> getUserDetails() async {
|
Future<List<User>> getUserDetails() async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.get('/api/customer');
|
final response = await _dio.get('/api/customer/details');
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
log('Response: ${response.data}');
|
log('Response: ${response.data}');
|
||||||
return (response.data as List)
|
return (response.data as List)
|
||||||
|
|||||||
31
lib/app.dart
31
lib/app.dart
@@ -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,8 +15,6 @@ 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';
|
||||||
@@ -37,7 +37,6 @@ class KMobile extends StatefulWidget {
|
|||||||
|
|
||||||
class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
|
class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
|
||||||
Timer? _backgroundTimer;
|
Timer? _backgroundTimer;
|
||||||
bool showSplash = true;
|
|
||||||
Locale? _locale;
|
Locale? _locale;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -45,11 +44,6 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
|
|||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
loadPreferences();
|
loadPreferences();
|
||||||
Future.delayed(const Duration(seconds: 3), () {
|
|
||||||
setState(() {
|
|
||||||
showSplash = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -132,8 +126,7 @@ class _KMobileState extends State<KMobile> with WidgetsBindingObserver {
|
|||||||
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(),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -205,7 +198,11 @@ 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) {
|
||||||
@@ -214,7 +211,11 @@ class _AuthGateState extends State<AuthGate> {
|
|||||||
future: _tryBiometric(),
|
future: _tryBiometric(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const SplashScreen();
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (snapshot.data == true) {
|
if (snapshot.data == true) {
|
||||||
return const NavigationScaffold();
|
return const NavigationScaffold();
|
||||||
@@ -422,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 {
|
||||||
|
|||||||
@@ -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
15
lib/core/logger.dart
Normal 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
14
lib/core/toast.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ Dio _createDioClient() {
|
|||||||
final dio = Dio(
|
final dio = Dio(
|
||||||
BaseOptions(
|
BaseOptions(
|
||||||
baseUrl:
|
baseUrl:
|
||||||
// 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
|
// 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
|
||||||
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
|
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
|
||||||
'https://kccbmbnk.net', //prod small
|
'https://kccbmbnk.net', //prod small
|
||||||
connectTimeout: const Duration(seconds: 60),
|
connectTimeout: const Duration(seconds: 60),
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
131
lib/features/auth/screens/sms_verification_helper.dart
Normal file
131
lib/features/auth/screens/sms_verification_helper.dart
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import 'package:package_info_plus/package_info_plus.dart';
|
|
||||||
|
|
||||||
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> {
|
|
||||||
String _version = '';
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_loadVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _loadVersion() async {
|
|
||||||
final PackageInfo info = await PackageInfo.fromPlatform();
|
|
||||||
if (mounted) {
|
|
||||||
// Check if the widget is still in the tree
|
|
||||||
setState(() {
|
|
||||||
_version = 'Version ${info.version} (${info.buildNumber})';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: <Widget>[
|
|
||||||
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)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 90,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
child: Text(
|
|
||||||
_version,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Color(0xFFFFFFFF),
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
146
lib/features/auth/screens/verification_screen.dart
Normal file
146
lib/features/auth/screens/verification_screen.dart
Normal 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"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||||||
final storage = getIt<SecureStorage>();
|
final storage = getIt<SecureStorage>();
|
||||||
final isEnabled = await storage.read('biometric_enabled');
|
final isEnabled = await storage.read('biometric_enabled');
|
||||||
setState(() {
|
setState(() {
|
||||||
_isBiometricEnabled = isEnabled == 'true';
|
_isBiometricEnabled = isEnabled == true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:kmobile/features/service/screens/branch_locator_screen.dart';
|
import 'package:kmobile/features/service/screens/branch_locator_screen.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';
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
34
pubspec.lock
34
pubspec.lock
@@ -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:
|
||||||
@@ -633,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:
|
||||||
@@ -725,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:
|
||||||
@@ -813,6 +829,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
|
simcards:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: simcards
|
||||||
|
sha256: b621cc265ebbb3e11009ca9be67063efbc011396c4224aff8b08edaba30fa5ae
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.1"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -947,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
|
||||||
|
|||||||
@@ -57,10 +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
|
showcaseview: ^2.0.3
|
||||||
package_info_plus: ^4.2.0
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user