diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 8601c77..7d68958 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -2,6 +2,8 @@
+
+
simVerify(String uuid, String cifNo) async{
+ try{
+ final response = await _dio.post(
+ '/api/sim-details-verify',
+ data: {
+ 'uuid': uuid,
+ 'cifNo': cifNo,
+ }
+ );
+ if (response.statusCode == 200) {
+ final String message = response.data.toString().trim().toUpperCase();
+
+ if (message == "VERIFIED") {
+ return; // Success
+ } else {
+ throw AuthException(message); // Throw message received
+ }
+ } else {
+ throw AuthException('Verification Failed');
+ }
+ } on DioException catch (e) {
+ if (kDebugMode) {
+ print(e.toString());
+ }
+ if (e.response?.statusCode == 401) {
+ throw AuthException(
+ e.response?.data['error'] ?? 'SOMETHING WENT WRONG');
+ }
+ throw NetworkException('Network error during verification');
+ }
+ catch (e) {
+ throw UnexpectedException(
+ 'Unexpected error during verification: ${e.toString()}');
+ }
+ }
+
Future login(AuthCredentials credentials) async {
try {
final response = await _dio.post(
diff --git a/lib/api/services/send_sms_service.dart b/lib/api/services/send_sms_service.dart
new file mode 100644
index 0000000..caf5c1c
--- /dev/null
+++ b/lib/api/services/send_sms_service.dart
@@ -0,0 +1,102 @@
+import 'dart:io';
+ import 'package:flutter/material.dart';
+ import 'package:permission_handler/permission_handler.dart';
+ import 'package:send_message/send_message.dart' show sendSMS;
+ import 'package:simcards/sim_card.dart';
+ import 'package:simcards/simcards.dart';
+
+ // This enum provides detailed status back to the UI layer.
+ enum PermissionStatusResult { granted, denied, permanentlyDenied, restricted }
+
+ class SmsService {
+ final Simcards _simcards = Simcards();
+
+ /// Handles the requesting of SMS and Phone permissions.
+ /// Returns a detailed status: granted, denied, or permanentlyDenied.
+ Future 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 sendVerificationSms({
+ required BuildContext context,
+ required String destinationNumber,
+ required String message,
+ }) async {
+ try {
+ List 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 _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;
+ }
+ }
+ }
\ No newline at end of file
diff --git a/lib/di/injection.dart b/lib/di/injection.dart
index d68bca6..e3eba3a 100644
--- a/lib/di/injection.dart
+++ b/lib/di/injection.dart
@@ -74,9 +74,9 @@ Dio _createDioClient() {
final dio = Dio(
BaseOptions(
baseUrl:
- //'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
+ 'http://lb-test-mobile-banking-app-192209417.ap-south-1.elb.amazonaws.com:8080', //test
//'http://lb-kccb-mobile-banking-app-848675342.ap-south-1.elb.amazonaws.com', //prod
- 'https://kccbmbnk.net', //prod small
+ //'https://kccbmbnk.net', //prod small
connectTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
headers: {
diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart
index 557748a..c71ed89 100644
--- a/lib/features/auth/screens/login_screen.dart
+++ b/lib/features/auth/screens/login_screen.dart
@@ -3,6 +3,7 @@ import 'package:kmobile/app.dart';
import 'package:kmobile/features/auth/screens/mpin_screen.dart';
import 'package:kmobile/features/auth/screens/set_password_screen.dart';
import 'package:kmobile/features/auth/screens/tnc_required_screen.dart';
+import 'package:kmobile/features/auth/screens/verification_screen.dart';
import 'package:kmobile/widgets/tnc_dialog.dart';
import '../../../l10n/app_localizations.dart';
import 'package:flutter/material.dart';
@@ -30,12 +31,23 @@ class LoginScreenState extends State
super.dispose();
}
- void _submitForm() {
+ void _submitForm() async {
if (_formKey.currentState!.validate()) {
- context.read().login(
- _customerNumberController.text.trim(),
- _passwordController.text,
- );
+ final bool? verificationSuccess = await Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => VerificationScreen(
+ customerNo: _customerNumberController.text.trim(),
+ password: _passwordController.text,
+ ),
+ ),
+ );
+
+ if (verificationSuccess == true && mounted) {
+ context.read().login(
+ _customerNumberController.text.trim(),
+ _passwordController.text,
+ );
+ }
}
}
diff --git a/lib/features/auth/screens/sms_verification_helper.dart b/lib/features/auth/screens/sms_verification_helper.dart
new file mode 100644
index 0000000..ee6732b
--- /dev/null
+++ b/lib/features/auth/screens/sms_verification_helper.dart
@@ -0,0 +1,169 @@
+
+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 _showPermanentlyDeniedDialog(BuildContext context) async {
+ await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text("Permission Required"),
+ content: const Text("SMS and Phone permissions are required for device verification. Please enable them in your app settings to continue."),
+ actions: [
+ TextButton(
+ child: const Text("Cancel"),
+ onPressed: () => Navigator.of(context).pop(),
+ ),
+ TextButton(
+ child: const Text("Open Settings"),
+ onPressed: () {
+ openAppSettings(); // Opens the phone's settings screen for this app
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ ),
+ );
+ }
+
+ Future _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(),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showSnackBar(BuildContext context, String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ duration: const Duration(seconds: 3),
+ ),
+ );
+ }
+
+ Future initiateSmsSequence({
+ required BuildContext context,
+ }) async {
+ bool hasPermission = false;
+
+ // --- PERMISSION LOOP ---
+ while (!hasPermission) {
+ // handleSmsPermission will check the status and request if not granted.
+ final status = await _smsService.handleSmsPermission();
+
+ switch (status) {
+ case PermissionStatusResult.granted:
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("Permissions Granted! Proceeding..."), duration: Duration(seconds: 2)),
+ );
+ hasPermission = true; // This will break the loop
+ break;
+ case PermissionStatusResult.denied:
+ // The user denied the permission. We show a dialog to explain why we need it
+ // and give them a chance to cancel or let the loop try again.
+ final tryAgain = await showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ title: const Text("Permission Required"),
+ content: const Text("This app requires SMS and Phone permissions to verify your device. Please grant the permissions to continue."),
+ actions: [
+ TextButton(
+ child: const Text("Cancel"),
+ onPressed: () => Navigator.of(context).pop(false),
+ ),
+ TextButton(
+ child: const Text("Try Again"),
+ onPressed: () => Navigator.of(context).pop(true),
+ ),
+ ],
+ ),
+ );
+ if (tryAgain != true) {
+ return null; // User chose to cancel.
+ }
+ // If they chose "Try Again", the loop will repeat.
+ break;
+ case PermissionStatusResult.permanentlyDenied:
+ await _showPermanentlyDeniedDialog(context);
+ // Give user time to come back from settings
+ await Future.delayed(const Duration(seconds: 5));
+ // The loop will repeat and re-check the status.
+ break;
+ case PermissionStatusResult.restricted:
+ await _showRestrictedSmsDialog(context);
+ // Give user time to come back from settings
+ await Future.delayed(const Duration(seconds: 5));
+ // The loop will repeat and re-check the status.
+ break;
+ }
+ }
+
+ // --- SMS SENDING LOOP ---
+ // This part will only be reached if hasPermission is true.
+ int retries = 3;
+ while (retries > 0) {
+ var uuid = const Uuid();
+ String uniqueId = uuid.v4();
+ String smsMessage = uniqueId;
+
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(content: Text("Attempting to send verification SMS... (${4 - retries})"), duration: const Duration(seconds: 2)),
+ );
+
+ bool isSmsSent = await _smsService.sendVerificationSms(
+ context: context,
+ destinationNumber: '9580079717', // Replace with your number
+ message: smsMessage,
+ );
+
+ if (isSmsSent) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("SMS sent successfully!"), duration: Duration(seconds: 2)),
+ );
+ return uniqueId;
+ } else {
+ retries--;
+ if (retries > 0) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text("SMS failed to send. Retrying in 5 seconds..."), duration: Duration(seconds: 4)),
+ );
+ await Future.delayed(const Duration(seconds: 5));
+ }
+ }
+ }
+
+ // If all retries fail
+ return null;
+ }
+}
diff --git a/lib/features/auth/screens/verification_screen.dart b/lib/features/auth/screens/verification_screen.dart
new file mode 100644
index 0000000..d297f15
--- /dev/null
+++ b/lib/features/auth/screens/verification_screen.dart
@@ -0,0 +1,166 @@
+import 'dart:async';
+import 'package:flutter/material.dart';
+import 'package:kmobile/api/services/auth_service.dart';
+import 'package:kmobile/di/injection.dart';
+import 'package:kmobile/features/auth/screens/sms_verification_helper.dart';
+
+class VerificationScreen extends StatefulWidget {
+ final String customerNo;
+ final String password;
+
+ const VerificationScreen({
+ super.key,
+ required this.customerNo,
+ required this.password,
+ });
+
+ @override
+ State createState() => _VerificationScreenState();
+}
+
+class _VerificationScreenState extends State {
+ String _statusMessage = "Starting verification...";
+ Timer? _timer;
+ int _countdown = 120;
+ bool _isVerifying = false;
+ bool _verificationFailed = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _startVerificationProcess();
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ void _startTimer() {
+ _timer?.cancel(); // Cancel any existing timer
+ _countdown = 120;
+ _timer = Timer.periodic(const Duration(seconds: 1), (timer) {
+ if (_countdown > 0) {
+ setState(() {
+ _countdown--;
+ });
+ } else {
+ _timer?.cancel();
+ if (mounted) {
+ setState(() {
+ _statusMessage = "Verification timed out. Please try again.";
+ _isVerifying = false;
+ _verificationFailed = true;
+ });
+ }
+ }
+ });
+ }
+
+ Future _startVerificationProcess() async {
+ setState(() {
+ _isVerifying = true;
+ _verificationFailed = false;
+ _statusMessage = "Starting verification...";
+ });
+ _startTimer();
+
+ // 1. Send SMS
+ setState(() {
+ _statusMessage = "SMS sending...";
+ });
+ final smsHelper = SmsVerificationHelper();
+ final uuid = await smsHelper.initiateSmsSequence(context: context);
+
+ if (uuid != null && mounted) {
+ // SMS sending was successful, now wait before verifying.
+ setState(() {
+ _statusMessage = "SMS sent. Waiting for network delivery...";
+ });
+
+ // Adding a 10-second delay to account for SMS network latency.
+ await Future.delayed(const Duration(seconds: 10));
+
+ if (!mounted) return;
+
+ // 2. Verify SIM
+ setState(() {
+ _statusMessage = "Verifying with server...";
+ });
+
+ final authService = getIt();
+ try {
+ await authService.simVerify(uuid, widget.customerNo);
+
+ setState(() {
+ _statusMessage = "Verification successful!";
+ _isVerifying = false;
+ });
+ _timer?.cancel();
+
+ // Pop with success result
+ Navigator.of(context).pop(true);
+
+ } catch (e) {
+ setState(() {
+ _statusMessage = " $e SIM verification failed. Please try again.";
+ _isVerifying = false;
+ _verificationFailed = true;
+ });
+ }
+ } else if (mounted) {
+ setState(() {
+ _statusMessage = "SMS sending failed. Please check permissions and try again.";
+ _isVerifying = false;
+ _verificationFailed = true;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: const Text("Device Verification"),
+ automaticallyImplyLeading: !_isVerifying,
+ ),
+ body: Center(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (_isVerifying)
+ const CircularProgressIndicator(),
+ if (!_isVerifying && _verificationFailed)
+ const Icon(Icons.error_outline, color: Colors.red, size: 50),
+ if (!_isVerifying && !_verificationFailed)
+ const Icon(Icons.check_circle_outline, color: Colors.green, size: 50),
+
+ const SizedBox(height: 32),
+ Text(
+ _statusMessage,
+ textAlign: TextAlign.center,
+ style: const TextStyle(fontSize: 18),
+ ),
+ const SizedBox(height: 16),
+ if (_isVerifying)
+ Text(
+ "Time remaining: $_countdown seconds",
+ style: const TextStyle(fontSize: 16, color: Colors.grey),
+ ),
+ if (_verificationFailed && !_isVerifying) ...[
+ const SizedBox(height: 20),
+ ElevatedButton(
+ onPressed: _startVerificationProcess,
+ child: const Text('Retry'),
+ ),
+ ]
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/pubspec.lock b/pubspec.lock
index d9ada43..b9d69fe 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -773,6 +773,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ send_message:
+ dependency: "direct main"
+ description:
+ name: send_message
+ sha256: "79b5f69fd3ab0b9e6265f8d972800d7989b3082a0523c7f4b8e38bf4e1c71235"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
share_plus:
dependency: "direct main"
description:
@@ -861,6 +869,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
+ simcards:
+ dependency: "direct main"
+ description:
+ name: simcards
+ sha256: b621cc265ebbb3e11009ca9be67063efbc011396c4224aff8b08edaba30fa5ae
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.0.1"
sky_engine:
dependency: transitive
description: flutter
@@ -1003,7 +1019,7 @@ packages:
source: hosted
version: "3.1.4"
uuid:
- dependency: transitive
+ dependency: "direct main"
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
diff --git a/pubspec.yaml b/pubspec.yaml
index ca2b318..2d122eb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -34,7 +34,7 @@ dependencies:
cupertino_icons: ^1.0.6
jailbreak_root_detection: ^1.1.6
equatable: ^2.0.7
- dio: ^5.8.0+1
+ dio: ^5.9.0
flutter_secure_storage: ^9.2.4
bloc: ^9.0.0
flutter_bloc: ^9.1.0
@@ -63,6 +63,9 @@ dependencies:
package_info_plus: ^4.2.0
flutter_local_notifications: ^19.5.0
open_filex: ^4.7.0
+ simcards: ^0.0.1
+ uuid: ^4.5.1
+ send_message: ^1.0.0
# jailbreak_root_detection: "^1.1.6"