import 'dart:io' show Platform; import 'package:flutter/foundation.dart'; /// Which backend this build talks to. /// /// Set at compile time: `flutter run --dart-define=DM_ENV=prod`. enum DoormileEnvironment { dev, staging, prod } /// Build-time configuration: which backend this build talks to, and the /// identity it sends on every request. /// /// ── The one bypass, and why it is not the Miler app's bypass ── /// /// The Miler app shipped a `USE_NEW_API=false` flag that silently swapped /// authentication for a bypass — any four digits logged you in as a fake rider, /// with invented earnings, in a build that could ship. /// /// This app has one API implementation that talks to a server, and exactly one /// exception to it: [useDevData], now the **default in a debug build** because /// staging has no SMS gateway and the login screen cannot otherwise be walked. /// Three things keep it from becoming the flag that was deleted — it cannot /// exist in a release build (`!kReleaseMode`, not a define), it is named on the /// Account screen every time it is on, and nothing it does reaches a server. class AppConfig { AppConfig._(); // ---------------------------------------------------------------- environment static const String _envName = String.fromEnvironment( 'DM_ENV', defaultValue: 'staging', ); static DoormileEnvironment get environment => switch (_envName) { 'prod' || 'production' => DoormileEnvironment.prod, 'dev' || 'development' => DoormileEnvironment.dev, _ => DoormileEnvironment.staging, }; static bool get isProd => environment == DoormileEnvironment.prod; // ------------------------------------------------------------------- base URL /// Overrides the per-environment default. The staging host is not yet /// confirmed by the backend team, so staging builds are expected to pass /// `--dart-define=DM_API_BASE=https:///api/v1`. static const String _baseOverride = String.fromEnvironment('DM_API_BASE'); static const String _prodBase = 'https://api.doormile.com/api/v1'; /// Placeholder until the backend team names the staging host. A staging build /// without `DM_API_BASE` therefore talks to production, which is wrong but /// loud — it is better than silently talking to a host that does not exist. static const String _stagingBase = _prodBase; static String get baseUrl { if (_baseOverride.isNotEmpty) return _stripTrailingSlash(_baseOverride); return switch (environment) { DoormileEnvironment.prod => _prodBase, DoormileEnvironment.staging => _stagingBase, DoormileEnvironment.dev => _stripTrailingSlash( const String.fromEnvironment( 'DM_API_BASE_DEV', defaultValue: 'http://10.0.2.2:8080/api/v1', ), ), }; } static String _stripTrailingSlash(String s) => s.endsWith('/') ? s.substring(0, s.length - 1) : s; /// Everything in this document lives under one namespace. static String url(String path) => '$baseUrl/customer${path.startsWith('/') ? path : '/$path'}'; // ---------------------------------------------------------- development data // // ── The one exception to "no invented data", and it is now the debug default ── // // The shipping app has one API implementation and talks to a server; a build // that cannot reach one shows its error state rather than inventing a // booking, a slot or a price. [useDevData] is the single, loud exception. // // **Off by default again.** It was briefly the debug default so the app could // be opened at all, and the cost showed up immediately: a booking made in // that mode reaches no server and never appears in the admin console, which // looks exactly like a broken integration. Skipping the login screen is worth // a flag; faking every booking behind it is not. // // For a session without typing a code, use [autoLoginIdentifier] instead — // it signs in against the **real** API, so everything after it is real. // // --dart-define=DM_MOCK=true offline fake, for UI work with no server // --dart-define=DM_DEV_LOGIN=false keep the fake, but walk the entrance // // The guard has not moved: `!kReleaseMode`, so no define puts this in a // release build. When it is on the app talks to [DevDoormileApi] instead of // the network, `9876543210` + any 4-digit code signs in, and the Account // screen says `DEV DATA (offline)`. **Nothing it does reaches a server or the // admin console** — a booking made in this mode is not a booking. static const bool _devDataRequested = bool.fromEnvironment( 'DM_MOCK', defaultValue: false, ); /// True under `flutter test`, where the widget tests drive the whole journey /// against [DevDoormileApi] and there is no server to reach. static bool get isTest { if (kIsWeb) return false; try { return Platform.environment.containsKey('FLUTTER_TEST'); } catch (_) { return false; } } /// True only when dev data was asked for — or we are under test — **and** /// this is not a release build. There is no define that can turn it on in a /// release. static bool get useDevData => (_devDataRequested || isTest) && !kReleaseMode; /// Opens the app already signed in, skipping the login screen. /// /// On by default wherever [useDevData] is, which is the point of the pair: /// one `flutter run` and you are looking at Home. Only meaningful alongside /// dev data — there is no one to sign in as without it, so a build talking to /// the real API still signs in properly. /// /// To walk the entrance itself — the sign-in, sign-up and verify screens — /// turn this off and keep the dev data: `--dart-define=DM_DEV_LOGIN=false`, /// where `9876543210` and any 4-digit code gets you in. /// /// Never under `flutter test`: the widget tests drive the real entrance — /// phone, code, and the guard that refuses to open the code screen when no /// code was sent — and a session waiting for them at launch would delete /// that coverage silently. static bool get devAutoLogin => useDevData && !isTest && const bool.fromEnvironment('DM_DEV_LOGIN', defaultValue: true); // --------------------------------------------------------------- auto sign-in // // ── Skipping the login SCREEN without faking the login ── // // This is the flag to reach for when you want the app to open on Home and // still be a real customer: at launch, with no stored session, the app runs // the actual `POST /customer/auth/otp/request` + `/verify` pair for you. The // token it gets back is the server's, every call after it is authorised // normally, and a booking made in this build lands in `pickupbookings` and // shows up in the admin console. // // --dart-define=DM_LOGIN_AS=9876543210 --dart-define=DM_LOGIN_CODE=1234 // // The code has to be one the server will accept. Two ways to have one: // set `CX_STAGING_OTP` on a non-production backend (a fixed code, refused // outright when ENV=production), or read the code the backend logged — with // no SMS gateway registered its `logSender` writes every code to the // application log and reports the send as successful. // // It only has to work once per install: the session is persisted, so later // launches restore it and these defines stop mattering. // // `!kReleaseMode`, like everything else here, and named on the Account // screen whenever it is on. /// The phone number or email to sign in as. static String get autoLoginIdentifier => kReleaseMode ? '' : const String.fromEnvironment('DM_LOGIN_AS'); /// The verification code that identifier will accept. static String get autoLoginCode => kReleaseMode ? '' : const String.fromEnvironment('DM_LOGIN_CODE'); static bool get hasAutoLogin => autoLoginIdentifier.isNotEmpty && autoLoginCode.isNotEmpty; // ------------------------------------------------------------- ops QA only /// Mirrors the backend's own `CX_ALLOW_STAGE_OVERRIDE`: the QA helper at /// `POST /customer/ops/bookings/{reference}/stage` exists only off /// production, and only when ops has turned it on. /// /// Turning it on here does not create the endpoint — a build that asks for /// it against a server that has it disabled gets a refusal, which is the /// right outcome. It only decides whether the control is offered. static bool get allowStageOverride => !kReleaseMode && !isProd && const bool.fromEnvironment( 'DM_ALLOW_STAGE_OVERRIDE', defaultValue: false, ); /// Whether the tracking screen offers its stage stepper. On a dev build it /// walks [DevDoormileApi]'s in-memory booking; on staging it drives the real /// QA endpoint. static bool get showStageStepper => useDevData || allowStageOverride; // ------------------------------------------------------- development access // // ── The dev token is not a login bypass ── // // The Miler app shipped one of those: `USE_NEW_API=false` handed you a fake // "Demo Rider" on any four digits, with invented earnings. It was deleted, // and the reason is worth restating — a build flag that swaps authentication // for a bypass is not a development convenience, because the flag ships with // the binary and nothing in the UI says which mode you are in. // // So neither switch below authenticates anybody: // // [devToken] carries a **real, server-issued token** you already hold. The // server still authorises every request; this only spares you the OTP round // trip that staging cannot complete without an SMS gateway. It is not a // sign-in bypass — a revoked token signs the build straight back out. // // It is `!kReleaseMode`, so no define can put it into a release build, and it // is named on the Account screen whenever it is on. /// A real access token, pasted in at build time: /// `--dart-define=DM_DEV_TOKEN=eyJ...` static String get devToken => kReleaseMode ? '' : const String.fromEnvironment('DM_DEV_TOKEN'); /// The matching refresh token, if you have one. Without it the session /// simply dies when the access token expires, and you paste a fresh one. static String get devRefreshToken => kReleaseMode ? '' : const String.fromEnvironment('DM_DEV_REFRESH_TOKEN'); static bool get hasDevToken => devToken.isNotEmpty; /// True when this build got its session from somewhere other than a real /// sign-in — a pasted dev token, or dev data where any code is accepted. static bool get authBypassed => hasDevToken || useDevData || hasAutoLogin; // -------------------------------------------------------------- client identity /// `X-Client: doormile-cx/1.0.0+1`. Passed in at build time because reading /// it at runtime would mean another plugin for one string. static const String appVersion = String.fromEnvironment( 'DM_APP_VERSION', defaultValue: '1.0.0+1', ); static String get clientHeader => 'doormile-cx/$appVersion'; static String get platformHeader { if (kIsWeb) return 'web'; try { if (Platform.isAndroid) return 'android'; if (Platform.isIOS) return 'ios'; return Platform.operatingSystem; } catch (_) { return 'unknown'; } } // ------------------------------------------------------------------ timeouts static const Duration requestTimeout = Duration(seconds: 15); /// Booking creation is allowed longer: the backend's own budget for it is /// 1.2s p95, but it writes across several tables and we would rather wait /// than orphan a booking the server did create. static const Duration writeTimeout = Duration(seconds: 30); /// A one-line summary for the Account screen and for bug reports. /// /// Names the dev token whenever one is in use: a build whose session did not /// come from a sign-in must say so on screen. static String get describe => '${environment.name} · ${useDevData ? 'DEV DATA (offline)' : baseUrl}' '${allowStageOverride ? ' · STAGE OVERRIDE' : ''}' '${hasDevToken ? ' · DEV TOKEN' : ''}' '${hasAutoLogin ? ' · AUTO LOGIN ($autoLoginIdentifier)' : ''}'; }