updated the sheet

This commit is contained in:
2026-08-28 18:16:28 +05:30
parent 69f4f3e909
commit 074cc0eccf
43 changed files with 2617 additions and 1201 deletions

View File

@@ -3,15 +3,15 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:audioplayers/audioplayers.dart';
import 'dart:math' as math;
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/device_telemetry.dart';
import 'package:miler/providers/notifications/notificationservce.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/utils/kalman_filter.dart';
/// Background service for managing active pickup logs
@@ -20,7 +20,6 @@ class BackgroundCollectionLog {
static DateTime? _lastUpdateTime;
static final CreatePickupLogProvider _logProvider = CreatePickupLogProvider();
static final http.Client _httpClient = http.Client();
static final FlutterTts _tts = FlutterTts();
static final AudioPlayer _proximityPlayer = AudioPlayer();
@@ -60,38 +59,39 @@ class BackgroundCollectionLog {
final now = DateTime.now();
final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}';
// Build API URL using v1 endpoint (getpickup)
final bool isLive = ApiConstants.mainRoute == 'live';
final baseUrl = isLive
? ApiConstants.currentPickupLive
: ApiConstants.currentPickupDev;
final uri = Uri.parse(baseUrl).replace(
queryParameters: {
'userid': userId.toString(),
'fromdate': today,
'todate': today,
't': DateTime.now().millisecondsSinceEpoch.toString(),
},
);
debugPrint('[ACTIVE_COLLECTION_LOG][BG] Fetching Pickup: $uri');
// Fetch pickup from API using v1 endpoint
final pickups = await _fetchPickupsFromApi(uri);
// ── This asked the pre-v1 backend, with the wrong question ──
//
// It built a `pickups/getpickups` URL on the retired backend by hand
// and fetched it with a bare `_httpClient.get` — **no bearer token**, and
// `userid` in the query string, so the rider's day was readable by id
// from an unauthenticated call.
//
// It could never have worked anyway. The filter below looked for
// `orderstatus == 'active'`, and `active` is not a value this app's own
// vocabulary contains: [ApiConfig.legacyStatusFromNew] maps the v1
// statuses to `assigned` / `accepted` / `arrived` / `picked`, and never to
// `active`. So on every tick this returned 0 items and fell through to
// the shift-end check — which is exactly what the device log shows.
//
// `GET /miler/bookings` is the v1 question, asked with the token, about
// the rider the token names. The filter is the real vocabulary: work the
// rider has taken on and not yet collected.
final res = await MilerApi.bookings(date: today);
final pickups = res.list;
debugPrint(
'[ACTIVE_COLLECTION_LOG][BG] API returned ${pickups.length} items',
'[ACTIVE_COLLECTION_LOG][BG] /miler/bookings -> ${res.status}, '
'${pickups.length} items',
);
// Filter for active bookings only
const inProgress = {'accepted', 'arrived'};
final activeOrders = pickups.whereType<Map<String, dynamic>>().where((
Booking,
) {
final status = (Booking['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
final isActive = status == 'active';
return isActive;
final status = ApiConfig.legacyStatusFromNew(
(Booking['status'] ?? Booking['orderstatus'])?.toString(),
);
return inProgress.contains(status);
}).toList();
if (activeOrders.isEmpty) {
@@ -189,7 +189,9 @@ class BackgroundCollectionLog {
// so every per-stop breadcrumb reached the console with a position and
// nothing to explain a gap in the trail. Read from the same place the
// rider heartbeat reads it. See [DeviceTelemetry].
final telemetry = await DeviceTelemetry.read(locationService: coords['status']);
final telemetry = await DeviceTelemetry.read(
locationService: coords['status'],
);
// Build payload with coordinates and timestamp
payloadWithCoords = <String, dynamic>{
@@ -211,11 +213,6 @@ class BackgroundCollectionLog {
'logstatus': await _resolveLogStatus(orderId, coords),
};
// Determine API endpoint
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createPickupLogLive
: ApiConstants.createPickupLogDev;
// Post the log
debugPrint('[ACTIVE_COLLECTION_LOG][BG] Posting pickup log for $orderId');
@@ -427,33 +424,6 @@ class BackgroundCollectionLog {
}
/// Fetch pickup from API using v1 endpoint
static Future<List<dynamic>> _fetchPickupsFromApi(Uri uri) async {
try {
final res = await _httpClient
.get(uri)
.timeout(const Duration(seconds: 10));
if (res.statusCode >= 200 && res.statusCode < 300) {
final decoded = json.decode(res.body);
final data = decoded is Map<String, dynamic>
? (decoded['details'] ?? decoded['data'] ?? decoded)
: decoded;
if (data is List) {
return data;
}
if (data is Map && data['items'] is List) {
return data['items'] as List;
}
return [];
}
return [];
} catch (e) {
return [];
}
}
/// Helper to pad numbers with leading zero
static String _pad(int n) => n.toString().padLeft(2, '0');
@@ -533,10 +503,6 @@ class BackgroundCollectionLog {
final List<String> remaining = [];
bool anySuccess = false;
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createPickupLogLive
: ApiConstants.createPickupLogDev;
for (final itemStr in queue) {
try {
final Map<String, dynamic> item = jsonDecode(itemStr);
@@ -761,14 +727,18 @@ class BackgroundCollectionLog {
'[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...',
);
// 1. Get Required IDs
// ── The four ids and the fix are no longer this function's business ──
//
// `userid`, `partnerid`, `shiftid`, `logid` and a position were assembled
// here to fill a legacy break-log body. On v1 the break and the duty end
// are both keyed on the **token**: `POST /miler/breaks/start` opens a
// break for whoever the session names, and `PUT /miler/duty/end` closes
// whichever duty log is open for them. Sending an id would not make it
// more correct; it is what let the old call be made unauthenticated.
//
// `userid` is still read, as the one guard worth keeping: no signed-in
// rider, nothing to end.
final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
final int partnerid =
prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0;
final int shiftid =
prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0;
final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0;
if (userid == 0) {
debugPrint(
'[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log',
@@ -776,61 +746,28 @@ class BackgroundCollectionLog {
return;
}
// 2. Get Location
final Map<String, String>? coords = await _getCoordinatesWithFallback();
final String lat = coords?['lat'] ?? '0';
final String lng = coords?['lng'] ?? '0';
// 3. Prepare Break Log Payload
final now = DateTime.now();
final int localBreakId =
(DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID
final String breakdate =
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final String breakstart =
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final payload = <String, dynamic>{
"breakid": localBreakId,
"logid": logid,
"breakdate": breakdate,
"userid": userid,
"partnerid": partnerid,
"shiftid": shiftid,
"breakstart": breakstart,
"breakend": "",
"breakhours": 0.0,
"latitude": lat,
"longitude": lng,
};
// 4. Call API to Create Break
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
debugPrint('[AUTO_SHIFT_END] Creating break log: $url');
// We use a separate provider instance or http call if needed,
// but _logProvider is for pickup logs. We need a generic post or use http directly.
// Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation.
// 4. Close the shift with a break, on v1.
//
// This hand-posted the retired backend's `partners/createbreaklog`
// with a bare `_httpClient.post` and no bearer
// token — an unauthenticated write keyed on a `userid` in the
// body. `POST /miler/breaks/start` is the v1 endpoint, and
// [MilerApi] attaches the session.
debugPrint('[AUTO_SHIFT_END] Creating break log (v1)');
try {
final response = await _httpClient
.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
)
.timeout(const Duration(seconds: 10));
final res = await MilerApi.startBreak('Shift_End');
if (response.statusCode >= 200 && response.statusCode < 300) {
if (res.ok) {
debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully');
// Parse response to get server break ID if needed, but mainly we just need to go offline
final body = jsonDecode(response.body);
final det = (body['details'] is Map) ? body['details'] : body;
final serverBreakId = det['breakid'];
final serverBreakId = res.map['breaklogid'] ?? res.map['breakid'];
if (serverBreakId != null) {
await prefs.setInt(
@@ -842,7 +779,7 @@ class BackgroundCollectionLog {
await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch);
} else {
debugPrint(
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}',
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${res.status}',
);
}
} catch (e) {
@@ -854,27 +791,15 @@ class BackgroundCollectionLog {
await prefs.setInt('onduty', 0);
await prefs.setBool('online', false);
// 6. Update Rider Log (Set Duty = 0)
// We should also update the main rider log to say onduty=0
final updateUrl = ApiConstants.mainRoute == 'live'
? ApiConstants.updateRiderLogLive
: ApiConstants.updateRiderLogDev;
final updatePayload = {
"userid": userid,
"onduty": 0,
"latitude": lat,
"longitude": lng,
};
// 6. End duty, on v1.
//
// Was an unauthenticated POST of `{userid, onduty: 0}` to the
// legacy `updateriderlog`. `PUT /miler/duty/end` closes whichever
// duty log is open for the token's rider, which is the same fact
// without the id or the open door.
try {
await _httpClient
.post(
Uri.parse(updateUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(updatePayload),
)
.timeout(const Duration(seconds: 5));
await MilerApi.endDuty();
debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline');
} catch (_) {}

View File

@@ -6,11 +6,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'dart:math' as math;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/providers/Riderlog/riderlog_provider.dart';
import 'package:miler/background/backgroundservice.dart';
import 'package:geolocator/geolocator.dart';
import 'package:miler/data/device_telemetry.dart';
import 'package:miler/data/heartbeat.dart';
import 'package:miler/utils/kalman_filter.dart';
import 'package:miler/utils/mqtt_service.dart';
import 'package:miler/views/helpers/constants/mqtt_constants.dart';
@@ -149,7 +149,7 @@ class _BackgroundRiderLog {
// ✅ Check if there are active pickup to set status
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
final String riderStatus = hasActivePickups ? 'active' : 'idle';
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
final now = DateTime.now();
final iso = _formatDateTimeFull(now);
@@ -226,9 +226,6 @@ class _BackgroundRiderLog {
payload['lastname'] = lastName.trim();
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
final provider = CreateRiderLogProvider();
final resp = await provider.createRiderLog(payload);
@@ -304,7 +301,7 @@ class _BackgroundRiderLog {
// 5. Lane: Status
mqttService.updateStatus(
riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline,
riderStatus == 'On_Pickup' ? 'Active' : MqttConstants.statusOnline,
);
// 6. Lane: Periodic Log (Comprehensive Snapshot)

View File

@@ -9,7 +9,6 @@ import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/background/foreground_service.dart' as fg;
import 'package:geolocator/geolocator.dart';

View File

@@ -12,7 +12,6 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:geolocator/geolocator.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
import 'dart:io';
import 'package:minio/minio.dart';
@@ -1203,12 +1202,6 @@ class PickupsController extends GetxController
return "$y-$m-$d $hh:$mm:$ss";
}
String _resolveUpdateUrl() {
return ApiConstants.mainRoute == 'live'
? ApiConstants.updatePickupLive
: ApiConstants.updatePickupDev;
}
// ---------------- SharedPrefs helpers ----------------
Future<void> setNotificationSent(bool value) async {
try {

View File

@@ -5,7 +5,7 @@ import 'package:miler/data/miler_api.dart';
///
/// ── Two things this used to do and no longer does ──
///
/// It called a second, legacy backend (`jupiter.doormile.app/.../
/// It called a second, legacy backend (the retired `.../
/// getuserbonussummary`) when the new-API flag was off, and it filled an empty
/// balance with seeded demo points in debug builds. Both are gone: there is one
/// backend now, and a points balance is a number about somebody's pay — the

View File

@@ -1,7 +1,6 @@
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/Models/riders/riders_models.dart';
import 'package:miler/providers/Riderlog/riderlog_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -16,6 +15,7 @@ import 'package:miler/background/foreground_service.dart' as fg;
import 'package:miler/utils/kalman_filter.dart';
import 'package:miler/utils/mqtt_service.dart';
import 'package:miler/data/device_telemetry.dart';
import 'package:miler/data/heartbeat.dart';
import 'package:miler/views/helpers/constants/mqtt_constants.dart';
import 'package:battery_plus/battery_plus.dart';
import 'package:miler/controllers/connectivity_mixin.dart';
@@ -235,7 +235,7 @@ class RiderLogController extends GetxController
// ✅ Check if there are active pickup to set status
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
final String riderStatus = hasActivePickups ? 'active' : 'idle';
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
final String orderId = prefs.getString('current_riding_order_id') ?? '';
debugPrint(
'[RIDERLOG][CREATE LOGIN NOW] Active pickup: $hasActivePickups -> status: $riderStatus',
@@ -315,7 +315,7 @@ class RiderLogController extends GetxController
// 1. Lane: Status
mqttService.updateStatus(
riderStatus == 'active' ? 'Active' : MqttConstants.statusOnline,
riderStatus == 'On_Pickup' ? 'Active' : MqttConstants.statusOnline,
);
// 2. Lane: Profile (Send only if significantly changed or first time)
@@ -448,7 +448,7 @@ class RiderLogController extends GetxController
// Check status for offline fallback too
final bool hasActivePickups =
prefs.getBool('has_live_pickup') ?? false;
final String riderStatus = hasActivePickups ? 'active' : 'idle';
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
final Map<String, dynamic> payload = {
...baseTemplate,
@@ -520,10 +520,19 @@ class RiderLogController extends GetxController
await prefs.setInt('onduty', on ? 1 : 0);
if (on) {
await createLoginNowV2();
final int interval = prefs.getInt('logseconds') ?? 0;
if (interval > 0) {
startAutoCreateLoginLoop(seconds: interval);
}
// ── Going on duty always starts the heartbeat ──
//
// This was gated on `interval > 0`, reading a stored zero as a
// decision not to beat. The v1 login has no `logseconds` to store, so
// the gate was shut on every rider: duty went on, one log was posted
// by the call above, and nothing followed it — no telemetry trail and
// no location writes for the rest of the shift.
//
// There is no cadence at which a rider on duty should not report, so
// there is no branch here any more. [resolveLogSeconds] supplies the
// floor when the hub has not set one.
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
startAutoCreateLoginLoop(seconds: interval);
// Ensure foreground logging notification is started when going on-duty
try {
@@ -590,15 +599,45 @@ class RiderLogController extends GetxController
}
}
/// Whether the periodic heartbeat is currently running on the stream path.
///
/// ── Why a test needs to be able to see this ──
///
/// The `logseconds` bug was invisible from outside the class. Duty went on,
/// the API accepted it, one log was posted by the immediate call — and the
/// loop that was supposed to follow it simply never started. Nothing threw,
/// nothing logged an error, and the only symptom was a console panel that had
/// been blank for so long it read as a backend problem.
///
/// Android normally runs the heartbeat as a foreground service instead, and
/// leaves this null; the test host is never Android, so the stream path is
/// the one under test. See [startAutoCreateLoginLoop].
@visibleForTesting
bool get autoLoopRunning => _autoLoginSubscription != null;
// Start periodic createRiderLog calls based on seconds (or prefs 'logseconds')
// ✅ CRITICAL: When there are active pickup, use 30 seconds (same as pickup logs)
// Otherwise, use the configured logseconds interval
void startAutoCreateLoginLoop({int? seconds}) async {
/// `Future<void>`, not `void`. It was fire-and-forget, which meant no caller
/// could wait for the loop to actually be up — and the one place that most
/// wanted to, [setOnDuty], had no way to tell whether the heartbeat it had
/// just asked for existed. Every existing call site ignores the future and is
/// unchanged by this.
Future<void> startAutoCreateLoginLoop({int? seconds}) async {
final prefs = await SharedPreferences.getInstance();
// ✅ Check if there are active pickup - if yes, use 30 seconds (same as pickup logs)
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
final int baseInterval = seconds ?? (prefs.getInt('logseconds') ?? 0);
// ── The configured cadence, or the floor under it ──
//
// `?? 0` here was the second half of the same bug as the one in
// [setOnDuty]: a caller that passes no `seconds` — the app-resume path in
// `main.dart` is one — fell through to a stored zero and then returned at
// the `interval <= 0` guard below, so the loop the resume was trying to
// revive silently did not start. See [resolveLogSeconds].
final int baseInterval = resolveLogSeconds(
seconds ?? prefs.getInt('logseconds'),
);
// When there are active pickup, post rider logs every 30 seconds (matching pickup logs)
// Otherwise, use the configured interval
@@ -610,12 +649,20 @@ class RiderLogController extends GetxController
);
}
await stopAutoCreateLoginLoop(); // Cancel existing subscription
// Stop before start, which is what keeps this method safe to call from the
// three places that do — going on duty, resuming the app, and a pickup
// going live. Whichever arrives last wins, and there is never a second
// loop: the subscription is cancelled and the foreground service stopped
// before either is created again.
await stopAutoCreateLoginLoop();
// Attempt to flush offline logs on loop start
flushOfflineLogs();
if (interval <= 0) return;
// No `interval <= 0` guard. [resolveLogSeconds] cannot return one, and the
// guard that used to be here is what turned a missing `logseconds` into a
// rider who never reported. Duty state decides whether the loop runs; the
// cadence only decides how often.
final int onduty = prefs.getInt('onduty') ?? 0;
if (onduty != 1) {
debugPrint('[RIDERLOG][AUTO LOOP] Not starting - onduty=$onduty');
@@ -1049,9 +1096,6 @@ class RiderLogController extends GetxController
final breakhoursDouble = duration.inSeconds / 3600.0;
// Debug logs for terminal visibility
debugPrint(
'[BREAK][UPDATE] URL: ${ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev}',
);
debugPrint(
'[BREAK][UPDATE] Fields: breakid=$breakid, logid=$logid, userid=$userid, partnerid=$partnerid, shiftid=$shiftid, breakend=$breakend, breakhours=$breakhoursDouble, lat=$latitude, lng=$longitude',
);
@@ -1107,7 +1151,7 @@ class RiderLogController extends GetxController
// ✅ Check if there are active pickup to set status
final bool hasActivePickups = prefs.getBool('has_live_pickup') ?? false;
final String riderStatus = hasActivePickups ? 'active' : 'idle';
final String riderStatus = heartbeatStatus(hasActiveWork: hasActivePickups);
// Resolve rider display name (username) from prefs to include in log
String? username = prefs.getString('username');

View File

@@ -13,7 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart';
/// ── The flag is gone ──
///
/// This class used to carry `useNewApi`, a `--dart-define` switch between the
/// v1 backend and a legacy one (`jupiter.doormile.app` / `queue.workolik.com`).
/// v1 backend and a legacy one, on two now-retired hosts.
/// Every provider branched on it, so the app shipped two implementations of
/// every call and only one of them was ever exercised.
///

View File

@@ -46,10 +46,11 @@ class DeviceTelemetry {
/// problem as a rider at 12% and riding.
final bool? isCharging;
/// `wifi`, `mobile`, `none` … as the platform names it.
/// `WiFi`, `4G` or `none` — the contract's vocabulary, not the plugin's.
/// See [_connectionName].
final String? connection;
/// `enabled`, `disabled`, `denied`, `denied_forever`, `unknown`.
/// `enabled` or `disabled`. See [_locationServiceName].
///
/// The one field here that is about a *choice the rider made*, which is why
/// it is worth a column of its own on the console: a disabled location
@@ -86,9 +87,7 @@ class DeviceTelemetry {
String? connection;
try {
final result = await Connectivity().checkConnectivity();
connection = result.isNotEmpty
? result.first.toString().split('.').last
: 'none';
connection = _connectionName(result);
} catch (e) {
debugPrint('[TELEMETRY] connectivity unavailable: $e');
}
@@ -104,6 +103,7 @@ class DeviceTelemetry {
service = null;
}
}
service = _locationServiceName(service);
return DeviceTelemetry(
battery: level,
@@ -113,6 +113,48 @@ class DeviceTelemetry {
);
}
/// `WiFi` / `4G` / `none`, which is the vocabulary `/miler/logs` documents.
///
/// ── The plugin's own spelling is not the contract's ──
///
/// This was `result.first.toString().split('.').last`, which is the *enum
/// constant* — `wifi`, `mobile`, `ethernet`, `vpn`, `bluetooth`, `other`.
/// Two of those happen to look right in lower case and the rest do not
/// appear in the contract at all, so the console's Connection column was
/// rendering whatever `connectivity_plus` happened to call the transport
/// this release.
///
/// `first` was also the wrong pick: the list is every active transport, so a
/// phone on Wi-Fi with mobile data up could report either depending on the
/// order the platform returned them. Wi-Fi wins where both are present,
/// because it is the one that explains a rider whose data has run out.
static String _connectionName(List<ConnectivityResult> results) {
if (results.isEmpty) return 'none';
if (results.contains(ConnectivityResult.wifi)) return 'WiFi';
if (results.contains(ConnectivityResult.mobile)) return '4G';
if (results.contains(ConnectivityResult.ethernet)) return 'ethernet';
if (results.every((r) => r == ConnectivityResult.none)) return 'none';
return 'other';
}
/// `enabled` or `disabled` — the two values the contract names.
///
/// The richer answers the fix lookup produces — `denied`, `denied_forever`,
/// `unknown` — are more useful to a dispatcher, and they are not what this
/// field accepts. On a backend that validates its enums (and this one does
/// silently: see the `Break` / `On_Break` note in [MilerApi]) an unrecognised
/// value risks the whole row, which costs the battery and the connection
/// alongside it.
///
/// So anything that is not `enabled` is `disabled` here, which is the fact
/// the column exists to report — this rider's location is not reaching us.
/// The distinction between *off* and *denied* is not lost: it still rides on
/// the MQTT `location_turned_off` alert as `error_type`.
static String? _locationServiceName(String? raw) {
if (raw == null || raw.isEmpty) return null;
return raw == 'enabled' ? 'enabled' : 'disabled';
}
/// The heartbeat payload's own field names, ready to spread into it.
///
/// Only what was actually read: an absent key is a field the console draws as

89
lib/data/heartbeat.dart Normal file
View File

@@ -0,0 +1,89 @@
/// ─────────────────────────────────────────────────────────────────────────
/// HOW OFTEN THE RIDER LOG BEATS
///
/// ── Why this constant exists ──
///
/// The heartbeat's cadence came from one place: `logseconds`, a field the
/// **legacy** login returned and the app persisted at sign-in. The v1 contract
/// at `api.doormile.com` does not send it — `POST /miler/verify-pin` answers
/// with `{success, token, user:{…, profile:{…}}}` and nothing about logging
/// cadence — so the adapter in `AuthProvider` had no value to map and wrote the
/// `?? 0` fallback into prefs.
///
/// Zero was then read as an instruction rather than as an absence. Both call
/// sites treated it as "do not beat":
///
/// • `RiderLogController.setOnDuty` started the loop only `if (interval > 0)`.
/// • `startAutoCreateLoginLoop` computed `baseInterval = 0` and returned at
/// `if (interval <= 0)`.
///
/// So on every v1 deployment the periodic loop never started. What went with
/// it was not just telemetry: `_heartbeat()` in the rider-log provider is the
/// one place that writes **both** `POST /miler/logs` (the trail the console's
/// Battery, Charging, Connection, GPS Accuracy and Location Service columns are
/// read from) and `PUT /miler/location` (the Redis geo-index dispatch searches
/// to find a rider at all). A rider on duty was reporting neither, and the
/// console drew an em dash against a phone that was measuring all of it
/// correctly — see [DeviceTelemetry], which was never the problem.
///
/// The only reason it was not total silence is that a live pickup forces the
/// interval to 30 by a separate path, so a rider mid-collection beat and a
/// rider between stops did not.
///
/// ── Why 30 seconds ──
///
/// It is the cadence the rest of the app already assumes when nobody has told
/// it otherwise: the pickup log's own `_getLogInterval` falls back to 30, and
/// both controllers hard-code 30 for the live-pickup case. Matching it means a
/// rider's location trail has one shape rather than two, and a hub that later
/// starts sending `logseconds` still wins — this is a floor under a missing
/// answer, not a replacement for a real one.
/// ─────────────────────────────────────────────────────────────────────────
library;
/// Seconds between rider-log heartbeats when the backend has not said.
const int kDefaultLogSeconds = 30;
/// The heartbeat interval to actually use, given whatever the backend said.
///
/// A positive value is honoured exactly. `null`, a zero, a negative, and a
/// string that is none of those all mean *the backend did not answer*, and the
/// answer to that is [kDefaultLogSeconds] — never zero, because zero is what
/// stopped the heartbeat starting in the first place.
///
/// Accepts an [Object] rather than an `int?` because the value arrives from two
/// different shapes: `prefs.getInt('logseconds')` gives an `int?`, while the
/// login envelope's `details['logseconds']` is untyped JSON and has been seen
/// as both a number and a string.
int resolveLogSeconds(Object? configured) {
final int? parsed = switch (configured) {
final int n => n,
final num n => n.toInt(),
final String s => int.tryParse(s.trim()),
_ => null,
};
return (parsed != null && parsed > 0) ? parsed : kDefaultLogSeconds;
}
/// The `status` a heartbeat reports, given whether the rider has live work.
///
/// ── `active` and `idle` are not words this contract knows ──
///
/// Four call sites built this string inline as `hasActivePickups ? 'active' :
/// 'idle'`, and neither value appears anywhere in the API. `status` on
/// `POST /miler/logs` takes an **availability** value — the set
/// [MilerApi.availabilityStatuses] lists, and the same set
/// `PUT /miler/availability` validates:
///
/// Offline · Available · Assigned · On_Pickup · At_Customer ·
/// Picked_Up · On_Delivery · Break · Blocked
///
/// This backend rejects an unrecognised enum **silently** — the `Break` /
/// `On_Break` note in [MilerApi] records the last time the obvious guess was
/// quietly dropped — so a bad `status` risks the whole row, and takes the
/// battery, the connection and the location reading down with it.
///
/// A rider working a counter is `On_Pickup`; a rider between stops is
/// `Available`. Both are values the console already renders.
String heartbeatStatus({required bool hasActiveWork}) =>
hasActiveWork ? 'On_Pickup' : 'Available';

View File

@@ -135,9 +135,32 @@ class MilerApi {
/// Telemetry wants strings for values that are numbers everywhere else.
static String _str(dynamic v) => v == null ? '' : v.toString();
/// `YYYY-MM-DD HH:MM:SS`, IST wall-clock, for telemetry `logdate`.
/// `YYYY-MM-DD HH:MM:SS`, the phone's **local wall clock**, for `logdate`.
///
/// ── This was briefly changed to UTC, and that was wrong ──
///
/// The reasoning looked sound: the field carries no zone suffix, so one side
/// has to name the convention, and UTC is the usual answer. It is not the
/// answer here. The Doormile backend's read range — `DBNow` / `DBToday` — and
/// the database itself run on **IST wall-clock**, so a UTC stamp from an
/// Indian handset lands 5h30m *behind* the window the console asks for, and
/// after 18:30 local it files the evening's work under the previous day.
///
/// Confirmed against the live backend on 28 Aug 2026, after the UTC change
/// had already shipped to a test build. It went unnoticed there because that
/// handset's own clock was running on UTC, so local and UTC agreed and the
/// rows landed in the window anyway.
///
/// So: local, and the phone's timezone is assumed to be the fleet's. That is
/// true for every Doormile Miler today. The durable fix is an offset on the
/// wire — an ISO-8601 stamp the backend parses with its zone — and until the
/// contract carries one, this is the convention both sides have agreed.
static String logStamp([DateTime? at]) {
final t = at ?? DateTime.now();
// `.toLocal()` because a caller can hand this a UTC `DateTime` — the
// consignment breadcrumb does — and reading `.hour` off one of those
// renders the UTC wall clock, which is the bug this method just came back
// from. A local `DateTime` passes through unchanged.
final t = (at ?? DateTime.now()).toLocal();
String p(int n) => n.toString().padLeft(2, '0');
return '${t.year}-${p(t.month)}-${p(t.day)} '
'${p(t.hour)}:${p(t.minute)}:${p(t.second)}';

View File

@@ -12,10 +12,9 @@ import 'dart:io';
/// day's cash figures without a word.
///
/// It is the kind of thing that gets added once to get past a staging server
/// and then ships. Both live hosts were checked before removing it and both
/// present valid, publicly-trusted certificates — `api.doormile.com` and
/// `queue.workolik.com` verify strictly under TLS 1.3 against Let's Encrypt.
/// Nothing needed the bypass.
/// and then ships. The live host was checked before removing it:
/// `api.doormile.com` verifies strictly under TLS 1.3 against a publicly
/// trusted certificate. Nothing needed the bypass.
///
/// ── What is left, and why the class is still here ──
///

View File

@@ -11,7 +11,7 @@ import 'package:miler/data/miler_api.dart';
///
/// A `_buildSslBypassClient()` that returned an `IOClient` whose
/// `badCertificateCallback` returned `true` for every certificate, pinned a
/// hardcoded IP for `queue.workolik.com`, and hand-rolled its own
/// hardcoded IP for the retired backend's host, and hand-rolled its own
/// `SecureSocket` upgrade. It existed to work around a carrier's broken DNS on
/// a backend that is no longer deployed — and it meant every request it carried
/// could be read and rewritten by anything on the path, including a rider's GPS
@@ -33,6 +33,132 @@ String _s(dynamic v) => v == null ? '' : v.toString();
double? _d(dynamic v) => double.tryParse(_s(v));
/// ─────────────────────────────────────────────────────────────────────────
/// WHY THE CONSOLE'S DEVICE PANEL WAS EMPTY
///
/// Not the telemetry. [DeviceTelemetry] reads the handset correctly, its
/// payload carries every field the console draws, and every caller spreads it
/// into the heartbeat. The reading was right and it never left the phone.
///
/// [_heartbeat] is the only thing that posts `POST /miler/logs`, and it is also
/// the only thing that writes `PUT /miler/location`. Both were unreachable,
/// because the two entry points below gate on one locally cached number:
///
/// ```
/// if ((prefs.getInt('dutylogid') ?? 0) <= 0) return _startDuty(data);
/// return _heartbeat(data);
/// ```
///
/// `dutylogid` is written in exactly one place — a successful
/// `POST /miler/duty/start` — and it is zeroed on going off duty. So there are
/// two ordinary ways to end up on duty with a zero, and both are permanent:
///
/// • **The server already has the rider on duty.** `duty/start` answers 400,
/// which [_startDuty] correctly treats as a reconciliation rather than a
/// failure and delegates to [_currentDuty] — which read `dutylogid` out of
/// the response and wrote it to `logid` and `logId` **and not to
/// `dutylogid`**. The gate stayed shut. Next tick: `duty/start` again, 400
/// again, reconcile again, still shut. Forever, and silently, because
/// nothing in that loop is an error. It happens after a reinstall, after
/// cleared storage, after an `endDuty` that never landed, and on any fresh
/// login for a rider the backend still has on shift.
///
/// • **The response spells the id differently.** [_startDuty] read one key on
/// one level. Anything else — `logid`, `id`, a top-level field rather than
/// one under `data` — parsed to zero and was *stored* as zero, which puts a
/// rider into the same permanent loop on his very first shift. This codebase
/// has been bitten by exactly this before; see `AuthProvider._tokenIn`,
/// which looks in nine places for the bearer for precisely this reason.
///
/// ── The design error underneath both ──
///
/// `POST /miler/logs` does not take a duty log id. Its contract is
/// `{logdate, latitude, longitude, battery, is_charging, connection,
/// location_service, accuracy, is_background}` — nothing about duty. Gating
/// telemetry on an identifier the endpoint does not want, that the backend is
/// not obliged to return, is what turned one unmapped field into total
/// silence on both writes.
///
/// So the gate is now *"has duty been established"* — a fact — rather than
/// *"do we happen to hold a non-zero id"*, which is an artefact. The id is
/// still stored when it arrives, because [_dutyEnvelope] and the legacy duty
/// readers want it; it simply no longer decides whether a rider reports.
/// ─────────────────────────────────────────────────────────────────────────
/// Set once duty is known to be on, and cleared when it goes off.
///
/// Deliberately separate from `dutylogid`: this answers "should the heartbeat
/// run", which is a question about duty, and the id answers "which duty log
/// row", which is a question about the backend's bookkeeping. Conflating them
/// is the bug above.
const String _kDutyEstablished = 'duty_established';
/// Every spelling and nesting the duty endpoints have used for the log id.
///
/// Ordered by how the current contract answers, then by what deployments have
/// been seen to send. A miss is no longer fatal — see [_dutyEstablished] — but
/// finding the id keeps the legacy `logid` readers correct.
const List<String> _dutyLogIdKeys = [
'dutylogid',
'dutyLogId',
'duty_log_id',
'logid',
'logId',
'log_id',
'id',
];
/// The duty log id anywhere in [res], or `0`.
///
/// Looks in the envelope's `data` and in the undecorated body, because a
/// handler that returns `{success, dutylogid}` and one that returns
/// `{success, data:{dutylogid}}` are both shapes this contract has worn.
int _dutyLogIdIn(ApiResult res) {
final sources = <Map>[res.map, if (res.raw is Map) res.raw as Map];
for (final source in sources) {
for (final key in _dutyLogIdKeys) {
final v = source[key];
if (v == null) continue;
final n = int.tryParse(v.toString().trim());
if (n != null && n > 0) return n;
}
}
return 0;
}
/// Records that the rider is on duty, and the id if the backend sent one.
///
/// The flag is set whether or not there is an id, which is the whole fix: a
/// rider whose duty the server has confirmed reports, even when the response
/// did not name the row.
Future<void> _rememberDuty(int dutyLogId) async {
final prefs = await SharedPreferences.getInstance();
if (dutyLogId > 0) {
await prefs.setInt('logid', dutyLogId);
await prefs.setInt('logId', dutyLogId);
await prefs.setInt('dutylogid', dutyLogId);
}
await prefs.setBool(_kDutyEstablished, true);
await prefs.setInt('onduty', 1);
}
/// Forgets duty, on the way off shift.
Future<void> _forgetDuty() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('dutylogid', 0);
await prefs.setBool(_kDutyEstablished, false);
}
/// Whether duty has been established, so the heartbeat should run.
Future<bool> _dutyEstablished() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool(_kDutyEstablished) == true) return true;
// An install that predates the flag: a stored id means duty was started by
// an earlier build, and that rider must not have to go off and on again to
// start reporting.
return (prefs.getInt('dutylogid') ?? 0) > 0;
}
/// Legacy `{status, details}` for the duty-state readers.
Map<String, dynamic> _dutyEnvelope(Map data) {
final bool on = data['onduty'] == true;
@@ -54,11 +180,11 @@ Future<Map<String, dynamic>?> _startDuty(Map data) async {
lon: _d(data['longitude']),
);
if (res.ok) {
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('logid', id);
await prefs.setInt('logId', id);
await prefs.setInt('dutylogid', id);
// Looked at one key on one level and stored whatever it found, including
// a zero — which shut the heartbeat's gate permanently on a response that
// had merely spelled the id differently. See [_dutyLogIdIn].
final id = _dutyLogIdIn(res);
await _rememberDuty(id);
return {
'status': true,
'code': 200,
@@ -76,12 +202,19 @@ Future<Map<String, dynamic>?> _currentDuty() async {
if (!res.ok) return {'status': false, 'code': res.status};
final on = res.map['onduty'] == true;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', on ? 1 : 0);
if (on) {
final id = int.tryParse('${res.map['dutylogid'] ?? 0}') ?? 0;
await prefs.setInt('logid', id);
await prefs.setInt('logId', id);
// ── The line that was missing ──
//
// This wrote the id to `logid` and `logId` and stopped, leaving
// `dutylogid` — the only key the heartbeat's gate reads — at zero. So the
// reconciliation that exists to recover from "the server already has you
// on duty" recovered the duty state and not the ability to report it, and
// the next tick came straight back here. See the note at the top.
await _rememberDuty(_dutyLogIdIn(res));
} else {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', 0);
await _forgetDuty();
}
return _dutyEnvelope(res.map);
}
@@ -108,11 +241,28 @@ Future<Map<String, dynamic>?> _heartbeat(Map data) async {
heading: _d(data['heading']),
);
// Deliberately not awaited into the result: a telemetry failure must never
// look like a location failure, because the caller treats the latter as duty
// state going wrong.
unawaited(
MilerApi.postLog(
// ── Awaited, but its outcome is still discarded ──
//
// This was `unawaited(...)`, for a good reason that turned out to be the
// wrong mechanism: a telemetry failure must never read as a location failure,
// because the caller treats the latter as duty state going wrong. `unawaited`
// achieves that by never looking at the result — and also by not waiting for
// the request to finish.
//
// On Android this heartbeat runs inside `flutter_foreground_task`'s **own
// Flutter engine**, spun up per tick. When the callback returns, that engine
// can be suspended before an in-flight future completes. So the awaited
// `PUT /miler/location` above landed on every tick and this `POST /miler/logs`
// was killed mid-flight — which is exactly what the backend saw: a rider with
// a live position in Redis and no new telemetry row behind it, while the app
// logged a successful heartbeat.
//
// Awaiting it costs one round trip on a call that already had to happen and
// guarantees the request outlives the tick. The isolation the `unawaited` was
// protecting is kept by the `try`: the result is logged and thrown away, and
// nothing below branches on it.
try {
final logRes = await MilerApi.postLog(
latitude: lat,
longitude: lon,
speed: _d(data['speed']),
@@ -137,8 +287,18 @@ Future<Map<String, dynamic>?> _heartbeat(Map data) async {
? null
: _s(data['location_service']),
isBackground: data['is_background'] == true,
),
);
);
// The one line that says whether telemetry reached the console. It was
// silent on success *and* on failure, which is why a dropped post took
// several rounds of backend archaeology to find.
debugPrint(
'[RIDERLOG][LOGS] POST /miler/logs -> ${logRes.status} '
'ok=${logRes.ok} battery=${_s(data['battery'])} '
'connection=${_s(data['connection'])}',
);
} catch (e) {
debugPrint('[RIDERLOG][LOGS] POST /miler/logs failed: $e');
}
return res.ok
? ApiConfig.okEnvelope()
@@ -158,13 +318,28 @@ bool _isOffDutyIntent(Map data) {
class CreateRiderLogProvider {
/// The first call of a session starts duty; every later one is a heartbeat.
///
/// ── The start is no longer a tick the rider loses ──
///
/// This returned [_startDuty]'s envelope and stopped, so the tick that opened
/// the shift carried no telemetry and no position — the console had nothing
/// until the next one, thirty seconds later. Worse, when `duty/start`
/// reconciled a rider the server already had on duty, the old gate never
/// opened at all and *every* tick ended here. See the note at the top of this
/// file.
///
/// Duty is established first, then the same payload beats immediately. The
/// heartbeat is best-effort and its result is not merged into the duty
/// envelope, because the caller reads that envelope for duty state and a
/// telemetry failure is not a duty failure.
Future<Map<String, dynamic>?> createRiderLog(
Map<String, dynamic> data,
) async {
final prefs = await SharedPreferences.getInstance();
final int existing = prefs.getInt('dutylogid') ?? 0;
if (existing <= 0) return _startDuty(data);
return _heartbeat(data);
if (await _dutyEstablished()) return _heartbeat(data);
final started = await _startDuty(data);
if (await _dutyEstablished()) await _heartbeat(data);
return started;
}
}
@@ -173,19 +348,25 @@ class UpdateRiderLogProvider {
Map<String, dynamic> data,
) async {
if (_isOffDutyIntent(data)) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('dutylogid', 0);
await _forgetDuty();
final res = await MilerApi.endDuty();
if (res.ok) await prefs.setInt('onduty', 0);
if (res.ok) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('onduty', 0);
}
return res.ok
? ApiConfig.toLegacyEnvelope(res.raw ?? {'success': true})
: {'status': false, 'code': res.status};
}
final onduty = data['onduty'];
if (onduty == 1 || _s(onduty) == '1') {
final prefs = await SharedPreferences.getInstance();
if ((prefs.getInt('dutylogid') ?? 0) <= 0) return _startDuty(data);
if ((onduty == 1 || _s(onduty) == '1') && !await _dutyEstablished()) {
// Same shape as [CreateRiderLogProvider.createRiderLog]: establish duty,
// then beat on the same tick rather than returning and leaving the
// console with nothing for another interval.
final started = await _startDuty(data);
if (await _dutyEstablished()) await _heartbeat(data);
return started;
}
return _heartbeat(data);
}

View File

@@ -3,6 +3,7 @@ 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/heartbeat.dart';
import 'package:miler/data/mock_backend.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/service_profile.dart';
@@ -93,7 +94,7 @@ class AuthProvider {
int? pin,
String? pinRaw,
}) async {
// The legacy `jupiter.doormile.app/.../rider/login` path that used to sit
// The legacy `rider/login` path on the retired backend that used to sit
// behind a flag here is gone with the rest of the old backend.
return _loginNew(
contactNo: contactNo,
@@ -303,6 +304,15 @@ class AuthProvider {
'logid': 0,
'partnerid': 0,
'configid': 0,
// ── Zero is not a safe default for this one ──
//
// Every other field here is an *identifier*, and an unknown identifier
// is honestly zero. `logseconds` is a **cadence**, and zero read as a
// cadence means "never beat" — which is exactly how both call sites in
// [RiderLogController] took it, so the rider-log loop never started on
// a v1 deployment and the console lost battery, connection, GPS and
// the rider's live position with it. See [kDefaultLogSeconds].
'logseconds': kDefaultLogSeconds,
// ── Which line of work this rider is on ──
//
// The server's answer first, the build's declared tenant only when
@@ -432,24 +442,6 @@ class AuthProvider {
}
}
// Convenience: send using a Login model body
Future<http.Response> 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<Login> loginParsed({
required String contactNo,
@@ -498,7 +490,14 @@ class AuthProvider {
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);
// Not `?? 0`. This is read back as the heartbeat's interval, and a zero
// there stops the loop starting at all — see [resolveLogSeconds]. A
// legacy backend that sends a real cadence still wins; anything else
// lands on the 30s floor rather than on silence.
await prefs.setInt(
'logseconds',
resolveLogSeconds(details['logseconds']),
);
await prefs.setInt('locationid', details['locationid'] ?? 0);
await prefs.setInt('tenantid', details['tenantid'] ?? 0);

View File

@@ -15,7 +15,7 @@ import 'package:miler/data/accepted_store.dart';
/// ── What changed ──
///
/// This file used to carry two complete implementations of every call: a
/// legacy one that spoke to `queue.workolik.com` through a hand-built
/// legacy one that spoke to the retired backend through a hand-built
/// `IOClient` — one that disabled certificate checking, pinned a hardcoded IP
/// and did its own TLS upgrade to work around a carrier's broken DNS — and a
/// v1 one behind `if (ApiConfig.useNewApi)`. The legacy backend is gone, and

View File

@@ -2,12 +2,9 @@ import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:miler/Models/summary/pickupstats.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:flutter/foundation.dart';
class SummaryProvider {
final String baseUrl = ApiConstants.summaryApiLive;
Future<PickupStats?> fetchSummaryStats(int userId) => _fetchSummaryStatsNew();
/// `GET /miler/earnings?period=daily|weekly|monthly`

View File

@@ -17,7 +17,6 @@ import 'package:permission_handler/permission_handler.dart';
import 'package:miler/providers/pickup/pickup_provider.dart';
import 'package:miler/providers/pickuplog/pickuplog_provider.dart';
import 'package:miler/views/Dashboard/orders/orderstaus_button.dart';
import 'package:miler/views/helpers/constants/apiconstants.dart';
import 'package:miler/views/helpers/constants/Colorconstants.dart';
import 'package:miler/views/helpers/constants/miler_surface.dart';
import 'package:miler/views/helpers/widgets/app_widgets.dart';
@@ -59,6 +58,7 @@ import 'package:miler/views/Dashboard/pickups/pickups.dart' as pickups;
import 'package:miler/background/live_tracking_service.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/heartbeat.dart';
import 'package:miler/data/consignment_state.dart';
import 'package:miler/views/Dashboard/pickups/collect_payment.dart';
import 'package:miler/views/Dashboard/pickups/stop_type.dart';
@@ -1699,11 +1699,13 @@ class _HomepageState extends State<Homepage>
Future<Duration> _getLogInterval() async {
try {
final prefs = await SharedPreferences.getInstance();
final secs = prefs.getInt('logseconds');
final int interval = (secs != null && secs > 0) ? secs : 30;
// Already correct — routed through the shared resolver so the 30 lives in
// one place. See [kDefaultLogSeconds] for what a zero here used to cost
// the rider-log loop, which read the same pref and did not guard it.
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
return Duration(seconds: interval);
} catch (_) {
return const Duration(seconds: 30);
return const Duration(seconds: kDefaultLogSeconds);
}
}

View File

@@ -932,7 +932,15 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
final top = media.padding.top + 72;
await _camera.fit(
<LatLng>[_routeStart, _routeEnd],
padding: EdgeInsets.fromLTRB(48, top, 48, viewport * _sheetWorking + 24),
// The sheet is lifted by the gesture bar, so the space it covers is
// taller than the bare fraction — see [_sheetLift]. Framing against the
// old figure would tuck the route's near end under the sheet.
padding: EdgeInsets.fromLTRB(
48,
top,
48,
viewport * (_sheetWorking + _sheetLift(media)) + 24,
),
);
}
@@ -1102,18 +1110,49 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
/// Three stops and no more: a glance, the working height, and the detail
/// view. A drag with many arbitrary rest points feels loose and gives the
/// rider nothing to aim at.
/// The system's own bar at the foot of the screen, as a fraction of the
/// viewport — so the three snap fractions below can be lifted clear of it.
///
/// The snaps are fractions and the bar is an absolute height, so the two only
/// meet through the viewport. Clamped because a fraction derived from a
/// measurement should never be able to swallow the sheet on a device that
/// reports something strange.
double _sheetLift(MediaQueryData media) {
final double height = media.size.height;
if (height <= 0) return 0;
final double bar = math.max(
media.viewInsets.bottom,
media.viewPadding.bottom,
);
return (bar / height).clamp(0.0, 0.15);
}
static const double _sheetGlance = 0.35;
static const double _sheetWorking = 0.45;
static const double _sheetDetail = 0.75;
Widget _buildDraggableBottomSheet() {
// ── Every stop is lifted by the height of the gesture bar ──
//
// Padding the content alone would have kept the sheet the same height and
// simply moved everything in it up, costing the rider a bar's worth of the
// stop he is reading. Growing the sheet by the same amount puts the glass
// *behind* the bar and leaves the visible content area exactly the size it
// was — which is what "the sheet is touching the navigation" actually asks
// for. The two work together: this makes room, the list's bottom padding
// keeps the CTA out of it.
final double lift = _sheetLift(MediaQuery.of(context));
final double glance = (_sheetGlance + lift).clamp(0.0, 1.0);
final double working = (_sheetWorking + lift).clamp(0.0, 1.0);
final double detail = (_sheetDetail + lift).clamp(0.0, 1.0);
return DraggableScrollableSheet(
initialChildSize: _sheetWorking,
minChildSize: _sheetGlance,
maxChildSize: _sheetDetail,
initialChildSize: working,
minChildSize: glance,
maxChildSize: detail,
// Snap to the three, so the sheet always settles somewhere meant.
snap: true,
snapSizes: const [_sheetGlance, _sheetWorking, _sheetDetail],
snapSizes: [glance, working, detail],
// ── Frosted, not opaque ──
//
// A solid white slab over a map takes away 45% of the one thing this
@@ -1157,7 +1196,32 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
// has, and it is why that sheet reads at arm's length.
child: ListView(
controller: scrollController,
padding: EdgeInsets.fromLTRB(20.w, 16.h, 20.w, 8.h),
// ── The gesture bar sat on top of the CTA ──
//
// Eight points of bottom padding, on a sheet anchored to
// the bottom of the screen: the last thing in this list is
// [_buildPrimaryAction] — the slide control that closes the
// stop — and on a gesture-navigation phone the system's own
// bar was drawn across it. The rider's swipe either missed
// the control or went to the OS.
//
// The glass still bleeds to the screen edge, which is what
// it is for; what has to clear the bar is the *content*.
// Same rule the sheet kit applies to every modal sheet —
// `max(keyboard, gesture bar)`, never the sum, because the
// keyboard replaces the bar rather than stacking with it.
// This sheet is hand-built rather than presented through
// [MilerSheetScaffold], so it never inherited that rule.
padding: EdgeInsets.fromLTRB(
20.w,
16.h,
20.w,
8.h +
math.max(
MediaQuery.viewInsetsOf(context).bottom,
MediaQuery.viewPaddingOf(context).bottom,
),
),
children: [
_LiveEtaHeader(
meters: _liveMetersToStop,
@@ -1635,13 +1699,11 @@ class _PickupMapScreenState extends State<_PickupMapScreen>
if (_isNavigating || !mounted) return;
final outcome = await DeliveryProofPage.askOutcome(context);
if (!mounted || outcome == null) return;
await _closeDelivery(
switch (outcome) {
ProofOutcome.delivered => DeliveryOutcome.delivered,
ProofOutcome.skipped => DeliveryOutcome.skipped,
ProofOutcome.cancelled => DeliveryOutcome.cancelled,
},
);
await _closeDelivery(switch (outcome) {
ProofOutcome.delivered => DeliveryOutcome.delivered,
ProofOutcome.skipped => DeliveryOutcome.skipped,
ProofOutcome.cancelled => DeliveryOutcome.cancelled,
});
}
/// The end of the stop, whichever of the three it is.

View File

@@ -1,90 +0,0 @@
class ApiConstants {
static String mainDev = "dev";
static String mainRoute = "live";
// ── There is no maps key any more ──
//
// A `googleMapsApiKey` used to live here, feeding `flutter_polyline_points`
// and both native projects. It was revoked, so every tile request and every
// Directions call came back `REQUEST_DENIED` and the app looked broken rather
// than unconfigured.
//
// The maps no longer need one: tiles come from OpenStreetMap and routes from
// OSRM, both keyless. See `lib/views/helpers/widgets/miler_map.dart` and
// `lib/helpers/miler_router.dart` — including the note on what those free
// services are and are not suitable for.
//Collection Queue - v2
static String pickupQueueDev =
"https://jupiter.doormile.app/$mainDev/api/v2/pickups/getpickupqueues";
static String pickupQueueLive =
"https://jupiter.doormile.app/$mainRoute/api/v2/pickups/getpickupqueues";
//Current Collection - v1
static String currentPickupDev =
"https://jupiter.doormile.app/$mainDev/api/v1/pickups/getpickups";
static String currentPickupLive =
"https://jupiter.doormile.app/$mainRoute/api/v1/pickups/getpickups";
//Current Collection V3 - v3 (date-bounded)
static String currentPickupV3Dev =
"https://jupiter.doormile.app/$mainDev/api/v3/pickups/getpickups";
static String currentPickupV3Live =
"https://jupiter.doormile.app/$mainRoute/api/v3/pickups/getpickups";
//Update Collection - v1
static String updatePickupDev =
"https://queue.workolik.com/live/api/v1/pickups/updatepickup";
static String updatePickupLive =
"https://queue.workolik.com/live/api/v1/pickups/updatepickup";
//Get Rider Log - v1
static String getRiderLogDev =
"https://jupiter.doormile.app/$mainDev/api/v1/partners/getriderlog";
static String getRiderLogLive =
"https://jupiter.doormile.app/$mainRoute/api/v1/partners/getriderlog";
//Create Rider Log - v2
static String createRiderLogDev =
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
static String createRiderLogLive =
"https://queue.workolik.com/live/api/v2/partners/createriderlog";
//Update Rider Log - v1
static String updateRiderLogDev =
"https://jupiter.doormile.app/$mainDev/api/v1/partners/updateriderlog";
static String updateRiderLogLive =
"https://jupiter.doormile.app/$mainRoute/api/v1/partners/updateriderlog";
//Get Rider Count - v1
static String getRiderCountDev =
"https://jupiter.doormile.app/$mainDev/api/v1/partners/getridercount";
static String getRiderCountLive =
"https://jupiter.doormile.app/$mainRoute/api/v1/partners/getridercount";
//Create Break Rider Log - v2
static String createBreakRiderLogDev =
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
static String createBreakRiderLogLive =
"https://queue.workolik.com/live/api/v2/partners/createbreaklog";
//Update Break Rider Log - v2
static String updateBreakRiderLogDev =
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
static String updateBreakRiderLogLive =
"https://queue.workolik.com/live/api/v2/partners/updatebreaklog";
//Create Collection Log - v2
static String createPickupLogDev =
"https://queue.workolik.com/live/api/v2/pickups/createpickuplog";
static String createPickupLogLive =
"https://queue.workolik.com/live/api/v2/pickups/createpickuplog";
//Summary API - v2
static String summaryApiLive =
'https://jupiter.doormile.app/$mainRoute/api/v2/partners';
//Summary Rider Weekly KMs - v1
static String summaryriderkmLive =
'https://jupiter.doormile.app/$mainRoute/api/v1/partners';
}

View File

@@ -3,7 +3,8 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:geolocator/geolocator.dart';
import 'package:http/http.dart' as http;
import 'package:miler/data/api_config.dart';
import 'package:miler/data/miler_api.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:audioplayers/audioplayers.dart';
import 'dart:math' as math;
@@ -20,7 +21,6 @@ class BackgroundDeliveryLog {
static final CreateDeliveryLogProvider _logProvider =
CreateDeliveryLogProvider();
static final http.Client _httpClient = http.Client();
static final FlutterTts _tts = FlutterTts();
static final AudioPlayer _proximityPlayer = AudioPlayer();
@@ -60,38 +60,37 @@ class BackgroundDeliveryLog {
final now = DateTime.now();
final today = '${now.year}-${_pad(now.month)}-${_pad(now.day)}';
// Build API URL using v1 endpoint (getdeliveries)
final bool isLive = ApiConstants.mainRoute == 'live';
final baseUrl = isLive
? ApiConstants.currentDeliveryLive
: ApiConstants.currentDeliveryDev;
final uri = Uri.parse(baseUrl).replace(
queryParameters: {
'userid': userId.toString(),
'fromdate': today,
'todate': today,
't': DateTime.now().millisecondsSinceEpoch.toString(),
},
);
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Fetching deliveries: $uri');
// Fetch deliveries from API using v1 endpoint
final deliveries = await _fetchDeliveriesFromApi(uri);
// ── The delivery twin of the pickup fetch, with the same two faults ──
//
// It built a `deliveries/getdeliveries` URL by hand and fetched it with a
// bare `_httpClient.get` — **no bearer token** — and put `userid` in the
// query string. The URL itself already resolved to a v1 route through the
// [DeliveryApi] facade, so the call could only ever be answered 401.
//
// And the filter looked for `orderstatus == 'active'`, which is not a
// value this app's vocabulary contains: [ApiConfig.legacyStatusFromNew]
// maps the v1 statuses to `assigned` / `accepted` / `arrived` / `picked`,
// never to `active`. So it returned nothing on every tick and fell
// through to the shift-end check, which is what the device log shows.
//
// `GET /miler/bookings` through [MilerApi], asked with the session, about
// the rider the session names.
final res = await MilerApi.bookings(date: today);
final deliveries = res.list;
debugPrint(
'[ACTIVE_DELIVERY_LOG][BG] API returned ${deliveries.length} items',
'[ACTIVE_DELIVERY_LOG][BG] /miler/bookings -> ${res.status}, '
'${deliveries.length} items',
);
// Filter for active orders only
const inProgress = {'accepted', 'arrived', 'picked'};
final activeOrders = deliveries.whereType<Map<String, dynamic>>().where((
order,
) {
final status = (order['orderstatus']?.toString().toLowerCase() ?? '')
.trim();
final isActive = status == 'active';
return isActive;
final status = ApiConfig.legacyStatusFromNew(
(order['status'] ?? order['orderstatus'])?.toString(),
);
return inProgress.contains(status);
}).toList();
if (activeOrders.isEmpty) {
@@ -202,11 +201,8 @@ class BackgroundDeliveryLog {
'logstatus': await _resolveLogStatus(orderId, coords),
};
// Determine API endpoint
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createDeliveryLogLive
: ApiConstants.createDeliveryLogDev;
// v1: `/miler/consignments/logs`, via the DeliveryApi facade.
final url = ApiConstants.createDeliveryLogLive;
// Post the log
debugPrint('[ACTIVE_DELIVERY_LOG][BG] Posting log for $orderId');
@@ -434,33 +430,6 @@ class BackgroundDeliveryLog {
}
/// Fetch deliveries from API using v1 endpoint
static Future<List<dynamic>> _fetchDeliveriesFromApi(Uri uri) async {
try {
final res = await _httpClient
.get(uri)
.timeout(const Duration(seconds: 10));
if (res.statusCode >= 200 && res.statusCode < 300) {
final decoded = json.decode(res.body);
final data = decoded is Map<String, dynamic>
? (decoded['details'] ?? decoded['data'] ?? decoded)
: decoded;
if (data is List) {
return data;
}
if (data is Map && data['items'] is List) {
return data['items'] as List;
}
return [];
}
return [];
} catch (e) {
return [];
}
}
/// Helper to pad numbers with leading zero
static String _pad(int n) => n.toString().padLeft(2, '0');
@@ -540,10 +509,6 @@ class BackgroundDeliveryLog {
final List<String> remaining = [];
bool anySuccess = false;
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createDeliveryLogLive
: ApiConstants.createDeliveryLogDev;
for (final itemStr in queue) {
try {
final Map<String, dynamic> item = jsonDecode(itemStr);
@@ -554,6 +519,8 @@ class BackgroundDeliveryLog {
if (payload.isEmpty) continue;
// v1: `/miler/consignments/logs`, via the DeliveryApi facade.
final url = ApiConstants.createDeliveryLogLive;
debugPrint(
'[ACTIVE_DELIVERY_LOG][BG][OFFLINE] Retrying for orderId: $orderId',
);
@@ -770,14 +737,18 @@ class BackgroundDeliveryLog {
'[AUTO_SHIFT_END] 🚀 Initiating auto-break and offline sequence...',
);
// 1. Get Required IDs
// ── The four ids and the fix are no longer this function's business ──
//
// `userid`, `partnerid`, `shiftid`, `logid` and a position were assembled
// here to fill a legacy break-log body. On v1 the break and the duty end
// are both keyed on the **token**: `POST /miler/breaks/start` opens a
// break for whoever the session names, and `PUT /miler/duty/end` closes
// whichever duty log is open for them. Sending an id would not make it
// more correct; it is what let the old call be made unauthenticated.
//
// `userid` is still read, as the one guard worth keeping: no signed-in
// rider, nothing to end.
final int userid = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0;
final int partnerid =
prefs.getInt('partnerid') ?? prefs.getInt('partnerId') ?? 0;
final int shiftid =
prefs.getInt('shiftid') ?? prefs.getInt('shiftId') ?? 0;
final int logid = prefs.getInt('logid') ?? prefs.getInt('logId') ?? 0;
if (userid == 0) {
debugPrint(
'[AUTO_SHIFT_END] ❌ Missing userid, cannot create break log',
@@ -785,61 +756,28 @@ class BackgroundDeliveryLog {
return;
}
// 2. Get Location
final Map<String, String>? coords = await _getCoordinatesWithFallback();
final String lat = coords?['lat'] ?? '0';
final String lng = coords?['lng'] ?? '0';
// 3. Prepare Break Log Payload
final now = DateTime.now();
final int localBreakId =
(DateTime.now().millisecondsSinceEpoch % 900) + 100; // Random-ish ID
final String breakdate =
'${now.year}-${_pad(now.month)}-${_pad(now.day)} ${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final String breakstart =
'${_pad(now.hour)}:${_pad(now.minute)}:${_pad(now.second)}';
final payload = <String, dynamic>{
"breakid": localBreakId,
"logid": logid,
"breakdate": breakdate,
"userid": userid,
"partnerid": partnerid,
"shiftid": shiftid,
"breakstart": breakstart,
"breakend": "",
"breakhours": 0.0,
"latitude": lat,
"longitude": lng,
};
// 4. Call API to Create Break
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
debugPrint('[AUTO_SHIFT_END] Creating break log: $url');
// We use a separate provider instance or http call if needed,
// but _logProvider is for delivery logs. We need a generic post or use http directly.
// Since we don't have BreakRiderLogProvider here, we'll use http directly for simplicity and isolation.
// 4. Close the shift with a break, on v1.
//
// This hand-posted the retired backend's `partners/createbreaklog`
// with a bare `_httpClient.post` and no bearer
// token — an unauthenticated write keyed on a `userid` in the
// body. `POST /miler/breaks/start` is the v1 endpoint, and
// [MilerApi] attaches the session.
debugPrint('[AUTO_SHIFT_END] Creating break log (v1)');
try {
final response = await _httpClient
.post(
Uri.parse(url),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(payload),
)
.timeout(const Duration(seconds: 10));
final res = await MilerApi.startBreak('Shift_End');
if (response.statusCode >= 200 && response.statusCode < 300) {
if (res.ok) {
debugPrint('[AUTO_SHIFT_END] ✅ Break log created successfully');
// Parse response to get server break ID if needed, but mainly we just need to go offline
final body = jsonDecode(response.body);
final det = (body['details'] is Map) ? body['details'] : body;
final serverBreakId = det['breakid'];
final serverBreakId = res.map['breaklogid'] ?? res.map['breakid'];
if (serverBreakId != null) {
await prefs.setInt(
@@ -851,7 +789,7 @@ class BackgroundDeliveryLog {
await prefs.setInt('break_start_epoch', now.millisecondsSinceEpoch);
} else {
debugPrint(
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${response.statusCode}',
'[AUTO_SHIFT_END] ⚠️ Failed to create break log: ${res.status}',
);
}
} catch (e) {
@@ -863,27 +801,15 @@ class BackgroundDeliveryLog {
await prefs.setInt('onduty', 0);
await prefs.setBool('online', false);
// 6. Update Rider Log (Set Duty = 0)
// We should also update the main rider log to say onduty=0
final updateUrl = ApiConstants.mainRoute == 'live'
? ApiConstants.updateRiderLogLive
: ApiConstants.updateRiderLogDev;
final updatePayload = {
"userid": userid,
"onduty": 0,
"latitude": lat,
"longitude": lng,
};
// 6. End duty, on v1.
//
// Was an unauthenticated POST of `{userid, onduty: 0}` to the
// legacy `updateriderlog`. `PUT /miler/duty/end` closes whichever
// duty log is open for the token's rider, which is the same fact
// without the id or the open door.
try {
await _httpClient
.post(
Uri.parse(updateUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(updatePayload),
)
.timeout(const Duration(seconds: 5));
await MilerApi.endDuty();
debugPrint('[AUTO_SHIFT_END] ✅ Rider status updated to Offline');
} catch (_) {}

View File

@@ -6,7 +6,6 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'dart:math' as math;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
import 'package:miler/xpress/providers/Riderlog/riderlog_provider.dart';
import 'package:miler/xpress/background/backgroundservice.dart';
import 'package:geolocator/geolocator.dart';
@@ -354,12 +353,9 @@ class _BackgroundRiderLog {
payload['lastname'] = lastName.trim();
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
final provider = CreateRiderLogProvider();
final resp = await provider.createRiderLog(base, payload);
final resp = await provider.createRiderLog(payload);
if (resp == null || resp.isEmpty) return;
final det = (resp['details'] is Map<String, dynamic>)

View File

@@ -1,7 +1,6 @@
import 'package:get/get.dart';
import 'package:flutter/foundation.dart';
import 'dart:convert';
import 'package:miler/xpress/views/helpers/constants/apiconstants.dart';
import 'package:miler/xpress/Models/riders/riders_models.dart';
import 'package:miler/xpress/providers/Riderlog/riderlog_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -15,6 +14,7 @@ import 'package:miler/xpress/controllers/logcontroller.dart';
import 'package:miler/xpress/background/foreground_service.dart' as fg;
import 'package:miler/xpress/utils/kalman_filter.dart';
import 'package:miler/xpress/utils/mqtt_service.dart';
import 'package:miler/data/heartbeat.dart';
import 'package:miler/xpress/views/helpers/constants/mqtt_constants.dart';
import 'package:battery_plus/battery_plus.dart';
@@ -40,11 +40,9 @@ class RiderLogController extends GetxController {
Future<void> fetchRiderLogs({required String userId}) async {
isLoading.value = true;
try {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.getRiderLogLive
: ApiConstants.getRiderLogDev;
final url = "$base?userid=$userId";
final response = await _getProvider.getRiderLog(url);
// v1 answers duty for the *token's* rider — `userid` is neither sent
// nor needed. See [GetRiderLogProvider].
final response = await _getProvider.getRiderLog();
// Handle either {details: {...}} or {data: [...]} shapes
if (response == null) {
@@ -122,11 +120,7 @@ class RiderLogController extends GetxController {
}
Future<void> fetchRiderCount({required String userId}) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.getRiderCountLive
: ApiConstants.getRiderCountDev;
final url = "$base?userid=$userId";
final response = await _getProvider.getRiderCount(url);
final response = await _getProvider.getRiderCount();
final dynamic cnt = response != null ? response['count'] : null;
riderCount.value = cnt is num
? cnt.toInt()
@@ -134,46 +128,30 @@ class RiderLogController extends GetxController {
}
Future<bool> createLogin(RiderLogin login) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
final ok = await _createProvider.createRiderLog(
base,
_CreateRiderLogRequestCompat.fromRiderLogin(login),
);
return ok != null && ok.isNotEmpty;
}
Future<bool> updateLog(RiderUpdate update) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.updateRiderLogLive
: ApiConstants.updateRiderLogDev;
final ok = await _updateProvider.updateRiderLog(
base,
_UpdateRiderLogRequestCompat.fromRiderUpdate(update),
);
return ok != null && ok.isNotEmpty;
}
Future<bool> createBreak(RiderBreak brk) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
final ok = await _breakProvider.createBreakRiderLog(
base,
_BreakLogRequestCompat.fromRiderBreak(brk),
);
return ok != null && ok.isNotEmpty;
}
Future<bool> updateBreak(RiderBreak brk) async {
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.updateBreakRiderLogLive
: ApiConstants.updateBreakRiderLogDev;
final ok = await _breakProvider.updateBreakRiderLog(
base,
_BreakLogRequestCompat.fromRiderBreak(brk),
);
// `PUT /miler/breaks/end` takes no body and closes whichever break is
// open, which the server knows and the app was only guessing at.
final ok = await _breakProvider.updateBreakRiderLog();
return ok != null && ok.isNotEmpty;
}
@@ -190,10 +168,6 @@ class RiderLogController extends GetxController {
required String latitude,
required String longitude,
}) async {
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.updateBreakRiderLogLive
: ApiConstants.updateBreakRiderLogDev;
final payload = {
"breakid": breakid,
"logid": logid,
@@ -209,11 +183,9 @@ class RiderLogController extends GetxController {
};
try {
// Debug: show exact URL and JSON body
debugPrint('[BREAK][UPDATE] URL: $url');
debugPrint('[BREAK][UPDATE] Body: $payload');
final response = await _breakProvider.updateBreakRiderLog(url, payload);
final response = await _breakProvider.updateBreakRiderLog();
// Debug: show raw response
debugPrint(
@@ -375,13 +347,9 @@ class RiderLogController extends GetxController {
payload.remove('lastname');
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
debugPrint('[RIDERLOG][CREATE LOGIN NOW] URL: $base');
debugPrint('[RIDERLOG][CREATE LOGIN NOW] Payload: $payload');
final resp = await _createProvider.createRiderLog(base, payload);
final resp = await _createProvider.createRiderLog(payload);
debugPrint(
'[RIDERLOG][CREATE LOGIN NOW] Response: ${resp == null ? 'null' : resp.toString()}',
);
@@ -391,7 +359,7 @@ class RiderLogController extends GetxController {
debugPrint(
'[RIDERLOG][CREATE LOGIN NOW] Failed, saving to offline queue',
);
await _saveToOfflineQueue(base, payload);
await _saveToOfflineQueue('/miler/logs', payload);
return false;
}
@@ -500,11 +468,7 @@ class RiderLogController extends GetxController {
payload.remove('lastname');
}
final base = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
await _saveToOfflineQueue(base, payload);
await _saveToOfflineQueue('/miler/logs', payload);
}
} catch (e) {
debugPrint('[RIDERLOG] Error saving offline log: $e');
@@ -532,10 +496,15 @@ class RiderLogController extends GetxController {
await prefs.setInt('onduty', on ? 1 : 0);
if (on) {
await createLoginNowV2();
final int interval = prefs.getInt('logseconds') ?? 0;
if (interval > 0) {
startAutoCreateLoginLoop(seconds: interval);
}
// ── Going on duty always starts the heartbeat ──
//
// Gated on `interval > 0`, this read a stored zero as a decision not
// to beat. The v1 login has no `logseconds` to store, so the gate was
// shut on every rider: duty went on, one log was posted by the call
// above, and nothing followed it. Mirrors the logistics controller —
// see [resolveLogSeconds].
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
startAutoCreateLoginLoop(seconds: interval);
// Ensure foreground logging notification is started when going on-duty
try {
@@ -604,16 +573,43 @@ class RiderLogController extends GetxController {
}
}
/// Whether the periodic heartbeat is currently running on the stream path.
///
/// ── Why a test needs to be able to see this ──
///
/// The `logseconds` bug was invisible from outside the class. Duty went on,
/// the API accepted it, one log was posted by the immediate call — and the
/// loop that was supposed to follow it simply never started. Nothing threw,
/// nothing logged an error, and the only symptom was a console panel that had
/// been blank for so long it read as a backend problem.
///
/// Android normally runs the heartbeat as a foreground service instead, and
/// leaves this null; the test host is never Android, so the stream path is
/// the one under test. See [startAutoCreateLoginLoop].
@visibleForTesting
bool get autoLoopRunning => _autoLoginSubscription != null;
// Start periodic createRiderLog calls based on seconds (or prefs 'logseconds')
// ✅ CRITICAL: When there are active deliveries, use 30 seconds (same as delivery logs)
// Otherwise, use the configured logseconds interval
void startAutoCreateLoginLoop({int? seconds}) async {
/// `Future<void>`, not `void`. It was fire-and-forget, which meant no caller
/// could wait for the loop to actually be up — and the one place that most
/// wanted to, [setOnDuty], had no way to tell whether the heartbeat it had
/// just asked for existed. Every existing call site ignores the future and is
/// unchanged by this.
Future<void> startAutoCreateLoginLoop({int? seconds}) async {
final prefs = await SharedPreferences.getInstance();
// ✅ Check if there are active deliveries - if yes, use 30 seconds (same as delivery logs)
final bool hasActiveDeliveries =
prefs.getBool('has_live_deliveries') ?? false;
final int baseInterval = seconds ?? (prefs.getInt('logseconds') ?? 0);
// The configured cadence, or the floor under it. `?? 0` here was the
// second half of the same bug as the one in [setOnDuty]: a caller passing
// no `seconds` fell through to a stored zero and returned at the
// `interval <= 0` guard below. See [resolveLogSeconds].
final int baseInterval = resolveLogSeconds(
seconds ?? prefs.getInt('logseconds'),
);
// When there are active deliveries, post rider logs every 30 seconds (matching delivery logs)
// Otherwise, use the configured interval
@@ -625,12 +621,18 @@ class RiderLogController extends GetxController {
);
}
await stopAutoCreateLoginLoop(); // Cancel existing subscription
// Stop before start, which is what keeps this method safe to call from
// every place that does. Whichever call arrives last wins, and there is
// never a second loop: the subscription is cancelled and the foreground
// service stopped before either is created again.
await stopAutoCreateLoginLoop();
// Attempt to flush offline logs on loop start
flushOfflineLogs();
if (interval <= 0) return;
// No `interval <= 0` guard. [resolveLogSeconds] cannot return one, and the
// guard that used to be here is what turned a missing `logseconds` into a
// rider who never reported.
final int onduty = prefs.getInt('onduty') ?? 0;
if (onduty != 1) {
debugPrint('[RIDERLOG][AUTO LOOP] Not starting - onduty=$onduty');
@@ -921,10 +923,6 @@ class RiderLogController extends GetxController {
final breakdate = _formatDateTimeFull(now); // e.g. 2025-10-16 16:36:16
final breakstart = _formatTime(now); // e.g. 16:36:16
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createBreakRiderLogLive
: ApiConstants.createBreakRiderLogDev;
// Build payload with all required fields in the exact format the API expects
final payload = <String, dynamic>{
"breakid": localBreakId,
@@ -941,13 +939,12 @@ class RiderLogController extends GetxController {
};
// Debug logs for terminal visibility
debugPrint('[BREAK][CREATE] URL: $url');
debugPrint('[BREAK][CREATE] Payload: $payload');
// Persist local break id immediately to ensure update can reference it
await prefs.setInt('breakId', localBreakId);
final resp = await _breakProvider.createBreakRiderLog(url, payload);
final resp = await _breakProvider.createBreakRiderLog(payload);
debugPrint(
'[BREAK][CREATE] Response: ${resp == null ? 'null' : resp.toString()}',
@@ -1045,9 +1042,6 @@ class RiderLogController extends GetxController {
final breakhoursDouble = duration.inSeconds / 3600.0;
// Debug logs for terminal visibility
debugPrint(
'[BREAK][UPDATE] URL: ${ApiConstants.mainRoute == 'live' ? ApiConstants.updateBreakRiderLogLive : ApiConstants.updateBreakRiderLogDev}',
);
debugPrint(
'[BREAK][UPDATE] Fields: breakid=$breakid, logid=$logid, userid=$userid, partnerid=$partnerid, shiftid=$shiftid, breakend=$breakend, breakhours=$breakhoursDouble, lat=$latitude, lng=$longitude',
);
@@ -1101,10 +1095,6 @@ class RiderLogController extends GetxController {
final iso = _formatDateTimeFull(now);
final loginTime = _formatTime(now);
final url = ApiConstants.mainRoute == 'live'
? ApiConstants.createRiderLogLive
: ApiConstants.createRiderLogDev;
// ✅ Check if there are active deliveries to set status
final bool hasActiveDeliveries =
prefs.getBool('has_live_deliveries') ?? false;
@@ -1141,10 +1131,9 @@ class RiderLogController extends GetxController {
userfcmtoken: userfcmtoken ?? '',
).toJson();
debugPrint('[RIDERLOG][CREATE LOGIN] URL: $url');
debugPrint('[RIDERLOG][CREATE LOGIN] Payload: $payload');
final resp = await _createProvider.createRiderLog(url, payload);
final resp = await _createProvider.createRiderLog(payload);
debugPrint(
'[RIDERLOG][CREATE LOGIN] Response: ${resp == null ? 'null' : resp.toString()}',
);
@@ -1205,7 +1194,7 @@ class RiderLogController extends GetxController {
// We can use _createProvider generic call
debugPrint('[RIDERLOG][OFFLINE] Retrying: $payload');
final resp = await _createProvider.createRiderLog(url, payload);
final resp = await _createProvider.createRiderLog(payload);
if (resp != null && resp.isNotEmpty) {
debugPrint('[RIDERLOG][OFFLINE] Success!');

View File

@@ -107,7 +107,7 @@ class DeliveryApi {
/// **ABSENT** — the generic status write the ported flow uses for every
/// transition (accepted → active → arrived → delivered → skipped).
///
/// Was `queue.workolik.com/live/api/v1/deliveries/updatedelivery`. There is no
/// Was `deliveries/updatedelivery` on the retired backend. There is no
/// single status-write route on Doormile; the transitions above are separate
/// endpoints. Call sites that still post a bare status change land here and
/// will 404 until either they are split or the route is built.
@@ -117,7 +117,7 @@ class DeliveryApi {
/// **LIVE** — periodic location/telemetry log.
///
/// Was `queue.workolik.com/live/api/v2/partners/createriderlog`.
/// Was `partners/createriderlog` on the retired backend.
static String get createRiderLog => url('/miler/logs');
/// **LIVE** — read them back.
@@ -142,7 +142,7 @@ class DeliveryApi {
/// **ABSENT** — per-delivery event log.
///
/// Was `queue.workolik.com/live/api/v2/deliveries/createdeliverylog`. The
/// Was `deliveries/createdeliverylog` on the retired backend. The
/// closest real thing is consignment logs, which key on a consignmentid the
/// delivery flow does not always hold.
static String get createDeliveryLog => url('/miler/consignments/logs');

View File

@@ -1,260 +1,46 @@
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:flutter/foundation.dart';
// Combined Riderlog providers:
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken nodes.
/// ─────────────────────────────────────────────────────────────────────────
/// THE DELIVERY LINE'S RIDER LOG IS THE PARCEL LINE'S RIDER LOG
///
/// **Empty since the delivery line moved to Doormile.** The one entry here
/// pinned `queue.workolik.com` to 66.116.225.226, because some Indian carriers'
/// DNS resolved that host to a CDN node that answered 404. Nothing calls that
/// host any more — every delivery-line write goes to `api.doormile.com` via
/// [DeliveryApi] — so the pin was routing advice about a server this app no
/// longer talks to.
/// ── What this file used to be ──
///
/// It is deliberately not re-pointed at the Doormile host: that IP belongs to
/// the old backend, and pinning it for `api.doormile.com` would send every
/// request to the wrong server with the right SNI. If Doormile ever needs the
/// same workaround, add it here with its own verified address.
const _knownGoodIPs = <String, String>{};
/// A second, complete implementation of duty, breaks and the location
/// heartbeat, posting to the two retired pre-v1 hosts over raw `http`. It survived the migration only because
/// nothing in `lib/` imported it: the delivery shell reaches it through
/// [riderShell], which is production for any tenant on the delivery line.
///
/// It carried two things that had already been deleted from the parcel line:
///
/// • **`_buildSslBypassClient()`** — an `IOClient` whose
/// `badCertificateCallback` returned `true` for *every* certificate, with a
/// hand-rolled `SecureSocket` upgrade and a hardcoded IP pin. Every request
/// it carried — a rider's GPS track, his duty state, his break times — could
/// be read and rewritten by anything on the path. The parcel line removed
/// this; the delivery line kept it.
/// • **The legacy hosts themselves**, which the v1 migration left behind.
///
/// ── Why this is an export and not a rewrite ──
///
/// `lib/providers/Riderlog/riderlog_provider.dart` already does exactly this
/// job against `api.doormile.com`: same four class names, same legacy
/// `{status, details}` envelopes, mapped onto `POST /miler/duty/start`,
/// `PUT /miler/location`, `POST /miler/logs` and the break endpoints. The
/// delivery line's controller calls the same four classes by the same names.
///
/// So there is nothing to port. Re-exporting means one implementation, one duty
/// gate, one telemetry payload and one place where a contract change lands —
/// which is the whole point of having migrated the parcel line at all. The two
/// lines cannot drift, because there is only one of them.
///
/// The `String urldata` first parameter is gone from every method; callers that
/// were building a legacy URL to pass in have had that construction deleted
/// rather than made to compile.
/// ─────────────────────────────────────────────────────────────────────────
library;
/// Creates an IOClient that:
/// 1. Bypasses SSL certificate errors
/// 2. Forces known-good IPs for hosts where carrier DNS returns broken CDN nodes
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
IOClient _buildSslBypassClient() {
final httpClient = HttpClient()
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
true;
httpClient
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
final host = uri.host;
final port = uri.port;
// Use known-good IP if available, else resolve normally (prefer IPv4)
InternetAddress? target;
final knownIP = _knownGoodIPs[host];
if (knownIP != null) {
target = InternetAddress(knownIP);
debugPrint('[SSL_CLIENT] Using known-good IP: $knownIP for $host');
} else {
try {
final addresses = await InternetAddress.lookup(
host,
type: InternetAddressType.IPv4,
);
if (addresses.isNotEmpty) target = addresses.first;
} catch (_) {}
}
if (uri.scheme == 'https') {
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
.then(
(plain) => SecureSocket.secure(
plain,
host: host, // SNI = original hostname for Nginx routing
onBadCertificate: (_) => true,
supportedProtocols: ['http/1.1'],
),
)
.then((s) => s as Socket);
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
}
return Socket.startConnect(target ?? InternetAddress(host), port);
};
return IOClient(httpClient);
}
class CreateRiderLogProvider {
Future<Map<String, dynamic>?> createRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
const maxAttempts = 3;
try {
debugPrint('createRiderLog payload ${json.encode(data)}');
} catch (_) {}
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
final client = _buildSslBypassClient();
try {
final url = Uri.parse(urldata);
final response = await client.post(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('createRiderLog url $urldata (attempt $attempt)');
debugPrint('createRiderLog status ${response.statusCode}');
debugPrint('createRiderLog response ${response.body}');
if (response.statusCode >= 200 && response.statusCode < 300) {
return json.decode(response.body.toString()) as Map<String, dynamic>;
} else {
debugPrint(
'createRiderLog failed: HTTP ${response.statusCode} (attempt $attempt/$maxAttempts)',
);
// On 404/5xx, wait and retry to potentially hit a different CDN node
if (attempt < maxAttempts) {
await Future.delayed(const Duration(seconds: 1));
}
}
} catch (e) {
debugPrint('createRiderLog exception (attempt $attempt): $e');
if (attempt < maxAttempts) {
await Future.delayed(const Duration(seconds: 1));
}
} finally {
client.close();
}
}
debugPrint('createRiderLog failed after $maxAttempts attempts');
return null;
}
}
class UpdateRiderLogProvider {
Future<Map<String, dynamic>?> updateRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
try {
final url = Uri.parse(urldata);
final response = await put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('updateRiderLog url: $urldata');
debugPrint('updateRiderLog response: ${response.body}');
if (response.statusCode >= 200 && response.statusCode < 300) {
final decoded = json.decode(response.body);
if (decoded is Map<String, dynamic>) {
return decoded;
} else {
debugPrint(
'⚠️ updateRiderLog: Expected Map but got ${decoded.runtimeType}',
);
return {};
}
} else {
debugPrint('❌ updateRiderLog failed with code ${response.statusCode}');
return {};
}
} catch (e) {
debugPrint('❌ Exception in updateRiderLog: $e');
return {};
}
}
}
class GetRiderLogProvider {
Future<Map<String, dynamic>?> getRiderLog(String urldata) async {
Map<String, dynamic>? getRiderLogResponse;
try {
final url = Uri.parse(urldata);
final response = await get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('getRiderLog response ${response.body}');
debugPrint('getRiderLog url ${urldata.toString()}');
getRiderLogResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return getRiderLogResponse;
}
Future<Map<String, dynamic>?> getRiderCount(String urldata) async {
Map<String, dynamic>? getRiderCountResponse;
try {
final url = Uri.parse(urldata);
final response = await get(
url,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('getRiderCount response ${response.body}');
debugPrint('getRiderCount url ${urldata.toString()}');
getRiderCountResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return getRiderCountResponse;
}
}
class BreakRiderLogProvider {
Future<Map<String, dynamic>?> createBreakRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
Map<String, dynamic>? breakLogResponse;
try {
final url = Uri.parse(urldata);
final response = await post(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('createBreakRiderLog url $urldata');
debugPrint('createBreakRiderLog response ${response.body}');
breakLogResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return breakLogResponse;
}
Future<Map<String, dynamic>?> updateBreakRiderLog(
String urldata,
Map<String, dynamic> data,
) async {
Map<String, dynamic>? breakLogResponse;
try {
final url = Uri.parse(urldata);
final response = await put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
debugPrint('updateBreakRiderLog url $urldata');
debugPrint('updateBreakRiderLog response ${response.body}');
breakLogResponse =
json.decode(response.body.toString()) as Map<String, dynamic>;
} catch (e) {
debugPrint(e.toString());
}
return breakLogResponse;
}
}
export 'package:miler/providers/Riderlog/riderlog_provider.dart'
show
BreakRiderLogProvider,
CreateRiderLogProvider,
GetRiderLogProvider,
UpdateRiderLogProvider,
unawaited;

View File

@@ -1,67 +1,25 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'package:http/io_client.dart';
import 'package:http/http.dart' as http;
import 'package:miler/data/api_config.dart';
import 'package:flutter/foundation.dart';
// Combined Deliverylog providers:
/// Hardcoded known-good IPs for hosts where carrier DNS returns broken CDN nodes.
/// Confirmed: 66.116.225.226 = 200 OK, 125.21.240.67 = 404/405.
/// Empty since the delivery line moved to Doormile — see the note on the
/// matching map in `providers/Riderlog/riderlog_provider.dart`. The old entry
/// pinned `queue.workolik.com`, a host nothing calls now, and must not be
/// re-pointed at `api.doormile.com`.
const _knownGoodIPs = <String, String>{};
/// Creates an IOClient that:
/// 1. Bypasses SSL certificate errors
/// 2. Forces known-good IPs to avoid broken CDN nodes from carrier DNS
/// 3. Manually does TLS upgrade with correct SNI (hostname, not IP)
IOClient _buildSslBypassClient() {
final httpClient = HttpClient()
..badCertificateCallback = (X509Certificate cert, String host, int port) =>
true;
httpClient
.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
final host = uri.host;
final port = uri.port;
InternetAddress? target;
final knownIP = _knownGoodIPs[host];
if (knownIP != null) {
target = InternetAddress(knownIP);
} else {
try {
final addresses = await InternetAddress.lookup(
host,
type: InternetAddressType.IPv4,
);
if (addresses.isNotEmpty) target = addresses.first;
} catch (_) {}
}
if (uri.scheme == 'https') {
final socketFuture = Socket.connect(target ?? InternetAddress(host), port)
.then(
(plain) => SecureSocket.secure(
plain,
host: host,
onBadCertificate: (_) => true,
supportedProtocols: ['http/1.1'],
),
)
.then((s) => s as Socket);
return ConnectionTask.fromSocket<Socket>(socketFuture, () {});
}
return Socket.startConnect(target ?? InternetAddress(host), port);
};
return IOClient(httpClient);
}
/// ── The SSL bypass is gone ──
///
/// This file built its own `IOClient` with `onBadCertificate: (_) => true` and
/// a hand-rolled `SecureSocket` upgrade, so **every certificate was accepted**
/// — a delivery rider's position, his order ids and his session were readable
/// and rewritable by anything on the path. The parcel line deleted the same
/// code (see `providers/Riderlog/riderlog_provider.dart`); the delivery line
/// kept it because nothing in `lib/` imported this file directly.
///
/// It also sent **no `Authorization` header**. The URLs it posts to are v1
/// routes on `api.doormile.com` — resolved through the [DeliveryApi] facade —
/// so every one of these calls could only ever have been answered with a 401.
/// A plain client plus [ApiConfig.authHeaders] fixes both at once.
Future<Map<String, String>> _headers() => ApiConfig.authHeaders();
class CreateDeliveryLogProvider {
Future<Map<String, dynamic>?> createDeliveryLog(
@@ -70,19 +28,12 @@ class CreateDeliveryLogProvider {
bool wrapInArray = true,
}) async {
Map<String, dynamic>? result;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final body = json.encode(wrapInArray ? [data] : data);
final response = await client
.post(
url,
body: body,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.post(url, body: body, headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('createDeliveryLog url $urldata');
debugPrint(body);
@@ -110,18 +61,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updateDelivery url $urldata');
debugPrint('updateDelivery status ${response.statusCode}');
@@ -148,18 +92,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updateArrived url $urldata');
debugPrint('updateArrived status ${response.statusCode}');
@@ -185,18 +122,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updatePicked url $urldata');
debugPrint('updatePicked status ${response.statusCode}');
@@ -222,18 +152,11 @@ class UpdateDeliveryProvider {
String urldata,
) async {
Map<String, dynamic>? updateDeliveryResponse;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client
.put(
url,
body: json.encode(data),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
)
.put(url, body: json.encode(data), headers: await _headers())
.timeout(const Duration(seconds: 10));
debugPrint('updateActive url $urldata');
debugPrint('updateActive status ${response.statusCode}');
@@ -258,7 +181,7 @@ class UpdateDeliveryProvider {
class GetDeliveryLogProvider {
Future<Map<String, dynamic>?> getDeliveryLog(String urldata) async {
Map<String, dynamic>? result;
final client = _buildSslBypassClient();
final client = http.Client();
try {
final url = Uri.parse(urldata);
final response = await client.get(

View File

@@ -34,6 +34,7 @@ import 'package:miler/xpress/views/Dashboard/deliveries/deliveries.dart'
as deliveries;
import 'package:miler/xpress/background/live_tracking_service.dart';
import 'package:miler/xpress/data/delivery_api.dart';
import 'package:miler/data/heartbeat.dart';
/// Helper function to launch phone dialer - works in both debug and release builds
/// In release builds, canLaunchUrl may fail due to R8/ProGuard, so we always try to launch
@@ -320,11 +321,13 @@ class _HomepageState extends State<Homepage>
Future<Duration> _getLogInterval() async {
try {
final prefs = await SharedPreferences.getInstance();
final secs = prefs.getInt('logseconds');
final int interval = (secs != null && secs > 0) ? secs : 30;
// Already correct — routed through the shared resolver so the 30 lives in
// one place. See [kDefaultLogSeconds] for what a zero here used to cost
// the rider-log loop, which read the same pref and did not guard it.
final int interval = resolveLogSeconds(prefs.getInt('logseconds'));
return Duration(seconds: interval);
} catch (_) {
return const Duration(seconds: 30);
return const Duration(seconds: kDefaultLogSeconds);
}
}