diff --git a/app/(api)/(stripe)/create+api.ts b/app/(api)/(stripe)/create+api.ts index c9d67e7..c802115 100644 --- a/app/(api)/(stripe)/create+api.ts +++ b/app/(api)/(stripe)/create+api.ts @@ -32,7 +32,7 @@ export async function POST(req: Request) { const paymentIntent = await stripe.paymentIntents.create({ amount: parseInt(amount) * 100, - currency: "usd", + currency: "USD", customer: customer.id, automatic_payment_methods: { enabled: true, diff --git a/app/(root)/book-ride.tsx b/app/(root)/book-ride.tsx index 5a3778f..13318b7 100644 --- a/app/(root)/book-ride.tsx +++ b/app/(root)/book-ride.tsx @@ -1,11 +1,15 @@ +import { useUser } from "@clerk/clerk-expo"; +import { StripeProvider } from "@stripe/stripe-react-native"; import { Image, Text, View } from "react-native"; +import { Payment } from "@/components/payment"; import { RideLayout } from "@/components/ride-layout"; import { icons } from "@/constants"; import { formatTime } from "@/lib/utils"; import { useDriverStore, useLocationStore } from "@/store"; const BookRide = () => { + const { user } = useUser(); const { userAddress, destinationAddress } = useLocationStore(); const { drivers, selectedDriver } = useDriverStore(); @@ -14,84 +18,98 @@ const BookRide = () => { )[0]; return ( - - <> - - Ride Information - + + + <> + + Ride Information + - - Driver Avatar + + Driver Avatar - - - {driverDetails?.title} - + + + {driverDetails?.title} + - - Star + + Star + + + {driverDetails?.rating} + + + + + + + + Ride Price + + + ${driverDetails?.price} + + + + + Pickup Time - {driverDetails?.rating} + {formatTime(driverDetails?.time!)} + + + + + Car Seats + + + {driverDetails?.car_seats} - - - - Ride Price + + + - - ${driverDetails?.price} - + + {userAddress} + + + + + + + + {destinationAddress} + + - - Pickup Time - - - {formatTime(driverDetails?.time!)} - - - - - Car Seats - - - {driverDetails?.car_seats} - - - - - - - - - - {userAddress} - - - - - - - - {destinationAddress} - - - - - + + + + ); }; diff --git a/components/payment.tsx b/components/payment.tsx new file mode 100644 index 0000000..162b0fb --- /dev/null +++ b/components/payment.tsx @@ -0,0 +1,170 @@ +import { useAuth } from "@clerk/clerk-expo"; +import { PaymentSheetError, useStripe } from "@stripe/stripe-react-native"; +import type { Result } from "@stripe/stripe-react-native/lib/typescript/src/types/PaymentMethod"; +import type { IntentCreationCallbackParams } from "@stripe/stripe-react-native/lib/typescript/src/types/PaymentSheet"; +import { router } from "expo-router"; +import { useState } from "react"; +import { Alert, Image, Text, View } from "react-native"; +import ReactNativeModal from "react-native-modal"; + +import { images } from "@/constants"; +import { fetchAPI } from "@/lib/fetch"; +import { useLocationStore } from "@/store"; +import type { PaymentProps } from "@/types/type"; + +import { CustomButton } from "./custom-button"; + +export const Payment = ({ + fullName, + email, + amount, + driverId, + rideTime, +}: PaymentProps) => { + const { + userAddress, + userLongitude, + userLatitude, + destinationLatitude, + destinationAddress, + destinationLongitude, + } = useLocationStore(); + const { userId } = useAuth(); + const { initPaymentSheet, presentPaymentSheet } = useStripe(); + const [success, setSuccess] = useState(false); + + const confirmHandler = async ( + paymentMethod: Result, + _shouldSavePaymentMethod: boolean, + intentCreationCallback: (result: IntentCreationCallbackParams) => void, + ) => { + const { paymentIntent, customer } = await fetchAPI( + "/(api)/(stripe)/create", + { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify({ + name: fullName || email, + email, + amount, + paymentMethodId: paymentMethod.id, + }), + }, + ); + + if (paymentIntent.client_secret) { + const { result } = await fetchAPI("/(api)/(stripe)/pay", { + method: "POST", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify({ + payment_method_id: paymentMethod.id, + payment_intent_id: paymentIntent.id, + customer_id: customer, + }), + }); + + if (result.client_secret) { + 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: parseInt(amount) * 100, // in cents + payment_status: "paid", + driver_id: driverId, + user_id: userId, + }), + }); + + intentCreationCallback({ + clientSecret: result.client_secret, + }); + } + } + }; + + const initializePaymentSheet = async () => { + const { error } = await initPaymentSheet({ + merchantDisplayName: "Ryde, Inc.", + intentConfiguration: { + mode: { + amount: parseInt(amount) * 100, + currencyCode: "USD", + }, + confirmHandler, + }, + style: "alwaysLight", + returnURL: "ryde://book-ride", // make sure protocol matches scheme in app.json + }); + + if (error) { + Alert.alert( + "Error", + "Something went wrong while initializing your payment. Please try again.", + ); + } + }; + + const openPaymentSheet = async () => { + await initializePaymentSheet(); + + const { error } = await presentPaymentSheet(); + + if (error) { + if (error.code !== PaymentSheetError.Canceled) + Alert.alert(`Error code: ${error.code}`, error.message); + } else { + setSuccess(true); + } + }; + + return ( + <> + + + setSuccess(false)} + > + + Check + + + Ride Booked! + + + + Thank you for your booking.{"\n"} Your reservation has been placed. + {"\n"} + Please proceed with your trip. + + + { + setSuccess(false); + router.push("/(root)/(tabs)/home"); + }} + className="mt-5" + /> + + + + ); +};