Files
web/apps/scandic-web/components/HotelReservation/MyStay/index.tsx
Anton Gunnarsson 76ee5e97bf Merged in chore/improve-my-stay-load-times (pull request #3514)
chore: Improve My Stay load times

* Restructure My Stay page to avoid data fetching waterfalls


Approved-by: Linus Flood
Approved-by: Matilda Landström
2026-01-30 14:29:49 +00:00

421 lines
12 KiB
TypeScript

import { cookies } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { BookingFlowConfig } from "@scandic-hotels/booking-flow/BookingFlowConfig"
import { filterOverlappingDates } from "@scandic-hotels/booking-flow/utils/SelectRate"
import { findMyBookingRoutes } from "@scandic-hotels/common/constants/routes/findMyBookingRoutes"
import { dt } from "@scandic-hotels/common/dt"
import { logger } from "@scandic-hotels/common/logger"
import * as maskValue from "@scandic-hotels/common/utils/maskValue"
import Image from "@scandic-hotels/design-system/Image"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { parseRefId } from "@scandic-hotels/trpc/utils/refId"
import { bookingFlowConfig } from "@/constants/bookingFlowConfig"
import { env } from "@/env/server"
import {
findBooking,
getAncillaryPackages,
getBookingConfirmation,
getLinkedReservations,
getPackages,
getProfileSafely,
getSavedPaymentCardsSafely,
} from "@/lib/trpc/memoizedRequests"
import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/AdditionalInfoForm"
import accessBooking, {
ACCESS_GRANTED,
ERROR_BAD_REQUEST,
ERROR_NOT_FOUND,
ERROR_UNAUTHORIZED,
} from "@/components/HotelReservation/MyStay/accessBooking"
import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries"
import BookingSummary from "@/components/HotelReservation/MyStay/BookingSummary"
import { Header } from "@/components/HotelReservation/MyStay/Header"
import Promo from "@/components/HotelReservation/MyStay/Promo"
import { ReferenceCard } from "@/components/HotelReservation/MyStay/ReferenceCard"
import MultiRoom from "@/components/HotelReservation/MyStay/Rooms/MultiRoom"
import SingleRoom from "@/components/HotelReservation/MyStay/Rooms/SingleRoom"
import { getIntl } from "@/i18n"
import MyStayProvider from "@/providers/MyStay"
import { isLoggedInUser } from "@/utils/isLoggedInUser"
import FindMyBooking from "../FindMyBooking"
import { FindMyBookingErrorEnum } from "../FindMyBooking/utils"
import styles from "./index.module.css"
import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { SafeUser } from "@/types/user"
export default function MyStayWrapper(props: {
refId?: string
lang: Lang
isWebview?: boolean
}) {
return (
<BookingFlowConfig config={bookingFlowConfig}>
<MyStay {...props} />
</BookingFlowConfig>
)
}
async function MyStay(props: {
refId?: string
lang: Lang
isWebview?: boolean
}) {
const { refId, lang, isWebview } = props
if (!refId) {
notFound()
}
const cookieStore = await cookies()
const bv = cookieStore.get("bv")?.value
const { confirmationNumber, lastName } = parseRefId(refId)
const isLoggedIn = await isLoggedInUser()
const [{ error, bookingConfirmation }, user] = await Promise.all([
getOrFindBookingConfirmation({
refId,
isLoggedIn,
confirmationNumber,
lastName,
bv,
}),
getProfileSafely(),
])
if (error === "MISSING_INFO") {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
}
if (!bookingConfirmation) {
redirect(
`${findMyBookingRoutes[lang]}?error=${FindMyBookingErrorEnum.BOOKING_NOT_FOUND}`
)
}
const { booking } = bookingConfirmation
const { code } = accessBooking(booking.guest, lastName, user, bv)
switch (code) {
case ACCESS_GRANTED.code:
return (
<MyStayPage
bookingConfirmation={bookingConfirmation}
user={user}
lang={lang}
isWebview={!!isWebview}
/>
)
case ERROR_NOT_FOUND.code:
return notFound()
case ERROR_BAD_REQUEST.code:
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
case ERROR_UNAUTHORIZED.code: {
if (!bv) return notFound()
return (
<RenderFindMyBookingForm
bv={bv}
lastName={lastName}
confirmationNumber={confirmationNumber}
/>
)
}
default:
const _exhaustiveCheck: never = code
throw new Error(`Unknown access code: ${code}`)
}
}
async function MyStayPage({
bookingConfirmation,
user,
lang,
isWebview,
}: {
bookingConfirmation: BookingConfirmation
user: SafeUser | null
lang: Lang
isWebview: boolean
}) {
const intl = await getIntl()
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
const packagesInput = {
adults: booking.adults,
children: booking.childrenAges.length,
endDate: toDate,
hotelId: hotel.operaId,
lang,
startDate: fromDate,
packageCodes: [
BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST,
BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST,
BreakfastPackageEnum.FREE_CHILD_BREAKFAST,
],
}
const supportedCards = hotel.merchantInformationData.cards
const savedPaymentCardsInput = { supportedCards }
const hasBreakfastPackage = booking.packages.find(
(pkg) => pkg.code === BreakfastPackageEnum.REGULAR_BREAKFAST
)
const breakfastIncluded = booking.rateDefinition.breakfastIncluded
const shouldFetchBreakfastPackages =
!hasBreakfastPackage && !breakfastIncluded
const isOwnBooking = user?.email === booking.guest.email
const shouldGetCards = user && isOwnBooking
const [breakfastPackages, savedCreditCards] = await Promise.all([
shouldFetchBreakfastPackages ? getPackages(packagesInput) : noop(),
shouldGetCards
? getSavedPaymentCardsSafely(savedPaymentCardsInput)
: noop(),
])
const imageSrc =
hotel.hotelContent.images.src ||
additionalData.gallery?.heroImages[0]?.src ||
hotel.galleryImages[0]?.src
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
const promoUrl = new URL(`${baseUrl}/${lang}/`)
const hotelUrl = new URL(`${baseUrl}${bookingConfirmation.url}/`)
promoUrl.searchParams.set("hotel", hotel.operaId)
const maskedBookingConfirmation = maskBookingConfirmation(bookingConfirmation)
const maskedUser = isOwnBooking ? maskUser(user) : null
const hotelWithFilteredAlerts = {
...hotel,
specialAlerts: filterOverlappingDates(
hotel.specialAlerts,
dt.utc(fromDate),
dt.utc(toDate)
),
}
const linkedReservationsPromise = getLinkedReservations(booking.refId)
const ancillaryPackagesPromise = booking.showAncillaries
? getAncillaryPackages({
fromDate,
hotelId: hotel.operaId,
toDate,
})
: null
return (
<MyStayProvider
bookingConfirmation={maskedBookingConfirmation}
breakfastPackages={breakfastPackages}
lang={lang}
linkedReservationsPromise={linkedReservationsPromise}
refId={booking.refId}
roomCategories={roomCategories}
savedCreditCards={savedCreditCards}
isLoggedIn={!!user}
>
<main className={styles.main}>
<div className={styles.imageContainer}>
<div className={styles.blurOverlay} />
{imageSrc && (
<Image
className={styles.image}
src={imageSrc}
alt={hotelWithFilteredAlerts.name}
fill
/>
)}
</div>
<div className={styles.content}>
<div className={styles.headerContainer}>
<Header
cityName={hotelWithFilteredAlerts.cityName}
name={hotelWithFilteredAlerts.name}
/>
<ReferenceCard />
</div>
{booking.showAncillaries && ancillaryPackagesPromise && (
<Ancillaries
ancillariesPromise={ancillaryPackagesPromise}
packages={breakfastPackages}
user={maskedUser}
savedCreditCards={savedCreditCards}
/>
)}
<SingleRoom user={maskedUser} />
<MultiRoom user={maskedUser} />
<BookingSummary
hotelUrl={hotelUrl.toString()}
hotel={hotelWithFilteredAlerts}
/>
{!isWebview && (
<Promo
title={intl.formatMessage({
id: "booking.bookNextStay.title",
defaultMessage: "Book your next stay",
})}
text={intl.formatMessage({
id: "booking.bookAnotherStayDescription",
defaultMessage:
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
})}
buttonText={intl.formatMessage({
id: "myStay.promo.bookNextStay.buttonText",
defaultMessage: "Explore Scandic hotels",
})}
href={promoUrl.toString()}
image={hotelWithFilteredAlerts.hotelContent.images}
/>
)}
</div>
</main>
</MyStayProvider>
)
}
function RenderFindMyBookingForm({
bv,
lastName,
confirmationNumber,
}: {
bv: string
lastName: string
confirmationNumber: string
}) {
const { firstName, email } = JSON.parse(bv) as AdditionalInfoCookieValue
return (
<main className={styles.main}>
<div className={styles.form}>
<FindMyBooking
error={FindMyBookingErrorEnum.BOOKING_ACCESS_DENIED}
defaultValues={{
firstName,
lastName,
confirmationNumber,
email,
}}
/>
</div>
</main>
)
}
function RenderAdditionalInfoForm({
confirmationNumber,
lastName,
}: {
confirmationNumber: string
lastName: string
}) {
return (
<main className={styles.main}>
<div className={styles.form}>
<AdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
</div>
</main>
)
}
function maskUser(user: SafeUser | null): SafeUser | null {
if (!user) return null
return {
...user,
email: maskValue.email(user.email),
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
}
}
function maskBookingConfirmation(
bookingConfirmation: BookingConfirmation
): BookingConfirmation {
return {
...bookingConfirmation,
booking: {
...bookingConfirmation.booking,
guest: {
...bookingConfirmation.booking.guest,
email: maskValue.email(bookingConfirmation.booking.guest.email),
phoneNumber: maskValue.phone(
bookingConfirmation.booking.guest.phoneNumber ?? ""
),
},
},
}
}
async function getOrFindBookingConfirmation({
refId,
confirmationNumber,
lastName,
isLoggedIn,
bv,
}: {
refId: string
confirmationNumber: string
lastName: string
isLoggedIn: boolean
bv?: string
}) {
if (isLoggedIn)
return { bookingConfirmation: await getBookingConfirmation(refId) } as const
if (!bv) return { error: "MISSING_INFO", bookingConfirmation: null } as const
logger.debug(`MyStay: bv`, bv)
const {
firstName,
email,
confirmationNumber: bvConfirmationNo,
} = JSON.parse(bv) as AdditionalInfoCookieValue
if (!firstName || !email || bvConfirmationNo !== confirmationNumber) {
return { error: "MISSING_INFO", bookingConfirmation: null } as const
}
return {
bookingConfirmation: await findBooking(
confirmationNumber,
lastName,
firstName,
email
),
} as const
}
// Helper function to handle conditional Promise.all calls
async function noop() {
return Promise.resolve(null)
}