Driver side (was a stub): - In-app driver onboarding: a driver-role user creates their own linked drivers profile (driver/profile+api GET/POST/PATCH). - Driver dashboard: online/offline toggle, today's earnings, incoming request cards (accept/decline), active ride panel (start/complete trip). Polls /driver/rides every 4s while online. - Location heartbeat (use-driver-location): watchPositionAsync pings /driver/location every ~5s; restarts the watch on app foreground so a backgrounded driver doesn't go permanently stale and miss requests. Dispatch (auto-match nearest, Uber-style): - Ride state machine: requested -> accepted -> en_route -> completed/cancelled with a nullable driver_id until matched (lib/dispatch.matchNextDriver). - matchNextDriver locks the ride (SELECT FOR UPDATE), expires 15s-stale offers, picks the nearest eligible driver of the matching service by haversine, offers one at a time. Called from ride/create, ride/[id] GET (lazy match on the rider's poll), and ride/[id]/respond (on decline). - ride/create is now a request endpoint (driver_id NULL, status=requested, service); drops the pre-match driver_id payment reconciliation. - ride/[id] GET returns status/service/nullable driver; PATCH handles rider cancel + driver en_route/completed. ride/list backs the history tabs. Rider flow (best experience): - confirm-ride is now a request screen: single trip fare + nearest-driver ETA + cash/card + Request Ride -> live status. Periodically polls online drivers of the selected service and disables Request when none are online (prevents the "stuck searching forever" state). - book-ride is the live ride-status screen (searching -> accepted -> en_route -> completed/cancelled + Cancel), polling every 3s. - lib/request-ride unifies the Areeba card flow + cash path. - Map reads /driver/nearby (real positions, service-filtered); lib/map adds calculateTripFare + service-aware fares. POI suggestions: - lib/places (Google Nearby Search) + nearby-suggestions chips for mall/hospital/pharmacy/restaurant on the home screen. Service categories now drive both matching and a per-service fare multiplier (car 1.0 / moto 0.7 / courier 0.85 / chauffeur 1.5). Map tiles: react-native-maps rendered blank on Android because no Google Maps key was set. Switched app.json -> app.config.js so android.config.googleMaps.apiKey is injected from EXPO_PUBLIC_GOOGLE_API_KEY at build time (keeps the key out of git). Requires a native rebuild (expo run:android) to take effect. Also includes the prior payment/auth hardening (server-authoritative payment_orders ledger with double-spend guards, peppered OTP, register TOCTOU fix, stats cents fix) that was left uncommitted. Co-Authored-By: Claude <noreply@anthropic.com>
71 lines
1.8 KiB
TypeScript
71 lines
1.8 KiB
TypeScript
import type { Ride } from "@/types/type";
|
|
|
|
export const sortRides = (rides: Ride[]): Ride[] => {
|
|
return [...rides].sort(
|
|
(a, b) =>
|
|
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
|
|
);
|
|
};
|
|
|
|
export function formatTime(minutes: number): string {
|
|
const formattedMinutes = Math.round(minutes) || 0;
|
|
|
|
if (formattedMinutes < 60) {
|
|
return `${formattedMinutes} min`;
|
|
} else {
|
|
const hours = Math.floor(formattedMinutes / 60);
|
|
const remainingMinutes = formattedMinutes % 60;
|
|
return `${hours}h ${remainingMinutes}m`;
|
|
}
|
|
}
|
|
|
|
export function formatDate(dateString: string): string {
|
|
const date = new Date(dateString);
|
|
const day = date.getDate();
|
|
const monthNames = [
|
|
"Jan",
|
|
"Feb",
|
|
"Mar",
|
|
"Apr",
|
|
"May",
|
|
"Jun",
|
|
"Jul",
|
|
"Aug",
|
|
"Sep",
|
|
"Oct",
|
|
"Nov",
|
|
"Dec",
|
|
];
|
|
const month = monthNames[date.getMonth()];
|
|
const year = date.getFullYear();
|
|
|
|
return `${day < 10 ? "0" + day : day} ${month} ${year}`;
|
|
}
|
|
|
|
// Normalizes a Lebanese phone number to E.164 (+961...).
|
|
export function normalizePhone(raw: string): string {
|
|
const cleaned = raw.replace(/[^\d+]/g, "");
|
|
|
|
if (cleaned.startsWith("+")) return cleaned;
|
|
|
|
return `+961${cleaned.replace(/^0+/, "")}`;
|
|
}
|
|
|
|
// Great-circle distance between two lat/lng points, in meters. Used for
|
|
// nearest-driver matching and "X m away" POI chips. Haversine formula.
|
|
export function haversine(
|
|
lat1: number,
|
|
lng1: number,
|
|
lat2: number,
|
|
lng2: number,
|
|
): number {
|
|
const R = 6371000; // Earth radius, meters
|
|
const toRad = (d: number) => (d * Math.PI) / 180;
|
|
const dLat = toRad(lat2 - lat1);
|
|
const dLng = toRad(lng2 - lng1);
|
|
const a =
|
|
Math.sin(dLat / 2) ** 2 +
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
|
|
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
}
|