Files
rentall-app/frontend/src/components/UpdatePaymentMethod.tsx
2026-01-18 16:55:19 -05:00

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;