191 lines
5.3 KiB
TypeScript
191 lines
5.3 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(
|
|
import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || ""
|
|
);
|
|
|
|
interface UpdatePaymentMethodProps {
|
|
rentalId: string;
|
|
itemName: string;
|
|
onSuccess: () => void;
|
|
onError: (error: string) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
const UpdatePaymentMethod: React.FC<UpdatePaymentMethodProps> = ({
|
|
rentalId,
|
|
itemName,
|
|
onSuccess,
|
|
onError,
|
|
onCancel,
|
|
}) => {
|
|
const [clientSecret, setClientSecret] = useState<string>("");
|
|
const [creating, setCreating] = useState(false);
|
|
const [sessionId, setSessionId] = useState<string>("");
|
|
const [updating, setUpdating] = useState(false);
|
|
const hasCreatedSession = useRef(false);
|
|
|
|
// Use refs to avoid recreating handleComplete when props change
|
|
const rentalIdRef = useRef(rentalId);
|
|
const onSuccessRef = useRef(onSuccess);
|
|
const onErrorRef = useRef(onError);
|
|
const sessionIdRef = useRef(sessionId);
|
|
|
|
// Keep refs up to date
|
|
rentalIdRef.current = rentalId;
|
|
onSuccessRef.current = onSuccess;
|
|
onErrorRef.current = onError;
|
|
sessionIdRef.current = sessionId;
|
|
|
|
const createCheckoutSession = useCallback(async () => {
|
|
// Prevent multiple session creations
|
|
if (hasCreatedSession.current) return;
|
|
|
|
try {
|
|
setCreating(true);
|
|
hasCreatedSession.current = true;
|
|
|
|
// Create a setup checkout session without rental data
|
|
// (we're updating an existing rental, not creating a new one)
|
|
const response = await stripeAPI.createSetupCheckoutSession({});
|
|
|
|
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);
|
|
}
|
|
}, [onError]);
|
|
|
|
useEffect(() => {
|
|
createCheckoutSession();
|
|
}, [createCheckoutSession]);
|
|
|
|
// Use useCallback with empty deps - refs provide access to latest values
|
|
const handleComplete = useCallback(() => {
|
|
(async () => {
|
|
try {
|
|
setUpdating(true);
|
|
|
|
if (!sessionIdRef.current) {
|
|
throw new Error("No session ID available");
|
|
}
|
|
|
|
// Get the completed checkout session
|
|
const sessionResponse = await stripeAPI.getCheckoutSession(
|
|
sessionIdRef.current
|
|
);
|
|
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");
|
|
}
|
|
|
|
// Update the rental's payment method
|
|
await rentalAPI.updatePaymentMethod(rentalIdRef.current, paymentMethodId);
|
|
|
|
onSuccessRef.current();
|
|
} catch (error: any) {
|
|
onErrorRef.current(
|
|
error.response?.data?.error ||
|
|
error.message ||
|
|
"Failed to update payment method"
|
|
);
|
|
} finally {
|
|
setUpdating(false);
|
|
}
|
|
})();
|
|
}, []);
|
|
|
|
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 (updating) {
|
|
return (
|
|
<div className="text-center py-4">
|
|
<div className="spinner-border mb-3" role="status">
|
|
<span className="visually-hidden">Updating...</span>
|
|
</div>
|
|
<p>Updating payment method...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!clientSecret) {
|
|
return (
|
|
<div className="text-center py-4">
|
|
<p className="text-muted">Unable to load checkout</p>
|
|
<button className="btn btn-secondary mt-2" onClick={onCancel}>
|
|
Go Back
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="update-payment-method">
|
|
<div className="mb-4">
|
|
<h5>Update Payment Method</h5>
|
|
<p className="text-muted">
|
|
Update your payment method for <strong>{itemName}</strong>. Once
|
|
updated, the owner will be notified and can re-attempt to approve your
|
|
rental request.
|
|
</p>
|
|
</div>
|
|
|
|
<div id="embedded-checkout">
|
|
<EmbeddedCheckoutProvider
|
|
key={clientSecret}
|
|
stripe={stripePromise}
|
|
options={{
|
|
clientSecret,
|
|
onComplete: handleComplete,
|
|
}}
|
|
>
|
|
<EmbeddedCheckout />
|
|
</EmbeddedCheckoutProvider>
|
|
</div>
|
|
|
|
<div className="mt-3 text-center">
|
|
<button className="btn btn-link text-muted" onClick={onCancel}>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UpdatePaymentMethod;
|