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
240 lines
8.7 KiB
TypeScript
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>
|
|
)
|
|
}
|