Files
web/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx
Bianca Widstam abd401c4f4 Merged in feat/SW-1368-1369-Guarantee-late-arrival (pull request #1512)
Feat/SW-1368 1369 Guarantee late arrival

* feat(SW-1368-SW-1369): guarantee late arrival for confirmation page and my stay

* feat(SW-1368-SW-1369): guarantee late arrival updated design

* feat(SW-1368-SW-1369): add translations

* feat(SW-1368-SW-1369): add translations

* feat(SW-1368-SW-1369): fix merge with master

* feat(SW-1368-SW-1369): add translations

* feat(SW-1368-SW-1369): add redirect with refId

* feat(SW-1368-SW-1369): if booking completed redirect to confirmation page

* feat(SW-1368-SW-1369): fix comments pr

* feat(SW-1368-SW-1369): fix comments pr

* feat(SW-1368-SW-1369): fix rebase master

* feat(SW-1368-SW-1369): fix duplicate flex rate check

* feat(SW-1368-SW-1369): if any room is flex, card must be used

* feat(SW-1368-SW-1369): move callback route

* feat(SW-1368-SW-1369): top align checkbox

* feat(SW-1368-SW-1369): top align checkbox


Approved-by: Tobias Johansson
Approved-by: Niclas Edenvin
2025-03-14 10:43:14 +00:00

253 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { BookingStatusEnum, PaymentMethodEnum } from "@/constants/booking"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
import { env } from "@/env/client"
import { trpc } from "@/lib/trpc/client"
import LoadingSpinner from "@/components/LoadingSpinner"
import { ModalContentWithActions } from "@/components/Modal/ModalContentWithActions"
import Divider from "@/components/TempDesignSystem/Divider"
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 { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import MySavedCards from "../../EnterDetails/Payment/MySavedCards"
import PaymentOption from "../../EnterDetails/Payment/PaymentOption"
import { type GuaranteeFormData, paymentSchema } from "./schema"
import styles from "./guaranteeLateArrival.module.css"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { CreditCard } from "@/types/user"
const maxRetries = 15
const retryInterval = 2000
export interface GuaranteeLateArrivalProps {
booking: BookingConfirmation["booking"]
handleCloseModal: () => void
handleBackToManageStay: () => void
savedCreditCards: CreditCard[] | null
refId: string
}
export default function GuaranteeLateArrival({
booking,
handleCloseModal,
handleBackToManageStay,
savedCreditCards,
refId,
}: GuaranteeLateArrivalProps) {
const intl = useIntl()
const lang = useLang()
const router = useRouter()
const methods = useForm<GuaranteeFormData>({
defaultValues: {
paymentMethod: savedCreditCards?.length
? savedCreditCards[0].id
: PaymentMethodEnum.card,
termsAndConditions: false,
},
mode: "all",
reValidateMode: "onChange",
resolver: zodResolver(paymentSchema),
})
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
useState(false)
const handlePaymentError = useCallback(() => {
toast.error(
intl.formatMessage({
id: "We had an issue guaranteeing your booking. Please try again.",
})
)
}, [intl])
const guaranteeBooking = trpc.booking.guarantee.useMutation({
onSuccess: (result) => {
if (result) {
setIsPollingForBookingStatus(true)
} else {
handlePaymentError()
}
},
onError: () => {
toast.error(
intl.formatMessage({
id: "Something went wrong!",
})
)
},
})
const bookingStatus = useHandleBookingStatus({
confirmationNumber: booking.confirmationNumber,
expectedStatus: BookingStatusEnum.BookingCompleted,
maxRetries,
retryInterval,
enabled: isPollingForBookingStatus,
})
useEffect(() => {
if (bookingStatus?.data?.paymentUrl) {
router.push(bookingStatus.data.paymentUrl)
} else if (bookingStatus.isTimeout) {
handlePaymentError()
}
}, [bookingStatus, router, intl, handlePaymentError])
if (
guaranteeBooking.isPending ||
(isPollingForBookingStatus &&
!bookingStatus.data?.paymentUrl &&
!bookingStatus.isTimeout)
) {
return (
<div className={styles.loading}>
<LoadingSpinner />
</div>
)
}
const handleGuaranteeLateArrival = (data: GuaranteeFormData) => {
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
if (booking.confirmationNumber) {
const card = savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined
guaranteeBooking.mutate({
confirmationNumber: booking.confirmationNumber,
language: lang,
...(card !== undefined && { card }),
success: `${guaranteeRedirectUrl}/success/${encodeURIComponent(refId)}`,
error: `${guaranteeRedirectUrl}/error/${encodeURIComponent(refId)}`,
cancel: `${guaranteeRedirectUrl}/cancel/${encodeURIComponent(refId)}`,
})
} else {
toast.error(
intl.formatMessage({
id: "Confirmation number is missing!",
})
)
}
}
return (
<FormProvider {...methods}>
<ModalContentWithActions
title={intl.formatMessage({ id: "Guarantee late arrival" })}
onClose={handleCloseModal}
content={
<>
<Caption>
{intl.formatMessage({
id: "Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
})}
</Caption>
<Caption type="bold">
{intl.formatMessage({
id: "In case of no-show you will be charged for the first night.",
})}
</Caption>
{savedCreditCards?.length ? (
<>
<MySavedCards savedCreditCards={savedCreditCards} />
<Body color="uiTextHighContrast" textTransform="bold">
{intl.formatMessage({ id: "OTHER" })}
</Body>
</>
) : null}
<PaymentOption
name="paymentMethod"
value={PaymentMethodEnum.card}
label={intl.formatMessage({ id: "Credit card" })}
/>
<div className={styles.termsAndConditions}>
<Checkbox topAlign name={"termsAndConditions"}>
<Caption>
{intl.formatMessage(
{
id: "By guaranteeing with any of the payment methods available, I accept the terms for this stay and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandics Privacy Policy</privacyPolicyLink>. I accept Scandic requiring a valid credit card during my visit in case anything is left unpaid.",
},
{
termsAndConditionsLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={bookingTermsAndConditions[lang]}
target="_blank"
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={privacyPolicy[lang]}
target="_blank"
>
{str}
</Link>
),
}
)}
</Caption>
</Checkbox>
</div>
<div className={styles.guaranteeCost}>
<div className={styles.guaranteeCostText}>
<Caption type="bold">
{intl.formatMessage({ id: "Guarantee cost" })}
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage({
id: "Your card will only be used for authorisation",
})}
</Caption>
</div>
<Divider variant="vertical" color="subtle" />
<Body textTransform="bold">
{formatPrice(intl, 0, booking.currencyCode)}
</Body>
</div>
</>
}
primaryAction={{
label: intl.formatMessage({ id: "Guarantee" }),
onClick: methods.handleSubmit(handleGuaranteeLateArrival),
intent: "primary",
}}
secondaryAction={{
label: intl.formatMessage({ id: "Back" }),
onClick: handleBackToManageStay,
intent: "text",
}}
/>
</FormProvider>
)
}