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(
const { confirmationNumber, lastName } = parseRefId(refId)
const isLoggedIn = await isLoggedInUser()
const [{ error, bookingConfirmation }, user] = await Promise.all([
getOrFindBookingConfirmation({
refId,
isLoggedIn,
confirmationNumber,
lastName,
firstName,
email
)
} else {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
}
} else {
bv,
}),
getProfileSafely(),
])
if (error === "MISSING_INFO") {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
@@ -121,20 +106,64 @@ 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)
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 access = accessBooking(booking.guest, lastName, user, bv)
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
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,
@@ -157,32 +186,16 @@ async function MyStay(props: {
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
)
}
let ancillaryPackagesPromise = null
if (booking.showAncillaries) {
ancillaryPackagesPromise = getAncillaryPackages({
fromDate,
hotelId: hotel.operaId,
toDate,
})
}
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 ||
@@ -192,37 +205,28 @@ async function MyStay(props: {
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 maskedBookingConfirmation = maskBookingConfirmation(bookingConfirmation)
const maskedUser = isOwnBooking ? maskUser(user) : null
const maskedUser =
user && isOwnBooking
? ({
...user,
email: maskValue.email(user.email),
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
} satisfies SafeUser)
: null
hotel.specialAlerts = filterOverlappingDates(
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
@@ -233,7 +237,7 @@ async function MyStay(props: {
refId={booking.refId}
roomCategories={roomCategories}
savedCreditCards={savedCreditCards}
isLoggedIn={isLoggedIn}
isLoggedIn={!!user}
>
<main className={styles.main}>
<div className={styles.imageContainer}>
@@ -242,14 +246,17 @@ async function MyStay(props: {
<Image
className={styles.image}
src={imageSrc}
alt={hotel.name}
alt={hotelWithFilteredAlerts.name}
fill
/>
)}
</div>
<div className={styles.content}>
<div className={styles.headerContainer}>
<Header cityName={hotel.cityName} name={hotel.name} />
<Header
cityName={hotelWithFilteredAlerts.cityName}
name={hotelWithFilteredAlerts.name}
/>
<ReferenceCard />
</div>
{booking.showAncillaries && ancillaryPackagesPromise && (
@@ -264,7 +271,10 @@ async function MyStay(props: {
<SingleRoom user={maskedUser} />
<MultiRoom user={maskedUser} />
<BookingSummary hotelUrl={hotelUrl.toString()} hotel={hotel} />
<BookingSummary
hotelUrl={hotelUrl.toString()}
hotel={hotelWithFilteredAlerts}
/>
{!isWebview && (
<Promo
title={intl.formatMessage({
@@ -281,7 +291,7 @@ async function MyStay(props: {
defaultMessage: "Explore Scandic hotels",
})}
href={promoUrl.toString()}
image={hotel.hotelContent.images}
image={hotelWithFilteredAlerts.hotelContent.images}
/>
)}
</div>
@@ -290,17 +300,15 @@ async function MyStay(props: {
)
}
if (access === ERROR_BAD_REQUEST) {
return (
<RenderAdditionalInfoForm
confirmationNumber={confirmationNumber}
lastName={lastName}
/>
)
}
if (access === ERROR_UNAUTHORIZED) {
if (bv) {
function RenderFindMyBookingForm({
bv,
lastName,
confirmationNumber,
}: {
bv: string
lastName: string
confirmationNumber: string
}) {
const { firstName, email } = JSON.parse(bv) as AdditionalInfoCookieValue
return (
@@ -318,11 +326,6 @@ async function MyStay(props: {
</div>
</main>
)
} else {
}
}
return notFound()
}
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)
}