From 1a2dea611bdcfd5794eeea169760a56d37fd2daa Mon Sep 17 00:00:00 2001 From: asif Date: Tue, 9 Dec 2025 18:11:46 +0530 Subject: [PATCH] SMS integrated with new ui --- android/app/src/main/AndroidManifest.xml | 2 + lib/api/services/auth_service.dart | 36 ++++ lib/api/services/send_sms_service.dart | 102 +++++++++++ lib/di/injection.dart | 4 +- lib/features/auth/screens/login_screen.dart | 22 ++- .../auth/screens/sms_verification_helper.dart | 169 ++++++++++++++++++ .../auth/screens/verification_screen.dart | 166 +++++++++++++++++ pubspec.lock | 18 +- pubspec.yaml | 5 +- 9 files changed, 515 insertions(+), 9 deletions(-) create mode 100644 lib/api/services/send_sms_service.dart create mode 100644 lib/features/auth/screens/sms_verification_helper.dart create mode 100644 lib/features/auth/screens/verification_screen.dart 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"