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>
This commit is contained in:
2026-09-15 16:07:33 +05:30
parent 6c7d656de5
commit 0d66627c3c
305 changed files with 21930 additions and 11056 deletions

View File

@@ -0,0 +1,89 @@
import 'package:doormile_cx/data/location_service.dart';
import 'package:doormile_cx/main.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
/// Drives the real app on the simulator all the way through a booking, then
/// holds on the confirmation so it can be photographed from outside.
///
/// Run against the dev data, which is where any 4-digit code signs in and the
/// serviceability / slots / booking all exist without a server:
///
/// flutter test integration_test/book_one_test.dart \
/// -d <simulator> --dart-define=DM_MOCK=true
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('books a pickup to Chennai and lands on the confirmation',
(tester) async {
// Deterministic pin, so the pickup step never stops on an OS location
// dialog and resolves the same way every run.
LocationService.instance = FixedLocationService();
await tester.pumpWidget(const DoormileApp());
await tester.pumpAndSettle(const Duration(milliseconds: 400));
// The app runs continuous animations, so pumpAndSettle would spin forever.
// Pump fixed slices instead.
Future<void> hold([int ms = 900]) async {
await tester.pump();
await tester.pump(Duration(milliseconds: ms));
await tester.pump(const Duration(milliseconds: 400));
}
// ---- sign in: 9876543210 + any 4-digit code ----------------------------
await tester.enterText(find.byType(TextField).first, '9876543210');
await hold(300);
await tester.tap(find.text('Continue'));
await hold();
final boxes = find.byType(TextField);
for (var i = 0; i < 4; i++) {
await tester.enterText(boxes.at(i), '1');
await tester.pump();
}
await hold(1200);
expect(find.text('Where should we pick up?'), findsOneWidget);
debugPrint('DRIVER: signed in, on Home');
// ---- BOOK → confirm the pin -----------------------------------
await tester.tap(find.text('BOOK'));
await hold(1200);
await tester.tap(find.text('Next'));
await hold();
expect(find.text('Where are you sending it?'), findsOneWidget);
// ---- destination: Tamil Nadu ▸ Chennai ---------------------------------
await tester.tap(find.text('Tamil Nadu'));
await hold();
await tester.tap(find.text('Chennai'));
await hold();
expect(find.text('Chennai, Tamil Nadu'), findsOneWidget);
await tester.tap(find.text('Next'));
await hold();
// ---- pickup window -----------------------------------------------------
expect(find.text('When should we come?'), findsOneWidget);
await tester.tap(find.text('2:00 – 4:00 PM').first);
await hold();
await tester.tap(find.text('Review booking'));
await hold(1200);
// ---- review → book -----------------------------------------------------
expect(find.text('Review pickup details'), findsOneWidget);
debugPrint('DRIVER: on review');
await tester.tap(find.text('Book Pickup'));
await hold(1600);
// ---- confirmation ------------------------------------------------------
expect(find.text('Pickup booked'), findsOneWidget);
debugPrint('DRIVER: BOOKED — holding on confirmation');
// Hold so the confirmation can be photographed from outside the test.
for (var i = 0; i < 50; i++) {
await tester.pump(const Duration(milliseconds: 500));
}
});
}

View File

@@ -1,67 +0,0 @@
// Confirms Firebase Phone Auth actually dispatches an OTP SMS.
//
// This must run on a real device or emulator with Google Play services —
// `flutter_test`'s headless VM has no platform channels, so FirebaseAuth
// would throw MissingPluginException there. Each run sends one real, billed
// SMS (Blaze plan) to the hardcoded number below.
//
// Run with a device attached:
// flutter test integration_test/otp_send_test.dart -d <device-id>
// (list device ids with `flutter devices`)
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Firebase dispatches OTP SMS to a real phone number',
(tester) async {
await Firebase.initializeApp();
const phoneNumber = '+916374946729';
final completer = Completer<String>();
await FirebaseAuth.instance.verifyPhoneNumber(
phoneNumber: phoneNumber,
timeout: const Duration(seconds: 60),
verificationCompleted: (PhoneAuthCredential credential) {
if (!completer.isCompleted) {
completer.complete('verificationCompleted (device auto-verified)');
}
},
verificationFailed: (FirebaseAuthException e) {
if (!completer.isCompleted) {
completer.completeError('verificationFailed: ${e.code} — ${e.message}');
}
},
codeSent: (String verificationId, int? resendToken) {
if (!completer.isCompleted) {
completer.complete('codeSent (verificationId: $verificationId)');
}
},
codeAutoRetrievalTimeout: (String verificationId) {
if (!completer.isCompleted) {
completer.complete(
'codeAutoRetrievalTimeout (verificationId: $verificationId) — request was accepted, auto-retrieval just timed out');
}
},
);
final result = await completer.future.timeout(
const Duration(seconds: 45),
onTimeout: () => throw TimeoutException(
'No callback fired within 45s — check phone provider is enabled, '
'Blaze billing is active, and SHA-1/SHA-256 fingerprints are '
'registered in Firebase console.'),
);
debugPrint('OTP SEND RESULT: $result');
expect(result, isNotEmpty);
expect(result, isNot(contains('verificationFailed')));
});
}

View File

@@ -0,0 +1,52 @@
import 'package:doormile_cx/data/location_service.dart';
import 'package:doormile_cx/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
/// Walks the app the way a customer does — sign in, Orders, Completed, open an
/// order — then holds on the receipt so it can be captured from the simulator.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('Orders ▸ Completed ▸ order details', (tester) async {
// Deterministic pin: the walkthrough must never stop on an OS permission
// dialog, and the receipt it captures should look the same every run.
LocationService.instance = FixedLocationService();
await tester.pumpWidget(const DoormileApp());
await tester.pumpAndSettle(const Duration(milliseconds: 400));
Future<void> hold([int ms = 900]) async {
await tester.pump();
await tester.pump(Duration(milliseconds: ms));
await tester.pump(const Duration(milliseconds: 400));
}
await tester.enterText(find.byType(TextField).first, '9876543210');
await hold(300);
await tester.tap(find.text('Continue'));
await hold();
final boxes = find.byType(TextField);
for (var i = 0; i < 4; i++) {
await tester.enterText(boxes.at(i), '1');
await tester.pump();
}
await hold(1200);
await tester.tap(find.text('Orders'));
await hold();
await tester.tap(find.text('Completed'));
await hold();
await tester.tap(find.text('Salem, Tamil Nadu'));
await hold(1200);
expect(find.text('Collected at your door'), findsOneWidget);
// Hold the screen so it can be photographed from outside the test.
for (var i = 0; i < 40; i++) {
await tester.pump(const Duration(milliseconds: 500));
}
});
}