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:
@@ -78,9 +78,68 @@ class MilkRun {
|
||||
static const bool deliveryArrivalIsLocalOnly = true;
|
||||
|
||||
/// Order id of a stop, however the payload spells it.
|
||||
///
|
||||
/// On a customer-app pickup with several destinations this is the **stop**
|
||||
/// key, not the booking's — `DM-482913#1` — because everything the device
|
||||
/// remembers is filed under it and three doors sharing one key means three
|
||||
/// doors sharing one memory. The suffix is added by `ApiConfig` where the row
|
||||
/// is adapted; see the note there. [bookingKeyOf] is the plain booking key,
|
||||
/// and `bookingreference` on the row is what the rider is shown.
|
||||
static String idOf(Map<String, dynamic> stop) =>
|
||||
(stop['orderid'] ?? stop['orderId'] ?? '').toString();
|
||||
|
||||
/// The booking this stop belongs to, with any destination suffix removed.
|
||||
///
|
||||
/// Used where the fact being recorded is about the **visit** rather than the
|
||||
/// drop. Collection is the case that matters: the rider collects a whole
|
||||
/// pickup once, so the collected set is written with the booking key while
|
||||
/// the delivery stops that come out of it each carry their own. See
|
||||
/// [wasCollected].
|
||||
static String bookingKeyOf(Map<String, dynamic> stop) {
|
||||
final raw = idOf(stop);
|
||||
final cut = raw.indexOf('#');
|
||||
if (cut > 0) return raw.substring(0, cut);
|
||||
final ref = (stop['bookingreference'] ?? '').toString().trim();
|
||||
return ref.isNotEmpty ? ref : raw;
|
||||
}
|
||||
|
||||
/// How many doors this one pickup visit is for. 1 for everything but a
|
||||
/// multi-destination customer-app booking.
|
||||
static int destinationCountOf(Map<String, dynamic> stop) =>
|
||||
int.tryParse((stop['destinationcount'] ?? '').toString()) ?? 1;
|
||||
|
||||
/// Which door of the visit this stop is, counting from 0.
|
||||
static int destinationSeqOf(Map<String, dynamic> stop) =>
|
||||
int.tryParse((stop['destinationseq'] ?? '').toString()) ?? 0;
|
||||
|
||||
/// `Stop 2 of 3`, or `''` when the pickup has only one door.
|
||||
///
|
||||
/// Empty rather than `Stop 1 of 1` deliberately: a label that appears on
|
||||
/// every card in the app stops being read, and the fact it carries — this
|
||||
/// bag is one of several from the same counter — is only ever news when
|
||||
/// there are several.
|
||||
static String stopLabel(Map<String, dynamic> stop) {
|
||||
final count = destinationCountOf(stop);
|
||||
if (count <= 1) return '';
|
||||
return 'Stop ${destinationSeqOf(stop) + 1} of $count';
|
||||
}
|
||||
|
||||
/// True when the visit this stop came from has been collected.
|
||||
///
|
||||
/// Asks for the stop's own key first and the booking's second, and the second
|
||||
/// question is the one that matters. A multi-destination pickup is collected
|
||||
/// **once**, under the booking key, while it is still a single pre-pickup
|
||||
/// row; the poll after `pickup-complete` then replaces that row with one per
|
||||
/// door, each carrying a key the collected set has never seen. Asking only
|
||||
/// the stop key would drop every one of those bags back to "not collected"
|
||||
/// at the moment the rider actually had them in his hands.
|
||||
static bool wasCollected(
|
||||
Map<String, dynamic> stop,
|
||||
Set<String> collectedIds,
|
||||
) =>
|
||||
collectedIds.contains(idOf(stop)) ||
|
||||
collectedIds.contains(bookingKeyOf(stop));
|
||||
|
||||
/// The consignment this stop became once it was collected, or '' before that.
|
||||
///
|
||||
/// The delivery route keys on this and not on the booking id — they come from
|
||||
@@ -326,7 +385,7 @@ class MilkRun {
|
||||
// this function did before and is still right for a milk run, whose rows
|
||||
// often carry no consignment state at all.
|
||||
if (!ServiceProfile.active.deliversToCustomer) return false;
|
||||
if (collectedIds.contains(idOf(stop))) return true;
|
||||
if (wasCollected(stop, collectedIds)) return true;
|
||||
final status = stopStatusOf(stop);
|
||||
return status.isPicked || status.isDeliveryLeg;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user