# Drive (ride-hailing)

The Drive module adds a full **ride-hailing** flow: users request a ride as a **passenger** or switch to **driver mode** to accept nearby requests. The map uses **Mapbox**; route and fare are computed on the **server**, not on the client.

- **Three backends.** Firebase (Cloud Functions + triggers), Supabase (Edge Function + SQL), and REST API (endpoints documented in the patch README) expose the same contract.
- **No fixed roles.** Any signed-in user can request a ride. Users who complete driver onboarding get a `drivers/{uid}` profile and can go online.
- **Price = estimate.** The amount on screen comes from the backend (base fare + km + minute). The kit does **not** process payments. To charge for real, integrate Stripe or another gateway on your server.

> **Note:** the displayed price is an estimate only. Real payment requires a future integration (the kit already ships [Stripe web](https://kasy.dev/docs/funcionalidades/stripe) as a separate module).

## Enable / disable

Drive does not ship in Quick mode by default. To add it to an existing project:

```
kasy add drive
```

It prompts for the Mapbox token (`pk.…`) and price coefficients, writes `MAPBOX_ACCESS_TOKEN` to `.env`, seeds `DRIVE_*` on the server environment, and tries to store the token as a secret (Firebase/Supabase).

```
kasy remove drive
```

Removes the feature, routes, Mapbox dependency, and `withDrive` flag.

Configure later anytime with `kasy configure` ("Drive (Ride-hailing)" section) or `kasy configure drive`.

## Configuration

### Client (Flutter)

| Variable | Where | Purpose |
| --- | --- | --- |
| `MAPBOX_ACCESS_TOKEN` | `.env` / `--dart-define` | Map tiles in the app |

Get it at [account.mapbox.com](https://account.mapbox.com/access-tokens/). Use a public token (`pk.…`).

### Server (Firebase / Supabase)

| Variable | Where | Default |
| --- | --- | --- |
| `MAPBOX_ACCESS_TOKEN` | Secret | (required for routes) |
| `DRIVE_BASE_FARE` | `functions/.env` or Supabase secret | `5` |
| `DRIVE_PRICE_PER_KM` | same | `1.5` |
| `DRIVE_PRICE_PER_MIN` | same | `0.25` |
| `DRIVE_CURRENCY` | same | `USD` |
| `DRIVE_ALLOW_SELF_ACCEPT` | same | *(omitted)* |

`DRIVE_ALLOW_SELF_ACCEPT=true` is for **kit / dogfood only** (Firebase): the same account may request then accept its own ride. In production **do not set** this variable (or use any value other than `true`). Without it, the server rejects self-accept and also hides the driver's own request from the nearby list.

On **Supabase**, the equivalent is the SQL setting `app.drive_allow_self_accept = 'on'` (migrations `20240101000018`–`20`). Without it, `accept_ride` and `find_nearby_ride_requests` block/hide the caller's own request.

On Firebase, `kasy configure drive` writes coefficients to `functions/.env` and the token via `firebase functions:secrets:set MAPBOX_ACCESS_TOKEN`. On Supabase, the same values become `supabase secrets set`.

On **REST API**, put coefficients in your server `.env` and implement the endpoints described in the patch README.

## Test in debug

1. Run `kasy run` with `withDrive` on.
2. Open **Settings** and tap the **Drive** tile.
3. In the Drive side menu, turn on **Automated test** (only shown when `ENV` is not `prod`).
4. Single-account flow (simulated GPS, real backend):
   - Go online as driver (test GPS spawns near the Lima demo corridor).
   - Switch to passenger: home already has the corridor pickup and dropoff filled in.
   - Request a ride → app-bar **Back** (with test mode on the ride is **not** canceled; you jump to driver) → accept the request.
   - After accept, the car walks the Mapbox polyline to pickup (unlocks "Passenger on board") then to dropoff (unlocks "Complete").
   - The same Back works after accept / in progress: it opens the driver navigation for that ride instead of the cancel dialog.
5. Turn **Automated test** off when done. In production, Back again requires cancel, GPS is the device again, and the server blocks self-accept without `DRIVE_ALLOW_SELF_ACCEPT=true`.

Without the switch (or with `ENV=prod`), safe default behavior stays: real Geolocator GPS; Back on tracking opens the cancel dialog only while the ride is still cancelable (`requested` / `accepted`). After boarding, Back just leaves to Drive home and keeps the ride open.

### GPS in the browser

With **Automated test** on, Drive does **not** ask for browser location permission: position comes from the internal simulator. With the switch off, Drive asks for location on the **Settings → Drive** tap (user gesture; Chrome usually requires that). Without permission, the map stays on the placeholder until the user allows it. On web, if GPS is slow, the pin appears as soon as the browser yields a fix.

Want proximity tests **without** automated mode? DevTools → More tools → Sensors (or Android/Xcode emulator) and inject lat/lng near pickup/dropoff.

### Search radius (driver ↔ request)

An online driver only sees (and the create trigger only notifies for) requests within **5 km** of the pickup (`SEARCH_RADIUS_METERS = 5000` in `functions/src/drive/drive_functions.ts` and `triggers.ts`; on Supabase, the same value in SQL `find_nearby_ride_requests`). Want a different radius for your market? Change that constant on your backends and deploy. There is **no** environment variable for it yet.

Active-ride CTAs ("Passenger on board" / "Complete ride") require the driver within **100 m** of pickup or dropoff (app-side only, not re-checked on the server).

Without `MAPBOX_ACCESS_TOKEN`, the screens open but the map does not load tiles. Without a deployed backend, ride requests fail when calling the server.

## Where to look in the Firebase Console

Drive does **not** store driver or ride data under **Authentication**. That tab only shows the account (anonymous or email) and the **User UID**. Module data lives in **Firestore** on the same Firebase project the app uses (`firebase_options` / `google-services.json`).

| What you need | Where to look | Note |
| --- | --- | --- |
| Account that requested or accepted a ride | **Authentication → Users** | Copy the **User UID** |
| Driver registration (vehicle) | **Firestore → `drivers/{uid}`** | `{uid}` matches Authentication |
| Rides created | **Firestore → `rides/`** | Created by the `requestRide` callable (client is read-only) |
| Device push token | **Firestore → `users/{uid}/devices/`** | Not the driver's display name |

**Quick path:** open the correct project → **Authentication** → copy the test account UID → **Firestore Database** → collection **`drivers`** → document id = that UID (make, model, color, plate, `status`). Rides: collection **`rides`**.

**Automated test mode:** only GPS is simulated in the app. Request, accept, and status changes **always** hit the backend. If `rides/` is empty, check: Drive functions deployed, `MAPBOX_ACCESS_TOKEN` in secrets, and whether the app region in `drive_api.dart` matches deploy region (`functionsRegion` in `kit_setup.json`).

On **Supabase**, the equivalent is the `drive` schema from migrations (`drivers`, `rides`). Use **Table Editor** with the authenticated user's `uid`.

## UI flow

| Screen | Route (name) | Role |
| --- | --- | --- |
| Drive entry | `drive` | Intro + role choice on first open; then opens the saved role's home (passenger stays here) |
| Tracking | `driveRideTracking` | Passenger: live status |
| Driver onboarding | `driveDriverOnboarding` | First vehicle registration |
| Driver home | `driveDriverHome` | Online/offline, nearby-request overlay |
| Active ride (driver) | `driveDriverRide` | Navigate through completion |

The chosen role (passenger/driver) is saved; switching is done from Drive's side menu.

### Key flow rules

- **A driver can only go online with a registered vehicle.** The switch opens onboarding if it is missing.
- **Nearby rides are a server query** (`listNearbyRideRequests`), default radius **5 km** from the driver's published GPS to the pickup (not an open listener over every request).
- **Driver buttons unlock by GPS proximity:** "Picked up passenger" only enables within 100 m of the pickup point; "Complete ride" only within 100 m of the drop-off.
- **The passenger can only cancel while** the ride is `requested` or `accepted`; after boarding it is no longer possible.
- **While on a ride the driver receives no new requests**; declining a ride only hides it for that driver.
- **The server re-validates everything** on all 3 backends: accept only a `requested` ride (the first driver wins), start/complete only by the assigned driver and in order, cancel only before a terminal state.
- **Self-accept (same account) is blocked by default.** It only works with `DRIVE_ALLOW_SELF_ACCEPT=true` on the server **and** the **Automated test** switch in the menu (dev). Keep both off in a shipped app.

Push notifications fire when a ride is created or changes status (implementation varies by backend).

## Backend reference

| Backend | Implementation |
| --- | --- |
| Firebase | `functions/src/drive/drive_functions.ts` (callables) + `triggers.ts` |
| Supabase | Edge Function `request-ride` + migration `20240101000018_drive.sql` |
| REST API | `POST /drive/rides`, `POST /drive/rides/{id}/accept`, etc. |

After configuring secrets, deploy:

- **Firebase:** `kasy deploy`
- **Supabase:** `supabase db push` and `supabase functions deploy request-ride`

## Production

Minimum checklist:

1. Mapbox token in the app `.env` **and** as a server secret
2. `DRIVE_*` coefficients tuned for your region/currency
3. **No** `DRIVE_ALLOW_SELF_ACCEPT=true` (or remove the line)
4. **Automated test** off (hidden when `ENV=prod`)
5. Functions or migration deployed
6. Location permission tested on real iOS and Android devices
7. Payment strategy defined (Stripe or other) if you will charge

The kit does not include driver/platform payment split. That belongs in your backend when you add billing.

## Removing Drive

`kasy remove drive` turns off the module and removes generated code. Commit first if you customized Drive screens.
