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>
175 lines
6.5 KiB
Dart
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,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|