feat: fail fallback on reservation flow added

This commit is contained in:
Afra ‌ 2026-06-28 16:38:27 +03:30
parent 14f3fc68e9
commit b4f90a7d46
2 changed files with 73 additions and 42 deletions

View file

@ -0,0 +1,30 @@
import { createPortal } from 'react-dom'
import FailIcon from './FailIcon'
interface PaymentFailModalProps {
onGoHome: () => void
}
// covers both a gateway-reported payment failure and a payment that succeeded but
// whose reservation couldn't be created — same message, same recovery action either way
export default function PaymentFailModal({ onGoHome }: PaymentFailModalProps) {
return createPortal(
<>
<div className="fixed inset-0 z-1100 bg-black/70" />
<div className="fixed inset-0 z-1100 flex items-center justify-center p-4">
<div className="w-full max-w-sm rounded-3xl bg-white p-6 text-center shadow-lg">
<FailIcon />
<p className="mt-6 text-gray-900">رزرو با خطا مواجه شد. لطفا دوباره تلاش کنید.</p>
<button
type="button"
onClick={onGoHome}
className="mt-4 w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
بازگشت به صفحه اصلی
</button>
</div>
</div>
</>,
document.body,
)
}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom' import { useNavigate, useSearchParams } from 'react-router-dom'
import FailIcon from '../components/FailIcon'
import InfoIcon from '../components/InfoIcon' import InfoIcon from '../components/InfoIcon'
import PaymentFailModal from '../components/PaymentFailModal'
import SuccessIcon from '../components/SuccessIcon' import SuccessIcon from '../components/SuccessIcon'
import { createReservation, ReservationResult } from '../services/campaignApi' import { createReservation, ReservationResult } from '../services/campaignApi'
import { formatPersianNumber } from '../utils/formatNumber' import { formatPersianNumber } from '../utils/formatNumber'
@ -26,29 +26,31 @@ export default function PaymentSuccess() {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const isSuccess = searchParams.get('state') === SUCCESS_STATE const isSuccess = searchParams.get('state') === SUCCESS_STATE
const [reservation, setReservation] = useState<ReservationResult | null>(null) const [reservation, setReservation] = useState<ReservationResult | null>(null)
const [error, setError] = useState('') const [hasError, setHasError] = useState(false)
// gateway-reported failure, or a successful payment whose reservation couldn't be created // gateway-reported failure, or a successful payment whose reservation couldn't be created —
const isFail = !isSuccess || !!error // both land on the same fail modal
const isFail = !isSuccess || hasError
useEffect(() => { useEffect(() => {
if (!isSuccess) return if (!isSuccess) return
const pending = takePendingReservation() const pending = takePendingReservation()
if (!pending) { if (!pending) {
Promise.resolve().then(() => setError('اطلاعات رزرو یافت نشد.')) Promise.resolve().then(() => setHasError(true))
return return
} }
createReservation(pending) createReservation(pending)
.then(setReservation) .then(setReservation)
.catch(() => setError('ثبت رزرو با خطا مواجه شد.')) .catch(() => setHasError(true))
}, [isSuccess]) }, [isSuccess])
return ( return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4"> <div className="min-h-screen bg-gray-50">
{!isFail && (
<div className="flex min-h-screen items-center justify-center p-4">
<div className="w-full max-w-sm rounded-3xl bg-white p-6 shadow-lg"> <div className="w-full max-w-sm rounded-3xl bg-white p-6 shadow-lg">
{isFail ? <FailIcon /> : <SuccessIcon />} <SuccessIcon />
{isSuccess ? ( {reservation ? (
reservation ? (
<> <>
<h2 className="mt-6 text-right font-bold text-gray-900">جزئیات رزرو</h2> <h2 className="mt-6 text-right font-bold text-gray-900">جزئیات رزرو</h2>
<div className="mt-2"> <div className="mt-2">
@ -65,13 +67,8 @@ export default function PaymentSuccess() {
</p> </p>
</div> </div>
</> </>
) : error ? (
<p className="mt-6 text-center text-gray-900">{error}</p>
) : ( ) : (
<p className="mt-6 text-center text-gray-500">در حال ثبت رزرو...</p> <p className="mt-6 text-center text-gray-500">در حال ثبت رزرو...</p>
)
) : (
<p className="mt-6 text-center text-gray-900">پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</p>
)} )}
<button <button
@ -83,5 +80,9 @@ export default function PaymentSuccess() {
</button> </button>
</div> </div>
</div> </div>
)}
{isFail && <PaymentFailModal onGoHome={() => navigate('/home')} />}
</div>
) )
} }