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
This commit is contained in:
Anton Gunnarsson
2026-01-30 14:29:49 +00:00
parent fd38542863
commit 76ee5e97bf

View File

@@ -27,6 +27,7 @@ import AdditionalInfoForm from "@/components/HotelReservation/FindMyBooking/Addi
import accessBooking, {
ACCESS_GRANTED,
ERROR_BAD_REQUEST,
ERROR_NOT_FOUND,
ERROR_UNAUTHORIZED,
} from "@/components/HotelReservation/MyStay/accessBooking"
import { Ancillaries } from "@/components/HotelReservation/MyStay/Ancillaries"
@@ -74,39 +75,23 @@ async function MyStay(props: {
notFound()
}
const { confirmationNumber, lastName } = parseRefId(refId)
const isLoggedIn = await isLoggedInUser()
const cookieStore = await cookies()
const bv = cookieStore.get("bv")?.value
let bookingConfirmation
if (isLoggedIn) {
bookingConfirmation = await getBookingConfirmation(refId)
} else if (bv) {
logger.debug(`MyStay: bv`, bv)
const {
firstName,
email,
confirmationNumber: bvConfirmationNo,
} = JSON.parse(bv) as AdditionalInfoCookieValue
if (firstName && email && bvConfirmationNo === confirmationNumber) {
bookingConfirmation = await findBooking(
confirmationNumber,
lastName,
firstName,
email
)
} else {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
}
} else {
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}
@@ -121,208 +106,226 @@ async function MyStay(props: {
)
}
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
const { booking } = bookingConfirmation
const user = await getProfileSafely()
const { code } = accessBooking(booking.guest, lastName, user, bv)
const intl = await getIntl()
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()
const access = accessBooking(booking.guest, lastName, user, bv)
if (access === ACCESS_GRANTED) {
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
const linkedReservationsPromise = getLinkedReservations(booking.refId)
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
if (shouldFetchBreakfastPackages) {
void getPackages(packagesInput)
}
const isOwnBooking = user?.email === booking.guest.email
if (user && isOwnBooking) {
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
}
let breakfastPackages = null
if (shouldFetchBreakfastPackages) {
breakfastPackages = await getPackages(packagesInput)
}
let savedCreditCards = null
if (user && isOwnBooking) {
savedCreditCards = await getSavedPaymentCardsSafely(
savedPaymentCardsInput
return (
<RenderFindMyBookingForm
bv={bv}
lastName={lastName}
confirmationNumber={confirmationNumber}
/>
)
}
let ancillaryPackagesPromise = null
if (booking.showAncillaries) {
ancillaryPackagesPromise = getAncillaryPackages({
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
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 = {
...bookingConfirmation,
booking: {
...bookingConfirmation.booking,
guest: {
...bookingConfirmation.booking.guest,
email: maskValue.email(bookingConfirmation.booking.guest.email),
phoneNumber: maskValue.phone(
bookingConfirmation.booking.guest.phoneNumber ?? ""
),
},
},
} satisfies BookingConfirmation
const maskedUser =
user && isOwnBooking
? ({
...user,
email: maskValue.email(user.email),
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
} satisfies SafeUser)
: null
hotel.specialAlerts = filterOverlappingDates(
hotel.specialAlerts,
dt.utc(fromDate),
dt.utc(toDate)
)
return (
<MyStayProvider
bookingConfirmation={maskedBookingConfirmation}
breakfastPackages={breakfastPackages}
lang={lang}
linkedReservationsPromise={linkedReservationsPromise}
refId={booking.refId}
roomCategories={roomCategories}
savedCreditCards={savedCreditCards}
isLoggedIn={isLoggedIn}
>
<main className={styles.main}>
<div className={styles.imageContainer}>
<div className={styles.blurOverlay} />
{imageSrc && (
<Image
className={styles.image}
src={imageSrc}
alt={hotel.name}
fill
/>
)}
</div>
<div className={styles.content}>
<div className={styles.headerContainer}>
<Header cityName={hotel.cityName} name={hotel.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={hotel} />
{!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={hotel.hotelContent.images}
/>
)}
</div>
</main>
</MyStayProvider>
)
}
if (access === ERROR_BAD_REQUEST) {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
}
if (access === ERROR_UNAUTHORIZED) {
if (bv) {
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,
}}
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>
</main>
)
} else {
}
}
{booking.showAncillaries && ancillaryPackagesPromise && (
<Ancillaries
ancillariesPromise={ancillaryPackagesPromise}
packages={breakfastPackages}
user={maskedUser}
savedCreditCards={savedCreditCards}
/>
)}
return notFound()
<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({
@@ -343,3 +346,75 @@ function RenderAdditionalInfoForm({
</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)
}