first commit
This commit is contained in:
348
lib/controllers/auth.dart
Normal file
348
lib/controllers/auth.dart
Normal file
@@ -0,0 +1,348 @@
|
||||
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<void> _notifyProfileController() async {
|
||||
try {
|
||||
if (Get.isRegistered<ProfileController>()) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final pc = Get.find<ProfileController>();
|
||||
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<AuthNext> 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<bool> 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<bool> verifyOtp(String code) async {
|
||||
// Automatically succeed for mocked login
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> 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<bool> 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<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user