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:
@@ -240,6 +240,48 @@ class UpdatePickupProvider {
|
||||
// consignment id, and pressing **Delivered** at the door was refused by
|
||||
// the app itself. Recorded here, at the only moment it is guaranteed to
|
||||
// be in hand. See [rememberConsignmentId].
|
||||
// ── What the rider captured, and what the contract can carry ──
|
||||
//
|
||||
// `POST /miler/bookings/:id/pickup-complete` takes **latitude and
|
||||
// longitude and nothing else**. The door flow, meanwhile, *demands* a
|
||||
// photograph of the parcels and — on a short count or a broken seal —
|
||||
// a written explanation before it will let the rider confirm
|
||||
// (`StopVerificationPage`, `_needsPickupNote`).
|
||||
//
|
||||
// So the app was compelling a rider to produce exactly the evidence
|
||||
// that matters in a dispute, uploading the photo to object storage, and
|
||||
// then dropping the URL and the note on the floor: this method reads
|
||||
// `dropimage` for the delivery leg and has never read `pickupimage`,
|
||||
// `proofimage` or `notes` on this one. Nobody at the hub could see any
|
||||
// of it, and nobody knew it was missing.
|
||||
//
|
||||
// Two things change here, and neither of them invents a field:
|
||||
//
|
||||
// * the photo and the note are kept **on the device**, against the
|
||||
// order, so the record exists and the office can ask for it — see
|
||||
// `ProofStore` and the completed-stop record;
|
||||
// * the gap is *logged*, every time, with what was dropped. It is the
|
||||
// same channel every other missing route reports through, so it
|
||||
// shows up where the rest of the contract gaps do instead of being
|
||||
// a thing one person remembers.
|
||||
//
|
||||
// BACKEND DEPENDENCY: see handoff BE-2. When `pickup-complete` accepts
|
||||
// `pickupimageurl` / `notes` / `condition`, send them here and delete
|
||||
// this block.
|
||||
final droppedProof = (data['pickupimage'] ?? data['proofimage'] ?? '')
|
||||
.toString()
|
||||
.trim();
|
||||
if (droppedProof.isNotEmpty || notes.trim().isNotEmpty) {
|
||||
ApiConfig.logGap(
|
||||
'pickup-complete',
|
||||
'The rider captured evidence this route cannot carry — '
|
||||
'photo=${droppedProof.isNotEmpty ? 'yes' : 'no'}, '
|
||||
'note=${notes.trim().isNotEmpty ? '"${notes.trim()}"' : 'none'}. '
|
||||
'pickup-complete accepts latitude/longitude only. Held on the '
|
||||
'device against booking $id. See BE-2.',
|
||||
);
|
||||
}
|
||||
|
||||
final picked = await MilerApi.pickupComplete(id, lat: lat, lon: lon);
|
||||
// ── Which lifecycle the server is running, read from the server ──
|
||||
//
|
||||
@@ -566,7 +608,37 @@ class UpdatePickupProvider {
|
||||
// and it keeps the chargeable total correct even though the per-parcel
|
||||
// figures are an even split rather than a measurement.
|
||||
final double each = totalWeight / count;
|
||||
final parcels = [for (var i = 0; i < count; i++) ParcelEntry(weight: each)];
|
||||
|
||||
// ── The photographs, at last ──
|
||||
//
|
||||
// `photos` has been on this contract since the route shipped and the app
|
||||
// has never sent it. The keys arrive on the verification map (see
|
||||
// `_withParcelPhotos`); an empty list means the upload failed, and the
|
||||
// field is then omitted rather than sent empty — an empty array is a claim
|
||||
// that nobody photographed anything.
|
||||
//
|
||||
// Attached to the FIRST parcel only. The rider takes one photograph of the
|
||||
// load, not one per box — repeating the same key on every parcel would
|
||||
// report N photographs where one was taken.
|
||||
final photoKeys = <String>[
|
||||
for (final k in (pickup['photos'] as List?) ?? const [])
|
||||
if (k.toString().trim().isNotEmpty) k.toString().trim(),
|
||||
];
|
||||
|
||||
final parcels = [
|
||||
for (var i = 0; i < count; i++)
|
||||
ParcelEntry(
|
||||
weight: each,
|
||||
photos: i == 0 ? photoKeys : const <String>[],
|
||||
),
|
||||
];
|
||||
if (photoKeys.isEmpty) {
|
||||
ApiConfig.logGap(
|
||||
'submitParcels',
|
||||
'booking $bookingId: no photo key available — the parcels are being '
|
||||
'recorded without the door photograph.',
|
||||
);
|
||||
}
|
||||
|
||||
final res = await MilerApi.submitParcels(bookingId, parcels);
|
||||
debugPrint(
|
||||
|
||||
Reference in New Issue
Block a user