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);
|
gap: var(--Space-x2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.termsAndConditions {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--Space-x2);
|
|
||||||
color: var(--Text-Secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.totalPointsContainer {
|
.totalPointsContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -17,9 +11,27 @@
|
|||||||
padding: var(--Space-x1) var(--Space-x15);
|
padding: var(--Space-x1) var(--Space-x15);
|
||||||
border-radius: var(--Corner-radius-md);
|
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 {
|
.totalPoints {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Space-x15);
|
gap: var(--Space-x15);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.accordionItem {
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import { useWatch } from "react-hook-form"
|
import { useWatch } from "react-hook-form"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
|
||||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
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 { dt } from "@scandic-hotels/common/dt"
|
||||||
|
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
|
||||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
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 { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
|
||||||
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
|
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
|
||||||
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
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 { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
import { useAddAncillaryStore } from "@/stores/my-stay/add-ancillary-flow"
|
||||||
|
|
||||||
|
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||||
|
|
||||||
import { ancillaryError } from "../../../schema"
|
|
||||||
|
|
||||||
import styles from "./confirmationStep.module.css"
|
import styles from "./confirmationStep.module.css"
|
||||||
|
|
||||||
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
import type { ConfirmationStepProps } from "@/types/components/myPages/myStay/ancillaries"
|
||||||
@@ -33,17 +29,20 @@ export default function ConfirmationStep({
|
|||||||
}: ConfirmationStepProps) {
|
}: ConfirmationStepProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const { checkInDate, guaranteeInfo, selectedAncillary } =
|
|
||||||
|
const { checkInDate, guaranteeInfo, selectedAncillary, booking } =
|
||||||
useAddAncillaryStore((state) => ({
|
useAddAncillaryStore((state) => ({
|
||||||
checkInDate: state.booking.checkInDate,
|
checkInDate: state.booking.checkInDate,
|
||||||
guaranteeInfo: state.booking.guaranteeInfo,
|
guaranteeInfo: state.booking.guaranteeInfo,
|
||||||
selectedAncillary: state.selectedAncillary,
|
selectedAncillary: state.selectedAncillary,
|
||||||
|
booking: state.booking,
|
||||||
}))
|
}))
|
||||||
const refundableDate = dt(checkInDate)
|
const refundableDate = dt(checkInDate)
|
||||||
.subtract(1, "day")
|
.subtract(1, "day")
|
||||||
.locale(lang)
|
.locale(lang)
|
||||||
.format("23:59, dddd, D MMMM YYYY")
|
.format("23:59, dddd, D MMMM YYYY")
|
||||||
|
|
||||||
|
const mustBeGuaranteed = !guaranteeInfo && booking.isGuaranteeable
|
||||||
const quantityWithCard = useWatch({ name: "quantityWithCard" })
|
const quantityWithCard = useWatch({ name: "quantityWithCard" })
|
||||||
const quantityWithPoints = useWatch({ name: "quantityWithPoints" })
|
const quantityWithPoints = useWatch({ name: "quantityWithPoints" })
|
||||||
const currentPoints = user?.membership?.currentPoints ?? 0
|
const currentPoints = user?.membership?.currentPoints ?? 0
|
||||||
@@ -51,20 +50,22 @@ export default function ConfirmationStep({
|
|||||||
quantityWithPoints && selectedAncillary?.points
|
quantityWithPoints && selectedAncillary?.points
|
||||||
? selectedAncillary.points * quantityWithPoints
|
? selectedAncillary.points * quantityWithPoints
|
||||||
: null
|
: 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 (
|
return (
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
{error && <Alert type={error.type} text={error.message} />}
|
||||||
<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>
|
|
||||||
{!!quantityWithPoints && (
|
{!!quantityWithPoints && (
|
||||||
<>
|
<>
|
||||||
<Typography variant="Title/Subtitle/md">
|
<Typography variant="Title/Subtitle/md">
|
||||||
@@ -107,118 +108,118 @@ export default function ConfirmationStep({
|
|||||||
)}
|
)}
|
||||||
{!!quantityWithCard && (
|
{!!quantityWithCard && (
|
||||||
<>
|
<>
|
||||||
<header>
|
<Typography variant="Title/Subtitle/md">
|
||||||
<Typography variant="Title/Subtitle/md">
|
<h2>
|
||||||
<h2>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "addAncillary.confirmationStep.reserveWithCard",
|
|
||||||
defaultMessage: "Reserve with Card",
|
|
||||||
})}
|
|
||||||
</h2>
|
|
||||||
</Typography>
|
|
||||||
</header>
|
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "addAncillary.confirmationStep.paymentAtCheckInInfo",
|
id: "addAncillary.confirmationStep.reserveWithCard",
|
||||||
defaultMessage:
|
defaultMessage: "Reserve with Card",
|
||||||
"Payment will be made on check-in. The card will be only used to guarantee the ancillary in case of no-show.",
|
|
||||||
})}
|
})}
|
||||||
|
</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>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
{guaranteeInfo ? (
|
<div className={styles.guarantee}>
|
||||||
<PaymentOptionsGroup name="paymentMethod">
|
<div className={styles.paymentInfo}>
|
||||||
<PaymentOption
|
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
|
||||||
value={PaymentMethodEnum.card}
|
<span>
|
||||||
cardNumber={guaranteeInfo.maskedCard.slice(-4)}
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
label={intl.formatMessage({
|
<p>
|
||||||
id: "common.creditCard",
|
{intl.formatMessage({
|
||||||
defaultMessage: "Credit card",
|
id: "myStay.ancillary.guarantee.headingText",
|
||||||
})}
|
defaultMessage: "Payment will be made on check-in",
|
||||||
/>
|
})}
|
||||||
</PaymentOptionsGroup>
|
</p>
|
||||||
) : (
|
</Typography>
|
||||||
<>
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
{error ? (
|
<p>
|
||||||
<Alert type={error.type} text={error.message} />
|
{intl.formatMessage({
|
||||||
) : (
|
id: "myStay.ancillary.guarantee.infoText",
|
||||||
<Alert
|
defaultMessage:
|
||||||
type={AlertTypeEnum.Info}
|
"The card is used to reserve your extras. You will be charged in case of no-show.",
|
||||||
text={intl.formatMessage({
|
})}
|
||||||
id: "addAncillary.confirmationStep.guaranteeAddCard",
|
</p>
|
||||||
defaultMessage:
|
</Typography>
|
||||||
"By adding a card you also guarantee your room booking for late arrival.",
|
</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
|
</div>
|
||||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
|
||||||
...card,
|
|
||||||
cardType: card.cardType as PaymentMethodEnum,
|
|
||||||
}))}
|
|
||||||
onChange={(method) => {
|
|
||||||
trackUpdatePaymentMethod({ method })
|
|
||||||
}}
|
|
||||||
formName={"paymentMethod"}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<TermsAndConditions />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,21 @@ import ModalContent from "./ModalContent"
|
|||||||
|
|
||||||
import styles from "./modal.module.css"
|
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 (
|
return (
|
||||||
<ModalOverlay className={styles.overlay} isDismissable>
|
<ModalOverlay
|
||||||
|
className={styles.overlay}
|
||||||
|
isDismissable
|
||||||
|
isOpen={isOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
>
|
||||||
<ModalRAC className={styles.modal}>{children}</ModalRAC>
|
<ModalRAC className={styles.modal}>{children}</ModalRAC>
|
||||||
</ModalOverlay>
|
</ModalOverlay>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,21 +1,26 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
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 { 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 { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode"
|
||||||
|
|
||||||
|
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||||
import { isAncillaryError } from "@/components/HotelReservation/MyStay/utils"
|
import { isAncillaryError } from "@/components/HotelReservation/MyStay/utils"
|
||||||
|
|
||||||
export function useGuaranteePaymentFailedToast() {
|
import GuaranteeDialog from "../ManageStay/Actions/GuaranteeLateArrival/GuaranteeDialog"
|
||||||
const hasRunOnce = useRef(false)
|
|
||||||
|
export default function GuaranteePaymentFailed() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const [alert, setAlert] = useState<{
|
||||||
|
type: AlertTypeEnum
|
||||||
|
message: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
const getErrorMessage = useCallback(
|
const getErrorMessage = useCallback(
|
||||||
(errorCode: string | null) => {
|
(errorCode: string | null) => {
|
||||||
switch (errorCode) {
|
switch (errorCode) {
|
||||||
@@ -37,33 +42,36 @@ export function useGuaranteePaymentFailedToast() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// To prevent multiple toasts in strict mode
|
|
||||||
if (hasRunOnce.current) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const errorCode = searchParams.get("errorCode")
|
const errorCode = searchParams.get("errorCode")
|
||||||
if (!errorCode) {
|
if (!errorCode) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ancillary errors are handled in AddAncillaryFlowModal
|
|
||||||
if (isAncillaryError(searchParams)) {
|
if (isAncillaryError(searchParams)) {
|
||||||
hasRunOnce.current = true
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(errorCode)
|
const message = getErrorMessage(errorCode)
|
||||||
const toastType =
|
const type =
|
||||||
errorCode === BookingErrorCodeEnum.TransactionCancelled
|
errorCode === BookingErrorCodeEnum.TransactionCancelled
|
||||||
? "warning"
|
? AlertTypeEnum.Warning
|
||||||
: "error"
|
: AlertTypeEnum.Alarm
|
||||||
|
|
||||||
toast[toastType](errorMessage)
|
setAlert({ type, message })
|
||||||
|
|
||||||
const queryParams = new URLSearchParams(searchParams.toString())
|
const newParams = new URLSearchParams(searchParams.toString())
|
||||||
queryParams.delete("errorCode")
|
newParams.delete("errorCode")
|
||||||
|
|
||||||
router.push(`${pathname}?${queryParams.toString()}`)
|
router.replace(`${pathname}?${newParams.toString()}`)
|
||||||
hasRunOnce.current = true
|
|
||||||
}, [searchParams, pathname, router, getErrorMessage])
|
}, [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);
|
gap: var(--Space-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.termsAndConditions {
|
.guarantee {
|
||||||
color: var(--Text-Secondary);
|
display: flex;
|
||||||
display: grid;
|
flex-direction: column;
|
||||||
gap: var(--Space-x2);
|
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;
|
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 { writeGlaToSessionStorage } from "@scandic-hotels/booking-flow/components/EnterDetails/Payment/PaymentCallback/helpers"
|
||||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
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 { 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 { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||||
|
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
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 { 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 { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
|
||||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
|
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
|
||||||
@@ -23,6 +21,7 @@ import { isWebview } from "@/constants/routes/webviews"
|
|||||||
import { env } from "@/env/client"
|
import { env } from "@/env/client"
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
|
||||||
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||||
@@ -31,7 +30,12 @@ import { type GuaranteeFormData, paymentSchema } from "./schema"
|
|||||||
|
|
||||||
import styles from "./form.module.css"
|
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 intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const pathname = usePathname()
|
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 (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form
|
<form
|
||||||
@@ -153,24 +125,56 @@ export default function Form() {
|
|||||||
id="guarantee"
|
id="guarantee"
|
||||||
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
|
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
|
||||||
>
|
>
|
||||||
<SelectPaymentMethod
|
{error && <Alert type={error.type} text={error.message} />}
|
||||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
<div className={styles.guarantee}>
|
||||||
...card,
|
<div className={styles.paymentInfo}>
|
||||||
cardType: card.cardType as PaymentMethodEnum,
|
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
|
||||||
}))}
|
<span>
|
||||||
onChange={(method) => {
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
trackUpdatePaymentMethod({ method })
|
<p>
|
||||||
}}
|
{intl.formatMessage({
|
||||||
formName="paymentMethod"
|
id: "myStay.guarantee.headingText",
|
||||||
/>
|
defaultMessage: "Planning to arrive after 18.00?",
|
||||||
|
})}
|
||||||
<div className={styles.termsAndConditions}>
|
</p>
|
||||||
<Checkbox className={styles.checkbox} name="termsAndConditions">
|
</Typography>
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
<p>{guaranteeMsg}</p>
|
<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>
|
</Typography>
|
||||||
</Checkbox>
|
</div>
|
||||||
|
{savedCreditCards && <Divider />}
|
||||||
|
<SelectPaymentMethod
|
||||||
|
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||||
|
...card,
|
||||||
|
cardType: card.cardType as PaymentMethodEnum,
|
||||||
|
}))}
|
||||||
|
onChange={(method) => {
|
||||||
|
trackUpdatePaymentMethod({ method })
|
||||||
|
}}
|
||||||
|
formName="paymentMethod"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TermsAndConditions />
|
||||||
<div className={styles.guaranteeCost}>
|
<div className={styles.guaranteeCost}>
|
||||||
<div className={styles.guaranteeCostText}>
|
<div className={styles.guaranteeCostText}>
|
||||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
export const paymentError = {
|
||||||
|
TERMS_REQUIRED: "TERMS_REQUIRED",
|
||||||
|
} as const
|
||||||
|
|
||||||
export const paymentSchema = z.object({
|
export const paymentSchema = z.object({
|
||||||
paymentMethod: z.string().nullable(),
|
paymentMethod: z.string().nullable(),
|
||||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
termsAndConditions: z
|
||||||
message: "You must accept the terms and conditions",
|
.boolean()
|
||||||
}),
|
.refine((value) => value === true, paymentError.TERMS_REQUIRED),
|
||||||
})
|
})
|
||||||
|
|
||||||
export type GuaranteeFormData = z.output<typeof paymentSchema>
|
export type GuaranteeFormData = z.output<typeof paymentSchema>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
.dialog {
|
.dialog {
|
||||||
max-width: 690px;
|
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"
|
"use client"
|
||||||
import { Dialog, DialogTrigger } from "react-aria-components"
|
import { DialogTrigger } from "react-aria-components"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
||||||
|
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
import { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||||
|
|
||||||
import ActionsButton from "../ActionsButton"
|
import ActionsButton from "../ActionsButton"
|
||||||
import Form from "./Form"
|
import GuaranteeDialog from "./GuaranteeDialog"
|
||||||
|
|
||||||
import styles from "./guarantee.module.css"
|
|
||||||
|
|
||||||
export default function GuaranteeLateArrival() {
|
export default function GuaranteeLateArrival() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
@@ -29,14 +25,9 @@ export default function GuaranteeLateArrival() {
|
|||||||
trackMyStayPageLink("guarantee late arrival")
|
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({
|
const text = intl.formatMessage({
|
||||||
id: "myStay.gla.heading",
|
id: "myStay.gla.heading",
|
||||||
defaultMessage: "Guarantee late arrival",
|
defaultMessage: "Add late arrival guarantee",
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,34 +38,7 @@ export default function GuaranteeLateArrival() {
|
|||||||
icon="check"
|
icon="check"
|
||||||
/>
|
/>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog className={styles.dialog}>
|
<GuaranteeDialog />
|
||||||
{({ 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>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</DialogTrigger>
|
</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 {
|
.label {
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--Space-x1);
|
gap: var(--Space-x1);
|
||||||
}
|
}
|
||||||
@@ -13,3 +13,9 @@
|
|||||||
.textDefault {
|
.textDefault {
|
||||||
color: var(--Text-Default);
|
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 { useMyStayStore } from "@/stores/my-stay"
|
||||||
|
|
||||||
|
import { GuaranteeInfoModal } from "./GuaranteeInfoModal"
|
||||||
|
|
||||||
import styles from "./guaranteeInfo.module.css"
|
import styles from "./guaranteeInfo.module.css"
|
||||||
|
|
||||||
export default function GuaranteeInfo() {
|
export default function GuaranteeInfo() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const { allRoomsAreCancelled, guaranteeInfo, priceType } = useMyStayStore(
|
const { isGuaranteeable, guaranteeInfo, allRoomsAreCancelled } =
|
||||||
(state) => ({
|
useMyStayStore((state) => ({
|
||||||
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
isGuaranteeable: state.bookedRoom.isGuaranteeable,
|
||||||
guaranteeInfo: state.bookedRoom.guaranteeInfo,
|
guaranteeInfo: state.bookedRoom.guaranteeInfo,
|
||||||
priceType: state.bookedRoom.priceType,
|
allRoomsAreCancelled: state.allRoomsAreCancelled,
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
|
|
||||||
const isRewardNight = priceType === "points"
|
if ((isGuaranteeable && !guaranteeInfo) || allRoomsAreCancelled) {
|
||||||
if (allRoomsAreCancelled || (!guaranteeInfo && !isRewardNight)) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,20 +30,23 @@ export default function GuaranteeInfo() {
|
|||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<p className={styles.textDefault}>
|
<p className={styles.textDefault}>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "myStay.lateArrival",
|
id: "myStay.bookingGuaranteed",
|
||||||
defaultMessage: "Late arrival",
|
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>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "myStay.checkInAfter18",
|
|
||||||
defaultMessage: "Check-in after 18:00",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import { useIntl } from "react-intl"
|
|||||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useGuaranteePaymentFailedToast } from "@/hooks/booking/useGuaranteePaymentFailedToast"
|
|
||||||
|
|
||||||
import TotalPrice from "../Rooms/TotalPrice"
|
import TotalPrice from "../Rooms/TotalPrice"
|
||||||
|
import GuaranteePaymentFailed from "./Actions/Upcoming/GuaranteePaymentFailed"
|
||||||
import Actions from "./Actions"
|
import Actions from "./Actions"
|
||||||
import BookingCode from "./BookingCode"
|
import BookingCode from "./BookingCode"
|
||||||
import Cancellations from "./Cancellations"
|
import Cancellations from "./Cancellations"
|
||||||
@@ -20,7 +19,6 @@ import styles from "./referenceCard.module.css"
|
|||||||
|
|
||||||
export function ReferenceCard() {
|
export function ReferenceCard() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
useGuaranteePaymentFailedToast()
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.referenceCard}>
|
<div className={styles.referenceCard}>
|
||||||
<Reference />
|
<Reference />
|
||||||
@@ -44,6 +42,7 @@ export function ReferenceCard() {
|
|||||||
</div>
|
</div>
|
||||||
<BookingCode />
|
<BookingCode />
|
||||||
<Actions />
|
<Actions />
|
||||||
|
<GuaranteePaymentFailed />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding-top: var(--Space-x1);
|
padding-top: var(--Space-x1);
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
import TotalPrice from "../../../TotalPrice"
|
||||||
|
|
||||||
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
|
||||||
|
|
||||||
import styles from "./details.module.css"
|
import styles from "./details.module.css"
|
||||||
|
|
||||||
export default function PriceDetails() {
|
export default function PriceDetails() {
|
||||||
const intl = useIntl()
|
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 (
|
return (
|
||||||
<div className={styles.priceDetails}>
|
<div className={styles.priceDetails}>
|
||||||
<div className={styles.price}>
|
<div className={styles.price}>
|
||||||
@@ -45,7 +21,7 @@ export default function PriceDetails() {
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
<PriceType {...pricing} totalPrice={totalPrice} />
|
<TotalPrice />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export default function RoomDetailsSidePeek({
|
|||||||
user,
|
user,
|
||||||
}: RoomDetailsSidePeekProps) {
|
}: RoomDetailsSidePeekProps) {
|
||||||
const intl = useIntl()
|
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)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +57,11 @@ export default function RoomDetailsSidePeek({
|
|||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={() => setIsOpen(false)}
|
onClose={() => setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<BookedRoomSidePeekContent room={bookedRoom} user={user} />
|
<BookedRoomSidePeekContent
|
||||||
|
room={bookedRoom}
|
||||||
|
user={user}
|
||||||
|
totalPriceBooking={totalPrice}
|
||||||
|
/>
|
||||||
</SidePeekSelfControlled>
|
</SidePeekSelfControlled>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,41 +1,22 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import { sumPackages } from "@scandic-hotels/booking-flow/utils/SelectRate"
|
|
||||||
|
|
||||||
import { useMyStayStore } from "@/stores/my-stay"
|
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"
|
import type { PriceType as _PriceType } from "@/types/components/hotelReservation/myStay/myStay"
|
||||||
|
|
||||||
export default function TotalPrice() {
|
export default function TotalPrice() {
|
||||||
const { bookedRoom, formattedTotalPrice, rooms } = useMyStayStore(
|
const { bookedRoom, totalPrice } = useMyStayStore((state) => ({
|
||||||
(state) => ({
|
bookedRoom: state.bookedRoom,
|
||||||
bookedRoom: state.bookedRoom,
|
totalPrice: state.totalPrice,
|
||||||
formattedTotalPrice: state.totalPrice,
|
rooms: state.rooms,
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PriceType
|
<Price
|
||||||
cheques={totalCheques}
|
|
||||||
formattedTotalPrice={formattedTotalPrice}
|
|
||||||
isCancelled={bookedRoom.isCancelled}
|
isCancelled={bookedRoom.isCancelled}
|
||||||
currencyCode={bookedRoom.currencyCode}
|
isMember={bookedRoom.rateDefinition.isMemberRate}
|
||||||
priceType={bookedRoom.priceType}
|
price={totalPrice}
|
||||||
rateDefinition={bookedRoom.rateDefinition}
|
|
||||||
totalPoints={totalPoints}
|
|
||||||
totalPrice={totalPrice}
|
|
||||||
vouchers={bookedRoom.vouchers}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
const priceType = getPriceType(
|
||||||
booking.cheques,
|
booking.cheques,
|
||||||
booking.totalPoints,
|
booking.roomPoints,
|
||||||
booking.vouchers
|
booking.vouchers
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
|||||||
|
|
||||||
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
import GuestDetails from "@/components/HotelReservation/MyStay/GuestDetails"
|
||||||
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
import PriceType from "@/components/HotelReservation/MyStay/PriceType"
|
||||||
|
import Price from "@/components/HotelReservation/MyStay/PriceType/Price"
|
||||||
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
import { hasModifiableRate } from "@/components/HotelReservation/MyStay/utils"
|
||||||
import useLang from "@/hooks/useLang"
|
import useLang from "@/hooks/useLang"
|
||||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||||
@@ -70,11 +71,13 @@ type Room = Pick<
|
|||||||
interface BookedRoomSidepeekContentProps {
|
interface BookedRoomSidepeekContentProps {
|
||||||
room: Room
|
room: Room
|
||||||
user: SafeUser
|
user: SafeUser
|
||||||
|
totalPriceBooking?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BookedRoomSidePeekContent({
|
export default function BookedRoomSidePeekContent({
|
||||||
room,
|
room,
|
||||||
user,
|
user,
|
||||||
|
totalPriceBooking,
|
||||||
}: BookedRoomSidepeekContentProps) {
|
}: BookedRoomSidepeekContentProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
@@ -407,18 +410,25 @@ export default function BookedRoomSidePeekContent({
|
|||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{totalPriceBooking ? (
|
||||||
<PriceType
|
<Price
|
||||||
cheques={cheques}
|
isCancelled={isCancelled}
|
||||||
formattedTotalPrice={formattedTotalPrice}
|
price={totalPriceBooking}
|
||||||
isCancelled={isCancelled}
|
isMember={rateDefinition.isMemberRate}
|
||||||
priceType={priceType}
|
/>
|
||||||
currencyCode={currencyCode}
|
) : (
|
||||||
rateDefinition={rateDefinition}
|
<PriceType
|
||||||
totalPoints={totalPoints}
|
cheques={cheques}
|
||||||
totalPrice={totalRoomPrice}
|
formattedTotalPrice={formattedTotalPrice}
|
||||||
vouchers={vouchers}
|
isCancelled={isCancelled}
|
||||||
/>
|
priceType={priceType}
|
||||||
|
currencyCode={currencyCode}
|
||||||
|
rateDefinition={rateDefinition}
|
||||||
|
totalPoints={totalPoints}
|
||||||
|
totalPrice={totalRoomPrice}
|
||||||
|
vouchers={vouchers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function calculateTotalPrice(
|
|||||||
}
|
}
|
||||||
// room.totalPrice is a negative value when
|
// room.totalPrice is a negative value when
|
||||||
// its a vouchers booking (╯°□°)╯︵ ┻━┻
|
// its a vouchers booking (╯°□°)╯︵ ┻━┻
|
||||||
if (room.totalPrice && !room.vouchers) {
|
if (room.totalPrice > 0) {
|
||||||
total.cash = total.cash + room.totalPrice
|
total.cash = total.cash + room.totalPrice
|
||||||
}
|
}
|
||||||
return total
|
return total
|
||||||
@@ -42,13 +42,6 @@ export function calculateTotalPrice(
|
|||||||
)
|
)
|
||||||
|
|
||||||
let totalPrice = ""
|
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) {
|
if (totals.vouchers) {
|
||||||
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
||||||
totalPrice = `${appendTotalPrice}${totals.vouchers} ${intl.formatMessage(
|
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) {
|
if (totals.cash) {
|
||||||
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
const appendTotalPrice = totalPrice ? `${totalPrice} + ` : ""
|
||||||
const cashPrice = formatPrice(intl, totals.cash, currency)
|
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 { editProfileErrors } from "@/components/Forms/Edit/Profile/schema"
|
||||||
import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema"
|
import { findMyBookingErrors } from "@/components/HotelReservation/FindMyBooking/schema"
|
||||||
import { ancillaryError } from "@/components/HotelReservation/MyStay/Ancillaries/AddAncillaryFlow/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"
|
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",
|
defaultMessage: "You must select at least one quantity",
|
||||||
})
|
})
|
||||||
case ancillaryError.TERMS_NOT_ACCEPTED:
|
case ancillaryError.TERMS_NOT_ACCEPTED:
|
||||||
|
case paymentError.TERMS_REQUIRED:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage({
|
||||||
id: "addAncillary.confirmationStep.termsAndConditionsNoticeError",
|
id: "common.mustAcceptTermsError",
|
||||||
defaultMessage: "You must accept the terms and conditions to proceed",
|
defaultMessage: "You must accept the terms and conditions",
|
||||||
})
|
})
|
||||||
case findMyBookingErrors.BOOKING_NUMBER_INVALID:
|
case findMyBookingErrors.BOOKING_NUMBER_INVALID:
|
||||||
return intl.formatMessage({
|
return intl.formatMessage({
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--Space-x1);
|
|
||||||
padding-top: var(--Space-x2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.content ol {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary {
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Space-x05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.summary::-webkit-details-marker,
|
|
||||||
.summary::marker {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import Body from "@scandic-hotels/design-system/Body"
|
|
||||||
import Caption from "@scandic-hotels/design-system/Caption"
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|
||||||
|
|
||||||
import styles from "./guaranteeDetails.module.css"
|
|
||||||
|
|
||||||
export default function GuaranteeDetails() {
|
|
||||||
const intl = useIntl()
|
|
||||||
return (
|
|
||||||
<details>
|
|
||||||
<Caption color="burgundy" type="bold" asChild>
|
|
||||||
<summary className={styles.summary}>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "common.howItWorks",
|
|
||||||
defaultMessage: "How it works",
|
|
||||||
})}
|
|
||||||
<MaterialIcon
|
|
||||||
icon="keyboard_arrow_down"
|
|
||||||
color="Icon/Interactive/Default"
|
|
||||||
size={16}
|
|
||||||
/>
|
|
||||||
</summary>
|
|
||||||
</Caption>
|
|
||||||
<section className={styles.content}>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "enterDetails.payment.guaranteeInfoDescription",
|
|
||||||
defaultMessage:
|
|
||||||
"When guaranteeing your booking, we will hold the booking until 07:00 until the day after check-in. This will provide you as a guest with added flexibility for check-in times.",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "enterDetails.payment.guaranteeInfoWhatToDo",
|
|
||||||
defaultMessage: "What you have to do to guarantee booking:",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
<ol>
|
|
||||||
<Body asChild>
|
|
||||||
<li>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "enterDetails.payment.guaranteeInfoCompleteBooking",
|
|
||||||
defaultMessage: "Complete the booking",
|
|
||||||
})}
|
|
||||||
</li>
|
|
||||||
</Body>
|
|
||||||
<Body asChild>
|
|
||||||
<li>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "enterDetails.payment.guaranteeInfoProvideCard",
|
|
||||||
defaultMessage: "Provide a payment card in the next step",
|
|
||||||
})}
|
|
||||||
</li>
|
|
||||||
</Body>
|
|
||||||
</ol>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "enterDetails.payment.guaranteeInfoMandatoryNote",
|
|
||||||
defaultMessage:
|
|
||||||
"Please note that this is mandatory, and that your card will only be charged in the event of a no-show.",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
</section>
|
|
||||||
</details>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ const meta: Meta<typeof Accordion> = {
|
|||||||
argTypes: {
|
argTypes: {
|
||||||
type: {
|
type: {
|
||||||
control: 'select',
|
control: 'select',
|
||||||
options: ['card', 'sidepeek'],
|
options: ['card', 'sidepeek', 'inline'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -137,3 +137,22 @@ export const WithSubtitle: Story = {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Inline: Story = {
|
||||||
|
args: {
|
||||||
|
type: 'inline',
|
||||||
|
},
|
||||||
|
render: () => (
|
||||||
|
<Accordion type="inline">
|
||||||
|
<AccordionItem title="Read more about our rooms">
|
||||||
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
|
<p>
|
||||||
|
All rooms feature comfortable beds, modern amenities, and
|
||||||
|
complimentary Wi-Fi. Check-in is available from 3 PM and check-out
|
||||||
|
is at 12 PM.
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
list-style: none;
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
background-color: var(--Surface-Primary-Default);
|
||||||
|
.content {
|
||||||
|
padding: var(--Space-x1) 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.summary:hover {
|
.summary:hover {
|
||||||
background-color: var(--Surface-Primary-Hover);
|
background-color: var(--Surface-Primary-Hover);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ export default function AccordionItem({
|
|||||||
<Typography variant="Title/Subtitle/md">
|
<Typography variant="Title/Subtitle/md">
|
||||||
<p className={styles.title}>{title}</p>
|
<p className={styles.title}>{title}</p>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
) : type === 'inline' ? (
|
||||||
|
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||||
|
<p className={styles.title}>{title}</p>
|
||||||
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
{subtitle || showAsSubtitle ? (
|
{subtitle || showAsSubtitle ? (
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const accordionItemVariants = cva(styles.accordionItem, {
|
|||||||
type: {
|
type: {
|
||||||
card: styles.card,
|
card: styles.card,
|
||||||
sidepeek: styles.sidepeek,
|
sidepeek: styles.sidepeek,
|
||||||
|
inline: styles.inline,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const accordionVariants = cva(styles.accordion, {
|
|||||||
type: {
|
type: {
|
||||||
card: styles.card,
|
card: styles.card,
|
||||||
sidepeek: styles.sidepeek,
|
sidepeek: styles.sidepeek,
|
||||||
|
inline: styles.inline,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export type PaymentOptionProps = {
|
|||||||
value: PaymentMethodEnum
|
value: PaymentMethodEnum
|
||||||
label: string
|
label: string
|
||||||
cardNumber?: string
|
cardNumber?: string
|
||||||
|
hideRadioButton?: boolean
|
||||||
}
|
}
|
||||||
export function PaymentOption({
|
export function PaymentOption({
|
||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
cardNumber,
|
cardNumber,
|
||||||
|
hideRadioButton = false,
|
||||||
}: PaymentOptionProps) {
|
}: PaymentOptionProps) {
|
||||||
return (
|
return (
|
||||||
<Radio
|
<Radio
|
||||||
@@ -28,10 +30,12 @@ export function PaymentOption({
|
|||||||
{({ isSelected }) => (
|
{({ isSelected }) => (
|
||||||
<>
|
<>
|
||||||
<div className={styles.titleContainer}>
|
<div className={styles.titleContainer}>
|
||||||
<span
|
{!hideRadioButton && (
|
||||||
className={cx(styles.radio, { [styles.selected]: isSelected })}
|
<span
|
||||||
aria-hidden
|
className={cx(styles.radio, { [styles.selected]: isSelected })}
|
||||||
/>
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Typography variant="Body/Paragraph/mdRegular">
|
<Typography variant="Body/Paragraph/mdRegular">
|
||||||
<Label>{label}</Label>
|
<Label>{label}</Label>
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|||||||
@@ -31,12 +31,19 @@ export function SelectPaymentMethod({
|
|||||||
formName,
|
formName,
|
||||||
}: SelectPaymentMethodProps) {
|
}: SelectPaymentMethodProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const hasSavedCards = paymentMethods.length > 0
|
||||||
|
|
||||||
|
if (!hasSavedCards) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const mySavedCardsLabel = paymentMethods.length
|
const mySavedCardsLabel = paymentMethods.length
|
||||||
? intl.formatMessage({
|
? intl.formatMessage({
|
||||||
id: 'payment.mySavedCards',
|
id: 'payment.mySavedCards',
|
||||||
defaultMessage: 'My saved cards',
|
defaultMessage: 'My saved cards',
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const otherCardLabel = paymentMethods.length
|
const otherCardLabel = paymentMethods.length
|
||||||
? intl.formatMessage({
|
? intl.formatMessage({
|
||||||
id: 'common.other',
|
id: 'common.other',
|
||||||
@@ -44,8 +51,6 @@ export function SelectPaymentMethod({
|
|||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const hasSavedCards = paymentMethods.length > 0
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<PaymentOptionsGroup
|
<PaymentOptionsGroup
|
||||||
@@ -63,32 +68,27 @@ export function SelectPaymentMethod({
|
|||||||
defaultMessage: 'Card options',
|
defaultMessage: 'Card options',
|
||||||
})}
|
})}
|
||||||
</Label>
|
</Label>
|
||||||
{hasSavedCards ? (
|
<Typography variant="Title/Overline/sm">
|
||||||
<>
|
<span>{mySavedCardsLabel}</span>
|
||||||
<Typography variant="Title/Overline/sm">
|
</Typography>
|
||||||
<span>{mySavedCardsLabel}</span>
|
{paymentMethods?.map((paymentMethods) => {
|
||||||
</Typography>
|
const label =
|
||||||
{paymentMethods?.map((paymentMethods) => {
|
PAYMENT_METHOD_TITLES[paymentMethods.cardType] ||
|
||||||
const label =
|
paymentMethods.alias ||
|
||||||
PAYMENT_METHOD_TITLES[paymentMethods.cardType] ||
|
paymentMethods.cardType
|
||||||
paymentMethods.alias ||
|
|
||||||
paymentMethods.cardType
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PaymentOption
|
|
||||||
key={paymentMethods.id}
|
|
||||||
value={paymentMethods.id as PaymentMethodEnum}
|
|
||||||
label={label}
|
|
||||||
cardNumber={paymentMethods.truncatedNumber}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<Typography variant="Title/Overline/sm">
|
|
||||||
<span>{otherCardLabel}</span>
|
|
||||||
</Typography>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PaymentOption
|
||||||
|
key={paymentMethods.id}
|
||||||
|
value={paymentMethods.id as PaymentMethodEnum}
|
||||||
|
label={label}
|
||||||
|
cardNumber={paymentMethods.truncatedNumber}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
<Typography variant="Title/Overline/sm">
|
||||||
|
<span>{otherCardLabel}</span>
|
||||||
|
</Typography>
|
||||||
<PaymentOption
|
<PaymentOption
|
||||||
value={PAYMENT_METHOD_TITLES.card as PaymentMethodEnum}
|
value={PAYMENT_METHOD_TITLES.card as PaymentMethodEnum}
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
|
|||||||
@@ -276,7 +276,7 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: block;
|
font-display: block;
|
||||||
src: url(/_static/shared/fonts/material-symbols/rounded-1b8067b7.woff2)
|
src: url(/_static/shared/fonts/material-symbols/rounded-3e9207ba.woff2)
|
||||||
format('woff2');
|
format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const icons = [
|
|||||||
"countertops",
|
"countertops",
|
||||||
"credit_card_heart",
|
"credit_card_heart",
|
||||||
"credit_card",
|
"credit_card",
|
||||||
|
"credit_score",
|
||||||
"curtains_closed",
|
"curtains_closed",
|
||||||
"curtains",
|
"curtains",
|
||||||
"deck",
|
"deck",
|
||||||
@@ -300,7 +301,7 @@ async function cleanFontDirs() {
|
|||||||
await writeFile(
|
await writeFile(
|
||||||
join(FONT_DIR, ".auto-generated"),
|
join(FONT_DIR, ".auto-generated"),
|
||||||
`Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.\nhash=${hash}\ncreated=${new Date().toISOString()}\n`,
|
`Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.\nhash=${hash}\ncreated=${new Date().toISOString()}\n`,
|
||||||
{ encoding: "utf-8" },
|
{ encoding: "utf-8" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,11 +316,11 @@ async function updateFontCSS() {
|
|||||||
file,
|
file,
|
||||||
css.replace(
|
css.replace(
|
||||||
/url\(\/_static\/shared\/fonts\/material-symbols\/rounded[^)]+\)/,
|
/url\(\/_static\/shared\/fonts\/material-symbols\/rounded[^)]+\)/,
|
||||||
`url(/_static/shared/fonts/material-symbols/rounded-${hash}.woff2)`,
|
`url(/_static/shared/fonts/material-symbols/rounded-${hash}.woff2)`
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +340,7 @@ async function main() {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
console.error(
|
console.error(
|
||||||
`Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}`,
|
`Unable to find the icon font src URL in CSS response from Google Fonts at ${fontUrl}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.
|
Auto-generated, do not edit. Use scripts/material-symbols-update.mts to update.
|
||||||
hash=1b8067b7
|
hash=3e9207ba
|
||||||
created=2025-11-11T10:02:22.385Z
|
created=2025-11-25T08:52:11.965Z
|
||||||
|
|||||||
Binary file not shown.
BIN
shared/fonts/material-symbols/rounded-3e9207ba.woff2
Normal file
BIN
shared/fonts/material-symbols/rounded-3e9207ba.woff2
Normal file
Binary file not shown.
Reference in New Issue
Block a user