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:
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user