diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx
index ca2f08674..35dbbdcee 100644
--- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx
+++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx
@@ -1,7 +1,9 @@
+import { cookies } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { Suspense } from "react"
import { BookingErrorCodeEnum } from "@/constants/booking"
+import { FamilyAndFriendsCodes } from "@/constants/booking"
import { selectRate } from "@/constants/routes/hotelReservation"
import {
getBreakfastPackages,
@@ -17,6 +19,7 @@ import RoomOne from "@/components/HotelReservation/EnterDetails/Room/One"
import DesktopSummary from "@/components/HotelReservation/EnterDetails/Summary/Desktop"
import MobileSummary from "@/components/HotelReservation/EnterDetails/Summary/Mobile"
import EnterDetailsTrackingWrapper from "@/components/HotelReservation/EnterDetails/Tracking"
+import FnFNotAllowedAlert from "@/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert"
import RoomProvider from "@/providers/Details/RoomProvider"
import EnterDetailsProvider from "@/providers/EnterDetailsProvider"
import { convertSearchParamsToObj } from "@/utils/url"
@@ -38,6 +41,18 @@ export default async function DetailsPage({
delete booking.modifyRateIndex
}
+ if (
+ booking.bookingCode &&
+ FamilyAndFriendsCodes.includes(booking.bookingCode)
+ ) {
+ const cookieStore = cookies()
+ const isInValidFNF = cookieStore.get("sc")?.value !== "1"
+
+ if (isInValidFNF) {
+ return
+ }
+ }
+
const breakfastInput = {
adults: 1,
fromDate: booking.fromDate,
diff --git a/apps/scandic-web/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert.module.css b/apps/scandic-web/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert.module.css
new file mode 100644
index 000000000..1df3eb7da
--- /dev/null
+++ b/apps/scandic-web/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert.module.css
@@ -0,0 +1,6 @@
+.fnfMain {
+ max-width: var(--max-width-page);
+ margin: auto;
+ min-height: 30dvh;
+ padding: var(--Spacing-x5) 0;
+}
diff --git a/apps/scandic-web/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert.tsx b/apps/scandic-web/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert.tsx
new file mode 100644
index 000000000..01462e869
--- /dev/null
+++ b/apps/scandic-web/components/HotelReservation/FnFNotAllowedAlert/FnFNotAllowedAlert.tsx
@@ -0,0 +1,22 @@
+import Alert from "@/components/TempDesignSystem/Alert"
+import { getIntl } from "@/i18n"
+
+import styles from "./FnFNotAllowedAlert.module.css"
+
+import { AlertTypeEnum } from "@/types/enums/alert"
+
+export default async function FnFNotAllowedAlert() {
+ const intl = await getIntl()
+
+ return (
+
+ )
+}
diff --git a/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx b/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx
index ecbaa274f..1f28f54af 100644
--- a/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectHotel/index.tsx
@@ -1,9 +1,11 @@
import stringify from "json-stable-stringify-without-jsonify"
+import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
+import { FamilyAndFriendsCodes } from "@/constants/booking"
import {
alternativeHotels,
alternativeHotelsMap,
@@ -21,6 +23,7 @@ import { getIntl } from "@/i18n"
import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { convertObjToSearchParams } from "@/utils/url"
+import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
import HotelCardListing from "../HotelCardListing"
import BookingCodeFilter from "./BookingCodeFilter"
import { getFiltersFromHotels, getHotels } from "./helpers"
@@ -67,6 +70,15 @@ export default async function SelectHotel({
if (!city) return notFound()
+ if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) {
+ const cookieStore = cookies()
+ const isInValidFNF = cookieStore.get("sc")?.value !== "1"
+
+ if (isInValidFNF) {
+ return
+ }
+ }
+
const hotels = await getHotels(
selectHotelParams,
isAlternativeFor,
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx
index 83250b678..687a450db 100644
--- a/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/SelectRate/index.tsx
@@ -1,8 +1,9 @@
import stringify from "json-stable-stringify-without-jsonify"
+import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import { Suspense } from "react"
-import { REDEMPTION } from "@/constants/booking"
+import { FamilyAndFriendsCodes, REDEMPTION } from "@/constants/booking"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
@@ -13,6 +14,7 @@ import { getHotelSearchDetails } from "@/utils/hotelSearchDetails"
import { convertSearchParamsToObj } from "@/utils/url"
import AvailabilityError from "./AvailabilityError"
+import FnFNotAllowedAlert from "../FnFNotAllowedAlert/FnFNotAllowedAlert"
import { getValidDates } from "./getValidDates"
import { getTracking } from "./tracking"
@@ -73,17 +75,26 @@ export default async function SelectRatePage({
const booking = convertSearchParamsToObj(searchParams)
+ let isInValidFNF = false
+ if (bookingCode && FamilyAndFriendsCodes.includes(bookingCode)) {
+ const cookieStore = cookies()
+ isInValidFNF = cookieStore.get("sc")?.value !== "1"
+ }
const suspenseKey = stringify(searchParams)
return (
<>
-
+ {isInValidFNF ? (
+
+ ) : (
+
+ )}
{
webView,
dateFormat,
bookingFlow,
+ familyAndFriends,
sasXScandic,
cmsContent,
redirect,
diff --git a/apps/scandic-web/middlewares/familyAndFriends.ts b/apps/scandic-web/middlewares/familyAndFriends.ts
new file mode 100644
index 000000000..58506ee5f
--- /dev/null
+++ b/apps/scandic-web/middlewares/familyAndFriends.ts
@@ -0,0 +1,46 @@
+import { type NextMiddleware, NextResponse } from "next/server"
+
+import { getPublicNextURL } from "@/server/utils"
+
+import { findLang } from "@/utils/languages"
+
+import { getDefaultRequestHeaders } from "./utils"
+
+import type { MiddlewareMatcher } from "@/types/middleware"
+
+export const middleware: NextMiddleware = async (request) => {
+ const lang = findLang(request.nextUrl.pathname)!
+ const nextUrlPublic = getPublicNextURL(request)
+ const redirectUrl = new URL(`/${lang}`, nextUrlPublic)
+
+ const key = request.nextUrl.searchParams.get("key")
+ const code = request.nextUrl.searchParams.get("code")
+
+ const md5 = require("md5")
+ if (
+ key &&
+ code &&
+ key === md5("scandic" + new Date().toISOString().substring(0, 10))
+ ) {
+ const headers = new Headers()
+ // Set cookie for this device/user to be valid to use the FnF code for 3 days starting now
+ headers.append(
+ "set-cookie",
+ `sc=1; max-age=259200; Path=/; HttpOnly; SameSite=Lax`
+ )
+ const redirectOpts = {
+ headers,
+ }
+ redirectUrl.searchParams.set("bookingCode", code)
+ return NextResponse.redirect(redirectUrl, redirectOpts)
+ }
+
+ const headers = getDefaultRequestHeaders(request)
+ return NextResponse.redirect(redirectUrl, {
+ headers,
+ })
+}
+
+export const matcher: MiddlewareMatcher = (request) => {
+ return request.nextUrl.pathname === "/en/familyandfriends"
+}
diff --git a/apps/scandic-web/package.json b/apps/scandic-web/package.json
index cbad11c74..b785f3ca1 100644
--- a/apps/scandic-web/package.json
+++ b/apps/scandic-web/package.json
@@ -91,6 +91,7 @@
"jsonwebtoken": "^9.0.2",
"libphonenumber-js": "^1.10.60",
"lodash-es": "^4.17.21",
+ "md5": "^2.3.0",
"nanoid": "^5.0.9",
"next": "^14.2.25",
"next-auth": "5.0.0-beta.19",
diff --git a/yarn.lock b/yarn.lock
index 296b20cbc..5fc128593 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6452,6 +6452,7 @@ __metadata:
lint-staged: "npm:^15.2.2"
lodash-es: "npm:^4.17.21"
material-symbols: "npm:^0.29.0"
+ md5: "npm:^2.3.0"
nanoid: "npm:^5.0.9"
netlify-plugin-cypress: "npm:^2.2.1"
next: "npm:^14.2.25"
@@ -10282,6 +10283,13 @@ __metadata:
languageName: node
linkType: hard
+"charenc@npm:0.0.2":
+ version: 0.0.2
+ resolution: "charenc@npm:0.0.2"
+ checksum: 10c0/a45ec39363a16799d0f9365c8dd0c78e711415113c6f14787a22462ef451f5013efae8a28f1c058f81fc01f2a6a16955f7a5fd0cd56247ce94a45349c89877d8
+ languageName: node
+ linkType: hard
+
"check-error@npm:^2.1.1":
version: 2.1.1
resolution: "check-error@npm:2.1.1"
@@ -10938,6 +10946,13 @@ __metadata:
languageName: node
linkType: hard
+"crypt@npm:0.0.2":
+ version: 0.0.2
+ resolution: "crypt@npm:0.0.2"
+ checksum: 10c0/adbf263441dd801665d5425f044647533f39f4612544071b1471962209d235042fb703c27eea2795c7c53e1dfc242405173003f83cf4f4761a633d11f9653f18
+ languageName: node
+ linkType: hard
+
"css-select@npm:^5.1.0":
version: 5.1.0
resolution: "css-select@npm:5.1.0"
@@ -14151,6 +14166,13 @@ __metadata:
languageName: node
linkType: hard
+"is-buffer@npm:~1.1.6":
+ version: 1.1.6
+ resolution: "is-buffer@npm:1.1.6"
+ checksum: 10c0/ae18aa0b6e113d6c490ad1db5e8df9bdb57758382b313f5a22c9c61084875c6396d50bbf49315f5b1926d142d74dfb8d31b40d993a383e0a158b15fea7a82234
+ languageName: node
+ linkType: hard
+
"is-bun-module@npm:^1.0.2":
version: 1.3.0
resolution: "is-bun-module@npm:1.3.0"
@@ -16457,6 +16479,17 @@ __metadata:
languageName: node
linkType: hard
+"md5@npm:^2.3.0":
+ version: 2.3.0
+ resolution: "md5@npm:2.3.0"
+ dependencies:
+ charenc: "npm:0.0.2"
+ crypt: "npm:0.0.2"
+ is-buffer: "npm:~1.1.6"
+ checksum: 10c0/14a21d597d92e5b738255fbe7fe379905b8cb97e0a49d44a20b58526a646ec5518c337b817ce0094ca94d3e81a3313879c4c7b510d250c282d53afbbdede9110
+ languageName: node
+ linkType: hard
+
"mdn-data@npm:2.0.28":
version: 2.0.28
resolution: "mdn-data@npm:2.0.28"