refund confirmation
This commit is contained in:
@@ -24,10 +24,15 @@ const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
|
|||||||
const [clientSecret, setClientSecret] = useState<string>("");
|
const [clientSecret, setClientSecret] = useState<string>("");
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const [sessionId, setSessionId] = useState<string>("");
|
const [sessionId, setSessionId] = useState<string>("");
|
||||||
|
const hasCreatedSession = useRef(false);
|
||||||
|
|
||||||
const createCheckoutSession = useCallback(async () => {
|
const createCheckoutSession = useCallback(async () => {
|
||||||
|
// Prevent multiple session creations
|
||||||
|
if (hasCreatedSession.current) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
|
hasCreatedSession.current = true;
|
||||||
|
|
||||||
const response = await stripeAPI.createSetupCheckoutSession({
|
const response = await stripeAPI.createSetupCheckoutSession({
|
||||||
rentalData
|
rentalData
|
||||||
@@ -36,6 +41,7 @@ const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
|
|||||||
setClientSecret(response.data.clientSecret);
|
setClientSecret(response.data.clientSecret);
|
||||||
setSessionId(response.data.sessionId);
|
setSessionId(response.data.sessionId);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
hasCreatedSession.current = false; // Reset on error so it can be retried
|
||||||
onError(
|
onError(
|
||||||
error.response?.data?.error || "Failed to create checkout session"
|
error.response?.data?.error || "Failed to create checkout session"
|
||||||
);
|
);
|
||||||
@@ -113,6 +119,7 @@ const EmbeddedStripeCheckout: React.FC<EmbeddedStripeCheckoutProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div id="embedded-checkout">
|
<div id="embedded-checkout">
|
||||||
<EmbeddedCheckoutProvider
|
<EmbeddedCheckoutProvider
|
||||||
|
key={clientSecret} // Force remount if clientSecret changes
|
||||||
stripe={stripePromise}
|
stripe={stripePromise}
|
||||||
options={{
|
options={{
|
||||||
clientSecret,
|
clientSecret,
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
|
|||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [reason, setReason] = useState("");
|
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(() => {
|
useEffect(() => {
|
||||||
if (show && rental) {
|
if (show && rental) {
|
||||||
@@ -52,8 +58,19 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const response = await rentalAPI.cancelRental(rental.id, reason.trim());
|
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) {
|
} catch (error: any) {
|
||||||
setError(error.response?.data?.error || "Failed to cancel rental");
|
setError(error.response?.data?.error || "Failed to cancel rental");
|
||||||
} finally {
|
} 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 formatCurrency = (amount: number | string | undefined) => {
|
||||||
const numAmount = Number(amount) || 0;
|
const numAmount = Number(amount) || 0;
|
||||||
return `$${numAmount.toFixed(2)}`;
|
return `$${numAmount.toFixed(2)}`;
|
||||||
@@ -75,19 +110,19 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
|
|||||||
const handleBackdropClick = useCallback(
|
const handleBackdropClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
onHide();
|
handleClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onHide]
|
[handleClose]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
onHide();
|
handleClose();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onHide]
|
[handleClose]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -116,31 +151,60 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
|
|||||||
<div className="modal-dialog modal-lg modal-dialog-centered">
|
<div className="modal-dialog modal-lg modal-dialog-centered">
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h5 className="modal-title">Cancel Rental</h5>
|
<h5 className="modal-title">
|
||||||
|
{success ? "Refund Confirmation" : "Cancel Rental"}
|
||||||
|
</h5>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn-close"
|
className="btn-close"
|
||||||
onClick={onHide}
|
onClick={handleClose}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body">
|
<div className="modal-body">
|
||||||
{loading && (
|
{success && processedRefund ? (
|
||||||
<div className="text-center py-4">
|
<div className="text-center py-4">
|
||||||
<div className="spinner-border me-2" role="status">
|
<div className="mb-4">
|
||||||
<span className="visually-hidden">Loading...</span>
|
<i className="bi bi-check-circle-fill text-success" style={{ fontSize: '4rem' }}></i>
|
||||||
</div>
|
</div>
|
||||||
Calculating refund...
|
<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>
|
</div>
|
||||||
)}
|
) : (
|
||||||
|
<>
|
||||||
|
{loading && (
|
||||||
|
<div className="text-center py-4">
|
||||||
|
<div className="spinner-border me-2" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
Calculating refund...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger mb-3" role="alert">
|
<div className="alert alert-danger mb-3" role="alert">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{refundPreview && !loading && (
|
{refundPreview && !loading && (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h5>Rental Details</h5>
|
<h5>Rental Details</h5>
|
||||||
@@ -206,41 +270,55 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
|
|||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-footer">
|
<div className="modal-footer">
|
||||||
<button
|
{success ? (
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={onHide}
|
|
||||||
disabled={processing}
|
|
||||||
>
|
|
||||||
Keep Rental
|
|
||||||
</button>
|
|
||||||
{refundPreview && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-danger"
|
className="btn btn-primary btn-lg"
|
||||||
onClick={handleCancel}
|
onClick={handleClose}
|
||||||
disabled={processing || loading}
|
|
||||||
>
|
>
|
||||||
{processing ? (
|
Done
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="spinner-border spinner-border-sm me-2"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
Processing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
`Cancel with ${
|
|
||||||
refundPreview.refundAmount > 0
|
|
||||||
? `Refund ${formatCurrency(refundPreview.refundAmount)}`
|
|
||||||
: "No Refund"
|
|
||||||
}`
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={processing}
|
||||||
|
>
|
||||||
|
Keep Rental
|
||||||
|
</button>
|
||||||
|
{refundPreview && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-danger"
|
||||||
|
onClick={handleCancel}
|
||||||
|
disabled={processing || loading}
|
||||||
|
>
|
||||||
|
{processing ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="spinner-border spinner-border-sm me-2"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
`Cancel with ${
|
||||||
|
refundPreview.refundAmount > 0
|
||||||
|
? `Refund ${formatCurrency(refundPreview.refundAmount)}`
|
||||||
|
: "No Refund"
|
||||||
|
}`
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ const MyRentals: React.FC = () => {
|
|||||||
|
|
||||||
const handleCancellationComplete = (updatedRental: Rental) => {
|
const handleCancellationComplete = (updatedRental: Rental) => {
|
||||||
// Update the rental in the list
|
// Update the rental in the list
|
||||||
setRentals(prev =>
|
setRentals((prev) =>
|
||||||
prev.map(rental =>
|
prev.map((rental) =>
|
||||||
rental.id === updatedRental.id ? updatedRental : rental
|
rental.id === updatedRental.id ? updatedRental : rental
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -155,15 +155,22 @@ const MyRentals: React.FC = () => {
|
|||||||
: "bg-danger"
|
: "bg-danger"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{rental.status === "pending"
|
{rental.status === "pending"
|
||||||
? "Awaiting Owner Approval"
|
? "Awaiting Owner Approval"
|
||||||
: rental.status === "confirmed"
|
: rental.status === "confirmed"
|
||||||
? "Confirmed & Paid"
|
? "Confirmed & Paid"
|
||||||
: rental.status.charAt(0).toUpperCase() + rental.status.slice(1)
|
: rental.status.charAt(0).toUpperCase() +
|
||||||
}
|
rental.status.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<p className="mb-1 text-dark">
|
||||||
<strong>Rental Period:</strong>
|
<strong>Rental Period:</strong>
|
||||||
<br />
|
<br />
|
||||||
@@ -185,14 +192,6 @@ const MyRentals: React.FC = () => {
|
|||||||
</p>
|
</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.renterPrivateMessage &&
|
||||||
rental.renterReviewVisible && (
|
rental.renterReviewVisible && (
|
||||||
<div className="alert alert-info mt-2 mb-2 p-2 small">
|
<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.refundAmount?.toFixed(2) || "0.00"}
|
||||||
{rental.refundProcessedAt && (
|
{rental.refundProcessedAt && (
|
||||||
<small className="d-block text-muted mt-1">
|
<small className="d-block text-muted mt-1">
|
||||||
Processed: {new Date(rental.refundProcessedAt).toLocaleDateString()}
|
Processed:{" "}
|
||||||
|
{new Date(
|
||||||
|
rental.refundProcessedAt
|
||||||
|
).toLocaleDateString()}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
{rental.refundReason && (
|
{rental.refundReason && (
|
||||||
<small className="d-block mt-1">{rental.refundReason}</small>
|
<small className="d-block mt-1">
|
||||||
|
{rental.refundReason}
|
||||||
|
</small>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -234,7 +238,8 @@ const MyRentals: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="d-flex gap-2 mt-3">
|
<div className="d-flex gap-2 mt-3">
|
||||||
{(rental.status === "pending" || rental.status === "confirmed") && (
|
{(rental.status === "pending" ||
|
||||||
|
rental.status === "confirmed") && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-danger"
|
className="btn btn-sm btn-danger"
|
||||||
onClick={() => handleCancelClick(rental)}
|
onClick={() => handleCancelClick(rental)}
|
||||||
|
|||||||
Reference in New Issue
Block a user