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

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