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:
@@ -93,6 +93,11 @@ class AuthProvider {
|
||||
required String fcmToken,
|
||||
int? pin,
|
||||
String? pinRaw,
|
||||
|
||||
/// True for a rider setting his PIN for the first time — routes to
|
||||
/// `POST /miler/set-pin` and sends the digits as `new_pin`. The response,
|
||||
/// the token handling and the persisted identity are identical either way.
|
||||
bool firstTime = false,
|
||||
}) async {
|
||||
// The legacy `rider/login` path on the retired backend that used to sit
|
||||
// behind a flag here is gone with the rest of the old backend.
|
||||
@@ -101,6 +106,8 @@ class AuthProvider {
|
||||
pin: pin,
|
||||
pinRaw: pinRaw,
|
||||
fcmToken: fcmToken,
|
||||
path: firstTime ? '/miler/set-pin' : '/miler/verify-pin',
|
||||
pinField: firstTime ? 'new_pin' : 'pin',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,13 +124,27 @@ class AuthProvider {
|
||||
/// AssignMilerToBooking pushes "New Pickup Assigned" to exactly that token.
|
||||
/// Omitting it leaves the profile's token empty and silently makes the app
|
||||
/// poll-only.
|
||||
/// ── One normaliser, two routes ──
|
||||
///
|
||||
/// `POST /miler/set-pin` answers with the **same envelope** as verify-pin —
|
||||
/// `{success, token, tenantid, tenantname, user:{authname, contactno,
|
||||
/// profile:{…}}}` — because it is also a sign-in: a rider who sets his PIN is
|
||||
/// logged in by that one call and does not verify afterwards.
|
||||
///
|
||||
/// So the path and the PIN field name are arguments rather than a second copy
|
||||
/// of this function. Everything below — finding the token in any of the nine
|
||||
/// places this contract has worn it, splitting the display name, building the
|
||||
/// legacy `{status, details:{…}}` envelope that [loginParsed] persists from —
|
||||
/// is the part that must not drift between the two paths, and now cannot.
|
||||
Future<http.Response> _loginNew({
|
||||
required String contactNo,
|
||||
int? pin,
|
||||
String? pinRaw,
|
||||
String? fcmToken,
|
||||
String path = '/miler/verify-pin',
|
||||
String pinField = 'pin',
|
||||
}) async {
|
||||
final uri = Uri.parse(ApiConfig.url('/miler/verify-pin'));
|
||||
final uri = Uri.parse(ApiConfig.url(path));
|
||||
// The backend bcrypt-compares the PIN as a STRING, so a leading zero is
|
||||
// significant. Prefer the raw text the rider typed — round-tripping through
|
||||
// int drops it ("0512" -> 512 -> "512") and fails a valid PIN.
|
||||
@@ -132,7 +153,7 @@ class AuthProvider {
|
||||
: pin?.toString();
|
||||
final body = {
|
||||
'phone': contactNo,
|
||||
if (pinValue != null) 'pin': pinValue,
|
||||
if (pinValue != null) pinField: pinValue,
|
||||
// Riders live in partition 1001. It defaults server-side, so omitting it
|
||||
// appeared to work — but a miler row created without it can never log in,
|
||||
// and sending it explicitly is the only way the app and the console agree
|
||||
@@ -168,7 +189,7 @@ class AuthProvider {
|
||||
// Signing in with nothing to sign in to. This is the one call that cannot
|
||||
// be intercepted in [MilerApi] — auth posts its own request, because it
|
||||
// runs before there is a token for the shared transport to attach.
|
||||
final mocked = MockBackend.respond('POST', '/miler/verify-pin', body: body);
|
||||
final mocked = MockBackend.respond('POST', path, body: body);
|
||||
|
||||
final http.Response res = mocked != null
|
||||
? http.Response(
|
||||
@@ -451,6 +472,7 @@ class AuthProvider {
|
||||
required String fcmToken,
|
||||
int? pin,
|
||||
String? pinRaw,
|
||||
bool firstTime = false,
|
||||
}) async {
|
||||
final res = await login(
|
||||
contactNo: contactNo,
|
||||
@@ -460,6 +482,7 @@ class AuthProvider {
|
||||
fcmToken: fcmToken,
|
||||
pin: pin,
|
||||
pinRaw: pinRaw,
|
||||
firstTime: firstTime,
|
||||
);
|
||||
final Map<String, dynamic> jsonMap = res.body.isNotEmpty
|
||||
? json.decode(res.body) as Map<String, dynamic>
|
||||
@@ -608,51 +631,39 @@ class AuthProvider {
|
||||
return Login.fromJson(jsonMap);
|
||||
}
|
||||
|
||||
/// Deprecated. `POST /miler/reset-pin` is admin-only and this app must never
|
||||
/// call it — it was once open, and reset-pin followed by verify-pin took over
|
||||
/// any rider account given nothing but a phone number.
|
||||
///
|
||||
/// ── The 403 this used to manufacture is gone ──
|
||||
///
|
||||
/// There was no rider-facing PIN write, so this returned a synthetic
|
||||
/// `403 "Your MPIN is issued by your office and cannot be changed from the
|
||||
/// app."` — true when written, and a dead end for the Create-MPIN screen.
|
||||
///
|
||||
/// `POST /miler/set-pin` is that route, shipped 2026-09-16. It is
|
||||
/// self-service, cannot overwrite an existing PIN (409), and returns a full
|
||||
/// session. Riders set their own PIN on first sign-in and the console no
|
||||
/// longer issues one, so nothing needs this method any more.
|
||||
@Deprecated('Use AuthController.setPin, which calls POST /miler/set-pin.')
|
||||
Future<http.Response> updatePin({
|
||||
required int userId,
|
||||
required int pin,
|
||||
}) async {
|
||||
// ── There is no rider-facing set-PIN endpoint, and that is deliberate ──
|
||||
//
|
||||
// The only PIN-write route on the backend is `POST /miler/reset-pin`, and
|
||||
// it requires an ADMIN token. It was once open, and reset-pin followed by
|
||||
// verify-pin took over any rider account given nothing but a phone number.
|
||||
// The app must not call it; rider PIN resets go through ops.
|
||||
//
|
||||
// So this stays a no-op success: the Create-MPIN screen's flow completes
|
||||
// and the PIN the account was issued with remains the one that works.
|
||||
// Making it fail instead would strand a rider on a screen with no way
|
||||
// forward, which is worse and no more honest.
|
||||
// ── It used to answer 200 ──
|
||||
//
|
||||
// "Making it fail instead would strand a rider on a screen with no way
|
||||
// forward, which is worse and no more honest." Half of that was right and
|
||||
// the conclusion was wrong. What the manufactured 200 actually did:
|
||||
//
|
||||
// 1. Create-MPIN told the rider his new MPIN was saved.
|
||||
// 2. The app wrote it to `dbPin` locally.
|
||||
// 3. The server never heard about it.
|
||||
// 4. Every login from then on returned `incorrect PIN`, forever, with no
|
||||
// way for the rider to tell that the PIN he was typing had never
|
||||
// existed anywhere but his own handset.
|
||||
//
|
||||
// Being stranded on a screen that tells you who can help is not worse than
|
||||
// that. It is the only version of this that a rider can act on.
|
||||
ApiConfig.logGap(
|
||||
'updatePin',
|
||||
'No rider-facing set-PIN route; reset-pin is admin-only by design. '
|
||||
'Refusing rather than reporting a write that did not happen.',
|
||||
'reset-pin is admin-only; first-time PIN creation goes through '
|
||||
'POST /miler/set-pin. This method should have no callers.',
|
||||
);
|
||||
return http.Response(
|
||||
json.encode(<String, dynamic>{
|
||||
'status': false,
|
||||
'code': 403,
|
||||
'code': 410,
|
||||
'message':
|
||||
'Your MPIN is issued by your office and cannot be changed from the '
|
||||
'app. Ask your supervisor to reset it, then sign in with the MPIN '
|
||||
'they give you.',
|
||||
'This app no longer changes PINs through reset-pin. Set your PIN '
|
||||
'on the sign-in screen.',
|
||||
}),
|
||||
403,
|
||||
410,
|
||||
headers: {'content-type': 'application/json'},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user