first commit

This commit is contained in:
2026-08-11 13:16:33 +05:30
parent 18531bf46b
commit c8350563d9
388 changed files with 69656 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

30
.metadata Normal file
View File

@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: web
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

682
ABOUT_MILER.md Normal file
View File

@@ -0,0 +1,682 @@
# Miler — the Doormile rider app
> *Miler Rider — Smarter Pickups, Powered by AI*
> Flutter · Android + iOS · `com.doormile.partner` · v1.2.28+144
This is the single reference for what this app is, who it serves, how it is built,
and how it was designed. If you are new to the codebase, read sections 1–3. If you
are about to change UI, read sections 5–7 before you write a line.
---
## Contents
1. [What Miler is](#1-what-miler-is)
2. [The people and the workflow](#2-the-people-and-the-workflow)
3. [The routing problem underneath](#3-the-routing-problem-underneath)
4. [Architecture](#4-architecture)
5. [Design principles](#5-design-principles)
6. [The design system](#6-the-design-system)
7. [Screen by screen — what was designed and why](#7-screen-by-screen--what-was-designed-and-why)
8. [Backend integration and known gaps](#8-backend-integration-and-known-gaps)
9. [What we are doing with this app now](#9-what-we-are-doing-with-this-app-now)
10. [Conventions for anyone touching this code](#10-conventions-for-anyone-touching-this-code)
---
## 1. What Miler is
Miler is the **rider app** built by **Doormile**, a logistics company that moves
parcels between customers and its hubs. It is the tool carried by the field agent
we call a **"miler"** — the person on a bike who actually collects and delivers
packages on the ground.
The system works like this. Customers use the separate **Doormile customer app**
to book a **time slot** — a window in which they want a parcel picked up or
delivered. Behind the scenes, for each slot, a **hub manager (admin)** gathers all
the customer bookings falling in that window and **assigns them to one miler as a
single ordered route** — Stop 1, Stop 2, Stop 3, through to the last stop. That
route appears inside this app in the exact sequence the admin set.
The miler goes **On Duty**, works the stops one by one in order, and at each stop
does either a pickup or a delivery or both :
- **Pickup** — collects the parcel from the customer or store, photographs it,
takes payment if money is owed (cash or UPI/QR), confirms it as picked up.
- **Delivery** — hands over a parcel carried from the hub, verifies with an **OTP
and a photo**, confirms it as delivered. Deliveries are prepaid, so no money
is collected.
He can **skip** a stop when a store is closed or a customer isn't available, and
come back to it later. He **cannot reorder** the route — the sequence is fixed by
the admin. After the last stop he **returns to the same hub he started from**,
bringing back everything he collected plus anything he couldn't deliver.
In short: a **first-mile + last-mile round trip**, `Hub → stop 1 → stop 2 → … →
stop N → back to the same Hub`, much like an Amazon or Flipkart delivery associate
runs a route. This app is the miler's companion for the whole journey.
### Doormile's two apps
| App | Who uses it | What they do |
|---|---|---|
| **Doormile customer app** | End customers | Book a time slot for a pickup or delivery |
| **Miler app** (this one) | The rider | Carry out the assigned route |
### Who this app is for
One person, on a bike, working through an assigned list of stops for a shift.
That is the entire user base. Every design decision in this document traces back
to that single user and one fact about him: **the daily loop repeats 30–50 times a
day.**
---
## 2. The people and the workflow
| Role | What they do | Which app |
|---|---|---|
| **Customer (CX)** | Books a time slot for a pickup or delivery | Doormile customer app |
| **Hub manager / Admin** | Bundles a slot's bookings into an ordered route and assigns it to a miler | Admin / hub system |
| **Miler (rider)** | Runs the route — pickups and deliveries — and returns to the hub | **This app** |
### The loop, step by step
1. **Slot booking** — customers book time slots in the customer app.
2. **Route assignment** — for a slot, the hub admin groups the bookings and
assigns them to one miler as a **fixed-order route** (Stop 1 … Stop N).
3. **Route appears** — the ordered stops show up in this app.
4. **On Duty** — the miler goes online to start working.
5. **Work each stop in order**
- *Pickup stop* → navigate → collect parcel → photo proof (+ weight) →
take payment if owed (Cash / UPI-QR) → confirm **Picked up**.
- *Delivery stop* → navigate → hand over parcel → verify **OTP + photo** →
confirm **Delivered** (prepaid, no payment).
6. **Skip if needed** — a stop can be skipped and resumed later. The order of the
remaining stops never changes.
7. **Move to next stop** — after each success the app prompts the next stop.
8. **Return to hub** — after the last stop, back to the **same hub** with all
collected parcels and any undelivered (RTO) items.
### Hard rules
- **One route per slot**, assigned by the admin.
- **Stops are strictly ordered** — the miler follows the sequence and cannot
reorder it.
- **Skip is allowed and normal** — not a failure; the stop can be resumed.
- **Round trip** — always starts and ends at the **same hub**.
- **Pickup vs Delivery** — each stop is one or the other; the app shows the type
clearly and runs the right flow.
### Vocabulary
Rider-facing language is deliberately narrow. **Route** (the whole assignment) →
**Stops** (ordered, each tagged Pickup or Delivery) → **Hub** (start and return
point). Avoid "booking", "task", and "order" in UI copy — they were used
interchangeably in the old build and meant nothing specific to a rider.
---
## 3. The routing problem underneath
A miler's day is one solved **Vehicle Routing Problem** instance. The hub admin
solves it; the rider executes it. He is explicitly *not* allowed to re-optimise —
he can't reorder stops — but he absolutely needs to see the **shape of the
solution he has been handed**, because every classic VRP constraint has a physical
consequence on the bike.
This is the intellectual backbone of the Home screen redesign. Each constraint maps
to something concrete:
| VRP concept | What it means to a miler | Where it appears |
|---|---|---|
| **Depot / round trip** | The route starts and ends at the same hub, so the return leg is real distance he has to ride | `hub → progress → hub` rail + `≈ km round trip` |
| **Capacity (CVRP)** | Total parcels and weight must fit in the box. Discovering an overload at stop 9 is too late | Parcels · weight kg |
| **Time windows (VRPTW)** | Each booking has a customer slot. Arriving after it closes is a failed stop | "*N* stops past their booked slot" / "close within 30 min" |
| **Route duration limit** | The shift is the hard cap on the route. Time left ÷ stops left is the pace he is actually racing | Shift countdown + "only *X* per stop left" |
| **Fixed sequence** | The admin's order is the solution; re-sorting it would break it | The list never reorders; skip-not-reorder |
| **Service time** | Each stop takes real minutes (photo, weight, payment, OTP) | Folded into the per-stop budget |
| **Cash on route** | Not a textbook VRP term, but he is carrying it and is accountable for it | ₹ to collect |
**All of this is derived from data the app already has** — stop coordinates,
quantities, `expected_pickup_time`, the shift window in prefs. No new endpoint was
required. The computation lives in `lib/views/Dashboard/home/route_brief.dart` as
a pure, unit-tested `RouteBrief` value object.
Two honesty constraints in that code:
- Distance is **straight-line haversine**, so it under-reads real road distance.
It is always rendered with a `≈` and never presented as a promise.
- Accepted stops have left the Home list for the Bookings tab, but they are still
on the route — their parcels still need collecting, their kilometres still need
riding. `RouteBrief.from(extraStops: …)` pulls them back into the totals so the
brief never understates the day.
Reference reading: [FarEye — Vehicle Routing
Problem](https://fareye.com/resources/blogs/vehicle-routing-problem-how-to-solve-it),
[ArcGIS Pro — Solve Vehicle Routing
Problem](https://pro.arcgis.com/en/pro-app/latest/tool-reference/ready-to-use/itemdesc-solvevehicleroutingproblem.htm).
---
## 4. Architecture
### Stack
| Concern | Choice |
|---|---|
| Framework | Flutter (Dart SDK ^3.9.2) |
| State / DI / routing | **GetX** (`GetMaterialApp`, `Get.put`, `Get.offAll`) + a little `provider` |
| Responsive sizing | `flutter_screenutil`, design size **390 × 844** — `.w` `.h` `.sp` `.r` everywhere |
| Persistence | `shared_preferences` (session, duty state, shift window, last GPS fix, accepted stops) |
| Maps & nav | `google_maps_flutter`, `flutter_polyline_points`, `geolocator`, `geocoding` |
| Push | Firebase Core + Messaging + `flutter_local_notifications` |
| Live tracking | MQTT (`mqtt_client`) + `flutter_foreground_task` |
| Media | `image_picker` (proof photos), `minio` (uploads), `qr_flutter` (UPI QR) |
| Motion | `lottie`, `shimmer`, custom primitives in `app_widgets.dart` |
### Directory map
```
lib/
├── main.dart App bootstrap, Firebase, GetMaterialApp, lifecycle
├── helpers/
│ ├── app_bootstrap.dart Decides the opening screen (replaced the splash)
│ ├── shift_end_alarm.dart Android AlarmManager via MethodChannel doormile/shift_end
│ └── http_overrides.dart
├── data/
│ ├── api_config.dart THE backend switch + legacy/v1 adapter
│ ├── accepted_store.dart Locally persisted accepted stops
│ ├── assignment_lookup.dart bookingid → bookingassignmentid resolver
│ └── mock_bookings.dart Demo data (gated behind !useNewApi)
├── Models/
│ └── stop_status.dart Canonical StopStatus enum + raw-string normaliser
├── controllers/ GetX controllers (auth, duty, pickups, summary, …)
├── providers/ HTTP layer, one per domain
├── background/ Foreground service, live tracking, background logs
├── utils/ Kalman filter, MQTT service, device info
└── views/
├── introscreens/ First-run intro
├── onboardscreens/ Sign in, OTP, MPIN
├── Dashboard/
│ ├── home/ Home + route_brief.dart
│ ├── pickups/ The whole per-stop flow (see §7)
│ ├── summary/ Earnings
│ └── profile/ Account, support, rewards, FAQ
├── helpers/constants/ Colors, fonts, spacing, theme
├── helpers/widgets/ Shared kit: app_widgets.dart, miler_app_bar.dart
└── offline/ Full-screen offline state
```
### App entry
`main.dart` initialises Firebase, registers permanent controllers
(`ProfileController`, `RiderLogController`, `PickupController`, `LogController`),
starts logging, checks whether the shift already ended while the app was closed,
and runs `GetMaterialApp` with `AppBootstrap` as `home`.
**`AppBootstrap`** (`lib/helpers/app_bootstrap.dart`) decides the opening screen
and nothing else:
- app version changed since last run → force re-verification (MPIN if the rider
was signed in, otherwise Sign In)
- signed in + on duty → dashboard
- signed in + off duty → the go-on-duty banner
- signed out, intro already seen → Sign In
- first ever launch → Introscreen
This replaced a four-second animated splash screen. That splash held two jobs: a
brand animation and these routing rules. Only the rules were load-bearing. The
animation was pure cost for someone opening this app 30–50 times a shift, layered
on top of the native launch image that already covers cold start. The routing
moved across intact; the four seconds are gone.
### Shell
`BottomPage` (`lib/widget/Bottom_page.dart`) is a floating pill nav over an
`IndexedStack` — four tabs, state preserved, no page ever mounted twice:
| Tab | Screen | Purpose |
|---|---|---|
| **Home** | `Homepage` | Greeting, duty toggle, route brief, new stops |
| **Bookings** | `MyPickups` | The active route — ordered stops, hub → progress → hub |
| **Earnings** | `Summary` | Pickup counts, success rate, reward points, distance |
| **Account** | `ProfilePage` | Rider details, support, settings |
Tab switches run a single 280 ms fade + rise controller. Pushed pages use
`Transition.cupertino` at 300 ms globally.
### Background and location
Location is the app's most safety-critical data path, so it gets real treatment:
- **`MilerKalmanFilter`** (`lib/utils/kalman_filter.dart`) — a 4-D Kalman filter
(`[lat, lng, v_lat, v_lng]`) smoothing raw GPS. Phone GPS on a moving bike is
noisy; unsmoothed fixes produce jumping breadcrumbs and inflated distances,
which matters because distance feeds rider payout.
- **`LiveTrackingService`** — position stream → Kalman → MQTT publish, with
battery level attached.
- **`foreground_service.dart`** — keeps logging alive with the screen off via
`flutter_foreground_task` in its own isolate.
- **`ShiftEndAlarm`** — Android `AlarmManager` through a `doormile/shift_end`
`MethodChannel`, so the shift-end break log fires **even if the app is killed**.
This is why "Shift not set" is treated as a genuine fault, not a cosmetic gap:
with no end time there is no alarm.
### Duty state
`DutyController` is the single source of truth for "is the rider online?".
Duty state used to be re-derived from raw prefs in a dozen places, and the server
`onduty` int is clobbered by a flaky rider-log sync — which auto-kicked riders
offline after a pickup. The controller fixes the priority in one place:
1. `onduty == 1` → a fresh server shift is authoritative
2. explicit `online` flag → the rider's own toggle, which survives the flaky sync
3. `onduty == 0` → offline
4. otherwise → default online
Similarly, `StopStatus` (`lib/Models/stop_status.dart`) normalises the backend's
free-form `orderstatus` string. It used to be compared as raw literals in ~90
places with case-sensitivity landmines (`'active'` vs `'ACTIVE'`) and variant
spellings (`'picked'` / `'picked up'` / `'pickuped'`). One enum, one parser, and
every filter in the app now agrees.
---
## 5. Design principles
These are not aspirational. Each one was derived from a specific failure in the
old build and is enforced in the code today.
### 1. The 30–50× rule
The daily loop repeats 30–50 times a day. Any confusion or extra tap becomes real
frustration multiplied many times over. This is the lens for every trade-off: a
one-second delay is not one second, it is a minute a day; a redundant tap is not
one tap, it is fifty.
*Consequence:* the splash animation was deleted. Slide gestures were replaced with
taps everywhere except money.
### 2. One screen, one primary action
The rider is standing at a gate with a parcel under one arm. There is exactly one
thing he wants to do next. That thing gets the biggest, highest-contrast control;
everything else recedes.
*Consequence:* the confirm sheet's hidden "swipe to update status" became three
visible tap buttons — one primary (Picked up / Delivered), two secondary (Skip,
Cancel). The Home stop card was cut down to one identity and one primary action.
### 3. Gestures are earned, not default
A slide is a deliberate friction device. It belongs only where an accidental tap
would be expensive. Everything else is a tap.
*Consequence:* slides remain only on **payment confirmation** and **going Off
Duty**. Start-pickup, arrived, and status confirm are all taps now.
### 4. Alert only when actionable; stay silent when healthy
If a coloured banner appears when nothing is wrong, riders learn to ignore all
coloured banners. Warnings must be rare enough to still mean something.
*Consequence:* the route brief's risk lines render **only** when there is risk. A
healthy route is a short, calm card. "Shift set" is a quiet grey strip; "Shift not
set" is a full warning surface with a pulsing icon, the consequence spelled out,
and a Retry — because an alert with no way out is just noise.
### 5. Never move things under the finger
The Bookings list polls every few seconds. Re-sorting a list while a thumb is
descending on it is how riders tap the wrong stop.
*Consequence:* polls don't reorder the visible list mid-interaction. `Reveal`
animations are keyed per logical item and play **once**, in `initState`, so a
3-second poll never replays them.
### 6. Readable at arm's length, in sunlight, with gloves
Minimum font sizes were raised (the old build went down to 7sp). Touch targets
have a 44 px floor. Contrast is checked against a white surface in daylight, not
against a designer's dark monitor.
### 7. Colour carries meaning, never decoration
See §6. Green means go. Maroon means brand or pickup. Blue means delivery. Amber
means warning or skip. Red means danger. A colour never appears for variety.
### 8. Motion is feedback, not garnish
Every animation answers a question: *did my tap register* (`PressScale`), *is this
new* (`Reveal`), *is something loading* (shimmer skeletons), *did I change tabs*
(the nav fade + rise). Nothing animates just to look expensive.
### 9. Degrade gracefully, log loudly
The backend has real gaps (§8). The app never blocks on a missing endpoint — it
falls back, logs `[API_GAP]`, and keeps the rider moving. A rider stuck at a gate
because a summary endpoint is missing is an unacceptable failure mode.
### 10. Honest numbers
If a number is an estimate, it is rendered as one. Straight-line distance shows
`≈`. Missing data renders `—`, never `0`. A metric that can't include accepted
stops doesn't get called "Stops left" until it does.
---
## 6. The design system
Reuse the tokens. Do not hardcode new shades or sizes.
### Colour — `lib/views/helpers/constants/Colorconstants.dart`
**Semantic roles** (agreed; use these names, not raw hex):
| Role | Token | Value |
|---|---|---|
| Brand / pickup accent | `ColorConstants.primary` | `#960019` maroon |
| Go / success / positive completion | `ColorConstants.acceptGreen` | `#12B76A` |
| Delivery / info accent | (in `stop_type.dart`) | `#2563EB` blue |
| Danger / cancel | `ColorConstants.errorRed` | `#DC2626` |
| Warning / skip | `ColorConstants.warning` | `#B45309` amber |
The green was consolidated from four competing values (`#16A34A`, `#34C759`,
`#2E7D32`, `#4CAF50`) that were scattered across Accept, Picked up, Delivered,
Confirm, payment dialogs, verify success, and the done screen. They are now one
token. A few `#007AFF` and `#BA1A1A` strays remain in nav and summary.
The full palette is a Material-3 role set — `surface*`, `onSurface*`, `outline*`,
`primary`/`onPrimary`/`primaryContainer`, `error*`, `warning*`, `success*` — so
new surfaces have a correct token without inventing one.
### Typography — `Font_constant.dart`
**Manrope**, bundled as the OFL variable font (`assets/fonts/Manrope/Manrope-VF.ttf`).
It is the free, legal stand-in for **Uber Move**, which is proprietary and cannot be
shipped. Fallback is Plus Jakarta Sans.
Every text style in the app routes through `FontConstants.fontFamily` — there are
no hardcoded family strings in `lib/`, so a single constant swaps the entire app's
typeface. Scale runs `displayLg 32` → `headlineMd 24` → `headlineSm 20` →
`bodyLg 18` → `bodyMd 16` → `labelBold 14` → `labelSm 12`.
### Spacing, radius, shadow — `design_constants.dart`
4 px base scale (`spacingXs 2` … `spacing5xl 40`), radius scale
(`radiusXs 2` … `radius2xl 20`, `radiusFull 999`), and five shadow levels
(`shadowXs` … `shadowXl`).
### Shared components — `views/helpers/widgets/`
| Component | File | Use |
|---|---|---|
| `AppCard` | `app_widgets.dart` | White rounded card with system shadow |
| `PrimaryButton` | `app_widgets.dart` | Solid maroon CTA |
| `Reveal` | `app_widgets.dart` | One-shot fade + rise; **keyed**, plays once |
| `staggerDelay(index)` | `app_widgets.dart` | Staggered list entrance |
| `PressScale` | `app_widgets.dart` | Press feedback for custom tappables |
| `SkeletonBone` / `MilerShimmer` / `skeletonCard()` / `SkeletonList` | `app_widgets.dart` | Shimmer loading, replaces bare spinners |
| `MilerAppBar` | `miler_app_bar.dart` | The one top bar — logo, title, subtitle, trailing, 60 px, hairline |
| `RouteBrief` / `RouteBriefCard` / `ShiftBanner` | `home/route_brief.dart` | VRP route summary + shift alert |
| `stopKindOf` / `StopKindUi` | `pickups/stop_type.dart` | Pickup vs delivery branch — accent, icon, labels, verbs |
**Do not hand-roll an AppBar.** Use `MilerAppBar`. Home is the one exception — it
carries a richer dashboard header — and its logo and typography are kept aligned
to the shared bar by hand.
### Auth screen pattern
All onboarding screens follow one shape: **top-anchored** content (heading and
input in the upper third, never vertically centred — this fixed the "input box too
low" complaint), a large left-aligned heading (26sp, w700, `-0.6` tracking), a
one-line helper (14sp, height 1.45), and a **pinned footer** with a single
high-contrast CTA — solid maroon fill, white text, w700, radius 14, 56.h, disabled
at alpha .35. Structure: top Column → `Expanded(SingleChildScrollView)` → pinned
footer. Mirror this for any new auth screen.
---
## 7. Screen by screen — what was designed and why
### Home — `views/Dashboard/home/homepage.dart`
The rider's staging area: who he is, whether he's on duty, what the day looks
like, and what's new.
**Header.** The brand mark sits at 56 px on a bordered, brand-tinted tile. At its
old 46 px, flat on a white header, it read as a smudge rather than a logo — the
tile gives it an edge. Next to it, **"Hi, {name}"** is the heaviest type in the
header (17sp / w800, near-black), with `Miler · {city}` beneath at 11.5sp / w600
secondary. The greeting is the anchor of the screen, so it gets the weight; the
location is context, so it recedes. On the right, the duty toggle.
**Shift banner.** Two deliberately different states. *Set* is a quiet grey strip
with the window and time remaining — confirmation, not news. *Not set* is a
warning surface with a left accent bar, a pulsing icon, the consequence stated
plainly ("duty hours and the shift-end reminder are off"), and a **Retry**. Without
a shift window the app cannot close duty and cannot fire the shift-end alarm, so
this is an operational fault. Retry re-reads prefs and says so honestly when the
fix is off-app — there is no standalone endpoint to re-pull the shift, only the
login / MPIN profile sync.
**Route brief.** The VRP card described in §3 — a `hub → progress → hub` rail
showing *X of Y stops done*, then stops left · parcels + weight · `≈ km round
trip` · ₹ to collect, then risk lines only when there is risk.
**Stop cards.** Redesigned Uber-style: PICKUP/DELIVERY badge + order id + circular
select in the header, a hero name (16.5sp w700, `-0.3` tracking), pin + address, a
grey items chip, and labelled Call / Details buttons. Removed along the way: a
misleading two-dot route timeline (a single stop is not a route — now a compact
location badge), and a redundant second identity row.
### Bookings / My Pickups — `views/Dashboard/pickups/pickups.dart`
The active route: the ordered list of stops with a hub → progress → hub overview.
Staggered `Reveal` on entry, keyed per stop so the poll never replays it.
### The per-stop flow — `views/Dashboard/pickups/`
| File | Role |
|---|---|
| `card.dart` | The stop card in the route list |
| `map.dart` | Map preview, navigation launch, **and the owner of the success-screen transition** |
| `multi_map.dart` | Whole-route map with real step numbers |
| `map_btn.dart` | The map-view entry row on the stop card |
| `pip.dart` | Picture-in-picture info card while navigating |
| `pickup_verify.dart` | Proof capture — photo, weight, OTP, signature plumbing |
| `sheet.dart` | The confirm bottom sheet |
| `skip_sheet.dart` | Skip with reason |
| `payment_screen.dart` | Cash / UPI-QR collection |
| `done.dart` | Success screen + "move to next stop" |
**Critical navigation architecture — do not regress this.** The confirm sheet
(`_PickupBottomSheet._handleConfirm`) must **not** navigate to `PickupsDone`
itself. Pushing a full page from inside a modal bottom sheet is unreliable — the
success screen silently never showed, and an `isCurrent` race then popped
everything. Instead the sheet **pops with an outcome map**:
```dart
{'outcome': 'completed'|'cancelled', 'isDelivery': bool,
'bonusPoints': int, 'paymentMethod': String}
// skip pops `true`; dismiss pops `null`
```
The **caller** — `map.dart`'s `_startPickupNavigation`, which is a normal page
route — inspects the result and does
`Navigator.pushReplacement(context, PickupsDone(...))` from its own route context
(`map.dart:788`). It also removes the finished stop from the list first, so it
doesn't linger as "active" blocking the next stop. This is what makes "Move to
next stop" appear after picked up / delivered / cancelled.
Other flow decisions: no-payment pickups skip the payment-method list entirely
(the reason list only shows for cancel or pickup-with-collection). The signature
step was dropped for pickups — photo plus weight is enough proof. Test pre-fills
in `pickup_verify.dart` were removed.
### Earnings — `views/Dashboard/summary/summary.dart`
Pickup counts, success rate, reward points, distance ridden. The hero count is
wrapped in an `AnimatedSwitcher` (fade + scale) so switching period animates
rather than cutting.
### Account — `views/Dashboard/profile/`
Rider details, support tickets, FAQ, notifications, alert sound, saved addresses,
rewards, terms. Page-entrance `Reveal`; `PressScale` on the tile rows.
### Offline — `views/offline/offline_page.dart`
A full-screen state with a breathing halo, stacked over the app whenever
connectivity drops. Retry routes back through `AppBootstrap`, which re-decides
login vs. home from scratch.
---
## 8. Backend integration and known gaps
The app targets the **new v1 backend** — `https://api.doormile.com/api/v1`,
Bearer-token auth, `{success, data, message}` envelopes, camelCase — alongside the
legacy jupiter/workolik backend.
**Strategy: adapter + env flag.** `lib/data/api_config.dart` is the one switch and
mapper:
- `ApiConfig.useNewApi` — defaults **true**; override with
`--dart-define=USE_NEW_API=false` to go back to the old backend and mock data.
- `ApiConfig.toLegacyEnvelope()` re-wraps v1 responses into the legacy
`{status, details}` shape with legacy field names.
- Providers branch on the flag and call the new endpoints; **the ~90 UI files are
untouched.**
Two mock layers had to be disabled for live data: `getMockQueues()` /
`getMockBookingPageItems()` fallbacks (now gated behind `!useNewApi`), and the
auth controller, which used to fake a Demo Rider (userid 9999) and bypass the
server entirely. In live mode `verifyPinWithServer` now does a real
`POST /miler/verify-pin` and succeeds only on a server OK plus a stored token.
**The pasted API doc was inaccurate** (verified live 2026-07-18). `verify-pin`
returns the login under **`user` / `user.profile`**, not `data`. The mapper reads
the real shape. Booking field names are still based on the unreliable doc —
**re-verify the `pickupFromBooking` mapping once a real booking is assigned.**
`AssignmentLookup` (`lib/data/assignment_lookup.dart`) exists because of one of
these mismatches: `POST /miler/assignments/{id}/accept` keys on
`bookingassignmentid`, but everything the app renders comes from
`GET /miler/bookings`, whose rows only carry `bookingid`. Different sequences,
different values. Sending a booking id made the backend answer 404 while the rider
saw success and the booking was never actually accepted. `GET /miler/assignments`
is the only place the pairing is exposed, so the app fetches it and keeps a
short-lived map.
### Known backend gaps
The app degrades gracefully around all of these and logs `[API_GAP]`:
- no skip / resume endpoint
- no booking-cancel endpoint
- **no per-stop `type` field** (pickup vs delivery) and no `step` ordering
- booking carries no `assignmentid` / `consignmentid` — accept, reject and deliver
assume they equal the booking id
- no COD amount on the booking
- no update-PIN or rider-count endpoints
- no bonus-summary endpoint (rewards are derived from `/miler/earnings`)
### Release safety
Demo and test toggles are tied to `kDebugMode` so they auto-disable in release
builds: `kBypassGeofenceForTesting` (`pickups_controller.dart`), `kDemoMixedRoute`
(`stop_type.dart`), and the summary weekly-kms mock fallback.
---
## 9. What we are doing with this app now
The programme is one thing: **make the app simple enough that a rider running it
30–50 times a day never has to think about it.** The app was already
well-built — branded app bars, a coherent maroon palette, a premium Earnings hero,
clean cards. It did not need a layout rewrite. It needed clarity, motion, and
honest data.
### Done
- **Terminology and IA** — one noun set (Route / Stops / Hub). Home = new stops
and route start; Bookings = the active route.
- **Typeface** — Plus Jakarta Sans → **Manrope**, verified on device.
- **Semantic colour consolidation** — four greens → one `acceptGreen`; roles
documented and applied.
- **Slide fatigue killed** — start-pickup and arrived became taps; the confirm
sheet's hidden swipe became three visible buttons. Slides remain only on payment
and Off Duty.
- **Motion layer** — `Reveal`, `PressScale`, shimmer skeletons; staggered lists on
Bookings, Home and notifications; `Transition.cupertino` globally; animated tab
switches over a state-preserving `IndexedStack`.
- **Auth screens** — one top-anchored pattern with a pinned solid-maroon CTA;
logo shrunk 180 → 104.h so content sits higher.
- **Mixed pickup/delivery plumbing** — `stop_type.dart` gives one branch point;
the delivery flow is fully wired end to end (card badge, OTP + photo verify,
no-payment "Delivered", done screen) behind `kDemoMixedRoute`.
- **Duty and status normalisation** — `DutyController` and `StopStatus` ended the
"rider randomly goes offline" and "status string mismatch" classes of bug.
- **Splash screen deleted** — routing preserved in `AppBootstrap`, four seconds
per launch removed.
- **Home redesign** — bigger, framed logo; bold greeting; shift-not-set promoted
to a real alert with a Retry; and the **VRP route brief** (§3), covered by 15
unit tests in `test/route_brief_test.dart`.
### Next
1. **Ship mixed routes for real.** Everything on the app side is built. It is
blocked on the backend sending a per-stop `type` (`pickup` | `delivery`) and a
`step` order. The moment it does, `stopKindOf` prefers it automatically and
`kDemoMixedRoute` becomes irrelevant.
2. **Re-verify `pickupFromBooking`** against a real assigned booking (§8).
3. **Retire the accept/reject metaphor on Home.** This is not an on-demand
marketplace — the route is pre-assigned, so multi-select checkboxes plus
"Select All" plus single-tap accept is three interaction models for a decision
the rider doesn't actually get to make. Reframe as *start assigned route*.
4. **Make skip/resume first-class** in the route view, and get a real backend
endpoint behind it.
5. **Close the remaining colour strays** (`#007AFF`, `#BA1A1A`) in nav and summary.
6. **Real road distance** in the route brief once a routing/matrix API is
available — the `≈` haversine is a deliberate placeholder.
---
## 10. Conventions for anyone touching this code
1. **Read §7's navigation warning before touching the pickup flow.** The
sheet-pops-an-outcome architecture is load-bearing and was arrived at after a
silent-failure bug.
2. **Use the tokens.** `ColorConstants`, `DesignConstants`, `FontConstants`. No
new hex values, no hardcoded font families, no magic spacing.
3. **Use the shared components.** `MilerAppBar` for headers, `AppCard` for cards,
`PrimaryButton` for CTAs, `Reveal`/`PressScale`/skeletons for motion and
loading. Hand-rolling one of these is how the app got four different greens.
4. **Size with ScreenUtil.** `.w` `.h` `.sp` `.r` against the 390 × 844 design
size. Use `.w` (not `.sp`) for anything that must not resize with the user's
font setting — logos, icon frames.
5. **New gestures are taps.** A slide needs a justification: it is only for
actions that would be expensive to trigger by accident.
6. **Keep `Reveal` keyed.** An unkeyed `Reveal` in a polled list replays every few
seconds and looks broken.
7. **Never block the rider on a missing endpoint.** Fall back, log `[API_GAP]`,
keep him moving.
8. **Render honesty.** `≈` for estimates, `—` for missing, never a fake `0`.
9. **Run `flutter analyze lib/ test/` before you call it done.** The target is
zero errors; the existing infos are legacy noise, don't add to them.
10. **Test the pure logic.** `RouteBrief` is a value object with no Flutter
dependency precisely so it can be unit-tested. Follow that pattern for new
derived data.
---
## The core idea, simply put
**A miler is handed a fixed, ordered route for a time slot, does every pickup and
delivery on it in sequence, and brings everything back to the hub he started
from — and this app guides him through every stop of that journey.**
Everything else in this document is in service of making that loop cost him as
little attention as possible, fifty times a day.

400
API_SPEC.md Normal file
View File

@@ -0,0 +1,400 @@
# Miler (Rider App) — Backend API Specification
**Audience:** Doormile backend team
**Purpose:** This is the contract the **Miler rider app** needs the backend to implement so we can connect everything and go live. It documents (1) what the app already sends and expects today, and (2) the changes required to ship the mixed pickup + delivery route feature.
**How to use this doc:** Design/confirm each endpoint below, then send back the finalized doc (exact URLs, request/response JSON, and any field renames). We then wire the app to your final contract and go live.
> Status legend for each endpoint:
> **[LIVE]** already called by the current app — keep the contract stable.
> **[CHANGE]** needs a change/addition before go-live.
> **[NEW]** not built yet.
---
## 1. Global conventions
### 1.1 Base URLs / environments
The app switches between **dev** and **live** by an environment flag. Please expose the same path on both:
```
DEV base: https://jupiter.doormile.app/dev/api
LIVE base: https://jupiter.doormile.app/live/api
```
> **⚠️ Must fix before go-live:** Today a few write endpoints (update pickup, create rider log, break logs) point at a **second host** `https://queue.workolik.com/live/api/...`, and the app currently has to **bypass TLS cert validation and hard-code the server IP** (`66.116.225.226`) because carrier DNS returns broken CDN nodes for that host and the certificate doesn't validate. **Please serve everything from one host (`jupiter.doormile.app`) with a valid TLS certificate and correct DNS** so we can remove the SSL-bypass hack. This is a security and reliability blocker.
### 1.2 Auth
- The app does **not** currently send a bearer token — requests are keyed by `userid`. **Please tell us the intended auth model.** Recommended: return a JWT/session token from login and require `Authorization: Bearer <token>` on every other call. If you keep `userid`-only, confirm that explicitly.
### 1.3 Request/response format
- Content type: `application/json` (both directions).
- **Standard response envelope** (already used by read endpoints — please use it everywhere):
```json
{
"code": 200,
"status": true,
"message": "Success",
"details": [ ... ] // object OR array — the actual payload
}
```
- The app reads the payload from `details` first, then falls back to `data`, then the root. **Please standardize on `details`.**
- On error return `status: false`, a non-2xx HTTP code, and a human-readable `message`.
### 1.4 Formats
- **Dates (query params):** `YYYY-MM-DD` (e.g. `2026-07-18`).
- **Timestamps (bodies):** full date-time string, currently `YYYY-MM-DD HH:mm:ss`. Confirm timezone — please use **IST** consistently and state it.
- **Lat/Long:** strings, decimal degrees (e.g. `"12.9716"`). Do not truncate precision.
- **Money:** number (₹). Confirm 2-decimal.
- **Booleans / flags:** confirm whether you use `true/false` or `1/0` — the app currently tolerates both for `status` but please pick one.
### 1.5 Cache-busting
Read endpoints receive a `t=<epoch-millis>` query param — ignore it server-side; it exists to defeat caching.
---
## 2. Authentication
### 2.1 Rider Login **[LIVE]**
`POST /v2/users/rider/login`
Request:
```json
{
"contactno": "9876543210",
"devicetype": "android", // "android" | "ios"
"configid": 123,
"deviceid": "<device-uuid>",
"userfcmtoken": "<fcm-token>",
"pin": 1234 // optional; sent on PIN login
}
```
Response `details` (object) — **every field below is consumed by the app**, so keep them:
```json
{
"userid": 1001,
"riderid": 55,
"partnerid": 12,
"configid": 123,
"shiftid": 7,
"logid": 0,
"logseconds": 0,
"tenantid": 3,
"locationid": 9,
"applocationid": 9,
"roleid": 2,
"authmode": 1,
"authname": "…",
"firstname": "Suriya",
"lastname": "K",
"username": "suriya",
"email": "…",
"onduty": 0, // 0 = off duty, 1 = on duty
"starttime": "09:00", // shift window (display)
"endtime": "18:00",
"pickupradius": 100, // meters — geofence radius for arrived/pickup
"fuelcharge": 5.0, // ₹ per km (rider payout)
"firstmilecharge": 0.0, // per-km first-mile charge (alias: firstmilecharges)
"userfcmtoken": "<echoed>"
}
```
Notes:
- `authmode` decides the flow (e.g. whether a PIN step is required). **Please document the possible values.**
- If a rider needs to set a PIN on first login, tell us how that state is signaled.
### 2.2 Update PIN **[LIVE]**
`PUT /v2/users/update`
```json
{ "userid": 1001, "pin": 1234 }
```
Response: standard envelope, `status: true` on success.
---
## 3. Rider duty log (On/Off Duty, breaks)
The rider goes **On Duty** → works stops → **Off Duty**. These calls power the duty timer and location tracking.
### 3.1 Create Rider Log (go On Duty) **[LIVE]**
`POST /v2/partners/createriderlog`
Request:
```json
{
"logid": 0, // 0 → server assigns new logid; returned in response
"userid": 1001,
"partnerid": 12,
"shiftid": 7,
"logdate": "2026-07-18T09:00:00",
"login": "2026-07-18 09:00:00", // on-duty timestamp
"onduty": 1,
"status": "online",
"latitude": "12.9716",
"longitude": "77.5946",
"raw_latitude": "12.9716",
"raw_longitude": "77.5946",
"velocity_lat": "0",
"velocity_lng": "0",
"speed": "0",
"heading": "0",
"contactno": "9876543210",
"tenantid": 3,
"locationid": 9,
"applocationid": 9,
"userfcmtoken": "<fcm-token>",
"orderid": "" // optional; current stop context if any
}
```
**Response must return the new `logid`** (the app stores it and uses it for updates). Return it in `details`.
### 3.2 Update Rider Log (heartbeat / go Off Duty) **[LIVE]**
`PUT /v1/partners/updateriderlog`
Sent periodically as a location heartbeat and once when going Off Duty:
```json
{
"logid": 4567,
"userid": 1001,
"logdate": "2026-07-18T13:00:00",
"latitude": "12.9722",
"longitude": "77.5950",
"speed": "0",
"heading": "0",
"status": "online", // "offline" when going off duty
"orderid": ""
}
```
Confirm the exact field(s) used to mark **Off Duty** (e.g. `status: "offline"` and/or `logout` timestamp + `onduty: 0`). Please state it explicitly.
### 3.3 Get Rider Log **[LIVE]**
`GET /v1/partners/getriderlog?userid=1001` → current log record (used to restore duty state on app restart). Return `logid`, `onduty`, `login` time, accumulated seconds, etc.
### 3.4 Get Rider Count **[LIVE]**
`GET /v1/partners/getridercount?userid=1001` → counts for the dashboard (e.g. completed stops today). **Please document exact fields.**
### 3.5 Break logs **[LIVE]**
- `POST /v2/partners/createbreaklog` — start break.
- `PUT /v2/partners/updatebreaklog` — end break.
Please document the exact request bodies (rider id, logid, start/end timestamps, break type).
---
## 4. Route & Stops (the core flow)
**Domain recap for the backend:** For a booked time slot, the hub/admin assigns a rider an **ordered route** of stops. The rider starts at a **hub**, works stops **in fixed sequence** (cannot reorder), can **skip and resume** a stop, and after the last stop **returns to the same hub**. Each stop is either a **PICKUP** or a **DELIVERY** (see §4.5 — this is the key new requirement).
### 4.1 Get Pickup Queue (assigned/pending stops) **[LIVE]**
`GET /v2/pickups/getpickupqueues?userid=1001&fromdate=2026-07-18&todate=2026-07-18&orderstatus=<optional>&t=<epoch>`
Returns `details` = **array of stop objects**. Fields the app reads today (please keep these names, lowercase):
| Field | Type | Meaning |
|---|---|---|
| `orderid` | string/int | Order identifier shown to rider |
| `pickupid` | int | **Stop id — primary key for all status updates** |
| `orderheaderid` | int | Order header id (sent back on updates) |
| `pickuplocationid` | int | Location id of the stop |
| `orderstatus` | string | Current status (see §4.6 lifecycle) |
| `step` | int | **Sequence position in the route (1..N) — defines fixed order** |
| `pickupcustomer` | string | Customer / store name |
| `pickupcontactno` | string | Customer phone (Call button) |
| `pickupaddress` | string | Stop address |
| `pickuplat` / `pickuplong` | string | Stop coordinates (geofence + navigation) |
| `dropaddress` | string | Drop address (delivery stops) |
| `droplat` / `droplon` | string | Drop coordinates (delivery stops) |
| `collectionamt` | number | Amount to collect at this stop (0 = none) |
| `pickupamt` | number | Pickup charge |
| `eta` / `expected_pickup_time` | string | ETA / expected time (display) |
| `tenantid` / `tenantname` | int/string | Tenant |
| `starttime` | string | Slot / assignment start |
> **⚠️ Casing:** the app has seen **both** `pickuplat`/`PickupLat` and `orderid`/`OrderId` variants in responses. **Please return one consistent casing (lowercase preferred) across all endpoints** and never mix within a payload.
### 4.2 Get Current Pickups **[LIVE]**
`GET /v1/pickups/getpickups?userid=1001&fromdate=&todate=&t=` — the rider's active/in-progress stops. Same object shape as §4.1.
### 4.3 Get Pickups V3 (date-bounded / picked history) **[LIVE]**
`GET /v3/pickups/getpickups?userid=1001&fromdate=&todate=&t=` — used for completed/"picked" history. Same object shape.
> Please clarify the intended difference between v1/v2/v3 of `getpickups` so we can consolidate. Ideally **one** endpoint filtered by `orderstatus` and date range.
### 4.4 Update Stop status **[LIVE — needs delivery extension, see §4.5]**
`PUT /v1/pickups/updatepickup`
This one endpoint is called at **every** state transition; `orderstatus` selects the transition. Common fields on all transitions:
```json
{
"pickupid": 8890,
"orderheaderid": 4501,
"orderstatus": "<state>",
"riderslat": "12.9716", // rider GPS at the moment
"riderslon": "77.5946",
"raw_latitude": "12.9716", "raw_longitude": "77.5946",
"velocity_lat": "0", "velocity_lng": "0", "speed": "0", "heading": "0",
"notes": ""
}
```
Per-transition additional fields:
**`accepted`** — rider starts the assigned route/stop.
**`active`** — rider en route to the stop.
**`arrived`** — rider reached the stop (passes geofence check):
```json
{ "orderstatus": "arrived", "arrivaltime": "2026-07-18 10:05:00", "pickuplat": "", "pickuplong": "", "actualkms": "", "pickupamt": 0.0 }
```
**`Picked up`** (pickup complete) — the big one:
```json
{
"orderstatus": "Picked up",
"pickupedtime": "2026-07-18 10:07:00",
"pickuptime": "2026-07-18 10:07:00",
"pickuplocationid": 9,
"pickuplat": "12.9716", "pickuplong": "77.5946",
"riderkms": "0.4200", // distance rider travelled to this stop
"ridercharges": 2.10, // payout for this leg
"ridertime": 12, // minutes
"pickupamt": 0.0,
"collectionamt": 100.0, // amount due
"collectedamt": 100.0, // amount actually collected
"collectionstatus": "collected",
"smspickup": 0,
"wasskipped": false, // true if this stop had been skipped earlier
"bonuspts": 5,
"dropimage": "<base64-or-url>" // photo proof of pickup
}
```
**`skipped`** — rider skips this stop, will resume later (first-class; sequence preserved).
**`cancelled`** — pickup could not be completed (reason in `notes`).
**`rejected`** — rider rejects the assigned stop.
**`picked`** — internal "picked" marker (via `updatepickup` v1). Please clarify vs `Picked up`.
> **⚠️ Please normalize `orderstatus` values.** Today they are inconsistent (`"Picked up"` with a space & capital, vs `"arrived"`, `"active"`, `"skipped"` lowercase). **Give us one canonical set of machine values** (e.g. all lowercase snake: `assigned`, `active`, `arrived`, `picked_up`, `delivered`, `skipped`, `cancelled`, `rejected`) and we'll map the UI labels ourselves.
Response for all updates: standard envelope with `status: true`.
### 4.5 ⭐ REQUIRED CHANGE — Stop `type` (Pickup vs Delivery) **[CHANGE]**
**This is the single most important change for go-live.** Each stop on a route can be a **pickup** or a **delivery**, and the app UI must branch on it. Today the API returns no such field, so the app treats **every stop as a pickup**. Please add:
1. **On every stop object** (§4.1–4.3) add a stop type field:
```json
"type": "pickup" // "pickup" | "delivery"
```
(Name it `type` or `stoptype` — the app already looks for `type`/`stoptype`/`stopType`; **pick one and tell us**.)
2. **For `delivery` stops**, the stop object must carry delivery details:
| Field | Type | Meaning |
|---|---|---|
| `dropaddress` | string | Where to deliver |
| `droplat` / `droplon` | string | Delivery coordinates (geofence + nav) |
| `otp` | string/int | Delivery OTP the customer gives (proof of delivery) |
| `collectionamt` | number | COD to collect on delivery (0 = prepaid) |
| customer name/phone | string | Recipient contact (reuse `pickupcustomer`/`pickupcontactno` or give delivery-specific fields — **tell us which**) |
3. **Delivery completion** goes through the same `PUT /v1/pickups/updatepickup` with:
```json
{
"pickupid": 8891,
"orderstatus": "delivered", // canonical value TBD (see §4.4 note)
"deliveredtime": "2026-07-18 10:20:00",
"otp": "4821", // OTP the rider entered — verify server-side
"dropimage": "<base64-or-url>", // photo proof of delivery
"collectedamt": 0.0, "collectionstatus": "prepaid",
"riderslat": "…", "riderslon": "…", "riderkms": "…", "bonuspts": 5
}
```
Please confirm: (a) whether OTP is **verified server-side** (recommended) or just recorded, (b) the canonical `orderstatus` for a completed delivery, (c) delivery-specific proof fields (signature? photo? OTP-only?).
### 4.6 Stop status lifecycle (state machine)
```
assigned ──► active ──► arrived ──► ┌─ (pickup) picked_up ─┐
│ │ │ └─ (delivery) delivered ─┤──► [next stop]
│ │ └────────────► skipped ──► (resume later) ─► active
└────────────┴──────────────────────► cancelled / rejected
```
Rules the backend must enforce/allow:
- Rider **cannot reorder**; `step` is authoritative.
- Rider **can skip** any stop and resume it later — `skipped` is not terminal.
- After the **last** stop the rider returns to the **origin hub**. Please tell us whether hub-return is its own record/status or implicit.
### 4.7 Create Pickup Log **[LIVE]**
`POST /v2/pickups/createpickuplog` — audit/event log entries for a stop. Body is wrapped in an **array**: `[ { ...event } ]`. Please document the event schema (event type, timestamp, pickupid, lat/long).
---
## 5. Earnings / Summary
### 5.1 Partner summary **[LIVE]**
`GET /v2/partners/...` (base `https://jupiter.doormile.app/live/api/v2/partners`)
Powers the Earnings screen (daily/weekly/monthly totals, stop counts, payout). **Please document the exact path + params (userid, period, date range) and the response fields** (totals, per-day breakdown).
### 5.2 Rider weekly KMs **[LIVE]**
`GET /v1/partners/...` — weekly distance for payout. Document exact path + response.
---
## 6. Supporting endpoints
These are used by the app; **please document each** (request + response):
- **Notifications** — list rider notifications (used on the notifications tab). Need: list + mark-read.
- **Rewards / bonus points** — the app shows `bonuspts`/`bonusPoints`; document how points are earned and fetched.
- **Support tickets** — create ticket, list tickets (models exist: subject, description, status, timestamps).
- **FCM push** — confirm the payload schema for push notifications (new stop assigned, route updated, etc.) so we can handle taps/deep-links.
- **App version / force-update** — the app tracks `CurrentVersion`; if you gate minimum version, document the endpoint.
---
## 7. Field glossary (canonical names)
| Field | Meaning |
|---|---|
| `userid` | Rider's user id (primary key the app sends everywhere) |
| `riderid` / `partnerid` | Rider/partner identifiers |
| `shiftid` / `logid` | Shift and current duty-log ids |
| `tenantid` / `locationid` / `applocationid` | Org / hub / app-location scoping |
| `pickupid` | **Stop id** — the PK for a single stop, used on all status updates |
| `orderid` / `orderheaderid` | Order + order-header identifiers |
| `step` | Stop's fixed position in the route (1..N) |
| `type` / `stoptype` | **NEW:** `pickup` \| `delivery` |
| `orderstatus` | Stop state (see §4.6) |
| `collectionamt` / `collectedamt` / `collectionstatus` | COD due / collected / status |
| `pickuplat`,`pickuplong` / `droplat`,`droplon` | Stop / drop coordinates |
| `riderslat`,`riderslon` | Rider GPS at time of action |
| `riderkms` / `ridercharges` / `ridertime` | Distance / payout / minutes for a leg |
| `otp` | Delivery OTP (proof of delivery) |
| `dropimage` | Photo proof (pickup or delivery) |
| `bonuspts` | Bonus points for completing a stop |
| `pickupradius` | Geofence radius (m) for arrived/complete |
---
## 8. Open questions for the backend team (please answer in your returned doc)
1. **Auth model** — token-based or `userid`-only? (§1.2)
2. **Single host + valid TLS + working DNS** — can we drop the `queue.workolik.com` host and the SSL/IP hack? (§1.1)
3. **Canonical `orderstatus` values** — give us the final machine strings. (§4.4)
4. **Stop `type` field** — final field name (`type` vs `stoptype`) and the delivery fields. (§4.5)
5. **Delivery proof** — OTP verified server-side? photo/signature required? canonical `delivered` status? (§4.5)
6. **getpickups v1/v2/v3** — can we consolidate to one endpoint? (§4.3)
7. **Casing** — confirm all-lowercase field names across every endpoint. (§4.1)
8. **Timezone** — confirm IST for all timestamps. (§1.4)
9. **Hub return** — is returning to hub its own status/record? (§4.6)
10. **Summary, rewards, notifications, support, push** — full request/response schemas. (§5–6)
---
*Generated from the current Miler app's live API integration. Every field marked [LIVE] is already sent/consumed by the app in production code — please preserve those names or tell us the new ones so we can migrate.*

32
analysis_options.yaml Normal file
View File

@@ -0,0 +1,32 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
analyzer:
errors:
file_names: ignore
unused_field: ignore
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore

124
android/app/build.gradle Normal file
View File

@@ -0,0 +1,124 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "dev.flutter.flutter-gradle-plugin"
id "com.google.gms.google-services"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader("UTF-8") { reader ->
localProperties.load(reader)
}
}
def flutterVersionCode = localProperties.getProperty("flutter.versionCode") ?: "1"
def flutterVersionName = localProperties.getProperty("flutter.versionName") ?: "1.0"
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
// No maps key is read here. The Google Maps SDK is gone: tiles come from
// OpenStreetMap and routes from OSRM, both keyless, drawn by flutter_map inside
// the Flutter view. The key this block used to inject was revoked, which is why
// the map rendered as a blank grey grid.
android {
namespace = "com.doormile.partner"
compileSdk = 36
// Correct NDK version
ndkVersion "27.0.12077973"
packagingOptions {
jniLibs {
useLegacyPackaging = true
}
// ❌ removed doNotStrip (this was breaking your AAB build)
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled true
}
kotlinOptions {
jvmTarget = "1.8"
}
defaultConfig {
applicationId = "com.doormile.partner"
minSdkVersion flutter.minSdkVersion
targetSdkVersion 36
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
manifestPlaceholders = [
applicationName: "android.app.Application"
]
// Build only ARM64
ndk {
abiFilters "arm64-v8a"
}
}
signingConfigs {
release {
keyAlias keystoreProperties["keyAlias"]
keyPassword keystoreProperties["keyPassword"]
storeFile keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null
storePassword keystoreProperties["storePassword"]
}
}
buildTypes {
debug {
minifyEnabled false
debuggable true
}
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
// ❌ removed ndk { debugSymbolLevel } (this caused strip crash)
}
}
}
dependencies {
implementation "androidx.core:core:1.10.0"
implementation "com.google.android.gms:play-services-location:21.0.1"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5"
}
flutter {
source = "../.."
}
tasks.register("copyNotificationSounds", Copy) {
def audioDir = file("$projectDir/../../assets/audio")
from(audioDir)
include("*.mp3")
into("$projectDir/src/main/res/raw")
rename { String fileName ->
fileName.replaceAll(/[^A-Za-z0-9_.]/, "_").toLowerCase()
}
}
tasks.named("preBuild").configure {
dependsOn("copyNotificationSounds")
}

View File

@@ -0,0 +1,163 @@
{
"project_info": {
"project_number": "140444764229",
"firebase_url": "https://doormile-gear-default-rtdb.firebaseio.com",
"project_id": "doormile-gear",
"storage_bucket": "doormile-gear.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:140444764229:android:a584c5e9127c3789283b2c",
"android_client_info": {
"package_name": "com.doormile.admin"
}
},
"oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.doormile.gear"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:140444764229:android:a99eaae950fa5ada283b2c",
"android_client_info": {
"package_name": "com.doormile.bond"
}
},
"oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.doormile.gear"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:140444764229:android:88df9627e2990ef4283b2c",
"android_client_info": {
"package_name": "com.doormile.gear"
}
},
"oauth_client": [
{
"client_id": "140444764229-5cllr5f8u28psf8ttc220f6h487u9vjq.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.doormile.gear",
"certificate_hash": "1d8115901ded2af41250ffbfe99b9239a1953c05"
}
},
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.doormile.gear"
}
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:140444764229:android:578383f5a1d3a05c283b2c",
"android_client_info": {
"package_name": "com.doormile.partner"
}
},
"oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyBkzz2Yua74Q9YpzGmUPFP94fmJQqNMIiU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "140444764229-seu4nkl2k2hj6gebk3q20sv80k685ecp.apps.googleusercontent.com",
"client_type": 3
},
{
"client_id": "140444764229-m6l2v6eofrb9jgrno3qmsrjbtd4iccvd.apps.googleusercontent.com",
"client_type": 2,
"ios_info": {
"bundle_id": "com.doormile.gear"
}
}
]
}
}
}
],
"configuration_version": "1"
}

Binary file not shown.

89
android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,89 @@
##########################################
## ✅ FLUTTER
##########################################
-keep class io.flutter.** { *; }
-dontwarn io.flutter.**
##########################################
## ✅ FIREBASE (Safe)
##########################################
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
-dontwarn com.google.firebase.**
-dontwarn com.google.android.gms.**
##########################################
## ✅ FCM Background Messaging
##########################################
-keep class com.google.firebase.messaging.FirebaseMessagingService { *; }
-keep class com.google.firebase.messaging.FirebaseMessaging { *; }
-keep class com.google.firebase.messaging.RemoteMessage { *; }
##########################################
## ✅ Gson / JSON Models (Correct way)
##########################################
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# Keep all model classes to prevent crashes
-keep class **.model.** { *; }
-keep class **.models.** { *; }
##########################################
## ✅ Retrofit / OkHttp
##########################################
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn retrofit2.**
-keep class okhttp3.** { *; }
-keep class okio.** { *; }
-keep interface retrofit2.** { *; }
##########################################
## ✅ Glide / image loading (avoid BigPicture FCM crash)
##########################################
-dontwarn com.bumptech.glide.**
-keep class com.bumptech.glide.** { *; }
##########################################
## ✅ Prevent crashes for reflection
##########################################
-keepattributes Signature
-keepattributes Annotation
##########################################
## ✅ KEEP ENUMS (Firebase uses it)
##########################################
-keepclassmembers enum * { *; }
##########################################
## ✅ Do NOT break Kotlin (Important)
##########################################
-keep class kotlin.** { *; }
-dontwarn kotlin.**
##########################################
## ✅ GETX (State Management & Snackbars)
##########################################
# Keep all GetX classes and methods
-keep class get.** { *; }
-keep class * extends get.GetxController { *; }
-keepclassmembers class * extends get.GetxController {
<methods>;
<fields>;
}
# Keep GetX navigation and routing
-keep class get.Get { *; }
-keep class get.GetMaterialApp { *; }
-keep class get.GetNavigator { *; }
-keep class get.GetSnackbar { *; }
# Keep GetX observables and reactive variables
-keep class get.Rx* { *; }
-keep class get.Obs* { *; }
-dontwarn get.**
##########################################
## ✅ Reduce unnecessary warnings
##########################################
-ignorewarnings

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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"/>
</manifest>

View File

@@ -0,0 +1,164 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ===== Permissions ===== -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<!-- For older Android versions -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.PICTURE_IN_PICTURE" />
<!-- Android 13+ notifications -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- Foreground service for background logging -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Optional: declare types for Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<!-- Location permissions -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Optional: needed for background tracking (break auto-refresh, etc.) -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- Phone call (for tap-to-call customer / pickup) -->
<uses-permission android:name="android.permission.CALL_PHONE" />
<!-- Alarm permissions for shift end (works even when app is killed) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- ===== Application ===== -->
<application
android:label="Doormile Rider"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true"> <!-- allows http if any -->
<!-- No maps key. The Google Maps SDK is no longer used: tiles come
from OpenStreetMap and routes from OSRM, both keyless, drawn by
flutter_map inside the Flutter view. The key that used to be
declared here was revoked, which is why the map was blank. -->
<!-- Firebase notification channel -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="Doormile" />
<!-- Foreground task notification channel (for flutter_foreground_task) -->
<meta-data
android:name="com.pravera.flutter_foreground_task.notification_channel_name"
android:value="Background Service" />
<meta-data
android:name="com.pravera.flutter_foreground_task.notification_channel_description"
android:value="Keeps Doormile online updates running in background." />
<!-- Foreground service component for flutter_foreground_task -->
<!-- Using dataSync only - location is accessed separately and doesn't need to be part of foreground service type -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync|location" />
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Theme shown before Flutter UI loads -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Required for Flutter plugin registration -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Shift End BroadcastReceiver (works even when app is killed) -->
<receiver
android:name=".ShiftEndReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.doormile.partner.SHIFT_END_ALARM" />
</intent-filter>
</receiver>
<!-- Optional: Legacy HTTP support (useful for older plugins) -->
<uses-library
android:name="org.apache.http.legacy"
android:required="false" />
</application>
<!-- ===== Package visibility (Android 11+) ===== -->
<queries>
<!-- Existing text processing visibility -->
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<!-- Allow dialer / phone apps for tel: links (Android 11+) -->
<intent>
<action android:name="android.intent.action.DIAL" />
<data android:scheme="tel" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<intent>
<action android:name="android.intent.action.CALL" />
<data android:scheme="tel" />
</intent>
<!-- Allow launching turn-by-turn navigation / maps for "Start pickup"
(Android 11+ requires declaring these to resolve the intents). -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="google.navigation" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<package android:name="com.google.android.apps.maps" />
<!-- Package visibility for common dialer apps -->
<package android:name="com.android.dialer" />
<package android:name="com.google.android.dialer" />
<package android:name="com.samsung.android.dialer" />
<package android:name="com.miui.dialer" />
<package android:name="com.huawei.dialer" />
<package android:name="com.oneplus.dialer" />
<package android:name="com.coloros.dialer" />
<package android:name="com.oppo.dialer" />
<package android:name="com.vivo.dialer" />
<package android:name="com.realme.dialer" />
</queries>
</manifest>

View File

@@ -0,0 +1,315 @@
package com.doormile.partner
import android.app.AlarmManager
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.Rational
import androidx.core.view.WindowCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import java.util.Calendar
class MainActivity : FlutterActivity() {
private val channelName = "doormile/pip"
private val shiftEndChannelName = "doormile/shift_end"
// PiP tracking for safe exit on resume
private val PREFS = "app_prefs"
private val KEY_WAS_IN_PIP = "was_in_pip"
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// No automatic PiP entry here – only from explicit Flutter calls
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// Enable edge-to-edge for Android 15+ (SDK 35+)
if (Build.VERSION.SDK_INT >= 35) {
WindowCompat.setDecorFitsSystemWindows(window, false)
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"enterPip" -> {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val aspect = Rational(16, 9)
val params = PictureInPictureParams.Builder()
.setAspectRatio(aspect)
.build()
enterPictureInPictureMode(params)
} else {
@Suppress("DEPRECATION")
enterPictureInPictureMode()
}
result.success(true)
} catch (e: Exception) {
result.error("PIP_ERROR", e.message, null)
}
}
"moveToBack" -> {
try {
moveTaskToBack(true)
result.success(true)
} catch (e: Exception) {
result.error("MOVE_BACK_ERROR", e.message, null)
}
}
"hasActivePickups" -> {
try {
val hasActive = checkActivePickups()
result.success(hasActive)
} catch (e: Exception) {
result.error("CHECK_ACTIVE_ERROR", e.message, null)
}
}
"ensureExitPip" -> {
try {
ensureExitPip()
result.success(true)
} catch (e: Exception) {
result.error("ENSURE_EXIT_PIP_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
// Shift end alarm channel
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, shiftEndChannelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"scheduleShiftEndAlarm" -> {
try {
val endTimeStr = call.argument<String>("endTime") ?: ""
val startTimeStr = call.argument<String>("startTime") ?: ""
if (endTimeStr.isNotEmpty()) {
scheduleShiftEndAlarm(endTimeStr, startTimeStr)
result.success(true)
} else {
result.error("INVALID_TIME", "End time is required", null)
}
} catch (e: Exception) {
result.error("ALARM_ERROR", e.message, null)
}
}
"cancelShiftEndAlarm" -> {
try {
cancelShiftEndAlarm()
result.success(true)
} catch (e: Exception) {
result.error("CANCEL_ERROR", e.message, null)
}
}
"handleShiftEnd" -> {
try {
// This is called by the BroadcastReceiver
// We need to call Flutter code to create break log
// Since we're in MainActivity, we can use the existing Flutter engine
// But the receiver might not have access to it, so we'll handle it differently
// The receiver will directly call the background service method
result.success(true)
} catch (e: Exception) {
result.error("HANDLE_ERROR", e.message, null)
}
}
else -> result.notImplemented()
}
}
}
// Let Flutter-side explicitly control when to enter PiP (navigation only)
override fun onUserLeaveHint() {
super.onUserLeaveHint()
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration?
) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
// Persist PiP state so we can safely exit it on resume
val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
prefs.edit().putBoolean(KEY_WAS_IN_PIP, isInPictureInPictureMode).apply()
}
override fun onResume() {
super.onResume()
// When user brings app back, force full-screen (no PiP)
ensureExitPip()
}
private fun ensureExitPip() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val prefs = getSharedPreferences(PREFS, Context.MODE_PRIVATE)
val wasInPip = prefs.getBoolean(KEY_WAS_IN_PIP, false)
// Clear the persisted flag so we don't auto-re-enter PiP
// Note: Android doesn't provide a direct API to exit PiP programmatically,
// but when user opens app from launcher, it naturally returns to full-screen.
// We just ensure we don't auto-re-enter PiP on resume.
if (wasInPip) {
prefs.edit().putBoolean(KEY_WAS_IN_PIP, false).apply()
}
}
private fun checkActivePickups(): Boolean {
return try {
val prefs: SharedPreferences = getSharedPreferences(
"FlutterSharedPreferences",
Context.MODE_PRIVATE
)
// Only check has_live_Pick up (verified by API)
val hasLive = prefs.getBoolean("flutter.has_live_Pick up", false)
hasLive
} catch (e: Exception) {
false
}
}
private fun scheduleShiftEndAlarm(endTimeStr: String, startTimeStr: String) {
try {
Log.d("MainActivity", "📅 Scheduling shift end alarm - endTime: $endTimeStr, startTime: $startTimeStr")
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, ShiftEndReceiver::class.java).apply {
action = "com.doormile.partner.SHIFT_END_ALARM"
}
val pendingIntent = PendingIntent.getBroadcast(
this,
1001, // Unique request code
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Parse end time (format: HH:mm:ss or HH:mm)
val timeParts = endTimeStr.split(":")
if (timeParts.size < 2) {
Log.e("MainActivity", "❌ Invalid end time format: $endTimeStr")
return
}
val endHour = timeParts[0].toIntOrNull() ?: 0
val endMinute = timeParts[1].toIntOrNull() ?: 0
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, endHour)
set(Calendar.MINUTE, endMinute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
// Handle overnight shifts (if start > end)
if (startTimeStr.isNotEmpty()) {
val startParts = startTimeStr.split(":")
if (startParts.size >= 2) {
val startHour = startParts[0].toIntOrNull() ?: 0
val startMinute = startParts[1].toIntOrNull() ?: 0
if (startHour > endHour || (startHour == endHour && startMinute > endMinute)) {
// Overnight shift - if current time is before end time, schedule for tomorrow
val now = Calendar.getInstance()
if (now.before(calendar)) {
calendar.add(Calendar.DAY_OF_MONTH, 1)
}
}
}
}
// ✅ CRITICAL: If shift end time has already passed today, trigger immediately
val now = Calendar.getInstance()
Log.d("MainActivity", "Current time: ${now.time}, Shift end time: ${calendar.time}")
if (calendar.before(now) || calendar.equals(now)) {
// Shift end time has passed - trigger break log immediately
Log.d("MainActivity", "⚡ Shift end time ($endTimeStr) has already passed - triggering break log immediately")
// Trigger the receiver immediately
val immediateIntent = Intent(this, ShiftEndReceiver::class.java).apply {
action = "com.doormile.partner.SHIFT_END_ALARM"
}
sendBroadcast(immediateIntent)
Log.d("MainActivity", "✅ Sent immediate broadcast to ShiftEndReceiver")
// Also schedule for tomorrow to handle next shift
calendar.add(Calendar.DAY_OF_MONTH, 1)
Log.d("MainActivity", "📅 Also scheduled shift end alarm for tomorrow: ${calendar.time}")
}
// Schedule exact alarm (Android 12+ requires SCHEDULE_EXACT_ALARM permission)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d("MainActivity", "✅ Scheduled exact shift end alarm for: ${calendar.time} (${calendar.timeInMillis})")
} else {
// Fallback to inexact alarm
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.w("MainActivity", "⚠️ Scheduled inexact shift end alarm (no permission) for: ${calendar.time}")
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d("MainActivity", "✅ Scheduled exact shift end alarm (Android M+) for: ${calendar.time}")
} else {
@Suppress("DEPRECATION")
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d("MainActivity", "✅ Scheduled exact shift end alarm (legacy) for: ${calendar.time}")
}
}
} catch (e: Exception) {
Log.e("MainActivity", "❌ Error scheduling shift end alarm: ${e.message}", e)
e.printStackTrace()
}
}
private fun cancelShiftEndAlarm() {
try {
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(this, ShiftEndReceiver::class.java).apply {
action = "com.doormile.partner.SHIFT_END_ALARM"
}
val pendingIntent = PendingIntent.getBroadcast(
this,
1001,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
Log.d("MainActivity", "Cancelled shift end alarm")
} catch (e: Exception) {
Log.e("MainActivity", "Error cancelling shift end alarm: ${e.message}", e)
}
}
}

