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:
Bianca Widstam
2025-11-28 14:27:25 +00:00
parent 22dd2f60fe
commit 46fa42750f
39 changed files with 681 additions and 485 deletions

View File

@@ -0,0 +1,77 @@
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { useIntl } from "react-intl"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { isAncillaryError } from "@/components/HotelReservation/MyStay/utils"
import GuaranteeDialog from "../ManageStay/Actions/GuaranteeLateArrival/GuaranteeDialog"
export default function GuaranteePaymentFailed() {
const intl = useIntl()
const searchParams = useSearchParams()
const pathname = usePathname()
const router = useRouter()
const [alert, setAlert] = useState<{
type: AlertTypeEnum
message: string
} | null>(null)
const getErrorMessage = useCallback(
(errorCode: string | null) => {
switch (errorCode) {
case BookingErrorCodeEnum.TransactionCancelled:
return intl.formatMessage({
id: "guaranteePayment.cancelled",
defaultMessage:
"You have cancelled the payment. Your booking is not guaranteed.",
})
default:
return intl.formatMessage({
id: "guaranteePayment.failed",
defaultMessage:
"We had an issue guaranteeing your booking. Please try again.",
})
}
},
[intl]
)
useEffect(() => {
const errorCode = searchParams.get("errorCode")
if (!errorCode) {
return
}
if (isAncillaryError(searchParams)) {
return
}
const message = getErrorMessage(errorCode)
const type =
errorCode === BookingErrorCodeEnum.TransactionCancelled
? AlertTypeEnum.Warning
: AlertTypeEnum.Alarm
setAlert({ type, message })
const newParams = new URLSearchParams(searchParams.toString())
newParams.delete("errorCode")
router.replace(`${pathname}?${newParams.toString()}`)
}, [searchParams, pathname, router, getErrorMessage])
if (!alert) {
return null
}
return (
<Modal isOpen={!!alert} onOpenChange={() => setAlert(null)}>
<GuaranteeDialog error={alert} />
</Modal>
)
}

View File

@@ -13,13 +13,18 @@
gap: var(--Space-x3);
}
.termsAndConditions {
color: var(--Text-Secondary);
display: grid;
.guarantee {
display: flex;
flex-direction: column;
gap: var(--Space-x2);
background-color: var(--Surface-Secondary-Default);
border-radius: var(--Corner-radius-lg);
padding: var(--Space-x2);
}
.termsAndConditions .checkbox span {
.paymentInfo {
display: flex;
gap: var(--Space-x1);
align-items: flex-start;
}

View File

@@ -6,15 +6,13 @@ import { useIntl } from "react-intl"
import { writeGlaToSessionStorage } from "@scandic-hotels/booking-flow/components/EnterDetails/Payment/PaymentCallback/helpers"
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import { Alert } from "@scandic-hotels/design-system/Alert"
import { Divider } from "@scandic-hotels/design-system/Divider"
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import Link from "@scandic-hotels/design-system/OldDSLink"
import { toast } from "@scandic-hotels/design-system/Toast"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
@@ -23,6 +21,7 @@ import { isWebview } from "@/constants/routes/webviews"
import { env } from "@/env/client"
import { useMyStayStore } from "@/stores/my-stay"
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
import useLang from "@/hooks/useLang"
import { trackUpdatePaymentMethod } from "@/utils/tracking"
@@ -31,7 +30,12 @@ import { type GuaranteeFormData, paymentSchema } from "./schema"
import styles from "./form.module.css"
export default function Form() {
import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
interface FormProps {
error?: { type: AlertTypeEnum; message: string }
}
export default function Form({ error }: FormProps) {
const intl = useIntl()
const lang = useLang()
const pathname = usePathname()
@@ -114,38 +118,6 @@ export default function Form() {
}
}
const guaranteeMsg = intl.formatMessage(
{
id: "myStay.gla.termsAndConditionsMessage",
defaultMessage:
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>.",
},
{
termsAndConditionsLink: (str) => (
<Link
textDecoration="underline"
color="Text/Interactive/Secondary"
target="_blank"
href={bookingTermsAndConditionsRoutes[lang]}
onClick={(e) => e.stopPropagation()}
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
textDecoration="underline"
color="Text/Interactive/Secondary"
target="_blank"
href={privacyPolicyRoutes[lang]}
onClick={(e) => e.stopPropagation()}
>
{str}
</Link>
),
}
)
return (
<FormProvider {...methods}>
<form
@@ -153,24 +125,56 @@ export default function Form() {
id="guarantee"
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
>
<SelectPaymentMethod
paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card,
cardType: card.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
formName="paymentMethod"
/>
<div className={styles.termsAndConditions}>
<Checkbox className={styles.checkbox} name="termsAndConditions">
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>{guaranteeMsg}</p>
{error && <Alert type={error.type} text={error.message} />}
<div className={styles.guarantee}>
<div className={styles.paymentInfo}>
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
<span>
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
id: "myStay.guarantee.headingText",
defaultMessage: "Planning to arrive after 18.00?",
})}
</p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
id: "myStay.guarantee.infoText",
defaultMessage:
"Guarantee with a credit card is required to secure your booking. Without this guarantee, your room may be released after 18:00 in case of no-show.",
})}
</p>
</Typography>
</span>
</div>
<div className={styles.paymentInfo}>
<MaterialIcon icon="credit_score" size={24} color="CurrentColor" />
<Typography variant="Body/Supporting text (caption)/smBold">
<p>
{intl.formatMessage({
id: "myStay.guarantee.headingText",
defaultMessage:
"Confirm and provide your payment card details in the next step",
})}
</p>
</Typography>
</Checkbox>
</div>
{savedCreditCards && <Divider />}
<SelectPaymentMethod
paymentMethods={(savedCreditCards ?? []).map((card) => ({
...card,
cardType: card.cardType as PaymentMethodEnum,
}))}
onChange={(method) => {
trackUpdatePaymentMethod({ method })
}}
formName="paymentMethod"
/>
</div>
<TermsAndConditions />
<div className={styles.guaranteeCost}>
<div className={styles.guaranteeCostText}>
<Typography variant="Body/Supporting text (caption)/smBold">

