Merged in feat/BOOK-529-update-GLA-design-mystay (pull request #3230)
Feat/BOOK-529 update GLA design mystay * feat(BOOK-529): update gla design on my stay * feat(BOOK-529): open gla modal if error * feat(BOOK-529): add inline accordion to storybook * feat(529): move errormessage below message * feat(529): update infomodal * feat(BOOK-529): update infomodal * feat(BOOK-529): hide guarantee info for adding ancillaries if prepaid * feat(BOOK-529): update width on info dialog * feat(BOOK-529): fix alignment * feat(BOOK-529): check if member price * feat(BOOK-529): refactor msg * feat(BOOK-529): refactor terms and conditions to own component * feat(BOOK-529): clean up confirmation step Approved-by: Christel Westerberg
This commit is contained in:
@@ -4,12 +4,6 @@
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
.termsAndConditions {
|
||||
display: grid;
|
||||
gap: var(--Space-x2);
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
|
||||
.totalPointsContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -17,9 +11,27 @@
|
||||
padding: var(--Space-x1) var(--Space-x15);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
.guarantee {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
background-color: var(--Surface-Secondary-Default);
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
padding: var(--Space-x2);
|
||||
}
|
||||
|
||||
.paymentInfo {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.totalPoints {
|
||||
display: flex;
|
||||
gap: var(--Space-x15);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.accordionItem {
|
||||
border-radius: var(--Corner-radius-md);
|
||||
}
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
import { useWatch } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
|
||||
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
|
||||
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
|
||||
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||
|
||||
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||
|
||||
import { ancillaryError } from "../../../schema"
|
||||
|
||||
import styles from "./confirmationStep.module.css"
|
||||
|
||||
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||
@@ -33,17 +29,20 @@ export default function ConfirmationStep({
|
||||
}: ConfirmationStepProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const { checkInDate, guaranteeInfo, selectedAncillary } =
|
||||
|
||||
const { checkInDate, guaranteeInfo, selectedAncillary, booking } =
|
||||
useAddAncillaryStore((state) => ({
|
||||
checkInDate: state.booking.checkInDate,
|
||||
guaranteeInfo: state.booking.guaranteeInfo,
|
||||
selectedAncillary: state.selectedAncillary,
|
||||
booking: state.booking,
|
||||
}))
|
||||
const refundableDate = dt(checkInDate)
|
||||
.subtract(1, "day")
|
||||
.locale(lang)
|
||||
.format("23:59, dddd, D MMMM YYYY")
|
||||
|
||||
const mustBeGuaranteed = !guaranteeInfo && booking.isGuaranteeable
|
||||
const quantityWithCard = useWatch({ name: "quantityWithCard" })
|
||||
const quantityWithPoints = useWatch({ name: "quantityWithPoints" })
|
||||
const currentPoints = user?.membership?.currentPoints ?? 0
|
||||
@@ -51,20 +50,22 @@ export default function ConfirmationStep({
|
||||
quantityWithPoints && selectedAncillary?.points
|
||||
? selectedAncillary.points * quantityWithPoints
|
||||
: null
|
||||
|
||||
const accordionTitle = intl.formatMessage({
|
||||
id: "myStay.guarantee.guaranteeInformation",
|
||||
defaultMessage:
|
||||
"By adding your card, you also guarantee your room booking for late arrival ",
|
||||
})
|
||||
|
||||
const accordionContent = intl.formatMessage({
|
||||
id: "myStay.guarantee.guaranteeInformation.content",
|
||||
defaultMessage:
|
||||
"The hotel will hold your booking, even if you arrive after 18:00. Your card will only be charged in the event of a no-show.",
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.modalContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "addAncillary.confirmationStep.refundPolicy",
|
||||
defaultMessage:
|
||||
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.",
|
||||
},
|
||||
{ date: refundableDate }
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
{error && <Alert type={error.type} text={error.message} />}
|
||||
{!!quantityWithPoints && (
|
||||
<>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
@@ -107,118 +108,118 @@ export default function ConfirmationStep({
|
||||
)}
|
||||
{!!quantityWithCard && (
|
||||
<>
|
||||
<header>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.reserveWithCard",
|
||||
defaultMessage: "Reserve with Card",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
</header>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h2>
|
||||
{intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.paymentAtCheckInInfo",
|
||||
defaultMessage:
|
||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.",
|
||||
id: "addAncillary.confirmationStep.reserveWithCard",
|
||||
defaultMessage: "Reserve with Card",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "addAncillary.confirmationStep.refundPolicy",
|
||||
defaultMessage:
|
||||
"All ancillaries are fully refundable until {date}. Time selection and special requests are also modifiable.",
|
||||
},
|
||||
{ date: refundableDate }
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
{guaranteeInfo ? (
|
||||
<PaymentOptionsGroup name="paymentMethod">
|
||||
<PaymentOption
|
||||
value={PaymentMethodEnum.card}
|
||||
cardNumber={guaranteeInfo.maskedCard.slice(-4)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.creditCard",
|
||||
defaultMessage: "Credit card",
|
||||
})}
|
||||
/>
|
||||
</PaymentOptionsGroup>
|
||||
) : (
|
||||
<>
|
||||
{error ? (
|
||||
<Alert type={error.type} text={error.message} />
|
||||
) : (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
text={intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.guaranteeAddCard",
|
||||
defaultMessage:
|
||||
"By adding a card you also guarantee your room booking for late arrival.",
|
||||
})}
|
||||
<div className={styles.guarantee}>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
|
||||
<span>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.ancillary.guarantee.headingText",
|
||||
defaultMessage: "Payment will be made on check-in",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.ancillary.guarantee.infoText",
|
||||
defaultMessage:
|
||||
"The card is used to reserve your extras. You will be charged in case of no-show.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
{guaranteeInfo ? (
|
||||
<>
|
||||
<Divider />
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "payment.savedCard",
|
||||
defaultMessage: "Saved card",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<PaymentOptionsGroup name="paymentMethod">
|
||||
<PaymentOption
|
||||
value={PaymentMethodEnum.card}
|
||||
cardNumber={guaranteeInfo.maskedCard.slice(-4)}
|
||||
label={intl.formatMessage({
|
||||
id: "common.card",
|
||||
defaultMessage: "Card",
|
||||
})}
|
||||
hideRadioButton
|
||||
/>
|
||||
</PaymentOptionsGroup>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon
|
||||
icon="credit_score"
|
||||
size={24}
|
||||
color="CurrentColor"
|
||||
/>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.ancillary.guarantee.confirmationText",
|
||||
defaultMessage:
|
||||
"Confirm and provide your payment card details in the next step",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
{mustBeGuaranteed && (
|
||||
<AccordionItem
|
||||
title={accordionTitle}
|
||||
type="inline"
|
||||
className={styles.accordionItem}
|
||||
>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{accordionContent}</p>
|
||||
</Typography>
|
||||
</AccordionItem>
|
||||
)}
|
||||
{savedCreditCards && <Divider />}
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName={"paymentMethod"}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName={"paymentMethod"}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.termsAndConditions}>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "addAncillary.confirmationStep.termsAndConditionsNotice",
|
||||
defaultMessage:
|
||||
"Yes, I accept the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand that Scandic will process my personal data in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. There you can learn more about what data we process, your rights and where to turn if you have questions.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={bookingTermsAndConditionsRoutes[lang]}
|
||||
>
|
||||
{str}
|
||||
</TextLink>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<TextLink
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
href={privacyPolicyRoutes[lang]}
|
||||
>
|
||||
{str}
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
<Checkbox
|
||||
name="termsAndConditions"
|
||||
registerOptions={{ required: true }}
|
||||
errorCodeMessages={{
|
||||
[ancillaryError.TERMS_NOT_ACCEPTED]: intl.formatMessage({
|
||||
id: "common.mustAcceptTermsError",
|
||||
defaultMessage: "You must accept the terms and conditions",
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "booking.acceptBookingTerms",
|
||||
defaultMessage: "I accept the booking and cancellation terms",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</div>
|
||||
<TermsAndConditions />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,21 @@ import ModalContent from "./ModalContent"
|
||||
|
||||
import styles from "./modal.module.css"
|
||||
|
||||
export default function Modal({ children }: React.PropsWithChildren) {
|
||||
export default function Modal({
|
||||
children,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: React.PropsWithChildren<{
|
||||
isOpen?: boolean
|
||||
onOpenChange?: (value: boolean) => void
|
||||
}>) {
|
||||
return (
|
||||
<ModalOverlay className={styles.overlay} isDismissable>
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isDismissable
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<ModalRAC className={styles.modal}>{children}</ModalRAC>
|
||||
</ModalOverlay>
|
||||
)
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import { isAncillaryError } from "@/components/HotelReservation/MyStay/utils"
|
||||
|
||||
export function useGuaranteePaymentFailedToast() {
|
||||
const hasRunOnce = useRef(false)
|
||||
import GuaranteeDialog from "../ManageStay/Actions/GuaranteeLateArrival/GuaranteeDialog"
|
||||
|
||||
export default function GuaranteePaymentFailed() {
|
||||
const intl = useIntl()
|
||||
const searchParams = useSearchParams()
|
||||
const pathname = usePathname()
|
||||
const router = useRouter()
|
||||
|
||||
const [alert, setAlert] = useState<{
|
||||
type: AlertTypeEnum
|
||||
message: string
|
||||
} | null>(null)
|
||||
|
||||
const getErrorMessage = useCallback(
|
||||
(errorCode: string | null) => {
|
||||
switch (errorCode) {
|
||||
@@ -37,33 +42,36 @@ export function useGuaranteePaymentFailedToast() {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
// To prevent multiple toasts in strict mode
|
||||
if (hasRunOnce.current) {
|
||||
return
|
||||
}
|
||||
const errorCode = searchParams.get("errorCode")
|
||||
if (!errorCode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ancillary errors are handled in AddAncillaryFlowModal
|
||||
if (isAncillaryError(searchParams)) {
|
||||
hasRunOnce.current = true
|
||||
return
|
||||
}
|
||||
|
||||
const errorMessage = getErrorMessage(errorCode)
|
||||
const toastType =
|
||||
const message = getErrorMessage(errorCode)
|
||||
const type =
|
||||
errorCode === BookingErrorCodeEnum.TransactionCancelled
|
||||
? "warning"
|
||||
: "error"
|
||||
? AlertTypeEnum.Warning
|
||||
: AlertTypeEnum.Alarm
|
||||
|
||||
toast[toastType](errorMessage)
|
||||
setAlert({ type, message })
|
||||
|
||||
const queryParams = new URLSearchParams(searchParams.toString())
|
||||
queryParams.delete("errorCode")
|
||||
const newParams = new URLSearchParams(searchParams.toString())
|
||||
newParams.delete("errorCode")
|
||||
|
||||
router.push(`${pathname}?${queryParams.toString()}`)
|
||||
hasRunOnce.current = true
|
||||
router.replace(`${pathname}?${newParams.toString()}`)
|
||||
}, [searchParams, pathname, router, getErrorMessage])
|
||||
|
||||
if (!alert) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={!!alert} onOpenChange={() => setAlert(null)}>
|
||||
<GuaranteeDialog error={alert} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -13,13 +13,18 @@
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.termsAndConditions {
|
||||
color: var(--Text-Secondary);
|
||||
display: grid;
|
||||
.guarantee {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
background-color: var(--Surface-Secondary-Default);
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
padding: var(--Space-x2);
|
||||
}
|
||||
|
||||
.termsAndConditions .checkbox span {
|
||||
.paymentInfo {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,13 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { writeGlaToSessionStorage } from "@scandic-hotels/booking-flow/components/EnterDetails/Payment/PaymentCallback/helpers"
|
||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
|
||||
import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
|
||||
@@ -23,6 +21,7 @@ import { isWebview } from "@/constants/routes/webviews"
|
||||
import { env } from "@/env/client"
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
|
||||
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||
@@ -31,7 +30,12 @@ import { type GuaranteeFormData, paymentSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
export default function Form() {
|
||||
import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
|
||||
interface FormProps {
|
||||
error?: { type: AlertTypeEnum; message: string }
|
||||
}
|
||||
export default function Form({ error }: FormProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
@@ -114,38 +118,6 @@ export default function Form() {
|
||||
}
|
||||
}
|
||||
|
||||
const guaranteeMsg = intl.formatMessage(
|
||||
{
|
||||
id: "myStay.gla.termsAndConditionsMessage",
|
||||
defaultMessage:
|
||||
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
color="Text/Interactive/Secondary"
|
||||
target="_blank"
|
||||
href={bookingTermsAndConditionsRoutes[lang]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
color="Text/Interactive/Secondary"
|
||||
target="_blank"
|
||||
href={privacyPolicyRoutes[lang]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
@@ -153,24 +125,56 @@ export default function Form() {
|
||||
id="guarantee"
|
||||
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
|
||||
>
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName="paymentMethod"
|
||||
/>
|
||||
|
||||
<div className={styles.termsAndConditions}>
|
||||
<Checkbox className={styles.checkbox} name="termsAndConditions">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{guaranteeMsg}</p>
|
||||
{error && <Alert type={error.type} text={error.message} />}
|
||||
<div className={styles.guarantee}>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
|
||||
<span>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guarantee.headingText",
|
||||
defaultMessage: "Planning to arrive after 18.00?",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guarantee.infoText",
|
||||
defaultMessage:
|
||||
"Guarantee with a credit card is required to secure your booking. Without this guarantee, your room may be released after 18:00 in case of no-show.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon icon="credit_score" size={24} color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guarantee.headingText",
|
||||
defaultMessage:
|
||||
"Confirm and provide your payment card details in the next step",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</div>
|
||||
{savedCreditCards && <Divider />}
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName="paymentMethod"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TermsAndConditions />
|
||||
<div className={styles.guaranteeCost}>
|
||||
<div className={styles.guaranteeCostText}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const paymentError = {
|
||||
TERMS_REQUIRED: "TERMS_REQUIRED",
|
||||
} as const
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.string().nullable(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
}),
|
||||
termsAndConditions: z
|
||||
.boolean()
|
||||
.refine((value) => value === true, paymentError.TERMS_REQUIRED),
|
||||
})
|
||||
|
||||
export type GuaranteeFormData = z.output<typeof paymentSchema>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Dialog } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
|
||||
import Form from "../Form"
|
||||
|
||||
import styles from "./guaranteeDialog.module.css"
|
||||
|
||||
import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
|
||||
interface GuaranteeDialogProps {
|
||||
error?: { type: AlertTypeEnum; message: string }
|
||||
}
|
||||
|
||||
export default function GuaranteeDialog({ error }: GuaranteeDialogProps) {
|
||||
const intl = useIntl()
|
||||
const text = intl.formatMessage({
|
||||
id: "myStay.gla.heading",
|
||||
defaultMessage: "Add late arrival guarantee",
|
||||
})
|
||||
return (
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={close} title={text} />
|
||||
<Modal.Content.Body>
|
||||
<Form error={error} />
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={close}>
|
||||
{intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
<Modal.Content.Footer.Primary form="guarantee" type="submit">
|
||||
{intl.formatMessage({
|
||||
id: "common.confirm",
|
||||
defaultMessage: "Confirm",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
"use client"
|
||||
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||
import { DialogTrigger } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
import Form from "./Form"
|
||||
|
||||
import styles from "./guarantee.module.css"
|
||||
import GuaranteeDialog from "./GuaranteeDialog"
|
||||
|
||||
export default function GuaranteeLateArrival() {
|
||||
const intl = useIntl()
|
||||
@@ -29,14 +25,9 @@ export default function GuaranteeLateArrival() {
|
||||
trackMyStayPageLink("guarantee late arrival")
|
||||
}
|
||||
|
||||
const arriveLateMsg = intl.formatMessage({
|
||||
id: "myStay.gla.arriveLateMessage",
|
||||
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.",
|
||||
})
|
||||
const text = intl.formatMessage({
|
||||
id: "myStay.gla.heading",
|
||||
defaultMessage: "Guarantee late arrival",
|
||||
defaultMessage: "Add late arrival guarantee",
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -47,34 +38,7 @@ export default function GuaranteeLateArrival() {
|
||||
icon="check"
|
||||
/>
|
||||
<Modal>
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={close} title={text}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{arriveLateMsg}</p>
|
||||
</Typography>
|
||||
</Modal.Content.Header>
|
||||
<Modal.Content.Body>
|
||||
<Form />
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={close}>
|
||||
{intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
<Modal.Content.Footer.Primary form="guarantee" type="submit">
|
||||
{intl.formatMessage({
|
||||
id: "myStay.gla.guarantee",
|
||||
defaultMessage: "Guarantee",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)}
|
||||
</Dialog>
|
||||
<GuaranteeDialog />
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Space-x3);
|
||||
align-content: start;
|
||||
margin-top: var(--Space-x2);
|
||||
}
|
||||
|
||||
.infoButton {
|
||||
background-color: transparent;
|
||||
border-width: 0;
|
||||
padding: var(--Space-x025);
|
||||
color: var(--Icon-Interactive-Default);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
justify-self: stretch;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.content {
|
||||
max-width: 512px;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
justify-self: end;
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.textSecondary {
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button as ButtonRAC } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import Modal from "@scandic-hotels/design-system/Modal"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import styles from "./guaranteeInfoModal.module.css"
|
||||
|
||||
export function GuaranteeInfoModal() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonRAC
|
||||
type="button"
|
||||
className={styles.infoButton}
|
||||
onPress={() => setIsOpen(true)}
|
||||
>
|
||||
<MaterialIcon icon="info" size={20} color="CurrentColor" />
|
||||
</ButtonRAC>
|
||||
<Modal
|
||||
title={intl.formatMessage({
|
||||
id: "myStay.guaranteeInfoModal.heading",
|
||||
defaultMessage: "Your booking is guaranteed",
|
||||
})}
|
||||
isOpen={isOpen}
|
||||
onToggle={setIsOpen}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<Typography variant="Body/Lead text">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guaranteeInfo.description",
|
||||
defaultMessage:
|
||||
"The hotel will hold your booking, even if you arrive after 18:00. In case of a no-show, you will be charged for the first night.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="Body/Paragraph/mdRegular"
|
||||
className={styles.textSecondary}
|
||||
>
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guaranteeInfoModal.ancillariesInfo",
|
||||
defaultMessage:
|
||||
"If you added extras, they'll be charged in case of a no-show, unless cancelled by 23:59 the night before.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
className={styles.closeButton}
|
||||
variant="Secondary"
|
||||
color="Primary"
|
||||
size="Medium"
|
||||
typography="Body/Paragraph/mdBold"
|
||||
onPress={() => setIsOpen(false)}
|
||||
>
|
||||
{intl.formatMessage({
|
||||
id: "common.close",
|
||||
defaultMessage: "Close",
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
.label {
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
@@ -13,3 +13,9 @@
|
||||
.textDefault {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.guaranteeInfo {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--Space-x05);
|
||||
}
|
||||
|
||||
@@ -6,20 +6,20 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import { GuaranteeInfoModal } from "./GuaranteeInfoModal"
|
||||
|
||||
import styles from "./guaranteeInfo.module.css"
|
||||
|
||||
export default function GuaranteeInfo() {
|
||||
const intl = useIntl()
|
||||
const { allRoomsAreCancelled, guaranteeInfo, priceType } = useMyStayStore(
|
||||
(state) => ({
|
||||
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
||||
const { isGuaranteeable, guaranteeInfo, allRoomsAreCancelled } =
|
||||
useMyStayStore((state) => ({
|
||||
isGuaranteeable: state.bookedRoom.isGuaranteeable,
|
||||
guaranteeInfo: state.bookedRoom.guaranteeInfo,
|
||||
priceType: state.bookedRoom.priceType,
|
||||
})
|
||||
)
|
||||
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
||||
}))
|
||||
|
||||
const isRewardNight = priceType === "points"
|
||||
if (allRoomsAreCancelled || (!guaranteeInfo && !isRewardNight)) {
|
||||
if ((isGuaranteeable && !guaranteeInfo) || allRoomsAreCancelled) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -30,20 +30,23 @@ export default function GuaranteeInfo() {
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.textDefault}>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.lateArrival",
|
||||
defaultMessage: "Late arrival",
|
||||
id: "myStay.bookingGuaranteed",
|
||||
defaultMessage: "Booking guaranteed",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<div className={styles.guaranteeInfo}>
|
||||
<GuaranteeInfoModal />
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.roomHeldAfter18",
|
||||
defaultMessage: "Room held after 18:00",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.checkInAfter18",
|
||||
defaultMessage: "Check-in after 18:00",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast"
|
||||
|
||||
import TotalPrice from "../Rooms/TotalPrice"
|
||||
import GuaranteePaymentFailed from "./Actions/Upcoming/GuaranteePaymentFailed"
|
||||
import Actions from "./Actions"
|
||||
import BookingCode from "./BookingCode"
|
||||
import Cancellations from "./Cancellations"
|
||||
@@ -20,7 +19,6 @@ import styles from "./referenceCard.module.css"
|
||||
|
||||
export function ReferenceCard() {
|
||||
const intl = useIntl()
|
||||
useGuaranteePaymentFailedToast()
|
||||
return (
|
||||
<div className={styles.referenceCard}>
|
||||
<Reference />
|
||||
@@ -44,6 +42,7 @@ export function ReferenceCard() {
|
||||
</div>
|
||||
<BookingCode />
|
||||
<Actions />
|
||||
<GuaranteePaymentFailed />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
}
|
||||
|
||||
.row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: var(--Space-x1);
|
||||
gap: var(--Space-x2);
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,15 @@
|
||||
"use client"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
||||
import TotalPrice from "../../../TotalPrice"
|
||||
|
||||
import styles from "./details.module.css"
|
||||
|
||||
export default function PriceDetails() {
|
||||
const intl = useIntl()
|
||||
|
||||
const pricing = useMyStayStore((state) => ({
|
||||
cheques: state.bookedRoom.cheques,
|
||||
formattedTotalPrice: state.totalPrice,
|
||||
isCancelled: state.bookedRoom.isCancelled,
|
||||
currencyCode: state.bookedRoom.currencyCode,
|
||||
packages: state.bookedRoom.packages,
|
||||
priceType: state.bookedRoom.priceType,
|
||||
rateDefinition: state.bookedRoom.rateDefinition,
|
||||
totalPoints: state.bookedRoom.totalPoints,
|
||||
totalPrice: state.bookedRoom.totalPrice,
|
||||
vouchers: state.bookedRoom.vouchers,
|
||||
}))
|
||||
|
||||
let totalPrice = pricing.totalPrice
|
||||
// API returns negative values for totalPrice
|
||||
// on voucher bookings (╯°□°)╯︵ ┻━┻
|
||||
if (pricing.vouchers && totalPrice < 0) {
|
||||
const pkgsSum = sumPackages(pricing.packages)
|
||||
totalPrice = pkgsSum.price
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.priceDetails}>
|
||||
<div className={styles.price}>
|
||||
@@ -45,7 +21,7 @@ export default function PriceDetails() {
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<PriceType {...pricing} totalPrice={totalPrice} />
|
||||
<TotalPrice />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -23,7 +23,11 @@ export default function RoomDetailsSidePeek({
|
||||
user,
|
||||
}: RoomDetailsSidePeekProps) {
|
||||
const intl = useIntl()
|
||||
const bookedRoom = useMyStayStore((state) => state.bookedRoom)
|
||||
const { bookedRoom, totalPrice } = useMyStayStore((state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
totalPrice: state.totalPrice,
|
||||
}))
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
@@ -53,7 +57,11 @@ export default function RoomDetailsSidePeek({
|
||||
isOpen={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
>
|
||||
<BookedRoomSidePeekContent room={bookedRoom} user={user} />
|
||||
<BookedRoomSidePeekContent
|
||||
room={bookedRoom}
|
||||
user={user}
|
||||
totalPriceBooking={totalPrice}
|
||||
/>
|
||||
</SidePeekSelfControlled>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,41 +1,22 @@
|
||||
"use client"
|
||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import PriceType from "../PriceType"
|
||||
import Price from "../PriceType/Price"
|
||||
|
||||
import type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay"
|
||||
|
||||
export default function TotalPrice() {
|
||||
const { bookedRoom, formattedTotalPrice, rooms } = useMyStayStore(
|
||||
(state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
formattedTotalPrice: state.totalPrice,
|
||||
rooms: state.rooms,
|
||||
})
|
||||
)
|
||||
|
||||
const totalCheques = rooms.reduce((total, room) => total + room.cheques, 0)
|
||||
const totalPoints = rooms.reduce((total, room) => total + room.totalPoints, 0)
|
||||
|
||||
let totalPrice = rooms.reduce((total, room) => total + room.totalPrice, 0)
|
||||
if (rooms.some((room) => room.vouchers)) {
|
||||
const pkgsSum = sumPackages(rooms.flatMap((r) => r.packages || []))
|
||||
totalPrice = pkgsSum.price
|
||||
}
|
||||
|
||||
const { bookedRoom, totalPrice } = useMyStayStore((state) => ({
|
||||
bookedRoom: state.bookedRoom,
|
||||
totalPrice: state.totalPrice,
|
||||
rooms: state.rooms,
|
||||
}))
|
||||
return (
|
||||
<PriceType
|
||||
cheques={totalCheques}
|
||||
formattedTotalPrice={formattedTotalPrice}
|
||||
<Price
|
||||
isCancelled={bookedRoom.isCancelled}
|
||||
currencyCode={bookedRoom.currencyCode}
|
||||
priceType={bookedRoom.priceType}
|
||||
rateDefinition={bookedRoom.rateDefinition}
|
||||
totalPoints={totalPoints}
|
||||
totalPrice={totalPrice}
|
||||
vouchers={bookedRoom.vouchers}
|
||||
isMember={bookedRoom.rateDefinition.isMemberRate}
|
||||
price={totalPrice}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useFormContext } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
|
||||
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { ErrorMessage } from "@scandic-hotels/design-system/Form/ErrorMessage"
|
||||
import { TextLink } from "@scandic-hotels/design-system/TextLink"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { getErrorMessage } from "@/utils/getErrorMessage"
|
||||
|
||||
import styles from "./termsAndConditions.module.css"
|
||||
|
||||
export default function TermsAndConditions() {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const {
|
||||
formState: { errors },
|
||||
} = useFormContext()
|
||||
const termsAndConditionsMsg = intl.formatMessage(
|
||||
{
|
||||
id: "myStay.guarantee.termsAndConditions",
|
||||
defaultMessage:
|
||||
"Please accept the general <termsAndConditionsLink>Booking & Cancellation Terms </termsAndConditionsLink> and acknowledge that your data will be processed in accordance with <privacyPolicyLink> Scandic's Privacy policy </privacyPolicyLink>.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<TextLink
|
||||
href={bookingTermsAndConditionsRoutes[lang]}
|
||||
theme="InteractiveDefault"
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
isInline
|
||||
>
|
||||
{str}
|
||||
</TextLink>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<TextLink
|
||||
href={privacyPolicyRoutes[lang]}
|
||||
theme="InteractiveDefault"
|
||||
typography="Link/sm"
|
||||
target="_blank"
|
||||
isInline
|
||||
>
|
||||
{str}
|
||||
</TextLink>
|
||||
),
|
||||
}
|
||||
)
|
||||
return (
|
||||
<div className={styles.termsAndConditions}>
|
||||
<Checkbox
|
||||
name="termsAndConditions"
|
||||
registerOptions={{ required: true }}
|
||||
hideError
|
||||
>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "booking.acceptBookingTerms",
|
||||
defaultMessage: "I accept the booking and cancellation terms",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
<span>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{termsAndConditionsMsg}</p>
|
||||
</Typography>
|
||||
<ErrorMessage
|
||||
name="termsAndConditions"
|
||||
errors={errors}
|
||||
messageLabel={getErrorMessage(
|
||||
intl,
|
||||
errors["termsAndConditions"]?.message?.toString()
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.termsAndConditions {
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
@@ -93,7 +93,7 @@ export function mapRoomDetails({
|
||||
|
||||
const priceType = getPriceType(
|
||||
booking.cheques,
|
||||
booking.totalPoints,
|
||||
booking.roomPoints,
|
||||
booking.vouchers
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||
|
||||
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
||||
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
||||
import Price from "@/components/HotelReservation/MyStay/PriceType/Price"
|
||||
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
@@ -70,11 +71,13 @@ type Room = Pick<
|
||||
interface BookedRoomSidepeekContentProps {
|
||||
room: Room
|
||||
user: SafeUser
|
||||
totalPriceBooking?: string
|
||||
}
|
||||
|
||||
export default function BookedRoomSidePeekContent({
|
||||
room,
|
||||
user,
|
||||
totalPriceBooking,
|
||||
}: BookedRoomSidepeekContentProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
@@ -407,18 +410,25 @@ export default function BookedRoomSidePeekContent({
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
|
||||
<PriceType
|
||||
cheques={cheques}
|
||||
formattedTotalPrice={formattedTotalPrice}
|
||||
isCancelled={isCancelled}
|
||||
priceType={priceType}
|
||||
currencyCode={currencyCode}
|
||||
rateDefinition={rateDefinition}
|
||||
totalPoints={totalPoints}
|
||||
totalPrice={totalRoomPrice}
|
||||
vouchers={vouchers}
|
||||
/>
|
||||
{totalPriceBooking ? (
|
||||
<Price
|
||||
isCancelled={isCancelled}
|
||||
price={totalPriceBooking}
|
||||
isMember={rateDefinition.isMemberRate}
|
||||
/>
|
||||
) : (
|
||||
<PriceType
|
||||
cheques={cheques}
|
||||
formattedTotalPrice={formattedTotalPrice}
|
||||
isCancelled={isCancelled}
|
||||
priceType={priceType}
|
||||
currencyCode={currencyCode}
|
||||
rateDefinition={rateDefinition}
|
||||
totalPoints={totalPoints}
|
||||
totalPrice={totalRoomPrice}
|
||||
vouchers={vouchers}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@ export function calculateTotalPrice(
|
||||
}
|
||||
// room.totalPrice is a negative value when
|
||||
// its a vouchers booking (╯°□°)╯︵ ┻━┻
|
||||
if (room.totalPrice && !room.vouchers) {
|
||||
if (room.totalPrice > 0) {
|
||||
total.cash = total.cash + room.totalPrice
|
||||
}
|
||||
return total
|
||||
@@ -42,13 +42,6 @@ export function calculateTotalPrice(
|
||||
)
|
||||
|
||||
let totalPrice = ""
|
||||
if (totals.cheques) {
|
||||
totalPrice = `${totals.cheques} ${CurrencyEnum.CC}`
|
||||
}
|
||||
if (totals.points) {
|
||||
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
||||
totalPrice = `${appendTotalPrice}${totals.points} ${CurrencyEnum.POINTS}`
|
||||
}
|
||||
if (totals.vouchers) {
|
||||
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
||||
totalPrice = `${appendTotalPrice}${totals.vouchers} ${intl.formatMessage(
|
||||
@@ -62,6 +55,13 @@ export function calculateTotalPrice(
|
||||
}
|
||||
)}`
|
||||
}
|
||||
if (totals.cheques) {
|
||||
totalPrice = `${totals.cheques} ${CurrencyEnum.CC}`
|
||||
}
|
||||
if (totals.points) {
|
||||
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
||||
totalPrice = `${appendTotalPrice}${totals.points} ${CurrencyEnum.POINTS}`
|
||||
}
|
||||
if (totals.cash) {
|
||||
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
||||
const cashPrice = formatPrice(intl, totals.cash, currency)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { signupErrors } from "@scandic-hotels/trpc/routers/user/schemas"
|
||||
import { editProfileErrors } from "@/components/Forms/Edit/Profile/schema"
|
||||
import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema"
|
||||
import { ancillaryError } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/schema"
|
||||
import { paymentError } from "@/components/HotelReservation/MyStay/ReferenceCard/Actions/Upcoming/ManageStay/Actions/GuaranteeLateArrival/Form/schema"
|
||||
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
@@ -22,9 +23,10 @@ export function getErrorMessage(intl: IntlShape, errorCode?: string) {
|
||||
defaultMessage: "You must select at least one quantity",
|
||||
})
|
||||
case ancillaryError.TERMS_NOT_ACCEPTED:
|
||||
case paymentError.TERMS_REQUIRED:
|
||||
return intl.formatMessage({
|
||||
id: "addAncillary.confirmationStep.termsAndConditionsNoticeError",
|
||||
defaultMessage: "You must accept the terms and conditions to proceed",
|
||||
id: "common.mustAcceptTermsError",
|
||||
defaultMessage: "You must accept the terms and conditions",
|
||||
})
|
||||
case findMyBookingErrors.BOOKING_NUMBER_INVALID:
|
||||
return intl.formatMessage({
|
||||
|
||||
Reference in New Issue
Block a user