From 2ae3fcb6094504f41445b18813695db6c5b9fc06 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Mon, 24 Nov 2025 14:46:39 +0000 Subject: [PATCH] Merged in fix/STAY-17-find-my-booking-errors (pull request #3181) fix: improve error messages in find my booking flow * fix: improve error messages in find my booking flow Approved-by: Linus Flood Approved-by: Erik Tiekstra --- .../hotelreservation/get-booking/page.tsx | 19 ++- .../FindMyBooking/AdditionalInfoForm.tsx | 26 +--- .../Title/findMyBookingTitle.module.css | 3 + .../FindMyBooking/Title/index.tsx | 36 +++++ .../FindMyBooking/findMyBooking.module.css | 4 + .../HotelReservation/FindMyBooking/index.tsx | 125 ++++++++++-------- .../HotelReservation/FindMyBooking/utils.ts | 40 ++++++ .../HotelReservation/MyStay/index.tsx | 78 ++++++----- packages/trpc/lib/routers/booking/utils.ts | 2 +- 9 files changed, 213 insertions(+), 120 deletions(-) create mode 100644 apps/scandic-web/components/HotelReservation/FindMyBooking/Title/findMyBookingTitle.module.css create mode 100644 apps/scandic-web/components/HotelReservation/FindMyBooking/Title/index.tsx create mode 100644 apps/scandic-web/components/HotelReservation/FindMyBooking/utils.ts diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/get-booking/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/get-booking/page.tsx index 35b05864d..75b454e1b 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/get-booking/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/get-booking/page.tsx @@ -1,3 +1,5 @@ +import { cookies } from "next/headers" + import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK" import { TrackingChannelEnum, @@ -9,8 +11,11 @@ import { getIntl } from "@/i18n" import styles from "./page.module.css" +import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue" import type { Lang } from "@scandic-hotels/common/constants/language" +import type { FindMyBookingErrorEnum } from "@/components/HotelReservation/FindMyBooking/utils" + export default async function GetBookingPage( props: PageProps<"/[lang]/hotelreservation/get-booking"> ) { @@ -27,11 +32,23 @@ export default async function GetBookingPage( siteVersion: "new-web", } + const searchParams = await props.searchParams + const error = searchParams.error as FindMyBookingErrorEnum | undefined + const cookieStore = await cookies() + + const previousValuesCookie = cookieStore.get("bv")?.value + + let defaultValues: AdditionalInfoCookieValue | undefined + // Only prepopulate previous values if there is an error + if (previousValuesCookie && error) { + defaultValues = JSON.parse(previousValuesCookie) + } + return (
- +
) diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx b/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx index 8b27e5c73..af0c92ddb 100644 --- a/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/AdditionalInfoForm.tsx @@ -5,9 +5,7 @@ import { useRouter } from "next/navigation" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" -import Body from "@scandic-hotels/design-system/Body" -import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" -import Title from "@scandic-hotels/design-system/Title" +import { Button } from "@scandic-hotels/design-system/Button" import Input from "@/components/TempDesignSystem/Form/Input" @@ -15,6 +13,7 @@ import { type AdditionalInfoFormSchema, additionalInfoFormSchema, } from "./schema" +import { Title } from "./Title" import styles from "./findMyBooking.module.css" @@ -51,20 +50,7 @@ export default function AdditionalInfoForm({ return (
-
- - {intl.formatMessage({ - id: "hotelReservation.findMyBooking.title", - defaultMessage: "Find your booking", - })} - - - {intl.formatMessage({ - id: "hotelReservation.findMyBooking.additionalInfoText", - defaultMessage: "We need some details to confirm your identity.", - })} - -
+ <div className={styles.inputs}> <Input label={intl.formatMessage({ @@ -87,9 +73,9 @@ export default function AdditionalInfoForm({ <div className={styles.buttons}> <Button type="submit" - intent="primary" - theme="base" - disabled={form.formState.isSubmitting} + variant="Primary" + size="Medium" + isDisabled={form.formState.isSubmitting} > {intl.formatMessage({ id: "common.confirm", diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/Title/findMyBookingTitle.module.css b/apps/scandic-web/components/HotelReservation/FindMyBooking/Title/findMyBookingTitle.module.css new file mode 100644 index 000000000..6b27eca4e --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/Title/findMyBookingTitle.module.css @@ -0,0 +1,3 @@ +.title { + color: var(--Text-Heading); +} diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/Title/index.tsx b/apps/scandic-web/components/HotelReservation/FindMyBooking/Title/index.tsx new file mode 100644 index 000000000..d1f370bae --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/Title/index.tsx @@ -0,0 +1,36 @@ +"use client" +import { useIntl } from "react-intl" + +import { Typography } from "@scandic-hotels/design-system/Typography" + +import styles from "./findMyBookingTitle.module.css" + +export function Title({ isAdditional = false }: { isAdditional?: boolean }) { + const intl = useIntl() + + const message = isAdditional + ? intl.formatMessage({ + id: "hotelReservation.findMyBooking.additionalInfoText", + defaultMessage: "We need some details to confirm your identity.", + }) + : intl.formatMessage({ + id: "findMyBooking.manageBooking", + defaultMessage: + "View and manage your booking made via our website or app.", + }) + return ( + <div> + <Typography variant="Title/sm" className={styles.title}> + <h2> + {intl.formatMessage({ + id: "findMyBooking.findYourStay", + defaultMessage: "Find your stay", + })} + </h2> + </Typography> + <Typography variant="Body/Paragraph/mdRegular"> + <h3>{message}</h3> + </Typography> + </div> + ) +} diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css b/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css index e3eb29d07..a813d3c75 100644 --- a/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/findMyBooking.module.css @@ -9,6 +9,10 @@ padding: 0 var(--Space-x3); } +.alert { + margin: 0 var(--Space-x3); +} + .inputs { display: grid; gap: var(--Space-x3); diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx b/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx index 4419fc933..d18486696 100644 --- a/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/index.tsx @@ -5,37 +5,48 @@ import { useRouter } from "next/navigation" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" +import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" import { customerService } from "@scandic-hotels/common/constants/routes/customerService" import { myStay } from "@scandic-hotels/common/constants/routes/myStay" import { logger } from "@scandic-hotels/common/logger" -import Body from "@scandic-hotels/design-system/Body" -import Caption from "@scandic-hotels/design-system/Caption" -import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton" -import Link from "@scandic-hotels/design-system/OldDSLink" -import Title from "@scandic-hotels/design-system/Title" +import { Alert } from "@scandic-hotels/design-system/Alert" +import { Button } from "@scandic-hotels/design-system/Button" +import { TextLink } from "@scandic-hotels/design-system/TextLink" import { toast } from "@scandic-hotels/design-system/Toast" +import { Typography } from "@scandic-hotels/design-system/Typography" import { trpc } from "@scandic-hotels/trpc/client" import Input from "@/components/TempDesignSystem/Form/Input" import useLang from "@/hooks/useLang" import { type FindMyBookingFormSchema, findMyBookingFormSchema } from "./schema" +import { Title } from "./Title" +import { type FindMyBookingErrorEnum, getErrorMessage } from "./utils" import styles from "./findMyBooking.module.css" import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue" -export default function FindMyBooking() { +const DEFAULT_VALUES: FindMyBookingFormSchema = { + confirmationNumber: "", + firstName: "", + lastName: "", + email: "", +} + +export default function FindMyBooking({ + error, + defaultValues = DEFAULT_VALUES, +}: { + error?: FindMyBookingErrorEnum + defaultValues: AdditionalInfoCookieValue | undefined +}) { const router = useRouter() + const intl = useIntl() const lang = useLang() const form = useForm<FindMyBookingFormSchema>({ - defaultValues: { - confirmationNumber: "", - firstName: "", - lastName: "", - email: "", - }, + defaultValues, resolver: zodResolver(findMyBookingFormSchema), mode: "all", criteriaMode: "all", @@ -69,24 +80,20 @@ export default function FindMyBooking() { }) } + const errorMessage = getErrorMessage(intl, error) + return ( <FormProvider {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className={styles.form}> - <div> - <Title level="h2" as="h3"> - {intl.formatMessage({ - id: "findMyBooking.findYourStay", - defaultMessage: "Find your stay", - })} - - - {intl.formatMessage({ - id: "findMyBooking.manageBooking", - defaultMessage: - "View and manage your booking made via our website or app.", - })} - - + + {errorMessage ? ( + <Alert + type={AlertTypeEnum.Alarm} + heading={errorMessage.heading} + text={errorMessage.text} + className={styles.alert} + /> + ) : null} <div className={[styles.inputs, styles.grid].join(" ")}> <Input label={intl.formatMessage({ @@ -124,39 +131,43 @@ export default function FindMyBooking() { </div> <div className={styles.buttons}> <div className={styles.footnote}> - <Caption type="bold"> - {intl.formatMessage({ - id: "findMyBooking.cantFindYourStay", - defaultMessage: "Can't find your stay?", - })} - </Caption> - <Caption> - {intl.formatMessage( - { - id: "findMyBooking.customerService", - defaultMessage: - "Please contact <link>customer service</link>.", - }, - { - link: (str) => ( - <Link - href={customerService[lang]} - size="small" - textDecoration="underline" - target="_blank" - > - {str} - </Link> - ), - } - )} - </Caption> + <Typography variant="Body/Supporting text (caption)/smBold"> + <p> + {intl.formatMessage({ + id: "findMyBooking.cantFindYourStay", + defaultMessage: "Can't find your stay?", + })} + </p> + </Typography> + <Typography variant="Body/Supporting text (caption)/smRegular"> + <p> + {intl.formatMessage( + { + id: "findMyBooking.customerService", + defaultMessage: + "Please contact <link>customer service</link>.", + }, + { + link: (str) => ( + <TextLink + isInline + href={customerService[lang]} + typography="Link/sm" + target="_blank" + > + {str} + </TextLink> + ), + } + )} + </p> + </Typography> </div> <Button type="submit" - intent="primary" - theme="base" - disabled={form.formState.isSubmitting || update.isPending} + variant="Primary" + size="Large" + isDisabled={form.formState.isSubmitting || update.isPending} > {intl.formatMessage({ id: "common.find", diff --git a/apps/scandic-web/components/HotelReservation/FindMyBooking/utils.ts b/apps/scandic-web/components/HotelReservation/FindMyBooking/utils.ts new file mode 100644 index 000000000..f3ea5fab2 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/FindMyBooking/utils.ts @@ -0,0 +1,40 @@ +import type { IntlShape } from "react-intl" + +export enum FindMyBookingErrorEnum { + BOOKING_NOT_FOUND = "BOOKING_NOT_FOUND", + BOOKING_ACCESS_DENIED = "BOOKING_ACCESS_DENIED", +} + +export function getErrorMessage( + intl: IntlShape, + error?: FindMyBookingErrorEnum +) { + switch (error) { + case FindMyBookingErrorEnum.BOOKING_ACCESS_DENIED: + return { + heading: intl.formatMessage({ + id: "myStay.accessDenied.loginRequired", + defaultMessage: "You need to be logged in to view your booking", + }), + text: intl.formatMessage({ + id: "myStay.accessDenied.loginRequiredMessage", + defaultMessage: + "And you need to be logged in with the same member account that made the booking.", + }), + } + case FindMyBookingErrorEnum.BOOKING_NOT_FOUND: + return { + heading: intl.formatMessage({ + id: "myStay.accessDenied.bookingNotFound", + defaultMessage: "We couldn't find your booking", + }), + text: intl.formatMessage({ + id: "myStay.accessDenied.bookingNotFoundMessage", + defaultMessage: + "Please make sure you have entered the correct booking details and try again.", + }), + } + default: + return null + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx index aeedfc1d8..19d648cd3 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/index.tsx @@ -1,13 +1,13 @@ import { cookies } from "next/headers" -import { notFound } from "next/navigation" +import { notFound, redirect } from "next/navigation" import { BookingFlowConfig } from "@scandic-hotels/booking-flow/BookingFlowConfig" import { filterOverlappingDates } from "@scandic-hotels/booking-flow/utils/SelectRate" +import { findMyBookingRoutes } from "@scandic-hotels/common/constants/routes/findMyBookingRoutes" import { dt } from "@scandic-hotels/common/dt" import { logger } from "@scandic-hotels/common/logger" import * as maskValue from "@scandic-hotels/common/utils/maskValue" import Image from "@scandic-hotels/design-system/Image" -import { Typography } from "@scandic-hotels/design-system/Typography" import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" import { parseRefId } from "@scandic-hotels/trpc/utils/refId" @@ -40,6 +40,9 @@ import { getIntl } from "@/i18n" import MyStayProvider from "@/providers/MyStay" import { isLoggedInUser } from "@/utils/isLoggedInUser" +import FindMyBooking from "../FindMyBooking" +import { FindMyBookingErrorEnum } from "../FindMyBooking/utils" + import styles from "./index.module.css" import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue" @@ -72,9 +75,6 @@ async function MyStay(props: { } const { confirmationNumber, lastName } = parseRefId(refId) - if (!confirmationNumber) { - return notFound() - } const isLoggedIn = await isLoggedInUser() @@ -85,10 +85,11 @@ async function MyStay(props: { bookingConfirmation = await getBookingConfirmation(refId) } else if (bv) { logger.info(`MyStay: bv`, bv) - const values = JSON.parse(bv) as AdditionalInfoCookieValue - const firstName = values.firstName - const email = values.email - const bvConfirmationNo = values.confirmationNumber + const { + firstName, + email, + confirmationNumber: bvConfirmationNo, + } = JSON.parse(bv) as AdditionalInfoCookieValue if (firstName && email && bvConfirmationNo === confirmationNumber) { bookingConfirmation = await findBooking( @@ -115,7 +116,9 @@ async function MyStay(props: { } if (!bookingConfirmation) { - return notFound() + redirect( + `${findMyBookingRoutes[lang]}?error=${FindMyBookingErrorEnum.BOOKING_NOT_FOUND}` + ) } const { additionalData, booking, hotel, roomCategories } = bookingConfirmation @@ -288,41 +291,34 @@ async function MyStay(props: { if (access === ERROR_BAD_REQUEST) { return ( - <main className={styles.main}> - <div className={styles.form}> - <AdditionalInfoForm - confirmationNumber={confirmationNumber} - lastName={lastName} - /> - </div> - </main> + <RenderAdditionalInfoForm + confirmationNumber={confirmationNumber} + lastName={lastName} + /> ) } if (access === ERROR_UNAUTHORIZED) { - return ( - <main className={styles.main}> - <div className={styles.logIn}> - <Typography variant="Title/md"> - <h1> - {intl.formatMessage({ - id: "myStay.accessDenied.loginRequired", - defaultMessage: "You need to be logged in to view your booking", - })} - </h1> - </Typography> - <Typography variant="Body/Lead text"> - <p> - {intl.formatMessage({ - id: "myStay.accessDenied.loginRequiredMessage", - defaultMessage: - "And you need to be logged in with the same member account that made the booking.", - })} - </p> - </Typography> - </div> - </main> - ) + if (bv) { + const { firstName, email } = JSON.parse(bv) as AdditionalInfoCookieValue + + return ( + <main className={styles.main}> + <div className={styles.form}> + <FindMyBooking + error={FindMyBookingErrorEnum.BOOKING_ACCESS_DENIED} + defaultValues={{ + firstName, + lastName, + confirmationNumber, + email, + }} + /> + </div> + </main> + ) + } else { + } } return notFound() diff --git a/packages/trpc/lib/routers/booking/utils.ts b/packages/trpc/lib/routers/booking/utils.ts index 3b159309c..0a041e158 100644 --- a/packages/trpc/lib/routers/booking/utils.ts +++ b/packages/trpc/lib/routers/booking/utils.ts @@ -90,7 +90,7 @@ export async function findBooking( // If the booking is not found, return null. // This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them. - if (apiResponse.status === 400) { + if (apiResponse.status === 400 || apiResponse.status === 404) { return null }