View File

@@ -0,0 +1,278 @@
package com.doormile.partner
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.PowerManager
import android.util.Log
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.*
/**
* BroadcastReceiver that triggers when shift end alarm fires
* This works even when app is killed
*
* When app is killed, we directly call the API to create break log
* since we can't reliably start Flutter engine
*/
class ShiftEndReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("ShiftEndReceiver", "🔔 Shift end alarm triggered at ${java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.US).format(java.util.Calendar.getInstance().time)}")
// ✅ CRITICAL: Acquire WakeLock to keep device awake during API call
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ShiftEndReceiver::WakeLock")
wakeLock.acquire(60 * 1000L) // Hold for 60 seconds max
// Execute API call in background thread
Thread {
try {
// Check if rider is still on duty
val prefs: SharedPreferences = context.getSharedPreferences(
"FlutterSharedPreferences",
Context.MODE_PRIVATE
)
// ✅ Try multiple key formats (Flutter SharedPreferences uses "flutter." prefix)
val onduty = prefs.getInt("flutter.onduty", 0)
Log.d("ShiftEndReceiver", "Checking onduty: $onduty")
if (onduty != 1) {
Log.d("ShiftEndReceiver", "Rider already offline (onduty=$onduty), skipping break log creation")
return@Thread
}
// Get required IDs - try both flutter.userid and flutter.userId
val userid = prefs.getInt("flutter.userid", 0).takeIf { it != 0 }
?: prefs.getInt("flutter.userId", 0)
val partnerid = prefs.getInt("flutter.partnerid", 0).takeIf { it != 0 }
?: prefs.getInt("flutter.partnerId", 0)
val shiftid = prefs.getInt("flutter.shiftid", 0).takeIf { it != 0 }
?: prefs.getInt("flutter.shiftId", 0)
val logid = prefs.getInt("flutter.logid", 0).takeIf { it != 0 }
?: prefs.getInt("flutter.logId", 0)
Log.d("ShiftEndReceiver", "Retrieved IDs: userid=$userid, partnerid=$partnerid, shiftid=$shiftid, logid=$logid")
if (userid == 0) {
Log.e("ShiftEndReceiver", "❌ Missing userid, cannot create break log. Available keys: ${prefs.all.keys}")
return@Thread
}
// Get API base URL (check if live or dev)
// ✅ Match homepage logic: use createbreaklog (not createbreakriderlog)
val mainRoute = prefs.getString("flutter.mainRoute", "dev") ?: "dev"
val isLive = mainRoute == "live"
Log.d("ShiftEndReceiver", "API Route: $mainRoute (isLive=$isLive)")
val baseUrl = if (isLive) {
"https://jupiter.doormile.app/live/api/v2/partners/createbreaklog"
} else {
"https://jupiter.doormile.app/dev/api/v2/partners/createbreaklog"
}
// Create break log payload
val now = Calendar.getInstance()
val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.US)
val breakdate = dateFormat.format(now.time)
val breakstart = timeFormat.format(now.time)
val localBreakId = (System.currentTimeMillis() % 900).toInt() + 100
// ✅ Compact JSON (no extra whitespace) - matches Flutter format
val payload = """{"breakid":$localBreakId,"logid":$logid,"breakdate":"$breakdate","userid":$userid,"partnerid":$partnerid,"shiftid":$shiftid,"breakstart":"$breakstart","breakend":"","breakhours":0.0,"latitude":"0","longitude":"0"}"""
Log.d("ShiftEndReceiver", "📤 Sending break log request to: $baseUrl")
Log.d("ShiftEndReceiver", "📦 Payload: $payload")
// Make API call
val url = URL(baseUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "POST"
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("Accept", "application/json")
connection.doOutput = true
connection.doInput = true
connection.useCaches = false
connection.connectTimeout = 15000 // Increased timeout
connection.readTimeout = 15000
Log.d("ShiftEndReceiver", "🔌 Connecting to API...")
// Write payload
val outputStream = connection.outputStream
val writer = OutputStreamWriter(outputStream, "UTF-8")
writer.write(payload)
writer.flush()
writer.close()
Log.d("ShiftEndReceiver", "📨 Request sent, waiting for response...")
val responseCode = connection.responseCode
Log.d("ShiftEndReceiver", "📥 Break log API response: HTTP $responseCode")
// Read response body for debugging
try {
val responseStream = if (responseCode in 200..299) {
connection.inputStream
} else {
connection.errorStream
}
if (responseStream != null) {
val responseBody = responseStream.bufferedReader().use { it.readText() }
Log.d("ShiftEndReceiver", "📄 Response body: $responseBody")
}
} catch (e: Exception) {
Log.w("ShiftEndReceiver", "Could not read response body: ${e.message}")
}
if (responseCode in 200..299) {
Log.d("ShiftEndReceiver", "✅ Break log created successfully (HTTP $responseCode)")
// Set offline locally
prefs.edit()
.putInt("flutter.onduty", 0)
.putBoolean("flutter.online", false)
.apply()
// Also update rider log to set onduty=0
val updateUrl = if (isLive) {
"https://jupiter.doormile.app/live/api/v2/partners/updateriderlog"
} else {
"https://jupiter.doormile.app/dev/api/v2/partners/updateriderlog"
}
// ✅ Compact JSON (no extra whitespace)
val updatePayload = """{"userid":$userid,"onduty":0,"latitude":"0","longitude":"0"}"""
Log.d("ShiftEndReceiver", "📤 Updating rider status to offline")
try {
val updateConnection = URL(updateUrl).openConnection() as HttpURLConnection
updateConnection.requestMethod = "POST"
updateConnection.setRequestProperty("Content-Type", "application/json")
updateConnection.doOutput = true
updateConnection.connectTimeout = 5000
updateConnection.readTimeout = 5000
val updateWriter = OutputStreamWriter(updateConnection.outputStream, "UTF-8")
updateWriter.write(updatePayload)
updateWriter.flush()
updateWriter.close()
if (updateConnection.responseCode in 200..299) {
Log.d("ShiftEndReceiver", "✅ Rider status updated to Offline")
}
updateConnection.disconnect()
} catch (e: Exception) {
Log.e("ShiftEndReceiver", "Error updating rider status: ${e.message}")
}
} else {
// Read error response for debugging
try {
val errorStream = connection.errorStream
if (errorStream != null) {
val errorResponse = errorStream.bufferedReader().use { it.readText() }
Log.e("ShiftEndReceiver", "❌ Failed to create break log: HTTP $responseCode\nError: $errorResponse")
} else {
Log.e("ShiftEndReceiver", "❌ Failed to create break log: HTTP $responseCode")
}
} catch (e: Exception) {
Log.e("ShiftEndReceiver", "❌ Failed to create break log: HTTP $responseCode (Error reading response: ${e.message})")
}
}
connection.disconnect()
// ✅ CRITICAL: Reschedule alarm for tomorrow (so it works every day automatically)
// This ensures the alarm fires every day at shift end time even if app is killed
try {
val endTimeStr = prefs.getString("flutter.endtime", "") ?: ""
val startTimeStr = prefs.getString("flutter.starttime", "") ?: ""
Log.d("ShiftEndReceiver", "Rescheduling alarm - endTime: $endTimeStr, startTime: $startTimeStr")
if (endTimeStr.isNotEmpty()) {
// Schedule alarm for tomorrow at the same time
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, ShiftEndReceiver::class.java).apply {
action = "com.doormile.partner.SHIFT_END_ALARM"
}
val pendingIntent = PendingIntent.getBroadcast(
context,
1001,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val timeParts = endTimeStr.split(":")
if (timeParts.size >= 2) {
val endHour = timeParts[0].toIntOrNull() ?: 0
val endMinute = timeParts[1].toIntOrNull() ?: 0
val calendar = Calendar.getInstance().apply {
add(Calendar.DAY_OF_MONTH, 1) // Tomorrow
set(Calendar.HOUR_OF_DAY, endHour)
set(Calendar.MINUTE, endMinute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d("ShiftEndReceiver", "✅ Rescheduled alarm for tomorrow: ${calendar.time}")
} else {
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
} else {
@Suppress("DEPRECATION")
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
Log.d("ShiftEndReceiver", "✅ Rescheduled alarm for tomorrow: ${calendar.time}")
}
}
}
} catch (e: Exception) {
Log.e("ShiftEndReceiver", "Error rescheduling alarm: ${e.message}", e)
}
} catch (e: Exception) {
Log.e("ShiftEndReceiver", "❌ Error handling shift end: ${e.message}", e)
e.printStackTrace()
} finally {
// ✅ CRITICAL: Release WakeLock in finally block
try {
if (wakeLock.isHeld) {
wakeLock.release()
Log.d("ShiftEndReceiver", "🔓 WakeLock released")
}
} catch (e: Exception) {
Log.e("ShiftEndReceiver", "Error releasing WakeLock: ${e.message}")
}
}
}.start()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,22 @@
<?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>
<item name="android:windowSplashScreenIconBackgroundColor">#FFFFFF</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>
</resources>

View File

@@ -0,0 +1,22 @@
<?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 -->
<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>
</style>
</resources>

View File

@@ -0,0 +1,22 @@
<?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 -->
<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:windowSplashScreenIconBackgroundColor">#FFFFFF</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>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/transparent</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 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"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip

4
android/key.properties Normal file
View File

@@ -0,0 +1,4 @@
storePassword=123456789
keyPassword=123456789
keyAlias= doormile
storeFile=doormilerider-keystore.jks

View File

@@ -0,0 +1,27 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
include(":app")

BIN
assets/audio/alert-1.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-10.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-2.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-3.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-4.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-5.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-6.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-7.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-8.mp3 Normal file

Binary file not shown.

BIN
assets/audio/alert-9.mp3 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/images/preloader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
assets/images/week.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More