From 3565652962b8b8429534b66483dc2a474d81f31a Mon Sep 17 00:00:00 2001 From: neel Date: Wed, 9 Apr 2025 15:01:23 +0530 Subject: [PATCH] Stripe 45 min --- .env | 4 +- app/(root)/book-ride.tsx | 19 ++++++-- app/api/(stripe)/create+api.ts | 42 +++++++++++++++++ app/api/(stripe)/pay+api.ts | 33 ++++++++++++++ app/api/driver+api.ts | 13 ++++++ app/api/ride/[id]+api.ts | 46 +++++++++++++++++++ app/api/ride/create+api.ts | 75 +++++++++++++++++++++++++++++++ components/DriverCard.tsx | 2 +- components/Map.tsx | 78 +++++++++++++------------------- components/Payment.tsx | 82 ++++++++++++++++++++++++++++++++++ package-lock.json | 52 ++++++++++++++++++--- package.json | 2 + 12 files changed, 390 insertions(+), 58 deletions(-) create mode 100644 app/api/(stripe)/create+api.ts create mode 100644 app/api/(stripe)/pay+api.ts create mode 100644 app/api/driver+api.ts create mode 100644 app/api/ride/[id]+api.ts create mode 100644 app/api/ride/create+api.ts create mode 100644 components/Payment.tsx 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"