Merged in feat/SW-3477-hide-voucher-booking-code-sas- (pull request #2836)

feat(SW-3477) Updated booking widget for SAS white label

Approved-by: Anton Gunnarsson
This commit is contained in:
Hrishikesh Vaipurkar
2025-09-23 08:44:55 +00:00
parent 046d342b6f
commit 16e6c1596c
21 changed files with 173 additions and 79 deletions

View File

@@ -19,6 +19,10 @@
color: var(--Text-Secondary);
}
.colorSecondary {
color: var(--Text-Secondary);
}
.errorContainer {
display: flex;
flex-direction: column;
@@ -46,6 +50,7 @@
.bookingCodeTooltip {
max-width: 560px;
margin-top: var(--Spacing-x2);
color: var(--Text-Secondary);
}
.bookingCodeRememberVisible label {

View File

@@ -4,15 +4,11 @@ import { type FieldError, useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import Body from "@scandic-hotels/design-system/Body"
import Caption from "@scandic-hotels/design-system/Caption"
import { Button } from "@scandic-hotels/design-system/Button"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal"
import {
type ButtonProps,
OldDSButton as Button,
} from "@scandic-hotels/design-system/OldDSButton"
import Switch from "@scandic-hotels/design-system/Switch"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
@@ -20,6 +16,7 @@ import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import BookingFlowInput from "../../../../BookingFlowInput"
import { getErrorMessage } from "../../../../BookingFlowInput/errors"
import { Input as BookingWidgetInput } from "../Input"
import { RemoveExtraRooms } from "../RemoveExtraRooms/RemoveExtraRooms"
import { isMultiRoomError } from "../utils"
import styles from "./booking-code.module.css"
@@ -192,13 +189,15 @@ export default function BookingCode() {
}
>
<Switch name="bookingCode.remember" className="mobile-switch">
<Caption asChild>
<Typography
variant={"Body/Supporting text (caption)/smRegular"}
>
<span>
{intl.formatMessage({
defaultMessage: "Remember code",
})}
</span>
</Caption>
</Typography>
</Switch>
</div>
)}
@@ -226,19 +225,19 @@ function CodeRulesModal() {
return (
<Modal
trigger={
<Button intent="text">
<IconButton theme={"Black"} wrapping>
<MaterialIcon
icon="info"
color="Icon/Interactive/Placeholder"
size={20}
/>
</Button>
</IconButton>
}
title={codeVoucher}
>
<Body color="uiTextHighContrast" className={styles.bookingCodeTooltip}>
{bookingCodeTooltipText}
</Body>
<Typography variant={"Body/Paragraph/mdRegular"}>
<p className={styles.bookingCodeTooltip}>{bookingCodeTooltipText}</p>
</Typography>
</Modal>
)
}
@@ -249,20 +248,19 @@ function CodeRemember({ bookingCodeValue, onApplyClick }: CodeRememberProps) {
return (
<>
<Checkbox name="bookingCode.remember">
<Caption asChild>
<Typography variant={"Body/Supporting text (caption)/smRegular"}>
<span>
{intl.formatMessage({
defaultMessage: "Remember code",
})}
</span>
</Caption>
</Typography>
</Checkbox>
{bookingCodeValue ? (
<Button
size="small"
size="Small"
className={styles.hideOnMobile}
intent="tertiary"
theme="base"
variant="Tertiary"
type="button"
onClick={onApplyClick}
>
@@ -303,41 +301,13 @@ function BookingCodeError({
</Typography>
{isMultiroomError ? (
<div className={styles.removeButton}>
<RemoveExtraRooms fullWidth />
<RemoveExtraRooms />
</div>
) : null}
</div>
)
}
export function RemoveExtraRooms({ ...props }: ButtonProps) {
const intl = useIntl()
const { getValues, setValue, trigger } = useFormContext<BookingWidgetSchema>()
function removeExtraRooms() {
// Timeout to delay the event scheduling issue with touch events on mobile
window.setTimeout(() => {
const rooms = getValues("rooms")[0]
setValue("rooms", [rooms], { shouldValidate: true, shouldDirty: true })
trigger("bookingCode.value")
trigger(SEARCH_TYPE_REDEMPTION)
}, 300)
}
return (
<Button
type="button"
onClick={removeExtraRooms}
size="small"
intent="secondary"
{...props}
>
{intl.formatMessage({
defaultMessage: "Remove extra rooms",
})}
</Button>
)
}
function TabletBookingCode({
bookingCode,
updateValue,
@@ -381,7 +351,7 @@ function TabletBookingCode({
return (
<div className={styles.container}>
<DialogTrigger isOpen={isOpen} onOpenChange={toggleModal}>
<Button type="button" intent="text">
<Button type="button" variant={"Text"}>
{/* For some reason Checkbox click triggers twice modal state change, which returns the modal back to old state. So we are using overlay as trigger for modal */}
<div className={styles.overlayTrigger}></div>
<Checkbox
@@ -394,9 +364,9 @@ function TabletBookingCode({
},
})}
>
<Caption color="uiTextMediumContrast" type="bold" asChild>
<span>{codeVoucher}</span>
</Caption>
<Typography variant={"Body/Supporting text (caption)/smBold"}>
<span className={styles.colorSecondary}>{codeVoucher}</span>
</Typography>
</Checkbox>
</Button>
<Popover

View File

@@ -0,0 +1,36 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { Button, type ButtonProps } from "@scandic-hotels/design-system/Button"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import type { BookingWidgetSchema } from "../../../Client"
export function RemoveExtraRooms({ ...props }: ButtonProps) {
const intl = useIntl()
const { getValues, setValue, trigger } = useFormContext<BookingWidgetSchema>()
function removeExtraRooms() {
// Timeout to delay the event scheduling issue with touch events on mobile
window.setTimeout(() => {
const rooms = getValues("rooms")[0]
setValue("rooms", [rooms], { shouldValidate: true, shouldDirty: true })
trigger("bookingCode.value")
trigger(SEARCH_TYPE_REDEMPTION)
}, 300)
}
return (
<Button
style={{ width: "100%" }}
type="button"
onClick={removeExtraRooms}
size="Small"
variant={"Secondary"}
{...props}
>
{intl.formatMessage({
defaultMessage: "Remove extra rooms",
})}
</Button>
)
}

View File

@@ -5,16 +5,20 @@ import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import Caption from "@scandic-hotels/design-system/Caption"
import { ScandicPartnersEnum } from "@scandic-hotels/common/constants/scandicPartners"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Modal from "@scandic-hotels/design-system/Modal"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import {
useBookingFlowConfig,
useIsPartner,
} from "../../../../../bookingFlowConfig/bookingFlowConfigContext"
import { getErrorMessage } from "../../../../BookingFlowInput/errors"
import { RemoveExtraRooms } from "../BookingCode"
import { RemoveExtraRooms } from "../RemoveExtraRooms/RemoveExtraRooms"
import { isMultiRoomError } from "../utils"
import styles from "./reward-night.module.css"
@@ -23,6 +27,8 @@ import type { BookingWidgetSchema } from "../../../Client"
export default function RewardNight() {
const intl = useIntl()
const config = useBookingFlowConfig()
const isPartnerSas = useIsPartner(ScandicPartnersEnum.sas)
const {
setValue,
getValues,
@@ -30,13 +36,22 @@ export default function RewardNight() {
trigger,
} = useFormContext<BookingWidgetSchema>()
const ref = useRef<HTMLDivElement | null>(null)
const reward = intl.formatMessage({
defaultMessage: "Reward Night",
})
const rewardNightTooltip = intl.formatMessage({
defaultMessage:
"To book a reward night, make sure you're logged in to your Scandic Friends account.",
})
const reward = isPartnerSas
? intl.formatMessage({
defaultMessage: "EuroBonus Points",
})
: intl.formatMessage({
defaultMessage: "Reward Night",
})
const rewardNightTooltip = isPartnerSas
? intl.formatMessage({
defaultMessage:
"To book with EuroBonus points, make sure you're logged into your SAS EuroBonus account.",
})
: intl.formatMessage({
defaultMessage:
"To book a reward night, make sure you're logged in to your Scandic Friends account.",
})
const redemptionErr = errors[SEARCH_TYPE_REDEMPTION]
const isDesktop = useMediaQuery("(min-width: 767px)")
@@ -92,19 +107,22 @@ export default function RewardNight() {
}}
>
<div className={styles.rewardNightLabel}>
<Caption color="uiTextMediumContrast" asChild>
<Typography
variant={"Body/Supporting text (caption)/smRegular"}
className={styles.label}
>
<span>{reward}</span>
</Caption>
</Typography>
<Modal
trigger={
<Button intent="text">
<IconButton theme={"Black"} wrapping>
<MaterialIcon
icon="info"
size={20}
color="Icon/Interactive/Placeholder"
className={styles.errorIcon}
/>
</Button>
</IconButton>
}
title={reward}
>
@@ -131,12 +149,12 @@ export default function RewardNight() {
className={styles.errorIcon}
isFilled={!isDesktop}
/>
{getErrorMessage(intl, redemptionErr.message)}
{getErrorMessage(intl, redemptionErr.message, config.partner)}
</span>
</Typography>
{isMultiRoomError(redemptionErr.message) ? (
<div className={styles.hideOnMobile}>
<RemoveExtraRooms fullWidth />
<RemoveExtraRooms />
</div>
) : null}
</div>

View File

@@ -17,7 +17,9 @@
.rewardNightLabel {
align-items: center;
display: flex;
gap: var(--Spacing-x1);
color: var(--Text-Secondary);
min-width: 160px;
gap: var(--Space-x1);
}
.rewardNightTooltip {

View File

@@ -185,4 +185,8 @@
.input {
gap: var(--Spacing-x2);
}
.bookingCodeDisabled {
flex: none;
}
}

View File

@@ -13,10 +13,11 @@ import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { useBookingFlowConfig } from "../../../../bookingFlowConfig/bookingFlowConfigContext"
import useLang from "../../../../hooks/useLang"
import GuestsRoomsPickerForm from "../../../BookingWidget/GuestsRoomsPicker"
import DatePicker from "../../DatePicker"
import { RemoveExtraRooms } from "./BookingCode"
import { RemoveExtraRooms } from "./RemoveExtraRooms/RemoveExtraRooms"
import { Search, SearchSkeleton } from "./Search"
import { isMultiRoomError } from "./utils"
import ValidationError from "./ValidationError"
@@ -40,6 +41,7 @@ export default function FormContent({
const {
formState: { errors, isDirty },
} = useFormContext<BookingWidgetSchema>()
const { bookingCodeEnabled } = useBookingFlowConfig()
const lang = useLang()
const pathName = usePathname()
@@ -109,14 +111,20 @@ export default function FormContent({
</span>
</Button>
</div>
<div className={cx(styles.voucherContainer, styles.voucherRow)}>
<div
className={cx(
styles.voucherContainer,
styles.voucherRow,
bookingCodeEnabled ? null : styles.bookingCodeDisabled
)}
>
<Voucher />
</div>
<div className={cx(styles.buttonContainer, styles.hideOnTablet)}>
{isMultiRoomError(errors.bookingCode?.value?.message) ||
isMultiRoomError(errors[SEARCH_TYPE_REDEMPTION]?.message) ? (
<div className={styles.showOnMobile}>
<RemoveExtraRooms size="medium" fullWidth />
<RemoveExtraRooms />
</div>
) : null}
<Button