import { router } from "expo-router"; import * as WebBrowser from "expo-web-browser"; import { useState } from "react"; import { Alert, Image, Text, TouchableOpacity, View } from "react-native"; import ReactNativeModal from "react-native-modal"; import { images } from "@/constants"; import { ApiError, fetchAPI } from "@/lib/fetch"; import { useT } from "@/lib/i18n"; import { formatLBP } from "@/lib/pricing"; import { useLocationStore } from "@/store"; import type { PaymentProps } from "@/types/type"; import { CustomButton } from "./custom-button"; type PaymentMethod = "cash" | "card"; export const Payment = ({ fullName, email, amount, driverId, rideTime, }: PaymentProps) => { const { userAddress, userLongitude, userLatitude, destinationLatitude, destinationAddress, destinationLongitude, } = useLocationStore(); const [method, setMethod] = useState("cash"); const [success, setSuccess] = useState(false); const [processing, setProcessing] = useState(false); const t = useT(); const fareCents = Math.round(parseFloat(amount) * 100); // in cents const recordRide = async (paymentMethod: PaymentMethod, orderId?: string) => { await fetchAPI("/(api)/ride/create", { method: "POST", headers: { "Content-type": "application/json", }, body: JSON.stringify({ origin_address: userAddress, destination_address: destinationAddress, origin_latitude: userLatitude, origin_longitude: userLongitude, destination_latitude: destinationLatitude, destination_longitude: destinationLongitude, ride_time: rideTime.toFixed(0), fare_price: fareCents, payment_method: paymentMethod, ...(orderId ? { payment_order_id: orderId } : {}), driver_id: driverId, }), }); }; // Cash is settled directly with the driver at drop-off. const payWithCash = async () => { setProcessing(true); try { await recordRide("cash"); setSuccess(true); } catch (err) { console.log("[PAYMENT]: ", err); Alert.alert( t("components.payment.alertErrorTitle"), t("components.payment.alertErrorBody"), ); } finally { setProcessing(false); } }; const payWithCard = async () => { setProcessing(true); try { // 1. Create an Areeba checkout session on our server. The server stores // the ride intent and the successIndicator; the client only gets an // orderId + checkoutUrl. const { orderId, checkoutUrl, error } = await fetchAPI( "/(api)/(areeba)/create", { method: "POST", headers: { "Content-type": "application/json", }, body: JSON.stringify({ name: fullName || email, email, fare_cents: fareCents, driver_id: driverId, origin_address: userAddress, destination_address: destinationAddress, origin_latitude: userLatitude, origin_longitude: userLongitude, destination_latitude: destinationLatitude, destination_longitude: destinationLongitude, ride_time: rideTime.toFixed(0), }), }, ); if (error || !checkoutUrl) throw new Error(error || "No checkout URL"); // 2. Open 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", ) as string; } // 3. Verify the payment server-side. The server compares // resultIndicator against the stored successIndicator, reconciles // the captured amount, and marks the order paid. const verification = await fetchAPI("/(api)/(areeba)/verify", { method: "POST", headers: { "Content-type": "application/json", }, body: JSON.stringify({ orderId, resultIndicator }), }); if (verification.success) { // 4. Record the ride, consuming the paid order atomically. The client // never sets payment_status itself. await recordRide("card", orderId); setSuccess(true); } else { Alert.alert( t("components.payment.alertPaymentNotCompletedTitle"), t("components.payment.alertPaymentNotCompletedBody"), ); } } catch (err) { console.log("[PAYMENT]: ", err); // Verification failures (cancelled, not captured, amount/intent mismatch) // come back as 400s. fetchAPI throws ApiError on non-2xx, so without this // branch every cancellation lands in the generic "something went wrong". if (err instanceof ApiError && err.status === 400) { Alert.alert( t("components.payment.alertPaymentNotCompletedTitle"), t("components.payment.alertPaymentNotCompletedBody"), ); } else { Alert.alert( t("components.payment.alertProcessingTitle"), t("components.payment.alertProcessingBody"), ); } } finally { setProcessing(false); } }; const confirm = () => method === "cash" ? payWithCash() : Alert.alert( t("components.payment.alertPayCardTitle"), t("components.payment.alertPayCardBody", { amount }), [ { text: t("common.cancel"), style: "cancel" }, { text: t("common.continue"), onPress: () => void payWithCard() }, ], ); return ( <> {t("components.payment.paymentMethod")} setMethod("cash")} className={`flex-1 items-center py-3 rounded-xl border ${ method === "cash" ? "bg-general-600 dark:bg-primary-500/20 border-primary-500" : "bg-white dark:bg-neutral-900 border-general-700 dark:border-neutral-700" }`} > {t("components.payment.cash")} setMethod("card")} className={`flex-1 items-center py-3 rounded-xl border ${ method === "card" ? "bg-general-600 dark:bg-primary-500/20 border-primary-500" : "bg-white dark:bg-neutral-900 border-general-700 dark:border-neutral-700" }`} > {t("components.payment.card")} setSuccess(false)} > {t("components.payment.checkAlt")} {t("components.payment.rideBooked")} {t("components.payment.successBody")} {method === "cash" ? t("components.payment.cashInstruction", { lbp: formatLBP(parseFloat(amount)), }) : null} { setSuccess(false); router.push("/(root)/(tabs)/home"); }} className="mt-5" /> ); };