"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 { type AncillaryFormData, ancillaryFormSchema } from "../schema" import Description from "./Description" import Steps from "./Steps" 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(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 (
) } return (
) }