135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
import { loadStripe } from "@stripe/stripe-js";
|
|
import {
|
|
EmbeddedCheckoutProvider,
|
|
EmbeddedCheckout,
|
|
} from "@stripe/react-stripe-js";
|
|
import { stripeAPI, rentalAPI } from "../services/api";
|
|
|
|
const stripePromise = loadStripe(
|
|
process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY || ""
|
|
);
|
|
|
|
interface EmbeddedStripeCheckoutProps {
|
|
rentalData: any;
|
|
onSuccess: () => void;
|
|
onError: (error: string) => void;
|
|
}
|
|
|
|
const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
|
|
rentalData,
|
|
onSuccess,
|
|
onError,
|
|
}) => {
|
|
const [clientSecret, setClientSecret] = useState<string>("");
|
|
const [creating, setCreating] = useState(false);
|
|
const [sessionId, setSessionId] = useState<string>("");
|
|
const hasCreatedSession = useRef(false);
|
|
|
|
const createCheckoutSession = useCallback(async () => {
|
|
// Prevent multiple session creations
|
|
if (hasCreatedSession.current) return;
|
|
|
|
try {
|
|
setCreating(true);
|
|
hasCreatedSession.current = true;
|
|
|
|
const response = await stripeAPI.createSetupCheckoutSession({
|
|
rentalData
|
|
});
|
|
|
|
setClientSecret(response.data.clientSecret);
|
|
setSessionId(response.data.sessionId);
|
|
} catch (error: any) {
|
|
hasCreatedSession.current = false; // Reset on error so it can be retried
|
|
onError(
|
|
error.response?.data?.error || "Failed to create checkout session"
|
|
);
|
|
} finally {
|
|
setCreating(false);
|
|
}
|
|
}, [rentalData, onError]);
|
|
|
|
useEffect(() => {
|
|
createCheckoutSession();
|
|
}, [createCheckoutSession]);
|
|
|
|
const handleComplete = useCallback(() => {
|
|
// For embedded checkout, we need to retrieve the session to get payment method
|
|
(async () => {
|
|
try {
|
|
if (!sessionId) {
|
|
throw new Error("No session ID available");
|
|
}
|
|
|
|
// Get the completed checkout session
|
|
const sessionResponse = await stripeAPI.getCheckoutSession(sessionId);
|
|
const { status: sessionStatus, setup_intent } = sessionResponse.data;
|
|
|
|
if (sessionStatus !== "complete") {
|
|
throw new Error("Payment setup was not completed");
|
|
}
|
|
|
|
if (!setup_intent?.payment_method) {
|
|
throw new Error("No payment method found in setup intent");
|
|
}
|
|
|
|
// Extract payment method ID - handle both string ID and object cases
|
|
const paymentMethodId = typeof setup_intent.payment_method === 'string'
|
|
? setup_intent.payment_method
|
|
: setup_intent.payment_method.id;
|
|
|
|
if (!paymentMethodId) {
|
|
throw new Error("No payment method ID found");
|
|
}
|
|
|
|
// Create the rental with the payment method ID
|
|
const rentalPayload = {
|
|
...rentalData,
|
|
stripePaymentMethodId: paymentMethodId
|
|
};
|
|
|
|
await rentalAPI.createRental(rentalPayload);
|
|
onSuccess();
|
|
} catch (error: any) {
|
|
onError(error.response?.data?.error || error.message || "Failed to complete rental request");
|
|
}
|
|
})();
|
|
}, [sessionId, rentalData, onSuccess, onError]);
|
|
|
|
if (creating) {
|
|
return (
|
|
<div className="text-center py-4">
|
|
<div className="spinner-border mb-3" role="status">
|
|
<span className="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p>Preparing secure checkout...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!clientSecret) {
|
|
return (
|
|
<div className="text-center py-4">
|
|
<p className="text-muted">Unable to load checkout</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div id="embedded-checkout">
|
|
<EmbeddedCheckoutProvider
|
|
key={clientSecret} // Force remount if clientSecret changes
|
|
stripe={stripePromise}
|
|
options={{
|
|
clientSecret,
|
|
onComplete: handleComplete
|
|
}}
|
|
>
|
|
<EmbeddedCheckout />
|
|
</EmbeddedCheckoutProvider>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EmbeddedStripeCheckout; |