Merged in chore/move-enter-details (pull request #2778)

Chore/move enter details

Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-09-11 07:16:24 +00:00
parent 15711cb3a4
commit 7dee6d5083
238 changed files with 1656 additions and 1602 deletions

View File

@@ -0,0 +1,112 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { PaymentCallbackStatusEnum } from "@scandic-hotels/common/constants/paymentCallbackStatusEnum"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { trackEvent } from "@scandic-hotels/tracking/base"
import { detailsStorageName } from "../../../../stores/enter-details"
import { useTrackingContext } from "../../../../trackingContext"
import { serializeBookingSearchParams } from "../../../../utils/url"
import {
clearPaymentInfoSessionStorage,
readPaymentInfoFromSessionStorage,
} from "../helpers"
import { clearGlaSessionStorage, readGlaFromSessionStorage } from "./helpers"
import type { PersistedState } from "../../../../stores/enter-details/types"
export function HandleErrorCallback({
returnUrl,
searchObject,
status,
errorMessage,
}: {
returnUrl: string
searchObject: URLSearchParams
status: PaymentCallbackStatusEnum
errorMessage?: string
}) {
const router = useRouter()
const { trackPaymentEvent } = useTrackingContext()
useEffect(() => {
const bookingData = window.sessionStorage.getItem(detailsStorageName)
if (bookingData) {
const detailsStorage: PersistedState = JSON.parse(bookingData)
const searchParams = serializeBookingSearchParams(
detailsStorage.booking,
{
initialSearchParams: searchObject,
}
)
const glaSessionData = readGlaFromSessionStorage()
const paymentInfoSessionData = readPaymentInfoFromSessionStorage()
if (status === PaymentCallbackStatusEnum.Cancel) {
if (glaSessionData) {
trackEvent({
event: "glaCardSaveCancelled",
hotelInfo: {
hotelId: glaSessionData.hotelId,
lateArrivalGuarantee: glaSessionData.lateArrivalGuarantee,
guaranteedProduct: "room",
},
paymentInfo: {
hotelId: glaSessionData.hotelId,
status: "glacardsavecancelled",
type: glaSessionData.paymentMethod,
isSavedCreditCard: glaSessionData.isSavedCreditCard,
},
})
} else {
trackPaymentEvent({
event: "paymentCancel",
hotelId: detailsStorage.booking.hotelId,
status: "cancelled",
method: paymentInfoSessionData?.paymentMethod,
isSavedCreditCard: paymentInfoSessionData?.isSavedCreditCard,
})
}
}
if (status === PaymentCallbackStatusEnum.Error) {
if (glaSessionData) {
trackEvent({
event: "glaCardSaveFailed",
hotelInfo: {
hotelId: glaSessionData.hotelId,
lateArrivalGuarantee: glaSessionData.lateArrivalGuarantee,
guaranteedProduct: "room",
},
paymentInfo: {
hotelId: glaSessionData.hotelId,
status: "glacardsavefailed",
type: glaSessionData.paymentMethod,
isSavedCreditCard: glaSessionData.isSavedCreditCard,
},
})
} else {
trackPaymentEvent({
event: "paymentFail",
hotelId: detailsStorage.booking.hotelId,
errorMessage,
status: "failed",
method: paymentInfoSessionData?.paymentMethod,
isSavedCreditCard: paymentInfoSessionData?.isSavedCreditCard,
})
}
}
clearGlaSessionStorage()
clearPaymentInfoSessionStorage()
if (searchParams.size > 0) {
router.replace(`${returnUrl}?${searchParams.toString()}`)
}
}
}, [returnUrl, router, searchObject, status, errorMessage, trackPaymentEvent])
return <LoadingSpinner fullPage />
}

View File

@@ -0,0 +1,79 @@
"use client"
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { useHandleBookingStatus } from "../../../../hooks/useHandleBookingStatus"
import { MEMBERSHIP_FAILED_ERROR } from "../../../../types/membershipFailedError"
import TimeoutSpinner from "./TimeoutSpinner"
import { trackGuaranteeBookingSuccess } from "./tracking"
const validBookingStatuses = [
BookingStatusEnum.PaymentSucceeded,
BookingStatusEnum.BookingCompleted,
]
interface HandleStatusPollingProps {
refId: string
sig: string
successRedirectUrl: string
cardType?: string
}
export function HandleSuccessCallback({
refId,
sig,
successRedirectUrl,
cardType,
}: HandleStatusPollingProps) {
const router = useRouter()
useEffect(() => {
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
document.cookie = `bcsig=${sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
}, [sig])
const {
data: bookingStatus,
error,
isTimeout,
} = useHandleBookingStatus({
refId,
expectedStatuses: validBookingStatuses,
maxRetries: 10,
retryInterval: 2000,
enabled: true,
})
useEffect(() => {
if (!bookingStatus?.booking.reservationStatus) {
return
}
if (
validBookingStatuses.includes(
bookingStatus.booking.reservationStatus as BookingStatusEnum
)
) {
trackGuaranteeBookingSuccess(cardType)
// a successful booking can still have membership errors
const membershipFailedError = bookingStatus.booking.errors.find(
(e) => e.errorCode === MEMBERSHIP_FAILED_ERROR
)
const errorParam = membershipFailedError
? `&errorCode=${membershipFailedError.errorCode}`
: ""
router.replace(`${successRedirectUrl}${errorParam}`)
}
}, [bookingStatus, cardType, successRedirectUrl, router])
if (isTimeout || error) {
return <TimeoutSpinner />
}
return <LoadingSpinner fullPage />
}

View File

@@ -0,0 +1,48 @@
"use client"
import { useIntl } from "react-intl"
import { customerService } from "@scandic-hotels/common/constants/routes/customerService"
import Body from "@scandic-hotels/design-system/Body"
import Link from "@scandic-hotels/design-system/Link"
import { LoadingSpinner } from "@scandic-hotels/design-system/LoadingSpinner"
import Subtitle from "@scandic-hotels/design-system/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({
defaultMessage: "Taking longer than usual",
})}
</Subtitle>
<Body textAlign="center" className={styles.messageContainer}>
{intl.formatMessage(
{
defaultMessage:
"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]}
textDecoration="underline"
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

@@ -0,0 +1,48 @@
import "client-only"
import { logger } from "@scandic-hotels/common/logger"
export const glaStorageName = "gla-storage"
type GlaSessionData = {
lateArrivalGuarantee: string
hotelId: string
paymentMethod?: string
isSavedCreditCard?: boolean
}
export function readGlaFromSessionStorage(): GlaSessionData | null {
try {
const glaSessionData = sessionStorage.getItem(glaStorageName)
if (!glaSessionData) return null
return JSON.parse(glaSessionData)
} catch (error) {
logger.error("Error reading from session storage:", error)
return null
}
}
export function writeGlaToSessionStorage(
lateArrivalGuarantee: string,
hotelId: string,
paymentMethod: string,
isSavedCreditCard: boolean
) {
try {
sessionStorage.setItem(
glaStorageName,
JSON.stringify({
lateArrivalGuarantee,
hotelId,
paymentMethod,
isSavedCreditCard,
})
)
} catch (error) {
logger.error("Error writing to session storage:", error)
}
}
export function clearGlaSessionStorage() {
sessionStorage.removeItem(glaStorageName)
}

View File

@@ -0,0 +1,23 @@
import { trackEvent } from "@scandic-hotels/tracking/base"
import { clearGlaSessionStorage, readGlaFromSessionStorage } from "./helpers"
export function trackGuaranteeBookingSuccess(cardType?: string) {
const glaSessionData = readGlaFromSessionStorage()
if (glaSessionData) {
trackEvent({
event: "guaranteeBookingSuccess",
hotelInfo: {
lateArrivalGuarantee: glaSessionData.lateArrivalGuarantee,
hotelId: glaSessionData.hotelId,
guaranteedProduct: "room",
},
paymentInfo: {
hotelId: glaSessionData.hotelId,
type: cardType ?? glaSessionData.paymentMethod,
isSavedCreditCard: glaSessionData.isSavedCreditCard,
},
})
}
clearGlaSessionStorage()
}