Files
doormile_customer_app/lib/data/doormile_api.dart
Thiru-tenext 0d66627c3c Replace the customer app with Doormile CX
Book a pickup, track it to delivery — the rebuilt customer app.

- Design language from doormile-screens.html: brand #8F0F06, Manrope +
  Geist Mono (variable fonts), bordered cards instead of shadows, crimson
  brand headers, sliding tab indicator, mono for anything read digit by digit.
- lib/data (one live API implementation, plus a debug-only offline fake),
  lib/state, lib/ui (tokens, widgets, screens).
- 84 tests, plus a design snapshot harness that renders every screen with the
  real fonts: flutter test test/design_snapshot_test.dart --run-skipped
  --update-goldens

This replaces the previous app (pubspec 'doormile', app id
com.doormile.customer). That tree remains in history at 6c7d656; note its
android/app/google-services.json is not carried over, and the application id
here is in.doormile.customer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-15 16:07:33 +05:30

178 lines
6.5 KiB
Dart

import 'app_config.dart';
import 'dev_doormile_api.dart';
import 'live_doormile_api.dart';
import 'models.dart';
export 'api_exception.dart';
/// The service surface the app is written against.
///
/// **A shipping build has one implementation: [LiveDoormileApi],** against
/// `/customer/*` on `api.doormile.com`. When the server cannot be reached the
/// screens show their designed error state, which is the only honest answer.
///
/// The single exception is [DevDoormileApi], reached only when
/// [AppConfig.useDevData] is set — a `!kReleaseMode` guard, so no define swaps
/// invented data into a release. It exists because the real backend has no SMS
/// gateway, so there is otherwise no way to sign in and exercise the flow.
///
/// Screens never see this type at all — they call [AppState], which holds it.
///
/// A method the backend owns but no screen drives yet has a default here that
/// does nothing rather than throwing.
abstract class DoormileApi {
DoormileApi();
static DoormileApi? _instance;
/// The API this build talks to: the live backend, or [DevDoormileApi] when
/// [AppConfig.useDevData] allows it — which is never in a release build.
static DoormileApi get instance =>
_instance ??= AppConfig.useDevData ? DevDoormileApi() : LiveDoormileApi();
/// Replaces the singleton. Tests inject their double through here.
static void overrideInstance(DoormileApi? api) => _instance = api;
/// Called when the refresh chain dies and the customer must sign in again.
/// Set by [AppState].
void Function()? onSessionLost;
// --------------------------------------------------------------------- auth
/// Asks for a code. The answer describes the challenge — how many digits it
/// has and how long before it can be resent — so the screen is built from
/// what the server said rather than from constants.
Future<OtpChallenge> sendOtp(String identifier);
/// Creates the account **and** sends the first code, so it answers with the
/// same challenge [sendOtp] does.
Future<OtpChallenge> signUp({
required String name,
required String phone,
String? email,
});
/// [name] is set when verifying a freshly created account.
Future<Customer> verifyOtp(String identifier, String code, {String? name});
/// Restores a persisted session at launch, or null when there is none.
Future<Customer?> restoreSession() async => null;
/// Revokes the refresh token and unregisters the push token.
Future<void> signOut() async {}
/// Re-reads the signed-in customer — `GET /customer/auth/me`.
Future<Customer?> me() async => null;
// ----------------------------------------------------------- serviceability
Future<List<ServiceArea>> getServiceableStates();
Future<List<District>> getServiceableDistricts(String stateCode);
/// Synchronous district lookup for a code we already hold, backed by
/// whatever [getServiceableDistricts] last returned.
District? districtByCode(String? code);
// -------------------------------------------------------------------- slots
Future<List<PickupSlot>> getPickupSlots({Place? pickup});
PickupSlot? slotById(String? id);
// ----------------------------------------------------------------- location
Future<Place> reverseGeocode({double? lat, double? lng});
/// An empty query returns the customer's saved and recent places, which is
/// what the search sheet shows the moment it opens.
///
/// [lat]/[lng] bias the results towards where the customer is looking —
/// "MG Road" is in most Indian cities, and the one they mean is the near one.
Future<List<Place>> searchPlaces(String query, {double? lat, double? lng});
// ------------------------------------------------------------------- limits
Future<BookingLimits> getBookingLimits({Place? pickup});
// --------------------------------------------------------------------- fare
Future<FareEstimate> estimateFare({
required Place pickup,
required List<DestinationGroup> destinations,
});
// ------------------------------------------------------------------ booking
/// Creates the pickup booking. No tracking number yet — those are minted per
/// destination when the Miler completes the pickup.
///
/// [idempotencyKey] must be **held across retries of the same intent**. A new
/// key is a new booking, which is exactly what a double tap must not create.
Future<Booking> createBooking({
required Place pickup,
required List<DestinationGroup> destinations,
required String? slotId,
FareEstimate? fare,
String? idempotencyKey,
});
Future<void> cancelBooking(String reference, String? reason);
/// One page of the customer's bookings, newest first.
Future<BookingPage> getBookingPage({
BookingStatus? status,
String? cursor,
int limit = 20,
});
/// One booking, fresh. Null means the server answered `304` — nothing has
/// changed since the last read, so the caller keeps what it has.
Future<Booking?> getBooking(String reference);
/// One order by its tracking number — for a push deep link.
Future<Booking?> getOrder(String trackingId) async => null;
/// Adds the optional address and recipient after booking, up to collection.
Future<void> updateDestinationDetails(
String reference,
int index,
DeliveryDetails details,
) async {}
/// Backend policy decides this; the UI only asks.
bool isCancellable(JourneyStage stage);
// ------------------------------------------------------------------ ops QA
/// Moves a booking to [stage] through the backend's own non-production QA
/// helper — `POST /customer/ops/bookings/{reference}/stage`.
///
/// It exists so the tracking screen's stepper can walk a **real** staging
/// booking. The stage that then appears on screen is the one the server
/// recorded, not one the client wished for. Refused on production.
Future<void> setStage(String reference, JourneyStage stage) async {}
// ------------------------------------------------------------------ devices
Future<void> registerDevice(String token) async {}
Future<void> unregisterDevice(String token) async {}
// ------------------------------------------------------- profile & addresses
Future<Customer?> getProfile() async => null;
Future<Customer?> updateProfile({String? name, String? email}) async => null;
Future<List<SavedPlace>> getSavedLocations() async => const [];
Future<SavedPlace?> addSavedLocation(Place place, {String? label}) async =>
null;
Future<SavedPlace?> updateSavedLocation(SavedPlace place) async => null;
Future<void> deleteSavedLocation(String id) async {}
}