diff --git a/backend/routes/stripe.js b/backend/routes/stripe.js index bc4b464..a98b700 100644 --- a/backend/routes/stripe.js +++ b/backend/routes/stripe.js @@ -8,7 +8,7 @@ const platformFee = 0.1; router.post("/create-checkout-session", async (req, res) => { try { - const { itemName, total, return_url } = req.body; + const { itemName, total, return_url, rentalData } = req.body; if (!itemName) { return res.status(400).json({ error: "No item name found" }); @@ -20,10 +20,24 @@ router.post("/create-checkout-session", async (req, res) => { return res.status(400).json({ error: "No return_url found" }); } + // Prepare metadata - Stripe metadata keys must be strings + const metadata = rentalData + ? { + itemId: rentalData.itemId, + startDate: rentalData.startDate, + endDate: rentalData.endDate, + startTime: rentalData.startTime, + endTime: rentalData.endTime, + totalAmount: rentalData.totalAmount.toString(), + deliveryMethod: rentalData.deliveryMethod, + } + : {}; + const session = await StripeService.createCheckoutSession({ item_name: itemName, total: total, return_url: return_url, + metadata: metadata, }); res.json({ clientSecret: session.client_secret }); @@ -44,6 +58,7 @@ router.get("/checkout-session/:sessionId", async (req, res) => { status: session.status, payment_status: session.payment_status, customer_email: session.customer_details?.email, + metadata: session.metadata, }); } catch (error) { console.error("Error retrieving checkout session:", error); diff --git a/backend/services/stripeService.js b/backend/services/stripeService.js index 0ab067d..3e91523 100644 --- a/backend/services/stripeService.js +++ b/backend/services/stripeService.js @@ -1,9 +1,14 @@ const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); class StripeService { - static async createCheckoutSession({ item_name, total, return_url }) { + static async createCheckoutSession({ + item_name, + total, + return_url, + metadata = {}, + }) { try { - const session = await stripe.checkout.sessions.create({ + const sessionConfig = { line_items: [ { price_data: { @@ -18,12 +23,15 @@ class StripeService { ], mode: "payment", ui_mode: "embedded", - return_url: return_url, //"https://example.com/checkout/return?session_id={CHECKOUT_SESSION_ID}" - }); + return_url: return_url, + metadata: metadata, + }; + + const session = await stripe.checkout.sessions.create(sessionConfig); return session; } catch (error) { - console.error("Error creating connected account:", error); + console.error("Error creating checkout session:", error); throw error; } } diff --git a/frontend/src/components/CheckoutReturn.tsx b/frontend/src/components/CheckoutReturn.tsx index 6faec22..757799a 100644 --- a/frontend/src/components/CheckoutReturn.tsx +++ b/frontend/src/components/CheckoutReturn.tsx @@ -10,6 +10,8 @@ const CheckoutReturn: React.FC = () => { >("loading"); const [error, setError] = useState(null); const [processing, setProcessing] = useState(false); + const [itemId, setItemId] = useState(null); + const [sessionMetadata, setSessionMetadata] = useState(null); const hasProcessed = useRef(false); useEffect(() => { @@ -27,31 +29,32 @@ const CheckoutReturn: React.FC = () => { checkSessionStatus(sessionId); }, [searchParams]); - const createRental = async () => { + const createRental = async (metadata: any) => { try { - // Get rental data from localStorage (set before payment) - const rentalDataString = localStorage.getItem("pendingRental"); - - if (!rentalDataString) { - console.error("No rental data found in localStorage"); - throw new Error("No rental data found in localStorage"); + if (!metadata || !metadata.itemId) { + throw new Error("No rental data found in Stripe metadata"); } - const rentalData = JSON.parse(rentalDataString); + // Convert metadata back to proper types + const rentalData = { + itemId: metadata.itemId, + startDate: metadata.startDate, + endDate: metadata.endDate, + startTime: metadata.startTime, + endTime: metadata.endTime, + totalAmount: parseFloat(metadata.totalAmount), + deliveryMethod: metadata.deliveryMethod, + }; const response = await rentalAPI.createRental(rentalData); - // Clear the pending rental data - localStorage.removeItem("pendingRental"); - localStorage.removeItem("lastItemId"); - return response; } catch (error: any) { const errorMessage = error.response?.data?.message || error.message || "Failed to create rental"; - console.error("Throwing error:", errorMessage); + console.error("Rental creation error:", errorMessage); throw new Error(errorMessage); } }; @@ -63,12 +66,18 @@ const CheckoutReturn: React.FC = () => { // Get checkout session status const response = await stripeAPI.getCheckoutSession(sessionId); - const { status: sessionStatus, payment_status } = response.data; + const { status: sessionStatus, payment_status, metadata } = response.data; + + // Store metadata for retry functionality + setSessionMetadata(metadata); + if (metadata?.itemId) { + setItemId(metadata.itemId); + } if (sessionStatus === "complete" && payment_status === "paid") { // Payment was successful - now create the rental try { - const rentalResult = await createRental(); + const rentalResult = await createRental(metadata); setStatus("success"); } catch (rentalError: any) { // Payment succeeded but rental creation failed @@ -95,7 +104,6 @@ const CheckoutReturn: React.FC = () => { const handleRetry = () => { // Go back to the item page to try payment again - const itemId = localStorage.getItem("lastItemId"); if (itemId) { navigate(`/items/${itemId}`); } else { @@ -106,7 +114,7 @@ const CheckoutReturn: React.FC = () => { const handleRetryRentalCreation = async () => { setProcessing(true); try { - await createRental(); + await createRental(sessionMetadata); setStatus("success"); setError(null); } catch (error: any) { diff --git a/frontend/src/components/StripePaymentForm.tsx b/frontend/src/components/StripePaymentForm.tsx new file mode 100644 index 0000000..4b63318 --- /dev/null +++ b/frontend/src/components/StripePaymentForm.tsx @@ -0,0 +1,103 @@ +import React, { useState, useEffect, useCallback, useRef } from "react"; +import { loadStripe } from "@stripe/stripe-js"; +import { + EmbeddedCheckoutProvider, + EmbeddedCheckout, + Elements, + useStripe, + useElements, +} from "@stripe/react-stripe-js"; +import { stripeAPI } from "../services/api"; + +const stripePromise = loadStripe( + process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || "" +); + +interface PaymentFormProps { + itemName: string; + total: number; + rentalData: any; + onSuccess: () => void; + onError: (error: string) => void; + disabled?: boolean; +} + +const PaymentForm: React.FC = ({ + itemName, + total, + rentalData, + onSuccess, + onError, + disabled, +}) => { + // const stripe = useStripe(); + // const elements = useElements(); + // const [processing, setProcessing] = useState(false); + const [clientSecret, setClientSecret] = useState(""); + const hasCreatedSession = useRef(false); + + const createCheckoutSession = useCallback(async () => { + if (hasCreatedSession.current) return; + + try { + hasCreatedSession.current = true; + const return_url = `${window.location.origin}/checkout/return?session_id={CHECKOUT_SESSION_ID}`; + + const requestData = { + itemName, + total, + return_url, + rentalData, + }; + + const response = await stripeAPI.createCheckoutSession(requestData); + + setClientSecret(response.data.clientSecret); + } catch (error: any) { + hasCreatedSession.current = false; // Reset on error so it can be retried + onError( + error.response?.data?.error || "Failed to create checkout session" + ); + } + }, [itemName, total, rentalData, onError]); + + useEffect(() => { + if (itemName && total > 0 && !clientSecret) { + createCheckoutSession(); + } + }, [itemName, total, clientSecret, createCheckoutSession]); + + return ( +
+ {clientSecret && ( + + + + )} + {!clientSecret && ( +
+
+ Loading payment... +
+

Preparing payment...

+
+ )} +
+ ); +}; + +interface StripePaymentFormProps extends PaymentFormProps {} + +const StripePaymentForm: React.FC = (props) => { + return ( + + + + ); +}; + +export default StripePaymentForm; diff --git a/frontend/src/pages/RentItem.tsx b/frontend/src/pages/RentItem.tsx index 575e6af..6fbfe6f 100644 --- a/frontend/src/pages/RentItem.tsx +++ b/frontend/src/pages/RentItem.tsx @@ -81,29 +81,6 @@ const RentItem: React.FC = () => { calculateTotalCost(); }, [item, manualSelection]); - // Save rental data to localStorage whenever the form is ready - useEffect(() => { - if ( - item && - manualSelection.startDate && - manualSelection.endDate && - totalCost > 0 - ) { - const rentalData = { - itemId: item.id, - startDate: manualSelection.startDate, - endDate: manualSelection.endDate, - startTime: manualSelection.startTime, - endTime: manualSelection.endTime, - totalAmount: totalCost, - deliveryMethod: "pickup", - }; - - localStorage.setItem("pendingRental", JSON.stringify(rentalData)); - localStorage.setItem("lastItemId", item.id); - } - }, [item, manualSelection, totalCost]); - const fetchItem = async () => { try { const response = await itemAPI.getItem(id!); @@ -126,9 +103,6 @@ const RentItem: React.FC = () => { }; const handlePaymentSuccess = () => { - // This is called when Stripe checkout session is created successfully - // The rental data is already saved to localStorage via useEffect - // The actual rental creation happens in CheckoutReturn component after payment console.log("Stripe checkout session created successfully"); }; @@ -191,6 +165,15 @@ const RentItem: React.FC = () => { setError(error)} disabled={ diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 5d05941..1f50646 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -44,10 +44,10 @@ export const authAPI = { export const userAPI = { getProfile: () => api.get("/users/profile"), updateProfile: (data: any) => api.put("/users/profile", data), - uploadProfileImage: (formData: FormData) => + uploadProfileImage: (formData: FormData) => api.post("/users/profile/image", formData, { headers: { - 'Content-Type': 'multipart/form-data', + "Content-Type": "multipart/form-data", }, }), getPublicProfile: (id: string) => api.get(`/users/${id}`), @@ -58,7 +58,8 @@ export const userAPI = { export const addressAPI = { getAddresses: () => api.get("/users/addresses"), createAddress: (data: any) => api.post("/users/addresses", data), - updateAddress: (id: string, data: any) => api.put(`/users/addresses/${id}`, data), + updateAddress: (id: string, data: any) => + api.put(`/users/addresses/${id}`, data), deleteAddress: (id: string) => api.delete(`/users/addresses/${id}`), }; @@ -79,8 +80,10 @@ export const rentalAPI = { updateRentalStatus: (id: string, status: string) => api.put(`/rentals/${id}/status`, { status }), markAsCompleted: (id: string) => api.post(`/rentals/${id}/mark-completed`), - reviewRenter: (id: string, data: any) => api.post(`/rentals/${id}/review-renter`, data), - reviewItem: (id: string, data: any) => api.post(`/rentals/${id}/review-item`, data), + reviewRenter: (id: string, data: any) => + api.post(`/rentals/${id}/review-renter`, data), + reviewItem: (id: string, data: any) => + api.post(`/rentals/${id}/review-item`, data), addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data), // Legacy }; @@ -97,12 +100,33 @@ export const itemRequestAPI = { getItemRequests: (params?: any) => api.get("/item-requests", { params }), getItemRequest: (id: string) => api.get(`/item-requests/${id}`), createItemRequest: (data: any) => api.post("/item-requests", data), - updateItemRequest: (id: string, data: any) => api.put(`/item-requests/${id}`, data), + updateItemRequest: (id: string, data: any) => + api.put(`/item-requests/${id}`, data), deleteItemRequest: (id: string) => api.delete(`/item-requests/${id}`), getMyRequests: () => api.get("/item-requests/my-requests"), - respondToRequest: (id: string, data: any) => api.post(`/item-requests/${id}/responses`, data), + respondToRequest: (id: string, data: any) => + api.post(`/item-requests/${id}/responses`, data), updateResponseStatus: (responseId: string, status: string) => api.put(`/item-requests/responses/${responseId}/status`, { status }), }; +export const stripeAPI = { + createCheckoutSession: (data: { + itemName: string; + total: number; + return_url: string; + rentalData?: any; + }) => api.post("/stripe/create-checkout-session", data), + getCheckoutSession: (sessionId: string) => + api.get(`/stripe/checkout-session/${sessionId}`), + // createConnectedAccount: () => + // api.post("/stripe/accounts"), + // createAccountLink: (data: { refreshUrl: string; returnUrl: string }) => + // api.post("/stripe/account-links", data), + // getAccountStatus: () => + // api.get("/stripe/account-status"), + // createPaymentIntent: (data: { rentalId: string; amount: number }) => + // api.post("/stripe/payment-intents", data), +}; + export default api;