Files
web/apps/scandic-web/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/index.tsx
Christel Westerberg cd8b30f2ec Merged in fix/STAY-133 (pull request #3313)
Fix/STAY-133

* fix: Add static summary buttons row on add ancillary flow

* fix: refactor handling of modals

* fix: refactor file structure for add ancillary flow

* Merged in chore/replace-deprecated-body (pull request #3300)

Replace deprecated <Body> with <Typography>

* chore: replace deprecated body component

* refactor: replace Body component with Typography across various components

* merge

Approved-by: Bianca Widstam
Approved-by: Matilda Landström


Approved-by: Bianca Widstam
Approved-by: Matilda Landström
2025-12-11 07:29:36 +00:00

326 lines
9.2 KiB
TypeScript

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import { dt } from "@scandic-hotels/common/dt"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { toast } from "@scandic-hotels/design-system/Toast"
import { trpc } from "@scandic-hotels/trpc/client"
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
import {
buildAncillaryPackages,
clearAncillarySessionData,
generateDeliveryOptions,
getAncillarySessionData,
setAncillarySessionData,
} from "@/components/HotelReservation/MyStay/utils/ancillaries"
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
import useLang from "@/hooks/useLang"
import {
trackAncillaryFailed,
trackAncillarySuccess,
trackGlaAncillaryAttempt,
} from "@/utils/tracking/myStay"
import { isAncillaryError } from "../../utils"
import AddAncillaryFlow from "./Modal"
import { type AncillaryFormData, ancillaryFormSchema } from "./schema"
import {
buildBreakfastPackages,
calculateBreakfastData,
getErrorMessage,
getGuaranteeCallback,
} from "./utils"
import styles from "./addAncillaryFlowModal.module.css"
import type {
AddAncillaryFlowModalProps,
AncillaryErrorMessage,
AncillaryItem,
} from "@/types/components/myPages/myStay/ancillaries"
export default function AddAncillaryFlowModal({
booking,
packages,
user,
savedCreditCards,
}: AddAncillaryFlowModalProps) {
const {
selectedAncillary,
closeModal,
breakfastData,
setBreakfastData,
isBreakfast,
} = useAddAncillaryStore((state) => ({
selectedAncillary: state.selectedAncillary,
closeModal: state.closeModal,
breakfastData: state.breakfastData,
setBreakfastData: state.setBreakfastData,
isBreakfast: state.isBreakfast,
}))
const intl = useIntl()
const lang = useLang()
const router = useRouter()
const searchParams = useSearchParams()
const pathname = usePathname()
const [errorMessage, setErrorMessage] =
useState<AncillaryErrorMessage | null>(null)
const guaranteeRedirectUrl = getGuaranteeCallback(lang, pathname)
const deliveryTimeOptions = generateDeliveryOptions()
const hasInsufficientPoints =
(user?.membership?.currentPoints ?? 0) < (selectedAncillary?.points ?? 0)
const formMethods = useForm({
defaultValues: {
quantityWithPoints: null,
quantityWithCard:
!user || hasInsufficientPoints || !selectedAncillary?.requiresQuantity
? 1
: null,
deliveryTime:
booking.ancillary?.deliveryTime ?? deliveryTimeOptions[0].value,
optionalText: "",
termsAndConditions: false,
paymentMethod: booking.guaranteeInfo
? PaymentMethodEnum.card
: savedCreditCards?.length
? savedCreditCards[0].id
: PaymentMethodEnum.card,
},
mode: "onChange",
reValidateMode: "onChange",
resolver: zodResolver(ancillaryFormSchema),
})
const ancillaryErrorMessage = intl.formatMessage(
{
id: "addAncillaryFlowModal.errorMessage.ancillary",
defaultMessage:
"Something went wrong. {ancillary} could not be added to your booking!",
},
{ ancillary: selectedAncillary?.title }
)
const utils = trpc.useUtils()
const addAncillary = trpc.booking.packages.useMutation()
const { guaranteeBooking, isLoading, handleGuaranteeError } =
useGuaranteeBooking(booking.refId, true, booking.hotelId)
async function handleAncillarySubmission(
data: AncillaryFormData,
packages: {
code: string
quantity: number
comment: string | undefined
}[]
) {
await addAncillary.mutateAsync(
{
refId: booking.refId,
ancillaryComment: data.optionalText,
ancillaryDeliveryTime: selectedAncillary?.requiresDeliveryTime
? data.deliveryTime
: undefined,
packages: packages,
language: lang,
},
{
onSuccess: (result) => {
if (result) {
trackAncillarySuccess(
booking.confirmationNumber,
packages,
data.deliveryTime,
"ancillary",
selectedAncillary,
breakfastData,
booking.guaranteeInfo?.cardType,
booking.roomTypeCode
)
toast.success(
intl.formatMessage(
{
id: "addAncillaryFlowModal.ancillaryAdded",
defaultMessage: "{ancillary} added to your booking!",
},
{ ancillary: selectedAncillary?.title }
)
)
clearAncillarySessionData()
closeModal()
utils.booking.get.invalidate({
refId: booking.refId,
})
router.refresh()
} else {
trackAncillaryFailed(
packages,
data.deliveryTime,
selectedAncillary,
breakfastData
)
toast.error(ancillaryErrorMessage)
closeModal()
}
},
onError: () => {
trackAncillaryFailed(
packages,
data.deliveryTime,
selectedAncillary,
breakfastData
)
toast.error(ancillaryErrorMessage)
closeModal()
},
}
)
}
async function handleGuaranteePayment(
data: AncillaryFormData,
packages: AncillaryItem[]
) {
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
trackGlaAncillaryAttempt(
savedCreditCard,
packages,
selectedAncillary,
data.deliveryTime,
breakfastData
)
if (booking.refId) {
const card = savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined
await guaranteeBooking.mutateAsync({
refId: booking.refId,
language: lang,
...(card && { card }),
success: `${guaranteeRedirectUrl}?status=success&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`,
error: `${guaranteeRedirectUrl}?status=error&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`,
cancel: `${guaranteeRedirectUrl}?status=cancel&ancillary=1&RefId=${encodeURIComponent(booking.refId)}`,
})
} else {
handleGuaranteeError("No confirmation number")
}
}
const onSubmit = async (data: AncillaryFormData) => {
const packagesToAdd = !isBreakfast
? buildAncillaryPackages(data, selectedAncillary)
: breakfastData
? buildBreakfastPackages(data, breakfastData)
: []
if (isBreakfast && !breakfastData) {
toast.error(
intl.formatMessage({
id: "errorMessage.somethingWentWrong",
defaultMessage: "Something went wrong!",
})
)
return
}
setAncillarySessionData({
formData: data,
selectedAncillary,
packages: packagesToAdd,
isBreakfast,
breakfastData,
})
const shouldSkipGuarantee = booking.guaranteeInfo || !data.quantityWithCard
if (shouldSkipGuarantee) {
await handleAncillarySubmission(data, packagesToAdd)
} else {
await handleGuaranteePayment(data, packagesToAdd)
}
}
useEffect(() => {
if (isAncillaryError(searchParams)) {
const errorCode = searchParams.get("errorCode")
const queryParams = new URLSearchParams(searchParams.toString())
const savedData = getAncillarySessionData()
if (savedData?.formData) {
const updatedFormData = {
...savedData.formData,
paymentMethod: booking?.guaranteeInfo
? PaymentMethodEnum.card
: savedData.formData.paymentMethod,
}
formMethods.reset(updatedFormData)
}
setErrorMessage(getErrorMessage(intl, errorCode))
queryParams.delete("ancillary")
queryParams.delete("errorCode")
router.replace(`${pathname}?${queryParams.toString()}`)
}
}, [searchParams, pathname, formMethods, router, booking.guaranteeInfo, intl])
useEffect(() => {
setBreakfastData(
calculateBreakfastData(
isBreakfast,
packages,
booking.adults,
booking.childrenAges,
dt(booking.checkOutDate)
.startOf("day")
.diff(dt(booking.checkInDate).startOf("day"), "days")
)
)
}, [
booking.adults,
booking.checkInDate,
booking.checkOutDate,
booking.childrenAges,
isBreakfast,
packages,
setBreakfastData,
])
if (isLoading) {
return (
<div>
<LoadingSpinner />
</div>
)
}
return (
<FormProvider {...formMethods}>
<form
onSubmit={formMethods.handleSubmit(onSubmit)}
className={styles.form}
id="add-ancillary-form-id"
>
<AddAncillaryFlow
error={errorMessage}
savedCreditCards={savedCreditCards}
user={user}
/>
</form>
</FormProvider>
)
}