"use client" import { zodResolver } from "@hookform/resolvers/zod" import { useRouter, useSearchParams } from "next/navigation" import { useCallback, useEffect, useState } from "react" import { FormProvider, useForm } from "react-hook-form" import { useIntl } from "react-intl" import { BookingStatusEnum, PAYMENT_METHOD_TITLES, PaymentMethodEnum, } from "@/constants/booking" import { bookingTermsAndConditions, privacyPolicy, } from "@/constants/currentWebHrefs" import { selectRate } from "@/constants/routes/hotelReservation" import { env } from "@/env/client" import { trpc } from "@/lib/trpc/client" import { useEnterDetailsStore } from "@/stores/enter-details" import LoadingSpinner from "@/components/LoadingSpinner" import Button from "@/components/TempDesignSystem/Button" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { toast } from "@/components/TempDesignSystem/Toasts" import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions" import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus" import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast" import useLang from "@/hooks/useLang" import { bedTypeMap } from "../../SelectRate/RoomSelection/utils" import PriceChangeDialog from "../PriceChangeDialog" import GuaranteeDetails from "./GuaranteeDetails" import PaymentOption from "./PaymentOption" import { PaymentFormData, paymentSchema } from "./schema" import styles from "./payment.module.css" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { PaymentClientProps } from "@/types/components/hotelReservation/selectRate/section" const maxRetries = 4 const retryInterval = 2000 export const formId = "submit-booking" function isPaymentMethodEnum(value: string): value is PaymentMethodEnum { return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum) } export default function PaymentClient({ user, roomPrice, otherPaymentOptions, savedCreditCards, mustBeGuaranteed, }: PaymentClientProps) { const router = useRouter() const lang = useLang() const intl = useIntl() const searchParams = useSearchParams() const totalPrice = useEnterDetailsStore((state) => state.totalPrice) const { bedType, booking, breakfast } = useEnterDetailsStore((state) => ({ bedType: state.bedType, booking: state.booking, breakfast: state.breakfast, })) const userData = useEnterDetailsStore((state) => state.guest) const setIsSubmittingDisabled = useEnterDetailsStore( (state) => state.actions.setIsSubmittingDisabled ) const [confirmationNumber, setConfirmationNumber] = useState("") const [isPollingForBookingStatus, setIsPollingForBookingStatus] = useState(false) const availablePaymentOptions = useAvailablePaymentOptions(otherPaymentOptions) const [priceChangeData, setPriceChangeData] = useState<{ oldPrice: number newPrice: number } | null>() usePaymentFailedToast() const methods = useForm({ defaultValues: { paymentMethod: savedCreditCards?.length ? savedCreditCards[0].id : PaymentMethodEnum.card, smsConfirmation: false, termsAndConditions: false, }, mode: "all", reValidateMode: "onChange", resolver: zodResolver(paymentSchema), }) const initiateBooking = trpc.booking.create.useMutation({ onSuccess: (result) => { if (result?.confirmationNumber) { setConfirmationNumber(result.confirmationNumber) if (result.metadata?.priceChangedMetadata) { setPriceChangeData({ oldPrice: roomPrice.publicPrice, newPrice: result.metadata.priceChangedMetadata.totalPrice, }) } else { setIsPollingForBookingStatus(true) } } else { toast.error( intl.formatMessage({ id: "payment.error.failed", }) ) } }, onError: (error) => { console.error("Error", error) toast.error( intl.formatMessage({ id: "payment.error.failed", }) ) }, }) const priceChange = trpc.booking.priceChange.useMutation({ onSuccess: (result) => { if (result?.confirmationNumber) { setIsPollingForBookingStatus(true) } else { toast.error(intl.formatMessage({ id: "payment.error.failed" })) } setPriceChangeData(null) }, onError: (error) => { console.error("Error", error) setPriceChangeData(null) toast.error(intl.formatMessage({ id: "payment.error.failed" })) }, }) const bookingStatus = useHandleBookingStatus({ confirmationNumber, expectedStatus: BookingStatusEnum.BookingCompleted, maxRetries, retryInterval, enabled: isPollingForBookingStatus, }) useEffect(() => { if (bookingStatus?.data?.paymentUrl) { router.push(bookingStatus.data.paymentUrl) } else if (bookingStatus.isTimeout) { toast.error( intl.formatMessage({ id: "payment.error.failed", }) ) } }, [bookingStatus, router, intl]) useEffect(() => { setIsSubmittingDisabled( !methods.formState.isValid || methods.formState.isSubmitting ) }, [ methods.formState.isValid, methods.formState.isSubmitting, setIsSubmittingDisabled, ]) const handleSubmit = useCallback( (data: PaymentFormData) => { const { firstName, lastName, email, phoneNumber, countryCode, membershipNo, join, dateOfBirth, zipCode, } = userData const { toDate, fromDate, rooms, hotel } = booking // set payment method to card if saved card is submitted const paymentMethod = isPaymentMethodEnum(data.paymentMethod) ? data.paymentMethod : PaymentMethodEnum.card const savedCreditCard = savedCreditCards?.find( (card) => card.id === data.paymentMethod ) const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback` initiateBooking.mutate({ language: lang, hotelId: hotel, checkInDate: fromDate, checkOutDate: toDate, rooms: rooms.map((room) => ({ adults: room.adults, childrenAges: room.children?.map((child) => ({ age: child.age, bedType: bedTypeMap[parseInt(child.bed.toString())], })), rateCode: user || join || membershipNo ? room.counterRateCode : room.rateCode, roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step. guest: { firstName, lastName, email, phoneNumber, countryCode, membershipNumber: membershipNo, becomeMember: join, dateOfBirth, postalCode: zipCode, }, packages: { breakfast: !!(breakfast && breakfast.code), allergyFriendly: room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ?? false, petFriendly: room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false, accessibility: room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ?? false, }, smsConfirmationRequested: data.smsConfirmation, roomPrice, })), payment: { paymentMethod, card: savedCreditCard ? { alias: savedCreditCard.alias, expiryDate: savedCreditCard.expirationDate, cardType: savedCreditCard.cardType, } : undefined, success: `${paymentRedirectUrl}/success`, error: `${paymentRedirectUrl}/error`, cancel: `${paymentRedirectUrl}/cancel`, }, }) }, [ breakfast, bedType, userData, booking, roomPrice, savedCreditCards, lang, user, initiateBooking, ] ) if ( initiateBooking.isPending || (isPollingForBookingStatus && !bookingStatus.data?.paymentUrl) ) { return } const guaranteeing = intl.formatMessage({ id: "guaranteeing" }) const paying = intl.formatMessage({ id: "paying" }) const paymentVerb = mustBeGuaranteed ? guaranteeing : paying return ( <>
{mustBeGuaranteed ? (
{intl.formatMessage({ id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", })}
) : null} {savedCreditCards?.length ? (
{intl.formatMessage({ id: "MY SAVED CARDS" })}
{savedCreditCards?.map((savedCreditCard) => ( ))}
) : null}
{savedCreditCards?.length ? ( {intl.formatMessage({ id: "OTHER PAYMENT METHODS" })} ) : null}
{availablePaymentOptions.map((paymentMethod) => ( ))}
{intl.formatMessage( { id: "booking.terms", }, { paymentVerb, termsLink: (str) => ( {str} ), privacyLink: (str) => ( {str} ), } )} {intl.formatMessage({ id: "I accept the terms and conditions", })} {intl.formatMessage({ id: "I would like to get my booking confirmation via sms", })}
{priceChangeData ? ( { const allSearchParams = searchParams.size ? `?${searchParams.toString()}` : "" router.push(`${selectRate(lang)}${allSearchParams}`) }} onAccept={() => priceChange.mutate({ confirmationNumber })} /> ) : null} ) }