From cfa8c166a3f400d3422d57bd1aa759bb878ceca6 Mon Sep 17 00:00:00 2001 From: Linus Flood Date: Thu, 17 Apr 2025 08:48:52 +0000 Subject: [PATCH] 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 --- .../[lang]/webview/hotelreservation/README.md | 27 ++++ .../webview/hotelreservation/layout.tsx | 21 +++ .../hotelreservation/my-stay/layout.tsx | 14 ++ .../hotelreservation/my-stay/loading.tsx | 1 + .../webview/hotelreservation/my-stay/page.tsx | 21 +++ apps/scandic-web/constants/routes/webviews.ts | 12 ++ apps/scandic-web/middlewares/webView.ts | 129 ++++++++++-------- 7 files changed, 169 insertions(+), 56 deletions(-) create mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/README.md create mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/layout.tsx create mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx create mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/loading.tsx create mode 100644 apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/README.md b/apps/scandic-web/app/[lang]/webview/hotelreservation/README.md new file mode 100644 index 000000000..fdc0f1ad4 --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/README.md @@ -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. diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/layout.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/layout.tsx new file mode 100644 index 000000000..7d99ab2d7 --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/layout.tsx @@ -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} +} diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx new file mode 100644 index 000000000..97481c9bf --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/layout.tsx @@ -0,0 +1,14 @@ +import SidePeek from "@/components/HotelReservation/SidePeek" + +import type { LangParams, LayoutArgs } from "@/types/params" + +export default function HotelReservationLayout({ + children, +}: React.PropsWithChildren>) { + return ( + <> + {children} + + + ) +} diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/loading.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/loading.tsx new file mode 100644 index 000000000..bae8bb797 --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/loading.tsx @@ -0,0 +1 @@ +export { MyStaySkeleton as default } from "@/components/HotelReservation/MyStay/myStaySkeleton" diff --git a/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx new file mode 100644 index 000000000..aa1ec16aa --- /dev/null +++ b/apps/scandic-web/app/[lang]/webview/hotelreservation/my-stay/page.tsx @@ -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) { + if (!searchParams.RefId) { + notFound() + } + + return ( + }> + + + ) +} diff --git a/apps/scandic-web/constants/routes/webviews.ts b/apps/scandic-web/constants/routes/webviews.ts index 5eda7b753..7f5788bcd 100644 --- a/apps/scandic-web/constants/routes/webviews.ts +++ b/apps/scandic-web/constants/routes/webviews.ts @@ -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)] diff --git a/apps/scandic-web/middlewares/webView.ts b/apps/scandic-web/middlewares/webView.ts index 240c818ae..972945b91 100644 --- a/apps/scandic-web/middlewares/webView.ts +++ b/apps/scandic-web/middlewares/webView.ts @@ -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