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
253 lines
8.7 KiB
TypeScript
253 lines
8.7 KiB
TypeScript
"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>Scandic’s 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>
|
||
)
|
||
}
|