Files
doormile_milderapp/lib/data/session.dart
Thiru-tenext d612916fe4 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
2026-09-18 11:05:40 +05:30

78 lines
4.0 KiB
Dart

import 'package:shared_preferences/shared_preferences.dart';
import 'package:miler/data/accepted_store.dart';
import 'package:miler/data/api_config.dart';
import 'package:miler/data/geofence.dart';
import 'package:miler/data/mutation_guard.dart';
import 'package:miler/data/proof_store.dart';
/// ─────────────────────────────────────────────────────────────────────────
/// ENDING A SESSION, IN ONE PLACE
///
/// A rider's session ends two ways, and until now only one of them was
/// implemented:
///
/// * **He presses Log out.** The Account screen tore everything down —
/// scoped stores, doorstep photos, bearer token, in-flight guard, cached
/// fix — raised `logged_out` and sent him to sign-in.
/// * **The server stops accepting his token.** `MilerApi` drops the token on
/// any 401 and calls `onUnauthorized`, which **nothing ever assigned**. So
/// the credential vanished and nothing else did.
///
/// The second case left a state that looks signed in and cannot work: the
/// profile is still in SharedPreferences, `logged_out` is still `false`, so
/// the app draws the rider's own name over a dashboard whose every call comes
/// back `401 authorization header is required`. Observed on a real handset —
/// Karthikeyan CBE Rider, rider 23, name and tenant on screen, `authtoken`
/// absent from disk, Home / Deliveries / Activity and the background heartbeat
/// all failing in a loop.
///
/// What that costs is not a blank screen. A rider opens the app, sees himself
/// signed in and sees no jobs, and concludes there is no work today — there is
/// no prompt anywhere telling him to sign in again. He waits; the stops go
/// undelivered; support cannot tell over the phone.
///
/// So the teardown lives here, both callers use it, and neither can drift from
/// the other. The rule it encodes: **the credential and the appearance of
/// being signed in end together, always.**
/// ─────────────────────────────────────────────────────────────────────────
/// Tears down everything this device holds about the signed-in rider.
///
/// Safe to call twice — every step is idempotent — which matters because a 401
/// rarely arrives alone: the home poll, the deliveries queue and the heartbeat
/// can all get one within the same second.
///
/// Deliberately knows nothing about navigation. Where the rider goes next is a
/// UI decision and belongs to the caller; this is the part that must happen
/// identically whether he chose to leave or was shown the door.
Future<void> endSession() async {
// Finished work, skipped stops, carried bags and bag labels are scoped per
// rider/tenant/line — this drops *this* scope so nothing survives into the
// next rider's session on a shared handset.
await clearScopedStores();
// Doorstep photos are exactly the kind of record that must not outlive the
// session that took them.
await ProofStore.clearScope();
// The credential goes with the session, not at the next sign-in. Anything
// that reads the token without checking a flag — a background isolate, the
// notification handler, a heartbeat that outlives a route change — must not
// be able to keep calling as the rider who has left.
await ApiConfig.clearToken();
// A stale in-flight mutation must not find a key still held from the session
// being closed.
MutationGuard.reset();
// A cached position is a fact about a rider and must not outlive him: the
// next person to sign in would have his first proximity check measured from
// wherever the last rider was standing.
Geofence.resetCache();
// The flag the launch path reads to decide between Home and sign-in.
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('logged_out', true);
}