Files
doormile_customer_app/lib/ui/screens/account_screen.dart
Thiru-tenext 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>
2026-09-15 16:07:33 +05:30

175 lines
6.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import '../../data/app_config.dart';
import '../../state/app_scope.dart';
import '../format.dart';
import '../tokens.dart';
import '../widgets/buttons.dart';
import '../widgets/cards.dart';
import '../widgets/chrome.dart';
import '../widgets/feedback.dart';
import '../widgets/inputs.dart';
import '../widgets/misc.dart';
import 'auth/login_screen.dart';
import 'sheets/place_search_sheet.dart';
/// Account — profile, preferences, and the build's environment line.
///
/// The profile folds into the brand header: the avatar takes the slot the other
/// tab roots give an action, the customer's name takes the slot the title holds
/// elsewhere, and the phone number sits in the recessed track. That removes the
/// profile *card* the screen used to open with — a card whose entire content
/// was three facts about the person already looking at the screen.
class AccountScreen extends StatelessWidget {
const AccountScreen({super.key});
@override
Widget build(BuildContext context) {
final app = AppScope.of(context);
final customer = app.customer;
// Empty rather than a stand-in name: this screen is only ever built for a
// signed-in customer, and a placeholder here would read as somebody's
// actual account.
final name = customer?.name ?? '';
final phone = customer?.phone ?? '';
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DmBrandHeader(
eyebrow: 'Account',
title: name.isEmpty ? 'Your account' : name,
leading: DmAvatar(initials: initialsOf(name), onBrand: true),
action: DmIconButton(
icon: LucideIcons.circleHelp,
tooltip: 'Help',
background: DmColors.onBrandRaise,
foreground: Colors.white,
border: Colors.transparent,
radius: DmRadius.lg,
size: 42,
onPressed: () => DmToast.show(context, 'Support is on the way'),
),
child: phone.isEmpty
? null
: DmHeaderTrack(
trailing: DmTextAction(
label: 'Edit',
color: Colors.white,
minHeight: 24,
onPressed: () => DmToast.show(context, 'Editing is coming soon'),
),
child: Text(
phone,
style: DmText.monoLg.copyWith(fontSize: 14.5, color: Colors.white),
),
),
),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(DmSpace.pad, 4, DmSpace.pad, 28),
children: [
const DmMicroHead('Preferences', top: 16),
DmRowGroup(
children: [
DmRow(
icon: LucideIcons.mapPin,
label: 'Saved addresses',
value: app.pickup?.title ?? 'None',
showChevron: true,
onTap: () async {
final place = await showPlaceSearchSheet(context);
if (place != null && context.mounted) {
app.setPickup(place);
DmToast.show(context, 'Pickup location updated');
}
},
),
const DmRow(
icon: LucideIcons.bell,
label: 'Notifications',
value: 'SMS and push',
showChevron: true,
),
const DmRow(
icon: LucideIcons.wallet,
label: 'Payment methods',
value: 'UPI',
showChevron: true,
),
const DmRow(
icon: LucideIcons.circleHelp,
label: 'Help and support',
showChevron: true,
),
],
),
// The environment line is not a control — it is what a bug
// report needs in order to be actionable. Always shown when the
// session came from a dev token rather than a sign-in.
if (!AppConfig.isProd || AppConfig.authBypassed) ...[
const DmMicroHead('Build', brand: false),
DmCard(
tone: DmCardTone.sunken,
children: [
DmCardCell(
// Not a DmRow: that lays a short label against a short
// value on one line, and this value is a URL plus every
// flag the build carries. It overflowed by 59px the
// moment a bypass was on — which is exactly when someone
// needs to read it.
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('API', style: DmText.eyebrow),
const SizedBox(height: 6),
Text(
AppConfig.describe,
style: DmText.mono.copyWith(
height: 1.5,
color: DmColors.ink2,
),
),
],
),
),
],
),
],
const SizedBox(height: 24),
DmButton(
label: 'Sign out',
kind: DmButtonKind.ghost,
danger: true,
onPressed: () {
app.signOut();
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute<void>(builder: (_) => const LoginScreen()),
(route) => false,
);
},
),
const SizedBox(height: 6),
Text.rich(
TextSpan(
style: DmText.tiny.copyWith(color: DmColors.ink4),
children: [
const TextSpan(text: 'Doormile Customer '),
TextSpan(
text: 'v1.0.0',
style: DmText.monoSm.copyWith(color: DmColors.ink4),
),
],
),
textAlign: TextAlign.center,
),
],
),
),
],
);
}
}