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:
@@ -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.
|
||||
@@ -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}</>
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MyStaySkeleton as default } from "@/components/HotelReservation/MyStay/myStaySkeleton"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user