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 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? respond( String method, String path, { Object? body, Map? 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 = { ...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 []); } // ── 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 {}); } static Map _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() : ''; }