fix: avoid sending query params to planet
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
.layout {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
export default function PaymentCallbackLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
return <div className={styles.layout}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
PaymentErrorCodeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
payment,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<
|
||||
LangParams,
|
||||
{ status: "error" | "success" | "cancel"; confirmationNumber?: string }
|
||||
>) {
|
||||
console.log(`[payment-callback] callback started`)
|
||||
const lang = params.lang
|
||||
const status = searchParams.status
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
|
||||
if (status === "success" && confirmationNumber) {
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}`
|
||||
|
||||
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
|
||||
redirect(confirmationUrl)
|
||||
}
|
||||
|
||||
const returnUrl = payment(lang)
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
if (confirmationNumber) {
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.status({
|
||||
confirmationNumber,
|
||||
})
|
||||
if (bookingStatus.metadata) {
|
||||
searchObject.set(
|
||||
"errorCode",
|
||||
bookingStatus.metadata.errorCode?.toString() ?? ""
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||
)
|
||||
if (status === "cancel") {
|
||||
searchObject.set("errorCode", PaymentErrorCodeEnum.Cancelled.toString())
|
||||
}
|
||||
if (status === "error") {
|
||||
searchObject.set("errorCode", PaymentErrorCodeEnum.Failed.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PaymentCallback
|
||||
returnUrl={returnUrl.toString()}
|
||||
searchObject={searchObject}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
PaymentErrorCodeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
payment,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { getPublicURL } from "@/server/utils"
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { lang: string; status: string } }
|
||||
): Promise<NextResponse> {
|
||||
const publicURL = getPublicURL(request)
|
||||
|
||||
console.log(`[payment-callback] callback started`)
|
||||
const lang = params.lang as Lang
|
||||
const status = params.status
|
||||
|
||||
const queryParams = request.nextUrl.searchParams
|
||||
const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER)
|
||||
|
||||
if (status === "success" && confirmationNumber) {
|
||||
const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation(lang)}`)
|
||||
confirmationUrl.searchParams.set(
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
confirmationNumber
|
||||
)
|
||||
|
||||
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
|
||||
return NextResponse.redirect(confirmationUrl)
|
||||
}
|
||||
|
||||
const returnUrl = new URL(`${publicURL}/${payment(lang)}`)
|
||||
returnUrl.search = queryParams.toString()
|
||||
|
||||
if (confirmationNumber) {
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.status({
|
||||
confirmationNumber,
|
||||
})
|
||||
if (bookingStatus.metadata) {
|
||||
returnUrl.searchParams.set(
|
||||
"errorCode",
|
||||
bookingStatus.metadata.errorCode?.toString() ?? ""
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||
)
|
||||
|
||||
if (status === "cancel") {
|
||||
returnUrl.searchParams.set(
|
||||
"errorCode",
|
||||
PaymentErrorCodeEnum.Cancelled.toString()
|
||||
)
|
||||
}
|
||||
if (status === "error") {
|
||||
returnUrl.searchParams.set(
|
||||
"errorCode",
|
||||
PaymentErrorCodeEnum.Failed.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[payment-callback] redirecting to: ${returnUrl}`)
|
||||
return NextResponse.redirect(returnUrl)
|
||||
}
|
||||
@@ -55,4 +55,8 @@ export const signedInDetailsSchema = z.object({
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
phoneNumber: z.string().optional(),
|
||||
join: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.transform((_) => false),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { detailsStorageName } from "@/stores/details"
|
||||
|
||||
import { createQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
import { DetailsState } from "@/types/stores/details"
|
||||
|
||||
export default function PaymentCallback({
|
||||
returnUrl,
|
||||
searchObject,
|
||||
}: {
|
||||
returnUrl: string
|
||||
searchObject: URLSearchParams
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const bookingData = window.sessionStorage.getItem(detailsStorageName)
|
||||
|
||||
if (bookingData) {
|
||||
const detailsStorage: Record<
|
||||
"state",
|
||||
Pick<DetailsState, "data">
|
||||
> = JSON.parse(bookingData)
|
||||
const searchParams = createQueryParamsForEnterDetails(
|
||||
detailsStorage.state.data.booking,
|
||||
searchObject
|
||||
)
|
||||
|
||||
if (searchParams.size > 0) {
|
||||
router.replace(`${returnUrl}?${searchParams.toString()}`)
|
||||
}
|
||||
}
|
||||
}, [returnUrl, router, searchObject])
|
||||
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -60,7 +60,6 @@ export default function Payment({
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const queryParams = useSearchParams()
|
||||
const { booking, ...userData } = useDetailsStore((state) => state.data)
|
||||
const setIsSubmittingDisabled = useDetailsStore(
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
@@ -164,9 +163,6 @@ export default function Payment({
|
||||
])
|
||||
|
||||
function handleSubmit(data: PaymentFormData) {
|
||||
const allQueryParams =
|
||||
queryParams.size > 0 ? `?${queryParams.toString()}` : ""
|
||||
|
||||
// set payment method to card if saved card is submitted
|
||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||
? data.paymentMethod
|
||||
@@ -176,6 +172,8 @@ export default function Payment({
|
||||
(card) => card.id === data.paymentMethod
|
||||
)
|
||||
|
||||
const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
||||
|
||||
initiateBooking.mutate({
|
||||
hotelId: hotel,
|
||||
checkInDate: fromDate,
|
||||
@@ -224,9 +222,9 @@ export default function Payment({
|
||||
}
|
||||
: undefined,
|
||||
|
||||
success: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/success`,
|
||||
error: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/error${allQueryParams}`,
|
||||
cancel: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/cancel${allQueryParams}`,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,3 +54,50 @@ export function getQueryParamsForEnterDetails(
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function createQueryParamsForEnterDetails(
|
||||
bookingData: BookingData,
|
||||
intitalSearchParams: URLSearchParams
|
||||
) {
|
||||
const { hotel, fromDate, toDate, rooms } = bookingData
|
||||
|
||||
const bookingSearchParams = new URLSearchParams({ hotel, fromDate, toDate })
|
||||
const searchParams = new URLSearchParams([
|
||||
...intitalSearchParams,
|
||||
...bookingSearchParams,
|
||||
])
|
||||
|
||||
rooms.forEach((item, index) => {
|
||||
if (item?.adults) {
|
||||
searchParams.set(`room[${index}].adults`, item.adults.toString())
|
||||
}
|
||||
if (item?.children) {
|
||||
item.children.forEach((child, childIndex) => {
|
||||
searchParams.set(
|
||||
`room[${index}].child[${childIndex}].age`,
|
||||
child.age.toString()
|
||||
)
|
||||
searchParams.set(
|
||||
`room[${index}].child[${childIndex}].bed`,
|
||||
child.bed.toString()
|
||||
)
|
||||
})
|
||||
}
|
||||
if (item?.roomTypeCode) {
|
||||
searchParams.set(`room[${index}].roomtype`, item.roomTypeCode)
|
||||
}
|
||||
if (item?.rateCode) {
|
||||
searchParams.set(`room[${index}].ratecode`, item.rateCode)
|
||||
}
|
||||
|
||||
if (item?.counterRateCode) {
|
||||
searchParams.set(`room[${index}].counterratecode`, item.counterRateCode)
|
||||
}
|
||||
|
||||
if (item.packages && item.packages.length > 0) {
|
||||
searchParams.set(`room[${index}].packages`, item.packages.join(","))
|
||||
}
|
||||
})
|
||||
|
||||
return searchParams
|
||||
}
|
||||
|
||||
4
env/client.ts
vendored
4
env/client.ts
vendored
@@ -5,14 +5,10 @@ export const env = createEnv({
|
||||
client: {
|
||||
NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
NEXT_PUBLIC_PORT: z.string().default("3000"),
|
||||
NEXT_PUBLIC_PAYMENT_CALLBACK_URL: z
|
||||
.string()
|
||||
.default("/api/web/payment-callback"),
|
||||
},
|
||||
emptyStringAsUndefined: true,
|
||||
runtimeEnv: {
|
||||
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
|
||||
NEXT_PUBLIC_PAYMENT_CALLBACK_URL: `${process.env.NODE_ENV === "development" ? `http://localhost:${process.env.NEXT_PUBLIC_PORT}` : ""}/api/web/payment-callback`,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -2,30 +2,14 @@ import "server-only"
|
||||
|
||||
import deepmerge from "deepmerge"
|
||||
|
||||
import { arrayMerge } from "@/utils/merge"
|
||||
|
||||
import { request } from "./request"
|
||||
|
||||
import type { BatchRequestDocument } from "graphql-request"
|
||||
|
||||
import type { Data } from "@/types/request"
|
||||
|
||||
function arrayMerge(
|
||||
target: any[],
|
||||
source: any[],
|
||||
options: deepmerge.ArrayMergeOptions | undefined
|
||||
) {
|
||||
const destination = target.slice()
|
||||
source.forEach((item, index) => {
|
||||
if (typeof destination[index] === "undefined") {
|
||||
destination[index] = options?.cloneUnlessOtherwiseSpecified(item, options)
|
||||
} else if (options?.isMergeableObject(item)) {
|
||||
destination[index] = deepmerge(target[index], item, options)
|
||||
} else if (target.indexOf(item) === -1) {
|
||||
destination.push(item)
|
||||
}
|
||||
})
|
||||
return destination
|
||||
}
|
||||
|
||||
export async function batchRequest<T>(
|
||||
queries: (BatchRequestDocument & { options?: RequestInit })[]
|
||||
): Promise<Data<T>> {
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
||||
@@ -282,6 +282,11 @@ const nextConfig = {
|
||||
"/:lang/hotelreservation/:step(breakfast|details|payment|select-bed)",
|
||||
destination: "/:lang/hotelreservation/step?step=:step",
|
||||
},
|
||||
{
|
||||
source: "/:lang/hotelreservation/payment-callback/:status",
|
||||
destination:
|
||||
"/:lang/hotelreservation/payment-callback?status=:status",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,13 +17,14 @@ export const createBookingSchema = z
|
||||
paymentUrl: z.string().nullable(),
|
||||
metadata: z
|
||||
.object({
|
||||
errorCode: z.number().optional(),
|
||||
errorMessage: z.string().optional(),
|
||||
errorCode: z.number().nullable().optional(),
|
||||
errorMessage: z.string().nullable().optional(),
|
||||
priceChangedMetadata: z
|
||||
.object({
|
||||
roomPrice: z.number().optional(),
|
||||
totalPrice: z.number().optional(),
|
||||
roomPrice: z.number().nullable().optional(),
|
||||
totalPrice: z.number().nullable().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
signedInDetailsSchema,
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
import { DetailsContext } from "@/contexts/Details"
|
||||
import { arrayMerge } from "@/utils/merge"
|
||||
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsState, InitialState } from "@/types/stores/details"
|
||||
|
||||
export const storageName = "details-storage"
|
||||
export const detailsStorageName = "details-storage"
|
||||
export function createDetailsStore(
|
||||
initialState: InitialState,
|
||||
isMember: boolean
|
||||
@@ -27,13 +28,15 @@ export function createDetailsStore(
|
||||
* we cannot use the data as `defaultValues` for our forms.
|
||||
* RHF caches defaultValues on mount.
|
||||
*/
|
||||
const detailsStorageUnparsed = sessionStorage.getItem(storageName)
|
||||
const detailsStorageUnparsed = sessionStorage.getItem(detailsStorageName)
|
||||
if (detailsStorageUnparsed) {
|
||||
const detailsStorage: Record<
|
||||
"state",
|
||||
Pick<DetailsState, "data">
|
||||
> = JSON.parse(detailsStorageUnparsed)
|
||||
initialState = merge(initialState, detailsStorage.state.data)
|
||||
initialState = merge(detailsStorage.state.data, initialState, {
|
||||
arrayMerge,
|
||||
})
|
||||
}
|
||||
}
|
||||
return create<DetailsState>()(
|
||||
@@ -140,7 +143,7 @@ export function createDetailsStore(
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: storageName,
|
||||
name: detailsStorageName,
|
||||
onRehydrateStorage() {
|
||||
return function (state) {
|
||||
if (state) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
import { StepsContext } from "@/contexts/Steps"
|
||||
|
||||
import { storageName as detailsStorageName } from "./details"
|
||||
import { detailsStorageName as detailsStorageName } from "./details"
|
||||
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsState } from "@/types/stores/details"
|
||||
|
||||
19
utils/merge.ts
Normal file
19
utils/merge.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import merge from "deepmerge"
|
||||
|
||||
export function arrayMerge(
|
||||
target: any[],
|
||||
source: any[],
|
||||
options: merge.ArrayMergeOptions
|
||||
) {
|
||||
const destination = target.slice()
|
||||
source.forEach((item, index) => {
|
||||
if (typeof destination[index] === "undefined") {
|
||||
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options)
|
||||
} else if (options?.isMergeableObject(item)) {
|
||||
destination[index] = merge(target[index], item, options)
|
||||
} else if (target.indexOf(item) === -1) {
|
||||
destination.push(item)
|
||||
}
|
||||
})
|
||||
return destination
|
||||
}
|
||||
Reference in New Issue
Block a user