import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:miler/views/helpers/constants/Font_constant.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/views/helpers/widgets/app_widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:miler/providers/auth/auth_provider.dart'; import 'package:miler/utils/device.dart'; import 'package:miler/controllers/profile_controller.dart'; import 'package:miler/Models/login/login.dart'; import 'package:miler/data/api_config.dart'; 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; 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( Builder( builder: (context) => Container( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: 16 + MediaQuery.of(context).viewPadding.bottom, ), decoration: BoxDecoration( color: ColorConstants.pureSurface, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.info_outline, color: ColorConstants.primary, 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, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(height: 16), MilerButton( label: 'Retry', onPressed: () { Get.back(); // If MPIN screen has registered a retry callback, run it onPinRetry?.call(); }, ), ], ), ), ), isScrollControlled: true, backgroundColor: Colors.transparent, ); } Future precheckPhone(String phone) async { try { final normalized = _normalizePhone(phone); currentPhone = normalized; final prefs = await SharedPreferences.getInstance(); // The mocked "Demo Rider" (userid 9999) that used to be written here is // gone. It bypassed the server entirely and left a fake identity in prefs // that outlived the session it was created for — every screen reading // 'userid' got 9999 until the app was reinstalled. The real user is // established by verify-pin and nowhere else. await prefs.setString(_prefsContactNoKey, normalized); // On the live backend, ask whether this phone already belongs to an // active miler account with a PIN on file. If it does, go straight to the // MPIN screen: OTP delivery isn't live yet, and the OTP path ends at // Create-MPIN, which would overwrite the PIN the account was issued. // Seeded development accounts take exactly this branch — enter the phone, // enter the seeded MPIN, done. final exists = await _api.milerAccountExists(normalized); if (exists) { lastDecision = AuthNext.verifyPin; return lastDecision!; } // Unknown number (or legacy backend) — fall through to the OTP step. lastDecision = AuthNext.otp; 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); } sendingOtp.value = true; try { await Future.delayed(const Duration(milliseconds: 500)); return true; } finally { sendingOtp.value = false; } } Future verifyOtp(String code) async { // Automatically succeed for mocked login return true; } 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; } // Demo mode: the mocked login flow stores a fake rider id (9999) that // the live server rejects, so updatePin fails. Save the PIN locally and // report success so the demo Create-MPIN flow works without a real // account or server call. if (userId == 9999) { await prefs.setString('dbPin', newPin); await prefs.remove(_prefsPendingPinUserIdKey); return true; } 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 { final prefs = await SharedPreferences.getInstance(); // The mocked-login bypass that used to sit here accepted ANY four digits // and logged the rider in without asking the server. It is gone: a PIN // check that cannot fail is not a PIN check. // Authenticate phone + PIN against POST /miler/verify-pin. Only a real // success (server ok + bearer token stored) logs the rider in. try { final String phone = currentPhone ?? prefs.getString(_prefsContactNoKey) ?? ''; final int? pinNum = int.tryParse(inputPin); if (phone.isEmpty || pinNum == null || inputPin.length != 4) { _showBottomSheet( title: 'Invalid PIN', message: 'Please enter your 4-digit PIN and try again.', ); return false; } final deviceId = await DeviceUtils.ensureDeviceId(prefs); final fcmToken = await DeviceUtils.ensureFcmToken(prefs); final Login res = await _api.loginParsed( contactNo: phone, deviceType: Platform.operatingSystem, configId: 6, deviceId: deviceId, fcmToken: fcmToken, pin: pinNum, pinRaw: inputPin, ); final String? token = await ApiConfig.getToken(); final bool ok = res.status == true && token != null && token.isNotEmpty; if (ok) { await prefs.setString('dbPin', inputPin); await prefs.setBool('logged_out', false); currentPhone = _normalizePhone(phone); // Overwrite any stale demo profile with the REAL logged-in identity. // The UI reads the display name from 'user_name'; loginParsed only wrote // 'username'/'firstname', so mirror the real name/email/contact here. final String realName = (res.fullname != null && res.fullname!.trim().isNotEmpty) ? res.fullname!.trim() : (prefs.getString('username') ?? '${res.firstname ?? ''} ${res.lastname ?? ''}') .trim(); if (realName.isNotEmpty) { await prefs.setString(_prefsUserNameKey, realName); } final String realEmail = (res.email ?? '').trim(); if (realEmail.isNotEmpty) { await prefs.setString(_prefsUserEmailKey, realEmail); } await prefs.setString(_prefsContactNoKey, _normalizePhone(phone)); await _notifyProfileController(); return true; } _showBottomSheet( title: 'Login failed', message: (res.message != null && res.message!.trim().isNotEmpty) ? res.message! : 'Incorrect phone number or PIN. Please try again.', ); return false; } catch (e) { debugPrint('verifyPinWithServer error: $e'); _showBottomSheet( title: 'Connection error', message: 'Could not reach the server. Check your internet and retry.', ); return false; } } }