feat(SW-2605): confirmation page only valid for 1 minute for the session

This commit is contained in:
Michael Zetterberg
2025-05-06 12:04:00 +02:00
parent e0fe5ff0d5
commit c6a0ce86df
9 changed files with 73 additions and 21 deletions

View File

@@ -1,21 +1,41 @@
import { notFound } from "next/navigation"
import { cookies } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
import { decrypt } from "@/utils/encryption"
import type { LangParams, PageArgs } from "@/types/params"
export default async function BookingConfirmationPage({
params,
searchParams,
}: PageArgs<LangParams, { RefId?: string }>) {
}: PageArgs<
LangParams,
{
RefId?: string
}
>) {
const refId = searchParams.RefId
if (!refId) {
notFound()
}
const sig = cookies().get("bcsig")?.value
if (!sig) {
redirect(`/${params.lang}`)
}
const expire = Number(decrypt(sig))
const now = Math.floor(Date.now() / 1000)
if (typeof expire === "number" && !isNaN(expire) && now > expire) {
redirect(`/${params.lang}`)
}
void getBookingConfirmation(refId)
const membershipFailedError =

View File

@@ -61,7 +61,9 @@ export default async function GuaranteePaymentCallbackPage({
refId,
})
const error = bookingStatus.errors.find((e) => e.errorCode)
const { booking } = bookingStatus
const error = booking.errors.find((e) => e.errorCode)
errorMessage =
error?.description ??
`No error message found for booking ${confirmationNumber}, status: ${status}`

View File

@@ -15,6 +15,7 @@ import { getServiceToken } from "@/server/tokenManager"
import { auth } from "@/auth"
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback"
import { encrypt } from "@/utils/encryption"
import type { LangParams, PageArgs } from "@/types/params"
@@ -62,6 +63,8 @@ export default async function PaymentCallbackPage({
const { refId } = booking
if (status === PaymentCallbackStatusEnum.Success && confirmationNumber) {
const expire = Math.floor(Date.now() / 1000) + 60
const sig = encrypt(expire.toString())
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(refId)}`
console.log(
`[payment-callback] rendering success callback with confirmation number: ${confirmationNumber}`
@@ -70,6 +73,7 @@ export default async function PaymentCallbackPage({
return (
<HandleSuccessCallback
refId={refId}
sig={sig}
successRedirectUrl={confirmationUrl}
/>
)
@@ -86,8 +90,10 @@ export default async function PaymentCallbackPage({
refId,
})
const { booking } = bookingStatus
// TODO: how to handle errors for multiple rooms?
const error = bookingStatus.errors.find((e) => e.errorCode)
const error = booking.errors.find((e) => e.errorCode)
errorMessage =
error?.description ??

View File

@@ -19,15 +19,22 @@ const validBookingStatuses = [
interface HandleStatusPollingProps {
refId: string
sig: string
successRedirectUrl: string
}
export default function HandleSuccessCallback({
refId,
sig,
successRedirectUrl,
}: 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,
@@ -41,13 +48,13 @@ export default function HandleSuccessCallback({
})
useEffect(() => {
if (!bookingStatus?.reservationStatus) {
if (!bookingStatus?.booking.reservationStatus) {
return
}
if (
validBookingStatuses.includes(
bookingStatus.reservationStatus as BookingStatusEnum
bookingStatus.booking.reservationStatus as BookingStatusEnum
)
) {
const glaSessionData = readGlaFromSessionStorage()
@@ -63,7 +70,7 @@ export default function HandleSuccessCallback({
clearGlaSessionStorage()
}
// a successful booking can still have membership errors
const membershipFailedError = bookingStatus.errors.find(
const membershipFailedError = bookingStatus.booking.errors.find(
(e) => e.errorCode === MEMBERSHIP_FAILED_ERROR
)
const errorParam = membershipFailedError

View File

@@ -155,9 +155,12 @@ export default function PaymentClient({
return
}
const mainRoom = result.rooms[0]
const { booking } = result
const mainRoom = booking.rooms[0]
if (result.reservationStatus == BookingStatusEnum.BookingCompleted) {
if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) {
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
router.push(confirmationUrl)
return
@@ -165,9 +168,9 @@ export default function PaymentClient({
setRefId(mainRoom.refId)
const hasPriceChange = result.rooms.some((r) => r.priceChangedMetadata)
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
if (hasPriceChange) {
const priceChangeData = result.rooms.map(
const priceChangeData = booking.rooms.map(
(room) => room.priceChangedMetadata || null
)
setPriceChangeData(priceChangeData)
@@ -258,13 +261,15 @@ export default function PaymentClient({
)
useEffect(() => {
if (bookingStatus?.data?.paymentUrl) {
router.push(bookingStatus.data.paymentUrl)
if (bookingStatus?.data?.booking.paymentUrl) {
router.push(bookingStatus.data.booking.paymentUrl)
} else if (
bookingStatus?.data?.reservationStatus ===
bookingStatus?.data?.booking.reservationStatus ===
BookingStatusEnum.BookingCompleted
) {
const mainRoom = bookingStatus.data.rooms[0]
const mainRoom = bookingStatus.data.booking.rooms[0]
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
document.cookie = `bcsig=${bookingStatus.data.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
router.push(confirmationUrl)
} else if (bookingStatus.isTimeout) {

View File

@@ -75,8 +75,8 @@ export function useGuaranteeBooking(
})
useEffect(() => {
if (bookingStatus?.data?.paymentUrl && isPollingForBookingStatus) {
router.push(bookingStatus.data.paymentUrl)
if (bookingStatus?.data?.booking.paymentUrl && isPollingForBookingStatus) {
router.push(bookingStatus.data.booking.paymentUrl)
utils.booking.get.invalidate({ refId })
setIsPollingForBookingStatus(false)
} else if (bookingStatus.isTimeout) {
@@ -95,7 +95,7 @@ export function useGuaranteeBooking(
const isLoading =
guaranteeBooking.isPending ||
(isPollingForBookingStatus &&
!bookingStatus.data?.paymentUrl &&
!bookingStatus.data?.booking.paymentUrl &&
!bookingStatus.isTimeout)
return {

View File

@@ -34,7 +34,7 @@ export function useHandleBookingStatus({
if (
expectedStatuses.includes(
query.state.data?.reservationStatus as BookingStatusEnum
query.state.data?.booking.reservationStatus as BookingStatusEnum
)
) {
return false

View File

@@ -4,6 +4,8 @@ import { getMembershipNumber } from "@/server/routers/user/utils"
import { createCounter } from "@/server/telemetry"
import { router, safeProtectedServiceProcedure } from "@/server/trpc"
import { encrypt } from "@/utils/encryption"
import {
addPackageInput,
cancelBookingsInput,
@@ -71,7 +73,12 @@ export const bookingMutationRouter = router({
metricsCreateBooking.success()
return verifiedData.data
const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry
return {
booking: verifiedData.data,
sig: encrypt(expire.toString()),
}
}),
priceChange: safeProtectedServiceProcedure
.concat(refIdPlugin.toConfirmationNumber)

View File

@@ -244,7 +244,12 @@ export const bookingQueryRouter = router({
metricsGetBookingStatus.success()
return verifiedData.data
const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry
return {
booking: verifiedData.data,
sig: encrypt(expire.toString()),
}
}),
createRefId: serviceProcedure
.input(createRefIdInput)