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:
2026-09-18 11:05:40 +05:30
parent 127fa062ed
commit d612916fe4
53 changed files with 6346 additions and 748 deletions

View File

@@ -527,16 +527,22 @@ void main() {
},
);
test('the geofence bypass does not stop the write leaving', () async {
// The suspicion this rules out. `_checkGeofence` returns true early when
// enforcement is off and its success branch contains nothing else — so
// the bypass skips the distance *rejection* and nothing more.
test('the provider itself holds no proximity gate', () async {
// ── What this test used to say ──
//
// Asserted through the provider rather than by reading the flag, because
// "the request went out with the fence off" is the claim, and the flag is
// only evidence for it.
expect(kBypassGeofenceForTesting, isTrue);
// `expect(kBypassGeofenceForTesting, isTrue)` — it asserted the fence was
// OFF, and then that the write still went out. The flag is gone: the
// fence is enforced by default now and lives in `lib/data/geofence.dart`.
//
// The claim underneath it survives and is worth keeping: **the fence is
// in the controller, not in the transport.** `UpdatePickupProvider` maps
// a legacy payload onto a v1 route and posts it, full stop. If a
// proximity check ever appears down here there would be two fences again,
// reachable by different paths, which is the shape that gave the app a
// 500 m radius on Home and a 10 m one on a stop.
//
// The controller-level rule — blocked means no request — is asserted
// end-to-end in `geofence_test.dart`.
await UpdatePickupProvider().updatePickup({
'pickupid': 4211,
'orderstatus': 'arrived',
@@ -544,7 +550,7 @@ void main() {
'riderslon': '76.955800',
});
expect(sent, isNotEmpty, reason: 'the bypass swallowed the request');
expect(sent, isNotEmpty, reason: 'the provider swallowed the request');
expect(only('reached').url.path, contains('/4211/reached'));
});
});