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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -13,13 +13,18 @@
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.termsAndConditions {
|
||||
color: var(--Text-Secondary);
|
||||
display: grid;
|
||||
.guarantee {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
background-color: var(--Surface-Secondary-Default);
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
padding: var(--Space-x2);
|
||||
}
|
||||
|
||||
.termsAndConditions .checkbox span {
|
||||
.paymentInfo {
|
||||
display: flex;
|
||||
gap: var(--Space-x1);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,13 @@ import { useIntl } from "react-intl"
|
||||
|
||||
import { writeGlaToSessionStorage } from "@scandic-hotels/booking-flow/components/EnterDetails/Payment/PaymentCallback/helpers"
|
||||
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
||||
import { bookingTermsAndConditionsRoutes } from "@scandic-hotels/common/constants/routes/bookingTermsAndConditionsRoutes"
|
||||
import { guaranteeCallback } from "@scandic-hotels/common/constants/routes/hotelReservation"
|
||||
import { privacyPolicyRoutes } from "@scandic-hotels/common/constants/routes/privacyPolicyRoutes"
|
||||
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
|
||||
import { Alert } from "@scandic-hotels/design-system/Alert"
|
||||
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||
import Checkbox from "@scandic-hotels/design-system/Form/Checkbox"
|
||||
import { SelectPaymentMethod } from "@scandic-hotels/design-system/Form/SelectPaymentMethod"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
|
||||
import Link from "@scandic-hotels/design-system/OldDSLink"
|
||||
import { toast } from "@scandic-hotels/design-system/Toast"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
import { trackGlaSaveCardAttempt } from "@scandic-hotels/tracking/payment"
|
||||
@@ -23,6 +21,7 @@ import { isWebview } from "@/constants/routes/webviews"
|
||||
import { env } from "@/env/client"
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import TermsAndConditions from "@/components/HotelReservation/MyStay/TermsAndConditions"
|
||||
import { useGuaranteeBooking } from "@/hooks/booking/useGuaranteeBooking"
|
||||
import useLang from "@/hooks/useLang"
|
||||
import { trackUpdatePaymentMethod } from "@/utils/tracking"
|
||||
@@ -31,7 +30,12 @@ import { type GuaranteeFormData, paymentSchema } from "./schema"
|
||||
|
||||
import styles from "./form.module.css"
|
||||
|
||||
export default function Form() {
|
||||
import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
|
||||
interface FormProps {
|
||||
error?: { type: AlertTypeEnum; message: string }
|
||||
}
|
||||
export default function Form({ error }: FormProps) {
|
||||
const intl = useIntl()
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
@@ -114,38 +118,6 @@ export default function Form() {
|
||||
}
|
||||
}
|
||||
|
||||
const guaranteeMsg = intl.formatMessage(
|
||||
{
|
||||
id: "myStay.gla.termsAndConditionsMessage",
|
||||
defaultMessage:
|
||||
"I accept the terms for this stay and the general <termsAndConditionsLink>Booking & Cancellation Terms</termsAndConditionsLink>, and understand Scandic will process my personal data for this stay in accordance with <privacyPolicyLink>Scandic's Privacy Policy</privacyPolicyLink>.",
|
||||
},
|
||||
{
|
||||
termsAndConditionsLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
color="Text/Interactive/Secondary"
|
||||
target="_blank"
|
||||
href={bookingTermsAndConditionsRoutes[lang]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
privacyPolicyLink: (str) => (
|
||||
<Link
|
||||
textDecoration="underline"
|
||||
color="Text/Interactive/Secondary"
|
||||
target="_blank"
|
||||
href={privacyPolicyRoutes[lang]}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{str}
|
||||
</Link>
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
@@ -153,24 +125,56 @@ export default function Form() {
|
||||
id="guarantee"
|
||||
onSubmit={methods.handleSubmit(handleGuaranteeLateArrival)}
|
||||
>
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName="paymentMethod"
|
||||
/>
|
||||
|
||||
<div className={styles.termsAndConditions}>
|
||||
<Checkbox className={styles.checkbox} name="termsAndConditions">
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>{guaranteeMsg}</p>
|
||||
{error && <Alert type={error.type} text={error.message} />}
|
||||
<div className={styles.guarantee}>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon icon="credit_card" size={24} color="CurrentColor" />
|
||||
<span>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guarantee.headingText",
|
||||
defaultMessage: "Planning to arrive after 18.00?",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guarantee.infoText",
|
||||
defaultMessage:
|
||||
"Guarantee with a credit card is required to secure your booking. Without this guarantee, your room may be released after 18:00 in case of no-show.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.paymentInfo}>
|
||||
<MaterialIcon icon="credit_score" size={24} color="CurrentColor" />
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
id: "myStay.guarantee.headingText",
|
||||
defaultMessage:
|
||||
"Confirm and provide your payment card details in the next step",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</Checkbox>
|
||||
</div>
|
||||
{savedCreditCards && <Divider />}
|
||||
<SelectPaymentMethod
|
||||
paymentMethods={(savedCreditCards ?? []).map((card) => ({
|
||||
...card,
|
||||
cardType: card.cardType as PaymentMethodEnum,
|
||||
}))}
|
||||
onChange={(method) => {
|
||||
trackUpdatePaymentMethod({ method })
|
||||
}}
|
||||
formName="paymentMethod"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TermsAndConditions />
|
||||
<div className={styles.guaranteeCost}>
|
||||
<div className={styles.guaranteeCostText}>
|
||||
<Typography variant="Body/Supporting text (caption)/smBold">
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const paymentError = {
|
||||
TERMS_REQUIRED: "TERMS_REQUIRED",
|
||||
} as const
|
||||
|
||||
export const paymentSchema = z.object({
|
||||
paymentMethod: z.string().nullable(),
|
||||
termsAndConditions: z.boolean().refine((value) => value === true, {
|
||||
message: "You must accept the terms and conditions",
|
||||
}),
|
||||
termsAndConditions: z
|
||||
.boolean()
|
||||
.refine((value) => value === true, paymentError.TERMS_REQUIRED),
|
||||
})
|
||||
|
||||
export type GuaranteeFormData = z.output<typeof paymentSchema>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.dialog {
|
||||
max-width: 690px;
|
||||
outline: none;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Dialog } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
|
||||
import Form from "../Form"
|
||||
|
||||
import styles from "./guaranteeDialog.module.css"
|
||||
|
||||
import type { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
|
||||
|
||||
interface GuaranteeDialogProps {
|
||||
error?: { type: AlertTypeEnum; message: string }
|
||||
}
|
||||
|
||||
export default function GuaranteeDialog({ error }: GuaranteeDialogProps) {
|
||||
const intl = useIntl()
|
||||
const text = intl.formatMessage({
|
||||
id: "myStay.gla.heading",
|
||||
defaultMessage: "Add late arrival guarantee",
|
||||
})
|
||||
return (
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={close} title={text} />
|
||||
<Modal.Content.Body>
|
||||
<Form error={error} />
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={close}>
|
||||
{intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
<Modal.Content.Footer.Primary form="guarantee" type="submit">
|
||||
{intl.formatMessage({
|
||||
id: "common.confirm",
|
||||
defaultMessage: "Confirm",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)}
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
"use client"
|
||||
import { Dialog, DialogTrigger } from "react-aria-components"
|
||||
import { DialogTrigger } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { useMyStayStore } from "@/stores/my-stay"
|
||||
|
||||
import Modal from "@/components/HotelReservation/MyStay/Modal"
|
||||
import { trackMyStayPageLink } from "@/utils/tracking"
|
||||
|
||||
import ActionsButton from "../ActionsButton"
|
||||
import Form from "./Form"
|
||||
|
||||
import styles from "./guarantee.module.css"
|
||||
import GuaranteeDialog from "./GuaranteeDialog"
|
||||
|
||||
export default function GuaranteeLateArrival() {
|
||||
const intl = useIntl()
|
||||
@@ -29,14 +25,9 @@ export default function GuaranteeLateArrival() {
|
||||
trackMyStayPageLink("guarantee late arrival")
|
||||
}
|
||||
|
||||
const arriveLateMsg = intl.formatMessage({
|
||||
id: "myStay.gla.arriveLateMessage",
|
||||
defaultMessage:
|
||||
"Planning to arrive after 18.00? Secure your room by guaranteeing it with a credit card. Without the guarantee and in case of no-show, the room might be reallocated after 18:00.",
|
||||
})
|
||||
const text = intl.formatMessage({
|
||||
id: "myStay.gla.heading",
|
||||
defaultMessage: "Guarantee late arrival",
|
||||
defaultMessage: "Add late arrival guarantee",
|
||||
})
|
||||
|
||||
return (
|
||||
@@ -47,34 +38,7 @@ export default function GuaranteeLateArrival() {
|
||||
icon="check"
|
||||
/>
|
||||
<Modal>
|
||||
<Dialog className={styles.dialog}>
|
||||
{({ close }) => (
|
||||
<Modal.Content>
|
||||
<Modal.Content.Header handleClose={close} title={text}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>{arriveLateMsg}</p>
|
||||
</Typography>
|
||||
</Modal.Content.Header>
|
||||
<Modal.Content.Body>
|
||||
<Form />
|
||||
</Modal.Content.Body>
|
||||
<Modal.Content.Footer>
|
||||
<Modal.Content.Footer.Secondary onClick={close}>
|
||||
{intl.formatMessage({
|
||||
id: "common.back",
|
||||
defaultMessage: "Back",
|
||||
})}
|
||||
</Modal.Content.Footer.Secondary>
|
||||
<Modal.Content.Footer.Primary form="guarantee" type="submit">
|
||||
{intl.formatMessage({
|
||||
id: "myStay.gla.guarantee",
|
||||
defaultMessage: "Guarantee",
|
||||
})}
|
||||
</Modal.Content.Footer.Primary>
|
||||
</Modal.Content.Footer>
|
||||
</Modal.Content>
|
||||
)}
|
||||
</Dialog>
|
||||
<GuaranteeDialog />
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user