Files
doormile_customer_app/lib/data/map_config.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

307 lines
11 KiB
Dart

/// Where map tiles come from, what must be credited, and any key needed to ask
/// for them.
///
/// This is the **only** place a tile URL, an attribution string or a map API
/// key exists. No widget builds a URL; screens ask for a style and get back
/// whatever the configured provider serves. Swapping CARTO for MapTiler, for a
/// keyed CARTO plan, or for self-hosted tiles is a build flag or one entry in
/// [MapTileProvider.presets] — no map screen changes.
///
/// Deliberately free of Flutter imports so the choice of provider is testable
/// as plain data. `lib/ui/widgets/map_tiles.dart` turns it into a `TileLayer`.
library;
/// The two cartographies the app uses.
enum DmMapStyle {
/// Streets, labels and landmarks. The pickup map, where the customer is
/// looking for their own door.
streets,
/// Quieter, near-monochrome. The route map, where the line is the subject
/// and the basemap is context.
muted,
}
/// A raster tile source: its URL templates, its credit line, and its key.
class MapTileProvider {
const MapTileProvider({
required this.id,
required this.name,
required this.templates,
required this.attribution,
this.attributionUrl = osmCopyright,
this.subdomains = const <String>[],
this.keyParam,
this.apiKey = '',
this.requiresKey = false,
this.maxNativeZoom = 20,
this.retina = true,
});
/// Stable id, also what `DM_MAP_PROVIDER` selects.
final String id;
/// Human name, for the "map source" line in diagnostics.
final String name;
/// One URL template per style. `{s}` `{z}` `{x}` `{y}` `{r}` as flutter_map
/// expects; the key is appended by [urlFor], never written into a template.
final Map<DmMapStyle, String> templates;
/// The credit line that must stay visible on every map using this source.
final String attribution;
/// Where the attribution links, for the licence.
final String attributionUrl;
/// Tile host shards, when the provider uses them.
final List<String> subdomains;
/// Query parameter the key travels in — `api_key` for CARTO and Stadia,
/// `key` for MapTiler. Null when the provider takes no key.
final String? keyParam;
/// The key itself. Supplied at build time, never committed.
final String apiKey;
/// True when the provider serves nothing without a key, so a missing key is
/// a misconfiguration rather than a downgrade.
final bool requiresKey;
final int maxNativeZoom;
/// Whether the provider serves `@2x` tiles for the `{r}` placeholder.
final bool retina;
static const osmCopyright = 'https://www.openstreetmap.org/copyright';
bool get hasKey => apiKey.isNotEmpty;
/// False when this provider cannot serve a tile as configured.
bool get usable => !requiresKey || hasKey;
/// The template for [style], with the key appended when there is one.
String urlFor(DmMapStyle style) {
final template = templates[style] ?? templates[DmMapStyle.streets]!;
if (!hasKey || keyParam == null) return template;
final separator = template.contains('?') ? '&' : '?';
return '$template$separator$keyParam=$apiKey';
}
MapTileProvider withKey(String key) => MapTileProvider(
id: id,
name: name,
templates: templates,
attribution: attribution,
attributionUrl: attributionUrl,
subdomains: subdomains,
keyParam: keyParam,
apiKey: key,
requiresKey: requiresKey,
maxNativeZoom: maxNativeZoom,
retina: retina,
);
// ------------------------------------------------------------- presets
/// CARTO's basemaps over OpenStreetMap data. The development default:
/// warm, legible, and usable without an account.
///
/// Anonymous use is **not** a production contract. CARTO issues API keys for
/// application use — set `DM_MAP_KEY` and it travels as `api_key` on every
/// tile request, no other change required.
static const carto = MapTileProvider(
id: 'carto',
name: 'CARTO basemaps (OpenStreetMap data)',
templates: {
DmMapStyle.streets:
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
DmMapStyle.muted:
'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}{r}.png',
},
attribution: '© OpenStreetMap contributors · © CARTO',
subdomains: ['a', 'b', 'c', 'd'],
keyParam: 'api_key',
);
/// OpenStreetMap's own tile servers.
///
/// Their usage policy does not permit a distributed app to lean on these —
/// present for local development and as a last-resort fallback only.
static const osm = MapTileProvider(
id: 'osm',
name: 'OpenStreetMap standard tiles',
templates: {
DmMapStyle.streets: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
DmMapStyle.muted: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
},
attribution: '© OpenStreetMap contributors',
maxNativeZoom: 19,
retina: false,
);
/// MapTiler — a commercial provider with an India-usable free tier.
static const maptiler = MapTileProvider(
id: 'maptiler',
name: 'MapTiler',
templates: {
DmMapStyle.streets:
'https://api.maptiler.com/maps/streets-v2/{z}/{x}/{y}{r}.png',
DmMapStyle.muted:
'https://api.maptiler.com/maps/dataviz-light/{z}/{x}/{y}{r}.png',
},
attribution: '© MapTiler · © OpenStreetMap contributors',
keyParam: 'key',
requiresKey: true,
);
/// Stadia Maps, serving OpenStreetMap-derived styles.
static const stadia = MapTileProvider(
id: 'stadia',
name: 'Stadia Maps',
templates: {
DmMapStyle.streets:
'https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.png',
DmMapStyle.muted:
'https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png',
},
attribution: '© Stadia Maps · © OpenStreetMap contributors',
keyParam: 'api_key',
requiresKey: true,
);
/// Everything that ships with the app, by id.
static const Map<String, MapTileProvider> presets = {
'carto': carto,
'osm': osm,
'maptiler': maptiler,
'stadia': stadia,
};
}
/// The resolved map configuration for this build.
///
/// Read once at startup from `--dart-define`s; [instance] is assignable so a
/// test or a staging build can substitute one without touching a screen.
class DmMapConfig {
const DmMapConfig({
required this.provider,
required this.userAgent,
this.warning,
});
final MapTileProvider provider;
/// Sent as the `User-Agent` on every tile request, so the provider can
/// identify and rate-limit the app properly rather than seeing raw traffic.
final String userAgent;
/// Set when the requested configuration could not be honoured — surfaced in
/// debug diagnostics rather than silently swallowed.
final String? warning;
/// Swappable for tests and staging.
static DmMapConfig instance = DmMapConfig.fromEnvironment();
/// The bundle id, which is also what identifies us to a tile provider.
static const packageName = 'in.doormile.customer';
// ---- build-time configuration -------------------------------------------
//
// flutter run \
// --dart-define=DM_MAP_PROVIDER=maptiler \
// --dart-define=DM_MAP_KEY=xxxxxxxx
//
// flutter build appbundle \
// --dart-define=DM_MAP_PROVIDER=carto \
// --dart-define=DM_MAP_KEY=$CARTO_API_KEY
//
// # a fully custom or self-hosted style
// flutter run \
// --dart-define=DM_MAP_PROVIDER=custom \
// --dart-define=DM_MAP_URL=https://tiles.doormile.in/streets/{z}/{x}/{y}.png \
// --dart-define=DM_MAP_URL_MUTED=https://tiles.doormile.in/light/{z}/{x}/{y}.png \
// --dart-define=DM_MAP_ATTRIBUTION='© Doormile · © OpenStreetMap contributors'
static const _provider = String.fromEnvironment('DM_MAP_PROVIDER');
static const _key = String.fromEnvironment('DM_MAP_KEY');
static const _url = String.fromEnvironment('DM_MAP_URL');
static const _urlMuted = String.fromEnvironment('DM_MAP_URL_MUTED');
static const _subdomains = String.fromEnvironment('DM_MAP_SUBDOMAINS');
static const _attribution = String.fromEnvironment('DM_MAP_ATTRIBUTION');
static const _attributionUrl = String.fromEnvironment('DM_MAP_ATTRIBUTION_URL');
static const _maxZoom = int.fromEnvironment('DM_MAP_MAX_ZOOM', defaultValue: 0);
static const _appVersion = String.fromEnvironment(
'DM_APP_VERSION',
defaultValue: '1.0.0',
);
factory DmMapConfig.fromEnvironment() {
final id = _provider.isEmpty ? 'carto' : _provider;
String? warning;
MapTileProvider provider;
if (id == 'custom') {
if (_url.isEmpty) {
provider = MapTileProvider.carto;
warning =
'DM_MAP_PROVIDER=custom needs DM_MAP_URL; fell back to CARTO.';
} else {
provider = MapTileProvider(
id: 'custom',
name: 'Custom tile source',
templates: {
DmMapStyle.streets: _url,
DmMapStyle.muted: _urlMuted.isEmpty ? _url : _urlMuted,
},
attribution: _attribution.isEmpty
? '© OpenStreetMap contributors'
: _attribution,
attributionUrl: _attributionUrl.isEmpty
? MapTileProvider.osmCopyright
: _attributionUrl,
subdomains: _subdomains.isEmpty
? const []
: _subdomains.split(',').map((s) => s.trim()).toList(),
maxNativeZoom: _maxZoom == 0 ? 20 : _maxZoom,
retina: _url.contains('{r}'),
);
}
} else {
provider = MapTileProvider.presets[id] ?? MapTileProvider.carto;
if (!MapTileProvider.presets.containsKey(id)) {
warning = 'Unknown DM_MAP_PROVIDER "$id"; fell back to CARTO.';
}
}
if (_key.isNotEmpty) provider = provider.withKey(_key);
// A keyed provider with no key would render nothing at all. Better a
// downgraded map than a grey rectangle in a customer's hands.
if (!provider.usable) {
warning =
'${provider.name} needs DM_MAP_KEY; fell back to CARTO for this build.';
provider = MapTileProvider.carto;
if (_key.isNotEmpty) provider = provider.withKey(_key);
}
return DmMapConfig(
provider: provider,
userAgent: 'Doormile/$_appVersion ($packageName)',
warning: warning,
);
}
String urlFor(DmMapStyle style) => provider.urlFor(style);
String get attribution => provider.attribution;
String get attributionUrl => provider.attributionUrl;
List<String> get subdomains => provider.subdomains;
int get maxNativeZoom => provider.maxNativeZoom;
bool get retina => provider.retina;
/// One line for a diagnostics screen or a log at startup.
String get describe =>
'${provider.name}${provider.hasKey ? ' (keyed)' : ' (anonymous)'}';
}