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:
@@ -57,6 +57,84 @@ Future<void> _drainLegacy(String base, WorkScope scope) async {
|
||||
debugPrint('[SCOPE] drained legacy "$base" into ${scope.key}');
|
||||
}
|
||||
|
||||
/// Moves records out of the old line-suffixed keys into this scope's key.
|
||||
///
|
||||
/// ── Why this one MERGES where [_drainLegacy] drops ──
|
||||
///
|
||||
/// A line-suffixed key is not anonymous the way a global one is. It already
|
||||
/// names the rider and the tenant — `completed_bookings::u38.t13.milkMan` —
|
||||
/// so everything in it provably belongs to this scope. There is nothing to
|
||||
/// attribute and nothing to guess, and dropping it would throw away work the
|
||||
/// rider actually did.
|
||||
///
|
||||
/// Both old drawers are merged, because the whole point of removing the line
|
||||
/// is that one rider's day is one day: a meal round and a logistics collection
|
||||
/// finished in the same shift belong in the same history. Ids already present
|
||||
/// win, so a re-run cannot duplicate a row.
|
||||
Future<void> _drainLineScoped(String base, WorkScope scope) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final target = scope.scoped(base);
|
||||
|
||||
// Read the target through `get`, not the typed accessors: these base keys
|
||||
// hold a JSON blob for record stores and a String list for id stores, and
|
||||
// `getStringList` throws outright when it meets the blob.
|
||||
final Object? existing = prefs.get(target);
|
||||
final merged = <Map<String, dynamic>>[];
|
||||
final mergedIds = <String>{};
|
||||
final ids = <String>{};
|
||||
|
||||
if (existing is String) {
|
||||
for (final row in _decode(existing)) {
|
||||
merged.add(row);
|
||||
final id = _rowKey(row);
|
||||
if (id.isNotEmpty) mergedIds.add(id);
|
||||
}
|
||||
} else if (existing is List) {
|
||||
ids.addAll(existing.map((e) => e.toString()));
|
||||
}
|
||||
|
||||
var moved = false;
|
||||
for (final legacy in scope.legacyScopedKeys(base)) {
|
||||
if (legacy == target || !prefs.containsKey(legacy)) continue;
|
||||
final Object? value = prefs.get(legacy);
|
||||
|
||||
if (value is String) {
|
||||
for (final row in _decode(value)) {
|
||||
final id = _rowKey(row);
|
||||
if (id.isNotEmpty && !mergedIds.add(id)) continue;
|
||||
merged.add(scope.stamp(row));
|
||||
}
|
||||
} else if (value is List) {
|
||||
ids.addAll(value.map((e) => e.toString()));
|
||||
}
|
||||
await prefs.remove(legacy);
|
||||
moved = true;
|
||||
}
|
||||
if (!moved) return;
|
||||
|
||||
if (merged.isNotEmpty) {
|
||||
await prefs.setString(target, jsonEncode(merged));
|
||||
} else if (ids.isNotEmpty) {
|
||||
await prefs.setStringList(target, ids.toList());
|
||||
}
|
||||
debugPrint('[SCOPE] merged line-scoped "$base" into ${scope.key}');
|
||||
}
|
||||
|
||||
/// The identity a stored row is de-duplicated on while merging.
|
||||
String _rowKey(Map<String, dynamic> row) {
|
||||
for (final k in const [
|
||||
'orderid',
|
||||
'OrderId',
|
||||
'bookingid',
|
||||
'consignmentid',
|
||||
'id',
|
||||
]) {
|
||||
final v = row[k];
|
||||
if (v != null && v.toString().trim().isNotEmpty) return v.toString();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Runs the one-time drain for every legacy key. Cheap after the first call —
|
||||
/// `containsKey` on a loaded prefs map.
|
||||
Future<void> migrateLegacyStores() async {
|
||||
@@ -75,6 +153,9 @@ Future<void> migrateLegacyStores() async {
|
||||
_kOrderLabelsKeyBase,
|
||||
]) {
|
||||
await _drainLegacy(base, scope);
|
||||
// Then fold in anything the old line-suffixed keys still hold. Order
|
||||
// matters: the global drain writes the scoped key, and this merges into it.
|
||||
await _drainLineScoped(base, scope);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user