import { MaterialCommunityIcons } from "@expo/vector-icons";
// Every control on this screen lives inside the RideLayout bottom sheet, and
// on Android a react-native touchable in there loses its first press to the
// sheet's gesture handler — which is why "Find now" had to be tapped twice to
// send a request. The sheet's own touchables are the fix the library ships for
// this; on iOS they are react-native's, unchanged.
import { TouchableOpacity } from "@gorhom/bottom-sheet";
import { router } from "expo-router";
import { useEffect, useState } from "react";
import { Alert, Text, View } from "react-native";
import { CustomButton } from "@/components/custom-button";
import { GoogleTextInput } from "@/components/google-text-input";
import { RideLayout } from "@/components/ride-layout";
import { icons } from "@/constants";
import { SERVICES, type ServiceId } from "@/constants/services";
import { ApiError } from "@/lib/fetch";
import { useT } from "@/lib/i18n";
import { calculateTripFare } from "@/lib/map";
import { formatLBP } from "@/lib/pricing";
import { createRideRequest } from "@/lib/request-ride";
import { useServiceAvailability } from "@/lib/use-service-availability";
import { formatTime } from "@/lib/utils";
import { useLocationStore, useServiceStore } from "@/store";
/**
* "Set it on the map" for one of the two points.
*
* An autocomplete result lands on whatever the geocoder calls the centre of a
* place, which is regularly the wrong side of a building or the wrong end of a
* long street — and a driver sent to the wrong side of a divided road can't
* simply turn around. This is the escape hatch: the rider drags the map to the
* exact doorway.
*/
const AdjustOnMap = ({ mode }: { mode: "origin" | "destination" }) => {
const t = useT();
return (
router.push({ pathname: "/(root)/adjust-pin", params: { mode } })
}
className="flex-row items-center gap-x-2 mt-2 self-start px-1 py-1.5"
>
{t("findRide.adjustOnMap")}
);
};
/**
* Which service the request goes out on, with live availability.
*
* It lives on this screen because this is now the last screen before drivers
* are contacted — the request is broadcast on tap, so the choice of who to
* broadcast it to has to be made here, next to the button that sends it.
*/
const ServiceRow = ({
service,
counts,
onSelect,
}: {
service: ServiceId;
counts: Record;
onSelect: (id: ServiceId) => void;
}) => {
const t = useT();
return (
{SERVICES.map((item) => {
const active = item.id === service;
const available = counts[item.id] ?? 0;
return (
onSelect(item.id)}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityState={{ selected: active }}
className={`flex-1 items-center rounded-2xl border py-2.5 ${
active
? "border-primary-500 bg-primary-500/10"
: "border-neutral-100 bg-neutral-100 dark:border-neutral-800 dark:bg-neutral-800"
}`}
>
{t(item.labelKey)}
{/* The count is the honest version of an empty map: it says
whether asking this service is worth doing before the rider
sends a request nobody will answer. */}
0
? "text-emerald-600 dark:text-emerald-400"
: "text-general-200 dark:text-neutral-500"
}`}
>
{available > 0 ? t("findRide.nAvailable", { n: available }) : "—"}
);
})}
);
};
const FindRide = () => {
const t = useT();
const {
userAddress,
destinationAddress,
userLatitude,
userLongitude,
destinationLatitude,
destinationLongitude,
setDestinationLocation,
setUserLocation,
} = useLocationStore();
const { service, setService } = useServiceStore();
const [estimate, setEstimate] = useState<{
fare: string;
durationSeconds: number;
} | null>(null);
const [estimating, setEstimating] = useState(false);
const [sending, setSending] = useState(false);
const hasRoute =
!!userLatitude &&
!!userLongitude &&
!!destinationLatitude &&
!!destinationLongitude;
const { counts } = useServiceAvailability(userLatitude, userLongitude);
// The fare is quoted before the request goes out, not after: it is what the
// drivers deciding whether to take the job are shown, so it has to exist by
// the time the request does. Recomputed when the route or service changes.
useEffect(() => {
if (!hasRoute) {
setEstimate(null);
return;
}
let cancelled = false;
setEstimating(true);
void calculateTripFare({
userLatitude,
userLongitude,
destinationLatitude,
destinationLongitude,
service,
})
.then((trip) => {
if (cancelled) return;
setEstimate(
trip
? { fare: trip.fare, durationSeconds: trip.durationSeconds }
: null,
);
})
.finally(() => {
if (!cancelled) setEstimating(false);
});
return () => {
cancelled = true;
};
}, [
hasRoute,
userLatitude,
userLongitude,
destinationLatitude,
destinationLongitude,
service,
]);
const findNow = async () => {
if (!hasRoute || !estimate) return;
setSending(true);
try {
const ride = await createRideRequest({
service,
origin: {
address: userAddress ?? "",
latitude: userLatitude!,
longitude: userLongitude!,
},
destination: {
address: destinationAddress ?? "",
latitude: destinationLatitude!,
longitude: destinationLongitude!,
},
rideTimeSeconds: estimate.durationSeconds,
fareCents: Math.round(parseFloat(estimate.fare) * 100),
});
router.replace(`/(root)/book-ride?id=${ride.ride_id}`);
} catch (err) {
console.log("[FIND_RIDE]: ", err);
// The rider already has a ride in flight. Booking a second one isn't
// what they want — they want the one they lost track of, so take them
// to it instead of showing an error they can't act on.
if (
err instanceof ApiError &&
err.status === 409 &&
err.body?.code === "RIDE_IN_PROGRESS"
) {
const inProgressId = String(err.body.ride_id);
Alert.alert(
t("confirmRide.alertInProgressTitle"),
t("confirmRide.alertInProgressBody"),
[
{ text: t("common.cancel"), style: "cancel" },
{
text: t("confirmRide.viewRide"),
onPress: () =>
router.replace(`/(root)/book-ride?id=${inProgressId}`),
},
],
);
return;
}
Alert.alert(
t("confirmRide.alertErrorTitle"),
err instanceof ApiError
? err.message
: t("confirmRide.alertErrorFallback"),
);
} finally {
setSending(false);
}
};
return (
{t("findRide.from")}
{t("findRide.to")}
{t("findRide.service")}
{/* The quote, shown before the request goes out rather than on a screen
after it. This is the number the rider agrees to and the number every
driver who sees the request is offered, so it belongs next to the
button that sends it. */}
{t("findRide.estimatedFare")}
{estimate
? t("confirmRide.tripTime", {
time: formatTime(estimate.durationSeconds / 60),
})
: t("findRide.setBothPoints")}
{estimating ? "…" : estimate ? `$${estimate.fare}` : "—"}
{estimate ? (
{t("confirmRide.lbpEstimate", {
lbp: formatLBP(parseFloat(estimate.fare)),
})}
) : null}
{t("findRide.payLaterHint")}
void findNow()}
disabled={!hasRoute || !estimate || estimating || sending}
className={`mt-3 ${!hasRoute || !estimate || estimating || sending ? "opacity-50" : ""}`}
/>
);
};
export default FindRide;