diff --git a/.env b/.env
index 1c3f636..1e3887b 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,6 @@
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_bWVhc3VyZWQtcGFudGhlci04Mi5jbGVyay5hY2NvdW50cy5kZXYk
DATABASE_URL=postgresql://jsm_uber_owner:npg_wsprVk4dJ8XW@ep-rapid-field-a1prd9ss-pooler.ap-southeast-1.aws.neon.tech/jsm_uber?sslmode=require
EXPO_PUBLIC_GEOAPIFY_API_KEY=0e7ccfac62054b0f846032bff6bae5c9
-EXPO_PUBLIC_GOOGLE_API_KEY=AIzaSyD10tc4ec2FFVXQcWDXCT2CeBy-jwbRZB8
\ No newline at end of file
+EXPO_PUBLIC_GOOGLE_API_KEY=AIzaSyD10tc4ec2FFVXQcWDXCT2CeBy-jwbRZB8
+EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RBCg0EEiRKTsjR3t4krSuirMpl6cf28QcyIpMlWD6eIZoU9qBCGBhoSwr9zlRLEQsoUwCusZupgnOMmOAyYG19U00S86heUPX
+STRIPE_SECRET_KEY=sk_test_51RBCg0EEiRKTsjR3wFKcVMiOhedbosVqKnl5fCYhTDsQkXZVx7UJqUiXRu6b9ghbP6yrqMnom1GePKtUAgVl9twg00dJzPIgBz
\ No newline at end of file
diff --git a/app/(root)/book-ride.tsx b/app/(root)/book-ride.tsx
index a99318d..5efe7e7 100644
--- a/app/(root)/book-ride.tsx
+++ b/app/(root)/book-ride.tsx
@@ -5,21 +5,26 @@ import RideLayout from "@/components/RideLayout";
import {icons} from "@/constants";
import {formatTime} from "@/lib/utils";
import {useDriverStore, useLocationStore} from "@/store";
+import Payment from "@/components/Payment";
+import { StripeProvider } from '@stripe/stripe-react-native';
const BookRide = () => {
const {user} = useUser();
const {userAddress, destinationAddress} = useLocationStore();
const {drivers, selectedDriver} = useDriverStore();
- console.log(drivers);
- console.log(selectedDriver);
const driverDetails = drivers?.filter(
(driver) => +driver.id === selectedDriver,
)[0];
return (
+
<>
-
+
Ride Information
@@ -87,8 +92,16 @@ const BookRide = () => {
+
>
+
);
};
diff --git a/app/api/(stripe)/create+api.ts b/app/api/(stripe)/create+api.ts
new file mode 100644
index 0000000..fdeffba
--- /dev/null
+++ b/app/api/(stripe)/create+api.ts
@@ -0,0 +1,42 @@
+import {Stripe} from "stripe";
+
+const stripe = new Stripe(process.env.STRIPE_SERCET_KEY!);
+export async function POST(request:Request) {
+ const body = await request.json();
+ const { name, email, amount} = body;
+ if(!name || !email || !amount){
+ return new Response (JSON.stringify({error:"please enter a valid email Address",status:400}))
+ }
+ let customer;
+ const existingCustomer = await stripe.customers.list({email});
+ if(existingCustomer.data.length > 0){
+ customer = existingCustomer.data[0];
+ }else{
+ const newCustomer = await stripe.customers.create({
+ name,
+ email,
+ });
+ customer = newCustomer;
+ }
+
+ const ephemeralKey = await stripe.ephemeralKeys.create(
+ {customer: customer.id},
+ {apiVersion: '2025-03-31.basil'}
+ );
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: parseInt(amount) * 100,
+ currency: 'usd',
+ customer: customer.id,
+ automatic_payment_methods: {
+ enabled: true,
+ allow_redirects:"never",
+ },
+ });
+
+ return new Response(JSON.stringify({
+ paymentIntent: paymentIntent.client_secret,
+ ephemeralKey: ephemeralKey.secret,
+ customer: customer.id,
+ }),
+ );
+}
diff --git a/app/api/(stripe)/pay+api.ts b/app/api/(stripe)/pay+api.ts
new file mode 100644
index 0000000..e53975a
--- /dev/null
+++ b/app/api/(stripe)/pay+api.ts
@@ -0,0 +1,33 @@
+import {Stripe} from "stripe";
+
+const stripe = new Stripe(process.env.STRIPE_SERCET_KEY!);
+export async function POST(request:Request) {
+ try {
+ const body = await request.json();
+ const { payment_method_id,payment_intent_id,customer_id} = body;
+ if(!payment_method_id || !payment_intent_id || !customer_id) {
+ return new Response (
+ JSON.stringify({
+ error:"Missing required payment infomation",
+ status:400
+ }),
+ );
+ }
+ const paymentMethod = await stripe.paymentMethods.attach(payment_method_id,{customer:customer_id});
+ const result = await stripe.paymentIntents.confirm(payment_intent_id,{payment_method:paymentMethod.id,});
+ return new Response (
+ JSON.stringify({
+ success:true,
+ message:"Payment confirmed successfully",result:result,
+ }),
+ );
+ } catch (error) {
+ console.log(error);
+ return new Response(
+ JSON.stringify({
+ error:error,
+ status:500,
+ })
+ )
+ }
+};
\ No newline at end of file
diff --git a/app/api/driver+api.ts b/app/api/driver+api.ts
new file mode 100644
index 0000000..04de850
--- /dev/null
+++ b/app/api/driver+api.ts
@@ -0,0 +1,13 @@
+import { data } from "@/constants";
+import { neon } from "@neondatabase/serverless";
+
+export async function GET() {
+ try {
+ const sql = neon(`${process.env.DATABASE_URL}`);
+ const response = await sql`SELECT * FROM drivers`;
+ return Response.json({data:response})
+ } catch (error) {
+ console.log(error);
+ return Response.json({error:error});
+ }
+}
\ No newline at end of file
diff --git a/app/api/ride/[id]+api.ts b/app/api/ride/[id]+api.ts
new file mode 100644
index 0000000..c331fe8
--- /dev/null
+++ b/app/api/ride/[id]+api.ts
@@ -0,0 +1,46 @@
+import {neon} from "@neondatabase/serverless";
+
+export async function GET(request: Request, {id}: { id: string }) {
+ if (!id)
+ return Response.json({error: "Missing required fields"}, {status: 400});
+
+ try {
+ const sql = neon(`${process.env.DATABASE_URL}`);
+ const response = await sql`
+ SELECT
+ rides.ride_id,
+ rides.origin_address,
+ rides.destination_address,
+ rides.origin_latitude,
+ rides.origin_longitude,
+ rides.destination_latitude,
+ rides.destination_longitude,
+ rides.ride_time,
+ rides.fare_price,
+ rides.payment_status,
+ rides.created_at,
+ 'driver', json_build_object(
+ 'driver_id', drivers.id,
+ 'first_name', drivers.first_name,
+ 'last_name', drivers.last_name,
+ 'profile_image_url', drivers.profile_image_url,
+ 'car_image_url', drivers.car_image_url,
+ 'car_seats', drivers.car_seats,
+ 'rating', drivers.rating
+ ) AS driver
+ FROM
+ rides
+ INNER JOIN
+ drivers ON rides.driver_id = drivers.id
+ WHERE
+ rides.user_id = ${id}
+ ORDER BY
+ rides.created_at DESC;
+ `;
+
+ return Response.json({data: response});
+ } catch (error) {
+ console.error("Error fetching recent rides:", error);
+ return Response.json({error: "Internal Server Error"}, {status: 500});
+ }
+}
\ No newline at end of file
diff --git a/app/api/ride/create+api.ts b/app/api/ride/create+api.ts
new file mode 100644
index 0000000..ae27ea7
--- /dev/null
+++ b/app/api/ride/create+api.ts
@@ -0,0 +1,75 @@
+import {neon} from "@neondatabase/serverless";
+
+export async function POST(request: Request) {
+ try {
+ const body = await request.json();
+ const {
+ origin_address,
+ destination_address,
+ origin_latitude,
+ origin_longitude,
+ destination_latitude,
+ destination_longitude,
+ ride_time,
+ fare_price,
+ payment_status,
+ driver_id,
+ user_id,
+ } = body;
+
+ if (
+ !origin_address ||
+ !destination_address ||
+ !origin_latitude ||
+ !origin_longitude ||
+ !destination_latitude ||
+ !destination_longitude ||
+ !ride_time ||
+ !fare_price ||
+ !payment_status ||
+ !driver_id ||
+ !user_id
+ ) {
+ return Response.json(
+ {error: "Missing required fields"},
+ {status: 400},
+ );
+ }
+
+ const sql = neon(`${process.env.DATABASE_URL}`);
+
+ const response = await sql`
+ INSERT INTO rides (
+ origin_address,
+ destination_address,
+ origin_latitude,
+ origin_longitude,
+ destination_latitude,
+ destination_longitude,
+ ride_time,
+ fare_price,
+ payment_status,
+ driver_id,
+ user_id
+ ) VALUES (
+ ${origin_address},
+ ${destination_address},
+ ${origin_latitude},
+ ${origin_longitude},
+ ${destination_latitude},
+ ${destination_longitude},
+ ${ride_time},
+ ${fare_price},
+ ${payment_status},
+ ${driver_id},
+ ${user_id}
+ )
+ RETURNING *;
+ `;
+
+ return Response.json({data: response[0]}, {status: 201});
+ } catch (error) {
+ console.error("Error inserting data into recent_rides:", error);
+ return Response.json({error: "Internal Server Error"}, {status: 500});
+ }
+}
\ No newline at end of file
diff --git a/components/DriverCard.tsx b/components/DriverCard.tsx
index 2bd14ed..e02fbfe 100644
--- a/components/DriverCard.tsx
+++ b/components/DriverCard.tsx
@@ -41,7 +41,7 @@ const DriverCard = ({item, selected, setSelected}: DriverCardProps) => {
- {formatTime(item.time!)}
+ {formatTime(parseInt(`${item.time}`) || 5)}
diff --git a/components/Map.tsx b/components/Map.tsx
index 66bc2f3..7783a61 100644
--- a/components/Map.tsx
+++ b/components/Map.tsx
@@ -1,65 +1,49 @@
import { useDriverStore, useLocationStore } from "@/store";
-import { View, Text } from "react-native";
-import { calculateRegion, generateMarkersFromData } from "@/lib/map";
+import { calculateDriverTimes, calculateRegion, generateMarkersFromData } from "@/lib/map";
import MapView, { Marker, PROVIDER_DEFAULT } from "react-native-maps";
import tw from "twrnc";
import { useEffect, useState } from "react";
-import { MarkerData } from "@/types/type";
+import { Driver, MarkerData } from "@/types/type";
import { icons } from "@/constants";
-const drivers = [
- {
- "id": "1",
- "first_name": "James",
- "last_name": "Wilson",
- "profile_image_url": "https://ucarecdn.com/dae59f69-2c1f-48c3-a883-017bcf0f9950/-/preview/1000x666/",
- "car_image_url": "https://ucarecdn.com/a2dc52b2-8bf7-4e49-9a36-3ffb5229ed02/-/preview/465x466/",
- "car_seats": 4,
- "rating": "4.80"
- },
- {
- "id": "2",
- "first_name": "David",
- "last_name": "Brown",
- "profile_image_url": "https://ucarecdn.com/6ea6d83d-ef1a-483f-9106-837a3a5b3f67/-/preview/1000x666/",
- "car_image_url": "https://ucarecdn.com/a3872f80-c094-409c-82f8-c9ff38429327/-/preview/930x932/",
- "car_seats": 5,
- "rating": "4.60"
- },
- {
- "id": "3",
- "first_name": "Michael",
- "last_name": "Johnson",
- "profile_image_url": "https://ucarecdn.com/0330d85c-232e-4c30-bd04-e5e4d0e3d688/-/preview/826x822/",
- "car_image_url": "https://ucarecdn.com/289764fb-55b6-4427-b1d1-f655987b4a14/-/preview/930x932/",
- "car_seats": 4,
- "rating": "4.70"
- },
- {
- "id": "4",
- "first_name": "Robert",
- "last_name": "Green",
- "profile_image_url": "https://ucarecdn.com/fdfc54df-9d24-40f7-b7d3-6f391561c0db/-/preview/626x417/",
- "car_image_url": "https://ucarecdn.com/b6fb3b55-7676-4ff3-8484-fb115e268d32/-/preview/930x932/",
- "car_seats": 4,
- "rating": "4.90"
- }
-]
+import { useFetch } from "@/lib/fetch";
+import { ActivityIndicator, Text, View } from "react-native";
+
const Map = () => {
+ const { data: drivers, loading, error } = useFetch("/(api)/driver");
const { userLongitude, userLatitude, destinationLatitude, destinationLongitude } = useLocationStore();
const { selectedDriver, setDrivers } = useDriverStore();
- const [markers, setMarkers] = useState();
+ const [markers, setMarkers] = useState([]);
const region = calculateRegion({
userLongitude, userLatitude, destinationLatitude, destinationLongitude,
});
useEffect(() => {
- // TODO:Remove
- setDrivers(drivers);
if (Array.isArray(drivers)) {
if (!userLatitude || !userLongitude) return;
- const newMarkers = generateMarkersFromData({ data : drivers, userLatitude, userLongitude, });
- setMarkers(newMarkers)
+ const newMarkers = generateMarkersFromData({ data: drivers, userLatitude, userLongitude, });
+ setMarkers(newMarkers);
}
- }, [drivers])
+ }, [drivers]);
+ useEffect(() => {
+ if (markers.length > 0 && destinationLatitude && destinationLongitude) {
+ calculateDriverTimes({ markers, userLongitude, userLatitude, destinationLatitude, destinationLongitude }).then((drivers) => {
+ setDrivers(drivers as MarkerData[]);
+ });
+ }
+ }, [markers, destinationLatitude, destinationLongitude]);
+ if (loading || !userLatitude || !userLongitude) {
+ return (
+
+
+
+ );
+ }
+ if (error) {
+ return (
+
+ Error:{error}
+
+ );
+ }
return (
{markers?.map((marker) => (
diff --git a/components/Payment.tsx b/components/Payment.tsx
new file mode 100644
index 0000000..5ea1e0b
--- /dev/null
+++ b/components/Payment.tsx
@@ -0,0 +1,82 @@
+import { Alert, Text, View } from "react-native";
+import CustomButton from "./CustomButton";
+import tw from "twrnc";
+import { PaymentMethod, PaymentSheetError, useStripe } from "@stripe/stripe-react-native";
+import { useState, useEffect } from "react";
+import { fetchAPI } from "@/lib/fetch";
+import { PaymentProps } from "@/types/type";
+const Payment = ({fullName,email,amount,driverId,rideTime}:PaymentProps) => {
+ const [success, setSuccess] = useState(false);
+ const { initPaymentSheet, presentPaymentSheet } = useStripe();
+ const confirmHandler = async (paymentMethod,_, intentCreationCallback) => {
+ const {paymentIntent,customer} = await fetchAPI("/(api)/(stripe)/create",{
+ method:"POST",
+ headers:{
+ "Content-Type": "application/json",
+ },
+ body:JSON.stringify({
+ name:fullName || email.split("@")[0],
+ email: email,
+ amount: amount,
+ paymentMethodId : paymentMethod.id,
+ }),
+ },
+ );
+ if(paymentIntent.client_sercet){
+ 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_sercet) {
+ //ride/create
+ }
+ }
+ const { clientSercet, error } = await response.json();
+ if(clientSercet){
+ intentCreationCallback({clientSercet})
+ }
+ else{
+ intentCreationCallback({error})
+ }
+ }
+ const initializePaymentSheet = async () => {
+ const { error } = await initPaymentSheet({
+ merchantDisplayName: "Example,Inc.",
+ intentConfiguration: {
+ mode: {
+ amount: 1099,
+ currencyCode: "USD",
+ },
+ confirmHandler: confirmHandler
+ }
+ });
+ if (error) {
+ //handle error
+ }
+ };
+ const openPaymentSheet = async () => {
+ await initializePaymentSheet();
+
+ const { error } = await presentPaymentSheet();
+
+ if (error) {
+ Alert.alert(`Error code: ${error.code}`, error.message);
+ }
+ else {
+ setSuccess(true);
+ }
+ };
+return (
+ <>
+
+ >
+)
+};
+export default Payment;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 163c8b2..680b6d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"@neondatabase/serverless": "^0.10.4",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
+ "@stripe/stripe-react-native": "0.38.6",
"expo": "52.0.42",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.8",
@@ -48,6 +49,7 @@
"react-native-swiper": "^1.6.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
+ "stripe": "^18.0.0",
"tailwindcss": "^3.4.17",
"twrnc": "^4.6.1",
"zustand": "^5.0.3"
@@ -4976,6 +4978,22 @@
"node": ">=12.16"
}
},
+ "node_modules/@stripe/stripe-react-native": {
+ "version": "0.38.6",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-react-native/-/stripe-react-native-0.38.6.tgz",
+ "integrity": "sha512-U6yELoRr4h4x+p9an0MiDXZBbm/FYNayPXJP0PtsR3iFVAGpGQm6DzM+cye2PVlhbDK8htBA5ReZ1YEFxT/hJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": ">=46.0.9",
+ "react": "*",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -6981,7 +6999,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@@ -14160,7 +14177,6 @@
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -16984,7 +17000,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -17004,7 +17019,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@@ -17021,7 +17035,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -17040,7 +17053,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -17602,6 +17614,34 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stripe": {
+ "version": "18.0.0",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-18.0.0.tgz",
+ "integrity": "sha512-3Fs33IzKUby//9kCkCa1uRpinAoTvj6rJgQ2jrBEysoxEvfsclvXdna1amyEYbA2EKkjynuB4+L/kleCCaWTpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": ">=8.1.0",
+ "qs": "^6.11.0"
+ },
+ "engines": {
+ "node": ">=12.*"
+ }
+ },
+ "node_modules/stripe/node_modules/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/structured-headers": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz",
diff --git a/package.json b/package.json
index 33a4f5b..64837c7 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@neondatabase/serverless": "^0.10.4",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
+ "@stripe/stripe-react-native": "0.38.6",
"expo": "52.0.42",
"expo-blur": "~14.0.3",
"expo-constants": "~17.0.8",
@@ -55,6 +56,7 @@
"react-native-swiper": "^1.6.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5",
+ "stripe": "^18.0.0",
"tailwindcss": "^3.4.17",
"twrnc": "^4.6.1",
"zustand": "^5.0.3"