main
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| c7a74c57b8 |
BOOK NOW, OpenStreetMap, and one segmented control instead of two
── The circle says what pressing it does ── "ONE TOUCH" named the *mechanism* — one gesture, no form — which is something the product team knows and a customer has to be taught. Nobody arrives at that screen wanting a touch; they want a parcel collected. The caption under the sphere still carries what makes it different from the form below. Renamed in the comments too. A codebase explaining "One Touch" against a button that says BOOK NOW is a trap for whoever reads it next. ── OpenStreetMap everywhere ── One line: the default provider was CARTO, it is `osm`. Nothing else moves — `DmMapTiles` already reads the template, subdomains, retina flag and attribution off the provider, so the credit line follows on its own. One thing recorded on the provider itself rather than left to be discovered: these are donated servers and the OSM Foundation's tile policy does not permit a distributed app to lean on them. A block looks like every tile turning into the ground colour at once, with no other symptom. Moving off it is one define — `DM_MAP_PROVIDER=carto|maptiler|stadia`, all serving OpenStreetMap data — and the map_config test now asserts the identifying User-Agent rather than only the URL, because that is what attributable traffic depends on. ── Orders had a second copy of the segmented control ── Its own `_Tab`, a pill radius, 3pt of padding and the count folded into the label's text — beside the pickup window's day switcher, which is DmChoiceChip in a rounded groove with 4pt of padding and the count in a bubble. Two controls doing one job, drifting apart a padding value at a time. It is the same control now, and `_Tab` is gone. DmChoiceChip's horizontal padding drops 12 → 9: three of them split a 390pt phone and "Cancelled" truncated to "Cancell…" at the old value. The day switcher has two chips and acres of room, so it loses nothing. |
|||
| 8757b16cf5 |
PIN sign-in, because the code could never arrive
── What was actually broken ── The SMS gateway was switched off, and `POST /auth/otp/request` does not fail when that happens: it still answers `sent: true`, still issues a valid 4-digit code, and writes it to the **server log**. So the phone path walked customers to a code screen for a code that could not arrive, and every digit they eventually typed was wrong. The failure read to them as "I entered it wrong". Phone sign-in is now a PIN, which needs no gateway. ── Email still sends codes, so email is untouched ── Email OTP goes over SMTP and works. Deleting a working way in to tidy up a broken one is a net loss for anyone with an email on their account, so "Use email instead" and the code screen stay exactly as they were. `login_otp_guard_test` moves to that path — the `sent: false` guard still matters there, and that is now the only place it can fire. ── One screen, three entrances ── `POST /auth/login` says which of them a number is before anything is asked, so the app never guesses. Guessing is not cosmetic: offer "create a PIN" to a returning customer and the server answers `pin_already_set` on a screen that cannot succeed; offer "enter your PIN" to somebody who has never set one and every attempt is wrong. The separate sign-up screen is deleted rather than hidden. It asked for a name and then sent an SMS code — a second entrance asking the same questions and posting a letter that never lands. A new number now gives its name and PIN on the same screen. ── A second sign-in path found a latent bug ── `AppState.signIn` only started `refreshOrders`, and the OTP screen called `detectPickupLocation` itself afterwards to make up the difference. That held exactly as long as there was one sign-in screen. PIN sign-in did not know about the extra call, so Home opened with no pickup and no serviceable cities. The work belongs to signing in, not to whichever screen happened to be last, so it moved into `signIn` and the OTP screen's copy is gone. ── What the screen deliberately does not do ── It does not greet by name. `POST /auth/login` returns the account holder's name, which tells anybody who types a number who owns it; the field is read but never displayed, so it disappears quietly when the backend drops it. It does not say whether the number or the PIN was wrong — the server answers identically for both on purpose, and narrowing it here would turn sign-in into a way of testing whether a number has an account. "Forgot your PIN?" renders only when a support contact is configured. There is no reset endpoint, so it can only point at a human — and telling somebody locked out that help exists without saying where is worse than silence. ── The handover note does not reach the Miler ── The app said "we pass this to your Miler as a note". It does not: `remarks` reaches the admin console and stops, because the rider app reads a `notes` field per stop that the backend never sends. A customer could hand their parcel to a neighbour believing the Miler had been told. Both screens now say it is recorded on the booking, and that the Miler still calls the account's number. ── Also ── DmTextField gains `obscure`, and PinScreen carries a back button — without one the only correction for a mistyped digit was killing the app. |
|||
| c3e25feaea |
Five payload bugs, four pages behind dead rows, and one sheet
── The full-address path was reaching the Miler empty ──
`DestinationGroup.toBookingJson` spread its details FLAT across the
destination. The contract nests them under `details{}`, and a destination
carrying keys the server does not recognise is accepted without a word — so
every building number, street, landmark, recipient name, recipient phone and
pin a customer typed was written, answered 201, and thrown away. The Miler
arrived with a district.
Four more on the same call. The destination pin spelled `latitude`/`longitude`
— the same spelling that answered 422 unserviceable for months on the pickup
before it was fixed there and missed here. A PATCH that sent `null` to clear a
field, with a comment saying so, when the server writes only non-nil values, so
a landmark could be added and never removed. Per-destination `instructions`
folded into the visit's one `remarks` line on the belief the contract had no
per-destination note; it has one. And `contactName`/`contactPhone` on the
pickup object, which the create contract has no room for and drops.
The fix ships unverified, deliberately. If `details{}` is also the wrong shape
the fields drop exactly as they do today — it cannot be worse, and holding it
costs every full-address booking in the meantime. docs/BACKEND_CHANGES.md asks
for the confirmation; tool/verify_booking.sh runs it in one command.
── Who the Miler rings ──
One number reaches the rider and it is the account's: `GET /miler/bookings`
returns a single `customerphone`, verified against production and written down
in the rider app's own stop_contact.dart. So "Someone else is handing it over?"
was collecting a number that reached nobody.
Review now shows the number that will actually be dialled, and the handover
person travels in `remarks` with a name, labelled for whoever reads it. Both
screens say plainly that the rider's call button still dials the account —
better than letting somebody hand their parcel to a neighbour believing
otherwise.
── Account's rows led nowhere ──
Two had no `onTap` at all — a chevron pointing at a page that did not exist —
and three answered with a toast. Five rows making a promise, one keeping it.
Notifications, Payment, Help and About are real screens now, written to one
rule: say only what is true of this app today. There is no notification
endpoint, no stored payment instrument and no push SDK wired in, so none of
them pretends to manage any of that. Support shows no contact block at all
rather than a number that rings nowhere — AppConfig carries the fields empty
until somebody fills them in.
── ONE TOUCH is one sheet ──
It was two in sequence with a dismissal between them, and the destination step
made you open a state to see any city — two levels of navigation for something
its own search already flattened. One flat list headed by state, which is also
the answer to "where do you deliver?", and one surface that changes its
question instead of closing so another can open.
Home says the reach in a line, and it needed two fixes to appear at all:
`cachedCities` walked closed states looking for districts that are only fetched
for open ones, and `loadCities` filled two caches while notifying nobody.
── Sending a second parcel ──
`maxDestinations` is 1 in production, so two parcels for two places means
booking twice — and that cost the whole flow twice, re-answering a door the
customer had not moved from. `startBookingFrom` carries the door, carries the
destination only when asked, and never carries the window: a slot fills up, and
a second booking pinned to one that is now full is refused at confirm with
nothing the customer can act on.
Review also says why there is no "add another destination", so a cap reads as a
limit rather than a missing button.
── Bundle ──
pubspec named its images one by one. Declaring `assets/images/` as a folder
shipped a 974 KB launcher-icon master to every customer for a file no code
opens.
|
|||
| 8427824951 |
A launch that opens in brand, and a sheet that stops resizing
── The splash, in three beats ── Crimson edge to edge with the truck running across it in white; the invert; then the mark. The red starts before Flutter does. Four surfaces painted white before any Dart runs — launch_background at both API levels, windowSplashScreenBackground on Android 12+ in light and dark, and the iOS launch storyboard — and leaving any one of them white makes the launch a white flash followed by a red one. That flash is the only part of a launch a customer consciously notices. The invert is one gesture rather than a fade. A white truck on a background turning white is an invisible truck, so the ground lightens as the truck darkens, off one controller, and the screen turns itself inside out with the truck still on it. Fading it would have left the screen empty for the moment before the mark. The truck's colour is no longer its own: it is painted through srcIn, so the file's palette is discarded and only alpha survives. A replacement Lottie now needs no preparation, and tool/lottie_brand.py is off this path. `splash.json` is a seamless 3.9s loop — frame 0 is frame 60, the truck never arrives or departs — so there is no completion to hand over on. _truckBeat is a decision about how long a launch may hold somebody, not a property of the file. The comment claiming six seconds was wrong on both counts. ── Three things the splash was getting wrong quietly ── It showed the wrong logo: doormile-icon.png, the previous mark, to a customer who had just tapped the new one. tool/icons.py now cuts doormile-mark.png from the same master alpha as the launcher icon, so they cannot drift again. The fallback loader was invisible on red. _RoadLoader painted in DmColors.brand on what used to be a white screen; on crimson that is crimson on crimson, and it drew nothing at all on exactly the devices that had fallen back to it. The mark appeared and left in the same frame — _minimum was the sum of the beats exactly, so the clock ran out as the entrance finished. Hence _markHold. ── The truck was not in the middle ── Not a layout bug: both beats sit in a Center and always did. The artwork is drawn low and to the right inside its own 500x500 composition, so a centred widget rendered an off-centre picture — 30pt right, 36pt down. splash_centring_test.dart renders a frame at phone size and density, finds the ink and fails if either beat drifts. It is the only form of test that could have caught this, and the one that will catch it again when the Lottie is replaced, which is when the correction goes stale. Two things it taught: one enormous pump does not let the splash's async start-up chain advance, and capturing at pixelRatio 1 rasterizes the speed lines too faintly to detect, which truncates the bounding box and moves the measured centre by 12pt. ── The destination sheet stops resizing ── Tapping ONE TOUCH opened a tall sheet that snapped shorter a few frames later. DmAsyncList renders four skeleton rows while it loads — 302pt — and the states that replace them are nearer 200; the sheet was Flexible, so it was as tall as whichever state its content happened to be in, and the modal is still animating up while that swap happens. The list now lives in a box of one height. That also removes a second resize: the sheet grew from 48% of the screen to 74% when a state was picked. Both steps now measure 64% and it never changes size again. And DmAsyncList takes initialItems, fed by AppState.cachedCities: FutureBuilder reports `waiting` on its first build even for an already-complete future, so a warm cache still flashed a skeleton over data it already had. Three skeleton rows here rather than four — sheet_stability_test caught that 302pt overflows the smallest box the clamp can produce. |
|||
| 8956e818a4 |
One Touch, and a pickup map worth the name
Two ways to book the same parcel, and the difference said where the choice is made rather than in a paragraph about it. ONE TOUCH is the sphere: a state, a district, a window, and nothing else. The door, the recipient and the weight go out omitted, which the contract already treats as "not added yet" - the Miler writes them down standing at the address with the parcel in hand, which is the only moment anybody knows them. The pick/drop form under it is the other way in. PICKUP already carries the detected address; DROP is the only row with a question on it, and it asks for the full door - once per destination - before the window. Captions rather than an explainer: "Just a city and a time" under the sphere, "Enter the full address yourself" on the row. The pickup screen is a map screen now instead of a screen with a map on it. Full-bleed basemap, pin nailed to the centre, floating search and back, and a fixed sheet carrying what the pin resolved to. The collect-from sheet can hand off to it and carry the pin back. Also: * Home reads what-is-running, then the one gesture, then the form. The live card lost its courier row and half its height. * The sphere turns inside out on press - white floods from the centre and the word inverts through the clip, not by tween - and bounces back on release. * Order IDs copy, on Orders and on tracking. * Tracking says PICKUP and DROP with an arrow between them, carries the reference at the foot of the card it identifies, and shows every timing exactly once. * The wordmark bar is gone from all three roots; Orders and Account had no SafeArea under it, so their titles were sitting under the notch. flutter analyze: clean. flutter test: 90 passing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ |
|||
| 06fa6b797a |
Redesign: Poppins, a two-step destination, and a splash that says what the app does
The effort pass, end to end. Every screen was run through one test — if I remove this sentence, does the customer make a worse decision? — and the parts that failed it are gone. The flow Home ▸ BOOK ▸ Where is it going? ▸ When shall we collect? ▸ details ▸ booked BOOK opens a sheet, not a form. The destination is browsed state-then-district because a flat list of every serviceable district survives twelve and not sixty, and search cuts across states because somebody who knows they are sending to Chennai should not have to know which state it is in. Districts multi-select, but only where the server allows it: BookingLimits advertises maxDestinations: 1 until the Miler build keys on consignmentid, and a sheet that ignored that would sell a booking the network cannot complete. The pickup window is now a step the customer answers rather than a slot chosen for them. A pickup window is a promise about somebody's afternoon. What the screens stopped saying Home lost the orb caption for returning customers and a four-cell live card. Send lost the city strip, both address fields, the optional disclosure and three sentences about charging — the route, the packages and the button are what is left. Tracking lost a radar with a bike in it, a Milers-in-your-zone count, a "Step 2 of 7" and a sentence describing the screen you were looking at. The window sheet lost "Fastest pickup", "4 Milers nearby" and "Relaxed evening handover". Type Poppins, which has no variable release — four static cuts, and the sans styles set fontWeight alone because fontVariations on a static font is ignored in silence. Every weight dropped a step and the tracking went deeper: Poppins is built on near-circles and carries more ink than the humanist faces before it. Objects One lit sphere on Home, and the primary button now takes its gradient and rim because a committing action that is not lit like the hero reads as a different material. The tracking rail's connector is crimson as far as the parcel has come, so the line is the progress bar. Confirmation is a white tick on green: crimson is this app's action colour and that screen has nothing left to do. Bugs found on the way The OTP screen dropped digits. Four fields passing focus along lose a keystroke that arrives mid-transition, so "1234" became "124" and the screen answered "That code did not match" — blaming the customer for its own race. One field now, four boxes that only draw. Nothing ever asked for the customer's location: detectPickupLocation was the OTP screen's job, so a restored session or an auto-login never triggered the permission prompt and the pickup map had nothing to centre on. The launcher icon and both splash screens pointed at a house drawn as two vector paths — a placeholder that shipped. The splash clock started when the widget was built rather than when it was visible, so the truck got 0.45s of a 1.8s beat behind Android's own splash. It waits on waitUntilFirstFrameRasterized now, raced against a timeout so a binding that never reports one cannot strand the app. Also: design/screens/ holds all 19 screens under readable names, tool/ has the scripts that refresh them and rebrand the Lottie, and DESIGN.md is current. flutter analyze clean. 88 tests, 1 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ |
|||
| 8207e27a97 |
Booking and sign-in flow, and the offline build back under guard
Picks up where
|
|||
| 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> |