import 'dart:io' show Platform; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; import 'package:nearle/views/helpers/constants/Font_constant.dart'; import 'package:nearle/views/helpers/constants/Colorconstants.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:nearle/providers/auth/auth_provider.dart'; import 'package:nearle/utils/device.dart'; import 'package:sms_autofill/sms_autofill.dart'; import 'package:nearle/controllers/profile_controller.dart'; // ignore: unused_import import 'package:nearle/controllers/riderlog.dart'; import 'package:nearle/Models/login/login.dart'; import 'dart:convert'; enum AuthNext { verifyPin, otp, notRegistered, error } class AuthController extends GetxController { final RxBool sendingOtp = false.obs; String? currentPhone; final AuthProvider _api = AuthProvider(); AuthNext? lastDecision; // Optional callback used by MPIN screen to clear and refocus fields when user taps "Retry" VoidCallback? onPinRetry; static const String _prefsUserIdKey = 'userid'; static const String _prefsPendingPinUserIdKey = 'pending_pin_userid'; static const String _prefsUserNameKey = 'user_name'; static const String _prefsUserEmailKey = 'user_email'; static const String _prefsContactNoKey = 'contactno'; static const String _prefsAddressKey = 'user_address'; static const String _prefsForceMasterPinKey = 'force_master_pin'; static const String _masterPinValue = '1234'; static const String forceMasterPinPrefKey = _prefsForceMasterPinKey; static const String masterPinValue = _masterPinValue; bool _forceMasterPinFlow = false; void _log(String msg) => debugPrint('[AUTH] $msg'); Future _notifyProfileController() async { try { if (Get.isRegistered()) { final prefs = await SharedPreferences.getInstance(); final pc = Get.find(); await pc.loadFromPrefs(); pc.setProfile( name: prefs.getString(_prefsUserNameKey), email: prefs.getString(_prefsUserEmailKey), contact: prefs.getString(_prefsContactNoKey), address: prefs.getString(_prefsAddressKey), ); } } catch (_) {} } String _normalizePhone(String input) { final digitsOnly = input.replaceAll(RegExp(r'\D'), ''); if (digitsOnly.length >= 10) { return digitsOnly.substring(digitsOnly.length - 10); } return digitsOnly; } void _showBottomSheet({required String title, required String message}) { Get.bottomSheet( SafeArea( child: Container( padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon( Icons.info_outline, color: Color(0xFF662582), size: 40, ), const SizedBox(height: 12), Text( title, textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.w700, fontFamily: FontConstants.fontFamily, fontSize: 20, ), ), const SizedBox(height: 8), Text( message, textAlign: TextAlign.center, style: const TextStyle(fontSize: 16), ), const SizedBox(height: 16), SizedBox( height: 50, width: double.infinity, child: ElevatedButton( onPressed: () { Get.back(); // If MPIN screen has registered a retry callback, run it onPinRetry?.call(); }, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF662582), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: const Text( 'Retry', style: TextStyle(color: Colors.white, fontSize: 20), ), ), ), ], ), ), ), isScrollControlled: true, backgroundColor: Colors.transparent, ); } Future precheckPhone(String phone) async { try { final normalized = _normalizePhone(phone); final prefs = await SharedPreferences.getInstance(); _forceMasterPinFlow = false; await prefs.remove(_prefsForceMasterPinKey); String deviceId; try { deviceId = await DeviceUtils.ensureDeviceId(prefs); } catch (e) { _showBottomSheet( title: 'Device Error', message: 'Failed to get device ID. Please restart the app and try again.', ); lastDecision = AuthNext.error; return lastDecision!; } final String fcmToken = await DeviceUtils.ensureFcmToken(prefs); final bool fcmWasEmpty = fcmToken.isEmpty; _log( 'POST /users/rider/login body=${jsonEncode({'contactno': normalized, 'devicetype': Platform.operatingSystem, 'configid': 6, 'deviceid': deviceId, 'userfcmtoken': fcmToken})}', ); final Login loginRes = await _api.loginParsed( contactNo: normalized, deviceType: Platform.operatingSystem, configId: 6, deviceId: deviceId, fcmToken: fcmToken, ); debugPrint('RAW LOGIN RESPONSE: $loginRes'); final String serverMessage = (loginRes.message ?? '').toLowerCase(); final bool masterPinFlow = (loginRes.authmode ?? 0) == 1; final bool requiresPinSetup = serverMessage.contains('pin not set') || serverMessage.contains('mpin not set') || serverMessage.contains('please set your pin') || (loginRes.code != null && loginRes.code == 201); if (serverMessage.contains('not registered') || serverMessage.contains('register first') || serverMessage.contains('not found') || serverMessage.contains('no user')) { currentPhone = null; _showBottomSheet( title: 'Not Registered', message: 'Please contact admin and register first.', ); lastDecision = AuthNext.notRegistered; return lastDecision!; } if (serverMessage.contains('inactive')) { currentPhone = null; _showBottomSheet( title: 'Rider Inactive', message: 'Rider is inactive. Please contact admin.', ); lastDecision = AuthNext.notRegistered; return lastDecision!; } final bool messageSaysEnterPin = (loginRes.message ?? '') .toLowerCase() .contains('enter your pin'); if (loginRes.status == true) { try { final SharedPreferences prefsSave = await SharedPreferences.getInstance(); // Persist from model if (loginRes.userid != null) { await prefsSave.setInt(_prefsUserIdKey, loginRes.userid!); final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString( 'username', loginRes.fullname ?? loginRes.firstname.toString(), ); await prefs.setInt('userid', loginRes.userid ?? 0); await prefs.setInt('userId', loginRes.userid ?? 0); await prefs.setInt('shiftid', loginRes.shiftid ?? 0); await prefs.setInt('shiftId', loginRes.shiftid ?? 0); await prefs.setInt('logid', loginRes.logid ?? 0); await prefs.setInt('logId', loginRes.logid ?? 0); await prefs.setInt('riderid', loginRes.riderid ?? 0); await prefs.setInt('partnerid', loginRes.partnerid ?? 0); await prefs.setInt('partnerId', loginRes.partnerid ?? 0); await prefs.setInt('rconfigid', loginRes.configid ?? 0); await prefs.setInt('locationid', loginRes.locationid ?? 0); await prefs.setInt('tenantid', loginRes.tenantid ?? 0); await prefs.setInt('applocationid', loginRes.applocationid ?? 0); if (loginRes.userfcmtoken != null && loginRes.userfcmtoken!.isNotEmpty) { await prefs.setString('userfcmtoken', loginRes.userfcmtoken!); } debugPrint('saved on shared pref :${loginRes.fullname}'); } final String? name = loginRes.fullname ?? loginRes.firstname; final String? email = loginRes.email; final String contact = (loginRes.contactno ?? normalized).toString(); final String? address = loginRes.address; _log( 'Saving from model: name=$name, email=$email, contact=$contact, address=$address', ); if (name != null && name.trim().isNotEmpty) { await prefsSave.setString(_prefsUserNameKey, name.trim()); } if (email != null && email.trim().isNotEmpty) { await prefsSave.setString(_prefsUserEmailKey, email.trim()); } if (contact.isNotEmpty) { await prefsSave.setString( _prefsContactNoKey, _normalizePhone(contact), ); } if (address != null && address.trim().isNotEmpty) { await prefsSave.setString(_prefsAddressKey, address.trim()); } await _notifyProfileController(); } catch (_) {} } // Persist basic user profile details from model even if above branch didn't run try { final prefs2 = await SharedPreferences.getInstance(); final String? name = loginRes.fullname ?? loginRes.firstname; final String? email = loginRes.email; final String contact = (loginRes.contactno ?? normalized).toString(); final String? address = loginRes.address; if (name != null && name.trim().isNotEmpty) { await prefs2.setString(_prefsUserNameKey, name.trim()); } if (email != null && email.trim().isNotEmpty) { await prefs2.setString(_prefsUserEmailKey, email.trim()); } if (contact.isNotEmpty) { final normalizedContact = _normalizePhone(contact); await prefs2.setString(_prefsContactNoKey, normalizedContact); } if (address != null && address.trim().isNotEmpty) { await prefs2.setString(_prefsAddressKey, address.trim()); } await _notifyProfileController(); } catch (_) {} if (masterPinFlow) { currentPhone = _normalizePhone(phone); _forceMasterPinFlow = true; await prefs.setBool(_prefsForceMasterPinKey, true); await prefs.remove('dbPin'); if (loginRes.userid != null) { await prefs.setInt(_prefsUserIdKey, loginRes.userid!); } Get.snackbar( 'Temporary PIN', 'Use $_masterPinValue as PIN to continue.', snackPosition: SnackPosition.BOTTOM, duration: const Duration(seconds: 4), backgroundColor: ColorConstants.primaryColor, colorText: Colors.white, ); lastDecision = AuthNext.verifyPin; return lastDecision!; } if (messageSaysEnterPin) { currentPhone = _normalizePhone(phone); lastDecision = AuthNext.verifyPin; return lastDecision!; } if (requiresPinSetup && !masterPinFlow) { currentPhone = _normalizePhone(phone); await prefs.remove('dbPin'); if (loginRes.userid != null) { await prefs.setInt(_prefsPendingPinUserIdKey, loginRes.userid!); } lastDecision = AuthNext.otp; return lastDecision!; } // Immediately create rider log entry after successful login currentPhone = normalized; String? dbPin = loginRes.pin?.toString(); if (dbPin != null && dbPin.isNotEmpty) { await prefs.setString('dbPin', dbPin); lastDecision = AuthNext.verifyPin; return lastDecision!; } await prefs.remove('dbPin'); lastDecision = AuthNext.verifyPin; // If FCM was empty during the request, try to refresh session once token becomes available if (fcmWasEmpty) { try { final String newToken = await DeviceUtils.ensureFcmToken(prefs); if (newToken.isNotEmpty) { await refreshSession(phone: normalized); } } catch (_) {} } return lastDecision!; } catch (e) { debugPrint('Precheck phone error: $e'); lastDecision = AuthNext.error; return lastDecision!; } } Future sendOtp([String? phoneArg]) async { if (sendingOtp.value) return false; if (phoneArg != null && phoneArg.isNotEmpty) { currentPhone = _normalizePhone(phoneArg); } if (currentPhone == null) { Get.snackbar( 'Error', 'Phone number not set. Please enter your number again.', ); return false; } sendingOtp.value = true; try { final prefs = await SharedPreferences.getInstance(); final phone = currentPhone!; // Get cached SMS provider settings or use defaults String templateId = prefs.getString('smsTemplateId') ?? '1107173468024541800'; String smsContent = prefs.getString('smsContent') ?? '<#> Dear customer, use this One Time Password {#var#} to sign-in to Nearle App. This OTP will be valid for the next 5 mins.'; // Only fetch SMS provider settings if not cached or cache is old (older than 1 hour) final lastProviderFetch = prefs.getInt('lastProviderFetch') ?? 0; final now = DateTime.now().millisecondsSinceEpoch; if (now - lastProviderFetch > 3600000) { // 1 hour in milliseconds try { final providerUri = Uri.parse( 'https://jupiter.nearle.app/live/api/v1/platform/getsmsprovider?templatetypeid=1', ); final provRes = await http .get(providerUri) .timeout(const Duration(seconds: 10)); if (provRes.statusCode == 200) { final Map prov = json.decode(provRes.body); final details = prov['details'] as Map?; if (details != null) { templateId = (details['templateid'] ?? templateId).toString(); smsContent = (details['content'] ?? smsContent).toString(); // Cache the settings await prefs.setString('smsTemplateId', templateId); await prefs.setString('smsContent', smsContent); await prefs.setInt('lastProviderFetch', now); } } } catch (_) {} } // Append app hash for Android SMS Retriever so auto-fill works silently String appHash = ''; try { appHash = await SmsAutoFill().getAppSignature; if (appHash.isNotEmpty) { _log('Using app hash for SMS Retriever: $appHash'); } } catch (_) {} // Generate OTP ourselves (like the original implementation) String actualOtp = _generateOtp(); await prefs.setString('lastOtp', actualOtp); _log('Generated OTP: $actualOtp'); // Replace {#var#} with actual OTP before sending to Lion SMS final composedSmsBase = smsContent.replaceAll('{#var#}', actualOtp); final composedSms = appHash.isNotEmpty ? ('$composedSmsBase\n$appHash') : composedSmsBase; final phoneWithCountry = phone.startsWith('+') ? phone : '+91$phone'; final encodedSms = Uri.encodeComponent(composedSms); final smsUrl = Uri.parse( 'https://msg.lionsms.com/api/smsapi?key=e57f5c9679af26077be1a7eadabb1b2a&route=7&sender=NEARLE&number=$phoneWithCountry&templateid=$templateId&sms=$encodedSms', ); final smsRes = await http .get(smsUrl) .timeout(const Duration(seconds: 10)); if (smsRes.statusCode == 200 && !(smsRes.body.contains('108') || smsRes.body.contains('110') || smsRes.body.toLowerCase().contains('error'))) { return true; } else { Get.snackbar( 'OTP Send Failed', 'Provider: ${smsRes.statusCode} ${smsRes.body}', ); return false; } } catch (e) { debugPrint('sendOtp error: $e'); Get.snackbar('Error', 'An unexpected error occurred while sending OTP.'); return false; } finally { sendingOtp.value = false; } } Future verifyOtp(String code) async { try { final prefs = await SharedPreferences.getInstance(); final sentOtp = prefs.getString('lastOtp'); if (sentOtp != null && code == sentOtp) { await prefs.remove('lastOtp'); _log('OTP verification successful: $code'); return true; } _log('OTP verification failed. Expected: $sentOtp, Got: $code'); return false; } catch (e) { _log('OTP verification error: $e'); return false; } } // Generate 6-digit OTP (same as original implementation) String _generateOtp() { final random = Random(); final otp = 100000 + random.nextInt(900000); // Generates 100000-999999 return otp.toString(); } Future setPin(String newPin) async { try { final prefs = await SharedPreferences.getInstance(); int? userId = prefs.getInt(_prefsPendingPinUserIdKey) ?? prefs.getInt(_prefsUserIdKey); if (newPin.length != 4 || int.tryParse(newPin) == null) { _showBottomSheet( title: 'Invalid PIN', message: 'Please enter a valid 4-digit PIN.', ); return false; } if (userId == null) { _showBottomSheet( title: 'Error', message: 'User ID not found. Please try again.', ); return false; } final int pinNum = int.parse(newPin); final res = await _api.updatePin(userId: userId, pin: pinNum); if (res.statusCode >= 200 && res.statusCode < 300) { await prefs.setString('dbPin', newPin); await prefs.remove(_prefsPendingPinUserIdKey); return true; } final bodyPreview = res.body.length > 200 ? '${res.body.substring(0, 200)}...' : res.body; _showBottomSheet( title: 'Failed (${res.statusCode})', message: 'Unable to set PIN. Server said: $bodyPreview', ); return false; } catch (e) { debugPrint('setPin error: $e'); _showBottomSheet( title: 'Error', message: 'Something went wrong while setting the PIN.', ); return false; } } /// Refresh session on backend with latest deviceId/FCM for the current phone. Future refreshSession({String? phone}) async { try { final prefs = await SharedPreferences.getInstance(); final String? usePhone = phone ?? currentPhone; if (usePhone == null || usePhone.isEmpty) { return false; } final deviceId = await DeviceUtils.ensureDeviceId(prefs); final fcmToken = await DeviceUtils.ensureFcmToken(prefs); final Login loginRes = await _api.loginParsed( contactNo: usePhone, deviceType: Platform.operatingSystem, configId: 6, deviceId: deviceId, fcmToken: fcmToken, ); if (loginRes.userid != null) { await prefs.setInt(_prefsUserIdKey, loginRes.userid!); } try { final String? name = loginRes.fullname ?? loginRes.firstname; final String? email = loginRes.email; final String contact = (loginRes.contactno ?? usePhone).toString(); final String? address = loginRes.address; if (name != null && name.trim().isNotEmpty) { await prefs.setString(_prefsUserNameKey, name.trim()); } if (email != null && email.trim().isNotEmpty) { await prefs.setString(_prefsUserEmailKey, email.trim()); } if (contact.isNotEmpty) { final normalizedContact = _normalizePhone(contact); await prefs.setString(_prefsContactNoKey, normalizedContact); } if (address != null && address.trim().isNotEmpty) { await prefs.setString(_prefsAddressKey, address.trim()); } await _notifyProfileController(); } catch (_) {} currentPhone = _normalizePhone(usePhone); return loginRes.status == true; } catch (_) { return false; } } Future verifyPinWithServer(String inputPin) async { try { if (inputPin.length != 4 || int.tryParse(inputPin) == null) { _showBottomSheet( title: 'Invalid PIN', message: 'Please enter a valid 4-digit PIN.', ); return false; } final prefs = await SharedPreferences.getInstance(); final String? phone = currentPhone; if (phone == null || phone.isEmpty) { _showBottomSheet( title: 'Session Expired', message: 'Please enter your number again.', ); return false; } final bool masterPinActive = _forceMasterPinFlow || (prefs.getBool(_prefsForceMasterPinKey) ?? false); if (masterPinActive) { if (inputPin != _masterPinValue) { _showBottomSheet(title: 'Invalid PIN', message: 'Please try again.'); return false; } _forceMasterPinFlow = false; await prefs.remove(_prefsForceMasterPinKey); await prefs.setString('dbPin', _masterPinValue); await prefs.setBool('logged_out', false); // Call loginParsed with the master pin to fetch and save all API data // This ensures shared_preferences has all the necessary data like regular pins String deviceId; try { deviceId = await DeviceUtils.ensureDeviceId(prefs); } catch (e) { _showBottomSheet( title: 'Device Error', message: 'Failed to get device ID. Please restart the app and try again.', ); return false; } final String fcmToken = await DeviceUtils.ensureFcmToken(prefs); final Login loginRes = await _api.loginParsed( contactNo: phone, deviceType: Platform.operatingSystem, configId: 6, deviceId: deviceId, fcmToken: fcmToken, pin: int.parse(_masterPinValue), ); // Save user details from API response if (loginRes.userid != null) { await prefs.setInt(_prefsUserIdKey, loginRes.userid!); } final String? name = loginRes.fullname ?? loginRes.firstname; final String? email = loginRes.email; final String contact = (loginRes.contactno ?? currentPhone ?? '') .toString(); final String? address = loginRes.address; if (name != null && name.trim().isNotEmpty) { await prefs.setString(_prefsUserNameKey, name.trim()); } if (email != null && email.trim().isNotEmpty) { await prefs.setString(_prefsUserEmailKey, email.trim()); } if (contact.isNotEmpty) { await prefs.setString(_prefsContactNoKey, _normalizePhone(contact)); } if (address != null && address.trim().isNotEmpty) { await prefs.setString(_prefsAddressKey, address.trim()); } try { if (currentPhone != null && currentPhone!.isNotEmpty) { currentPhone = _normalizePhone(currentPhone!); } await _notifyProfileController(); } catch (_) {} return true; } String deviceId; try { deviceId = await DeviceUtils.ensureDeviceId(prefs); } catch (e) { _showBottomSheet( title: 'Device Error', message: 'Failed to get device ID. Please restart the app and try again.', ); return false; } final String fcmToken = await DeviceUtils.ensureFcmToken(prefs); final Login loginRes = await _api.loginParsed( contactNo: phone, deviceType: Platform.operatingSystem, configId: 6, deviceId: deviceId, fcmToken: fcmToken, pin: int.parse(inputPin), ); final String msg = (loginRes.message ?? '').toLowerCase(); if ((loginRes.code != null && loginRes.code == 401) || msg.contains('invalid pin')) { _showBottomSheet( title: 'Invalid PIN', message: 'Incorrect PIN. Please try again.', ); return false; } if (loginRes.status == true || msg.contains('success')) { await prefs.setString('dbPin', inputPin); if (loginRes.userid != null) { await prefs.setInt(_prefsUserIdKey, loginRes.userid!); } await prefs.setBool('logged_out', false); final String? name = loginRes.fullname ?? loginRes.firstname; final String? email = loginRes.email; final String contact = (loginRes.contactno ?? currentPhone ?? '') .toString(); final String? address = loginRes.address; if (name != null && name.trim().isNotEmpty) { await prefs.setString(_prefsUserNameKey, name.trim()); } if (email != null && email.trim().isNotEmpty) { await prefs.setString(_prefsUserEmailKey, email.trim()); } if (contact.isNotEmpty) { await prefs.setString(_prefsContactNoKey, _normalizePhone(contact)); } if (address != null && address.trim().isNotEmpty) { await prefs.setString(_prefsAddressKey, address.trim()); } try { if (currentPhone != null && currentPhone!.isNotEmpty) { currentPhone = _normalizePhone(currentPhone!); } await _notifyProfileController(); } catch (_) {} // Ensure backend session is refreshed on this device with latest FCM/device id try { await refreshSession(phone: currentPhone); } catch (_) {} return true; } _showBottomSheet( title: 'Invalid PIN', message: 'Incorrect PIN. Please try again.', ); return false; } catch (e) { debugPrint('verifyPinWithServer error: $e'); _showBottomSheet( title: 'Error', message: 'Failed to verify PIN. Try again.', ); return false; } } }