Booking and sign-in flow, and the offline build back under guard

Picks up where 0d66627 left off. Three things.

SIGN-IN, CUT TO THE QUESTION IT ASKS
It opened with a quarter-screen crimson hero carrying a lockup, a CUSTOMER
badge, a display-size promise and a sub-line, then a Phone/Email toggle, then a
labelled field, then a card explaining what a verification code is. Six things
to read before the one thing to do.

Nobody arrives at a sign-in screen needing to be sold the product. It is a
heading, a field and a button now, which is where Uber, Bolt and Porter put
them. The toggle became one quiet line under the field — choosing a method was
the first decision on the screen, before the customer had seen what was being
asked, and almost everyone uses the phone. The privacy card went: it explained
that a code would be sent, which the next screen demonstrates a second later.

`DmTextField.label` is nullable for this — "Enter your mobile number" above a
field captioned "Phone number" is one sentence printed twice.

BOOKING, DOWN TO ONE SCREENFUL
Landmark, recipient name and recipient phone are all optional and were all
drawn at the weight of the two fields that are not, putting six rows of "you
may skip this" between the address and the button. They fold behind one row
that counts what is filled in rather than just saying "optional".

Four crimson section heads became one. An accent used five times on a screen is
not an accent; crimson now marks the destination, which is the only choice that
changes the price.

Together those put the window, the package count and the CTA above the fold.

THE OFFLINE BUILD, BACK, UNDER TWO RULES
Deleted on 15 Sep after it cost two rounds of hunting for bookings in the admin
console that had never left the phone. That was not caused by the fake
existing — it was caused by a fake that did not announce itself and that
nothing stopped from shipping. Both are closed:

  * `useDevData` is false in a release whatever the defines say;
  * `describe` leads with DEV DATA (offline) and shows "no network" rather than
    a host the build never contacts.

It is opt-in — `flutter run` still talks to the real API — which is the
property whose absence caused the original mess. `devAutoLogin` is deliberately
false under FLUTTER_TEST so the widget tests keep driving the real entrance.

    flutter run --dart-define=DM_MOCK=true

86 tests green, analyze clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EqVJPB9B4QuieZnBAAKgYQ
This commit is contained in:
2026-09-21 13:10:28 +05:30
parent 0d66627c3c
commit 8207e27a97
37 changed files with 1382 additions and 1566 deletions

View File

@@ -222,6 +222,45 @@ void main() {
});
});
group('which backend this build believes it is', () {
// `DM_ENV` used to default to `staging` for EVERY build, release included,
// while `_stagingBase` is `_prodBase`. So a release APK built without
// `--dart-define=DM_ENV=prod` talked to production and reported
// `isProd == false` — and `isProd` is what the non-network guards read: the
// Account screen's Build panel, `allowStageOverride`, `setStage`'s refusal,
// and the environment name on every bug report.
test('an unflagged debug build is staging, not production', () {
// This test run carries no DM_ENV, and under test kReleaseMode is false.
expect(AppConfig.environment, DoormileEnvironment.staging);
expect(AppConfig.isProd, isFalse);
});
test('staging and prod resolve to the same host today', () {
// The backend has not named a staging host, so the environment decides
// what the build CALLS ITSELF, not who it talks to. Worth pinning: the
// day a real staging host appears, this test failing is the reminder that
// the default now changes where traffic goes.
expect(AppConfig.baseUrl, 'https://api.doormile.com/api/v1');
});
test('the environment is named on every build description', () {
// What a bug report carries. A production session that called itself
// "staging" is how a real incident gets triaged against the wrong server.
expect(AppConfig.describe, contains(AppConfig.environment.name));
});
// NOT ASSERTABLE FROM HERE, and stated rather than faked: the release
// branch of `environment` is `kReleaseMode ? prod : staging`, and
// kReleaseMode is a compile-time const that is false under `flutter test`.
// There is no way to exercise the release arm from a debug test run, so
// this note is the coverage. Verify it with:
//
// flutter build apk --release → Account shows no Build panel
// flutter build apk --release --dart-define=DM_ENV=staging
// → Build panel returns
});
group('development access', () {
test('no dev token is compiled into this build', () {
// A pasted token is not present unless a define supplies one.
@@ -229,33 +268,45 @@ void main() {
expect(AppConfig.describe, isNot(contains('DEV TOKEN')));
});
test('the test environment is a bypass, and says so out loud', () {
// The Miler app shipped a flag that swapped authentication for a fake
// rider on any four digits, and said nothing on screen. The rule that
// replaces it: a build that did not sign in must name it. Under
// FLUTTER_TEST the dev data is on, so it must read as bypassed and the
// environment line must carry the DEV DATA marker.
expect(AppConfig.useDevData, isTrue, reason: 'dev data is on under test');
expect(AppConfig.authBypassed, isTrue);
expect(AppConfig.describe, contains('DEV DATA'));
test('an offline build says so, every time', () {
// The offline build is back, and this is the guarantee that makes it
// safe to have. It was deleted once because a build answering from the
// fake looked exactly like a build answering from the server — which
// cost two rounds of hunting for bookings in the admin console that had
// never left the phone.
//
// A test run IS an offline build, so this asserts the announcement in
// the one place it can be asserted at all.
expect(AppConfig.useDevData, isTrue);
expect(AppConfig.describe, contains('DEV DATA (offline)'));
expect(
AppConfig.describe,
isNot(contains(AppConfig.baseUrl)),
reason: 'naming a host a build never contacts is the confusion this '
'whole guard exists to prevent',
);
});
test('the auto-login default never reaches the tests', () {
// Dev data now defaults on in a debug build, and with it the login
// screen is skipped — one `flutter run` lands on Home, because staging
// has no SMS gateway to send a code through.
//
// The widget tests must keep walking that entrance anyway: they cover the
// phone field, the code screen, and the guard that refuses to open the
// code screen when nothing was sent. A session waiting for them at launch
// would delete all of it and nothing would fail, so [devAutoLogin] is
// switched off under FLUTTER_TEST on purpose. This is that promise.
expect(AppConfig.devAutoLogin, isFalse);
test('no dev token and no auto-login are compiled in', () {
// The two ways to skip the login SCREEN without faking the login. Both
// hold a server-issued token; neither define is passed to this run.
expect(AppConfig.hasAutoLogin, isFalse);
expect(AppConfig.hasDevToken, isFalse);
});
test('the auto-login flag can never fire under test', () {
// The widget tests drive the real entrance — phone, code, and the guard
// that refuses to open the code screen when no code was sent. A session
// waiting for them at launch would delete that coverage and nothing
// would fail, so the flag is inert under FLUTTER_TEST whatever the
// defines say.
expect(AppConfig.isTest, isTrue);
expect(AppConfig.hasAutoLogin, isFalse);
});
// The guarantee that matters cannot be asserted from a debug test — every
// switch is `!kReleaseMode`, so `useDevData`, `devAutoLogin`, `hasDevToken`
// and `allowStageOverride` are all forced false in a release build,
// switch is `!kReleaseMode`, so `autoLoginIdentifier`, `hasDevToken` and
// `allowStageOverride` are all forced empty/false in a release build,
// whatever the defines say. That is what keeps a bypass out of the store.
});