feat(BOOK-469): Enter details design changes with guarantee/non-guarantee flow

Approved-by: Bianca Widstam
This commit is contained in:
Erik Tiekstra
2025-11-24 07:24:52 +00:00
parent ea30e59ab7
commit 02aac9006e
18 changed files with 646 additions and 569 deletions

View File

@@ -1,102 +1,46 @@
@keyframes overlay-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes modal-anim {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.guarantee {
display: flex;
flex-direction: column;
gap: var(--Space-x1);
}
.checkboxContainer {
align-items: center;
display: flex;
flex: 1;
gap: var(--Space-x1);
justify-content: space-between;
}
.infoButton {
display: flex;
gap: var(--Space-x05);
justify-self: flex-end;
outline: none;
}
.infoButton:focus-visible {
outline: 2px auto -webkit-focus-ring-color;
outline-offset: 1px;
}
.overlay {
align-items: center;
background-color: var(--Overlay-40);
display: flex;
inset: 0;
justify-content: center;
position: fixed;
z-index: var(--default-modal-overlay-z-index);
&[data-entering] {
animation: overlay-fade 200ms;
}
&[data-exiting] {
animation: overlay-fade 150ms reverse ease-in;
}
}
.modal {
background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-lg);
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
overflow: hidden;
padding: var(--Space-x5) var(--Space-x3);
width: min(90dvw, 560px);
&[data-entering] {
animation: modal-anim 200ms;
}
&[data-exiting] {
animation: modal-anim 150ms reverse ease-in;
}
}
.container {
align-items: center;
display: flex;
flex-direction: column;
display: grid;
gap: var(--Space-x2);
background-color: var(--Surface-Secondary-Default);
border-radius: var(--Corner-radius-lg);
padding: var(--Space-x2);
}
.text {
text-align: center;
.paymentRequired {
display: flex;
gap: var(--Space-x15);
align-items: flex-start;
}
.closeButton {
margin-top: var(--Space-x15);
outline: none;
width: min(164px, 100%);
.guaranteeQuestion {
display: grid;
gap: var(--Space-x1);
justify-items: start;
}
@media screen and (max-width: 767px) {
.btnText {
display: none;
.textWrapper {
display: grid;
gap: var(--Space-x025);
flex-grow: 1;
}
.checkbox {
flex-grow: 1;
}
.guaranteeInfoButton {
flex-shrink: 0;
margin-left: calc(var(--Space-x3) + var(--Space-x15)); /* Align with checkbox */
}
@media screen and (min-width: 768px) {
.guaranteeQuestion {
display: flex;
gap: var(--Space-x15);
align-items: center;
}
.guaranteeInfoButton {
margin-left: 0;
}
}

View File

@@ -1,21 +1,13 @@
"use client"
import { useLayoutEffect } from "react"
import {
Dialog,
DialogTrigger,
Modal,
ModalOverlay,
} from "react-aria-components"
import { useWatch } from "react-hook-form"
import { useIntl } from "react-intl"
import useSetOverflowVisibleOnRA from "@scandic-hotels/common/hooks/useSetOverflowVisibleOnRA"
import { Button } from "@scandic-hotels/design-system/Button"
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 { Typography } from "@scandic-hotels/design-system/Typography"
import { trackUpdatePaymentMethod } from "@scandic-hotels/tracking/payment"
import { GuaranteeInfo } from "../../Payment/GuaranteeInfo"
import { ConfirmBookingPaymentOptions } from "../PaymentOptions"
import styles from "./guarantee.module.css"
@@ -23,114 +15,89 @@ import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/payment
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
interface GuaranteeProps {
mustBeGuaranteed: boolean
savedCreditCards: CreditCard[] | null
otherPaymentOptions: PaymentMethodEnum[]
}
export default function Guarantee({ savedCreditCards }: GuaranteeProps) {
export function Guarantee({
mustBeGuaranteed,
savedCreditCards,
otherPaymentOptions,
}: GuaranteeProps) {
const intl = useIntl()
const guarantee = useWatch({ name: "guarantee" })
return (
<div className={styles.guarantee}>
<Checkbox name="guarantee">
<div className={styles.checkboxContainer}>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{intl.formatMessage({
id: "enterDetails.confirmBooking.guaranteeLabel",
defaultMessage:
"I may arrive later than 18:00 and want to guarantee my booking.",
})}
</span>
</Typography>
<DialogTrigger>
<Button
className={styles.infoButton}
size="Small"
typography="Body/Supporting text (caption)/smBold"
variant="Text"
>
<MaterialIcon icon="info" size={20} color="CurrentColor" />
<span className={styles.btnText}>
{intl.formatMessage({
id: "common.howItWorks",
defaultMessage: "How it works",
})}
</span>
</Button>
<ModalOverlay className={styles.overlay} isDismissable>
<Modal className={styles.modal}>
<Dialog>
{({ close }) => (
<div className={styles.container}>
<Typography variant="Title/Subtitle/lg">
<h3>
{intl.formatMessage({
id: "enterDetails.confirmBooking.guaranteeInfoModalTitle",
defaultMessage: "Guarantee for late arrival",
})}
</h3>
</Typography>
<Typography variant="Body/Lead text">
<p className={styles.text}>
{intl.formatMessage({
id: "enterDetails.confirmBooking.guaranteeInfoModalDescription",
defaultMessage:
"When guaranteeing your booking with a credit card, we will hold the booking until 07:00 the day after check-in.",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.text}>
{intl.formatMessage({
id: "enterDetails.confirmBooking.guaranteeInfoModalNoShowInfo",
defaultMessage:
"In case of a no-show, your credit card will be charged for the first night.",
})}
</p>
</Typography>
<Button
className={styles.closeButton}
onPress={close}
size="Small"
typography="Body/Paragraph/mdBold"
variant="Secondary"
>
{intl.formatMessage({
id: "common.close",
defaultMessage: "Close",
})}
</Button>
<RestoreOverflow />
</div>
)}
</Dialog>
</Modal>
</ModalOverlay>
</DialogTrigger>
</div>
</Checkbox>
{!mustBeGuaranteed ? (
<>
<div className={styles.guaranteeQuestion}>
<Checkbox name="guarantee" topAlign className={styles.checkbox}>
<div className={styles.textWrapper}>
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
id: "enterDetails.guarantee.guaranteeLabel",
defaultMessage: "Guarantee for late arrival",
})}
</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
id: "enterDetails.guarantee.guaranteeInfo",
defaultMessage:
"I may arrive later than 18:00 and want the hotel to hold my booking.",
})}
</p>
</Typography>
</div>
</Checkbox>
<GuaranteeInfo buttonClassName={styles.guaranteeInfoButton} />
</div>
{guarantee ? <Divider color="Border/Divider/Default" /> : null}
</>
) : null}
{guarantee && (
<SelectPaymentMethod
paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card,
cardType: card.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
formName={"paymentMethod"}
/>
)}
{mustBeGuaranteed || guarantee ? (
<>
<div className={styles.paymentRequired}>
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
<div className={styles.textWrapper}>
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
id: "enterDetails.guarantee.paymentCardRequiredLabel",
defaultMessage:
"Payment card required to hold your booking",
})}
</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
id: "enterDetails.guarantee.paymentCardRequiredInfo",
defaultMessage:
"Complete the booking and provide your payment card details in the next step.",
})}
</p>
</Typography>
</div>
</div>
{savedCreditCards && savedCreditCards.length ? (
<>
<Divider color="Border/Divider/Default" />
<ConfirmBookingPaymentOptions
savedCreditCards={savedCreditCards}
hasMixedRates={false}
otherPaymentOptions={otherPaymentOptions}
/>
</>
) : null}
</>
) : null}
</div>
)
}
function RestoreOverflow() {
const setOverflowVisible = useSetOverflowVisibleOnRA()
useLayoutEffect(() => {
setOverflowVisible(true)
}, [setOverflowVisible])
return null
}

View File

@@ -0,0 +1,112 @@
"use client"
import { Label } from "react-aria-components"
import { useIntl } from "react-intl"
import {
PAYMENT_METHOD_TITLES,
PaymentMethodEnum,
} from "@scandic-hotels/common/constants/paymentMethod"
import { PaymentOption } from "@scandic-hotels/design-system/Form/PaymentOption"
import { PaymentOptionsGroup } from "@scandic-hotels/design-system/Form/PaymentOptionsGroup"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useAvailablePaymentOptions } from "../../../../hooks/useAvailablePaymentOptions"
import { useEnterDetailsStore } from "../../../../stores/enter-details"
import MixedRatePaymentBreakdown from "../../Payment/MixedRatePaymentBreakdown"
import styles from "./paymentOptions.module.css"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
interface ConfirmBookingPaymentOptionsProps {
savedCreditCards: CreditCard[] | null
hasMixedRates: boolean
otherPaymentOptions: PaymentMethodEnum[]
}
export function ConfirmBookingPaymentOptions({
savedCreditCards,
hasMixedRates,
otherPaymentOptions,
}: ConfirmBookingPaymentOptionsProps) {
const intl = useIntl()
const { rooms, currency } = useEnterDetailsStore((state) => ({
rooms: state.rooms,
currency: state.totalPrice.local.currency,
}))
const availablePaymentOptions =
useAvailablePaymentOptions(otherPaymentOptions)
return (
<>
<PaymentOptionsGroup
name="paymentMethod"
className={styles.paymentOptions}
>
<Label className="sr-only">
{intl.formatMessage({
id: "enterDetails.payment.paymentMethods",
defaultMessage: "Payment methods",
})}
</Label>
{savedCreditCards?.length ? (
<>
<Typography variant="Title/Overline/sm">
<span>
{intl.formatMessage({
id: "payment.mySavedCards",
defaultMessage: "My saved cards",
})}
</span>
</Typography>
{savedCreditCards.map((savedCreditCard) => (
<PaymentOption
key={savedCreditCard.id}
value={savedCreditCard.id as PaymentMethodEnum}
label={
PAYMENT_METHOD_TITLES[
savedCreditCard.cardType as PaymentMethodEnum
]
}
cardNumber={savedCreditCard.truncatedNumber}
/>
))}
<Typography variant="Title/Overline/sm">
<span>
{intl.formatMessage({
id: "enterDetails.payment.otherPaymentMethods",
defaultMessage: "Other payment methods",
})}
</span>
</Typography>
</>
) : null}
<PaymentOption
value={PaymentMethodEnum.card}
label={intl.formatMessage({
id: "common.creditCard",
defaultMessage: "Credit card",
})}
/>
{!hasMixedRates
? availablePaymentOptions.map((paymentMethod) => (
<PaymentOption
key={paymentMethod}
value={paymentMethod}
label={PAYMENT_METHOD_TITLES[paymentMethod]}
/>
))
: null}
</PaymentOptionsGroup>
{hasMixedRates ? (
<MixedRatePaymentBreakdown rooms={rooms} currency={currency} />
) : null}
</>
)
}

