Play's photo and video permissions policy refused version 150 over this, and
was right to. The policy allows the permission only where a system picker is
"technically insufficient to provide core app functionality", and the app's
real need is the opposite of that:
* every operational photograph opens the camera directly — parcel shots at
pickup, proof of delivery at the door, stop verification. None read the
library, deliberately: a picture chosen from the gallery could have been
taken anywhere at any time and would not be evidence that a parcel was
collected or delivered;
* the one gallery read in the whole app is a rider setting their profile
photograph, once, at sign-up.
That is the textbook "one-time or infrequent" case the policy points at the
Android photo picker for. image_picker 1.1.2 already routes
ImageSource.gallery through that system picker on Android 13+ when the
permission is absent, so the profile picker keeps working and needs no
permission at all.
READ_EXTERNAL_STORAGE stays, still capped at maxSdkVersion 32, for devices
older than the photo picker.
Verified absent from the built bundle's merged manifest rather than only from
the source.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
Play rejected the upload: the listing expects com.doormile.miler and the build
carried com.doormile.partner. This is a new app rather than an update — a
package rename gives Play a different app, so nothing carries over from the old
listing and versionCode restarts against an empty history.
applicationId moves; `namespace` and the Kotlin `package com.doormile.partner`
declarations deliberately do not. Those are the code's own package and are
allowed to differ from the application id — renaming them would mean moving
source directories to change a string nothing outside the build reads.
Four things did have to follow the id, because each of them names the app to
something outside it:
* the SHIFT_END_ALARM broadcast action, or two builds installed side by side
would answer each other's shift alarms;
* the update checker's androidId in main.dart and UpdateScreen.dart, which
looks the app up ON PLAY — left stale it would poll a different listing and
report a rider up to date when he is not;
* the Play URL the update screen sends him to, which would have opened the
store page for an app he does not have;
* the map tile and routing user-agents, which identify this app to OSM and
OSRM.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
Three fixes found by running the app on a real handset against production.
1. An expired token left the app looking signed in and unable to work.
MilerApi.onUnauthorized was declared and called on every 401 but never
assigned, so the token was dropped and nothing else happened: the profile
stayed on disk, logged_out stayed false, and the rider saw his own name over
a dashboard whose every call returned 401. He reads that as "no work today".
The teardown now lives in endSession() and both ways out of a session — the
Log out button and the 401 path — use it.
2. Arrived was written locally even when the rider was not there.
updateArrivedStatus answers false for three different things and the caller
treated all of them as "the write did not land", which is only true of one.
A geofence refusal and a server refusal now stop the rung and hand back the
reason; a dead network still advances, as it should.
3. A multi-destination customer pickup collapsed onto one stop.
GET /miler/bookings returns a row per destination once collected, all with
the same bookingid and reference. Every local store keys on that id, so the
accepted store deduped two of three drops away and their consignment ids
were unrecoverable. orderid is now the stop key; bookingreference stays the
booking's name. Cards show "Stop 2 of 3" and the receiver's own name and
number rather than the sender's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
Design system
- MilerSurface ladder (canvas → working → raised → floating) with MilerPanel
as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1.
- Visible vocabulary applied across Home, Deliveries, Activity, Account and
the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x),
canvas wells for anything that opens, small filled tags for shelf labels,
demoted placeholders. Recorded in DESIGN_SYSTEM.md §6.
- One icon family: 222 Material glyphs migrated to Lucide; none left outside
lib/xpress.
- Colour semantics corrected: amber only for what is genuinely owed, brand red
reserved for the live stop, disabled primaries go neutral rather than pale.
Data and lifecycle
- lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart
makes admin sequence the single ordering authority; service_day.dart, and
stop_area.dart rewritten against live Coimbatore addresses (digit-token
stripping, city stoplist, street suffixes, stammer collapse).
- countLabel states the load once, in bags.
Testing
- 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity,
sheets and verify, with test/failures/ now gitignored (diff debris).
- New pins: home_gutter_test, stop_area_test, plus updated structural bounds.
Note: this commit also carries pre-existing working-tree deletions that were
present before this work (API_SPEC.md, README.md, demo test fixtures).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>