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 [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,

View File

@@ -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,15 +151,44 @@ 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">
{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 && ( {loading && (
<div className="text-center py-4"> <div className="text-center py-4">
<div className="spinner-border me-2" role="status"> <div className="spinner-border me-2" role="status">
@@ -206,12 +270,24 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
</form> </form>
</> </>
)} )}
</>
)}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">
{success ? (
<button
type="button"
className="btn btn-primary btn-lg"
onClick={handleClose}
>
Done
</button>
) : (
<>
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary"
onClick={onHide} onClick={handleClose}
disabled={processing} disabled={processing}
> >
Keep Rental Keep Rental
@@ -242,6 +318,8 @@ const RentalCancellationModal: React.FC<RentalCancellationModalProps> = ({
)} )}
</button> </button>
)} )}
</>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -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
) )
); );
@@ -159,11 +159,18 @@ const MyRentals: React.FC = () => {
? "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)}