failed payment method handling
This commit is contained in:
190
frontend/src/components/UpdatePaymentMethod.tsx
Normal file
190
frontend/src/components/UpdatePaymentMethod.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
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 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;
|
||||
Reference in New Issue
Block a user