Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
183 lines
8.0 KiB
Dart
183 lines
8.0 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
/// THE APP WITH NO BACKEND BEHIND IT
|
|
///
|
|
/// One switch that takes the whole app off the network: sign-in, the day's
|
|
/// work, and every status the rider writes. It exists so the flow can be built,
|
|
/// demonstrated and judged without `api.doormile.com` answering — on a bench, on
|
|
/// a plane, or against a backend that does not have the meal contract yet.
|
|
///
|
|
/// ── This app has been burned by a mock layer before ──
|
|
///
|
|
/// One was ripped out for a good reason: it wrote rows into the same
|
|
/// SharedPreferences stores the real work uses, so demo stops surfaced on live
|
|
/// riders' tabs weeks later with nothing left in the repo to explain them.
|
|
/// `purgeDemoRecords()` still runs at every launch to clean up after it.
|
|
///
|
|
/// Four rules keep this one from becoming that:
|
|
///
|
|
/// 1. **It cannot ship.** [enabled] is false in a release build unless someone
|
|
/// types `--dart-define=MOCK_BACKEND=true` on the build command, which is
|
|
/// not something that happens by accident. Debug and profile builds — the
|
|
/// ones a developer actually runs — get it by default.
|
|
/// 2. **It intercepts the transport, not the stores.** Everything is served as
|
|
/// an *API response*; nothing writes to a store that real work shares. The
|
|
/// app cannot tell the difference and neither can its data layer.
|
|
/// 3. **Its ids are self-identifying.** Every record is `MOCK-…`, which is
|
|
/// exactly what `purgeDemoRecords()` looks for, so anything that does leak
|
|
/// into a store is removed at the next launch by code that already ships.
|
|
/// 4. **It says so, loudly.** Every intercepted call is logged with a `[MOCK]`
|
|
/// prefix, so a confusing session is one glance at the console away from
|
|
/// being explained.
|
|
///
|
|
/// ── What is given up while it is on ──
|
|
///
|
|
/// The hub is not told anything. A rider can accept, arrive, pick up and
|
|
/// deliver, and the console will show none of it — every write is answered with
|
|
/// a success that was manufactured on the phone. That is the correct trade for
|
|
/// design and demo work and the wrong one for a real shift, which is what rule 1
|
|
/// is for.
|
|
/// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
/// The one switch.
|
|
///
|
|
/// Default: **on** in debug and profile, **off** in release. Override either
|
|
/// way from the build command:
|
|
///
|
|
/// flutter run # mock
|
|
/// flutter run --dart-define=MOCK_BACKEND=false # real API
|
|
/// flutter build apk --dart-define=MOCK_BACKEND=true # deliberate demo
|
|
/// ── Opt in, not opt out ──
|
|
///
|
|
/// This defaulted to **on** in debug and profile, so every `flutter run` served
|
|
/// a canned rider, a canned day and a canned login — and the backend was only
|
|
/// ever exercised by someone who remembered to turn the mock off. Three
|
|
/// consequences, all of which actually happened:
|
|
///
|
|
/// • A rider signing in with his real number became the fixture's rider.
|
|
/// • A whole class of contract bugs — a field renamed, a number sent where a
|
|
/// string was required — could not be seen, because nothing left the device.
|
|
/// • The default development experience diverged from production silently,
|
|
/// which is the one kind of divergence nobody notices until release.
|
|
///
|
|
/// The canned day is still one flag away, and is genuinely useful on a bench or
|
|
/// a plane. It is just no longer what you get by not deciding:
|
|
///
|
|
/// flutter run # the real API
|
|
/// flutter run --dart-define=MOCK_BACKEND=true # the canned day
|
|
const bool kMockBackend = bool.fromEnvironment('MOCK_BACKEND');
|
|
|
|
/// Canned answers for the routes the rider's day passes through.
|
|
///
|
|
/// Everything else gets a generic success, on purpose: the alternative is a
|
|
/// mock that fails on an endpoint nobody thought about and reads to the person
|
|
/// using it as a broken app rather than an unmapped route.
|
|
class MockBackend {
|
|
MockBackend._();
|
|
|
|
/// Whether the canned answers are being served.
|
|
///
|
|
/// Settable so a test can exercise the transport itself — the interception
|
|
/// happens *above* the HTTP client, so with this on there is no request to
|
|
/// assert against. Nothing in the app assigns it; it follows [kMockBackend].
|
|
static bool enabled = kMockBackend;
|
|
|
|
/// The signed-in rider, when there is no server to ask.
|
|
///
|
|
/// `tenantname` is what puts the build on the meal line — see
|
|
/// [ServiceProfile] — so the mock day and the mock login agree about which
|
|
/// application the rider is in.
|
|
static const Map<String, dynamic> rider = {
|
|
'userid': 90001,
|
|
'id': 90001,
|
|
'name': 'Suresh Kumar',
|
|
'phone': '9876543210',
|
|
'mobile': '9876543210',
|
|
'email': 'suresh@doormile.test',
|
|
'roleid': 5,
|
|
'configid': 1001,
|
|
'tenantid': 916,
|
|
'tenantname': 'DailyGrubs',
|
|
'status': 'Active',
|
|
'vehicletype': 'Bike',
|
|
'vehicleno': 'TN 37 CV 4412',
|
|
};
|
|
|
|
/// A token that is obviously not a JWT, so nothing tries to read claims out
|
|
/// of it and no log line makes it look like a real session.
|
|
static const String token = 'MOCK-TOKEN-not-a-real-session';
|
|
|
|
/// Answers [method] [path], or null when the caller should fall through to
|
|
/// the network. Null is never returned while [enabled] — see the class note.
|
|
static Map<String, dynamic>? respond(
|
|
String method,
|
|
String path, {
|
|
Object? body,
|
|
Map<String, String>? query,
|
|
}) {
|
|
if (!enabled) return null;
|
|
|
|
final route = path.split('?').first;
|
|
debugPrint('[MOCK] $method $route');
|
|
|
|
// ── Auth ──
|
|
if (route.endsWith('/login')) {
|
|
return _ok({'otpsent': true, 'phone': _field(body, 'phone')});
|
|
}
|
|
if (route.endsWith('/verify-pin')) {
|
|
// The shape the real handler returns, verified against a live 200: the
|
|
// profile lives under `user` / `user.profile`, and there is no `data`
|
|
// key. Getting this wrong cost a debugging session once already — see the
|
|
// note in [MilerApi].
|
|
//
|
|
// The phone is the one the rider actually typed. It used to be the
|
|
// fixture's, so signing in with your own number made you Suresh Kumar and
|
|
// the screen you landed on disagreed with the number on the screen you
|
|
// came from — which reads as a broken login rather than as a mock.
|
|
final phone = _field(body, 'phone');
|
|
final signedIn = <String, dynamic>{
|
|
...rider,
|
|
if (phone.isNotEmpty) ...{
|
|
'phone': phone,
|
|
'mobile': phone,
|
|
'contactno': phone,
|
|
},
|
|
};
|
|
return {
|
|
'success': true,
|
|
'message': 'ok',
|
|
'token': token,
|
|
'user': {...signedIn, 'profile': signedIn},
|
|
};
|
|
}
|
|
if (route.endsWith('/profile')) return _ok(rider);
|
|
|
|
// ── The day's work ──
|
|
//
|
|
// Empty rather than invented: the meal line's stops come from
|
|
// [MealRunMock], which the provider reads *before* it reaches the network
|
|
// at all. A parcel booking list has no fixture and inventing one here would
|
|
// put fictional consignments in front of a parcel rider.
|
|
if (route.endsWith('/bookings') || route.endsWith('/assignments')) {
|
|
return _ok(const <dynamic>[]);
|
|
}
|
|
|
|
// ── Everything the rider writes ──
|
|
//
|
|
// Accept, reject, reached, parcels, payment, pickup-complete, deliver,
|
|
// skip, duty, breaks, telemetry, device tokens. All answered yes, and all
|
|
// going nowhere.
|
|
return _ok(const <String, dynamic>{});
|
|
}
|
|
|
|
static Map<String, dynamic> _ok(dynamic data) => {
|
|
'success': true,
|
|
'message': 'ok',
|
|
'data': data,
|
|
};
|
|
|
|
static String _field(Object? body, String key) =>
|
|
(body is Map && body[key] != null) ? body[key].toString() : '';
|
|
}
|