import * as WebBrowser from "expo-web-browser"; import { ApiError, fetchAPI } from "@/lib/fetch"; import type { ServiceId } from "@/constants/services"; import type { Ride } from "@/types/type"; // The rider's side of dispatch, in two steps that used to be one. // // Asking for a ride and paying for it are now separate moments: the request // goes out to nearby drivers the instant the rider taps "Find now", they watch // offers come back, and money only changes hands once they have picked the // driver they want. Nothing is charged for a ride nobody takes. export type RequestInput = { service: ServiceId; origin: { address: string; latitude: number; longitude: number }; destination: { address: string; latitude: number; longitude: number }; rideTimeSeconds: number; fareCents: number; }; /** * Step one: open the request. Returns the created ride, which is already * being broadcast to drivers by the time this resolves. */ export const createRideRequest = async (input: RequestInput): Promise => { const res = await fetchAPI("/(api)/ride/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ origin_address: input.origin.address, destination_address: input.destination.address, origin_latitude: input.origin.latitude, origin_longitude: input.origin.longitude, destination_latitude: input.destination.latitude, destination_longitude: input.destination.longitude, ride_time: Math.round(input.rideTimeSeconds), fare_price: input.fareCents, service: input.service, }), }); return res.data as Ride; }; /** * Take a card payment for a ride that already exists, and return the paid * order id for the selection call. * * The order is only *consumed* when the driver is assigned, so if the pick * then fails — the driver took another job while the rider was in the payment * sheet — the same order id can be used for the next driver rather than the * rider paying twice. */ export const payByCard = async (input: { ride: Ride; user: { name: string; email: string }; }): Promise => { const { ride, user } = input; const { orderId, checkoutUrl, error } = await fetchAPI( "/(api)/(areeba)/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: user.name || user.email, email: user.email, fare_cents: ride.fare_price, origin_address: ride.origin_address, destination_address: ride.destination_address, origin_latitude: ride.origin_latitude, origin_longitude: ride.origin_longitude, destination_latitude: ride.destination_latitude, destination_longitude: ride.destination_longitude, ride_time: ride.ride_time, }), }, ); if (error || !checkoutUrl) throw new Error(error || "No checkout URL"); // Areeba's hosted payment page. After payment the gateway redirects back to // the app (waseel://book-ride). const browserResult = await WebBrowser.openAuthSessionAsync( checkoutUrl, "waseel://book-ride", ); let resultIndicator: string | undefined; if (browserResult.type === "success" && browserResult.url) { resultIndicator = new URL(browserResult.url).searchParams.get("resultIndicator") ?? undefined; } // The gateway's word is never taken from the client: the server asks Areeba // directly before the order is marked paid. const verification = await fetchAPI("/(api)/(areeba)/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ orderId, resultIndicator }), }); if (!verification.success) { throw new ApiError( 400, "Your payment was cancelled or could not be verified. Please try again.", ); } return orderId as string; }; /** * Step two: pick a driver. This is the call that assigns the ride, records how * it will be paid, and releases every other driver who offered. */ export const selectDriver = async (input: { rideId: number; offerId: number; method: "cash" | "card"; orderId?: string; }): Promise => { await fetchAPI(`/(api)/ride/${input.rideId}/select`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ offer_id: input.offerId, payment_method: input.method, ...(input.orderId ? { payment_order_id: input.orderId } : {}), }), }); };