Skip to content

fix(rider-app): wire per-driver-fare fallback to also cover stale/missing key case #83

@variablefate

Description

@variablefate

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:

  • When the rider has a stale-or-missing RoadFlare key for a driver who is broadcasting Kind 30014, surface that driver in the rider's UI as available (per refactor(roadflare): decouple ride-on-demand from key state + actually-filter mute #82's LOCATION_STALE state) and pass driverLocation = null when calling sendRoadflareOffer.
  • Confirm the fallback path produces a sensible UI quote (we want the rider to see the iOS-style fare, not a blank/unknown).
  • Add a small inline UI hint when the fallback fare is shown — e.g., "estimated (driver location unavailable)" — so the rider isn't surprised if the actual ride length differs from the per-driver fare they would have seen with a fresh key.
  • Pin behavior with a regression test: a stale-key driver in the offer flow produces driverLocation = null → fallback fare is used → offer carries state.fareEstimate and matching state.fareEstimateUsd on 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

  • 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions