Merged in feat/SW-1149-handle-status-polling (pull request #1562)

Feat/SW-1149 handle status polling

* feat(SW-1149): move terms and conditions sections to separate component and added copy

* feat(SW-1149): Added client component to handle success callback for payment flow

* fix: check for bookingCompleted status as well

* feat(SW-1587): use alert instead of toast for showing payment errors

* fix: added enum for payment callback status

* fix: proper way of checking for multiple statuses

* fix: update schema type

* fix: use localised link to customer service

* fix: update to use enum for status strings


Approved-by: Arvid Norlin
This commit is contained in:
Tobias Johansson
2025-03-20 07:38:29 +00:00
parent 200ed55a2c
commit ac493fe325
25 changed files with 384 additions and 155 deletions

View File

@@ -1,6 +1,9 @@
import { redirect } from "next/navigation"
import { BookingErrorCodeEnum } from "@/constants/booking"
import {
BookingErrorCodeEnum,
PaymentCallbackStatusEnum,
} from "@/constants/booking"
import { hotelreservation } from "@/constants/routes/hotelReservation"
import { serverClient } from "@/lib/trpc/server"
@@ -14,7 +17,7 @@ export default async function GuaranteePaymentCallbackPage({
}: PageArgs<
LangParams,
{
status: "error" | "success" | "cancel"
status: PaymentCallbackStatusEnum
refId: string
confirmationNumber?: string
}
@@ -26,7 +29,11 @@ export default async function GuaranteePaymentCallbackPage({
const refId = searchParams.refId
const myStayUrl = `${hotelreservation(lang)}/my-stay?RefId=${refId}`
if (status === "success" && confirmationNumber && refId) {
if (
status === PaymentCallbackStatusEnum.Success &&
confirmationNumber &&
refId
) {
console.log(`[gla-payment-callback] redirecting to: ${myStayUrl}`)
redirect(myStayUrl)
}
@@ -58,10 +65,10 @@ export default async function GuaranteePaymentCallbackPage({
console.error(
`[gla-payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
)
if (status === "cancel") {
if (status === PaymentCallbackStatusEnum.Cancel) {
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
}
if (status === "error") {
if (status === PaymentCallbackStatusEnum.Error) {
searchObject.set(
"errorCode",
BookingErrorCodeEnum.TransactionFailed.toString()

View File

@@ -1,9 +1,7 @@
import { redirect } from "next/navigation"
import {
BOOKING_CONFIRMATION_NUMBER,
BookingErrorCodeEnum,
MEMBERSHIP_FAILED_ERROR,
PaymentCallbackStatusEnum,
} from "@/constants/booking"
import {
bookingConfirmation,
@@ -11,7 +9,8 @@ import {
} from "@/constants/routes/hotelReservation"
import { serverClient } from "@/lib/trpc/server"
import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback"
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback"
import type { LangParams, PageArgs } from "@/types/params"
@@ -21,7 +20,7 @@ export default async function PaymentCallbackPage({
}: PageArgs<
LangParams,
{
status: "error" | "success" | "cancel"
status: PaymentCallbackStatusEnum
confirmationNumber?: string
hotel?: string
}
@@ -31,21 +30,18 @@ export default async function PaymentCallbackPage({
const status = searchParams.status
const confirmationNumber = searchParams.confirmationNumber
if (status === "success" && confirmationNumber) {
const bookingStatus = await serverClient().booking.status({
confirmationNumber,
})
const membershipFailedError = bookingStatus.errors.find(
(e) => e.errorCode === MEMBERSHIP_FAILED_ERROR
if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) {
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}`
console.log(
`[payment-callback] rendering success callback with confirmation number: ${confirmationNumber}`
)
const errorParam = membershipFailedError
? `&errorCode=${membershipFailedError.errorCode}`
: ""
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}${errorParam}`
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
redirect(confirmationUrl)
return (
<HandleSuccessCallback
confirmationNumber={confirmationNumber}
successRedirectUrl={confirmationUrl}
/>
)
}
const returnUrl = details(lang)
@@ -76,10 +72,10 @@ export default async function PaymentCallbackPage({
console.error(
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
)
if (status === "cancel") {
if (status === PaymentCallbackStatusEnum.Cancel) {
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionCancelled)
}
if (status === "error") {
if (status === PaymentCallbackStatusEnum.Error) {
searchObject.set("errorCode", BookingErrorCodeEnum.TransactionFailed)
errorMessage = `Failed to get booking status for ${confirmationNumber}, status: ${status}`
}
@@ -87,7 +83,7 @@ export default async function PaymentCallbackPage({
}
return (
<PaymentCallback
<HandleErrorCallback
returnUrl={returnUrl.toString()}
searchObject={searchObject}
status={status}

View File

@@ -29,7 +29,6 @@
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
padding: 0 var(--Spacing-x2);
width: min(800px, 100%);
}

View File

@@ -8,20 +8,15 @@ import { Button } from "@scandic-hotels/design-system/Button"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { PaymentMethodEnum } from "@/constants/booking"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import { InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Divider from "@/components/TempDesignSystem/Divider"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import MySavedCards from "../Payment/MySavedCards"
import PaymentOption from "../Payment/PaymentOption"
import TermsAndConditions from "../Payment/TermsAndConditions"
import styles from "./confirm.module.css"
@@ -35,7 +30,6 @@ export default function ConfirmBooking({
savedCreditCards,
}: ConfirmBookingProps) {
const intl = useIntl()
const lang = useLang()
const [isModalOpen, setModalOpen] = useState(false)
const { watch } = useFormContext()
@@ -130,50 +124,7 @@ export default function ConfirmBooking({
)}
</div>
<div className={styles.checkboxContainer}>
<Checkbox name="smsConfirmation">
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage({
id: "I would like to get my booking confirmation via sms",
})}
</p>
</Typography>
</Checkbox>
<div className={styles.checkbox}>
<Checkbox name="termsAndConditions" topAlign>
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage(
{
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
},
{
termsAndConditionsLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={bookingTermsAndConditions[lang]}
target="_blank"
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={privacyPolicy[lang]}
target="_blank"
>
{str}
</Link>
),
}
)}
</p>
</Typography>
</Checkbox>
</div>
<TermsAndConditions />
</div>
</div>
)

View File

@@ -0,0 +1,84 @@
"use client"
import { usePathname, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import { BookingErrorCodeEnum } from "@/constants/booking"
import { useEnterDetailsStore } from "@/stores/enter-details"
import Alert from "@/components/TempDesignSystem/Alert"
import styles from "./paymentAlert.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
function useBookingErrorAlert() {
const updateSearchParams = useEnterDetailsStore(
(state) => state.actions.updateSeachParamString
)
const intl = useIntl()
const searchParams = useSearchParams()
const pathname = usePathname()
const errorCode = searchParams.get("errorCode")
const errorMessage = getErrorMessage(errorCode)
const severityLevel =
errorCode === BookingErrorCodeEnum.TransactionCancelled
? AlertTypeEnum.Warning
: AlertTypeEnum.Alarm
const [showAlert, setShowAlert] = useState(!!errorCode)
function getErrorMessage(errorCode: string | null) {
switch (errorCode) {
case BookingErrorCodeEnum.TransactionCancelled:
return intl.formatMessage({
id: "You have now cancelled your payment.",
})
default:
return intl.formatMessage({
id: "We had an issue processing your booking. Please try again. No charges have been made.",
})
}
}
function discardAlert() {
setShowAlert(false)
const queryParams = new URLSearchParams(searchParams.toString())
queryParams.delete("errorCode")
updateSearchParams(queryParams.toString())
window.history.replaceState({}, "", `${pathname}?${queryParams.toString()}`)
}
return { showAlert, errorMessage, severityLevel, discardAlert, setShowAlert }
}
interface PaymentAlertProps {
isVisible?: boolean
}
export default function PaymentAlert({ isVisible = false }: PaymentAlertProps) {
const { showAlert, errorMessage, severityLevel, discardAlert, setShowAlert } =
useBookingErrorAlert()
useEffect(() => {
if (isVisible) {
setShowAlert(true)
}
}, [isVisible, setShowAlert])
if (!showAlert) return null
return (
<div className={styles.wrapper}>
<Alert
type={severityLevel}
variant="inline"
text={errorMessage}
close={discardAlert}
/>
</div>
)
}

View File

@@ -0,0 +1,4 @@
.wrapper {
margin-top: var(--Spacing-x3);
max-width: min(100%, 620px);
}

View File

@@ -3,6 +3,7 @@
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { PaymentCallbackStatusEnum } from "@/constants/booking"
import { detailsStorageName } from "@/stores/enter-details"
import LoadingSpinner from "@/components/LoadingSpinner"
@@ -11,7 +12,7 @@ import { convertObjToSearchParams } from "@/utils/url"
import type { PersistedState } from "@/types/stores/enter-details"
export default function PaymentCallback({
export default function HandleErrorCallback({
returnUrl,
searchObject,
status,
@@ -19,7 +20,7 @@ export default function PaymentCallback({
}: {
returnUrl: string
searchObject: URLSearchParams
status: "error" | "success" | "cancel"
status: PaymentCallbackStatusEnum
errorMessage?: string
}) {
const router = useRouter()
@@ -34,14 +35,14 @@ export default function PaymentCallback({
searchObject
)
if (status === "cancel") {
if (status === PaymentCallbackStatusEnum.Cancel) {
trackPaymentEvent({
event: "paymentCancel",
hotelId: detailsStorage.booking.hotelId,
status: "cancelled",
})
}
if (status === "error") {
if (status === PaymentCallbackStatusEnum.Error) {
trackPaymentEvent({
event: "paymentFail",
hotelId: detailsStorage.booking.hotelId,

View File

@@ -0,0 +1,68 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { BookingStatusEnum, MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import LoadingSpinner from "@/components/LoadingSpinner"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import TimeoutSpinner from "./TimeoutSpinner"
const validBookingStatuses = [
BookingStatusEnum.PaymentSucceeded,
BookingStatusEnum.BookingCompleted,
]
interface HandleStatusPollingProps {
confirmationNumber: string
successRedirectUrl: string
}
export default function HandleSuccessCallback({
confirmationNumber,
successRedirectUrl,
}: HandleStatusPollingProps) {
const router = useRouter()
const {
data: bookingStatus,
error,
isTimeout,
} = useHandleBookingStatus({
confirmationNumber,
expectedStatuses: validBookingStatuses,
maxRetries: 10,
retryInterval: 2000,
enabled: true,
})
useEffect(() => {
if (!bookingStatus?.reservationStatus) {
return
}
if (
validBookingStatuses.includes(
bookingStatus.reservationStatus as BookingStatusEnum
)
) {
// a successful booking can still have membership errors
const membershipFailedError = bookingStatus.errors.find(
(e) => e.errorCode === MEMBERSHIP_FAILED_ERROR
)
const errorParam = membershipFailedError
? `&errorCode=${membershipFailedError.errorCode}`
: ""
router.replace(`${successRedirectUrl}${errorParam}`)
}
}, [bookingStatus, successRedirectUrl, router])
if (isTimeout || error) {
return <TimeoutSpinner />
}
return <LoadingSpinner fullPage />
}

View File

@@ -0,0 +1,45 @@
"use client"
import { useIntl } from "react-intl"
import { customerService } from "@/constants/currentWebHrefs"
import LoadingSpinner from "@/components/LoadingSpinner"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import styles from "./timeoutSpinner.module.css"
export default function TimeoutSpinner() {
const intl = useIntl()
const lang = useLang()
return (
<div className={styles.container}>
<LoadingSpinner />
<Subtitle className={styles.heading}>
{intl.formatMessage({ id: "Taking longer than usual" })}
</Subtitle>
<Body textAlign="center" className={styles.messageContainer}>
{intl.formatMessage(
{
id: "We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.",
},
{
link: (text) => (
<Link
href={customerService[lang]}
variant="underscored"
target="_blank"
>
{text}
</Link>
),
}
)}
</Body>
</div>
)
}

View File

@@ -0,0 +1,18 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: var(--Spacing-x2);
text-align: center;
}
.container .heading {
margin-bottom: var(--Spacing-x1);
}
.messageContainer {
max-width: 435px;
text-align: center;
}

View File

@@ -13,10 +13,6 @@ import {
PAYMENT_METHOD_TITLES,
PaymentMethodEnum,
} from "@/constants/booking"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import {
bookingConfirmation,
selectRate,
@@ -27,15 +23,10 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import LoadingSpinner from "@/components/LoadingSpinner"
import Button from "@/components/TempDesignSystem/Button"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Title from "@/components/TempDesignSystem/Text/Title"
import { toast } from "@/components/TempDesignSystem/Toasts"
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast"
import useLang from "@/hooks/useLang"
import { trackPaymentEvent } from "@/utils/tracking"
@@ -46,8 +37,10 @@ import GuaranteeDetails from "./GuaranteeDetails"
import { hasFlexibleRate, hasPrepaidRate, isPaymentMethodEnum } from "./helpers"
import MixedRatePaymentBreakdown from "./MixedRatePaymentBreakdown"
import MySavedCards from "./MySavedCards"
import PaymentAlert from "./PaymentAlert"
import PaymentOption from "./PaymentOption"
import { type PaymentFormData, paymentSchema } from "./schema"
import TermsAndConditions from "./TermsAndConditions"
import styles from "./payment.module.css"
@@ -74,6 +67,8 @@ export default function PaymentClient({
const intl = useIntl()
const searchParams = useSearchParams()
const [showPaymentAlert, setShowPaymentAlert] = useState(false)
const { booking, canProceedToPayment, rooms, totalPrice } =
useEnterDetailsStore((state) => ({
booking: state.booking,
@@ -109,8 +104,6 @@ export default function PaymentClient({
const hasFlexRates = rooms.some(hasFlexibleRate)
const hasMixedRates = hasPrepaidRates && hasFlexRates
usePaymentFailedToast()
const methods = useForm<PaymentFormData>({
defaultValues: {
paymentMethod: savedCreditCards?.length
@@ -181,7 +174,7 @@ export default function PaymentClient({
const bookingStatus = useHandleBookingStatus({
confirmationNumber: bookingNumber,
expectedStatus: BookingStatusEnum.BookingCompleted,
expectedStatuses: [BookingStatusEnum.BookingCompleted],
maxRetries,
retryInterval,
enabled: isPollingForBookingStatus,
@@ -189,11 +182,8 @@ export default function PaymentClient({
const handlePaymentError = useCallback(
(errorMessage: string) => {
toast.error(
intl.formatMessage({
id: "We had an issue processing your booking. Please try again. No charges have been made.",
})
)
setShowPaymentAlert(true)
const currentPaymentMethod = methods.getValues("paymentMethod")
const smsEnable = methods.getValues("smsConfirmation")
const isSavedCreditCard = savedCreditCards?.some(
@@ -210,7 +200,7 @@ export default function PaymentClient({
status: "failed",
})
},
[intl, methods, savedCreditCards, hotelId]
[methods, savedCreditCards, hotelId]
)
useEffect(() => {
@@ -396,6 +386,7 @@ export default function PaymentClient({
? confirm
: payment}
</Title>
<PaymentAlert isVisible={showPaymentAlert} />
</header>
<FormProvider {...methods}>
<form
@@ -466,49 +457,7 @@ export default function PaymentClient({
</section>
<section className={styles.section}>
<Caption>
{intl.formatMessage(
{
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
},
{
termsAndConditionsLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={bookingTermsAndConditions[lang]}
target="_blank"
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={privacyPolicy[lang]}
target="_blank"
>
{str}
</Link>
),
}
)}
</Caption>
<Checkbox name="termsAndConditions">
<Caption>
{intl.formatMessage({
id: "I accept the terms and conditions",
})}
</Caption>
</Checkbox>
<Checkbox name="smsConfirmation">
<Caption>
{intl.formatMessage({
id: "I would like to get my booking confirmation via sms",
})}
</Caption>
</Checkbox>
<TermsAndConditions />
</section>
</>
)}

View File

@@ -0,0 +1,70 @@
import { useIntl } from "react-intl"
import {
bookingTermsAndConditions,
privacyPolicy,
} from "@/constants/currentWebHrefs"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Link from "@/components/TempDesignSystem/Link"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import useLang from "@/hooks/useLang"
import styles from "../payment.module.css"
export default function TermsAndConditions() {
const intl = useIntl()
const lang = useLang()
return (
<>
<Caption>
{intl.formatMessage(
{
id: "By paying with any of the payment methods available, I accept the terms for this booking and the general <termsAndConditionsLink>Terms & Conditions</termsAndConditionsLink>, and understand that Scandic will process my personal data for this booking in accordance with <privacyPolicyLink>Scandic's Privacy policy</privacyPolicyLink>. I also accept that Scandic require a valid credit card during my visit in case anything is left unpaid.",
},
{
termsAndConditionsLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={bookingTermsAndConditions[lang]}
target="_blank"
weight="bold"
size="small"
>
{str}
</Link>
),
privacyPolicyLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={privacyPolicy[lang]}
target="_blank"
weight="bold"
size="small"
>
{str}
</Link>
),
}
)}
</Caption>
<Checkbox name="termsAndConditions">
<Caption>
{intl.formatMessage({
id: "I accept the terms and conditions",
})}
</Caption>
</Checkbox>
<Checkbox name="smsConfirmation">
<Caption>
{intl.formatMessage({
id: "I would like to get my booking confirmation via sms",
})}
</Caption>
</Checkbox>
</>
)
}

View File

@@ -98,7 +98,7 @@ export default function GuaranteeLateArrival({
const bookingStatus = useHandleBookingStatus({
confirmationNumber: booking.confirmationNumber,
expectedStatus: BookingStatusEnum.BookingCompleted,
expectedStatuses: [BookingStatusEnum.BookingCompleted],
maxRetries,
retryInterval,
enabled: isPollingForBookingStatus,
@@ -195,7 +195,6 @@ export default function GuaranteeLateArrival({
{
termsAndConditionsLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={bookingTermsAndConditions[lang]}
target="_blank"
@@ -205,7 +204,6 @@ export default function GuaranteeLateArrival({
),
privacyPolicyLink: (str) => (
<Link
className={styles.link}
variant="underscored"
href={privacyPolicy[lang]}
target="_blank"

View File

@@ -32,6 +32,12 @@
gap: var(--Spacing-x-half);
}
.alert .closeButton {
padding: var(--Spacing-x-one-and-half);
display: flex;
align-items: center;
}
/* Intent: inline */
.inline {
border-radius: var(--Corner-radius-Large);

View File

@@ -22,6 +22,7 @@ export interface AlertProps extends VariantProps<typeof alertVariants> {
title: string
keepSearchParams?: boolean
} | null
close?: () => void
ariaRole?: AriaRole
ariaLive?: "off" | "assertive" | "polite"
}

View File

@@ -1,5 +1,8 @@
"use client"
import { Button } from "@scandic-hotels/design-system/Button"
import { CloseLargeIcon } from "@/components/Icons"
import Body from "@/components/TempDesignSystem/Text/Body"
import Link from "../Link"
@@ -18,6 +21,7 @@ export default function Alert({
heading,
text,
link,
close,
phoneContact,
sidepeekCtaText,
sidepeekContent,
@@ -67,6 +71,7 @@ export default function Alert({
</Body>
) : null}
</div>
{link ? (
<Link
color="burgundy"
@@ -84,6 +89,11 @@ export default function Alert({
/>
) : null}
</div>
{close ? (
<Button onPress={close} variant="Text" className={styles.closeButton}>
<CloseLargeIcon height={24} width={24} color="uiTextPlaceholder" />
</Button>
) : null}
</div>
</section>
)

View File

@@ -163,3 +163,9 @@ export enum CancellationRuleEnum {
NonCancellable = "NonCancellable",
Changeable = "Changeable",
}
export enum PaymentCallbackStatusEnum {
Success = "success",
Error = "error",
Cancel = "cancel",
}

View File

@@ -8,13 +8,13 @@ import type { BookingStatusEnum } from "@/constants/booking"
export function useHandleBookingStatus({
confirmationNumber,
expectedStatus,
expectedStatuses,
maxRetries,
retryInterval,
enabled,
}: {
confirmationNumber: string | null
expectedStatus: BookingStatusEnum
expectedStatuses: BookingStatusEnum[]
maxRetries: number
retryInterval: number
enabled: boolean
@@ -32,7 +32,11 @@ export function useHandleBookingStatus({
return false
}
if (query.state.data?.reservationStatus === expectedStatus) {
if (
expectedStatuses.includes(
query.state.data?.reservationStatus as BookingStatusEnum
)
) {
return false
}

View File

@@ -712,6 +712,7 @@
"Surprise!": "Overraskelse!",
"Surprises": "Surprises",
"TUI Points": "TUI Points",
"Taking longer than usual": "Tager længere end normalt",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.",
"Terms and conditions": "Vilkår og betingelser",
"Thank you": "Tak",
@@ -786,6 +787,7 @@
"View your booking": "Se din booking",
"Visiting address": "Besøgsadresse",
"Voucher": "Voucher",
"We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.": "Vi er stadig bekræftende din booking. Dette er normalt en sak, der tager få minutter, og vi beklager ventetiden. Bedes du tjekke din indbakke for en booking bekræftelsesmail og hvis du stadig ikke har modtaget den indtil kl. 17.00, bedes du kontakte vores <link>kundesupport</link>.",
"We could not add a card right now, please try again later.": "Vi kunne ikke tilføje et kort lige nu. Prøv venligst igen senere.",
"We could not connect your accounts": "We could not connect your accounts",
"We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.": "We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.",

View File

@@ -711,6 +711,7 @@
"Surprise!": "Überraschung!",
"Surprises": "Surprises",
"TUI Points": "TUI Points",
"Taking longer than usual": "Länger als normal",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.",
"Terms and conditions": "Geschäftsbedingungen",
"Thank you": "Danke",
@@ -784,6 +785,7 @@
"View your booking": "Ihre Buchung ansehen",
"Visiting address": "Besuchsadresse",
"Voucher": "Gutschein",
"We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.": "Wir bestätigen Ihre Buchung noch. Dies dauert normalerweise nur wenige Minuten und wir bitten um Ihr Verständnis. Bitte überprüfen Sie Ihre E-Mail-Postfach für eine Buchungsbestätigung und wenn Sie die E-Mail noch nicht erhalten haben, kontaktieren Sie uns bitte bis zum Ende des Tages unter <link>Kundensupport</link>.",
"We could not add a card right now, please try again later.": "Wir konnten momentan keine Karte hinzufügen. Bitte versuchen Sie es später noch einmal.",
"We could not connect your accounts": "We could not connect your accounts",
"We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.": "We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.",

View File

@@ -710,6 +710,7 @@
"Surprise!": "Surprise!",
"Surprises": "Surprises",
"TUI Points": "TUI Points",
"Taking longer than usual": "Taking longer than usual",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
"Terms and conditions": "Terms and conditions",
"Thank you": "Thank you",
@@ -782,6 +783,7 @@
"View your booking": "View your booking",
"Visiting address": "Visiting address",
"Voucher": "Voucher",
"We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.": "We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.",
"We could not add a card right now, please try again later.": "We could not add a card right now, please try again later.",
"We could not connect your accounts": "We could not connect your accounts",
"We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.": "We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.",

View File

@@ -711,6 +711,7 @@
"Surprise!": "Yllätys!",
"Surprises": "Surprises",
"TUI Points": "TUI Points",
"Taking longer than usual": "Tällainen kestää pidemmän aikaa kuin normaalisti",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
"Terms and conditions": "Säännöt ja ehdot",
"Thank you": "Kiitos",
@@ -784,6 +785,7 @@
"View your booking": "Näytä varauksesi",
"Visiting address": "Käyntiosoite",
"Voucher": "Ravintolakuponki",
"We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.": "Olemme vielä vahvistamassa varauksessasi. Tämä tavallisesti kestää muutaman minuutin, ja me olemme pahoillamme siitä odotusaikaa. Tarkista postilaatikkoosi varauksen vahvistamiseksi ja jos et ole vielä saanut sitä päivän loppuun mennessä, ota yhteyttä <link>asiakaspalveluumme</link>.",
"We could not add a card right now, please try again later.": "Emme voineet lisätä korttia juuri nyt. Yritä myöhemmin uudelleen.",
"We could not connect your accounts": "We could not connect your accounts",
"We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.": "We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.",

View File

@@ -708,6 +708,7 @@
"Surprise!": "Overraskelse!",
"Surprises": "Surprises",
"TUI Points": "TUI Points",
"Taking longer than usual": "Tar lenger tid enn normalt",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.",
"Terms and conditions": "Vilkår og betingelser",
"Thank you": "Takk",
@@ -780,6 +781,7 @@
"View your booking": "Se din bestilling",
"Visiting address": "Besøksadresse",
"Voucher": "Voucher",
"We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.": "Vi bekrefter din bestilling. Dette gjør vi normalt i løpet av noen minutter, og vi beklager ventetiden. Vennligst sjekk e-posten din for en bestillingsbekreftelsesmail og hvis du ikke har mottatt den før kl. 17.00, vennligst kontakt vår <link>kundesupport</link>.",
"We could not add a card right now, please try again later.": "Vi kunne ikke legge til et kort akkurat nå. Prøv igjen senere.",
"We could not connect your accounts": "We could not connect your accounts",
"We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.": "We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.",

View File

@@ -709,6 +709,7 @@
"Surprise!": "Överraskning!",
"Surprises": "Surprises",
"TUI Points": "TUI Points",
"Taking longer than usual": "Tar längre tid än vanligt",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.",
"Terms and conditions": "Allmänna villkor",
"Thank you": "Tack",
@@ -782,6 +783,7 @@
"View your booking": "Visa din bokning",
"Visiting address": "Besöksadress",
"Voucher": "Kupong",
"We are still confirming your booking. This is usually a matter of minutes and we do apologise for the wait. Please check your inbox for a booking confirmation email and if you still haven't received it by end of day, please contact our <link>customer support</link>.": "Vi bekräftar din bokning. Detta gör vi normalt inom några minuter, och vi beklagar denna väntan. Vänligen kontrollera din inkorg för en bokningsbekräftelse och om du inte har fått detta senare på dagen, vänligen kontakta vår <link>kundsupport</link>.",
"We could not add a card right now, please try again later.": "Vi kunde inte lägga till ett kort just nu, vänligen försök igen senare.",
"We could not connect your accounts": "We could not connect your accounts",
"We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.": "We could not connect your accounts to give you access. Please contact us and well help you resolve this issue.",

View File

@@ -88,17 +88,17 @@ export type Guest = z.output<typeof guestSchema>
export const packageSchema = z
.object({
type: z.string().nullable(),
type: z.string().nullish(),
description: nullableStringValidator,
code: z.string().nullable().default(""),
code: nullableStringValidator,
price: z.object({
unit: z.number().int().nullable(),
unit: z.number().int().nullish(),
unitPrice: z.number(),
totalPrice: z.number().nullable(),
totalUnit: z.number().int().nullable(),
totalPrice: z.number().nullish(),
totalUnit: z.number().int().nullish(),
currency: z.string().default(""),
}),
comment: z.string().nullable().optional(),
comment: z.string().nullish(),
})
.transform((packageData) => ({
type: packageData.type,