refund confirmation

This commit is contained in:
jackiettran
2025-09-04 18:02:01 -04:00
parent 1b3c8a9691
commit bbab991e31
3 changed files with 154 additions and 64 deletions

View File

@@ -24,10 +24,15 @@ const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
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
@@ -36,6 +41,7 @@ const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
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"
);
@@ -113,6 +119,7 @@ const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
return (
<div id="embedded-checkout">
<EmbeddedCheckoutProvider
key={clientSecret} // Force remount if clientSecret changes
stripe={stripePromise}
options={{
clientSecret,

View File

@@ -22,6 +22,12 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
const [processing, setProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [reason, setReason] = useState("");
const [success, setSuccess] = useState(false);
const [processedRefund, setProcessedRefund] = useState<{
amount: number;
refundId?: string;
} | null>(null);
const [updatedRental, setUpdatedRental] = useState<Rental | null>(null);
useEffect(() => {
if (show && rental) {
@@ -52,8 +58,19 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
setError(null);
const response = await rentalAPI.cancelRental(rental.id, reason.trim());
onCancellationComplete(response.data.rental);
onHide();
// Store refund details for confirmation screen
setProcessedRefund({
amount: refundPreview.refundAmount,
refundId: response.data.rental.stripeRefundId
});
// Store updated rental data for later callback
setUpdatedRental(response.data.rental);
// Show success confirmation instead of closing immediately
setSuccess(true);
// Don't call onCancellationComplete here - wait until user clicks "Done"
} catch (error: any) {
setError(error.response?.data?.error || "Failed to cancel rental");
} finally {
@@ -61,6 +78,24 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
}
};
const handleClose = () => {
// Call parent callback with updated rental data if we have it
if (updatedRental) {
onCancellationComplete(updatedRental);
}
// Reset all states when closing
setRefundPreview(null);
setLoading(false);
setProcessing(false);
setError(null);
setReason("");
setSuccess(false);
setProcessedRefund(null);
setUpdatedRental(null);
onHide();
};
const formatCurrency = (amount: number | string | undefined) => {
const numAmount = Number(amount) || 0;
return `$${numAmount.toFixed(2)}`;
@@ -75,19 +110,19 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
const handleBackdropClick = useCallback(
(e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onHide();
handleClose();
}
},
[onHide]
[handleClose]
);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === "Escape") {
onHide();
handleClose();
}
},
[onHide]
[handleClose]
);
useEffect(() => {
@@ -116,15 +151,44 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
<div className="modal-dialog modal-lg modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Cancel Rental</h5>
<h5 className="modal-title">
{success ? "Refund Confirmation" : "Cancel Rental"}
</h5>
<button
type="button"
className="btn-close"
onClick={onHide}
onClick={handleClose}
aria-label="Close"
></button>
</div>
<div className="modal-body">
{success && processedRefund ? (
<div className="text-center py-4">
<div className="mb-4">
<i className="bi bi-check-circle-fill text-success" style={{ fontSize: '4rem' }}></i>
</div>
<h3 className="text-success mb-3">Refund Processed Successfully!</h3>
<div className="alert alert-success mb-4">
<h5 className="mb-3">
<strong>{formatCurrency(processedRefund.amount)}</strong> has been refunded
</h5>
<div className="small text-muted">
<p className="mb-2">
<i className="bi bi-clock me-2"></i>
Your refund will appear in your payment method within <strong>3-5 business days</strong>
</p>
<p className="mb-0">
<i className="bi bi-credit-card me-2"></i>
Refund will be processed to your original payment method
</p>
</div>
</div>
<p className="text-muted mb-4">
Thank you for using our platform. We hope you'll rent with us again soon!
</p>
</div>
) : (
<>
{loading && (
<div className="text-center py-4">
<div className="spinner-border me-2" role="status">
@@ -206,12 +270,24 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
</form>
</>
)}
</>
)}
</div>
<div className="modal-footer">
{success ? (
<button
type="button"
className="btn btn-primary btn-lg"
onClick={handleClose}
>
Done
</button>
) : (
<>
<button
type="button"
className="btn btn-secondary"
onClick={onHide}
onClick={handleClose}
disabled={processing}
>
Keep Rental
@@ -242,6 +318,8 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
)}
</button>
)}
</>
)}
</div>
</div>
</div>

View File

@@ -52,8 +52,8 @@ const MyRentals: React.FC = () => {
const handleCancellationComplete = (updatedRental: Rental) => {
// Update the rental in the list
setRentals(prev =>
prev.map(rental =>
setRentals((prev) =>
prev.map((rental) =>
rental.id === updatedRental.id ? updatedRental : rental
)
);
@@ -159,11 +159,18 @@ const MyRentals: React.FC = () => {
? "Awaiting Owner Approval"
: rental.status === "confirmed"
? "Confirmed & Paid"
: rental.status.charAt(0).toUpperCase() + rental.status.slice(1)
}
: rental.status.charAt(0).toUpperCase() +
rental.status.slice(1)}
</span>
</div>
{rental.status === "pending" && (
<div className="alert alert-info mt-2 mb-2 p-2 small">
You'll only be charged if the owner approves your
request.
</div>
)}
<p className="mb-1 text-dark">
<strong>Rental Period:</strong>
<br />
@@ -185,14 +192,6 @@ const MyRentals: React.FC = () => {
</p>
)}
{rental.status === "pending" && (
<div className="alert alert-info mt-2 mb-2 p-2 small">
<i className="bi bi-clock me-2"></i>
<strong>Awaiting Approval:</strong> Your payment method is saved.
You'll only be charged if the owner approves your request.
</div>
)}
{rental.renterPrivateMessage &&
rental.renterReviewVisible && (
<div className="alert alert-info mt-2 mb-2 p-2 small">
@@ -222,11 +221,16 @@ const MyRentals: React.FC = () => {
${rental.refundAmount?.toFixed(2) || "0.00"}
{rental.refundProcessedAt && (
<small className="d-block text-muted mt-1">
Processed: {new Date(rental.refundProcessedAt).toLocaleDateString()}
Processed:{" "}
{new Date(
rental.refundProcessedAt
).toLocaleDateString()}
</small>
)}
{rental.refundReason && (
<small className="d-block mt-1">{rental.refundReason}</small>
<small className="d-block mt-1">
{rental.refundReason}
</small>
)}
</div>
)}
@@ -234,7 +238,8 @@ const MyRentals: React.FC = () => {
)}
<div className="d-flex gap-2 mt-3">
{(rental.status === "pending" || rental.status === "confirmed") && (
{(rental.status === "pending" ||
rental.status === "confirmed") && (
<button
className="btn btn-sm btn-danger"
onClick={() => handleCancelClick(rental)}