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 { useNavigate, useSearchParams } from 'react-router-dom'
import FailIcon from '../components/FailIcon'
import InfoIcon from '../components/InfoIcon'
import PaymentFailModal from '../components/PaymentFailModal'
import SuccessIcon from '../components/SuccessIcon'
import { createReservation, ReservationResult } from '../services/campaignApi'
import { formatPersianNumber } from '../utils/formatNumber'
@ -26,62 +26,63 @@ export default function PaymentSuccess() {
const [searchParams] = useSearchParams()
const isSuccess = searchParams.get('state') === SUCCESS_STATE
const [reservation, setReservation] = useState<ReservationResult | null>(null)
const [error, setError] = useState('')
// gateway-reported failure, or a successful payment whose reservation couldn't be created
const isFail = !isSuccess || !!error
const [hasError, setHasError] = useState(false)
// gateway-reported failure, or a successful payment whose reservation couldn't be created —
// both land on the same fail modal
const isFail = !isSuccess || hasError
useEffect(() => {
if (!isSuccess) return
const pending = takePendingReservation()
if (!pending) {
Promise.resolve().then(() => setError('اطلاعات رزرو یافت نشد.'))
Promise.resolve().then(() => setHasError(true))
return
}
createReservation(pending)
.then(setReservation)
.catch(() => setError('ثبت رزرو با خطا مواجه شد.'))
.catch(() => setHasError(true))
}, [isSuccess])
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
<div className="w-full max-w-sm rounded-3xl bg-white p-6 shadow-lg">
{isFail ? <FailIcon /> : <SuccessIcon />}
<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">
<SuccessIcon />
{isSuccess ? (
reservation ? (
<>
<h2 className="mt-6 text-right font-bold text-gray-900">جزئیات رزرو</h2>
<div className="mt-2">
<DetailRow label="تعداد" value={`${formatPersianNumber(reservation.people_count)} نفر`} />
<DetailRow label="به نام" value={reservation.full_name} />
<DetailRow label="شماره تماس" value={fromE164(reservation.phone_number)} />
<DetailRow label="کد رزرو" value={reservation.reserve_code} />
</div>
{reservation ? (
<>
<h2 className="mt-6 text-right font-bold text-gray-900">جزئیات رزرو</h2>
<div className="mt-2">
<DetailRow label="تعداد" value={`${formatPersianNumber(reservation.people_count)} نفر`} />
<DetailRow label="به نام" value={reservation.full_name} />
<DetailRow label="شماره تماس" value={fromE164(reservation.phone_number)} />
<DetailRow label="کد رزرو" value={reservation.reserve_code} />
</div>
<div className="mt-4 flex items-center justify-end gap-2 rounded-2xl bg-[#ECF2F8] px-4 py-3">
<InfoIcon />
<p className="text-right text-sm text-gray-600">
کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات رزرو و کد ورود تا دقایقی دیگر برای شما پیامک می‌شود.
</p>
</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-900">پرداخت ناموفق بود. لطفاً دوباره تلاش کنید.</p>
)}
<div className="mt-4 flex items-center justify-end gap-2 rounded-2xl bg-[#ECF2F8] px-4 py-3">
<InfoIcon />
<p className="text-right text-sm text-gray-600">
کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات رزرو و کد ورود تا دقایقی دیگر برای شما پیامک می‌شود.
</p>
</div>
</>
) : (
<p className="mt-6 text-center text-gray-500">در حال ثبت رزرو...</p>
)}
<button
type="button"
onClick={() => navigate('/home')}
className="mt-4 w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
صفحه اصلی
</button>
</div>
<button
type="button"
onClick={() => navigate('/home')}
className="mt-4 w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
>
صفحه اصلی
</button>
</div>
</div>
)}
{isFail && <PaymentFailModal onGoHome={() => navigate('/home')} />}
</div>
)
}