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`,
|
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 = [
|
export const webviews = [
|
||||||
...Object.values(benefits),
|
...Object.values(benefits),
|
||||||
...Object.values(overview),
|
...Object.values(overview),
|
||||||
...Object.values(points),
|
...Object.values(points),
|
||||||
...Object.values(programOverview),
|
...Object.values(programOverview),
|
||||||
...Object.values(refreshUrl),
|
...Object.values(refreshUrl),
|
||||||
|
...Object.values(myStay),
|
||||||
]
|
]
|
||||||
|
|
||||||
export const myPagesWebviews = [
|
export const myPagesWebviews = [
|
||||||
@@ -68,4 +78,6 @@ export const myPagesWebviews = [
|
|||||||
|
|
||||||
export const loyaltyPagesWebviews = [...Object.values(programOverview)]
|
export const loyaltyPagesWebviews = [...Object.values(programOverview)]
|
||||||
|
|
||||||
|
export const myStayWebviews = [...Object.values(myStay)]
|
||||||
|
|
||||||
export const refreshWebviews = [...Object.values(refreshUrl)]
|
export const refreshWebviews = [...Object.values(refreshUrl)]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type NextMiddleware, NextResponse } from "next/server"
|
|||||||
import {
|
import {
|
||||||
loyaltyPagesWebviews,
|
loyaltyPagesWebviews,
|
||||||
myPagesWebviews,
|
myPagesWebviews,
|
||||||
|
myStayWebviews,
|
||||||
refreshWebviews,
|
refreshWebviews,
|
||||||
webviews,
|
webviews,
|
||||||
} from "@/constants/routes/webviews"
|
} from "@/constants/routes/webviews"
|
||||||
@@ -16,6 +17,7 @@ import { findLang } from "@/utils/languages"
|
|||||||
import { getDefaultRequestHeaders } from "./utils"
|
import { getDefaultRequestHeaders } from "./utils"
|
||||||
|
|
||||||
import type { MiddlewareMatcher } from "@/types/middleware"
|
import type { MiddlewareMatcher } from "@/types/middleware"
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
export const middleware: NextMiddleware = async (request) => {
|
export const middleware: NextMiddleware = async (request) => {
|
||||||
const { nextUrl } = 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")
|
const webviewToken = request.cookies.get("webviewToken")
|
||||||
if (webviewToken) {
|
if (webviewToken) {
|
||||||
// since the token exists, this is a subsequent visit
|
// since the token exists, this is a subsequent visit
|
||||||
// we're done, allow it
|
// we're done, allow it
|
||||||
if (myPagesWebviews.includes(nextUrl.pathname)) {
|
return handleWebviewRewrite({
|
||||||
return NextResponse.rewrite(
|
nextUrl,
|
||||||
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
|
headers,
|
||||||
{
|
decryptedData: null,
|
||||||
request: {
|
lang,
|
||||||
headers,
|
setCookie: false,
|
||||||
},
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
} else if (loyaltyPagesWebviews.includes(nextUrl.pathname)) {
|
|
||||||
return NextResponse.rewrite(
|
|
||||||
new URL(`/${lang}/webview/loyalty-page/${uid}`, nextUrl),
|
|
||||||
{
|
|
||||||
request: {
|
|
||||||
headers,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return notFound()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -124,31 +102,13 @@ export const middleware: NextMiddleware = async (request) => {
|
|||||||
|
|
||||||
headers.append("Cookie", `webviewToken=${decryptedData}`)
|
headers.append("Cookie", `webviewToken=${decryptedData}`)
|
||||||
|
|
||||||
if (myPagesWebviews.includes(nextUrl.pathname)) {
|
return handleWebviewRewrite({
|
||||||
return NextResponse.rewrite(
|
nextUrl,
|
||||||
new URL(`/${lang}/webview/account-page/${uid}`, nextUrl),
|
headers,
|
||||||
{
|
decryptedData,
|
||||||
headers: {
|
lang,
|
||||||
"Set-Cookie": `webviewToken=${decryptedData}; Secure; HttpOnly; Path=/; SameSite=Strict;`,
|
setCookie: true,
|
||||||
},
|
})
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
console.error("Error in webView middleware")
|
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) => {
|
export const matcher: MiddlewareMatcher = (request) => {
|
||||||
const { nextUrl } = request
|
const { nextUrl } = request
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user