Files
web/packages/booking-flow/lib/components/EnterDetails/Payment/BookingAlert/index.tsx
Anton Gunnarsson 3e3b15940f Merged in fix/booking-flow-eslint-fix (pull request #3342)
fix: Upgrade booking-flow eslint config

* Upgrade booking-flow eslint config


Approved-by: Bianca Widstam
2025-12-12 11:40:45 +00:00

219 lines
6.2 KiB
TypeScript

"use client"
import { usePathname, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useRef, useState } from "react"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
import { Alert } from "@scandic-hotels/design-system/Alert"
import { trackNoAvailability } from "@scandic-hotels/tracking/NoAvailabilityTracking"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode"
import useLang from "../../../../hooks/useLang"
import { useEnterDetailsStore } from "../../../../stores/enter-details"
import { mapPackageToLabel } from "../../../../utils/getRoomFeatureDescription"
import styles from "./bookingAlert.module.css"
function useBookingErrorAlert() {
const updateSearchParams = useEnterDetailsStore(
(state) => state.actions.updateSeachParamString
)
const intl = useIntl()
const lang = useLang()
const searchParams = useSearchParams()
const pathname = usePathname()
const errorCode = searchParams.get("errorCode")
const errorMessage = getErrorMessage(errorCode)
const severityLevel =
errorCode === BookingErrorCodeEnum.TransactionCancelled
? AlertTypeEnum.Warning
: AlertTypeEnum.Alarm
const [showAlert, setShowAlert] = useState(!!errorCode)
const selectRateReturnUrl = getSelectRateReturnUrl()
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setShowAlert(!!errorCode)
}, [errorCode])
function getErrorMessage(errorCode: string | null) {
switch (errorCode) {
case BookingErrorCodeEnum.TransactionCancelled:
return intl.formatMessage({
id: "enterDetails.bookingAlert.transactionCancelled",
defaultMessage: "You have now cancelled your payment.",
})
case BookingErrorCodeEnum.AvailabilityError:
case BookingErrorCodeEnum.NoAvailabilityForRateAndRoomType:
return intl.formatMessage({
id: "error.availabilityErrorMessage",
defaultMessage:
"Unfortunately, one of the rooms you selected is sold out. Please choose another room to proceed.",
})
default:
return intl.formatMessage({
id: "enterDetails.bookingAlert.genericError",
defaultMessage:
"We had an issue processing your booking. Please try again. No charges have been made.",
})
}
}
function discardAlert() {
setShowAlert(false)
const queryParams = new URLSearchParams(searchParams.toString())
queryParams.delete("errorCode")
updateSearchParams(queryParams.toString())
window.history.replaceState({}, "", `${pathname}?${queryParams.toString()}`)
}
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 BookingAlertProps {
isVisible?: boolean
}
export default function BookingAlert({ isVisible = false }: BookingAlertProps) {
const intl = useIntl()
const {
showAlert,
errorCode,
errorMessage,
severityLevel,
discardAlert,
setShowAlert,
selectRateReturnUrl,
} = useBookingErrorAlert()
const trackNoAvailabilityError = useNoAvailabilityTracking()
const isAvailabilityError =
errorCode === BookingErrorCodeEnum.AvailabilityError ||
errorCode === BookingErrorCodeEnum.NoAvailabilityForRateAndRoomType
const ref = useRef<HTMLDivElement>(null)
const topOffset = useStickyPosition().getTopOffset()
useEffect(() => {
if (isVisible) {
setShowAlert(true)
}
}, [isVisible, setShowAlert])
useEffect(() => {
const el = ref.current
if (showAlert && el) {
document.documentElement.style.overflow = ""
window.scrollTo({
top: el.offsetTop - topOffset,
behavior: "smooth",
})
}
}, [showAlert, topOffset])
useEffect(() => {
if (showAlert && isAvailabilityError) {
trackNoAvailabilityError()
}
}, [showAlert, isAvailabilityError, trackNoAvailabilityError])
if (!showAlert) return null
return (
<div className={styles.wrapper} ref={ref}>
<Alert
type={severityLevel}
variant="inline"
text={errorMessage}
close={discardAlert}
link={
isAvailabilityError
? {
title: intl.formatMessage({
id: "enterDetails.bookingAlert.changeRoomLink",
defaultMessage: "Change room",
}),
url: selectRateReturnUrl,
}
: undefined
}
/>
</div>
)
}
function useNoAvailabilityTracking() {
const { fromDate, toDate, hotelId, bookingCode, searchType, rooms } =
useEnterDetailsStore((state) => state.booking)
const hotelName = useEnterDetailsStore((state) => state.hotelName)
const lang = useLang()
const specialRoomType = rooms
?.map((room) => {
const packages = room.packages
?.map((pkg) => mapPackageToLabel(pkg))
.join(",")
return packages ?? ""
})
.join("|")
const track = useCallback(
() =>
trackNoAvailability({
specialRoomType,
lang,
searchTerm: hotelName,
fromDate,
toDate,
hotelId,
noOfRooms: rooms.length,
searchType: "hotel",
rewardNight: searchType === SEARCH_TYPE_REDEMPTION ? "yes" : "no",
bookingCode: bookingCode ?? "n/a",
pageId: "details",
pageName: "hotelreservation|details",
pageType: "bookingenterdetailspage",
siteSections: "hotelreservation|details",
domain: typeof window !== "undefined" ? window.location.host : "",
}),
[
specialRoomType,
lang,
hotelName,
fromDate,
toDate,
hotelId,
rooms.length,
searchType,
bookingCode,
]
)
return track
}