e2fe1b1771ef524e8632e54010c52a42132935f5
9 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.
|
|||
| 49b0de0d8f |
Pages fall away downward, the pickup comes back, and a real icon set
── Navigation ── Pages arrived from the right and left to the right. Right is the direction the system back gesture drags, so a page leaving under a button press read as a swipe nobody made — and every push in this app is a layer over the one before it, which arrives from below. The axis is vertical now: the incoming page rises and covers, the page underneath stays put and dims, and back drops the top page off the bottom. Full travel rather than the old 14% nudge, which is what makes the direction nameable. test/page_transition_test.dart holds it. ── Home ── The pickup pill is back under the greeting. It was cut on the argument that the form below already carried the pickup; the form is four hundred points down the screen, and a location you can only see by scrolling to a form is a location you do not trust. It shows the locality while the form keeps the door, so the screen answers one question once, and it now has a resolving state — it used to print the last place you were, confidently, while the fix was still in flight. The sphere's caption was falling off the screen. Adding the pill pushed it out of the scroll viewport, silently, with no overflow warning — the fourth time Home growing has clipped something under the sphere. DmBookOrb.field is no longer a constant: the glow gives way to whatever is left, the sphere never does. ── 74 points of dead canvas, on every root ── Home, Orders and Account each cleared the floating tab bar by `MediaQuery.paddingOf(context).bottom + 74`, on the reasoning that the inset is the home indicator and 74 is the bar above it. Scaffold with extendBody replaces the body's bottom padding with the bar's whole laid-out height — measured, a 34pt inset and this bar hand the body 106. They were clearing the bar, then clearing it again. One helper now, dmTabBarClear(). ── One journey, one vocabulary ── Review said ORIGIN/DESTINATION; Home's form and tracking say PICKUP/DROP. Three screens a customer walks in order. The tracking card had already written down the argument; the booking screen hadn't followed. The rail's first milestone read "Booking created" directly above "Order created" — down the rail: created, created, in transit. It is "Pickup booked", which is what JourneyStage.milestoneLabel has called that stage all along on Orders and on Home's live row. ── The launcher icon ── tool/icons.py renders all twenty-eight files from one master, because the failure mode of exporting by hand is nineteen replaced and nine left on the old mark, at the density nobody checks. The master is a bare mark on a field, so it cannot be dropped into Android's adaptive canvas whole: crimson is the background, the white mark is the foreground, and it is sized by its reach from the canvas centre so a round mask cannot take the arrow tip. Measured on the built file: 63.7dp against a 66dp safe zone. A real monochrome layer replaces the colour foreground that was standing in for one — the system tints by alpha, so that themed as a blob. ic_launcher_round.png exists at last; minSdk is 24 and android:roundIcon had nothing to resolve to below API 26. The artwork is flattened (the source vignettes over ten values of red) and its horizontal offset is placed deliberately rather than inherited — geometric centring made the mark lean, because the arrow carries the bounding box right while contributing almost none of the ink. ── Bundle ── pubspec declared assets/images/ as a folder, which shipped the 974 KB icon master to every customer for a file no code opens. Named explicitly now. |
|||
| 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 |
|||
| 86b6af48c2 |
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 |
|||
| 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> |