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>
This commit is contained in:
2026-09-15 16:07:33 +05:30
parent 6c7d656de5
commit 0d66627c3c
305 changed files with 21930 additions and 11056 deletions

View File

@@ -1,22 +1,25 @@
import java.util.Properties
import java.io.FileInputStream
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
// Upload-key credentials live outside version control. Copy
// `android/key.properties.example` to `android/key.properties` and fill it in
// before building a release bundle; without it we fall back to debug signing so
// `flutter run --release` still works on a developer machine.
val keystoreProperties = Properties().apply {
val file = rootProject.file("key.properties")
if (file.exists()) {
file.inputStream().use { load(it) }
}
}
val hasUploadKey = keystoreProperties.getProperty("storeFile") != null
android {
namespace = "com.doormile.customer"
namespace = "in.doormile.customer"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -26,33 +29,46 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.doormile.customer"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
applicationId = "in.doormile.customer"
minSdk = 24
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = keystoreProperties["storeFile"]?.let { file(it as String) }
storePassword = keystoreProperties["storePassword"] as String?
if (hasUploadKey) {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
}
buildTypes {
release {
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfig = if (hasUploadKey) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
debug {
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
}
}
bundle {
language { enableSplit = false }
}
}
@@ -65,10 +81,3 @@ kotlin {
flutter {
source = "../.."
}
dependencies {
// Import the Firebase BoM
implementation(platform("com.google.firebase:firebase-bom:34.14.1"))
// Add the dependencies for Firebase products you want to use
implementation("com.google.firebase:firebase-analytics")
}

View File

@@ -1,29 +0,0 @@
{
"project_info": {
"project_number": "923472686173",
"project_id": "doormilecustomer",
"storage_bucket": "doormilecustomer.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:923472686173:android:175b38552da45f2d4ef1b4",
"android_client_info": {
"package_name": "com.doormile.customer"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAoxaF4fmYXU372qZkYAo7WftFqgiEg_7k"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -1,24 +1,3 @@
# Flutter Wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Firebase
-keep class com.google.firebase.** { *; }
# Flutter Deferred Components (Play Core)
-dontwarn com.google.android.play.core.**
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\sdk\tools\proguard\proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# Flutter's own rules are applied by the Flutter Gradle plugin. Keep this file
# for app-specific rules — e.g. models used with reflection by a JSON codec.
-keep class in.doormile.customer.** { *; }

View File

@@ -1,7 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- DEBUG ONLY. Android 9+ blocks cleartext HTTP by default, which makes a
local backend (http://<dev-machine-ip>:8080) unreachable from a real
handset. This override lives in the debug manifest, so a release build
is unaffected and still refuses cleartext.
-->
<application
android:usesCleartextTraffic="true"
tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -1,13 +1,29 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The app talks to the Doormile backend for serviceability, slots and
bookings. Declared in the main manifest so release builds have it too. -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- Drops the pickup pin at the customer's door. Both are optional at
runtime: refusing either leaves the map on the service-area centre and
the address can still be searched. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- The app is useful without GPS hardware, so Play should not filter it
out for devices that lack it. -->
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<application
android:label="doormile"
android:label="Doormile"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon">
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:enableOnBackInvokedCallback="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:dataExtractionRules="@xml/data_extraction_rules">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -16,6 +32,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
@@ -30,10 +47,6 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Google Maps API key — replace with your key from Google Cloud Console -->
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBhkGfnq27sN0wV5y_S-M2KojpFTk_by-Q" />
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@@ -50,8 +63,5 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.DIAL" />
</intent>
</queries>
</manifest>

View File

@@ -1,4 +1,4 @@
package com.doormile.customer
package `in`.doormile.customer
import io.flutter.embedding.android.FlutterActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Shown from process start until the first Flutter frame. Matches the white
Login screen so the handover is invisible. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
<item android:drawable="@color/dm_surface" />
<item
android:gravity="center"
android:width="120dp"
android:height="120dp"
android:drawable="@drawable/dm_mark_brand" />
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Doormile mark in brand maroon, for the launch screen.
Art is authored on a 24x24 grid and scaled into the 66dp safe zone. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="2.2" android:scaleY="2.2"
android:translateX="27.6" android:translateY="27.6">
<path
android:pathData="M3.6,9.2 L12,4 L20.4,9.2 L20.4,20 L3.6,20 Z"
android:strokeColor="#960019"
android:strokeWidth="1.8"
android:strokeLineJoin="round"
android:strokeLineCap="round" />
<path
android:pathData="M9.6,20 L9.6,13.4 L14.4,13.4 L14.4,20"
android:strokeColor="#960019"
android:strokeWidth="1.8"
android:strokeLineJoin="round"
android:strokeLineCap="round" />
</group>
</vector>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Doormile mark in white, for the adaptive-icon foreground.
Art is authored on a 24x24 grid and scaled into the 66dp safe zone. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group android:scaleX="2.2" android:scaleY="2.2"
android:translateX="27.6" android:translateY="27.6">
<path
android:pathData="M3.6,9.2 L12,4 L20.4,9.2 L20.4,20 L3.6,20 Z"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.8"
android:strokeLineJoin="round"
android:strokeLineCap="round" />
<path
android:pathData="M9.6,20 L9.6,13.4 L14.4,13.4 L14.4,20"
android:strokeColor="#FFFFFF"
android:strokeWidth="1.8"
android:strokeLineJoin="round"
android:strokeLineCap="round" />
</group>
</vector>

View File

@@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Shown from process start until the first Flutter frame. Matches the white
Login screen so the handover is invisible. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
<item android:drawable="@color/dm_surface" />
<item
android:gravity="center"
android:width="120dp"
android:height="120dp"
android:drawable="@drawable/dm_mark_brand" />
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/dm_brand" />
<foreground android:drawable="@drawable/dm_mark_white" />
<monochrome android:drawable="@drawable/dm_mark_white" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/dm_brand" />
<foreground android:drawable="@drawable/dm_mark_white" />
<monochrome android:drawable="@drawable/dm_mark_white" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,21 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<!-- Android 12+ uses the platform splash screen API. -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenBackground">@color/dm_surface</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/dm_mark_brand</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/dm_surface</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/dm_surface</item>
</style>
</resources>

View File

@@ -1,22 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<!-- Theme applied to the Android Window while the process starts. -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<!-- Theme applied once Flutter takes over drawing. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/dm_surface</item>
</style>
</resources>

View File

@@ -1,21 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<!-- Android 12+ uses the platform splash screen API. -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#FFFFFF</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:windowSplashScreenBackground">@color/dm_surface</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/dm_mark_brand</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/dm_surface</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowBackground">@color/dm_surface</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Doormile brand tokens, mirrored from lib/ui/tokens.dart -->
<color name="dm_brand">#960019</color>
<color name="dm_surface">#FFFFFF</color>
</resources>

View File

@@ -1,22 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<!-- Theme applied to the Android Window while the process starts. -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<!-- Theme applied once Flutter takes over drawing. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
<item name="android:windowBackground">@color/dm_surface</item>
</style>
</resources>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Nothing in this app should be backed up or transferred between devices:
the session is re-established by signing in. -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
</cloud-backup>
<device-transfer>
<exclude domain="root" />
<exclude domain="file" />
<exclude domain="database" />
<exclude domain="sharedpref" />
</device-transfer>
</data-extraction-rules>

View File

@@ -1,10 +1,6 @@
org.gradle.jvmargs=-Xmx2560m -XX:MaxMetaspaceSize=1024m -Dfile.encoding=UTF-8
org.gradle.parallel=false
org.gradle.workers.max=1
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false
kotlin.incremental=false

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-all.zip

View File

@@ -0,0 +1,6 @@
# Copy to android/key.properties and fill in before `flutter build appbundle`.
# Never commit the real file or the .jks keystore.
storePassword=your-keystore-password
keyPassword=your-key-password
keyAlias=upload
storeFile=upload-keystore.jks

View File

@@ -19,9 +19,8 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
id("com.google.gms.google-services") version "4.4.4" apply false
id("com.android.application") version "9.1.0" apply false
id("org.jetbrains.kotlin.android") version "2.4.0" apply false
}
include(":app")