production
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'package:miler/data/api_config.dart';
|
||||
import 'package:miler/data/miler_api.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// WHICH LINE OF WORK THIS RIDER IS ON
|
||||
@@ -476,27 +479,108 @@ class ServiceProfile {
|
||||
///
|
||||
/// A free function rather than a controller method so it can be tested without
|
||||
/// a GetX container — see the note on [ServiceProfile.active].
|
||||
/// Re-reads the rider's tenant from `GET /miler/profile` and persists it.
|
||||
///
|
||||
/// ── Why a signed-in rider needed this ──
|
||||
///
|
||||
/// `tenantname` is written at verify-pin, so it only ever reached a device by
|
||||
/// way of a **login**. A rider already signed in when the backend began
|
||||
/// returning the field would never see it: his prefs carry no name, resolution
|
||||
/// falls back to the tenant id, and an id this app has not been told about
|
||||
/// lands him on logistics — no **Start delivery** button, and a parcel that
|
||||
/// strands at `Collected_By_Miler` the moment collected-state is enabled.
|
||||
///
|
||||
/// The fix is not to make every rider sign out. `GET /miler/profile` returns
|
||||
/// the same pair, so the tenant can be refreshed in place on launch.
|
||||
///
|
||||
/// ── What it costs, and when ──
|
||||
///
|
||||
/// Nothing, for a rider whose name is already known: he resolves from prefs on
|
||||
/// this launch and picks up any change on the next one, with no first frame
|
||||
/// spent waiting on a network call. A rider with **no** stored name is the case
|
||||
/// this exists for, and that one is worth a bounded wait.
|
||||
///
|
||||
/// Every failure is silent by design. A launch with no signal must land on the
|
||||
/// same screen it always did rather than on an error, and the stored pair is
|
||||
/// still there to resolve from.
|
||||
Future<void> refreshTenantFromProfile() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if ((prefs.getString('authtoken') ?? '').trim().isEmpty) return;
|
||||
|
||||
final known = (prefs.getString(TenantController.kTenantName) ?? '').trim();
|
||||
final fetch = _fetchTenantIntoPrefs();
|
||||
if (known.isNotEmpty) {
|
||||
unawaited(fetch);
|
||||
return;
|
||||
}
|
||||
await fetch.timeout(const Duration(seconds: 4), onTimeout: () {});
|
||||
}
|
||||
|
||||
Future<void> _fetchTenantIntoPrefs() async {
|
||||
try {
|
||||
final res = await MilerApi.getProfile();
|
||||
if (!res.ok) return;
|
||||
|
||||
final map = res.map;
|
||||
// The backend returns the pair top-level *and* inside `user`. Read both, so
|
||||
// a handler that later moves them cannot silently stop resolving riders.
|
||||
final user = map['user'] is Map ? map['user'] as Map : const {};
|
||||
Object? pick(String k) => map[k] ?? user[k];
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
final name = (pick('tenantname') ?? pick('tenantcode') ?? '')
|
||||
.toString()
|
||||
.trim();
|
||||
if (name.isNotEmpty) {
|
||||
await prefs.setString(TenantController.kTenantName, name);
|
||||
}
|
||||
|
||||
final id = int.tryParse('${pick('tenantid') ?? ''}'.trim());
|
||||
if (id != null && id > 0) {
|
||||
await prefs.setInt(TenantController.kTenantId, id);
|
||||
}
|
||||
} catch (_) {
|
||||
// Offline, timed out, malformed — all the same answer: keep what we have.
|
||||
}
|
||||
}
|
||||
|
||||
Future<ServiceProfile> resolveServiceProfile() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// ── What the rider's account says ──
|
||||
//
|
||||
// Name first: it is the field a human can check against the admin console,
|
||||
// and it means a new tenant does not need an app release. An unrecognised
|
||||
// name is not an answer, so it falls through to the id.
|
||||
// Both halves of the pair are read before either is acted on, because which
|
||||
// one answers depends on what the other said. See the note below.
|
||||
final name = (prefs.getString(TenantController.kTenantName) ?? '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (name.isNotEmpty) {
|
||||
final byName = TenantController.profileForName(name);
|
||||
if (byName != null) return byName;
|
||||
}
|
||||
final byName = name.isEmpty ? null : TenantController.profileForName(name);
|
||||
|
||||
final id = prefs.getInt(TenantController.kTenantId) ?? 0;
|
||||
if (id != 0) {
|
||||
final byId = TenantController.profileForId(id);
|
||||
if (byId != null) return byId;
|
||||
}
|
||||
final byId = id == 0 ? null : TenantController.profileForId(id);
|
||||
|
||||
// ── When the two disagree, the id wins ──
|
||||
//
|
||||
// Name-first is right for the case it was written for: an unrecognised name
|
||||
// falls through to the id, so a tenant this build has never heard of costs no
|
||||
// release. That property is untouched below — a tenant absent from
|
||||
// [TenantController.milkManTenantIds] is still decided by its name alone.
|
||||
//
|
||||
// What it did not survive is the two lists *contradicting each other*. The id
|
||||
// list names one specific tenant on purpose; the name list is a pattern, and
|
||||
// patterns collide. Tenant 13 carrying a display name that normalises to
|
||||
// `doormile` — which is, after all, the company that owns the tenant record —
|
||||
// resolved that rider to Logistics and never consulted the id at all. On the
|
||||
// Logistics line `_unreleased` is empty by design, so the **Start round** bar
|
||||
// does not render, and once `MILER_COLLECTED_STATE_ENABLED` is on his
|
||||
// collected parcels strand at `Collected_By_Miler` with no control to release
|
||||
// them. A global flag gets one safe shot, and this was the loose end in it.
|
||||
//
|
||||
// A deliberate statement about a known tenant outranks a match on a word
|
||||
// nobody on either side of the API controls the spelling of.
|
||||
if (byId != null) return byId;
|
||||
if (byName != null) return byName;
|
||||
|
||||
// ── The tenant the session's own token claims ──
|
||||
//
|
||||
@@ -618,11 +702,34 @@ class TenantController extends GetxController {
|
||||
/// One lookup used by both the account path and the build override, so the
|
||||
/// two can never disagree about what a name means.
|
||||
static ServiceProfile? profileForName(String name) {
|
||||
if (milkManTenantNames.contains(name)) return ServiceProfile.milkMan;
|
||||
if (logisticsTenantNames.contains(name)) return ServiceProfile.parcel;
|
||||
final key = _nameKey(name);
|
||||
if (key.isEmpty) return null;
|
||||
if (milkManTenantNames.any((n) => _nameKey(n) == key)) {
|
||||
return ServiceProfile.milkMan;
|
||||
}
|
||||
if (logisticsTenantNames.any((n) => _nameKey(n) == key)) {
|
||||
return ServiceProfile.parcel;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// A tenant name reduced to the letters and digits in it.
|
||||
///
|
||||
/// ── Why separators cannot be allowed to decide a rider's day ──
|
||||
///
|
||||
/// This was an exact-string lookup against a lower-cased name, which meant
|
||||
/// `DailyGrubs` resolved and `Daily Grubs` did not — and the difference
|
||||
/// between them is whether the rider gets a **Start delivery** button at all.
|
||||
/// Nobody on either side of the API controls how a client's display name was
|
||||
/// typed into the tenant record, so matching on it was a coin flip we had no
|
||||
/// reason to take: `daily-grubs`, `DAILY_GRUBS` and `Daily Grubs` are the
|
||||
/// same client by any reading, and all three missed.
|
||||
///
|
||||
/// Applied to both sides of the comparison, so the literals above stay
|
||||
/// readable as the words they are.
|
||||
static String _nameKey(String raw) =>
|
||||
raw.toLowerCase().replaceAll(RegExp('[^a-z0-9]'), '');
|
||||
|
||||
/// The profile a tenant *id* means, or null when this app does not know it.
|
||||
static ServiceProfile? profileForId(int id) {
|
||||
if (milkManTenantIds.contains(id)) return ServiceProfile.milkMan;
|
||||
|
||||
Reference in New Issue
Block a user