Files
waseel/app/(api)/chat/active+api.ts
T
KrikoriosandClaude Opus 5 8807ff41c5 Waseel: driver capture, chat/calls, dispatch, and session fixes
Driver onboarding now photographs the licence, ID card and vehicle
registration and reads the credential fields off them, plus a camera-only
profile selfie riders check the arriving driver against. Adds in-app chat
and WebRTC calls, push-backed ride offers, ratings, cancellation and
payment sheets, settlement, and the owner dashboard endpoints behind them.

Camera permission on Android:
  - Declare CAMERA and READ_MEDIA_IMAGES in the manifest. expo-image-picker's
    own plugin never declares CAMERA, and Android denies a request for an
    undeclared permission instantly and silently — no dialog is ever shown,
    which is indistinguishable from the app not asking at all.
  - Handle canAskAgain: once Android stops showing the dialog, repeating why
    we need it is a dead end, so offer Open Settings instead (lib/capture-
    permission.ts), matching what the location flow already did.

Session: a 401 on a request that carried a token now ends the session
instead of being reinterpreted per-screen — driver-home had been reading it
as "this user has no driver profile" and showing an onboarding form to an
already-onboarded driver. Requests without a token are exempt so a failed
sign-in doesn't sign you out, and the notification is latched per token so
concurrent polls tear the session down once. (root) gains the auth guard
that turns that into the sign-in screen; app/index.tsx only guarded the way
in, leaving a session that ended mid-screen with nowhere to go.

Also ignore .uploads/ — it holds driver licence, ID and vehicle scans plus
profile photos, which are personal data and must not be committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 02:17:55 +03:00

101 lines
3.4 KiB
TypeScript

import { requireAuth } from "@/lib/jwt";
import { requireDriverProfile } from "@/lib/driver";
import { sql } from "@/lib/db";
import { CONNECTED_STATUS_ARRAY } from "@/lib/ride-lifecycle";
// GET — the Chat tab's default view. Returns the caller's currently-active
// ride that has the other party assigned (so a conversation can open), or
// null when there's nothing to chat about. The caller is auto-detected: a
// rider by default, or a driver when ?role=driver is passed (the driver app
// hits this with role=driver since the same account could in principle be a
// rider elsewhere).
//
// We try the rider path first. If the signed-in user owns an active ride
// with a driver assigned, that's their conversation. Otherwise, if they have
// a driver profile, we look for a ride they're assigned to. Either way the
// response carries the caller's `role` and a `peer` summary for the header.
type ActiveRideRow = {
ride_id: number;
status: string;
role: "rider" | "driver";
peer_name: string;
peer_avatar: string | null;
peer_service: string | null;
peer_car_model: string | null;
};
// The client (chat.tsx, call.tsx) expects `peer` nested per the ChatActiveRide
// type, not the flat peer_* columns the query returns.
const toActiveRide = (row: ActiveRideRow) => ({
ride_id: row.ride_id,
status: row.status,
role: row.role,
peer: {
name: row.peer_name,
avatar: row.peer_avatar,
service: row.peer_service,
car_model: row.peer_car_model,
},
});
export async function GET(req: Request) {
const auth = requireAuth(req);
if ("error" in auth) return auth.error;
const wantsDriver = new URL(req.url).searchParams.get("role") === "driver";
try {
// Rider path: a ride this user owns that's active and has a driver.
if (!wantsDriver) {
const riderRides = await sql<ActiveRideRow>`
SELECT
r.ride_id,
r.status,
'rider' AS role,
CONCAT_WS(' ', d.first_name, d.last_name) AS peer_name,
d.profile_image_url AS peer_avatar,
d.service AS peer_service,
d.car_model AS peer_car_model
FROM rides r
JOIN drivers d ON d.id = r.driver_id
WHERE r.user_id = ${auth.userId}
AND r.status = ANY(${CONNECTED_STATUS_ARRAY}::text[])
AND r.driver_id IS NOT NULL
ORDER BY r.created_at DESC
LIMIT 1
`;
if (riderRides[0])
return Response.json({ data: toActiveRide(riderRides[0]) });
}
// Driver path: a ride this user (as a driver) is assigned to and is active.
const driver = await requireDriverProfile(req);
if (!("error" in driver)) {
const driverRides = await sql<ActiveRideRow>`
SELECT
r.ride_id,
r.status,
'driver' AS role,
u.name AS peer_name,
NULL::text AS peer_avatar,
r.service AS peer_service,
NULL::text AS peer_car_model
FROM rides r
JOIN users u ON u.id = r.user_id
WHERE r.driver_id = ${driver.driverId}
AND r.status = ANY(${CONNECTED_STATUS_ARRAY}::text[])
ORDER BY r.created_at DESC
LIMIT 1
`;
if (driverRides[0])
return Response.json({ data: toActiveRide(driverRides[0]) });
}
return Response.json({ data: null });
} catch (error) {
console.error("[GET_ACTIVE_CHAT]: ", error);
return Response.json({ error: "Internal Server Error." }, { status: 500 });
}
}