Files
web/apps/scandic-web/components/HotelReservation/MyStay/GuaranteeLateArrival/index.tsx
Chuma Mcphoy (We Ahead) 169094fc37 Merged in refactor/SW-2476-use-react-aria-radio-group-for-payment-options (pull request #1849)
Refactor(SW-2177): Use react aria RadioGroup & Radio for payment options

* fix(SW-SW-2177): enhance accessibility for payment options

* Added keyboard navigation support to payment options.
* Updated CSS to improve focus styles for payment option labels.

* refactor: use RadioGroup & Radio from react aria for payment options

* refactor(SW-2177): replace setValue and watch with useController for payment method handling

* fix(SW-2177): remove comment and use cx for styles on PaymentOption

* fix(SW-2177): Add keyboard focus indicator to payment option


Approved-by: Michael Zetterberg
Approved-by: Erik Tiekstra
2025-04-24 11:22:36 +00:00

240 lines
8.7 KiB
TypeScript

"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { PaymentMethodEnum } from "@/constants/booking"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import { guaranteeCallback } from "@/constants/routes/hotelReservation"
import { env } from "@/env/client"
import { useManageStayStore } from "@/stores/my-stay/manageStayStore"
import { useMyStayRoomDetailsStore } from "@/stores/my-stay/myStayRoomDetailsStore"
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 { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
import useLang from "@/hooks/useLang"
import { formatPrice } from "@/utils/numberFormatting"
import { trackGlaSaveCardAttempt } from "@/utils/tracking/myStay"
import MySavedCards from "../../EnterDetails/Payment/MySavedCards"
import PaymentOption from "../../EnterDetails/Payment/PaymentOption"
import PaymentOptionsGroup from "../../EnterDetails/Payment/PaymentOptionsGroup"
import { type GuaranteeFormData, paymentSchema } from "./schema"
import styles from "./guaranteeLateArrival.module.css"
import type { CreditCard } from "@/types/user"
export interface GuaranteeLateArrivalProps {
savedCreditCards: CreditCard[] | null
refId: string
}
export default function GuaranteeLateArrival({
savedCreditCards,
refId,
}: GuaranteeLateArrivalProps) {
const intl = useIntl()
const lang = useLang()
const router = useRouter()
const bookedRoom = useMyStayRoomDetailsStore((state) => state.bookedRoom)
const {
actions: { handleCloseView, handleCloseModal },
} = useManageStayStore()
const methods = useForm<GuaranteeFormData>({
defaultValues: {
paymentMethod: savedCreditCards?.length
? savedCreditCards[0].id
: PaymentMethodEnum.card,
termsAndConditions: false,
},
mode: "all",
reValidateMode: "onChange",
resolver: zodResolver(paymentSchema),
})
const guaranteeRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}${guaranteeCallback(lang)}`
const { guaranteeBooking, isLoading, handleGuaranteeError } =
useGuaranteeBooking({
confirmationNumber: bookedRoom.confirmationNumber,
handleBookingCompleted: router.refresh,
})
if (isLoading) {
return (
<div className={styles.loading}>
<LoadingSpinner />
</div>
)
}
const handleGuaranteeLateArrival = (data: GuaranteeFormData) => {
const savedCreditCard = savedCreditCards?.find(
(card) => card.id === data.paymentMethod
)
trackGlaSaveCardAttempt(bookedRoom.hotelId, savedCreditCard, "yes")
if (bookedRoom.confirmationNumber) {
const card = savedCreditCard
? {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
}
: undefined
guaranteeBooking.mutate({
confirmationNumber: bookedRoom.confirmationNumber,
language: lang,
...(card !== undefined && { card }),
success: `${guaranteeRedirectUrl}?status=success&RefId=${encodeURIComponent(refId)}`,
error: `${guaranteeRedirectUrl}?status=error&RefId=${encodeURIComponent(refId)}`,
cancel: `${guaranteeRedirectUrl}?status=cancel&RefId=${encodeURIComponent(refId)}`,
})
} else {
handleGuaranteeError("No confirmation number")
toast.error(
intl.formatMessage({
defaultMessage: "Something went wrong!",
})
)
}
}
return (
<FormProvider {...methods}>
<ModalContentWithActions
title={intl.formatMessage({
defaultMessage: "Guarantee late arrival",
})}
onClose={handleCloseModal}
content={
<>
<Caption>
{intl.formatMessage({
defaultMessage:
"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({
defaultMessage:
"In case of no-show you will be charged for the first night.",
})}
</Caption>
{savedCreditCards?.length ? (
<MySavedCards savedCreditCards={savedCreditCards} />
) : null}
<PaymentOptionsGroup
name="paymentMethod"
label={
savedCreditCards?.length
? intl.formatMessage({
defaultMessage: "OTHER",
})
: undefined
}
>
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
defaultMessage: "Credit card",
})}
/>
</PaymentOptionsGroup>
<div className={styles.termsAndConditions}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage(
{
defaultMessage:
"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
variant="underscored"
color="peach80"
target="_blank"
href={bookingTermsAndConditions[lang]}
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
variant="underscored"
color="peach80"
target="_blank"
href={privacyPolicy[lang]}
>
{str}
</Link>
),
}
)}
</p>
</Typography>
<Checkbox name="termsAndConditions">
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{intl.formatMessage({
defaultMessage: "I accept the terms and conditions",
})}
</span>
</Typography>
</Checkbox>
</div>
<div className={styles.guaranteeCost}>
<div className={styles.guaranteeCostText}>
<Caption type="bold">
{intl.formatMessage({
defaultMessage: "Guarantee cost",
})}
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage({
defaultMessage:
"Your card will only be used for authorisation",
})}
</Caption>
</div>
<Divider variant="vertical" color="subtle" />
<Body textTransform="bold">
{formatPrice(intl, 0, bookedRoom.currencyCode)}
</Body>
</div>
</>
}
primaryAction={{
label: intl.formatMessage({
defaultMessage: "Guarantee",
}),
onClick: methods.handleSubmit(handleGuaranteeLateArrival),
intent: "primary",
}}
secondaryAction={{
label: intl.formatMessage({
defaultMessage: "Back",
}),
onClick: handleCloseView,
intent: "text",
}}
/>
</FormProvider>
)
}