Merged in feat/sw-2403-mystay-webview (pull request #1828)

Feat/sw-2403 - Adding webview for MyStay

* feat/webview - added for my stay

* wip

* Passing headers so we can get the lang

* Cleanup

* Refactored and some performance improvements


Approved-by: Christian Andolf
This commit is contained in:
Linus Flood
2025-04-17 08:48:52 +00:00
parent 3ce3063973
commit cfa8c166a3
7 changed files with 169 additions and 56 deletions

View File

@@ -0,0 +1,27 @@
# 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.

View File

@@ -0,0 +1,21 @@
import { notFound } from "next/navigation"
import { env } from "@/env/server"
import type { Metadata } from "next"
export const metadata: Metadata = {
robots: {
index: false,
follow: false,
},
}
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren) {
if (!env.ENABLE_BOOKING_FLOW) {
return notFound()
}
return <>{children}</>
}

View File

@@ -0,0 +1,14 @@
import SidePeek from "@/components/HotelReservation/SidePeek"
import type { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return (
<>
{children}
<SidePeek />
</>
)
}

View File

@@ -0,0 +1 @@
export { MyStaySkeleton as default } from "@/components/HotelReservation/MyStay/myStaySkeleton"

View File

@@ -0,0 +1,21 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { MyStay } from "@/components/HotelReservation/MyStay"
import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkeleton"
import type { LangParams, PageArgs } from "@/types/params"
export default function MyStayPage({
searchParams,
}: PageArgs<LangParams, { RefId?: string }>) {
if (!searchParams.RefId) {
notFound()
}
return (
<Suspense fallback={<MyStaySkeleton />}>
<MyStay refId={searchParams.RefId} />
</Suspense>
)
}

View File

@@ -52,12 +52,22 @@ const refreshUrl = {
sv: `/sv/webview/refresh`,
}
const myStay = {
da: `/da/webview/hotelreservation/my-stay`,
de: `/de/webview/hotelreservation/my-stay`,
en: `/en/webview/hotelreservation/my-stay`,
fi: `/fi/webview/hotelreservation/my-stay`,
no: `/no/webview/hotelreservation/my-stay`,
sv: `/sv/webview/hotelreservation/my-stay`,
}
export const webviews = [
...Object.values(benefits),
...Object.values(overview),
...Object.values(points),
...Object.values(programOverview),
...Object.values(refreshUrl),
...Object.values(myStay),
]
export const myPagesWebviews = [
@@ -68,4 +78,6 @@ export const myPagesWebviews = [
export const loyaltyPagesWebviews = [...Object.values(programOverview)]
export const myStayWebviews = [...Object.values(myStay)]
export const refreshWebviews = [...Object.values(refreshUrl)]

View File

@@ -3,6 +3,7 @@ import { type NextMiddleware, NextResponse } from "next/server"
import {
loyaltyPagesWebviews,
myPagesWebviews,
myStayWebviews,
refreshWebviews,
webviews,
} from "@/constants/routes/webviews"
@@ -16,6 +17,7 @@ import { findLang } from "@/utils/languages"
import { getDefaultRequestHeaders } from "./utils"
import type { MiddlewareMatcher } from "@/types/middleware"
import type { Lang } from "@/constants/languages"
export const middleware: NextMiddleware = async (request) => {
const { nextUrl } = request
@@ -62,41 +64,17 @@ export const middleware: NextMiddleware = async (request) => {
)
}
const pathNameWithoutLang = nextUrl.pathname.replace(`/${lang}/webview`, "")
const { uid } = await fetchAndCacheEntry(pathNameWithoutLang, lang)
if (!uid) {
throw notFound(
`Unable to resolve CMS entry for locale "${lang}": ${pathNameWithoutLang}`
)
}
headers.set("x-uid", uid)
const webviewToken = request.cookies.get("webviewToken")
if (webviewToken) {
// since the token exists, this is a subsequent visit
// we're done, allow it
if (myPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
{
request: {
headers,
},
}
)
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl),
{
request: {
headers,
},
}
)
} else {
return notFound()
}
return handleWebviewRewrite({
nextUrl,
headers,
decryptedData: null,
lang,
setCookie: false,
})
}
try {
@@ -124,31 +102,13 @@ export const middleware: NextMiddleware = async (request) => {
headers.append("Cookie", `webviewToken=${decryptedData}`)
if (myPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
{
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
request: {
headers,
},
}
)
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl),
{
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
request: {
headers,
},
}
)
}
return handleWebviewRewrite({
nextUrl,
headers,
decryptedData,
lang,
setCookie: true,
})
} catch (e) {
if (e instanceof Error) {
console.error("Error in webView middleware")
@@ -159,6 +119,63 @@ export const middleware: NextMiddleware = async (request) => {
}
}
async function handleWebviewRewrite({
nextUrl,
headers,
decryptedData,
lang,
setCookie,
}: {
nextUrl: URL
headers: Headers
decryptedData: string | null
lang: Lang
setCookie: boolean
}) {
const path = nextUrl.pathname
if (myStayWebviews.includes(path)) {
return NextResponse.next({ request: { headers } })
}
const pathNameWithoutLang = path.replace(`/${lang}/webview`, "")
const { uid } = await fetchAndCacheEntry(pathNameWithoutLang, lang)
if (uid) {
headers.set("x-uid", uid)
}
if (myPagesWebviews.includes(path)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
{
request: { headers },
...(setCookie && {
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
}),
}
)
}
if (loyaltyPagesWebviews.includes(path)) {
return NextResponse.rewrite(
new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl),
{
request: { headers },
...(setCookie && {
headers: {
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
},
}),
}
)
}
return notFound()
}
export const matcher: MiddlewareMatcher = (request) => {
const { nextUrl } = request