Decision (2026-05-05)
Keep per-driver fare for Android. It's a premium UX iOS doesn't have — "this driver costs $11.20 because they're 2 miles away, that one costs $14.50 because they're 6 miles away." Don't drop it.
Add fallback for the cases where driver location isn't available — driver doesn't share location, OR rider doesn't have the key to decrypt it. In those cases, fall back to the rider-route-only fare (pickup→dropoff × per-mile + base). This is the iOS model; on Android it becomes the fallback when location-based pricing is unavailable.
What's already there
The fallback exists. RiderViewModel.kt:1758-1762:
```kotlin
val fareCalc: FareCalc = if (driverLocation != null) {
calculateRoadflareFare(pickup, driverLocation, rideRoute)
} else {
FareCalc(state.fareEstimate ?: return, state.fareEstimateUsd)
}
```
And calculateFare(route) at L4282 is exactly the iOS-style model: `distanceMiles * farePerMile + minimumFare floor`. So state.fareEstimate already holds the iOS-style rider-route fare — Android just upgrades it to per-driver pricing when location is available.
The same if (driverLocation != null) pattern repeats in sendRoadflareOfferWithAlternatePayment (~L1850) and sendRoadflareToAll (~L1945). All three RoadFlare offer entry points already have the fallback.
What's actually missing
Today the fallback only fires when the CALLER passes driverLocation = null — which happens when the driver isn't sharing location (Kind 30014 absent or expired). It does NOT fire when the driver IS sharing location but the rider can't decrypt it (stale key or missing key). In that case today the rider sees the driver as offline entirely and never reaches the offer flow.
The actual work for this issue:
Relationship to #82
Mostly unblocked. The fare fallback already exists and matches the iOS model, so #82's stale-key offer flow can use it directly. The wiring change above is small enough to fold into #82 if convenient, OR ship as a tiny separate PR before #82.
Out of scope
- Changing
calculateRoadflareFare's formula. It's the per-driver premium fare; keep it as is.
- Changing iOS or roadflare-rider behavior. They use the rider-route model unconditionally — that's their canonical path and matches Android's fallback exactly.
Decision (2026-05-05)
Keep per-driver fare for Android. It's a premium UX iOS doesn't have — "this driver costs $11.20 because they're 2 miles away, that one costs $14.50 because they're 6 miles away." Don't drop it.
Add fallback for the cases where driver location isn't available — driver doesn't share location, OR rider doesn't have the key to decrypt it. In those cases, fall back to the rider-route-only fare (pickup→dropoff × per-mile + base). This is the iOS model; on Android it becomes the fallback when location-based pricing is unavailable.
What's already there
The fallback exists.
RiderViewModel.kt:1758-1762:```kotlin
val fareCalc: FareCalc = if (driverLocation != null) {
calculateRoadflareFare(pickup, driverLocation, rideRoute)
} else {
FareCalc(state.fareEstimate ?: return, state.fareEstimateUsd)
}
```
And
calculateFare(route)at L4282 is exactly the iOS-style model: `distanceMiles * farePerMile + minimumFare floor`. Sostate.fareEstimatealready holds the iOS-style rider-route fare — Android just upgrades it to per-driver pricing when location is available.The same
if (driverLocation != null)pattern repeats insendRoadflareOfferWithAlternatePayment(~L1850) andsendRoadflareToAll(~L1945). All three RoadFlare offer entry points already have the fallback.What's actually missing
Today the fallback only fires when the CALLER passes
driverLocation = null— which happens when the driver isn't sharing location (Kind 30014 absent or expired). It does NOT fire when the driver IS sharing location but the rider can't decrypt it (stale key or missing key). In that case today the rider sees the driver as offline entirely and never reaches the offer flow.The actual work for this issue:
LOCATION_STALEstate) and passdriverLocation = nullwhen callingsendRoadflareOffer.driverLocation = null→ fallback fare is used → offer carriesstate.fareEstimateand matchingstate.fareEstimateUsdon the wire.Relationship to #82
Mostly unblocked. The fare fallback already exists and matches the iOS model, so #82's stale-key offer flow can use it directly. The wiring change above is small enough to fold into #82 if convenient, OR ship as a tiny separate PR before #82.
Out of scope
calculateRoadflareFare's formula. It's the per-driver premium fare; keep it as is.