# TODO

## Move live-tracking route rendering off the client's Google Maps key

**Why**

`EXPO_PUBLIC_GOOGLE_MAPS_API_KEY` ships inside the mobile app bundle, so it's
trivially extractable by anyone who pulls the APK apart. Most of the places
that used to expose it for a *billable* Google call have already been moved
server-side (reverse geocode, address autocomplete, place details, delivery
ETA — see `App\Http\Controllers\Api\V1\GeoController` and
`App\Services\GoogleMapsService`). Three call sites are still exposed:

- `components/pages/status/OrderTrackingMap.tsx`
- `components/pages/status/OrderRouteMap.tsx`
- `app/(protected)/(driver)/navigation/[id].tsx`

(all paths relative to `resto-app`, the mobile repo)

Each of these renders `<MapViewDirections origin=... destination=...
apikey={GOOGLE_MAPS_APIKEY}>` from `react-native-maps-directions`, which
calls `https://maps.googleapis.com/maps/api/directions/json` **directly from
the device** using the raw key. Anyone with the extracted key can run up our
Directions API bill with no rate limit on our side. This is the same class
of problem the four already-fixed call sites had.

The remaining `EXPO_PUBLIC_GOOGLE_MAPS_API_KEY` use in `app.config.ts`
(native iOS/Android Maps SDK config) is *not* part of this task — that one
inherently has to stay client-side; Google's model restricts abuse of it by
app identifier/SHA fingerprint in Cloud Console, not by keeping it secret.

**Why this one is harder than the others already fixed**

The four fixed endpoints (ETA, autocomplete, place details, reverse-geocode)
were places where *our own code* called `fetch()` against a Google URL —
swapping the URL for our own endpoint was the whole job. `MapViewDirections`
is a third-party component that does the `fetch()` internally; there's no
prop to redirect it. Fixing this means dropping the component and rendering
the route ourselves. It also sits on a *live* tracking screen — the
`driverLoc`/`driverLocation` prop updates continuously while a delivery is
in progress (websocket/polling), so a naive 1:1 proxy would just move today's
call volume onto our own key instead of reducing it. The design below exists
to bound that volume, not just relocate it.

**How to implement**

Backend (this repo):

1. Add `GoogleMapsService::getRoutePolyline(originLat, originLng, destLat,
   destLng): ?array` — reuse the existing private `fetchDirectionsRoute()`
   helper (already shared by `getDrivingDistanceKm()`/`getEta()`), decode
   `route['overview_polyline']['points']` (Google's encoded polyline format)
   server-side into a plain `[['lat' => ..., 'lng' => ...], ...]` array. PHP
   has no built-in decoder — either pull a small one in or port the ~20-line
   algorithm directly (it's a standard, short algorithm). Decoding
   server-side means the app never needs a polyline-decoding dependency,
   just consumes an array.
2. Add `GeoController::route()` → `GET /geo/route` (same
   `auth:sanctum` + `throttle:30,1` group as the other `/geo/*` endpoints in
   `routes/api.php`), validated params `origin_lat`, `origin_lng`,
   `destination_lat`, `destination_lng`, returning `{points, distance_km,
   duration}`.
3. Add a short-TTL cache in front of it (Laravel cache, key on both
   coordinates rounded to ~4 decimal places, TTL ~20-30s) so near-simultaneous
   requests for the same leg (e.g. the client's tracking screen and the
   driver's navigation screen both polling around the same time) don't each
   hit Google.
4. Feature test mirroring `tests/Feature/GeoControllerTest.php` — mock
   `GoogleMapsService`, assert auth is required, assert the encoded-polyline
   decode produces the right points for a known fixture string.

Mobile app (`resto-app`, separate repo — not part of this backend task, noted
here for context when this is picked up):

5. Add `fetchRoutePolyline(token, origin, destination)` in `helpers/fetch.ts`
   alongside `fetchEta`/`fetchReverseGeocode`, and a `useRoutePolyline` hook
   that **throttles**, not just fetches: only re-request when the moving
   point has moved past a distance threshold (~75m) *and* a minimum time has
   elapsed (~15s) since the last fetch, whichever is later.
6. In each of the three files, split what's currently one `MapViewDirections`
   call per leg into two concerns: the restaurant→destination leg is static
   per order (restaurant doesn't move, delivery address doesn't change) and
   should be fetched exactly once and cached in component state; only the
   driver-position leg needs the throttled hook.
7. Replace `<MapViewDirections .../>` with `<Polyline coordinates={points}
   strokeWidth={...} strokeColor={...} .../>` (built into `react-native-maps`,
   no new dependency) fed by the hook's data.
8. Remove `react-native-maps-directions` from `package.json` once all three
   call sites are converted and nothing else imports it, and drop
   `EXPO_PUBLIC_GOOGLE_MAPS_API_KEY` from these three files (leave it in
   `app.config.ts`).
9. Driver's navigation screen should get the tightest throttle window of the
   three — it's the screen open during actual active driving, where GPS
   pings are most frequent. The client-facing tracking screens can use a
   longer window; a customer watching their order doesn't need the line
   redrawn as often as the driver navigating does.

A stale-by-a-few-seconds polyline is visually imperceptible, so the
throttling above is about bounding worst-case Directions API call volume,
not about precision.

---

## Apply the tenant scopes to the Livewire admin panel

The API and the export/PDF paths are scoped; the Livewire pages are not,
because they were deliberately left untouched while the backend work landed.
`Driver::scopeForCurrentTenant()` and `Client::scopeForCurrentTenant()` already
exist, so each of these is a one-line change:

- `App\Livewire\Pages\Driver\IndexDriver` — the listing query, plus the
  `Driver::findOrFail()` in `delete()` and `forceDelete()`
- `App\Livewire\Pages\Driver\ActivateDriver`, `EditDriver`, and
  `App\Livewire\Pages\Wallet\ActivateDriver` — same `findOrFail` pattern
- `App\Livewire\Pages\Client\IndexClient` — the listing query

Until these land, a restaurant user signed into the panel still *sees* other
restaurants' drivers. They can no longer activate, export, print or delete
them (those paths are guarded), and `/clients` is admin-only today, so this is
a visibility gap rather than a write hole.

Also still open: `App\Livewire\Pages\Setting\EditSettings::mount()` reads
`Setting::first('*')` — unscoped, so it can load a different restaurant's row
than the one the save path writes to.

## Theme defaults should come from one place

`ThemeService::getDefaultThemeData()` hardcodes the brand palette while the
`restaurant_themes` migration declares different column defaults (#000000 /
#FFFFFF against the service's #FF3D77 / #131313). Move the brand values onto
the model as `protected $attributes`, the way `Setting` already does, and
reduce the service array to `['is_active' => true, 'name' => 'Default Theme']`.

`is_active` must stay explicit whatever happens: the column defaults to false
and `Restaurant::activeTheme()` filters on `is_active = true`, so a theme
created on column defaults would never be found and every call would create
another row.
