feat: fail fallback on reservation flow added
This commit is contained in:
parent
14f3fc68e9
commit
b4f90a7d46
2 changed files with 73 additions and 42 deletions
30
src/components/PaymentFailModal.tsx
Normal file
30
src/components/PaymentFailModal.tsx
Normal 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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,62 +26,63 @@ 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">
|
||||||
<div className="w-full max-w-sm rounded-3xl bg-white p-6 shadow-lg">
|
{!isFail && (
|
||||||
{isFail ? <FailIcon /> : <SuccessIcon />}
|
<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 ? (
|
||||||
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">
|
<DetailRow label="تعداد" value={`${formatPersianNumber(reservation.people_count)} نفر`} />
|
||||||
<DetailRow label="تعداد" value={`${formatPersianNumber(reservation.people_count)} نفر`} />
|
<DetailRow label="به نام" value={reservation.full_name} />
|
||||||
<DetailRow label="به نام" value={reservation.full_name} />
|
<DetailRow label="شماره تماس" value={fromE164(reservation.phone_number)} />
|
||||||
<DetailRow label="شماره تماس" value={fromE164(reservation.phone_number)} />
|
<DetailRow label="کد رزرو" value={reservation.reserve_code} />
|
||||||
<DetailRow label="کد رزرو" value={reservation.reserve_code} />
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 flex items-center justify-end gap-2 rounded-2xl bg-[#ECF2F8] px-4 py-3">
|
<div className="mt-4 flex items-center justify-end gap-2 rounded-2xl bg-[#ECF2F8] px-4 py-3">
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
<p className="text-right text-sm text-gray-600">
|
<p className="text-right text-sm text-gray-600">
|
||||||
کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات رزرو و کد ورود تا دقایقی دیگر برای شما پیامک میشود.
|
کد رزرو را برای ورود نزد خود نگه دارید. اطلاعات رزرو و کد ورود تا دقایقی دیگر برای شما پیامک میشود.
|
||||||
</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
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate('/home')}
|
onClick={() => navigate('/home')}
|
||||||
className="mt-4 w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
|
className="mt-4 w-full rounded-2xl bg-[#223D5D] py-4 text-lg font-semibold text-white"
|
||||||
>
|
>
|
||||||
صفحه اصلی
|
صفحه اصلی
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFail && <PaymentFailModal onGoHome={() => navigate('/home')} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue