Merged in fix/SW-2462-room-availability-error (pull request #1920)

Fix/SW-2462 room availability error

* fix: added toast error when availability fails and you get redirect to select-rate

* fix: added support for showing alert when availability error happens

* fix: rename PaymentAlert -> BookingAlert


Approved-by: Erik Tiekstra
This commit is contained in:
Tobias Johansson
2025-05-02 08:35:29 +00:00
parent 862d4abbe3
commit bf79168216
6 changed files with 109 additions and 43 deletions

View File

@@ -1,6 +1,7 @@
import { notFound, redirect } from "next/navigation" import { notFound, redirect } from "next/navigation"
import { Suspense } from "react" import { Suspense } from "react"
import { BookingErrorCodeEnum } from "@/constants/booking"
import { selectRate } from "@/constants/routes/hotelReservation" import { selectRate } from "@/constants/routes/hotelReservation"
import { import {
getBreakfastPackages, getBreakfastPackages,
@@ -16,8 +17,6 @@ import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop" import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile" import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDetails/Tracking" import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDetails/Tracking"
import Alert from "@/components/TempDesignSystem/Alert"
import { getIntl } from "@/i18n"
import RoomProvider from "@/providers/Details/RoomProvider" import RoomProvider from "@/providers/Details/RoomProvider"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider" import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
import { convertSearchParamsToObj } from "@/utils/url" import { convertSearchParamsToObj } from "@/utils/url"
@@ -25,7 +24,6 @@ import { convertSearchParamsToObj } from "@/utils/url"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
import type { Room } from "@/types/providers/details/room" import type { Room } from "@/types/providers/details/room"
@@ -71,6 +69,7 @@ export default async function DetailsPage({
// (possibly also add an error case to url?) // (possibly also add an error case to url?)
// ------------------------------------------------------- // -------------------------------------------------------
// redirect back to select-rate if availability call fails // redirect back to select-rate if availability call fails
selectRoomParams.set("errorCode", BookingErrorCodeEnum.AvailabilityError)
redirect(`${selectRate(lang)}?${selectRoomParams.toString()}`) redirect(`${selectRate(lang)}?${selectRoomParams.toString()}`)
} }
@@ -94,12 +93,9 @@ export default async function DetailsPage({
hotel.merchantInformationData.alternatePaymentOptions = [] hotel.merchantInformationData.alternatePaymentOptions = []
} }
const intl = await getIntl()
const firstRoom = rooms[0] const firstRoom = rooms[0]
const multirooms = rooms.slice(1) const multirooms = rooms.slice(1)
const isRoomNotAvailable = rooms.some((room) => !room.isAvailable)
return ( return (
<EnterDetailsProvider <EnterDetailsProvider
booking={booking} booking={booking}
@@ -112,26 +108,6 @@ export default async function DetailsPage({
<main> <main>
<HotelHeader hotelData={hotelData} /> <HotelHeader hotelData={hotelData} />
<div className={styles.container}> <div className={styles.container}>
{isRoomNotAvailable && (
<Alert
type={AlertTypeEnum.Alarm}
variant="inline"
heading={intl.formatMessage({
defaultMessage: "Room sold out",
})}
text={intl.formatMessage({
defaultMessage:
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
})}
link={{
title: intl.formatMessage({
defaultMessage: "Change room",
}),
url: `${selectRate(lang)}?${selectRoomParams.toString()}`,
keepSearchParams: true,
}}
/>
)}
<div className={styles.content}> <div className={styles.content}>
<RoomProvider idx={0} room={firstRoom}> <RoomProvider idx={0} room={firstRoom}>
<RoomOne user={user} /> <RoomOne user={user} />

View File

@@ -5,12 +5,14 @@ import { useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { BookingErrorCodeEnum } from "@/constants/booking" import { BookingErrorCodeEnum } from "@/constants/booking"
import { selectRate } from "@/constants/routes/hotelReservation"
import { useEnterDetailsStore } from "@/stores/enter-details" import { useEnterDetailsStore } from "@/stores/enter-details"
import Alert from "@/components/TempDesignSystem/Alert" import Alert from "@/components/TempDesignSystem/Alert"
import useLang from "@/hooks/useLang"
import useStickyPosition from "@/hooks/useStickyPosition" import useStickyPosition from "@/hooks/useStickyPosition"
import styles from "./paymentAlert.module.css" import styles from "./bookingAlert.module.css"
import { AlertTypeEnum } from "@/types/enums/alert" import { AlertTypeEnum } from "@/types/enums/alert"
@@ -19,6 +21,7 @@ function useBookingErrorAlert() {
(state) => state.actions.updateSeachParamString (state) => state.actions.updateSeachParamString
) )
const intl = useIntl() const intl = useIntl()
const lang = useLang()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const pathname = usePathname() const pathname = usePathname()
@@ -31,12 +34,19 @@ function useBookingErrorAlert() {
const [showAlert, setShowAlert] = useState(!!errorCode) const [showAlert, setShowAlert] = useState(!!errorCode)
const selectRateReturnUrl = getSelectRateReturnUrl()
function getErrorMessage(errorCode: string | null) { function getErrorMessage(errorCode: string | null) {
switch (errorCode) { switch (errorCode) {
case BookingErrorCodeEnum.TransactionCancelled: case BookingErrorCodeEnum.TransactionCancelled:
return intl.formatMessage({ return intl.formatMessage({
defaultMessage: "You have now cancelled your payment.", defaultMessage: "You have now cancelled your payment.",
}) })
case BookingErrorCodeEnum.AvailabilityError:
return intl.formatMessage({
defaultMessage:
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
})
default: default:
return intl.formatMessage({ return intl.formatMessage({
defaultMessage: defaultMessage:
@@ -54,16 +64,39 @@ function useBookingErrorAlert() {
window.history.replaceState({}, "", `${pathname}?${queryParams.toString()}`) window.history.replaceState({}, "", `${pathname}?${queryParams.toString()}`)
} }
return { showAlert, errorMessage, severityLevel, discardAlert, setShowAlert } function getSelectRateReturnUrl() {
const queryParams = new URLSearchParams(searchParams.toString())
queryParams.delete("errorCode")
return `${selectRate(lang)}?${queryParams.toString()}`
}
return {
showAlert,
errorCode,
errorMessage,
severityLevel,
discardAlert,
setShowAlert,
selectRateReturnUrl,
}
} }
interface PaymentAlertProps { interface BookingAlertProps {
isVisible?: boolean isVisible?: boolean
} }
export default function PaymentAlert({ isVisible = false }: PaymentAlertProps) { export default function BookingAlert({ isVisible = false }: BookingAlertProps) {
const { showAlert, errorMessage, severityLevel, discardAlert, setShowAlert } = const intl = useIntl()
useBookingErrorAlert()
const {
showAlert,
errorCode,
errorMessage,
severityLevel,
discardAlert,
setShowAlert,
selectRateReturnUrl,
} = useBookingErrorAlert()
const ref = useRef<HTMLDivElement>(null) const ref = useRef<HTMLDivElement>(null)
const { getTopOffset } = useStickyPosition() const { getTopOffset } = useStickyPosition()
@@ -87,6 +120,9 @@ export default function PaymentAlert({ isVisible = false }: PaymentAlertProps) {
if (!showAlert) return null if (!showAlert) return null
const isAvailabilityError =
errorCode === BookingErrorCodeEnum.AvailabilityError
return ( return (
<div className={styles.wrapper} ref={ref}> <div className={styles.wrapper} ref={ref}>
<Alert <Alert
@@ -94,6 +130,16 @@ export default function PaymentAlert({ isVisible = false }: PaymentAlertProps) {
variant="inline" variant="inline"
text={errorMessage} text={errorMessage}
close={discardAlert} close={discardAlert}
link={
isAvailabilityError
? {
title: intl.formatMessage({
defaultMessage: "Change room",
}),
url: selectRateReturnUrl,
}
: undefined
}
/> />
</div> </div>
) )

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { zodResolver } from "@hookform/resolvers/zod" import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter, useSearchParams } from "next/navigation" import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { Label } from "react-aria-components" import { Label } from "react-aria-components"
import { FormProvider, useForm } from "react-hook-form" import { FormProvider, useForm } from "react-hook-form"
@@ -11,7 +11,6 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { import {
BOOKING_CONFIRMATION_NUMBER, BOOKING_CONFIRMATION_NUMBER,
BookingErrorCodeEnum,
BookingStatusEnum, BookingStatusEnum,
PAYMENT_METHOD_TITLES, PAYMENT_METHOD_TITLES,
PaymentMethodEnum, PaymentMethodEnum,
@@ -42,10 +41,10 @@ import { bedTypeMap } from "../../utils"
import ConfirmBooking, { ConfirmBookingRedemption } from "../Confirm" import ConfirmBooking, { ConfirmBookingRedemption } from "../Confirm"
import PriceChangeDialog from "../PriceChangeDialog" import PriceChangeDialog from "../PriceChangeDialog"
import { writeGlaToSessionStorage } from "./PaymentCallback/helpers" import { writeGlaToSessionStorage } from "./PaymentCallback/helpers"
import BookingAlert from "./BookingAlert"
import GuaranteeDetails from "./GuaranteeDetails" import GuaranteeDetails from "./GuaranteeDetails"
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers" import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown" import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
import PaymentAlert from "./PaymentAlert"
import PaymentOptionsGroup from "./PaymentOptionsGroup" import PaymentOptionsGroup from "./PaymentOptionsGroup"
import { type PaymentFormData, paymentSchema } from "./schema" import { type PaymentFormData, paymentSchema } from "./schema"
import TermsAndConditions from "./TermsAndConditions" import TermsAndConditions from "./TermsAndConditions"
@@ -71,10 +70,11 @@ export default function PaymentClient({
const router = useRouter() const router = useRouter()
const lang = useLang() const lang = useLang()
const intl = useIntl() const intl = useIntl()
const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const { getTopOffset } = useStickyPosition({}) const { getTopOffset } = useStickyPosition({})
const [showPaymentAlert, setShowPaymentAlert] = useState(false) const [showBookingAlert, setShowBookingAlert] = useState(false)
const { booking, rooms, totalPrice } = useEnterDetailsStore((state) => ({ const { booking, rooms, totalPrice } = useEnterDetailsStore((state) => ({
booking: state.booking, booking: state.booking,
@@ -135,11 +135,14 @@ export default function PaymentClient({
onSuccess: (result) => { onSuccess: (result) => {
if (result) { if (result) {
if ("error" in result) { if ("error" in result) {
if (result.cause === BookingErrorCodeEnum.AvailabilityError) { const queryParams = new URLSearchParams(searchParams.toString())
window.location.reload() // reload to refetch room data because we dont know which room is unavailable queryParams.set("errorCode", result.cause)
} else { window.history.replaceState(
handlePaymentError(result.cause) {},
} "",
`${pathname}?${queryParams.toString()}`
)
handlePaymentError(result.cause)
return return
} }
@@ -196,7 +199,7 @@ export default function PaymentClient({
const handlePaymentError = useCallback( const handlePaymentError = useCallback(
(errorMessage: string) => { (errorMessage: string) => {
setShowPaymentAlert(true) setShowBookingAlert(true)
const currentPaymentMethod = methods.getValues("paymentMethod") const currentPaymentMethod = methods.getValues("paymentMethod")
const smsEnable = methods.getValues("smsConfirmation") const smsEnable = methods.getValues("smsConfirmation")
@@ -480,7 +483,7 @@ export default function PaymentClient({
? confirm ? confirm
: payment} : payment}
</Title> </Title>
<PaymentAlert isVisible={showPaymentAlert} /> <BookingAlert isVisible={showBookingAlert} />
</header> </header>
<FormProvider {...methods}> <FormProvider {...methods}>
<form <form

View File

@@ -0,0 +1,38 @@
"use client"
import { usePathname, useSearchParams } from "next/navigation"
import { useEffect } from "react"
import { useIntl } from "react-intl"
import { BookingErrorCodeEnum } from "@/constants/booking"
import { toast } from "@/components/TempDesignSystem/Toasts"
export default function AvailabilityError() {
const intl = useIntl()
const pathname = usePathname()
const searchParams = useSearchParams()
const errorCode = searchParams.get("errorCode")
const hasAvailabilityError =
errorCode === BookingErrorCodeEnum.AvailabilityError
const errorMessage = intl.formatMessage({
defaultMessage:
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
})
useEffect(() => {
if (!hasAvailabilityError) {
return
}
toast.error(errorMessage)
const newParams = new URLSearchParams(searchParams.toString())
newParams.delete("errorCode")
window.history.replaceState({}, "", `${pathname}?${newParams.toString()}`)
}, [errorMessage, hasAvailabilityError, pathname, searchParams])
return null
}

View File

@@ -12,6 +12,7 @@ import { setLang } from "@/i18n/serverContext"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails" import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { convertSearchParamsToObj } from "@/utils/url" import { convertSearchParamsToObj } from "@/utils/url"
import AvailabilityError from "./AvailabilityError"
import { getValidDates } from "./getValidDates" import { getValidDates } from "./getValidDates"
import { getTracking } from "./tracking" import { getTracking } from "./tracking"
@@ -90,6 +91,8 @@ export default async function SelectRatePage({
hotelInfo={hotelsTrackingData} hotelInfo={hotelsTrackingData}
/> />
</Suspense> </Suspense>
<AvailabilityError />
</> </>
) )
} }