View File

@@ -1,10 +1,14 @@
import { z } from "zod"
export const paymentError = {
TERMS_REQUIRED: "TERMS_REQUIRED",
} as const
export const paymentSchema = z.object({
paymentMethod: z.string().nullable(),
termsAndConditions: z.boolean().refine((value) => value === true, {
message: "You must accept the terms and conditions",
}),
termsAndConditions: z
.boolean()
.refine((value) => value === true, paymentError.TERMS_REQUIRED),
})
export type GuaranteeFormData = z.output<typeof paymentSchema>

View File

@@ -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>
)
}

View File

@@ -1,18 +1,14 @@
"use client"
import { Dialog, DialogTrigger } from "react-aria-components"
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useMyStayStore } from "@/stores/my-stay"
import Modal from "@/components/HotelReservation/MyStay/Modal"
import { trackMyStayPageLink } from "@/utils/tracking"
import ActionsButton from "../ActionsButton"
import Form from "./Form"
import styles from "./guarantee.module.css"
import GuaranteeDialog from "./GuaranteeDialog"
export default function GuaranteeLateArrival() {
const intl = useIntl()
@@ -29,14 +25,9 @@ export default function GuaranteeLateArrival() {
trackMyStayPageLink("guarantee late arrival")
}
const arriveLateMsg = intl.formatMessage({
id: "myStay.gla.arriveLateMessage",
defaultMessage:
"Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
})
const text = intl.formatMessage({
id: "myStay.gla.heading",
defaultMessage: "Guarantee late arrival",
defaultMessage: "Add late arrival guarantee",
})
return (
@@ -47,34 +38,7 @@ export default function GuaranteeLateArrival() {
icon="check"
/>
<Modal>
<Dialog className={styles.dialog}>
{({ close }) => (
<Modal.Content>
<Modal.Content.Header handleClose={close} title={text}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{arriveLateMsg}</p>
</Typography>
</Modal.Content.Header>
<Modal.Content.Body>
<Form />
</Modal.Content.Body>
<Modal.Content.Footer>
<Modal.Content.Footer.Secondary onClick={close}>
{intl.formatMessage({
id: "common.back",
defaultMessage: "Back",
})}
</Modal.Content.Footer.Secondary>
<Modal.Content.Footer.Primary form="guarantee" type="submit">
{intl.formatMessage({
id: "myStay.gla.guarantee",
defaultMessage: "Guarantee",
})}
</Modal.Content.Footer.Primary>
</Modal.Content.Footer>
</Modal.Content>
)}
</Dialog>
<GuaranteeDialog />
</Modal>
</DialogTrigger>
)