import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:miler/Models/login/login.dart'; import 'package:miler/data/api_config.dart'; import 'package:miler/data/miler_api.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AuthProvider { Future login({ required String contactNo, required String deviceType, required int configId, required String deviceId, required String fcmToken, int? pin, String? pinRaw, }) async { // The legacy `jupiter.doormile.app/.../rider/login` path that used to sit // behind a flag here is gone with the rest of the old backend. return _loginNew( contactNo: contactNo, pin: pin, pinRaw: pinRaw, fcmToken: fcmToken, ); } /// NEW API: POST /miler/verify-pin { phone, pin, device_token } /// -> { success, token, data:{ userid, displayname, phone, hubid, /// availabilitystatus, rating } } /// /// We store the bearer token, then return a synthetic http.Response whose body /// is the LEGACY `{status, details:{...}}` shape so [loginParsed] persists the /// same SharedPreferences keys it always has — no change to the auth UI flow. /// /// `device_token` is REQUIRED for the rider to receive push at all: the backend /// stores it on the miler profile at verify-pin time and /// AssignMilerToBooking pushes "New Pickup Assigned" to exactly that token. /// Omitting it leaves the profile's token empty and silently makes the app /// poll-only. Future _loginNew({ required String contactNo, int? pin, String? pinRaw, String? fcmToken, }) async { final uri = Uri.parse(ApiConfig.url('/miler/verify-pin')); // The backend bcrypt-compares the PIN as a STRING, so a leading zero is // significant. Prefer the raw text the rider typed — round-tripping through // int drops it ("0512" -> 512 -> "512") and fails a valid PIN. final String? pinValue = (pinRaw != null && pinRaw.isNotEmpty) ? pinRaw : pin?.toString(); final body = { 'phone': contactNo, if (pinValue != null) 'pin': pinValue, // Riders live in partition 1001. It defaults server-side, so omitting it // appeared to work — but a miler row created without it can never log in, // and sending it explicitly is the only way the app and the console agree // about which partition a rider belongs to. 'configid': MilerApi.configId, if (fcmToken != null && fcmToken.isNotEmpty) 'device_token': fcmToken, }; debugPrint('[AUTH][LOGIN][NEW] URL: $uri'); debugPrint('[AUTH][LOGIN][NEW] Body: ${json.encode(body)}'); final res = await http.post( uri, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: json.encode(body), ); debugPrint('[AUTH][LOGIN][NEW] Status: ${res.statusCode}'); debugPrint('[AUTH][LOGIN][NEW] Response: ${res.body}'); Map decoded = {}; try { if (res.body.isNotEmpty) { decoded = json.decode(res.body) as Map; } } catch (_) {} final bool ok = decoded['success'] == true && res.statusCode >= 200 && res.statusCode < 300; // REAL shape: { success, token, user:{ authname, contactno, email, userid, // profile:{ userid, displayname, phone, hubid, applocationid, // availabilitystatus, rating, ... } } } (the pasted doc was wrong). final Map user = (decoded['user'] is Map) ? decoded['user'] as Map : (decoded['data'] is Map ? decoded['data'] as Map : {}); final Map profile = (user['profile'] is Map) ? user['profile'] as Map : {}; // Prefer profile field, then top-level user field. dynamic pick(String k) => profile[k] ?? user[k]; // Persist bearer token for all subsequent authenticated calls. final String token = (decoded['token'] ?? '').toString(); if (token.isNotEmpty) await ApiConfig.setToken(token); // displayname -> first/last name split (best effort). final String displayName = (pick('displayname') ?? user['authname'] ?? '') .toString() .trim(); final int spaceIdx = displayName.indexOf(' '); final String firstName = spaceIdx > 0 ? displayName.substring(0, spaceIdx) : displayName; final String lastName = spaceIdx > 0 ? displayName.substring(spaceIdx + 1).trim() : ''; final String availability = (pick('availabilitystatus') ?? 'Offline') .toString(); final int onduty = (availability.toLowerCase() == 'offline' || availability.isEmpty) ? 0 : 1; final userId = user['userid'] ?? profile['userid'] ?? 0; final phone = user['contactno'] ?? profile['phone'] ?? contactNo; // Map the new payload into the legacy `details` shape loginParsed reads. final legacy = { 'status': ok, 'code': ok ? 200 : (decoded['code'] ?? 400), 'message': decoded['message']?.toString() ?? '', 'details': { 'userid': userId, 'riderid': userId, 'displayname': displayName, 'username': displayName, 'firstname': firstName, 'lastname': lastName, 'contactno': phone, 'email': user['email'] ?? '', // hub/app-location scoping. 'hubid': profile['hubid'] ?? 0, 'locationid': profile['applocationid'] ?? profile['hubid'] ?? 0, 'applocationid': profile['applocationid'] ?? 0, 'rating': pick('rating') ?? 0, 'availabilitystatus': availability, 'onduty': onduty, // Fields the new contract does not provide yet — safe defaults. 'shiftid': 0, 'logid': 0, 'partnerid': 0, 'configid': 0, 'tenantid': 0, 'pickupradius': 100, 'starttime': '', 'endtime': '', }, }; // Also persist contactno directly (rider logs read it from prefs). final prefs = await SharedPreferences.getInstance(); await prefs.setString('contactno', phone.toString()); return http.Response( json.encode(legacy), ok ? 200 : res.statusCode, headers: {'content-type': 'application/json'}, ); } /// NEW API: POST /miler/login { phone } /// /// Account-existence precheck. 200 means the phone belongs to an active miler /// account that already has a PIN on file, so the rider should go straight to /// the MPIN screen — no OTP, no Create-MPIN (which would overwrite the PIN /// they were given). 404 means the number isn't registered. /// /// Returns true only for an existing, active miler account. Future milerAccountExists(String contactNo) async { try { final uri = Uri.parse(ApiConfig.url('/miler/login')); final res = await http .post( uri, headers: const { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: json.encode({ 'phone': contactNo, 'configid': MilerApi.configId, }), ) .timeout(const Duration(seconds: 15)); debugPrint( '[AUTH][PRECHECK] $contactNo -> ${res.statusCode} ${res.body}', ); if (res.statusCode < 200 || res.statusCode >= 300) return false; final decoded = json.decode(res.body); return decoded is Map && decoded['success'] == true; } catch (e) { debugPrint('[AUTH][PRECHECK] error: $e'); return false; } } /// NEW API: PUT /miler/device-token { device_token } /// /// verify-pin only registers the token at login time, but FCM rotates tokens /// independently of the session. Without re-registering, the backend keeps /// pushing to a dead token and the rider stops seeing new-assignment alerts /// with no visible symptom. Call this whenever the token changes. Future saveDeviceToken(String fcmToken) async { if (fcmToken.isEmpty) return false; try { final uri = Uri.parse(ApiConfig.url('/miler/device-token')); final res = await http .put( uri, headers: await ApiConfig.authHeaders(), body: json.encode({'device_token': fcmToken}), ) .timeout(const Duration(seconds: 15)); debugPrint('[AUTH][DEVICE_TOKEN] status=${res.statusCode}'); return res.statusCode >= 200 && res.statusCode < 300; } catch (e) { debugPrint('[AUTH][DEVICE_TOKEN] error: $e'); return false; } } // Convenience: send using a Login model body Future loginWith(Login request) async { final uri = Uri.parse( 'https://jupiter.doormile.app/live/api/v2/users/rider/login', ); final body = request.toJson(); debugPrint('[AUTH][LOGIN] URL: ${uri.toString()}'); debugPrint('[AUTH][LOGIN] Body: ${json.encode(body)}'); final res = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: json.encode(body), ); debugPrint('[AUTH][LOGIN] Status: ${res.statusCode}'); debugPrint('[AUTH][LOGIN] Response: ${res.body}'); return res; } // Convenience: parsed response as Login model Future loginParsed({ required String contactNo, required String deviceType, required int configId, required String deviceId, required String fcmToken, int? pin, String? pinRaw, }) async { final res = await login( contactNo: contactNo, deviceType: deviceType, configId: configId, deviceId: deviceId, fcmToken: fcmToken, pin: pin, pinRaw: pinRaw, ); final Map jsonMap = res.body.isNotEmpty ? json.decode(res.body) as Map : {}; debugPrint('[AUTH] Raw Login JSON: $jsonMap'); if (jsonMap.containsKey('details')) { final details = jsonMap['details']; final prefs = await SharedPreferences.getInstance(); await prefs.setInt('userid', details['userid'] ?? 0); await prefs.setInt('userId', details['userid'] ?? 0); await prefs.setInt('shiftid', details['shiftid'] ?? 0); await prefs.setInt('shiftId', details['shiftid'] ?? 0); await prefs.setInt('logid', details['logid'] ?? 0); await prefs.setInt('logId', details['logid'] ?? 0); await prefs.setInt('riderid', details['riderid'] ?? 0); await prefs.setInt('partnerid', details['partnerid'] ?? 0); await prefs.setInt('partnerId', details['partnerid'] ?? 0); await prefs.setInt('configid', details['configid'] ?? 0); await prefs.setInt('logseconds', details['logseconds'] ?? 0); await prefs.setInt('locationid', details['locationid'] ?? 0); await prefs.setInt('tenantid', details['tenantid'] ?? 0); await prefs.setInt('applocationid', details['applocationid'] ?? 0); final String fcm = (details['userfcmtoken'] ?? '').toString(); if (fcm.isNotEmpty) { await prefs.setString('userfcmtoken', fcm); } // Persist rider name variants for downstream usage (e.g. rider logs) final String firstName = (details['firstname'] ?? '').toString(); final String lastName = (details['lastname'] ?? '').toString(); final String apiUsername = (details['username'] ?? '').toString(); final String combinedName = ('$firstName $lastName').trim(); if (apiUsername.isNotEmpty) { await prefs.setString('username', apiUsername); } else if (combinedName.isNotEmpty) { await prefs.setString('username', combinedName); } if (firstName.isNotEmpty) { await prefs.setString('firstname', firstName); } if (lastName.isNotEmpty) { await prefs.setString('lastname', lastName); } if (details['onduty'] != null) { final int od = (details['onduty'] is num) ? (details['onduty'] as num).toInt() : int.tryParse('${details['onduty']}') ?? 0; await prefs.setInt('onduty', od); } // Persist rider payout config (per-kilometer fuel/rider charge) if provided if (details.containsKey('fuelcharge')) { final double fuelCharge = double.tryParse('${details['fuelcharge']}') ?? 0.0; await prefs.setDouble('fuelcharge', fuelCharge); } // Backward compatibility with older field names if (details.containsKey('firstmilecharge')) { final double firstMileCharge = double.tryParse('${details['firstmilecharge']}') ?? 0.0; await prefs.setDouble('firstmilecharge', firstMileCharge); } else if (details.containsKey('firstmilecharges')) { final double firstMileCharge = double.tryParse('${details['firstmilecharges']}') ?? 0.0; await prefs.setDouble('firstmilecharge', firstMileCharge); } // Save shift window for header display if (details['starttime'] != null) { await prefs.setString('starttime', details['starttime'].toString()); } await prefs.setString('endtime', details['endtime'].toString()); // Save pickup radius for geofencing (default 100m if not provided) if (details['pickupradius'] != null) { final int radius = (details['pickupradius'] is num) ? (details['pickupradius'] as num).toInt() : int.tryParse('${details['pickupradius']}') ?? 100; await prefs.setInt('pickupradius', radius); debugPrint('[AUTH] Saved pickupradius: $radius meters'); } else { await prefs.setInt('pickupradius', 100); // Default debugPrint('[AUTH] Saved default pickupradius: 100 meters'); } debugPrint( '[AUTH] SharedPrefs Saved: ' 'userid=${details['userid']}, shiftid=${details['shiftid']}, ' 'logid=${details['logid']}, riderid=${details['riderid']},' 'partnerid=${details['partnerid']}, configid=${details['configid']}', ); // Rider log creation is deferred until the rider goes ON duty. // // NOTE: We intentionally do NOT auto-navigate from here anymore. // Navigation after login / PIN verification is handled in the UI flows // (e.g. MPIN screen) so that riders cannot reach the homepage before // successfully entering a valid PIN. } return Login.fromJson(jsonMap); } Future updatePin({ required int userId, required int pin, }) async { // ── There is no rider-facing set-PIN endpoint, and that is deliberate ── // // The only PIN-write route on the backend is `POST /miler/reset-pin`, and // it requires an ADMIN token. It was once open, and reset-pin followed by // verify-pin took over any rider account given nothing but a phone number. // The app must not call it; rider PIN resets go through ops. // // So this stays a no-op success: the Create-MPIN screen's flow completes // and the PIN the account was issued with remains the one that works. // Making it fail instead would strand a rider on a screen with no way // forward, which is worse and no more honest. ApiConfig.logGap( 'updatePin', 'No rider-facing set-PIN route; reset-pin is admin-only by design.', ); return http.Response( json.encode(ApiConfig.okEnvelope('pin is managed by ops')), 200, headers: {'content-type': 'application/json'}, ); } }