Merged in feat/SW-3050-webviews (pull request #2429)
Feat/SW-3050 webviews Approved-by: Anton Gunnarsson
This commit is contained in:
@@ -1,53 +1,11 @@
|
|||||||
import { cookies } from "next/headers"
|
import MyStay from "@/components/HotelReservation/MyStay"
|
||||||
import { notFound } from "next/navigation"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
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_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 SidePeek from "@/components/HotelReservation/SidePeek"
|
|
||||||
import Image from "@/components/Image"
|
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
import MyStayProvider from "@/providers/MyStay"
|
|
||||||
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
|
||||||
import * as maskValue from "@/utils/maskValue"
|
|
||||||
import { parseRefId } from "@/utils/refId"
|
|
||||||
import { getCurrentWebUrl } from "@/utils/url"
|
|
||||||
|
|
||||||
import Tracking from "./tracking"
|
import Tracking from "./tracking"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
|
||||||
|
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
|
||||||
import type { SafeUser } from "@/types/user"
|
|
||||||
|
|
||||||
export default async function MyStay(
|
export default async function MyStayPage(
|
||||||
props: PageArgs<LangParams, { RefId?: string }>
|
props: PageArgs<LangParams, { RefId?: string }>
|
||||||
) {
|
) {
|
||||||
const searchParams = await props.searchParams
|
const searchParams = await props.searchParams
|
||||||
@@ -55,269 +13,10 @@ export default async function MyStay(
|
|||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
const refId = searchParams.RefId
|
const refId = searchParams.RefId
|
||||||
|
|
||||||
if (!refId) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
|
||||||
if (!confirmationNumber) {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const params = new URLSearchParams(bv)
|
|
||||||
const firstName = params.get("firstName")
|
|
||||||
const email = params.get("email")
|
|
||||||
|
|
||||||
if (firstName && email) {
|
|
||||||
bookingConfirmation = await findBooking(
|
|
||||||
confirmationNumber,
|
|
||||||
lastName,
|
|
||||||
firstName,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<RenderAdditionalInfoForm
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<RenderAdditionalInfoForm
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bookingConfirmation) {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
|
||||||
|
|
||||||
const user = await getProfileSafely()
|
|
||||||
|
|
||||||
const intl = await getIntl()
|
|
||||||
|
|
||||||
const access = accessBooking(booking.guest, lastName, user, bv)
|
|
||||||
|
|
||||||
if (access === ACCESS_GRANTED) {
|
|
||||||
const lang = params.lang
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if (user) {
|
|
||||||
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
let breakfastPackages = null
|
|
||||||
if (shouldFetchBreakfastPackages) {
|
|
||||||
breakfastPackages = await getPackages(packagesInput)
|
|
||||||
}
|
|
||||||
let savedCreditCards = null
|
|
||||||
if (user) {
|
|
||||||
savedCreditCards = await getSavedPaymentCardsSafely(
|
|
||||||
savedPaymentCardsInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let ancillaryPackagesPromise = null
|
|
||||||
if (booking.showAncillaries) {
|
|
||||||
ancillaryPackagesPromise = getAncillaryPackages({
|
|
||||||
fromDate,
|
|
||||||
hotelId: hotel.operaId,
|
|
||||||
toDate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageSrc =
|
|
||||||
hotel.hotelContent.images.imageSizes.large ??
|
|
||||||
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
|
||||||
hotel.galleryImages[0]?.imageSizes.large
|
|
||||||
|
|
||||||
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
|
||||||
const promoUrl = !env.isLangLive(lang)
|
|
||||||
? new URL(getCurrentWebUrl({ path: "/", lang }))
|
|
||||||
: new URL(`${baseUrl}/${lang}/`)
|
|
||||||
|
|
||||||
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
|
|
||||||
? ({
|
|
||||||
...user,
|
|
||||||
email: maskValue.email(user.email),
|
|
||||||
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
|
|
||||||
} satisfies SafeUser)
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MyStayProvider
|
|
||||||
bookingConfirmation={maskedBookingConfirmation}
|
|
||||||
breakfastPackages={breakfastPackages}
|
|
||||||
lang={params.lang}
|
|
||||||
linkedReservationsPromise={linkedReservationsPromise}
|
|
||||||
refId={booking.refId}
|
|
||||||
roomCategories={roomCategories}
|
|
||||||
savedCreditCards={savedCreditCards}
|
|
||||||
>
|
|
||||||
<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 hotel={hotel} />
|
|
||||||
<Promo
|
|
||||||
title={intl.formatMessage({
|
|
||||||
defaultMessage: "Book your next stay",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
|
||||||
})}
|
|
||||||
buttonText={intl.formatMessage({
|
|
||||||
defaultMessage: "Explore Scandic hotels",
|
|
||||||
})}
|
|
||||||
href={promoUrl.toString()}
|
|
||||||
image={hotel.hotelContent.images}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<SidePeek />
|
|
||||||
<Tracking />
|
|
||||||
</MyStayProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (access === ERROR_BAD_REQUEST) {
|
|
||||||
return (
|
|
||||||
<main className={styles.main}>
|
|
||||||
<div className={styles.form}>
|
|
||||||
<AdditionalInfoForm
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (access === ERROR_UNAUTHORIZED) {
|
|
||||||
return (
|
|
||||||
<main className={styles.main}>
|
|
||||||
<div className={styles.logIn}>
|
|
||||||
<Typography variant="Title/md">
|
|
||||||
<h1>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "You need to be logged in to view your booking",
|
|
||||||
})}
|
|
||||||
</h1>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Lead text">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"And you need to be logged in with the same member account that made the booking.",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
function RenderAdditionalInfoForm({
|
|
||||||
confirmationNumber,
|
|
||||||
lastName,
|
|
||||||
}: {
|
|
||||||
confirmationNumber: string
|
|
||||||
lastName: string
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<>
|
||||||
<div className={styles.form}>
|
<MyStay refId={refId} lang={params.lang} />
|
||||||
<AdditionalInfoForm
|
<Tracking />
|
||||||
confirmationNumber={confirmationNumber}
|
</>
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
# Booking flow
|
|
||||||
|
|
||||||
The booking flow is the user journey of booking one or more rooms at our
|
|
||||||
hotels. Everything from choosing the date to payment and confirmation is
|
|
||||||
part of the booking flow.
|
|
||||||
|
|
||||||
## Booking widget
|
|
||||||
|
|
||||||
On most of the pages on the website we have a booking widget. This is where
|
|
||||||
the user starts the booking flow, by filling the form and submit. If they
|
|
||||||
entered a city as the destination they will land on the select hotel page
|
|
||||||
and if they entered a specific hotel they will land on the select rate page.
|
|
||||||
|
|
||||||
## Select hotel
|
|
||||||
|
|
||||||
Lists available hotels based on the search criteria. When the user selects
|
|
||||||
a hotel they land on the select rate page.
|
|
||||||
|
|
||||||
## Select rate, room, breakfast etc
|
|
||||||
|
|
||||||
This is a page with an accordion like design, but every accordion is handled
|
|
||||||
as its own page with its own URL.
|
|
||||||
|
|
||||||
## State management
|
|
||||||
|
|
||||||
The state, like search parameters and selected alternatives, is kept
|
|
||||||
throughout the booking flow in the URL.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { env } from "@/env/server"
|
|
||||||
|
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
|
||||||
|
|
||||||
export async function generateMetadata(props: PageArgs<LangParams>) {
|
|
||||||
const params = await props.params
|
|
||||||
return {
|
|
||||||
robots: {
|
|
||||||
index: env.isLangLive(params.lang),
|
|
||||||
follow: env.isLangLive(params.lang),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HotelReservationLayout({
|
|
||||||
children,
|
|
||||||
}: React.PropsWithChildren) {
|
|
||||||
if (env.NEW_SITE_LIVE_STATUS === "NOT_LIVE") {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { MyStaySkeleton as default } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
.main {
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
|
||||||
}
|
|
||||||
|
|
||||||
.imageContainer {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blurOverlay {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 1;
|
|
||||||
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 1) 0%, transparent 100%);
|
|
||||||
background: linear-gradient(
|
|
||||||
to bottom,
|
|
||||||
rgba(0, 0, 0, 0.5) 0%,
|
|
||||||
transparent 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headerContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 80px;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
padding-bottom: var(--Spacing-x3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
|
||||||
max-width: 640px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
padding: var(--Spacing-x5) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
padding: 0 var(--Spacing-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logIn {
|
|
||||||
padding: var(--Spacing-x9) var(--Spacing-x2);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
align-items: center;
|
|
||||||
color: var(--Scandic-Grey-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.content {
|
|
||||||
width: var(--max-width-content);
|
|
||||||
padding-bottom: 160px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +1,14 @@
|
|||||||
import { cookies } from "next/headers"
|
import { Suspense } from "react"
|
||||||
import { notFound } from "next/navigation"
|
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import MyStay from "@/components/HotelReservation/MyStay"
|
||||||
|
import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
||||||
import { env } from "@/env/server"
|
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
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_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 SidePeek from "@/components/HotelReservation/SidePeek"
|
|
||||||
import Image from "@/components/Image"
|
|
||||||
import { getIntl } from "@/i18n"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
import MyStayProvider from "@/providers/MyStay"
|
|
||||||
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
|
||||||
import * as maskValue from "@/utils/maskValue"
|
|
||||||
import { parseRefId } from "@/utils/refId"
|
|
||||||
import { getCurrentWebUrl } from "@/utils/url"
|
|
||||||
|
|
||||||
import Tracking from "./tracking"
|
import Tracking from "./tracking"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
|
||||||
|
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
|
||||||
import type { SafeUser } from "@/types/user"
|
|
||||||
|
|
||||||
export default async function MyStay(
|
export default async function MyStayWebviewPage(
|
||||||
props: PageArgs<LangParams, { RefId?: string }>
|
props: PageArgs<LangParams, { RefId?: string }>
|
||||||
) {
|
) {
|
||||||
const searchParams = await props.searchParams
|
const searchParams = await props.searchParams
|
||||||
@@ -55,272 +16,10 @@ export default async function MyStay(
|
|||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
const refId = searchParams.RefId
|
const refId = searchParams.RefId
|
||||||
|
|
||||||
if (!refId) {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { confirmationNumber, lastName } = parseRefId(refId)
|
|
||||||
|
|
||||||
if (!confirmationNumber) {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
const params = new URLSearchParams(bv)
|
|
||||||
const firstName = params.get("firstName")
|
|
||||||
const email = params.get("email")
|
|
||||||
|
|
||||||
if (firstName && email) {
|
|
||||||
bookingConfirmation = await findBooking(
|
|
||||||
confirmationNumber,
|
|
||||||
lastName,
|
|
||||||
firstName,
|
|
||||||
email
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<RenderAdditionalInfoForm
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<RenderAdditionalInfoForm
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!bookingConfirmation) {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
|
||||||
|
|
||||||
const user = await getProfileSafely()
|
|
||||||
const intl = await getIntl()
|
|
||||||
|
|
||||||
const access = accessBooking(booking.guest, lastName, user, bv)
|
|
||||||
|
|
||||||
if (access === ACCESS_GRANTED) {
|
|
||||||
const lang = params.lang
|
|
||||||
const fromDate = dt(booking.checkInDate).format("YYYY-MM-DD")
|
|
||||||
const toDate = dt(booking.checkOutDate).format("YYYY-MM-DD")
|
|
||||||
|
|
||||||
const linkedReservationsPromise = getLinkedReservations(booking.refId)
|
|
||||||
|
|
||||||
const ancillariesInput = {
|
|
||||||
fromDate,
|
|
||||||
hotelId: hotel.operaId,
|
|
||||||
toDate,
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if (user) {
|
|
||||||
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
|
|
||||||
}
|
|
||||||
if (booking.showAncillaries) {
|
|
||||||
void getAncillaryPackages(ancillariesInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
let breakfastPackages = null
|
|
||||||
if (shouldFetchBreakfastPackages) {
|
|
||||||
breakfastPackages = await getPackages(packagesInput)
|
|
||||||
}
|
|
||||||
let savedCreditCards = null
|
|
||||||
if (user) {
|
|
||||||
savedCreditCards = await getSavedPaymentCardsSafely(
|
|
||||||
savedPaymentCardsInput
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let ancillaryPackagesPromise = null
|
|
||||||
if (booking.showAncillaries) {
|
|
||||||
ancillaryPackagesPromise = getAncillaryPackages(ancillariesInput)
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageSrc =
|
|
||||||
hotel.hotelContent.images.imageSizes.large ??
|
|
||||||
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
|
||||||
hotel.galleryImages[0]?.imageSizes.large
|
|
||||||
|
|
||||||
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
|
||||||
const promoUrl = !env.isLangLive(params.lang)
|
|
||||||
? new URL(getCurrentWebUrl({ path: "/", lang }))
|
|
||||||
: new URL(`${baseUrl}/${lang}/`)
|
|
||||||
|
|
||||||
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
|
|
||||||
? ({
|
|
||||||
...user,
|
|
||||||
email: maskValue.email(user.email),
|
|
||||||
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
|
|
||||||
} satisfies SafeUser)
|
|
||||||
: null
|
|
||||||
|
|
||||||
promoUrl.searchParams.set("hotel", hotel.operaId)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MyStayProvider
|
|
||||||
bookingConfirmation={maskedBookingConfirmation}
|
|
||||||
breakfastPackages={breakfastPackages}
|
|
||||||
lang={params.lang}
|
|
||||||
linkedReservationsPromise={linkedReservationsPromise}
|
|
||||||
refId={booking.refId}
|
|
||||||
roomCategories={roomCategories}
|
|
||||||
savedCreditCards={savedCreditCards}
|
|
||||||
>
|
|
||||||
<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 hotel={hotel} />
|
|
||||||
<Promo
|
|
||||||
title={intl.formatMessage({
|
|
||||||
defaultMessage: "Book your next stay",
|
|
||||||
})}
|
|
||||||
text={intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
|
||||||
})}
|
|
||||||
buttonText={intl.formatMessage({
|
|
||||||
defaultMessage: "Explore Scandic hotels",
|
|
||||||
})}
|
|
||||||
href={promoUrl.toString()}
|
|
||||||
image={hotel.hotelContent.images}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<SidePeek />
|
|
||||||
<Tracking />
|
|
||||||
</MyStayProvider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (access === ERROR_BAD_REQUEST) {
|
|
||||||
return (
|
|
||||||
<main className={styles.main}>
|
|
||||||
<div className={styles.form}>
|
|
||||||
<AdditionalInfoForm
|
|
||||||
confirmationNumber={confirmationNumber}
|
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (access === ERROR_UNAUTHORIZED) {
|
|
||||||
return (
|
|
||||||
<main className={styles.main}>
|
|
||||||
<div className={styles.logIn}>
|
|
||||||
<Typography variant="Title/md">
|
|
||||||
<h1>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "You need to be logged in to view your booking",
|
|
||||||
})}
|
|
||||||
</h1>
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="Body/Lead text">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage:
|
|
||||||
"And you need to be logged in with the same member account that made the booking.",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
|
|
||||||
function RenderAdditionalInfoForm({
|
|
||||||
confirmationNumber,
|
|
||||||
lastName,
|
|
||||||
}: {
|
|
||||||
confirmationNumber: string
|
|
||||||
lastName: string
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<Suspense fallback={<MyStaySkeleton />}>
|
||||||
<div className={styles.form}>
|
<MyStay refId={refId} lang={params.lang} isWebview={true} />
|
||||||
<AdditionalInfoForm
|
<Tracking />
|
||||||
confirmationNumber={confirmationNumber}
|
</Suspense>
|
||||||
lastName={lastName}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useRouter } from "next/navigation"
|
import { usePathname, useRouter } from "next/navigation"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { Dialog } from "react-aria-components"
|
import { Dialog } from "react-aria-components"
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
@@ -10,6 +10,7 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { profileEdit } from "@/constants/routes/myPages"
|
import { profileEdit } from "@/constants/routes/myPages"
|
||||||
|
import { isWebview } from "@/constants/routes/webviews"
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
import MembershipLevelIcon from "@/components/Levels/Icon"
|
import MembershipLevelIcon from "@/components/Levels/Icon"
|
||||||
@@ -50,6 +51,7 @@ export default function GuestDetails({
|
|||||||
const utils = trpc.useUtils()
|
const utils = trpc.useUtils()
|
||||||
const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL)
|
const [currentStep, setCurrentStep] = useState(MODAL_STEPS.INITIAL)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] =
|
const [isModifyGuestDetailsOpen, setIsModifyGuestDetailsOpen] =
|
||||||
useState(false)
|
useState(false)
|
||||||
@@ -206,116 +208,120 @@ export default function GuestDetails({
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isMemberBooking ? (
|
{!isWebview(pathname) && (
|
||||||
<Button
|
|
||||||
variant="icon"
|
|
||||||
color="burgundy"
|
|
||||||
intent="secondary"
|
|
||||||
onClick={handleModifyMemberDetails}
|
|
||||||
disabled={isCancelled}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<MaterialIcon
|
|
||||||
icon="edit"
|
|
||||||
color="Icon/Interactive/Default"
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Modify guest details",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
<Button
|
{isMemberBooking ? (
|
||||||
variant="icon"
|
<Button
|
||||||
color="burgundy"
|
variant="icon"
|
||||||
intent="secondary"
|
color="burgundy"
|
||||||
onClick={() =>
|
intent="secondary"
|
||||||
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
|
onClick={handleModifyMemberDetails}
|
||||||
}
|
disabled={isCancelled}
|
||||||
disabled={isCancelled}
|
size="small"
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
<MaterialIcon
|
|
||||||
icon="edit"
|
|
||||||
color={
|
|
||||||
isCancelled
|
|
||||||
? "Icon/Interactive/Disabled"
|
|
||||||
: "Icon/Interactive/Default"
|
|
||||||
}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
defaultMessage: "Modify guest details",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Typography>
|
|
||||||
</Button>
|
|
||||||
{isModifyGuestDetailsOpen && (
|
|
||||||
<Modal
|
|
||||||
withActions
|
|
||||||
hideHeader
|
|
||||||
isOpen={isModifyGuestDetailsOpen}
|
|
||||||
onToggle={setIsModifyGuestDetailsOpen}
|
|
||||||
>
|
>
|
||||||
<Dialog
|
<MaterialIcon
|
||||||
aria-label={intl.formatMessage({
|
icon="edit"
|
||||||
defaultMessage: "Modify guest details",
|
color="Icon/Interactive/Default"
|
||||||
})}
|
size={20}
|
||||||
|
/>
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Modify guest details",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="icon"
|
||||||
|
color="burgundy"
|
||||||
|
intent="secondary"
|
||||||
|
onClick={() =>
|
||||||
|
setIsModifyGuestDetailsOpen(!isModifyGuestDetailsOpen)
|
||||||
|
}
|
||||||
|
disabled={isCancelled}
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
{({ close }) => (
|
<MaterialIcon
|
||||||
<FormProvider {...form}>
|
icon="edit"
|
||||||
<ModalContentWithActions
|
color={
|
||||||
title={intl.formatMessage({
|
isCancelled
|
||||||
defaultMessage: "Modify guest details",
|
? "Icon/Interactive/Disabled"
|
||||||
})}
|
: "Icon/Interactive/Default"
|
||||||
onClose={() => setIsModifyGuestDetailsOpen(false)}
|
}
|
||||||
content={
|
size={20}
|
||||||
guest && (
|
/>
|
||||||
<ModifyContact
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
guest={guest}
|
<span>
|
||||||
isFirstStep={isFirstStep}
|
{intl.formatMessage({
|
||||||
/>
|
defaultMessage: "Modify guest details",
|
||||||
)
|
})}
|
||||||
}
|
</span>
|
||||||
primaryAction={{
|
</Typography>
|
||||||
label: isFirstStep
|
</Button>
|
||||||
? intl.formatMessage({
|
{isModifyGuestDetailsOpen && (
|
||||||
defaultMessage: "Save updates",
|
<Modal
|
||||||
})
|
withActions
|
||||||
: intl.formatMessage({
|
hideHeader
|
||||||
defaultMessage: "Confirm",
|
isOpen={isModifyGuestDetailsOpen}
|
||||||
}),
|
onToggle={setIsModifyGuestDetailsOpen}
|
||||||
onClick: isFirstStep
|
>
|
||||||
? () => setCurrentStep(MODAL_STEPS.CONFIRMATION)
|
<Dialog
|
||||||
: form.handleSubmit(onSubmit),
|
aria-label={intl.formatMessage({
|
||||||
disabled: !form.formState.isValid || isLoading,
|
defaultMessage: "Modify guest details",
|
||||||
intent: isFirstStep ? "secondary" : "primary",
|
})}
|
||||||
}}
|
>
|
||||||
secondaryAction={{
|
{({ close }) => (
|
||||||
label: isFirstStep
|
<FormProvider {...form}>
|
||||||
? intl.formatMessage({
|
<ModalContentWithActions
|
||||||
defaultMessage: "Back",
|
title={intl.formatMessage({
|
||||||
})
|
defaultMessage: "Modify guest details",
|
||||||
: intl.formatMessage({
|
})}
|
||||||
defaultMessage: "Cancel",
|
onClose={() => setIsModifyGuestDetailsOpen(false)}
|
||||||
}),
|
content={
|
||||||
onClick: () => {
|
guest && (
|
||||||
close()
|
<ModifyContact
|
||||||
setCurrentStep(MODAL_STEPS.INITIAL)
|
guest={guest}
|
||||||
},
|
isFirstStep={isFirstStep}
|
||||||
}}
|
/>
|
||||||
/>
|
)
|
||||||
</FormProvider>
|
}
|
||||||
)}
|
primaryAction={{
|
||||||
</Dialog>
|
label: isFirstStep
|
||||||
</Modal>
|
? intl.formatMessage({
|
||||||
|
defaultMessage: "Save updates",
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
defaultMessage: "Confirm",
|
||||||
|
}),
|
||||||
|
onClick: isFirstStep
|
||||||
|
? () => setCurrentStep(MODAL_STEPS.CONFIRMATION)
|
||||||
|
: form.handleSubmit(onSubmit),
|
||||||
|
disabled: !form.formState.isValid || isLoading,
|
||||||
|
intent: isFirstStep ? "secondary" : "primary",
|
||||||
|
}}
|
||||||
|
secondaryAction={{
|
||||||
|
label: isFirstStep
|
||||||
|
? intl.formatMessage({
|
||||||
|
defaultMessage: "Back",
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
defaultMessage: "Cancel",
|
||||||
|
}),
|
||||||
|
onClick: () => {
|
||||||
|
close()
|
||||||
|
setCurrentStep(MODAL_STEPS.INITIAL)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormProvider>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
|
||||||
|
import { isWebview } from "@/constants/routes/webviews"
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
|
import AddToCalendar from "@/components/HotelReservation/AddToCalendar"
|
||||||
@@ -11,6 +14,8 @@ import AddToCalendarButton from "./AddToCalendarButton"
|
|||||||
import type { EventAttributes } from "ics"
|
import type { EventAttributes } from "ics"
|
||||||
|
|
||||||
export default function AddToCalendarAction() {
|
export default function AddToCalendarAction() {
|
||||||
|
const pathName = usePathname()
|
||||||
|
|
||||||
const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore(
|
const { checkInDate, checkOutDate, createDateTime, hotel } = useMyStayStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
checkInDate: state.bookedRoom.checkInDate,
|
checkInDate: state.bookedRoom.checkInDate,
|
||||||
@@ -44,6 +49,10 @@ export default function AddToCalendarAction() {
|
|||||||
hotel.hotelFacts.checkin.checkInTime
|
hotel.hotelFacts.checkin.checkInTime
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (isWebview(pathName)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AddToCalendar
|
<AddToCalendar
|
||||||
checkInDate={checkInDate}
|
checkInDate={checkInDate}
|
||||||
|
|||||||
320
apps/scandic-web/components/HotelReservation/MyStay/index.tsx
Normal file
320
apps/scandic-web/components/HotelReservation/MyStay/index.tsx
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
import { cookies } from "next/headers"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
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_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 SidePeek from "@/components/HotelReservation/SidePeek"
|
||||||
|
import Image from "@/components/Image"
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import MyStayProvider from "@/providers/MyStay"
|
||||||
|
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
||||||
|
import * as maskValue from "@/utils/maskValue"
|
||||||
|
import { parseRefId } from "@/utils/refId"
|
||||||
|
import { getCurrentWebUrl } from "@/utils/url"
|
||||||
|
|
||||||
|
import styles from "./index.module.css"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
|
import type { SafeUser } from "@/types/user"
|
||||||
|
|
||||||
|
export default async function MyStay(props: {
|
||||||
|
refId?: string
|
||||||
|
lang: Lang
|
||||||
|
isWebview?: boolean
|
||||||
|
}) {
|
||||||
|
const { refId, lang, isWebview } = props
|
||||||
|
|
||||||
|
if (!refId) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { confirmationNumber, lastName } = parseRefId(refId)
|
||||||
|
if (!confirmationNumber) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const params = new URLSearchParams(bv)
|
||||||
|
const firstName = params.get("firstName")
|
||||||
|
const email = params.get("email")
|
||||||
|
|
||||||
|
if (firstName && email) {
|
||||||
|
bookingConfirmation = await findBooking(
|
||||||
|
confirmationNumber,
|
||||||
|
lastName,
|
||||||
|
firstName,
|
||||||
|
email
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<RenderAdditionalInfoForm
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lastName={lastName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<RenderAdditionalInfoForm
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lastName={lastName}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookingConfirmation) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { additionalData, booking, hotel, roomCategories } = bookingConfirmation
|
||||||
|
|
||||||
|
const user = await getProfileSafely()
|
||||||
|
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
void getSavedPaymentCardsSafely(savedPaymentCardsInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
let breakfastPackages = null
|
||||||
|
if (shouldFetchBreakfastPackages) {
|
||||||
|
breakfastPackages = await getPackages(packagesInput)
|
||||||
|
}
|
||||||
|
let savedCreditCards = null
|
||||||
|
if (user) {
|
||||||
|
savedCreditCards = await getSavedPaymentCardsSafely(
|
||||||
|
savedPaymentCardsInput
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let ancillaryPackagesPromise = null
|
||||||
|
if (booking.showAncillaries) {
|
||||||
|
ancillaryPackagesPromise = getAncillaryPackages({
|
||||||
|
fromDate,
|
||||||
|
hotelId: hotel.operaId,
|
||||||
|
toDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSrc =
|
||||||
|
hotel.hotelContent.images.imageSizes.large ??
|
||||||
|
additionalData.gallery?.heroImages[0]?.imageSizes.large ??
|
||||||
|
hotel.galleryImages[0]?.imageSizes.large
|
||||||
|
|
||||||
|
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
||||||
|
const promoUrl = !env.isLangLive(lang)
|
||||||
|
? new URL(getCurrentWebUrl({ path: "/", lang }))
|
||||||
|
: new URL(`${baseUrl}/${lang}/`)
|
||||||
|
|
||||||
|
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
|
||||||
|
? ({
|
||||||
|
...user,
|
||||||
|
email: maskValue.email(user.email),
|
||||||
|
phoneNumber: maskValue.phone(user.phoneNumber ?? ""),
|
||||||
|
} satisfies SafeUser)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyStayProvider
|
||||||
|
bookingConfirmation={maskedBookingConfirmation}
|
||||||
|
breakfastPackages={breakfastPackages}
|
||||||
|
lang={lang}
|
||||||
|
linkedReservationsPromise={linkedReservationsPromise}
|
||||||
|
refId={booking.refId}
|
||||||
|
roomCategories={roomCategories}
|
||||||
|
savedCreditCards={savedCreditCards}
|
||||||
|
>
|
||||||
|
<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 hotel={hotel} />
|
||||||
|
{!isWebview && (
|
||||||
|
<Promo
|
||||||
|
title={intl.formatMessage({
|
||||||
|
defaultMessage: "Book your next stay",
|
||||||
|
})}
|
||||||
|
text={intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"Get inspired and start dreaming beyond your next trip. Explore more Scandic destinations.",
|
||||||
|
})}
|
||||||
|
buttonText={intl.formatMessage({
|
||||||
|
defaultMessage: "Explore Scandic hotels",
|
||||||
|
})}
|
||||||
|
href={promoUrl.toString()}
|
||||||
|
image={hotel.hotelContent.images}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<SidePeek />
|
||||||
|
</MyStayProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === ERROR_BAD_REQUEST) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<AdditionalInfoForm
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lastName={lastName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (access === ERROR_UNAUTHORIZED) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.logIn}>
|
||||||
|
<Typography variant="Title/md">
|
||||||
|
<h1>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "You need to be logged in to view your booking",
|
||||||
|
})}
|
||||||
|
</h1>
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="Body/Lead text">
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"And you need to be logged in with the same member account that made the booking.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
function RenderAdditionalInfoForm({
|
||||||
|
confirmationNumber,
|
||||||
|
lastName,
|
||||||
|
}: {
|
||||||
|
confirmationNumber: string
|
||||||
|
lastName: string
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<AdditionalInfoForm
|
||||||
|
confirmationNumber={confirmationNumber}
|
||||||
|
lastName={lastName}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -93,3 +93,7 @@ export const myStayWebviews = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const refreshWebviews = [...Object.values(refreshUrl)]
|
export const refreshWebviews = [...Object.values(refreshUrl)]
|
||||||
|
|
||||||
|
export function isWebview(path: string) {
|
||||||
|
return webviews.includes(path)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user