diff --git a/design/screens/04-home.png b/design/screens/04-home.png index 45f0660..a351902 100644 Binary files a/design/screens/04-home.png and b/design/screens/04-home.png differ diff --git a/design/screens/06-orders-active.png b/design/screens/06-orders-active.png index a332d76..6baa1f5 100644 Binary files a/design/screens/06-orders-active.png and b/design/screens/06-orders-active.png differ diff --git a/design/screens/07-orders-completed.png b/design/screens/07-orders-completed.png index 42db7a8..439f2ee 100644 Binary files a/design/screens/07-orders-completed.png and b/design/screens/07-orders-completed.png differ diff --git a/design/screens/16-pickup-booked.png b/design/screens/16-pickup-booked.png index 6bf63ed..7b66464 100644 Binary files a/design/screens/16-pickup-booked.png and b/design/screens/16-pickup-booked.png differ diff --git a/design/screens/17-receipt.png b/design/screens/17-receipt.png index a1014a6..a85c348 100644 Binary files a/design/screens/17-receipt.png and b/design/screens/17-receipt.png differ diff --git a/lib/data/map_config.dart b/lib/data/map_config.dart index 1186f0f..7ee9ec2 100644 --- a/lib/data/map_config.dart +++ b/lib/data/map_config.dart @@ -124,10 +124,22 @@ class MapTileProvider { keyParam: 'api_key', ); - /// OpenStreetMap's own tile servers. + /// OpenStreetMap's own tile servers. **The app's default.** /// - /// Their usage policy does not permit a distributed app to lean on these — - /// present for local development and as a last-resort fallback only. + /// ── One thing to know before this ships ── + /// + /// These are donated servers, and the OSM Foundation's tile usage policy + /// does not permit a distributed app to lean on them. It asks for a valid + /// identifying User-Agent (we send one — see `DmMapTiles.layer`), no bulk + /// downloading, and it reserves the right to block traffic that grows past + /// what a hobby project would make. A block looks like every tile in the app + /// turning into the ground colour at once, with no other symptom. + /// + /// Nothing in the app needs to change when that becomes a problem: set + /// `DM_MAP_PROVIDER` to `carto`, `maptiler` or `stadia` — all three serve + /// OpenStreetMap data and the map looks near enough the same — and add the + /// provider's key as `DM_MAP_KEY`. The attribution line follows the provider + /// on its own, so nothing else is touched. static const osm = MapTileProvider( id: 'osm', name: 'OpenStreetMap standard tiles', @@ -237,15 +249,17 @@ class DmMapConfig { ); factory DmMapConfig.fromEnvironment() { - final id = _provider.isEmpty ? 'carto' : _provider; + // OpenStreetMap unless a build says otherwise. See [MapTileProvider.osm] + // for what that commits us to, and how to move off it in one define. + final id = _provider.isEmpty ? 'osm' : _provider; String? warning; MapTileProvider provider; if (id == 'custom') { if (_url.isEmpty) { - provider = MapTileProvider.carto; + provider = MapTileProvider.osm; warning = - 'DM_MAP_PROVIDER=custom needs DM_MAP_URL; fell back to CARTO.'; + 'DM_MAP_PROVIDER=custom needs DM_MAP_URL; fell back to OpenStreetMap.'; } else { provider = MapTileProvider( id: 'custom', @@ -268,9 +282,9 @@ class DmMapConfig { ); } } else { - provider = MapTileProvider.presets[id] ?? MapTileProvider.carto; + provider = MapTileProvider.presets[id] ?? MapTileProvider.osm; if (!MapTileProvider.presets.containsKey(id)) { - warning = 'Unknown DM_MAP_PROVIDER "$id"; fell back to CARTO.'; + warning = 'Unknown DM_MAP_PROVIDER "$id"; fell back to OpenStreetMap.'; } } diff --git a/lib/data/models.dart b/lib/data/models.dart index f44fbbb..59a9523 100644 --- a/lib/data/models.dart +++ b/lib/data/models.dart @@ -1044,7 +1044,7 @@ class DestinationGroup { return { ...destination.toJson(), 'packageCount': packageCount, - // Omitted rather than sent empty: One Touch fills none of this in, and + // Omitted rather than sent empty: BOOK NOW fills none of this in, and // `details: {}` is a key that says nothing. if (detail.isNotEmpty) 'details': detail, }; diff --git a/lib/state/app_state.dart b/lib/state/app_state.dart index 4ecf81b..c1342c0 100644 --- a/lib/state/app_state.dart +++ b/lib/state/app_state.dart @@ -463,7 +463,7 @@ class AppState extends ChangeNotifier { /// /// ── Two ways to book the same parcel ── /// - /// **One Touch** asks three things: the state, the district and a pickup + /// **BOOK NOW** asks three things: the state, the district and a pickup /// window. Nothing else — no door number, no recipient, no weight. Those are /// filled in by the Miler standing at the address with the parcel in their /// hand, which is the only moment anybody actually knows them. diff --git a/lib/ui/screens/booking/send_screen.dart b/lib/ui/screens/booking/send_screen.dart index 22ae411..d56f1dd 100644 --- a/lib/ui/screens/booking/send_screen.dart +++ b/lib/ui/screens/booking/send_screen.dart @@ -266,7 +266,7 @@ class _SendScreenState extends State { ), // ── Only the long way asks for this ── // - // One Touch books on a state, a district and a window; the door is + // BOOK NOW books on a state, a district and a window; the door is // filled in by the Miler who is standing at it. A customer who chose // the full form is choosing to save them that step, so the row is // here, it is outstanding until it is answered, and the button waits diff --git a/lib/ui/screens/home_screen.dart b/lib/ui/screens/home_screen.dart index 43b5b43..00ec1ff 100644 --- a/lib/ui/screens/home_screen.dart +++ b/lib/ui/screens/home_screen.dart @@ -87,7 +87,7 @@ class _HomeScreenState extends State { // The full form asks for the door before it asks for the window, and it // asks once per destination: a visit that fans out to three places is // three addresses, and "for all parcels" is the whole point of choosing - // this path over One Touch. + // this path over BOOK NOW. if (detailed) { for (final group in app.draftDestinations) { if (!mounted) return; @@ -730,7 +730,7 @@ class _PickupLine extends StatelessWidget { /// /// ── Why the app has two ways to start and only one of them is a sphere ── /// -/// One Touch is the product: a state, a district, a window, and the Miler +/// BOOK NOW is the product: a state, a district, a window, and the Miler /// writes down the door when they get there. It is a sphere because it is one /// gesture, and a gesture cannot be a form. /// diff --git a/lib/ui/screens/orders_screen.dart b/lib/ui/screens/orders_screen.dart index 92feb9a..4618dc9 100644 --- a/lib/ui/screens/orders_screen.dart +++ b/lib/ui/screens/orders_screen.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../../data/models.dart'; import '../../state/app_scope.dart'; import '../tokens.dart'; +import '../widgets/pieces.dart'; import '../widgets/inputs.dart'; import '../widgets/states.dart'; import 'order_row.dart'; @@ -183,20 +183,22 @@ class _OrdersScreenState extends State { /// /// Scrollable because a fourth state is one backend release away and a row of /// four fixed chips at 1.8x text has nowhere to go. Each carries its count in -/// the same pill, because "Active" and "4" are one fact. /// Active / Past / Cancelled, as a segmented control. /// -/// ── Chips → text tabs → a segment ── +/// ── Chips → text tabs → a segment → the app's own segment ── /// -/// The chips were three containers, one of them filled solid black, sitting -/// above a list of cards — the loudest object on the screen was the filter. -/// Plain text tabs with a rule fixed that and left the row looking like a -/// heading rather than a control. +/// The chips were three containers, one filled solid black, sitting above a +/// list of cards — the loudest object on the screen was the filter. Plain text +/// tabs with a rule fixed that and left the row looking like a heading rather +/// than a control. A groove with a raised chip in it was the right shape. /// -/// A groove with a raised chip in it is the shape that is unambiguously a -/// control and still quiet: white lifted out of a warm bed, ink type, and the -/// count beside the label because that is the reason anyone looks at the two -/// tabs they are not on. +/// This was then a *second* implementation of that shape: its own `_Tab`, a +/// pill radius, 3pt of padding and the count folded into the label's text — +/// beside the pickup window's day switcher, which is [DmChoiceChip] in a +/// rounded groove with 4pt of padding and the count in a bubble. Two controls +/// doing one job, drifting apart a padding value at a time. +/// +/// It is the same control now. The count was already a parameter of it. class _Filter extends StatelessWidget { const _Filter({ required this.labels, @@ -213,88 +215,28 @@ class _Filter extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.all(3), + padding: const EdgeInsets.all(4), decoration: BoxDecoration( color: DmColors.groove, - borderRadius: DmRadius.all(DmRadius.pill), + borderRadius: DmRadius.all(DmRadius.md), ), child: Row( - mainAxisSize: MainAxisSize.min, children: [ - for (var i = 0; i < labels.length; i++) - Flexible( - child: _Tab( - label: labels[i], - count: counts[i], - selected: i == index, - onTap: () => onChanged(i), - ), + for (var i = 0; i < labels.length; i++) ...[ + if (i > 0) const SizedBox(width: 4), + DmChoiceChip( + label: labels[i], + // Zero is not a figure worth a bubble — an empty tab says so by + // being empty when you open it. + count: (counts[i] ?? 0) > 0 ? counts[i] : null, + selected: i == index, + expand: true, + onTap: () => onChanged(i), ), + ], ], ), ); } } -class _Tab extends StatelessWidget { - const _Tab({ - required this.label, - required this.count, - required this.selected, - required this.onTap, - }); - - final String label; - final int? count; - final bool selected; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return Semantics( - button: true, - selected: selected, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - HapticFeedback.selectionClick(); - onTap(); - }, - child: AnimatedContainer( - duration: DmMotion.base, - curve: DmMotion.ease, - constraints: const BoxConstraints(minHeight: 34), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - alignment: Alignment.center, - decoration: BoxDecoration( - color: selected ? DmColors.surface : Colors.transparent, - borderRadius: DmRadius.all(DmRadius.pill), - boxShadow: selected ? DmShadow.card : null, - ), - child: Text.rich( - TextSpan( - children: [ - TextSpan(text: label), - if (count != null && count! > 0) - TextSpan( - text: ' ($count)', - style: DmText.small.copyWith( - fontSize: 12, - color: selected ? DmColors.ink3 : DmColors.ink4, - ), - ), - ], - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: DmText.label.copyWith( - fontSize: 13, - color: selected ? DmColors.ink : DmColors.ink3, - fontWeight: selected ? FontWeight.w700 : FontWeight.w600, - ), - ), - ), - ), - ); - } -} diff --git a/lib/ui/screens/sheets/drop_address_sheet.dart b/lib/ui/screens/sheets/drop_address_sheet.dart index fe59075..f34b7a8 100644 --- a/lib/ui/screens/sheets/drop_address_sheet.dart +++ b/lib/ui/screens/sheets/drop_address_sheet.dart @@ -13,7 +13,7 @@ import '../../widgets/inputs.dart'; /// /// Every field on it is optional to the contract and every one of them is /// something the Miler can fill in at the door. That is the whole argument for -/// One Touch. A customer who chooses the full form is choosing to save the +/// BOOK NOW. A customer who chooses the full form is choosing to save the /// Miler that step, so the form has to be quick enough to be worth choosing: /// one surface, five fields, one button. /// diff --git a/lib/ui/screens/sheets/pickup_sheet.dart b/lib/ui/screens/sheets/pickup_sheet.dart index c8aef4b..c942f34 100644 --- a/lib/ui/screens/sheets/pickup_sheet.dart +++ b/lib/ui/screens/sheets/pickup_sheet.dart @@ -9,7 +9,7 @@ import '../../widgets/feedback.dart'; import 'destination_sheet.dart'; import 'window_sheet.dart'; -/// What ONE TOUCH asks: where, and when — on one surface. +/// What BOOK NOW asks: where, and when — on one surface. /// /// ── What this replaced ── /// diff --git a/lib/ui/widgets/book_orb.dart b/lib/ui/widgets/book_orb.dart index f942d2f..78f98bc 100644 --- a/lib/ui/widgets/book_orb.dart +++ b/lib/ui/widgets/book_orb.dart @@ -60,7 +60,14 @@ class DmBookOrb extends StatefulWidget { const DmBookOrb({ super.key, required this.onTap, - this.label = 'ONE TOUCH', + // ── "BOOK NOW", not "ONE TOUCH" ── + // + // The old word named the *mechanism* — one gesture, no form — which is a + // thing the product team knows and a customer has to be taught. Nobody + // arrives at this screen wanting a touch; they want a parcel collected. + // "BOOK NOW" says what pressing it does, and the caption underneath still + // carries what makes it different from the form below. + this.label = 'BOOK NOW', this.caption, this.held = false, this.field = maxField, diff --git a/lib/ui/widgets/pieces.dart b/lib/ui/widgets/pieces.dart index 396f35c..10a96e6 100644 --- a/lib/ui/widgets/pieces.dart +++ b/lib/ui/widgets/pieces.dart @@ -131,7 +131,11 @@ class DmChoiceChip extends StatelessWidget { duration: DmMotion.base, curve: DmMotion.ease, constraints: const BoxConstraints(minHeight: 40), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9), + // 9, not 12. Three of these split a 390pt phone between them on the + // Orders filter, and at 12 the widest label truncated to "Cancell…". + // The pickup window's day switcher has two chips and acres of room, so + // it loses nothing. + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 9), alignment: Alignment.center, decoration: BoxDecoration( color: selected ? DmColors.surface : Colors.transparent, @@ -155,7 +159,7 @@ class DmChoiceChip extends StatelessWidget { ), ), if (count != null) ...[ - const SizedBox(width: 6), + const SizedBox(width: 5), Container( padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), decoration: BoxDecoration( diff --git a/test/booking_flow_test.dart b/test/booking_flow_test.dart index daf7594..ee856ab 100644 --- a/test/booking_flow_test.dart +++ b/test/booking_flow_test.dart @@ -92,7 +92,7 @@ Future serverAdvance( /// The sheet is left open: tests that are about the sheet itself go no /// further. Future openSend(WidgetTester tester) async { - await tester.tap(find.text('ONE TOUCH')); + await tester.tap(find.text('BOOK NOW')); await settle(tester); } @@ -218,14 +218,14 @@ void main() { await tester.tap(find.text('Sign in')); await settle(tester); expect(find.text('That number or PIN is incorrect'), findsOneWidget); - expect(find.text('ONE TOUCH'), findsNothing); + expect(find.text('BOOK NOW'), findsNothing); // The right one lands on Home. await tester.enterText(find.byType(TextField).first, '1234'); await settle(tester, 200); await tester.tap(find.text('Sign in')); await settle(tester); - expect(find.text('ONE TOUCH'), findsOneWidget); + expect(find.text('BOOK NOW'), findsOneWidget); await drainToasts(tester); }); @@ -256,7 +256,7 @@ void main() { await tester.tap(find.text('Create PIN and continue')); await settle(tester); - expect(find.text('ONE TOUCH'), findsOneWidget); + expect(find.text('BOOK NOW'), findsOneWidget); await drainToasts(tester); }); @@ -265,7 +265,7 @@ void main() { // Home greets by name, carries the sphere, and ends on the live card. expect(find.text('Hello, Joe'), findsOneWidget); - expect(find.text('ONE TOUCH'), findsOneWidget); + expect(find.text('BOOK NOW'), findsOneWidget); // The seeded active booking surfaces on Home. Its state is the uppercase // micro-label beside the destination, in the chip vocabulary — "Assigned", // not the rail's "Miler assigned". @@ -934,7 +934,7 @@ void main() { addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue); await signIn(tester); // login + OTP - expect(find.text('ONE TOUCH'), findsOneWidget); // home + expect(find.text('BOOK NOW'), findsOneWidget); // home await openSend(tester); // the whole booking, one screen await pickCity(tester, 'Chennai'); @@ -1014,15 +1014,19 @@ void main() { await tester.tap(find.bySemanticsLabel('Orders')); await settle(tester); - // Each tab carries its own count — the figure is the rows that tab will - // actually show, which for a collected multi-destination pickup is one - // per destination. The count shares a paragraph with the label, so it is - // a span rather than a Text of its own. + // ── Label and count are separate widgets now ── + // + // The filter used to fold the figure into the label's own paragraph + // ("Active (4)"), so this matched one span. It is [DmChoiceChip] now — + // the same control as the pickup window's day switcher — which puts the + // count in a bubble beside the label. Two Texts, not one string. + // + // The figure is still the rows that tab will show, which for a collected + // multi-destination pickup is one per destination. + expect(find.text('Active'), findsOneWidget); expect( - find.textContaining( - 'Active (${state.entriesOf(BookingStatus.active).length})', - ), - findsOneWidget, + find.text('${state.entriesOf(BookingStatus.active).length}'), + findsWidgets, ); // The tab row scrolls — three labels plus their counts do not fit a 390pt diff --git a/test/design_snapshot_test.dart b/test/design_snapshot_test.dart index 2d19f0f..60783a7 100644 --- a/test/design_snapshot_test.dart +++ b/test/design_snapshot_test.dart @@ -230,7 +230,7 @@ void main() { // districts inside a state, then a separate window sheet. There are no // states to open now — the list is flat and the state is a heading — and // the window is the same sheet's second step. - await tester.tap(find.text('ONE TOUCH')); + await tester.tap(find.text('BOOK NOW')); await _settle(tester); await _shot(tester, '07-pickup-where'); diff --git a/test/map_config_test.dart b/test/map_config_test.dart index 52ffd51..f7eeda2 100644 --- a/test/map_config_test.dart +++ b/test/map_config_test.dart @@ -53,13 +53,19 @@ void main() { }); group('resolved configuration', () { - test('defaults to CARTO and identifies the app to the provider', () { + test('defaults to OpenStreetMap and identifies the app to it', () { + // ── The User-Agent is not decoration here ── + // + // OSM's tile usage policy asks a distributed app for a valid + // identifying agent, and blocks traffic it cannot attribute. A block + // looks like every tile turning into the ground colour at once, with no + // other symptom — so this asserts the identification, not just the URL. final config = DmMapConfig.fromEnvironment(); - expect(config.provider.id, 'carto'); + expect(config.provider.id, 'osm'); expect(config.warning, isNull); expect(config.userAgent, contains('Doormile/')); expect(config.userAgent, contains(DmMapConfig.packageName)); - expect(config.attribution, contains('CARTO')); + expect(config.attribution, contains('OpenStreetMap')); }); test('is swappable without touching a screen', () { diff --git a/test/sheet_stability_test.dart b/test/sheet_stability_test.dart index c005eb3..8baef36 100644 --- a/test/sheet_stability_test.dart +++ b/test/sheet_stability_test.dart @@ -8,7 +8,7 @@ import 'package:doormile_cx/ui/widgets/states.dart'; /// ───────────────────────────────────────────────────────────────────────── /// A SHEET DOES NOT CHANGE SIZE WHILE IT IS OPENING /// -/// Tapping ONE TOUCH opened a tall sheet that snapped shorter a few frames +/// Tapping BOOK NOW opened a tall sheet that snapped shorter a few frames /// later. Nothing was wrong with either height — the sheet was simply as tall /// as whatever [DmAsyncList] happened to be rendering, and that changes twice /// before the customer has done anything: four skeleton rows at 302pt, then diff --git a/test/snapshots/03-home.png b/test/snapshots/03-home.png index 45f0660..a351902 100644 Binary files a/test/snapshots/03-home.png and b/test/snapshots/03-home.png differ diff --git a/test/snapshots/04-orders.png b/test/snapshots/04-orders.png index a332d76..6baa1f5 100644 Binary files a/test/snapshots/04-orders.png and b/test/snapshots/04-orders.png differ diff --git a/test/snapshots/04b-orders-completed.png b/test/snapshots/04b-orders-completed.png index 42db7a8..439f2ee 100644 Binary files a/test/snapshots/04b-orders-completed.png and b/test/snapshots/04b-orders-completed.png differ diff --git a/test/snapshots/11-booked.png b/test/snapshots/11-booked.png index 6bf63ed..7b66464 100644 Binary files a/test/snapshots/11-booked.png and b/test/snapshots/11-booked.png differ diff --git a/test/snapshots/12-receipt.png b/test/snapshots/12-receipt.png index a1014a6..a85c348 100644 Binary files a/test/snapshots/12-receipt.png and b/test/snapshots/12-receipt.png differ