From c3b71a05d9f30187d522881dba76c1b96122a92d Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 12 Nov 2025 08:29:05 +0000 Subject: [PATCH] Merged in fix/STAY-2-GLA-cancelled (pull request #3109) Fix/STAY-2 GLA cancelled * fix: show toast on cancelling GLA flow * fix: show the ancillary GLA errors as inline alerts Approved-by: Bianca Widstam Approved-by: Erik Tiekstra --- .../Steps/ConfirmationStep/index.tsx | 22 ++- .../Steps/Desktop/index.tsx | 10 +- .../Steps/Mobile/index.tsx | 10 +- .../AddAncillaryFlowModal/index.tsx | 137 ++++-------------- .../AddAncillaryFlowModal/utils.ts | 136 +++++++++++++++++ .../ManageStay/Actions/actions.module.css | 2 +- .../HotelReservation/MyStay/utils.ts | 6 + .../booking/useGuaranteePaymentFailedToast.ts | 35 +++-- .../components/myPages/myStay/ancillaries.ts | 14 +- 9 files changed, 236 insertions(+), 136 deletions(-) create mode 100644 apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx index 1e0f5990b..6c6da2638 100755 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/ConfirmationStep/index.tsx @@ -29,6 +29,7 @@ import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/an export default function ConfirmationStep({ savedCreditCards, user, + error, }: ConfirmationStepProps) { const intl = useIntl() const lang = useLang() @@ -138,14 +139,19 @@ export default function ConfirmationStep({ ) : ( <> - + {error ? ( + + ) : ( + + )} + ({ ...card, diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx index f595f31ca..6deab3065 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Desktop/index.tsx @@ -10,7 +10,7 @@ import SelectQuantityStep from "../SelectQuantityStep" import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries" -export default function Desktop({ user, savedCreditCards }: StepsProps) { +export default function Desktop({ user, savedCreditCards, error }: StepsProps) { const currentStep = useAddAncillaryStore((state) => state.currentStep) if (currentStep === AncillaryStepEnum.selectAncillary) { return @@ -22,5 +22,11 @@ export default function Desktop({ user, savedCreditCards }: StepsProps) { return } - return + return ( + + ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx index fc3f80658..7d075dd45 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/Steps/Mobile/index.tsx @@ -9,7 +9,7 @@ import SelectQuantityStep from "../SelectQuantityStep" import type { StepsProps } from "@/types/components/myPages/myStay/ancillaries" -export default function Mobile({ user, savedCreditCards }: StepsProps) { +export default function Mobile({ user, savedCreditCards, error }: StepsProps) { const { currentStep, selectedAncillary } = useAddAncillaryStore((state) => ({ currentStep: state.currentStep, selectedAncillary: state.selectedAncillary, @@ -23,5 +23,11 @@ export default function Mobile({ user, savedCreditCards }: StepsProps) { ) } - return + return ( + + ) } diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx index 6e7edc4bd..0ab70494d 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/index.tsx @@ -16,13 +16,11 @@ import Modal from "@scandic-hotels/design-system/Modal" import { toast } from "@scandic-hotels/design-system/Toast" import { Typography } from "@scandic-hotels/design-system/Typography" import { trpc } from "@scandic-hotels/trpc/client" -import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" import { isWebview } from "@/constants/routes/webviews" import { env } from "@/env/client" import { AncillaryStepEnum, - type BreakfastData, useAddAncillaryStore, } from "@/stores/my-stay/add-ancillary-flow" @@ -41,16 +39,23 @@ import { trackGlaAncillaryAttempt, } from "@/utils/tracking/myStay" +import { isAncillaryError } from "../../../utils" import { type AncillaryFormData, ancillaryFormSchema } from "../schema" import ActionButtons from "./ActionButtons" import PriceDetails from "./PriceDetails" import Steps from "./Steps" +import { + buildBreakfastPackages, + calculateBreakfastData, + getErrorMessage, +} from "./utils" import styles from "./addAncillaryFlowModal.module.css" import type { AddAncillaryFlowModalProps, - Packages, + AncillaryErrorMessage, + AncillaryItem, } from "@/types/components/myPages/myStay/ancillaries" export default function AddAncillaryFlowModal({ @@ -81,6 +86,8 @@ export default function AddAncillaryFlowModal({ const pathname = usePathname() const [isPriceDetailsOpen, setIsPriceDetailsOpen] = useState(false) + const [errorMessage, setErrorMessage] = + useState(null) const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang, isWebview(pathname))}` const deliveryTimeOptions = generateDeliveryOptions() @@ -94,7 +101,7 @@ export default function AddAncillaryFlowModal({ quantityWithPoints: null, quantityWithCard: !user || hasInsufficientPoints || isBreakfast ? 1 : null, - deliveryTime: defaultDeliveryTime, + deliveryTime: booking.ancillary?.deliveryTime ?? defaultDeliveryTime, optionalText: "", termsAndConditions: false, paymentMethod: booking.guaranteeInfo @@ -127,16 +134,6 @@ export default function AddAncillaryFlowModal({ const { guaranteeBooking, isLoading, handleGuaranteeError } = useGuaranteeBooking(booking.refId, true, booking.hotelId) - function validateTermsAndConditions(data: AncillaryFormData): boolean { - if (!data.termsAndConditions) { - formMethods.setError("termsAndConditions", { - message: "You must accept the terms", - }) - return false - } - return true - } - function handleAncillarySubmission( data: AncillaryFormData, packages: { @@ -206,8 +203,10 @@ export default function AddAncillaryFlowModal({ ) } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function handleGuaranteePayment(data: AncillaryFormData, packages: any) { + function handleGuaranteePayment( + data: AncillaryFormData, + packages: AncillaryItem[] + ) { const savedCreditCard = savedCreditCards?.find( (card) => card.id === data.paymentMethod ) @@ -239,34 +238,7 @@ export default function AddAncillaryFlowModal({ } } - function buildBreakfastPackages( - data: AncillaryFormData, - breakfastData: BreakfastData - ) { - const packages = [ - { - code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, - quantity: breakfastData.nrOfAdults, - comment: data.optionalText || undefined, - }, - { - code: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, - quantity: breakfastData.nrOfPayingChildren, - comment: data.optionalText || undefined, - }, - { - code: BreakfastPackageEnum.FREE_CHILD_BREAKFAST, - quantity: breakfastData.nrOfFreeChildren, - comment: data.optionalText || undefined, - }, - ] - - return packages.filter((pkg) => pkg.quantity > 0) - } - const onSubmit = (data: AncillaryFormData) => { - if (!validateTermsAndConditions(data)) return - const packagesToAdd = !isBreakfast ? buildAncillaryPackages(data, selectedAncillary) : breakfastData @@ -300,14 +272,9 @@ export default function AddAncillaryFlowModal({ } useEffect(() => { - const errorCode = searchParams.get("errorCode") - const ancillary = searchParams.get("ancillary") - if ((errorCode && ancillary) || errorCode === "AncillaryFailed") { + if (isAncillaryError(searchParams)) { + const errorCode = searchParams.get("errorCode") const queryParams = new URLSearchParams(searchParams.toString()) - if (ancillary) { - queryParams.delete("ancillary") - } - queryParams.delete("errorCode") const savedData = getAncillarySessionData() if (savedData?.formData) { const updatedFormData = { @@ -318,9 +285,13 @@ export default function AddAncillaryFlowModal({ } formMethods.reset(updatedFormData) } + + setErrorMessage(getErrorMessage(intl, errorCode)) + queryParams.delete("ancillary") + queryParams.delete("errorCode") router.replace(`${pathname}?${queryParams.toString()}`) } - }, [searchParams, pathname, formMethods, router, booking.guaranteeInfo]) + }, [searchParams, pathname, formMethods, router, booking.guaranteeInfo, intl]) useEffect(() => { setBreakfastData( @@ -424,7 +395,11 @@ export default function AddAncillaryFlowModal({ )} )} - + {currentStep === AncillaryStepEnum.selectAncillary ? null : (
) } - -/** - * This function calculates some breakfast data in the store. - * It is used in various places in the add flow, but only needs - * to be calculated once. - */ -function calculateBreakfastData( - isBreakfast: boolean, - packages: Packages | null, - nrOfAdults: number, - childrenAges: number[], - nrOfNights: number -): BreakfastData | null { - if (!isBreakfast) { - return null - } - - const { nrOfPayingChildren, nrOfFreeChildren } = childrenAges.reduce( - (total, childAge) => { - if (childAge >= 4) { - total.nrOfPayingChildren = total.nrOfPayingChildren + 1 - } else { - total.nrOfFreeChildren = total.nrOfFreeChildren + 1 - } - return total - }, - { nrOfPayingChildren: 0, nrOfFreeChildren: 0 } - ) - - const adultPackage = packages?.find( - (p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST - ) - const childPackage = packages?.find( - (p) => p.code === BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST - ) - const priceAdult = adultPackage?.localPrice.price - const priceChild = childPackage?.localPrice.price - const currency = - adultPackage?.localPrice.currency ?? childPackage?.localPrice.currency - - if ( - typeof priceAdult !== "number" || - typeof priceChild !== "number" || - typeof currency !== "string" - ) { - return null - } else { - return { - nrOfAdults, - nrOfPayingChildren, - nrOfFreeChildren, - nrOfNights, - priceAdult, - priceChild, - currency, - } - } -} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts new file mode 100644 index 000000000..b9df784a3 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/AddAncillaryFlowModal/utils.ts @@ -0,0 +1,136 @@ +import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" +import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode" +import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast" + +import type { Packages } from "@scandic-hotels/trpc/types/packages" +import type { IntlShape } from "react-intl" + +import type { AncillaryErrorMessage } from "@/types/components/myPages/myStay/ancillaries" +import type { BreakfastData } from "@/stores/my-stay/add-ancillary-flow" +import type { AncillaryFormData } from "../schema" + +/** + * This function calculates some breakfast data in the store. + * It is used in various places in the add flow, but only needs + * to be calculated once. + */ +export function calculateBreakfastData( + isBreakfast: boolean, + packages: Packages | null, + nrOfAdults: number, + childrenAges: number[], + nrOfNights: number +): BreakfastData | null { + if (!isBreakfast || !packages) { + return null + } + + const { nrOfPayingChildren, nrOfFreeChildren } = childrenAges.reduce( + (total, childAge) => { + if (childAge >= 4) { + total.nrOfPayingChildren = total.nrOfPayingChildren + 1 + } else { + total.nrOfFreeChildren = total.nrOfFreeChildren + 1 + } + return total + }, + { nrOfPayingChildren: 0, nrOfFreeChildren: 0 } + ) + + const adultPackage = packages.find( + (p) => p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST + ) + const childPackage = packages.find( + (p) => p.code === BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST + ) + const priceAdult = adultPackage?.localPrice.price + const priceChild = childPackage?.localPrice.price + const currency = + adultPackage?.localPrice.currency ?? childPackage?.localPrice.currency + + if ( + typeof priceAdult !== "number" || + typeof priceChild !== "number" || + typeof currency !== "string" + ) { + return null + } else { + return { + nrOfAdults, + nrOfPayingChildren, + nrOfFreeChildren, + nrOfNights, + priceAdult, + priceChild, + currency, + } + } +} + +export function buildBreakfastPackages( + data: AncillaryFormData, + breakfastData: BreakfastData +) { + const packages = [ + { + code: BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST, + quantity: breakfastData.nrOfAdults, + comment: data.optionalText || undefined, + }, + { + code: BreakfastPackageEnum.ANCILLARY_CHILD_PAYING_BREAKFAST, + quantity: breakfastData.nrOfPayingChildren, + comment: data.optionalText || undefined, + }, + { + code: BreakfastPackageEnum.FREE_CHILD_BREAKFAST, + quantity: breakfastData.nrOfFreeChildren, + comment: data.optionalText || undefined, + }, + ] + + return packages.filter((pkg) => pkg.quantity > 0) +} + +export function getErrorMessage( + intl: IntlShape, + errorCode: string | null +): AncillaryErrorMessage { + switch (errorCode) { + case BookingErrorCodeEnum.TransactionFailed: + return { + message: intl.formatMessage({ + id: "guaranteePayment.failed", + defaultMessage: + "We had an issue guaranteeing your booking. Please try again.", + }), + type: AlertTypeEnum.Alarm, + } + case BookingErrorCodeEnum.TransactionCancelled: + return { + message: intl.formatMessage({ + id: "guaranteePayment.cancelled", + defaultMessage: + "You have cancelled the payment. Your booking is not guaranteed.", + }), + type: AlertTypeEnum.Warning, + } + case "AncillaryFailed": + return { + message: intl.formatMessage({ + id: "guaranteePayment.ancillaryFailed", + defaultMessage: + "The product could not be added. Your booking is guaranteed. Please try again.", + }), + type: AlertTypeEnum.Alarm, + } + default: + return { + message: intl.formatMessage({ + id: "guaranteePayment.genericError", + defaultMessage: "Something went wrong! Please try again later.", + }), + type: AlertTypeEnum.Alarm, + } + } +} diff --git a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/actions.module.css b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/actions.module.css index d66a6408e..b85d492b8 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/actions.module.css +++ b/apps/scandic-web/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/actions.module.css @@ -1,7 +1,7 @@ .list { display: flex; flex-direction: column; - align-items: start; + align-items: flex-start; margin: 0; padding: 0; gap: var(--Space-x15); diff --git a/apps/scandic-web/components/HotelReservation/MyStay/utils.ts b/apps/scandic-web/components/HotelReservation/MyStay/utils.ts index 44960d523..872ae730d 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/utils.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/utils.ts @@ -69,3 +69,9 @@ export function hasModifiableRate(cancellationRule: string | null): boolean { cancellationRule === CancellationRuleEnum.Changeable ) } + +export function isAncillaryError(searchParams: URLSearchParams): boolean { + const errorCode = searchParams.get("errorCode") + const ancillary = searchParams.get("ancillary") + return Boolean((errorCode && ancillary) || errorCode === "AncillaryFailed") +} diff --git a/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts b/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts index 921d1b7da..45e4c3978 100644 --- a/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts +++ b/apps/scandic-web/hooks/booking/useGuaranteePaymentFailedToast.ts @@ -1,13 +1,16 @@ "use client" import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { useCallback, useEffect } from "react" +import { useCallback, useEffect, useRef } from "react" import { useIntl } from "react-intl" import { toast } from "@scandic-hotels/design-system/Toast" import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode" +import { isAncillaryError } from "@/components/HotelReservation/MyStay/utils" + export function useGuaranteePaymentFailedToast() { + const hasRunOnce = useRef(false) const intl = useIntl() const searchParams = useSearchParams() const pathname = usePathname() @@ -16,11 +19,11 @@ export function useGuaranteePaymentFailedToast() { const getErrorMessage = useCallback( (errorCode: string | null) => { switch (errorCode) { - case "AncillaryFailed": + case BookingErrorCodeEnum.TransactionCancelled: return intl.formatMessage({ - id: "guaranteePayment.ancillaryFailed", + id: "guaranteePayment.cancelled", defaultMessage: - "The product could not be added. Your booking is guaranteed. Please try again.", + "You have cancelled the payment. Your booking is not guaranteed.", }) default: return intl.formatMessage({ @@ -34,25 +37,33 @@ export function useGuaranteePaymentFailedToast() { ) useEffect(() => { - const errorCode = searchParams.get("errorCode") - const errorMessage = getErrorMessage(errorCode) - if (!errorCode || errorCode === BookingErrorCodeEnum.TransactionCancelled) + // To prevent multiple toasts in strict mode + if (hasRunOnce.current) { return + } + const errorCode = searchParams.get("errorCode") + if (!errorCode) { + return + } + // Ancillary errors are handled in AddAncillaryFlowModal + if (isAncillaryError(searchParams)) { + hasRunOnce.current = true + return + } + + const errorMessage = getErrorMessage(errorCode) const toastType = errorCode === BookingErrorCodeEnum.TransactionCancelled ? "warning" : "error" - toast[toastType](errorMessage) - const ancillary = searchParams.get("ancillary") - if ((errorCode && ancillary) || errorCode === "AncillaryFailed") { - return - } + toast[toastType](errorMessage) const queryParams = new URLSearchParams(searchParams.toString()) queryParams.delete("errorCode") router.push(`${pathname}?${queryParams.toString()}`) + hasRunOnce.current = true }, [searchParams, pathname, router, getErrorMessage]) } diff --git a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts index 5bc241b5f..424e2f77b 100644 --- a/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts +++ b/apps/scandic-web/types/components/myPages/myStay/ancillaries.ts @@ -1,3 +1,4 @@ +import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert" import type { ancillaryPackagesSchema, packagesSchema, @@ -24,6 +25,12 @@ export interface AddedAncillariesProps { booking: Room } +export interface AncillaryItem { + code: string + quantity: number + comment: string | undefined +} + export interface AddAncillaryFlowModalProps { booking: Room packages: Packages | null @@ -34,15 +41,20 @@ export interface AddAncillaryFlowModalProps { export interface SelectQuantityStepProps { user: User | null } - +export interface AncillaryErrorMessage { + type: AlertTypeEnum + message: string +} export interface ConfirmationStepProps { savedCreditCards: CreditCard[] | null user: User | null + error: AncillaryErrorMessage | null } export interface StepsProps { user: User | null savedCreditCards: CreditCard[] | null + error: AncillaryErrorMessage | null } export interface ActionButtonsProps {