Redesign on the Stitch reference, in Plus Jakarta Sans
Four passes, and the shape they landed on. The type face is Plus Jakarta Sans (variable, wght 200-800), which brings a fix with it: it carries the rupee glyph and Switzer does not, so prices stop being set in Geist Mono to work around a missing character. Mono stays where it is earned - references and phone numbers, read digit by digit. Surfaces lift rather than outline. Cards carry two very soft shadow layers instead of a hairline, because eight outlined boxes down a screen read as a wireframe. The tab bar floats as a pill again for the same reason it was right to: it is now the same kind of object as everything above it. Screens: * Home is the greeting, the address, the sphere and one card. The card lost its progress bar - a filling line says "wait", and a parcel two days into a journey is not something anyone is waiting through - and gained the size that buys. * Orders cards are four bands: identity, destination, route, and whatever is happening right now. Plus a search field, because the list is the archive. * Tracking leads with the state at display size, then TRIP MILESTONES with a step counter, then the courier. * Review is a route thread over two particular cards. * Account opens on the person: avatar, name, and two counted figures. Three real bugs the redesign surfaced: * Quick dispatch handed `loadCities()` straight to a FutureBuilder, so the catalogue was refetched on every rebuild and Home never settled. * Order cards showed the whole visit's weight on one destination's row - somebody else's parcel. Per group now, and only once actually weighed. * The pickup window was printed beside "In transit", where it reads as a delivery time nobody promised. Nothing invented. The reference shows EXPRESS PRIORITY, CARBON OFFSET, CONCIERGE ELITE and hub-to-hub routing; this backend sends none of them, so they are absent rather than mocked up. flutter analyze: clean. flutter test: 88 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
@@ -345,7 +345,25 @@ class LiveDoormileApi extends DoormileApi {
|
||||
// This app does not have them: the pickup is a map pin, and the
|
||||
// reverse geocoder returns an address, not a serviceable-area code. The
|
||||
// coordinates say the same thing and the server already resolves them.
|
||||
'pickup': {'latitude': ?pickup.lat, 'longitude': ?pickup.lng},
|
||||
//
|
||||
// ── `lat`/`lng`, and this one was quoting the wrong price ──
|
||||
//
|
||||
// Same field-name mismatch as `Place.toJson`, and on this endpoint it
|
||||
// fails silently instead of erroring. With `latitude`/`longitude` the
|
||||
// server cannot read the pin, measures no route, and answers a flat
|
||||
// fallback band for every journey. Measured against production on
|
||||
// 2026-09-23, same pickup, one request apart:
|
||||
//
|
||||
// latitude/longitude local ₹280–520 routeKm 0
|
||||
// 427 km ₹280–520 routeKm 0
|
||||
// lat/lng local ₹45–90 routeKm 0.2
|
||||
// 427 km ₹150–280 routeKm 427.3
|
||||
//
|
||||
// So every fare this app has ever shown was the fallback: roughly six
|
||||
// times the real price on a local run, and the same number quoted for
|
||||
// a parcel crossing the state. A 422 tells you it is broken; this just
|
||||
// lies.
|
||||
'pickup': {'lat': ?pickup.lat, 'lng': ?pickup.lng},
|
||||
'destinations': [
|
||||
for (final d in destinations)
|
||||
{
|
||||
|
||||
@@ -114,6 +114,23 @@ enum JourneyStage {
|
||||
JourneyStage.outForDelivery => 'Out for delivery',
|
||||
JourneyStage.delivered => 'Delivered',
|
||||
};
|
||||
|
||||
/// The same state in one or two words, for a status chip.
|
||||
///
|
||||
/// [milestoneLabel] is a sentence fragment written to sit under a heading —
|
||||
/// "Miler assigned", "Miler at your door". Set in a 9.5pt chip beside a
|
||||
/// reference and a time it renders as "MILER ASSIG…", which is not a state.
|
||||
String get chipLabel => switch (this) {
|
||||
JourneyStage.booked => 'Booked',
|
||||
JourneyStage.assigned => 'Assigned',
|
||||
JourneyStage.onTheWay => 'On the way',
|
||||
JourneyStage.arrived => 'At your door',
|
||||
JourneyStage.pickedUp => 'Collected',
|
||||
JourneyStage.orderCreated => 'Created',
|
||||
JourneyStage.inTransit => 'In transit',
|
||||
JourneyStage.outForDelivery => 'Out for delivery',
|
||||
JourneyStage.delivered => 'Delivered',
|
||||
};
|
||||
}
|
||||
|
||||
/// What the customer sees on the timeline.
|
||||
@@ -418,11 +435,31 @@ class Place {
|
||||
lng: coordinate(json, 'lng'),
|
||||
);
|
||||
|
||||
/// ── The coordinates are `lat`/`lng`, NOT `latitude`/`longitude` ──
|
||||
///
|
||||
/// The written contract says `latitude`/`longitude` and this sent them for
|
||||
/// months. The server reads `lat`/`lng`, so it saw a pickup with no
|
||||
/// coordinates at all, could not place it in a serviceable area, and
|
||||
/// answered every single booking with **422 `unserviceable` — "We are not
|
||||
/// collecting from that area yet"**. The app could not create a booking
|
||||
/// against production; the error blamed the customer's address for a field
|
||||
/// name.
|
||||
///
|
||||
/// Verified against production on 2026-09-23, one request apart: the same
|
||||
/// pickup, the same destination, the same slot. `latitude`/`longitude` →
|
||||
/// 422 unserviceable. `lat`/`lng` → 201 and booking DM-252803. It is also
|
||||
/// what `GET /places/reverse-geocode` returns, which is where this Place
|
||||
/// came from in the first place — the app was renaming the server's own
|
||||
/// keys on the way back out.
|
||||
///
|
||||
/// Second one of these on this surface: `POST /auth/otp/verify` takes
|
||||
/// `code` where the document says `otp`. Follow the server, not the
|
||||
/// document.
|
||||
Map<String, dynamic> toJson() => {
|
||||
'title': title,
|
||||
'sub': sub,
|
||||
'latitude': ?lat,
|
||||
'longitude': ?lng,
|
||||
'lat': ?lat,
|
||||
'lng': ?lng,
|
||||
};
|
||||
|
||||
/// `POST /customer/bookings` ▸ `pickup`.
|
||||
@@ -471,6 +508,11 @@ class Destination {
|
||||
return coded.isEmpty ? '—' : coded.join(', ');
|
||||
}
|
||||
|
||||
/// The district alone, for a headline. Falls back to the full [label] when
|
||||
/// the backend sent only a state or only codes — a card headed "—" is worse
|
||||
/// than a card headed with two words.
|
||||
String get shortLabel => _clean(districtName) ?? _clean(districtCode) ?? label;
|
||||
|
||||
static String? _clean(String? v) {
|
||||
final t = v?.trim();
|
||||
return (t == null || t.isEmpty) ? null : t;
|
||||
|
||||
Reference in New Issue
Block a user