From 3a6cfcfe5c485f4c6f37867edee1e17612f8e185 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Tue, 19 Nov 2024 09:57:54 +0100 Subject: [PATCH] fix: avoid sending query params to planet --- .../payment-callback/layout.module.css | 3 + .../payment-callback/layout.tsx | 16 ++++ .../payment-callback/page.tsx | 70 +++++++++++++++++ .../payment-callback/[lang]/[status]/route.ts | 75 ------------------- .../EnterDetails/Details/schema.ts | 4 + .../Payment/PaymentCallback/index.tsx | 42 +++++++++++ .../EnterDetails/Payment/index.tsx | 12 ++- .../SelectRate/RoomSelection/utils.ts | 47 ++++++++++++ env/client.ts | 4 - lib/graphql/batchRequest.ts | 20 +---- next-env.d.ts | 2 +- next.config.js | 5 ++ server/routers/booking/output.ts | 9 ++- stores/details.ts | 11 ++- stores/steps.ts | 2 +- utils/merge.ts | 19 +++++ 16 files changed, 227 insertions(+), 114 deletions(-) create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx delete mode 100644 app/api/web/payment-callback/[lang]/[status]/route.ts create mode 100644 components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx create mode 100644 utils/merge.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css new file mode 100644 index 000000000..1730ffa68 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css @@ -0,0 +1,3 @@ +.layout { + background-color: var(--Base-Background-Primary-Normal); +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx new file mode 100644 index 000000000..b9ad3b13c --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx @@ -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>) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return
{children}
+} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx new file mode 100644 index 000000000..0e4e716f2 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -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 ( + + ) +} diff --git a/app/api/web/payment-callback/[lang]/[status]/route.ts b/app/api/web/payment-callback/[lang]/[status]/route.ts deleted file mode 100644 index 5884d63f9..000000000 --- a/app/api/web/payment-callback/[lang]/[status]/route.ts +++ /dev/null @@ -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 { - 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) -} diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index abb29ac2b..b3e161dc9 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -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), }) diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx new file mode 100644 index 000000000..1c303dfbe --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx @@ -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 + > = JSON.parse(bookingData) + const searchParams = createQueryParamsForEnterDetails( + detailsStorage.state.data.booking, + searchObject + ) + + if (searchParams.size > 0) { + router.replace(`${returnUrl}?${searchParams.toString()}`) + } + } + }, [returnUrl, router, searchObject]) + + return +} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index 6a2f8877b..ab1f78807 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -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`, }, }) } diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index d6c54450a..c47841708 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -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 +} diff --git a/env/client.ts b/env/client.ts index 4eafd5592..467100c01 100644 --- a/env/client.ts +++ b/env/client.ts @@ -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`, }, }) diff --git a/lib/graphql/batchRequest.ts b/lib/graphql/batchRequest.ts index b6b5dbe3f..86361d527 100644 --- a/lib/graphql/batchRequest.ts +++ b/lib/graphql/batchRequest.ts @@ -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( queries: (BatchRequestDocument & { options?: RequestInit })[] ): Promise> { diff --git a/next-env.d.ts b/next-env.d.ts index 40c3d6809..4f11a03dc 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // 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. diff --git a/next.config.js b/next.config.js index 222f085ac..34616f754 100644 --- a/next.config.js +++ b/next.config.js @@ -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", + }, ], } }, diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index ae2e14cf4..5c8879c00 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -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(), diff --git a/stores/details.ts b/stores/details.ts index 52e6d41a8..f8698e54c 100644 --- a/stores/details.ts +++ b/stores/details.ts @@ -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 > = JSON.parse(detailsStorageUnparsed) - initialState = merge(initialState, detailsStorage.state.data) + initialState = merge(detailsStorage.state.data, initialState, { + arrayMerge, + }) } } return create()( @@ -140,7 +143,7 @@ export function createDetailsStore( }, }), { - name: storageName, + name: detailsStorageName, onRehydrateStorage() { return function (state) { if (state) { diff --git a/stores/steps.ts b/stores/steps.ts index cf14f6768..efa356c8b 100644 --- a/stores/steps.ts +++ b/stores/steps.ts @@ -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" diff --git a/utils/merge.ts b/utils/merge.ts new file mode 100644 index 000000000..84aaae145 --- /dev/null +++ b/utils/merge.ts @@ -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 +}