View File

@@ -0,0 +1,4 @@
.paymentOptions {
display: grid;
gap: var(--Space-x15);
}

View File

@@ -0,0 +1,21 @@
import { useIntl } from "react-intl"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { Typography } from "@scandic-hotels/design-system/Typography"
export default function SmsConfirmation() {
const intl = useIntl()
return (
<Checkbox name="smsConfirmation">
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{intl.formatMessage({
id: "booking.smsConfirmationLabel",
defaultMessage:
"I would like to get my booking confirmation via sms",
})}
</span>
</Typography>
</Checkbox>
)
}

View File

@@ -1,12 +1,11 @@
.container {
display: flex;
flex-direction: column;
.confirmBooking {
display: grid;
gap: var(--Space-x3);
}
.selections {
background-color: var(--Surface-Secondary-Default);
border-radius: var(--Corner-radius-Large);
border-radius: var(--Corner-radius-lg);
display: flex;
flex-direction: column;
gap: var(--Space-x2);

View File

@@ -2,67 +2,41 @@
import { useIntl } from "react-intl"
import { Divider } from "@scandic-hotels/design-system/Divider"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { Typography } from "@scandic-hotels/design-system/Typography"
import TermsAndConditions from "../Payment/TermsAndConditions"
import Guarantee from "./Guarantee"
import { Guarantee } from "./Guarantee"
import { ConfirmBookingPaymentOptions } from "./PaymentOptions"
import SmsConfirmation from "./SmsConfirmation"
import styles from "./confirm.module.css"
import type { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
interface ConfirmBookingProps {
savedCreditCards: CreditCard[] | null
otherPaymentOptions: PaymentMethodEnum[]
hasOnlyFlexRates: boolean
hasMixedRates: boolean
isRedemptionBooking: boolean
bookingMustBeGuaranteed: boolean
}
export default function ConfirmBooking({
savedCreditCards,
otherPaymentOptions,
hasOnlyFlexRates,
hasMixedRates,
isRedemptionBooking,
bookingMustBeGuaranteed,
}: ConfirmBookingProps) {
const intl = useIntl()
return (
<div className={styles.container}>
<div className={styles.selections}>
<Guarantee savedCreditCards={savedCreditCards} />
<Divider color="Border/Divider/Default" />
<Checkbox name="smsConfirmation">
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{intl.formatMessage({
id: "booking.smsConfirmationLabel",
defaultMessage:
"I would like to get my booking confirmation via sms",
})}
</span>
</Typography>
</Checkbox>
</div>
<div className={styles.checkboxContainer}>
<TermsAndConditions isFlexBookingTerms />
</div>
</div>
)
}
export function ConfirmBookingRedemption() {
const intl = useIntl()
return (
<div className={styles.container}>
<div className={styles.selections}>
<Checkbox name="smsConfirmation">
<Typography variant="Body/Supporting text (caption)/smRegular">
<span>
{intl.formatMessage({
id: "booking.smsConfirmationLabel",
defaultMessage:
"I would like to get my booking confirmation via sms",
})}
</span>
</Typography>
</Checkbox>
</div>
<div className={styles.guaranteeContainer}>
<Typography variant="Body/Supporting text (caption)/smRegular">
if (isRedemptionBooking) {
return (
<div className={styles.confirmBooking}>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage({
id: "enterDetails.confirmBooking.redemptionGuaranteeInfo",
@@ -71,10 +45,49 @@ export function ConfirmBookingRedemption() {
})}
</p>
</Typography>
</div>
<div className={styles.checkboxContainer}>
<TermsAndConditions isFlexBookingTerms />
<SmsConfirmation />
</div>
)
}
if (hasOnlyFlexRates) {
return (
<div className={styles.confirmBooking}>
<Guarantee
mustBeGuaranteed={bookingMustBeGuaranteed}
savedCreditCards={savedCreditCards}
otherPaymentOptions={otherPaymentOptions}
/>
<Divider color="Border/Divider/Subtle" />
<TermsAndConditions isFlexBookingTerms />
<SmsConfirmation />
</div>
)
}
return (
<div className={styles.confirmBooking}>
{hasMixedRates ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage({
id: "enterDetails.payment.mixedRatesInfo",
defaultMessage:
"As your booking includes rooms with different terms, we will be charging part of the booking now and the remainder will be collected by the reception at check-in.",
})}
</p>
</Typography>
) : null}
<ConfirmBookingPaymentOptions
savedCreditCards={savedCreditCards}
hasMixedRates={hasMixedRates}
otherPaymentOptions={otherPaymentOptions}
/>
<TermsAndConditions isFlexBookingTerms={hasOnlyFlexRates} />
<SmsConfirmation />
</div>
)
}