fix: booking widget payment flow and success message
- On API error: show error message instead of false success screen - Success message: "Оплата прошла успешно" only when paidViaGateway=true (user actually clicked pay in YooKassa); otherwise "Бронирование принято. Менеджер свяжется с вами" or "Оплата на месте" - Payment screen: simplified to YooKassa-only redirect (removed bank transfer variant) - Removed bankDetails from WidgetSettings entirely (not needed per product decision) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1680,7 +1680,6 @@ export interface WidgetConfig {
|
||||
allowExtraBeds: boolean
|
||||
allowChildren: boolean
|
||||
hotelName: string
|
||||
bankDetails: string
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,6 @@ export interface WidgetSettings {
|
||||
showPromo: boolean
|
||||
allowExtraBeds: boolean
|
||||
allowChildren: boolean
|
||||
bankDetails: string
|
||||
formFields: FormField[]
|
||||
additionalServices: AdditionalService[]
|
||||
}
|
||||
@@ -216,6 +215,7 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
// Hourly service time selections: { serviceId: { date, timeFrom, timeTo } }
|
||||
const [serviceSchedule, setServiceSchedule] = useState<Record<string, { date: string; timeFrom: string; timeTo: string }>>({})
|
||||
const [bookingError, setBookingError] = useState<string | null>(null)
|
||||
const [paidViaGateway, setPaidViaGateway] = useState(false)
|
||||
// Guest lookup
|
||||
const [guestLookupTimer, setGuestLookupTimer] = useState<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [guestSuggestion, setGuestSuggestion] = useState<{ first_name: string; last_name: string; middle_name?: string; phone?: string; email?: string } | null>(null)
|
||||
@@ -291,9 +291,6 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
if (result.confirmationUrl) {
|
||||
setConfirmUrl(result.confirmationUrl)
|
||||
setStep('payment')
|
||||
} else if (settings.bankDetails || needsPayment) {
|
||||
// YooKassa failed or bank transfer configured — always show payment screen
|
||||
setStep('payment')
|
||||
} else {
|
||||
setStep('success')
|
||||
}
|
||||
@@ -301,14 +298,14 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
if (err?.status === 403) {
|
||||
setBookingError(err?.error ?? 'Невозможно завершить бронирование. Попробуйте позже или свяжитесь с отелем.')
|
||||
} else {
|
||||
setStep('success')
|
||||
setBookingError('Ошибка при создании бронирования. Попробуйте ещё раз или свяжитесь с отелем.')
|
||||
}
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
}
|
||||
} else {
|
||||
// Preview mode without real slug
|
||||
if (needsPayment || settings.bankDetails) {
|
||||
if (needsPayment) {
|
||||
setStep('payment')
|
||||
} else {
|
||||
setStep('success')
|
||||
@@ -318,6 +315,7 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
|
||||
const handlePay = () => {
|
||||
if (confirmUrl) {
|
||||
setPaidViaGateway(true)
|
||||
window.location.href = confirmUrl
|
||||
} else {
|
||||
setStep('success')
|
||||
@@ -332,6 +330,8 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
setServiceSchedule({})
|
||||
setBookingError(null)
|
||||
setGuestSuggestion(null)
|
||||
setPaidViaGateway(false)
|
||||
setConfirmUrl(null)
|
||||
}
|
||||
|
||||
const doGuestLookup = (email?: string, phone?: string) => {
|
||||
@@ -357,9 +357,11 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
</div>
|
||||
<p className="text-lg font-bold text-slate-900">Бронирование принято!</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
{needsPayment
|
||||
{paidViaGateway
|
||||
? 'Оплата прошла успешно. Подтверждение придёт на email.'
|
||||
: 'Оплата на месте при заезде. Подтверждение придёт на email.'}
|
||||
: needsPayment
|
||||
? 'Бронирование принято. Менеджер свяжется с вами для оплаты.'
|
||||
: 'Оплата на месте при заезде. Подтверждение придёт на email.'}
|
||||
</p>
|
||||
<div className="bg-slate-50 rounded-xl p-4 text-left space-y-1">
|
||||
<p className="text-xs text-slate-500">{isCategoryMode ? 'Категория' : 'Номер'}: <span className="font-medium text-slate-700">{selectedName}</span></p>
|
||||
@@ -379,11 +381,8 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
)
|
||||
}
|
||||
|
||||
// ── Payment screen ──
|
||||
// ── Payment screen (YooKassa redirect) ──
|
||||
if (step === 'payment') {
|
||||
const isYookassa = !!confirmUrl
|
||||
const hasBankDetails = !isYookassa && !!settings.bankDetails
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-2xl overflow-hidden border border-slate-200" style={{ fontFamily: 'Inter, sans-serif' }}>
|
||||
<div className="px-6 py-4 text-white flex items-center gap-3" style={{ background: settings.primaryColor }}>
|
||||
@@ -397,77 +396,33 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-4">
|
||||
{/* Amount */}
|
||||
<div className="bg-slate-50 rounded-xl p-3 flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">{selectedName} · {nights} ноч.</span>
|
||||
<span className="text-base font-bold text-slate-900">{grandTotal.toLocaleString('ru-RU')} ₽</span>
|
||||
</div>
|
||||
|
||||
{isYookassa ? (
|
||||
/* YooKassa redirect */
|
||||
<div className="text-center space-y-3 py-2">
|
||||
<div className="w-14 h-14 rounded-full flex items-center justify-center mx-auto bg-slate-100">
|
||||
<CreditCard size={28} className="text-slate-500" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 font-medium">
|
||||
{settings.language === 'ru' ? 'Для оплаты вы будете перенаправлены на страницу ЮKassa' : 'You will be redirected to YooKassa to complete payment'}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">🔒 Защищённое соединение · ЮKassa</p>
|
||||
<div className="text-center space-y-3 py-2">
|
||||
<div className="w-14 h-14 rounded-full flex items-center justify-center mx-auto bg-slate-100">
|
||||
<CreditCard size={28} className="text-slate-500" />
|
||||
</div>
|
||||
) : hasBankDetails ? (
|
||||
/* Bank transfer details */
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-medium text-slate-700">
|
||||
{settings.language === 'ru' ? 'Реквизиты для оплаты' : 'Payment details'}
|
||||
</p>
|
||||
<div className="bg-slate-50 rounded-xl p-4 text-sm text-slate-700 whitespace-pre-wrap leading-relaxed">
|
||||
{settings.bankDetails}
|
||||
</div>
|
||||
<p className="text-xs text-slate-400">
|
||||
{settings.language === 'ru'
|
||||
? 'После перевода отель подтвердит бронирование. Укажите в назначении платежа вашу фамилию и даты.'
|
||||
: 'After transfer, the hotel will confirm your booking. Include your name and dates in the payment reference.'}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
/* No payment configured — pending confirmation */
|
||||
<div className="text-center space-y-3 py-2">
|
||||
<div className="w-14 h-14 rounded-full flex items-center justify-center mx-auto bg-amber-50">
|
||||
<AlertCircle size={28} className="text-amber-500" />
|
||||
</div>
|
||||
<p className="text-sm text-slate-700 font-medium">
|
||||
{settings.language === 'ru' ? 'Бронирование принято. Менеджер отеля свяжется с вами для подтверждения оплаты.' : 'Booking received. Hotel manager will contact you to confirm payment.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-slate-700 font-medium">
|
||||
{settings.language === 'ru'
|
||||
? 'Для завершения бронирования перейдите к оплате'
|
||||
: 'Please proceed to payment to confirm your booking'}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400">🔒 Защищённое соединение · ЮKassa</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-5 pb-5">
|
||||
{isYookassa ? (
|
||||
<button
|
||||
onClick={handlePay}
|
||||
className="w-full py-3 rounded-xl text-white font-semibold text-sm transition-opacity hover:opacity-90"
|
||||
style={{ background: settings.primaryColor }}
|
||||
>
|
||||
{settings.language === 'ru' ? `Перейти к оплате · ${grandTotal.toLocaleString('ru-RU')} ₽` : `Proceed to pay · ${grandTotal.toLocaleString('ru-RU')} ₽`}
|
||||
</button>
|
||||
) : hasBankDetails ? (
|
||||
<button
|
||||
onClick={() => setStep('success')}
|
||||
className="w-full py-3 rounded-xl text-white font-semibold text-sm transition-opacity hover:opacity-90"
|
||||
style={{ background: settings.primaryColor }}
|
||||
>
|
||||
{settings.language === 'ru' ? 'Я перевёл оплату' : 'I have paid'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setStep('success')}
|
||||
className="w-full py-3 rounded-xl text-white font-semibold text-sm transition-opacity hover:opacity-90"
|
||||
style={{ background: settings.primaryColor }}
|
||||
>
|
||||
{settings.language === 'ru' ? 'Понятно' : 'OK'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handlePay}
|
||||
className="w-full py-3 rounded-xl text-white font-semibold text-sm transition-opacity hover:opacity-90"
|
||||
style={{ background: settings.primaryColor }}
|
||||
>
|
||||
{settings.language === 'ru'
|
||||
? `Оплатить · ${grandTotal.toLocaleString('ru-RU')} ₽`
|
||||
: `Pay · ${grandTotal.toLocaleString('ru-RU')} ₽`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -696,11 +651,11 @@ export function WidgetPreview({ settings, slug, realRooms, realCategories, payme
|
||||
style={{ background: settings.primaryColor }}
|
||||
>
|
||||
{submitting && <Loader2 size={14} className="animate-spin" />}
|
||||
{submitting ? 'Отправляем...' : (needsPayment || settings.bankDetails)
|
||||
{submitting ? 'Отправляем...' : needsPayment
|
||||
? (settings.language === 'ru' ? `Перейти к оплате · ${grandTotal.toLocaleString('ru-RU')} ₽` : `Proceed to payment · ${grandTotal.toLocaleString('ru-RU')} ₽`)
|
||||
: (settings.language === 'ru' ? `Подтвердить · ${grandTotal.toLocaleString('ru-RU')} ₽` : `Confirm · ${grandTotal.toLocaleString('ru-RU')} ₽`)}
|
||||
</button>
|
||||
{!needsPayment && !settings.bankDetails && (
|
||||
{!needsPayment && (
|
||||
<p className="text-center text-xs text-slate-400">Оплата на месте при заезде</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1188,7 +1143,6 @@ export function BookingWidgetPage() {
|
||||
showPromo: (hs as any).widgetShowPromo !== undefined ? Boolean((hs as any).widgetShowPromo) : prev.showPromo,
|
||||
allowExtraBeds: (hs as any).widgetExtraBeds !== undefined ? Boolean((hs as any).widgetExtraBeds) : prev.allowExtraBeds,
|
||||
allowChildren: (hs as any).widgetChildren !== undefined ? Boolean((hs as any).widgetChildren) : prev.allowChildren,
|
||||
bankDetails: String((hs as any).widgetBankDetails ?? prev.bankDetails ?? ''),
|
||||
}))
|
||||
setSettingsReady(true)
|
||||
}).finally(() => setGatewayLoading(false))
|
||||
@@ -1206,7 +1160,6 @@ export function BookingWidgetPage() {
|
||||
showPromo: true,
|
||||
allowExtraBeds: true,
|
||||
allowChildren: true,
|
||||
bankDetails: '',
|
||||
formFields: DEFAULT_FORM_FIELDS,
|
||||
additionalServices: DEFAULT_SERVICES,
|
||||
})
|
||||
@@ -1236,7 +1189,6 @@ export function BookingWidgetPage() {
|
||||
widget_show_promo: s.showPromo,
|
||||
widget_extra_beds: s.allowExtraBeds,
|
||||
widget_children: s.allowChildren,
|
||||
widget_bank_details: s.bankDetails,
|
||||
})
|
||||
setSaved(true)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
@@ -1576,24 +1528,6 @@ export function BookingWidgetPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bank transfer details */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<CreditCard size={14} className="text-slate-400" />
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">Реквизиты для оплаты переводом</p>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||
Если ЮKassa не подключена, гость увидит эти реквизиты после бронирования. Оставьте пустым если оплата на месте.
|
||||
</p>
|
||||
<textarea
|
||||
rows={4}
|
||||
className="input w-full text-sm resize-none font-mono"
|
||||
placeholder={'Номер карты: 4276 1234 5678 9012\nПолучатель: Иванов Иван Иванович\nБанк: Сбербанк\nКомментарий: укажите даты и фамилию'}
|
||||
value={settings.bankDetails}
|
||||
onChange={e => set('bankDetails', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Save button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
|
||||
@@ -23,7 +23,6 @@ export function BookingWidgetStandalonePage() {
|
||||
showPromo: searchParams.get('promo') !== 'false',
|
||||
allowExtraBeds: searchParams.get('extra-beds') !== 'false',
|
||||
allowChildren: searchParams.get('children') !== 'false',
|
||||
bankDetails: '',
|
||||
formFields: DEFAULT_FORM_FIELDS,
|
||||
additionalServices: DEFAULT_SERVICES,
|
||||
})
|
||||
@@ -48,7 +47,6 @@ export function BookingWidgetStandalonePage() {
|
||||
showPromo: ws.showPromo ?? prev.showPromo,
|
||||
allowExtraBeds: ws.allowExtraBeds ?? prev.allowExtraBeds,
|
||||
allowChildren: ws.allowChildren ?? prev.allowChildren,
|
||||
bankDetails: ws.bankDetails ?? prev.bankDetails,
|
||||
}))
|
||||
} else {
|
||||
setSettings(prev => ({ ...prev, hotelName: config.hotelName ?? prev.hotelName }))
|
||||
|
||||
Reference in New Issue
Block a user