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

@@ -15,7 +15,7 @@ library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart' show kDebugMode, mapEquals, setEquals;
import 'package:flutter/foundation.dart' show mapEquals, setEquals;
import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:miler/views/helpers/constants/design_constants.dart';
@@ -82,6 +82,10 @@ import 'package:miler/data/service_day.dart';
import 'package:miler/data/work_domain.dart';
import 'package:miler/data/work_repository.dart';
import 'package:miler/data/miler_api.dart';
import 'package:miler/data/lifecycle.dart';
import 'package:miler/data/geofence.dart';
import 'package:miler/data/task_profile.dart';
import 'package:miler/data/pickup_locations.dart';
import 'package:miler/data/assignment_lookup.dart';
import 'package:miler/utils/external_navigation.dart';
@@ -943,33 +947,10 @@ class _MyPickupsState extends State<MyPickups>
fontFamily: FontConstants.fontFamily,
),
),
_fetchDiagLine(),
],
);
}
/// The last fetch's stage counts, shown under an empty state in debug only.
/// Never compiled into release — [kDebugMode] is a const, so the whole widget
/// folds away.
Widget _fetchDiagLine() {
if (!kDebugMode || _fetchDiag.isEmpty) return const SizedBox.shrink();
return Padding(
padding: EdgeInsets.only(top: 18.h),
child: Text(
'debug · $_fetchDiag\n'
'${_bookingTrips.length} trip(s) · showing ${_visibleStops.length}',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 10.sp,
height: 1.5,
fontWeight: FontWeight.w600,
color: ColorConstants.secondaryText,
fontFamily: FontConstants.fontFamily,
),
),
);
}
Widget _buildTripRail() {
// The whole run, finished stops included — see [_railStops]. Drawn as long
// as *anything* happened today, so a rider who has just closed his last
@@ -2182,8 +2163,10 @@ class _MyPickupsState extends State<MyPickups>
// separate causes have made this tab look empty — a gate on the raw API
// count, a missing empty state on an unfilled trip tab, and stops
// filtered out as still-pending — and every one of them looked identical
// on screen. The counts are now shown on the empty state itself, so the
// failing stage is visible instead of guessed at.
// on screen. The counts go to the log so the failing stage can be read
// off a `flutter logs` rather than guessed at. They are deliberately NOT
// drawn on the empty state any more: that put developer diagnostics in
// front of the rider on every debug and demo build.
_fetchDiag =
// Which line the app resolved the rider onto. Three of the reports
// that landed here as "the tab is wrong" were really "the app thinks
@@ -3118,9 +3101,16 @@ class _MyPickupsState extends State<MyPickups>
// is the picture now, at a size worth looking at, and
// it keeps its own copy.
//
// [_fetchDiagLine] stays: it is `kDebugMode`-gated and
// folds away entirely in release, so nothing ships
// underneath the art.
// Nothing is drawn under the art. A stage-count
// readout used to sit here — `debug · line=milkMan
// api=0 accepted=0 …` — on the argument that it was
// `kDebugMode`-gated and folded away in release.
// True, and it still meant every internal build,
// every demo and every screenshot showed a rider a
// line of diagnostics under a picture telling him he
// was all caught up. The counts are still gathered
// and still logged — see `[MYPICKUPS][DIAG]` — where
// the person who needs them is looking.
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
@@ -3140,7 +3130,6 @@ class _MyPickupsState extends State<MyPickups>
),
),
),
_fetchDiagLine(),
],
),
),