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
This commit is contained in:
@@ -54,6 +54,19 @@ export default function Room({ booking, img, roomName }: RoomProps) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{booking.guaranteeInfo && (
|
||||
<div className={styles.benefits}>
|
||||
<CheckCircleIcon color="green" height={20} width={20} />
|
||||
<Caption>
|
||||
<strong>
|
||||
{intl.formatMessage({ id: "Booking guaranteed." })}
|
||||
</strong>{" "}
|
||||
{intl.formatMessage({
|
||||
id: "Your room will remain available for check-in even after 18:00.",
|
||||
})}
|
||||
</Caption>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div className={styles.booking}>
|
||||
<Image
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
}
|
||||
|
||||
.header {
|
||||
align-items: flex-end;
|
||||
display: grid;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.guaranteeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
padding: var(--Spacing-x-one-and-half) var(--Spacing-x2);
|
||||
width: min(512px, 100%);
|
||||
}
|
||||
.checkbox {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.checkboxContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: 0 var(--Spacing-x2);
|
||||
width: min(800px, 100%);
|
||||
}
|
||||
|
||||
.modalContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.infoButton {
|
||||
display: flex;
|
||||
gap: var(--Space-x05);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.modalText {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
margin-top: var(--Space-x15);
|
||||
width: min(164px, 100%);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modalContainer {
|
||||
width: 552px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
import {
|
||||
bookingTermsAndConditions,
|
||||
privacyPolicy,
|
||||
} from "@/constants/currentWebHrefs"
|
||||
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import Modal from "@/components/Modal"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import MySavedCards from "../Payment/MySavedCards"
|
||||
import PaymentOption from "../Payment/PaymentOption"
|
||||
|
||||
import styles from "./confirm.module.css"
|
||||
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
interface ConfirmBookingProps {
|
||||
savedCreditCards: CreditCard[] | null
|
||||
}
|
||||
|
||||
export default function ConfirmBooking({
|
||||
savedCreditCards,
|
||||
}: ConfirmBookingProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
|
||||
const { watch } = useFormContext()
|
||||
const guarantee = watch("guarantee")
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.guaranteeContainer}>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox name="guarantee" />
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "Guarantee room for late arrival",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Button
|
||||
variant="Text"
|
||||
size="Small"
|
||||
typography="Body/Supporting text (caption)/smBold"
|
||||
className={styles.infoButton}
|
||||
onPress={() => setModalOpen(true)}
|
||||
>
|
||||
<InfoCircleIcon
|
||||
width={20}
|
||||
height={20}
|
||||
color="uiTextMediumContrast"
|
||||
/>
|
||||
{intl.formatMessage({ id: "How does it work" })}
|
||||
</Button>
|
||||
|
||||
<Modal isOpen={isModalOpen} onToggle={() => setModalOpen(false)}>
|
||||
<div className={styles.modalContainer}>
|
||||
<Typography variant="Title/smRegular">
|
||||
<h3>
|
||||
{intl.formatMessage({ id: "Guarantee for late arrival" })}
|
||||
</h3>
|
||||
</Typography>
|
||||
<Typography variant="Body/Lead text">
|
||||
<p className={styles.modalText}>
|
||||
{intl.formatMessage({
|
||||
id: "When guaranteeing your booking with a credit card, we will hold the booking until 07:00 the day after check-in.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p className={styles.modalText}>
|
||||
{intl.formatMessage({
|
||||
id: "In case of a no-show, your credit card will be charged for the first night.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Button
|
||||
typography="Body/Paragraph/mdBold"
|
||||
variant="Secondary"
|
||||
size="Small"
|
||||
onPress={() => setModalOpen(false)}
|
||||
className={styles.closeButton}
|
||||
>
|
||||
{intl.formatMessage({ id: "Close" })}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
<Divider color="subtle" />
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "I may arrive later than 18:00 and want to guarantee my booking with a credit card.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
{savedCreditCards?.length && guarantee ? (
|
||||
<MySavedCards savedCreditCards={savedCreditCards} />
|
||||
) : null}
|
||||
{guarantee && (
|
||||
<>
|
||||
{savedCreditCards?.length && (
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h4>{intl.formatMessage({ id: "OTHER" })}</h4>
|
||||
</Typography>
|
||||
)}
|
||||
<PaymentOption
|
||||
name="paymentMethod"
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.checkboxContainer}>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox name="termsAndConditions" topAlign>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require 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>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { PAYMENT_METHOD_TITLES ,type PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
import PaymentOption from "../PaymentOption"
|
||||
|
||||
import styles from "./mySavedCards.module.css"
|
||||
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
interface MySavedCardsProps {
|
||||
savedCreditCards: CreditCard[] | null
|
||||
}
|
||||
|
||||
export default function MySavedCards({ savedCreditCards }: MySavedCardsProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<section className={styles.section}>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h4>{intl.formatMessage({ id: "MY SAVED CARDS" })}</h4>
|
||||
</Typography>
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
{savedCreditCards?.map((savedCreditCard) => (
|
||||
<PaymentOption
|
||||
key={savedCreditCard.id}
|
||||
name="paymentMethod"
|
||||
value={savedCreditCard.id}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[
|
||||
savedCreditCard.cardType as PaymentMethodEnum
|
||||
]
|
||||
}
|
||||
cardNumber={savedCreditCard.truncatedNumber}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.paymentOptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
BookingErrorCodeEnum,
|
||||
BookingStatusEnum,
|
||||
PAYMENT_METHOD_TITLES,
|
||||
@@ -16,7 +17,10 @@ import {
|
||||
bookingTermsAndConditions,
|
||||
privacyPolicy,
|
||||
} from "@/constants/currentWebHrefs"
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
selectRate,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { env } from "@/env/client"
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||
@@ -36,10 +40,12 @@ import useLang from "@/hooks/useLang"
|
||||
import { trackPaymentEvent } from "@/utils/tracking"
|
||||
|
||||
import { bedTypeMap } from "../../utils"
|
||||
import ConfirmBooking from "../Confirm"
|
||||
import PriceChangeDialog from "../PriceChangeDialog"
|
||||
import GuaranteeDetails from "./GuaranteeDetails"
|
||||
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
|
||||
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
|
||||
import MySavedCards from "./MySavedCards"
|
||||
import PaymentOption from "./PaymentOption"
|
||||
import { type PaymentFormData, paymentSchema } from "./schema"
|
||||
|
||||
@@ -56,6 +62,9 @@ export const formId = "submit-booking"
|
||||
export default function PaymentClient({
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
mustBeGuaranteed,
|
||||
memberMustBeGuaranteed,
|
||||
isFlexRate,
|
||||
}: PaymentClientProps) {
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
@@ -70,6 +79,14 @@ export default function PaymentClient({
|
||||
totalPrice: state.totalPrice,
|
||||
}))
|
||||
|
||||
const bookingMustBeGuaranteed = rooms.some(
|
||||
({ room }, idx) =>
|
||||
(room.guest.join || room.guest.membershipNo) &&
|
||||
booking.rooms[idx].counterRateCode
|
||||
)
|
||||
? memberMustBeGuaranteed
|
||||
: mustBeGuaranteed
|
||||
|
||||
const setIsSubmittingDisabled = useEnterDetailsStore(
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
)
|
||||
@@ -87,7 +104,6 @@ export default function PaymentClient({
|
||||
|
||||
const { toDate, fromDate, hotelId } = booking
|
||||
|
||||
const mustBeGuaranteed = rooms.every((r) => r.room.mustBeGuaranteed)
|
||||
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
||||
const hasFlexRates = rooms.some(hasFlexibleRate)
|
||||
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
||||
@@ -101,6 +117,7 @@ export default function PaymentClient({
|
||||
: PaymentMethodEnum.card,
|
||||
smsConfirmation: false,
|
||||
termsAndConditions: false,
|
||||
guarantee: false,
|
||||
},
|
||||
mode: "all",
|
||||
reValidateMode: "onChange",
|
||||
@@ -119,6 +136,11 @@ export default function PaymentClient({
|
||||
return
|
||||
}
|
||||
|
||||
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${result.id}`
|
||||
router.push(confirmationUrl)
|
||||
}
|
||||
|
||||
setBookingNumber(result.id)
|
||||
|
||||
const priceChange = result.rooms.find(
|
||||
@@ -212,18 +234,49 @@ export default function PaymentClient({
|
||||
setIsSubmittingDisabled,
|
||||
])
|
||||
|
||||
const getPaymentMethod = (
|
||||
isFlexRate: boolean,
|
||||
paymentMethod: string | null | undefined
|
||||
): PaymentMethodEnum => {
|
||||
if (isFlexRate) {
|
||||
return PaymentMethodEnum.card
|
||||
}
|
||||
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
||||
? paymentMethod
|
||||
: PaymentMethodEnum.card
|
||||
}
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(data: PaymentFormData) => {
|
||||
// set payment method to card if saved card is submitted
|
||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||
? data.paymentMethod
|
||||
: PaymentMethodEnum.card
|
||||
const paymentMethod = getPaymentMethod(isFlexRate, data.paymentMethod)
|
||||
|
||||
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`
|
||||
const guarantee = data.guarantee
|
||||
const useSavedCard = savedCreditCard
|
||||
? {
|
||||
card: {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
const shouldUsePayment = !isFlexRate || guarantee
|
||||
|
||||
const payment = shouldUsePayment
|
||||
? {
|
||||
paymentMethod: paymentMethod,
|
||||
...useSavedCard,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
}
|
||||
: undefined
|
||||
|
||||
trackPaymentEvent({
|
||||
event: "paymentAttemptStart",
|
||||
@@ -284,20 +337,7 @@ export default function PaymentClient({
|
||||
publicPrice: room.roomRate.publicRate?.localPrice.pricePerStay,
|
||||
},
|
||||
})),
|
||||
payment: {
|
||||
paymentMethod,
|
||||
card: savedCreditCard
|
||||
? {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
}
|
||||
: undefined,
|
||||
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
},
|
||||
payment,
|
||||
})
|
||||
},
|
||||
[
|
||||
@@ -309,6 +349,7 @@ export default function PaymentClient({
|
||||
toDate,
|
||||
rooms,
|
||||
booking,
|
||||
isFlexRate,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -327,6 +368,9 @@ export default function PaymentClient({
|
||||
const payment = intl.formatMessage({
|
||||
id: "Payment",
|
||||
})
|
||||
const confirm = intl.formatMessage({
|
||||
id: "Confirm booking",
|
||||
})
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -334,7 +378,11 @@ export default function PaymentClient({
|
||||
>
|
||||
<header>
|
||||
<Title level="h2" as="h4">
|
||||
{mustBeGuaranteed ? paymentGuarantee : payment}
|
||||
{bookingMustBeGuaranteed
|
||||
? paymentGuarantee
|
||||
: isFlexRate
|
||||
? confirm
|
||||
: payment}
|
||||
</Title>
|
||||
</header>
|
||||
<FormProvider {...methods}>
|
||||
@@ -343,127 +391,115 @@ export default function PaymentClient({
|
||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||
id={formId}
|
||||
>
|
||||
{mustBeGuaranteed ? (
|
||||
<section className={styles.section}>
|
||||
<Body>
|
||||
{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.",
|
||||
})}
|
||||
</Body>
|
||||
<GuaranteeDetails />
|
||||
</section>
|
||||
) : null}
|
||||
{isFlexRate && !bookingMustBeGuaranteed ? (
|
||||
<ConfirmBooking savedCreditCards={savedCreditCards} />
|
||||
) : (
|
||||
<>
|
||||
{bookingMustBeGuaranteed ? (
|
||||
<section className={styles.section}>
|
||||
<Body>
|
||||
{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.",
|
||||
})}
|
||||
</Body>
|
||||
<GuaranteeDetails />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{hasMixedRates ? (
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.",
|
||||
})}
|
||||
</Body>
|
||||
) : null}
|
||||
{hasMixedRates ? (
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.",
|
||||
})}
|
||||
</Body>
|
||||
) : null}
|
||||
|
||||
{savedCreditCards?.length ? (
|
||||
<section className={styles.section}>
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "MY SAVED CARDS" })}
|
||||
</Body>
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
{savedCreditCards?.map((savedCreditCard) => (
|
||||
{savedCreditCards?.length ? (
|
||||
<section className={styles.section}>
|
||||
<MySavedCards savedCreditCards={savedCreditCards} />
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section className={styles.section}>
|
||||
{savedCreditCards?.length ? (
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "OTHER PAYMENT METHODS" })}
|
||||
</Body>
|
||||
) : null}
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
<PaymentOption
|
||||
key={savedCreditCard.id}
|
||||
name="paymentMethod"
|
||||
value={savedCreditCard.id}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[
|
||||
savedCreditCard.cardType as PaymentMethodEnum
|
||||
]
|
||||
}
|
||||
cardNumber={savedCreditCard.truncatedNumber}
|
||||
hotelId={hotelId}
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
{availablePaymentOptions.map((paymentMethod) => (
|
||||
<PaymentOption
|
||||
key={paymentMethod}
|
||||
name="paymentMethod"
|
||||
value={paymentMethod}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[
|
||||
paymentMethod as PaymentMethodEnum
|
||||
]
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{hasMixedRates ? (
|
||||
<MixedRatePaymentBreakdown
|
||||
rooms={rooms}
|
||||
currency={totalPrice.local.currency}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
{savedCreditCards?.length ? (
|
||||
<Body color="uiTextHighContrast" textTransform="bold">
|
||||
{intl.formatMessage({ id: "OTHER PAYMENT METHODS" })}
|
||||
</Body>
|
||||
) : null}
|
||||
<div className={styles.paymentOptionContainer}>
|
||||
<PaymentOption
|
||||
name="paymentMethod"
|
||||
value={PaymentMethodEnum.card}
|
||||
label={intl.formatMessage({ id: "Credit card" })}
|
||||
hotelId={hotelId}
|
||||
/>
|
||||
{availablePaymentOptions.map((paymentMethod) => (
|
||||
<PaymentOption
|
||||
key={paymentMethod}
|
||||
name="paymentMethod"
|
||||
value={paymentMethod}
|
||||
label={
|
||||
PAYMENT_METHOD_TITLES[paymentMethod as PaymentMethodEnum]
|
||||
}
|
||||
hotelId={hotelId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{hasMixedRates ? (
|
||||
<MixedRatePaymentBreakdown
|
||||
rooms={rooms}
|
||||
currency={totalPrice.local.currency}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require 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 name="termsAndConditions">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I accept the terms and conditions",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
</section>
|
||||
<section className={styles.section}>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require 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 name="termsAndConditions">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I accept the terms and conditions",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
<Checkbox name="smsConfirmation">
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "I would like to get my booking confirmation via sms",
|
||||
})}
|
||||
</Caption>
|
||||
</Checkbox>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.submitButton}>
|
||||
<Button
|
||||
intent="primary"
|
||||
|
||||
@@ -7,5 +7,4 @@ export interface PaymentOptionProps {
|
||||
cardNumber?: string
|
||||
registerOptions?: RegisterOptions
|
||||
onChange?: () => void
|
||||
hotelId: string
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CancellationRuleEnum, PaymentMethodEnum } from "@/constants/booking"
|
||||
import { PaymentMethodEnum } from "@/constants/booking"
|
||||
|
||||
import type { RoomState } from "@/types/stores/enter-details"
|
||||
|
||||
@@ -7,11 +7,11 @@ export function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
}
|
||||
|
||||
export function hasFlexibleRate({ room }: RoomState): boolean {
|
||||
return room.cancellationRule === CancellationRuleEnum.CancellableBefore6PM
|
||||
return room.isFlexRate
|
||||
}
|
||||
|
||||
export function hasPrepaidRate({ room }: RoomState): boolean {
|
||||
return room.cancellationRule !== CancellationRuleEnum.CancellableBefore6PM
|
||||
return !room.isFlexRate
|
||||
}
|
||||
|
||||
export function calculateTotalRoomPrice({ room }: RoomState) {
|
||||
|
||||
@@ -6,7 +6,10 @@ import type { PaymentProps } from "@/types/components/hotelReservation/enterDeta
|
||||
|
||||
export default async function Payment({
|
||||
otherPaymentOptions,
|
||||
mustBeGuaranteed,
|
||||
memberMustBeGuaranteed,
|
||||
supportedCards,
|
||||
isFlexRate,
|
||||
}: PaymentProps) {
|
||||
const savedCreditCards = await getSavedPaymentCardsSafely({
|
||||
supportedCards,
|
||||
@@ -16,6 +19,9 @@ export default async function Payment({
|
||||
<PaymentClient
|
||||
otherPaymentOptions={otherPaymentOptions}
|
||||
savedCreditCards={savedCreditCards}
|
||||
mustBeGuaranteed={mustBeGuaranteed}
|
||||
memberMustBeGuaranteed={memberMustBeGuaranteed}
|
||||
isFlexRate={isFlexRate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.string(),
|
||||
paymentMethod: z.string().nullish(),
|
||||
smsConfirmation: z.boolean(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
}),
|
||||
guarantee: z.boolean(),
|
||||
})
|
||||
|
||||
export interface PaymentFormData extends z.output<typeof paymentSchema> {}
|
||||
|
||||
@@ -66,6 +66,7 @@ const rooms: RoomState[] = [
|
||||
roomTypeCode: "QS",
|
||||
isAvailable: true,
|
||||
mustBeGuaranteed: false,
|
||||
isFlexRate: false,
|
||||
},
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
@@ -94,7 +95,6 @@ const rooms: RoomState[] = [
|
||||
bedTypes: [],
|
||||
breakfast: undefined,
|
||||
breakfastIncluded: false,
|
||||
cancellationRule: "",
|
||||
cancellationText: "Non-refundable",
|
||||
childrenInRoom: [],
|
||||
guest: guestDetailsMember,
|
||||
@@ -106,6 +106,7 @@ const rooms: RoomState[] = [
|
||||
roomTypeCode: "QS",
|
||||
isAvailable: true,
|
||||
mustBeGuaranteed: false,
|
||||
isFlexRate: false,
|
||||
},
|
||||
steps: {
|
||||
[StepEnum.selectBed]: {
|
||||
|
||||
@@ -60,7 +60,11 @@ export default function ConfirmationStep() {
|
||||
<Body textTransform="bold">{"MasterCard"}</Body>
|
||||
<Body color="uiTextMediumContrast">{"**** 1234"}</Body>
|
||||
</div>
|
||||
<Checkbox name="termsAndConditions" registerOptions={{ required: true }}>
|
||||
<Checkbox
|
||||
name="termsAndConditions"
|
||||
registerOptions={{ required: true }}
|
||||
topAlign
|
||||
>
|
||||
<Caption>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
.card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x-one-and-half);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
.addCreditCard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.guaranteeCost {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--Spacing-x2);
|
||||
align-items: flex-end;
|
||||
gap: var(--Spacing-x3);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
}
|
||||
|
||||
.guaranteeCostText {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.termsAndConditions {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.paymentOptionContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 640px;
|
||||
max-width: 100%;
|
||||
height: 640px;
|
||||
max-height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.string().nullable(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
}),
|
||||
})
|
||||
|
||||
export interface GuaranteeFormData extends z.output<typeof paymentSchema> {}
|
||||
@@ -31,14 +31,18 @@ interface ActionPanelProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
showCancelStayButton: boolean
|
||||
showGuaranteeButton: boolean
|
||||
onCancelClick: () => void
|
||||
onGuaranteeClick: () => void
|
||||
}
|
||||
|
||||
export default function ActionPanel({
|
||||
booking,
|
||||
hotel,
|
||||
showCancelStayButton,
|
||||
showGuaranteeButton,
|
||||
onCancelClick,
|
||||
onGuaranteeClick,
|
||||
}: ActionPanelProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
@@ -74,15 +78,17 @@ export default function ActionPanel({
|
||||
{intl.formatMessage({ id: "Modify dates" })}
|
||||
<CalendarIcon width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={() => {}}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Guarantee late arrival" })}
|
||||
<CreditCard width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
{showGuaranteeButton && (
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={onGuaranteeClick}
|
||||
intent="text"
|
||||
className={styles.button}
|
||||
>
|
||||
{intl.formatMessage({ id: "Guarantee late arrival" })}
|
||||
<CreditCard width={24} height={24} color="burgundy" />
|
||||
</Button>
|
||||
)}
|
||||
<AddToCalendar
|
||||
checkInDate={booking.checkInDate}
|
||||
event={event}
|
||||
|
||||
@@ -10,18 +10,22 @@ import Modal from "@/components/Modal"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import CancelStay from "../CancelStay"
|
||||
import GuaranteeLateArrival from "../GuaranteeLateArrival"
|
||||
import ActionPanel from "./ActionPanel"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
type ActiveView = "actionPanel" | "cancelStay"
|
||||
type ActiveView = "actionPanel" | "cancelStay" | "guaranteeLateArrival"
|
||||
|
||||
interface ManageStayProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
setBookingStatus: (status: BookingStatusEnum) => void
|
||||
bookingStatus: string | null
|
||||
savedCreditCards: CreditCard[] | null
|
||||
refId: string
|
||||
}
|
||||
|
||||
export default function ManageStay({
|
||||
@@ -29,6 +33,8 @@ export default function ManageStay({
|
||||
hotel,
|
||||
setBookingStatus,
|
||||
bookingStatus,
|
||||
savedCreditCards,
|
||||
refId,
|
||||
}: ManageStayProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
@@ -39,6 +45,9 @@ export default function ManageStay({
|
||||
const showCancelStayButton =
|
||||
bookingStatus !== BookingStatusEnum.Cancelled && booking.isCancelable
|
||||
|
||||
const showGuaranteeButton =
|
||||
bookingStatus !== BookingStatusEnum.Cancelled && !booking.guaranteeInfo
|
||||
|
||||
function handleClose() {
|
||||
setIsOpen(false)
|
||||
setActiveView("actionPanel")
|
||||
@@ -61,13 +70,25 @@ export default function ManageStay({
|
||||
handleBackToManageStay={handleBack}
|
||||
/>
|
||||
)
|
||||
case "guaranteeLateArrival":
|
||||
return (
|
||||
<GuaranteeLateArrival
|
||||
booking={booking}
|
||||
handleCloseModal={handleClose}
|
||||
handleBackToManageStay={handleBack}
|
||||
savedCreditCards={savedCreditCards}
|
||||
refId={refId}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<ActionPanel
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
onCancelClick={() => setActiveView("cancelStay")}
|
||||
onGuaranteeClick={() => setActiveView("guaranteeLateArrival")}
|
||||
showCancelStayButton={showCancelStayButton}
|
||||
showGuaranteeButton={showGuaranteeButton}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import IconChip from "@/components/TempDesignSystem/IconChip"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { formatPrice } from "@/utils/numberFormatting"
|
||||
|
||||
@@ -24,13 +25,21 @@ import styles from "./referenceCard.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||
import type { CreditCard } from "@/types/user"
|
||||
|
||||
interface ReferenceCardProps {
|
||||
booking: BookingConfirmation["booking"]
|
||||
hotel: Hotel
|
||||
savedCreditCards: CreditCard[] | null
|
||||
refId: string
|
||||
}
|
||||
|
||||
export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
|
||||
export function ReferenceCard({
|
||||
booking,
|
||||
hotel,
|
||||
savedCreditCards,
|
||||
refId,
|
||||
}: ReferenceCardProps) {
|
||||
const [bookingStatus, setBookingStatus] = useState(booking.reservationStatus)
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
@@ -40,6 +49,7 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
|
||||
const toDate = dt(booking.checkOutDate).locale(lang)
|
||||
|
||||
const isCancelled = bookingStatus === BookingStatusEnum.Cancelled
|
||||
useGuaranteePaymentFailedToast()
|
||||
|
||||
const directionsUrl = `https://www.google.com/maps/dir/?api=1&destination=${hotel.location.latitude},${hotel.location.longitude}`
|
||||
|
||||
@@ -173,6 +183,8 @@ export function ReferenceCard({ booking, hotel }: ReferenceCardProps) {
|
||||
hotel={hotel}
|
||||
setBookingStatus={setBookingStatus}
|
||||
bookingStatus={bookingStatus}
|
||||
savedCreditCards={savedCreditCards}
|
||||
refId={refId}
|
||||
/>
|
||||
<Button fullWidth intent="secondary" asChild>
|
||||
<Link href={directionsUrl} target="_blank">
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getAncillaryPackages,
|
||||
getBookingConfirmation,
|
||||
getProfileSafely,
|
||||
getSavedPaymentCardsSafely,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
import { decrypt } from "@/server/routers/utils/encryption"
|
||||
|
||||
@@ -64,6 +65,10 @@ export async function MyStay({ refId }: { refId: string }) {
|
||||
hotelId: hotel.operaId,
|
||||
toDate: dt(booking.checkOutDate).format("YYYY-MM-DD"),
|
||||
})
|
||||
const supportedCards = hotel.merchantInformationData.cards
|
||||
const savedCreditCards = await getSavedPaymentCardsSafely({
|
||||
supportedCards,
|
||||
})
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
@@ -84,7 +89,12 @@ export async function MyStay({ refId }: { refId: string }) {
|
||||
<div className={styles.content}>
|
||||
<div className={styles.headerContainer}>
|
||||
<Header hotel={hotel} />
|
||||
<ReferenceCard booking={booking} hotel={hotel} />
|
||||
<ReferenceCard
|
||||
booking={booking}
|
||||
hotel={hotel}
|
||||
savedCreditCards={savedCreditCards}
|
||||
refId={refId}
|
||||
/>
|
||||
</div>
|
||||
{booking.showAncillaries && (
|
||||
<Ancillaries
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
forced-color-adjust: none;
|
||||
}
|
||||
|
||||
.topAlign {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.error {
|
||||
align-items: center;
|
||||
color: var(--Scandic-Red-60);
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function Checkbox({
|
||||
children,
|
||||
registerOptions,
|
||||
hideError,
|
||||
topAlign = false,
|
||||
}: React.PropsWithChildren<CheckboxProps>) {
|
||||
const { control } = useFormContext()
|
||||
const { field, fieldState } = useController({
|
||||
@@ -36,7 +37,9 @@ export default function Checkbox({
|
||||
>
|
||||
{({ isSelected }) => (
|
||||
<>
|
||||
<span className={styles.checkboxContainer}>
|
||||
<span
|
||||
className={`${styles.checkboxContainer} ${topAlign ? styles.topAlign : ""}`}
|
||||
>
|
||||
<span
|
||||
className={styles.checkbox}
|
||||
tabIndex={registerOptions?.disabled ? undefined : 0}
|
||||
|
||||
Reference in New Issue
Block a user