Session expiry, arrival geofence guard, multi-destination stops
Three fixes found by running the app on a real handset against production. 1. An expired token left the app looking signed in and unable to work. MilerApi.onUnauthorized was declared and called on every 401 but never assigned, so the token was dropped and nothing else happened: the profile stayed on disk, logged_out stayed false, and the rider saw his own name over a dashboard whose every call returned 401. He reads that as "no work today". The teardown now lives in endSession() and both ways out of a session — the Log out button and the 401 path — use it. 2. Arrived was written locally even when the rider was not there. updateArrivedStatus answers false for three different things and the caller treated all of them as "the write did not land", which is only true of one. A geofence refusal and a server refusal now stop the rung and hand back the reason; a dead network still advances, as it should. 3. A multi-destination customer pickup collapsed onto one stop. GET /miler/bookings returns a row per destination once collected, all with the same bookingid and reference. Every local store keys on that id, so the accepted store deduped two of three drops away and their consignment ids were unrecoverable. orderid is now the stop key; bookingreference stays the booking's name. Cards show "Stop 2 of 3" and the receiver's own name and number rather than the sender's. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:miler/data/geofence.dart';
|
||||
import 'package:miler/data/work_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart' show LatLng;
|
||||
@@ -19,133 +20,38 @@ import 'package:minio/io.dart';
|
||||
import 'package:miler/utils/kalman_filter.dart';
|
||||
import 'package:miler/utils/mqtt_service.dart';
|
||||
import 'package:miler/controllers/connectivity_mixin.dart';
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/data/meal_run_mock.dart';
|
||||
import 'package:miler/views/helpers/widgets/app_widgets.dart';
|
||||
|
||||
/// ── PROXIMITY ENFORCEMENT IS OFF ──
|
||||
/// ── PROXIMITY ENFORCEMENT IS ON ──
|
||||
///
|
||||
/// Off again on 2026-08-25, the same day it was turned on, at the founder's
|
||||
/// call. Nothing was found wrong with it — this is a decision about when to
|
||||
/// switch it on, not a retraction of the work.
|
||||
/// The fence itself moved to `lib/data/geofence.dart` on 2026-09-16. What used
|
||||
/// to live here — four constants, a `_freshFix` ladder and a 130-line
|
||||
/// `_checkGeofence` — could only be reached from a `GetxController`, so Home's
|
||||
/// bulk gate grew a second copy of the arithmetic with a different radius, and
|
||||
/// none of it could be tested without a real handset.
|
||||
///
|
||||
/// It is off for **every** status — arrived, picked, picked up, delivery
|
||||
/// arrived, delivered, cancelled — and on both gates, this one and the bulk
|
||||
/// check on Home. Half a fence is worse than none: it would block one route
|
||||
/// into a rung and wave through another, with two different messages and no
|
||||
/// explanation of why one worked.
|
||||
/// [Geofence] is that logic, extracted and corrected. Four things changed with
|
||||
/// it, and each one was a way a rider could mark a stop he was not standing at:
|
||||
///
|
||||
/// ── What it costs while it is off ──
|
||||
/// * **It is on.** `kGeofenceEnforced` defaulted to `false`, so
|
||||
/// `_checkGeofence` returned `true` on its third line in every shipped
|
||||
/// build.
|
||||
/// * **The radius is 100 m, not 10.** Ten metres is inside consumer-GPS error,
|
||||
/// which is what got the fence switched off twice before.
|
||||
/// * **It fails closed.** The old `catch` returned `true` with the comment
|
||||
/// "allow on error (fail-safe)". Any throw from the location stack opened
|
||||
/// every rung in the app.
|
||||
/// * **The phone's error is no longer credited to the rider.** The comparison
|
||||
/// was `distance - accuracy > radius`, so slack grew with inaccuracy and a
|
||||
/// ±250 m fix bought 250 m of it. A fix too vague to resolve the fence is
|
||||
/// refused instead — see `kGeofenceMaxAccuracyMetres`.
|
||||
///
|
||||
/// This is the one control that decides whether the app is telling the truth
|
||||
/// about where a rider was standing when he said a stop was done. With it off,
|
||||
/// "Picked up" means he pressed a button, not that he was there — the
|
||||
/// timestamps and the GPS still ride along on every status write, so the office
|
||||
/// can audit after the fact, but nothing is refused at the moment of the press.
|
||||
///
|
||||
/// ── What is ready for the day it goes back on ──
|
||||
///
|
||||
/// Recorded here because the work is done and the flag is the only thing
|
||||
/// holding it, so nobody has to rediscover any of it. The fence was off from
|
||||
/// 2026-08-19 because it refused real work — riders pressing **Picked up** at a
|
||||
/// counter were told "You're 4.2 km from this stop" — and that was a
|
||||
/// measurement problem rather than a strictness one. Three causes, all fixed:
|
||||
///
|
||||
/// • **The fix was not worth measuring with.** Home handed the fence a
|
||||
/// `LocationAccuracy.low` position — a ~1 km hint on Android — or a cached
|
||||
/// one of any age. See [_freshFix] and [kGeofenceFixMaxAge].
|
||||
/// • **The phone's own error was charged to the rider.** The comparison is
|
||||
/// `distance - accuracy > radius`, so a rider 12 m out on a ±20 m fix is not
|
||||
/// refused for a precision the hardware never provided.
|
||||
/// • **There were three radii.** A configured `pickupradius` defaulting to
|
||||
/// 100 here, a hardcoded 500 in Home's bulk gate, and no agreement between
|
||||
/// them. There is one now: [kGeofenceRadiusMeters], set to 10.
|
||||
///
|
||||
/// All three are live in the code below and simply do not run while this is
|
||||
/// false. The fourth cause is real and none of this fixes it: **the booking's
|
||||
/// own coordinates are often wrong**, because they come from wherever the
|
||||
/// customer dropped a pin. A stop carrying *no* coordinates is allowed through
|
||||
/// — that is the hub's data, not something a rider can resolve from a doorstep
|
||||
/// — but a stop carrying wrong ones will still refuse him.
|
||||
///
|
||||
/// ── Turning it on ──
|
||||
///
|
||||
/// flutter run --dart-define=ENFORCE_GEOFENCE=true
|
||||
/// flutter build apk --dart-define=ENFORCE_GEOFENCE=true
|
||||
///
|
||||
/// Or flip the default. Ten metres is tight — at or inside consumer GPS
|
||||
/// accuracy — so if the first reports are riders blocked at doors, raise
|
||||
/// [kGeofenceRadiusMeters] before reaching for this switch again.
|
||||
///
|
||||
/// Every bypass is logged in every build mode (see `_checkGeofence`), so a
|
||||
/// build's own log says which way it was compiled.
|
||||
///
|
||||
/// ── The history, so none of the old mistakes come back ──
|
||||
///
|
||||
/// This was once `kDebugMode`, which meant the fence was off in exactly the
|
||||
/// builds anyone tested with — so it was never really tested. It was then
|
||||
/// pinned to a hard `false`, which left no way to walk the flow at a desk. The
|
||||
/// shape below survives both: one named constant, one define, one default, and
|
||||
/// the default is a product decision rather than a side effect of how the app
|
||||
/// was built.
|
||||
const bool kGeofenceEnforced = bool.fromEnvironment(
|
||||
'ENFORCE_GEOFENCE',
|
||||
defaultValue: false,
|
||||
);
|
||||
|
||||
/// The name every call site reads. Derived, so there is one switch and not two.
|
||||
const bool kBypassGeofenceForTesting = !kGeofenceEnforced;
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// HOW CLOSE IS "AT THE STOP" — 10 metres
|
||||
///
|
||||
/// A product decision, taken deliberately and tight: the rider must be *at the
|
||||
/// door*, not on the street outside it, before he can mark a stop arrived,
|
||||
/// picked or delivered.
|
||||
///
|
||||
/// ── Why this is a constant and no longer the server's `pickupradius` ──
|
||||
///
|
||||
/// The fence used to read `pickupradius` out of prefs, which login writes from
|
||||
/// the profile and defaults to 100. That made the strictness of the app's one
|
||||
/// honesty control a per-tenant configuration value nobody on this side could
|
||||
/// see, and it silently disagreed with the second gate on Home, which was
|
||||
/// hardcoded to 500. Three numbers for one rule. This is the rule.
|
||||
///
|
||||
/// ── What 10 metres actually demands, stated plainly ──
|
||||
///
|
||||
/// This is at or inside the accuracy of consumer GPS. A phone reports a fix
|
||||
/// with an error radius, and 5–15 m in the open is normal while 30–50 m
|
||||
/// between buildings is ordinary rather than exceptional. A fence smaller than
|
||||
/// the error it is measured with will refuse a rider who is genuinely standing
|
||||
/// at the door — which is the exact failure that got this whole control turned
|
||||
/// off once before, and the reason two things below are not optional:
|
||||
///
|
||||
/// • **The fix must be worth 10 m.** [LocationAccuracy.best], not the `low`
|
||||
/// the callers were passing — `low` is a ~1 km hint on Android and against
|
||||
/// a 10 m fence it is not a measurement, it is a coin toss. See
|
||||
/// [_freshFix].
|
||||
///
|
||||
/// • **The phone's own error is credited to the rider.** The check is
|
||||
/// `distance - accuracy > radius`, not `distance > radius`: a rider 12 m
|
||||
/// away on a fix that says ±20 m has not been shown to be outside the
|
||||
/// fence, and refusing him is asserting a precision the hardware did not
|
||||
/// provide. He is refused when the *phone* says he is outside, not when the
|
||||
/// arithmetic does.
|
||||
///
|
||||
/// Together those keep 10 m meaning "at the door" without it meaning "when the
|
||||
/// satellites are kind". If riders still report being blocked at a door, this
|
||||
/// number is the knob — raise it here, in one place, rather than turning the
|
||||
/// fence off again.
|
||||
const double kGeofenceRadiusMeters = 10;
|
||||
|
||||
/// How stale a cached fix may be before the fence refuses to measure with it.
|
||||
///
|
||||
/// A last-known position is instant and free and can be an hour old. Against a
|
||||
/// 100 m fence that was survivable; against 10 m it is how a rider marks a
|
||||
/// delivery arrived from the previous street because that is where the phone
|
||||
/// last looked.
|
||||
const Duration kGeofenceFixMaxAge = Duration(seconds: 30);
|
||||
|
||||
/// What is left in this file is the wiring: [_checkGeofence] adapts [Geofence]
|
||||
/// to the eight call sites below, and every one of them returns **before**
|
||||
/// building a payload when it answers false. That is asserted end-to-end in
|
||||
/// `geofence_test.dart` with an HTTP client that fails the test if any request
|
||||
/// is sent.
|
||||
class PickupsController extends GetxController
|
||||
with ConnectivityControllerMixin {
|
||||
MilerKalmanFilter? _kf;
|
||||
@@ -332,12 +238,12 @@ class PickupsController extends GetxController
|
||||
// ---------------- Location helpers ----------------
|
||||
/// The error radius, in metres, of the fix [_ensureLatLng] last obtained.
|
||||
///
|
||||
/// Read by [_checkGeofence], which credits it to the rider — see
|
||||
/// [kGeofenceRadiusMeters]. Starts at zero so a fence measured before any fix
|
||||
/// has been taken is strict rather than accidentally generous.
|
||||
/// Telemetry only. The fence no longer reads this — [Geofence] takes its own
|
||||
/// fix and rejects one it cannot trust rather than crediting its error to the
|
||||
/// rider. Kept because the payload builders log it.
|
||||
double _lastFixAccuracy = 0;
|
||||
|
||||
/// A position good enough to measure a [kGeofenceRadiusMeters] fence with.
|
||||
/// A position good enough to put on a status payload.
|
||||
///
|
||||
/// ── What this replaced, and why it had to go ──
|
||||
///
|
||||
@@ -380,7 +286,7 @@ class PickupsController extends GetxController
|
||||
} else if (cached != null) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] cached fix is ${age?.inSeconds}s old — too stale to '
|
||||
'measure a ${kGeofenceRadiusMeters.toStringAsFixed(0)}m fence',
|
||||
'measure a ${kGeofenceRadiusMetres.toStringAsFixed(0)}m fence',
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -398,34 +304,28 @@ class PickupsController extends GetxController
|
||||
final needsFetch =
|
||||
(lat == '0' || lat.isEmpty || lng == '0' || lng.isEmpty);
|
||||
|
||||
// ── The two shortcuts below are disabled while the fence is on ──
|
||||
// ── These shortcuts are safe again, because nothing is decided on them ──
|
||||
//
|
||||
// Every caller of this method feeds its answer to [_checkGeofence] and
|
||||
// then puts the same pair on the payload. Both shortcuts hand back a
|
||||
// position of unknown provenance: the first trusts whatever the screen
|
||||
// passed in — Home passes a `LocationAccuracy.low` fix, which is a ~1 km
|
||||
// hint — and the second reuses a cached value with no age on it at all.
|
||||
// Neither carries an accuracy, so [_lastFixAccuracy] would be stale too
|
||||
// and the fence would measure a 10 m rule with a number it cannot
|
||||
// characterise.
|
||||
// They used to be gated on the fence being off, and rightly: the fence
|
||||
// measured with whatever this returned, and both shortcuts hand back a
|
||||
// position of unknown provenance — the first trusts whatever the screen
|
||||
// passed in (Home passes a `LocationAccuracy.low` fix, a ~1 km hint on
|
||||
// Android), the second reuses a cached value with no age on it at all.
|
||||
//
|
||||
// With the fence enforced this takes one real fix per status write. That
|
||||
// is a few seconds, once, at a door the rider is standing still at — and
|
||||
// it is the whole basis on which the app is about to refuse or allow his
|
||||
// press. With the fence off the shortcuts stand: nothing is being decided
|
||||
// on the answer, it is telemetry.
|
||||
if (kBypassGeofenceForTesting) {
|
||||
// Fast path: if we have valid coordinates, use them immediately
|
||||
if (!needsFetch) return {'lat': outLat, 'lng': outLng};
|
||||
// [Geofence] takes its own `best` fix now and characterises it before
|
||||
// deciding, so this method is back to what its name says: the coordinates
|
||||
// that go **on the payload**. Telemetry, not evidence. Taking a second
|
||||
// 8-second fix for it would cost the rider time at every door to improve
|
||||
// a number nobody adjudicates.
|
||||
// Fast path: if we have valid coordinates, use them immediately
|
||||
if (!needsFetch) return {'lat': outLat, 'lng': outLng};
|
||||
|
||||
// Reuse recently cached coordinates first if fresh (e.g. within 30s)
|
||||
// For now just check if they exist to save time
|
||||
if (currentLat.value.isNotEmpty &&
|
||||
currentLat.value != '0' &&
|
||||
currentLng.value.isNotEmpty &&
|
||||
currentLng.value != '0') {
|
||||
return {'lat': currentLat.value, 'lng': currentLng.value};
|
||||
}
|
||||
// Reuse recently cached coordinates first if fresh (e.g. within 30s)
|
||||
if (currentLat.value.isNotEmpty &&
|
||||
currentLat.value != '0' &&
|
||||
currentLng.value.isNotEmpty &&
|
||||
currentLng.value != '0') {
|
||||
return {'lat': currentLat.value, 'lng': currentLng.value};
|
||||
}
|
||||
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
@@ -1212,12 +1112,15 @@ class PickupsController extends GetxController
|
||||
|
||||
// ---------------- Geofencing helpers ----------------
|
||||
//
|
||||
// The radius is [kGeofenceRadiusMeters] and nothing else. It read the
|
||||
// server's `pickupradius` out of prefs, which meant the app's one honesty
|
||||
// control was a per-tenant number nobody here could see — and it disagreed
|
||||
// with Home's own hardcoded 500. `pickupradius` is still stored at login;
|
||||
// it simply no longer decides this.
|
||||
double get _geofenceRadius => kGeofenceRadiusMeters;
|
||||
// The radius is [kGeofenceRadiusMetres], in `lib/data/geofence.dart`, and
|
||||
// nothing else. It read the server's `pickupradius` out of prefs, which made
|
||||
// the app's one honesty control a per-tenant number nobody here could see —
|
||||
// and it disagreed with Home's own hardcoded 500. `pickupradius` is still
|
||||
// stored at login; it simply no longer decides this.
|
||||
|
||||
/// The fence's answer to the last rung that asked, for screens that want to
|
||||
/// show the live distance rather than only the refusal.
|
||||
GeofenceDecision? lastGeofence;
|
||||
|
||||
// Helper method to show snackbar reliably in both debug and release builds
|
||||
//
|
||||
@@ -1304,6 +1207,18 @@ class PickupsController extends GetxController
|
||||
/// a stop the hub keeps rejecting. See [updatePickedStatus].
|
||||
String? lastPickupRefusal;
|
||||
|
||||
/// The proximity gate for one rung. **False means do not call the API.**
|
||||
///
|
||||
/// Adapts [Geofence] to the call sites below. The rider's own coordinates are
|
||||
/// no longer taken as arguments: the fence takes its *own* fix, at
|
||||
/// [LocationAccuracy.best], because the pair the screens pass in comes from a
|
||||
/// `low`-accuracy Home poll or an unaged cache and carries no accuracy at all
|
||||
/// — so the fence could not tell how much to trust the number it was
|
||||
/// measuring a 100 m rule with. The parameters stay for the eight call sites
|
||||
/// that build a payload from the same values; they are ignored here.
|
||||
///
|
||||
/// Sets [lastBlockedReason] to the rider-facing sentence on every refusal, so
|
||||
/// a caller can say what happened instead of reporting a network failure.
|
||||
Future<bool> _checkGeofence(
|
||||
double targetLat,
|
||||
double targetLng,
|
||||
@@ -1313,132 +1228,20 @@ class PickupsController extends GetxController
|
||||
) async {
|
||||
lastBlockedReason = null;
|
||||
|
||||
// Opt-in via `--dart-define=BYPASS_GEOFENCE=true`; enforced otherwise. See
|
||||
// the declaration for why it is a define rather than a constant.
|
||||
//
|
||||
// Logged with `debugPrint` in *every* build mode, deliberately — not behind
|
||||
// `kDebugMode` like the diagnostics below. The whole risk of this flag is a
|
||||
// build going out with proximity silently off, so the one thing it must
|
||||
// never be is quiet.
|
||||
if (kBypassGeofenceForTesting) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] OFF for $action — enforcement is disabled in this build. '
|
||||
'Stop completion is NOT proximity-verified. '
|
||||
'See kGeofenceEnforced.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Validate coordinates - ensure they are valid GPS coordinates
|
||||
final bool hasValidTarget =
|
||||
targetLat != 0 &&
|
||||
targetLng != 0 &&
|
||||
targetLat.abs() <= 90 &&
|
||||
targetLng.abs() <= 180;
|
||||
final bool hasValidCurrent =
|
||||
currentLat != 0 &&
|
||||
currentLng != 0 &&
|
||||
currentLat.abs() <= 90 &&
|
||||
currentLng.abs() <= 180;
|
||||
|
||||
// ── Two ways to have no coordinates, and only one of them is the rider's ──
|
||||
//
|
||||
// This used to treat both the same and wave both through: "Missing
|
||||
// coordinates. Proceeding with update." That is the bypass that makes a
|
||||
// fence decorative — turn location off and every rung opens — and it was
|
||||
// survivable only because the fence itself was off.
|
||||
//
|
||||
// **No target.** The booking carries no pin. That is the hub's data, the
|
||||
// rider cannot fix it from a doorstep, and blocking him leaves the stop
|
||||
// unworkable by anyone. Allowed, and logged, exactly as before.
|
||||
if (!hasValidTarget) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] $action allowed: the stop carries no coordinates '
|
||||
'($targetLat, $targetLng), so proximity cannot be checked. This is a '
|
||||
'data gap on the booking, not a rider who is somewhere else.',
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// **No fix.** Location is off, permission is denied, or the GPS did not
|
||||
// settle in time. This one the rider *can* fix, and it is the difference
|
||||
// between a fence and a suggestion — so it is refused, and the message
|
||||
// says which of the three to go and change.
|
||||
if (!hasValidCurrent) {
|
||||
debugPrint(
|
||||
'[GEOFENCE] $action refused: no usable fix '
|
||||
'($currentLat, $currentLng)',
|
||||
);
|
||||
lastBlockedReason =
|
||||
'Your phone could not find your location, so this stop cannot be '
|
||||
'marked ${action.toLowerCase()}. Turn location on, allow it for '
|
||||
'Miler, and step outside if you can.';
|
||||
_showErrorSnackbar('Location Error', lastBlockedReason!, seconds: 5);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final radius = _geofenceRadius;
|
||||
final distance = Geolocator.distanceBetween(
|
||||
targetLat,
|
||||
targetLng,
|
||||
currentLat,
|
||||
currentLng,
|
||||
);
|
||||
final distanceKm = distance / 1000.0;
|
||||
final distanceMeters = distance;
|
||||
|
||||
// ── The phone's own error is credited to the rider ──
|
||||
//
|
||||
// A fix carries an accuracy in metres, and at a 10 m fence that number
|
||||
// is the same size as the thing being measured. Comparing a raw distance
|
||||
// against 10 m asserts a precision the hardware did not provide, and the
|
||||
// rider standing at the door on a ±25 m fix is the one it refuses.
|
||||
//
|
||||
// So the fence is measured against the *nearest point the phone allows*:
|
||||
// 12 m away on a ±20 m fix has not been shown to be outside it. He is
|
||||
// blocked when the phone says he is outside, not when the arithmetic
|
||||
// does. See [kGeofenceRadiusMeters].
|
||||
final slack = _lastFixAccuracy;
|
||||
final effective = (distance - slack).clamp(0.0, double.infinity);
|
||||
|
||||
debugPrint(
|
||||
'[GEOFENCE] $action | target ($targetLat, $targetLng) '
|
||||
'| rider ($currentLat, $currentLng) '
|
||||
'| ${distanceMeters.toStringAsFixed(1)}m ±${slack.toStringAsFixed(0)}m '
|
||||
'→ ${effective.toStringAsFixed(1)}m vs ${radius.toStringAsFixed(0)}m',
|
||||
);
|
||||
|
||||
if (effective > radius) {
|
||||
// ── One sentence, in metres he can act on ──
|
||||
//
|
||||
// Was three lines of "Distance: 4213 m (4.21 km) / Required: Within
|
||||
// 100 m" — a readout, in a snackbar, on a phone in a jacket pocket.
|
||||
// What the rider needs is how far he still has to go.
|
||||
final String away = distanceMeters >= 1000
|
||||
? '${distanceKm.toStringAsFixed(1)} km'
|
||||
: '${distanceMeters.toStringAsFixed(0)} m';
|
||||
lastBlockedReason =
|
||||
"You're $away from this stop — get within "
|
||||
'${radius.toStringAsFixed(0)} m to mark it '
|
||||
'${action.toLowerCase()}';
|
||||
_showErrorSnackbar('Location Error', lastBlockedReason!, seconds: 5);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('[GEOFENCE] Error calculating distance: $e');
|
||||
}
|
||||
// On error, show warning but allow (fail-safe)
|
||||
_showErrorSnackbar(
|
||||
'Location Warning',
|
||||
'Unable to verify distance. Proceeding with caution.',
|
||||
bgColor: ColorConstants.warning,
|
||||
seconds: 3,
|
||||
);
|
||||
return true; // Allow on error (fail-safe)
|
||||
final decision = await Geofence.check(
|
||||
targetLat: targetLat,
|
||||
targetLng: targetLng,
|
||||
action: action,
|
||||
);
|
||||
|
||||
lastGeofence = decision;
|
||||
if (decision.allowed) return true;
|
||||
|
||||
lastBlockedReason = decision.reason;
|
||||
if (decision.reason != null) {
|
||||
_showErrorSnackbar('Location Error', decision.reason!, seconds: 5);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get last pickup location (for calculating riderkms between pickup)
|
||||
@@ -1824,6 +1627,14 @@ class PickupsController extends GetxController
|
||||
final mock = _mockStatus(pickupId, 'arrived');
|
||||
if (mock != null) return mock;
|
||||
|
||||
// Cleared on entry, not only on the paths that set them. The caller now
|
||||
// reads these to decide whether a failed arrival may still advance the rung
|
||||
// locally, and a value left over from an earlier stop would answer for this
|
||||
// one — refusing an arrival because a different door refused twenty minutes
|
||||
// ago. `lastBlockedReason` gets the same treatment inside `_checkGeofence`.
|
||||
lastArrivalRefusal = null;
|
||||
lastArrivalNotice = null;
|
||||
|
||||
// START LOADING IMMEDIATELY for better UX
|
||||
arrivedShimmer.value = true;
|
||||
try {
|
||||
@@ -1878,6 +1689,20 @@ class PickupsController extends GetxController
|
||||
? resp!['message'].toString()
|
||||
: 'Your office would not accept this arrival.')
|
||||
: null;
|
||||
// ── A write that never landed must not look like one that did ──
|
||||
//
|
||||
// `lastArrivalRefusal` stays null on a transport failure, deliberately:
|
||||
// the caller lets a rider with no signal carry on rather than stranding
|
||||
// him at a door. But it then advanced the rung *silently*, so the rider
|
||||
// saw ARRIVED and had no way to know the hub had not been told — and
|
||||
// the first person to find out was an operator wondering why he had
|
||||
// been at a kitchen for forty minutes.
|
||||
//
|
||||
// He still carries on. He is simply told.
|
||||
lastArrivalNotice = lastArrivalRefusal == null
|
||||
? 'Marked arrived on your phone. Your office has not been told — '
|
||||
'you had no connection. Tell them if it matters.'
|
||||
: null;
|
||||
debugPrint('[UPDATE][ARRIVED][FAILED] resp=${jsonEncode(resp)}');
|
||||
return false;
|
||||
}
|
||||
@@ -1957,18 +1782,25 @@ class PickupsController extends GetxController
|
||||
final dLat = double.tryParse(dropLat) ?? 0.0;
|
||||
final dLng = double.tryParse(dropLng) ?? 0.0;
|
||||
|
||||
// ── Unconditional ──
|
||||
//
|
||||
// This was wrapped in `if (dLat != 0 && dLng != 0)`, so a consignment
|
||||
// whose drop pin was missing skipped the fence entirely — silently, with
|
||||
// no log and no record. That is the one case where the fence matters
|
||||
// most: nobody can say afterwards where the rider was standing. A missing
|
||||
// pin is now [GeofenceOutcome.noTarget] and is refused with a sentence
|
||||
// that sends the rider to his office.
|
||||
//
|
||||
// Fenced against the DROP, not the pickup. Using the pickup coordinates
|
||||
// here would fence the rider to the kitchen he left an hour ago.
|
||||
if (dLat != 0 && dLng != 0) {
|
||||
final inFence = await _checkGeofence(
|
||||
dLat,
|
||||
dLng,
|
||||
rLat,
|
||||
rLng,
|
||||
'Delivery arrived',
|
||||
);
|
||||
if (!inFence) return false;
|
||||
}
|
||||
// here would fence the rider to the counter he left an hour ago.
|
||||
final inFence = await _checkGeofence(
|
||||
dLat,
|
||||
dLng,
|
||||
rLat,
|
||||
rLng,
|
||||
'Delivery arrived',
|
||||
);
|
||||
if (!inFence) return false;
|
||||
|
||||
// The clock the completion record reads, same key the pickup leg uses.
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
@@ -2027,16 +1859,25 @@ class PickupsController extends GetxController
|
||||
final dLat = double.tryParse(dropLat) ?? 0.0;
|
||||
final dLng = double.tryParse(dropLng) ?? 0.0;
|
||||
|
||||
if (dLat != 0 && dLng != 0) {
|
||||
final inFence = await _checkGeofence(
|
||||
dLat,
|
||||
dLng,
|
||||
rLat,
|
||||
rLng,
|
||||
'Delivered',
|
||||
);
|
||||
if (!inFence) return false;
|
||||
}
|
||||
// ── Unconditional ──
|
||||
//
|
||||
// This was wrapped in `if (dLat != 0 && dLng != 0)`, so a consignment
|
||||
// whose drop pin was missing skipped the fence entirely — silently, with
|
||||
// no log and no record. That is the one case where the fence matters
|
||||
// most: nobody can say afterwards where the rider was standing. A missing
|
||||
// pin is now [GeofenceOutcome.noTarget] and is refused with a sentence
|
||||
// that sends the rider to his office.
|
||||
//
|
||||
// Fenced against the DROP, not the pickup. Using the pickup coordinates
|
||||
// here would fence the rider to the counter he left an hour ago.
|
||||
final inFence = await _checkGeofence(
|
||||
dLat,
|
||||
dLng,
|
||||
rLat,
|
||||
rLng,
|
||||
'Delivered',
|
||||
);
|
||||
if (!inFence) return false;
|
||||
|
||||
_pickedTime = _formatDateTimeFull(DateTime.now());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user