Stripe 45 min

master
neel 2025-04-09 15:01:23 +05:30
parent 742024ab35
commit 3565652962
12 changed files with 390 additions and 58 deletions

4
.env
View File

@ -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
EXPO_PUBLIC_GOOGLE_API_KEY=AIzaSyD10tc4ec2FFVXQcWDXCT2CeBy-jwbRZB8
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51RBCg0EEiRKTsjR3t4krSuirMpl6cf28QcyIpMlWD6eIZoU9qBCGBhoSwr9zlRLEQsoUwCusZupgnOMmOAyYG19U00S86heUPX
STRIPE_SECRET_KEY=sk_test_51RBCg0EEiRKTsjR3wFKcVMiOhedbosVqKnl5fCYhTDsQkXZVx7UJqUiXRu6b9ghbP6yrqMnom1GePKtUAgVl9twg00dJzPIgBz

View File

@ -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 (
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY!}
merchantIdentifier="merchant.identifier" // required for Apple Pay
urlScheme="your-url-scheme" // required for 3D Secure and bank redirects
>
<RideLayout title="Book Ride">
<>
<Text style={tw`text-xl font-JakartaSemiBold mb-3`}>
<Text style={tw`text-xl font-JakartaSemiBold mb-3`} >
Ride Information
</Text>
@ -87,8 +92,16 @@ const BookRide = () => {
</Text>
</View>
</View>
<Payment
fullName={user?.fullName!}
email={user?.emailAddresses[0].emailAddress!}
amount={driverDetails?.price!}
driverId={driverDetails?.id!}
rideTime={driverDetails?.time!}
/>
</>
</RideLayout>
</StripeProvider>
);
};

View File

@ -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,
}),
);
}

View File

@ -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,
})
)
}
};

13
app/api/driver+api.ts Normal file
View File

@ -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});
}
}

46
app/api/ride/[id]+api.ts Normal file
View File

@ -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});
}
}

View File

@ -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});
}
}

View File

@ -41,7 +41,7 @@ const DriverCard = ({item, selected, setSelected}: DriverCardProps) => {
</Text>
<Text style={tw`text-sm font-JakartaRegular text-general-800`}>
{formatTime(item.time!)}
{formatTime(parseInt(`${item.time}`) || 5)}
</Text>
<Text style={tw`text-sm font-JakartaRegular text-general-800 mx-1`}>

View File

@ -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<Driver[]>("/(api)/driver");
const { userLongitude, userLatitude, destinationLatitude, destinationLongitude } = useLocationStore();
const { selectedDriver, setDrivers } = useDriverStore();
const [markers, setMarkers] = useState<MarkerData[]>();
const [markers, setMarkers] = useState<MarkerData[]>([]);
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 (
<View style={tw`flex justify-between items-center w-full`}>
<ActivityIndicator size="small" color="#000" />
</View>
);
}
if (error) {
return (
<View style={tw`flex justify-between items-center w-full`}>
<Text>Error:{error}</Text>
</View>
);
}
return (
<MapView provider={PROVIDER_DEFAULT} style={tw`w-full h-full rounded-2xl `} tintColor="black" mapType="standard" showsPointsOfInterest={false} initialRegion={region} showsUserLocation={true} userInterfaceStyle="light" >
{markers?.map((marker) => (

82
components/Payment.tsx Normal file
View File

@ -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 (
<>
<CustomButton title="Confirm Ride" style={tw`my-10`} onPress={openPaymentSheet} />
</>
)
};
export default Payment;

52
package-lock.json generated
View File

@ -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",

View File

@ -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"