Merged in feat/SW-1007-saved-cards-filtering (pull request #980)
Feat/SW-1007 saved payment cards now shown based on supported cards by hotel * fix(SW-1007): refactored savedCards to only show supported payment cards * fix(SW-1007): show error message even if metadata is null * fix: merge changes that were missed * fix: remove use server Approved-by: Christel Westerberg
This commit is contained in:
@@ -42,12 +42,12 @@ export default async function PaymentCallbackPage({
|
|||||||
const bookingStatus = await serverClient().booking.status({
|
const bookingStatus = await serverClient().booking.status({
|
||||||
confirmationNumber,
|
confirmationNumber,
|
||||||
})
|
})
|
||||||
if (bookingStatus.metadata) {
|
searchObject.set(
|
||||||
searchObject.set(
|
"errorCode",
|
||||||
"errorCode",
|
bookingStatus?.metadata?.errorCode
|
||||||
bookingStatus.metadata.errorCode?.toString() ?? ""
|
? bookingStatus.metadata.errorCode.toString()
|
||||||
)
|
: PaymentErrorCodeEnum.Failed.toString()
|
||||||
}
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Suspense } from "react"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getBreakfastPackages,
|
getBreakfastPackages,
|
||||||
getCreditCardsSafely,
|
|
||||||
getHotelData,
|
getHotelData,
|
||||||
getPackages,
|
getPackages,
|
||||||
getProfileSafely,
|
getProfileSafely,
|
||||||
@@ -75,7 +74,6 @@ export default async function StepPage({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void getProfileSafely()
|
void getProfileSafely()
|
||||||
void getCreditCardsSafely()
|
|
||||||
void getBreakfastPackages(breakfastInput)
|
void getBreakfastPackages(breakfastInput)
|
||||||
void getSelectedRoomAvailability(selectedRoomAvailabilityInput)
|
void getSelectedRoomAvailability(selectedRoomAvailabilityInput)
|
||||||
if (packageCodes?.length) {
|
if (packageCodes?.length) {
|
||||||
@@ -110,7 +108,6 @@ export default async function StepPage({
|
|||||||
})
|
})
|
||||||
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
const breakfastPackages = await getBreakfastPackages(breakfastInput)
|
||||||
const user = await getProfileSafely()
|
const user = await getProfileSafely()
|
||||||
const savedCreditCards = await getCreditCardsSafely()
|
|
||||||
|
|
||||||
if (!hotelData || !roomAvailability) {
|
if (!hotelData || !roomAvailability) {
|
||||||
return notFound()
|
return notFound()
|
||||||
@@ -213,7 +210,9 @@ export default async function StepPage({
|
|||||||
hotelData.data.attributes.merchantInformationData
|
hotelData.data.attributes.merchantInformationData
|
||||||
.alternatePaymentOptions
|
.alternatePaymentOptions
|
||||||
}
|
}
|
||||||
savedCreditCards={savedCreditCards}
|
supportedCards={
|
||||||
|
hotelData.data.attributes.merchantInformationData.cards
|
||||||
|
}
|
||||||
mustBeGuaranteed={mustBeGuaranteed}
|
mustBeGuaranteed={mustBeGuaranteed}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation"
|
||||||
|
import { useCallback, useEffect, useState } from "react"
|
||||||
|
import { FormProvider, useForm } from "react-hook-form"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import {
|
||||||
|
BookingStatusEnum,
|
||||||
|
PAYMENT_METHOD_TITLES,
|
||||||
|
PaymentMethodEnum,
|
||||||
|
} from "@/constants/booking"
|
||||||
|
import {
|
||||||
|
bookingTermsAndConditions,
|
||||||
|
privacyPolicy,
|
||||||
|
} from "@/constants/currentWebHrefs"
|
||||||
|
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||||
|
import { env } from "@/env/client"
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
|
||||||
|
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
||||||
|
import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast"
|
||||||
|
import useLang from "@/hooks/useLang"
|
||||||
|
|
||||||
|
import { bedTypeMap } from "../../SelectRate/RoomSelection/utils"
|
||||||
|
import PriceChangeDialog from "../PriceChangeDialog"
|
||||||
|
import GuaranteeDetails from "./GuaranteeDetails"
|
||||||
|
import PaymentOption from "./PaymentOption"
|
||||||
|
import { PaymentFormData, paymentSchema } from "./schema"
|
||||||
|
|
||||||
|
import styles from "./payment.module.css"
|
||||||
|
|
||||||
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import type { PaymentClientProps } from "@/types/components/hotelReservation/selectRate/section"
|
||||||
|
|
||||||
|
const maxRetries = 4
|
||||||
|
const retryInterval = 2000
|
||||||
|
|
||||||
|
export const formId = "submit-booking"
|
||||||
|
|
||||||
|
function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||||
|
return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function PaymentClient({
|
||||||
|
user,
|
||||||
|
roomPrice,
|
||||||
|
otherPaymentOptions,
|
||||||
|
savedCreditCards,
|
||||||
|
mustBeGuaranteed,
|
||||||
|
}: PaymentClientProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
|
const intl = useIntl()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
|
const totalPrice = useEnterDetailsStore((state) => state.totalPrice)
|
||||||
|
const { bedType, booking, breakfast } = useEnterDetailsStore((state) => ({
|
||||||
|
bedType: state.bedType,
|
||||||
|
booking: state.booking,
|
||||||
|
breakfast: state.breakfast,
|
||||||
|
}))
|
||||||
|
const userData = useEnterDetailsStore((state) => state.guest)
|
||||||
|
const setIsSubmittingDisabled = useEnterDetailsStore(
|
||||||
|
(state) => state.actions.setIsSubmittingDisabled
|
||||||
|
)
|
||||||
|
|
||||||
|
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
||||||
|
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||||
|
useState(false)
|
||||||
|
|
||||||
|
const availablePaymentOptions =
|
||||||
|
useAvailablePaymentOptions(otherPaymentOptions)
|
||||||
|
const [priceChangeData, setPriceChangeData] = useState<{
|
||||||
|
oldPrice: number
|
||||||
|
newPrice: number
|
||||||
|
} | null>()
|
||||||
|
|
||||||
|
usePaymentFailedToast()
|
||||||
|
|
||||||
|
const methods = useForm<PaymentFormData>({
|
||||||
|
defaultValues: {
|
||||||
|
paymentMethod: savedCreditCards?.length
|
||||||
|
? savedCreditCards[0].id
|
||||||
|
: PaymentMethodEnum.card,
|
||||||
|
smsConfirmation: false,
|
||||||
|
termsAndConditions: false,
|
||||||
|
},
|
||||||
|
mode: "all",
|
||||||
|
reValidateMode: "onChange",
|
||||||
|
resolver: zodResolver(paymentSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const initiateBooking = trpc.booking.create.useMutation({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result?.confirmationNumber) {
|
||||||
|
setConfirmationNumber(result.confirmationNumber)
|
||||||
|
|
||||||
|
if (result.metadata?.priceChangedMetadata) {
|
||||||
|
setPriceChangeData({
|
||||||
|
oldPrice: roomPrice.publicPrice,
|
||||||
|
newPrice: result.metadata.priceChangedMetadata.totalPrice,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setIsPollingForBookingStatus(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: "payment.error.failed",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error", error)
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: "payment.error.failed",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const priceChange = trpc.booking.priceChange.useMutation({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result?.confirmationNumber) {
|
||||||
|
setIsPollingForBookingStatus(true)
|
||||||
|
} else {
|
||||||
|
toast.error(intl.formatMessage({ id: "payment.error.failed" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
setPriceChangeData(null)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error", error)
|
||||||
|
setPriceChangeData(null)
|
||||||
|
toast.error(intl.formatMessage({ id: "payment.error.failed" }))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const bookingStatus = useHandleBookingStatus({
|
||||||
|
confirmationNumber,
|
||||||
|
expectedStatus: BookingStatusEnum.BookingCompleted,
|
||||||
|
maxRetries,
|
||||||
|
retryInterval,
|
||||||
|
enabled: isPollingForBookingStatus,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bookingStatus?.data?.paymentUrl) {
|
||||||
|
router.push(bookingStatus.data.paymentUrl)
|
||||||
|
} else if (bookingStatus.isTimeout) {
|
||||||
|
toast.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: "payment.error.failed",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [bookingStatus, router, intl])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsSubmittingDisabled(
|
||||||
|
!methods.formState.isValid || methods.formState.isSubmitting
|
||||||
|
)
|
||||||
|
}, [
|
||||||
|
methods.formState.isValid,
|
||||||
|
methods.formState.isSubmitting,
|
||||||
|
setIsSubmittingDisabled,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
(data: PaymentFormData) => {
|
||||||
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
countryCode,
|
||||||
|
membershipNo,
|
||||||
|
join,
|
||||||
|
dateOfBirth,
|
||||||
|
zipCode,
|
||||||
|
} = userData
|
||||||
|
const { toDate, fromDate, rooms, hotel } = booking
|
||||||
|
|
||||||
|
// set payment method to card if saved card is submitted
|
||||||
|
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||||
|
? data.paymentMethod
|
||||||
|
: PaymentMethodEnum.card
|
||||||
|
|
||||||
|
const savedCreditCard = savedCreditCards?.find(
|
||||||
|
(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,
|
||||||
|
checkOutDate: toDate,
|
||||||
|
rooms: rooms.map((room) => ({
|
||||||
|
adults: room.adults,
|
||||||
|
childrenAges: room.children?.map((child) => ({
|
||||||
|
age: child.age,
|
||||||
|
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
||||||
|
})),
|
||||||
|
rateCode:
|
||||||
|
user || join || membershipNo ? room.counterRateCode : room.rateCode,
|
||||||
|
roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
||||||
|
guest: {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
phoneNumber,
|
||||||
|
countryCode,
|
||||||
|
membershipNumber: membershipNo,
|
||||||
|
becomeMember: join,
|
||||||
|
dateOfBirth,
|
||||||
|
postalCode: zipCode,
|
||||||
|
},
|
||||||
|
packages: {
|
||||||
|
breakfast: !!(breakfast && breakfast.code),
|
||||||
|
allergyFriendly:
|
||||||
|
room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ??
|
||||||
|
false,
|
||||||
|
petFriendly:
|
||||||
|
room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false,
|
||||||
|
accessibility:
|
||||||
|
room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ??
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
smsConfirmationRequested: data.smsConfirmation,
|
||||||
|
roomPrice,
|
||||||
|
})),
|
||||||
|
payment: {
|
||||||
|
paymentMethod,
|
||||||
|
card: savedCreditCard
|
||||||
|
? {
|
||||||
|
alias: savedCreditCard.alias,
|
||||||
|
expiryDate: savedCreditCard.expirationDate,
|
||||||
|
cardType: savedCreditCard.cardType,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
|
||||||
|
success: `${paymentRedirectUrl}/success`,
|
||||||
|
error: `${paymentRedirectUrl}/error`,
|
||||||
|
cancel: `${paymentRedirectUrl}/cancel`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
breakfast,
|
||||||
|
bedType,
|
||||||
|
userData,
|
||||||
|
booking,
|
||||||
|
roomPrice,
|
||||||
|
savedCreditCards,
|
||||||
|
lang,
|
||||||
|
user,
|
||||||
|
initiateBooking,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
initiateBooking.isPending ||
|
||||||
|
(isPollingForBookingStatus && !bookingStatus.data?.paymentUrl)
|
||||||
|
) {
|
||||||
|
return <LoadingSpinner />
|
||||||
|
}
|
||||||
|
|
||||||
|
const guaranteeing = intl.formatMessage({ id: "guaranteeing" })
|
||||||
|
const paying = intl.formatMessage({ id: "paying" })
|
||||||
|
const paymentVerb = mustBeGuaranteed ? guaranteeing : paying
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form
|
||||||
|
className={styles.paymentContainer}
|
||||||
|
onSubmit={methods.handleSubmit(handleSubmit)}
|
||||||
|
id={formId}
|
||||||
|
>
|
||||||
|
{mustBeGuaranteed ? (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<Body>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
<GuaranteeDetails />
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
{savedCreditCards?.length ? (
|
||||||
|
<section className={styles.section}>
|
||||||
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "MY SAVED CARDS" })}
|
||||||
|
</Body>
|
||||||
|
<div className={styles.paymentOptionContainer}>
|
||||||
|
{savedCreditCards?.map((savedCreditCard) => (
|
||||||
|
<PaymentOption
|
||||||
|
key={savedCreditCard.id}
|
||||||
|
name="paymentMethod"
|
||||||
|
value={savedCreditCard.id}
|
||||||
|
label={
|
||||||
|
PAYMENT_METHOD_TITLES[
|
||||||
|
savedCreditCard.cardType as PaymentMethodEnum
|
||||||
|
]
|
||||||
|
}
|
||||||
|
cardNumber={savedCreditCard.truncatedNumber}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
<section className={styles.section}>
|
||||||
|
{savedCreditCards?.length ? (
|
||||||
|
<Body color="uiTextHighContrast" textTransform="bold">
|
||||||
|
{intl.formatMessage({ id: "OTHER PAYMENT METHODS" })}
|
||||||
|
</Body>
|
||||||
|
) : null}
|
||||||
|
<div className={styles.paymentOptionContainer}>
|
||||||
|
<PaymentOption
|
||||||
|
name="paymentMethod"
|
||||||
|
value={PaymentMethodEnum.card}
|
||||||
|
label={intl.formatMessage({ id: "Credit card" })}
|
||||||
|
/>
|
||||||
|
{availablePaymentOptions.map((paymentMethod) => (
|
||||||
|
<PaymentOption
|
||||||
|
key={paymentMethod}
|
||||||
|
name="paymentMethod"
|
||||||
|
value={paymentMethod}
|
||||||
|
label={
|
||||||
|
PAYMENT_METHOD_TITLES[paymentMethod as PaymentMethodEnum]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className={styles.section}>
|
||||||
|
<Caption>
|
||||||
|
{intl.formatMessage<React.ReactNode>(
|
||||||
|
{
|
||||||
|
id: "booking.terms",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
paymentVerb,
|
||||||
|
termsLink: (str) => (
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
variant="underscored"
|
||||||
|
href={bookingTermsAndConditions[lang]}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
privacyLink: (str) => (
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
variant="underscored"
|
||||||
|
href={privacyPolicy[lang]}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{str}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
<Checkbox name="termsAndConditions">
|
||||||
|
<Caption>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "I accept the terms and conditions",
|
||||||
|
})}
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox name="smsConfirmation">
|
||||||
|
<Caption>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "I would like to get my booking confirmation via sms",
|
||||||
|
})}
|
||||||
|
</Caption>
|
||||||
|
</Checkbox>
|
||||||
|
</section>
|
||||||
|
<div className={styles.submitButton}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
!methods.formState.isValid || methods.formState.isSubmitting
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Complete booking" })}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
{priceChangeData ? (
|
||||||
|
<PriceChangeDialog
|
||||||
|
isOpen={!!priceChangeData}
|
||||||
|
oldPrice={priceChangeData.oldPrice}
|
||||||
|
newPrice={priceChangeData.newPrice}
|
||||||
|
currency={totalPrice.local.currency}
|
||||||
|
onCancel={() => {
|
||||||
|
const allSearchParams = searchParams.size
|
||||||
|
? `?${searchParams.toString()}`
|
||||||
|
: ""
|
||||||
|
router.push(`${selectRate(lang)}${allSearchParams}`)
|
||||||
|
}}
|
||||||
|
onAccept={() => priceChange.mutate({ confirmationNumber })}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,424 +1,27 @@
|
|||||||
"use client"
|
import { getSavedPaymentCardsSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import PaymentClient from "./PaymentClient"
|
||||||
import { useRouter, useSearchParams } from "next/navigation"
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
|
||||||
import { FormProvider, useForm } from "react-hook-form"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import {
|
import { PaymentProps } from "@/types/components/hotelReservation/selectRate/section"
|
||||||
BookingStatusEnum,
|
|
||||||
PAYMENT_METHOD_TITLES,
|
|
||||||
PaymentMethodEnum,
|
|
||||||
} from "@/constants/booking"
|
|
||||||
import {
|
|
||||||
bookingTermsAndConditions,
|
|
||||||
privacyPolicy,
|
|
||||||
} from "@/constants/currentWebHrefs"
|
|
||||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
|
||||||
import { env } from "@/env/client"
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
|
||||||
|
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
export default async function Payment({
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
|
||||||
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
|
|
||||||
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"
|
|
||||||
import { usePaymentFailedToast } from "@/hooks/booking/usePaymentFailedToast"
|
|
||||||
import useLang from "@/hooks/useLang"
|
|
||||||
|
|
||||||
import { bedTypeMap } from "../../SelectRate/RoomSelection/utils"
|
|
||||||
import PriceChangeDialog from "../PriceChangeDialog"
|
|
||||||
import GuaranteeDetails from "./GuaranteeDetails"
|
|
||||||
import PaymentOption from "./PaymentOption"
|
|
||||||
import { PaymentFormData, paymentSchema } from "./schema"
|
|
||||||
|
|
||||||
import styles from "./payment.module.css"
|
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
|
||||||
import type { PaymentProps } from "@/types/components/hotelReservation/selectRate/section"
|
|
||||||
|
|
||||||
const maxRetries = 4
|
|
||||||
const retryInterval = 2000
|
|
||||||
|
|
||||||
export const formId = "submit-booking"
|
|
||||||
|
|
||||||
function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
|
||||||
return Object.values(PaymentMethodEnum).includes(value as PaymentMethodEnum)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Payment({
|
|
||||||
user,
|
user,
|
||||||
roomPrice,
|
roomPrice,
|
||||||
otherPaymentOptions,
|
otherPaymentOptions,
|
||||||
savedCreditCards,
|
|
||||||
mustBeGuaranteed,
|
mustBeGuaranteed,
|
||||||
|
supportedCards,
|
||||||
}: PaymentProps) {
|
}: PaymentProps) {
|
||||||
const router = useRouter()
|
const savedCreditCards = await getSavedPaymentCardsSafely({
|
||||||
const lang = useLang()
|
supportedCards,
|
||||||
const intl = useIntl()
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const totalPrice = useEnterDetailsStore((state) => state.totalPrice)
|
|
||||||
const { bedType, booking, breakfast } = useEnterDetailsStore((state) => ({
|
|
||||||
bedType: state.bedType,
|
|
||||||
booking: state.booking,
|
|
||||||
breakfast: state.breakfast,
|
|
||||||
}))
|
|
||||||
const userData = useEnterDetailsStore((state) => state.guest)
|
|
||||||
const setIsSubmittingDisabled = useEnterDetailsStore(
|
|
||||||
(state) => state.actions.setIsSubmittingDisabled
|
|
||||||
)
|
|
||||||
|
|
||||||
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
|
||||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
|
||||||
useState(false)
|
|
||||||
|
|
||||||
const availablePaymentOptions =
|
|
||||||
useAvailablePaymentOptions(otherPaymentOptions)
|
|
||||||
const [priceChangeData, setPriceChangeData] = useState<{
|
|
||||||
oldPrice: number
|
|
||||||
newPrice: number
|
|
||||||
} | null>()
|
|
||||||
|
|
||||||
usePaymentFailedToast()
|
|
||||||
|
|
||||||
const methods = useForm<PaymentFormData>({
|
|
||||||
defaultValues: {
|
|
||||||
paymentMethod: savedCreditCards?.length
|
|
||||||
? savedCreditCards[0].id
|
|
||||||
: PaymentMethodEnum.card,
|
|
||||||
smsConfirmation: false,
|
|
||||||
termsAndConditions: false,
|
|
||||||
},
|
|
||||||
mode: "all",
|
|
||||||
reValidateMode: "onChange",
|
|
||||||
resolver: zodResolver(paymentSchema),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const initiateBooking = trpc.booking.create.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
if (result?.confirmationNumber) {
|
|
||||||
setConfirmationNumber(result.confirmationNumber)
|
|
||||||
|
|
||||||
if (result.metadata?.priceChangedMetadata) {
|
|
||||||
setPriceChangeData({
|
|
||||||
oldPrice: roomPrice.publicPrice,
|
|
||||||
newPrice: result.metadata.priceChangedMetadata.totalPrice,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setIsPollingForBookingStatus(true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "payment.error.failed",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Error", error)
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "payment.error.failed",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const priceChange = trpc.booking.priceChange.useMutation({
|
|
||||||
onSuccess: (result) => {
|
|
||||||
if (result?.confirmationNumber) {
|
|
||||||
setIsPollingForBookingStatus(true)
|
|
||||||
} else {
|
|
||||||
toast.error(intl.formatMessage({ id: "payment.error.failed" }))
|
|
||||||
}
|
|
||||||
|
|
||||||
setPriceChangeData(null)
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
console.error("Error", error)
|
|
||||||
setPriceChangeData(null)
|
|
||||||
toast.error(intl.formatMessage({ id: "payment.error.failed" }))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const bookingStatus = useHandleBookingStatus({
|
|
||||||
confirmationNumber,
|
|
||||||
expectedStatus: BookingStatusEnum.BookingCompleted,
|
|
||||||
maxRetries,
|
|
||||||
retryInterval,
|
|
||||||
enabled: isPollingForBookingStatus,
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (bookingStatus?.data?.paymentUrl) {
|
|
||||||
router.push(bookingStatus.data.paymentUrl)
|
|
||||||
} else if (bookingStatus.isTimeout) {
|
|
||||||
toast.error(
|
|
||||||
intl.formatMessage({
|
|
||||||
id: "payment.error.failed",
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}, [bookingStatus, router, intl])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsSubmittingDisabled(
|
|
||||||
!methods.formState.isValid || methods.formState.isSubmitting
|
|
||||||
)
|
|
||||||
}, [
|
|
||||||
methods.formState.isValid,
|
|
||||||
methods.formState.isSubmitting,
|
|
||||||
setIsSubmittingDisabled,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
|
||||||
(data: PaymentFormData) => {
|
|
||||||
const {
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
phoneNumber,
|
|
||||||
countryCode,
|
|
||||||
membershipNo,
|
|
||||||
join,
|
|
||||||
dateOfBirth,
|
|
||||||
zipCode,
|
|
||||||
} = userData
|
|
||||||
const { toDate, fromDate, rooms, hotel } = booking
|
|
||||||
|
|
||||||
// set payment method to card if saved card is submitted
|
|
||||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
|
||||||
? data.paymentMethod
|
|
||||||
: PaymentMethodEnum.card
|
|
||||||
|
|
||||||
const savedCreditCard = savedCreditCards?.find(
|
|
||||||
(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,
|
|
||||||
checkOutDate: toDate,
|
|
||||||
rooms: rooms.map((room) => ({
|
|
||||||
adults: room.adults,
|
|
||||||
childrenAges: room.children?.map((child) => ({
|
|
||||||
age: child.age,
|
|
||||||
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
|
||||||
})),
|
|
||||||
rateCode:
|
|
||||||
user || join || membershipNo ? room.counterRateCode : room.rateCode,
|
|
||||||
roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
|
||||||
guest: {
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
email,
|
|
||||||
phoneNumber,
|
|
||||||
countryCode,
|
|
||||||
membershipNumber: membershipNo,
|
|
||||||
becomeMember: join,
|
|
||||||
dateOfBirth,
|
|
||||||
postalCode: zipCode,
|
|
||||||
},
|
|
||||||
packages: {
|
|
||||||
breakfast: !!(breakfast && breakfast.code),
|
|
||||||
allergyFriendly:
|
|
||||||
room.packages?.includes(RoomPackageCodeEnum.ALLERGY_ROOM) ??
|
|
||||||
false,
|
|
||||||
petFriendly:
|
|
||||||
room.packages?.includes(RoomPackageCodeEnum.PET_ROOM) ?? false,
|
|
||||||
accessibility:
|
|
||||||
room.packages?.includes(RoomPackageCodeEnum.ACCESSIBILITY_ROOM) ??
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
smsConfirmationRequested: data.smsConfirmation,
|
|
||||||
roomPrice,
|
|
||||||
})),
|
|
||||||
payment: {
|
|
||||||
paymentMethod,
|
|
||||||
card: savedCreditCard
|
|
||||||
? {
|
|
||||||
alias: savedCreditCard.alias,
|
|
||||||
expiryDate: savedCreditCard.expirationDate,
|
|
||||||
cardType: savedCreditCard.cardType,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
|
|
||||||
success: `${paymentRedirectUrl}/success`,
|
|
||||||
error: `${paymentRedirectUrl}/error`,
|
|
||||||
cancel: `${paymentRedirectUrl}/cancel`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[
|
|
||||||
breakfast,
|
|
||||||
bedType,
|
|
||||||
userData,
|
|
||||||
booking,
|
|
||||||
roomPrice,
|
|
||||||
savedCreditCards,
|
|
||||||
lang,
|
|
||||||
user,
|
|
||||||
initiateBooking,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
initiateBooking.isPending ||
|
|
||||||
(isPollingForBookingStatus && !bookingStatus.data?.paymentUrl)
|
|
||||||
) {
|
|
||||||
return <LoadingSpinner />
|
|
||||||
}
|
|
||||||
|
|
||||||
const guaranteeing = intl.formatMessage({ id: "guaranteeing" })
|
|
||||||
const paying = intl.formatMessage({ id: "paying" })
|
|
||||||
const paymentVerb = mustBeGuaranteed ? guaranteeing : paying
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PaymentClient
|
||||||
<FormProvider {...methods}>
|
user={user}
|
||||||
<form
|
roomPrice={roomPrice}
|
||||||
className={styles.paymentContainer}
|
otherPaymentOptions={otherPaymentOptions}
|
||||||
onSubmit={methods.handleSubmit(handleSubmit)}
|
savedCreditCards={savedCreditCards}
|
||||||
id={formId}
|
mustBeGuaranteed={mustBeGuaranteed}
|
||||||
>
|
/>
|
||||||
{mustBeGuaranteed ? (
|
|
||||||
<section className={styles.section}>
|
|
||||||
<Body>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
|
|
||||||
})}
|
|
||||||
</Body>
|
|
||||||
<GuaranteeDetails />
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
{savedCreditCards?.length ? (
|
|
||||||
<section className={styles.section}>
|
|
||||||
<Body color="uiTextHighContrast" textTransform="bold">
|
|
||||||
{intl.formatMessage({ id: "MY SAVED CARDS" })}
|
|
||||||
</Body>
|
|
||||||
<div className={styles.paymentOptionContainer}>
|
|
||||||
{savedCreditCards?.map((savedCreditCard) => (
|
|
||||||
<PaymentOption
|
|
||||||
key={savedCreditCard.id}
|
|
||||||
name="paymentMethod"
|
|
||||||
value={savedCreditCard.id}
|
|
||||||
label={
|
|
||||||
PAYMENT_METHOD_TITLES[
|
|
||||||
savedCreditCard.cardType as PaymentMethodEnum
|
|
||||||
]
|
|
||||||
}
|
|
||||||
cardNumber={savedCreditCard.truncatedNumber}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
<section className={styles.section}>
|
|
||||||
{savedCreditCards?.length ? (
|
|
||||||
<Body color="uiTextHighContrast" textTransform="bold">
|
|
||||||
{intl.formatMessage({ id: "OTHER PAYMENT METHODS" })}
|
|
||||||
</Body>
|
|
||||||
) : null}
|
|
||||||
<div className={styles.paymentOptionContainer}>
|
|
||||||
<PaymentOption
|
|
||||||
name="paymentMethod"
|
|
||||||
value={PaymentMethodEnum.card}
|
|
||||||
label={intl.formatMessage({ id: "Credit card" })}
|
|
||||||
/>
|
|
||||||
{availablePaymentOptions.map((paymentMethod) => (
|
|
||||||
<PaymentOption
|
|
||||||
key={paymentMethod}
|
|
||||||
name="paymentMethod"
|
|
||||||
value={paymentMethod}
|
|
||||||
label={
|
|
||||||
PAYMENT_METHOD_TITLES[paymentMethod as PaymentMethodEnum]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className={styles.section}>
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage<React.ReactNode>(
|
|
||||||
{
|
|
||||||
id: "booking.terms",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
paymentVerb,
|
|
||||||
termsLink: (str) => (
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
variant="underscored"
|
|
||||||
href={bookingTermsAndConditions[lang]}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
privacyLink: (str) => (
|
|
||||||
<Link
|
|
||||||
className={styles.link}
|
|
||||||
variant="underscored"
|
|
||||||
href={privacyPolicy[lang]}
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{str}
|
|
||||||
</Link>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Caption>
|
|
||||||
<Checkbox name="termsAndConditions">
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "I accept the terms and conditions",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox name="smsConfirmation">
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "I would like to get my booking confirmation via sms",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
</Checkbox>
|
|
||||||
</section>
|
|
||||||
<div className={styles.submitButton}>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
disabled={
|
|
||||||
!methods.formState.isValid || methods.formState.isSubmitting
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Complete booking" })}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
{priceChangeData ? (
|
|
||||||
<PriceChangeDialog
|
|
||||||
isOpen={!!priceChangeData}
|
|
||||||
oldPrice={priceChangeData.oldPrice}
|
|
||||||
newPrice={priceChangeData.newPrice}
|
|
||||||
currency={totalPrice.local.currency}
|
|
||||||
onCancel={() => {
|
|
||||||
const allSearchParams = searchParams.size
|
|
||||||
? `?${searchParams.toString()}`
|
|
||||||
: ""
|
|
||||||
router.push(`${selectRate(lang)}${allSearchParams}`)
|
|
||||||
}}
|
|
||||||
onAccept={() => priceChange.mutate({ confirmationNumber })}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { formId } from "@/components/HotelReservation/EnterDetails/Payment"
|
import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import type {
|
|||||||
GetSelectedRoomAvailabilityInput,
|
GetSelectedRoomAvailabilityInput,
|
||||||
HotelDataInput,
|
HotelDataInput,
|
||||||
} from "@/server/routers/hotels/input"
|
} from "@/server/routers/hotels/input"
|
||||||
|
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
|
||||||
|
|
||||||
export const getLocations = cache(async function getMemoizedLocations() {
|
export const getLocations = cache(async function getMemoizedLocations() {
|
||||||
return serverClient().hotel.locations.get()
|
return serverClient().hotel.locations.get()
|
||||||
@@ -32,9 +33,11 @@ export const getProfileSafely = cache(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getCreditCardsSafely = cache(
|
export const getSavedPaymentCardsSafely = cache(
|
||||||
async function getMemoizedCreditCardsSafely() {
|
async function getMemoizedSavedPaymentCardsSafely(
|
||||||
return serverClient().user.safeCreditCards()
|
args: GetSavedPaymentCardsInput
|
||||||
|
) {
|
||||||
|
return serverClient().user.safePaymentCards(args)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -369,6 +369,7 @@ const merchantInformationSchema = z.object({
|
|||||||
return Object.entries(val)
|
return Object.entries(val)
|
||||||
.filter(([_, enabled]) => enabled)
|
.filter(([_, enabled]) => enabled)
|
||||||
.map(([key]) => key)
|
.map(([key]) => key)
|
||||||
|
.filter((key): key is PaymentMethodEnum => !!key)
|
||||||
}),
|
}),
|
||||||
alternatePaymentOptions: z
|
alternatePaymentOptions: z
|
||||||
.record(z.string(), z.boolean())
|
.record(z.string(), z.boolean())
|
||||||
|
|||||||
@@ -55,3 +55,11 @@ export const signupInput = signUpSchema
|
|||||||
streetAddress: "",
|
streetAddress: "",
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const getSavedPaymentCardsInput = z.object({
|
||||||
|
supportedCards: z.array(z.string()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GetSavedPaymentCardsInput = z.input<
|
||||||
|
typeof getSavedPaymentCardsInput
|
||||||
|
>
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
|||||||
import * as maskValue from "@/utils/maskValue"
|
import * as maskValue from "@/utils/maskValue"
|
||||||
import { getMembership, getMembershipCards } from "@/utils/user"
|
import { getMembership, getMembershipCards } from "@/utils/user"
|
||||||
|
|
||||||
import { friendTransactionsInput, staysInput } from "./input"
|
import {
|
||||||
|
friendTransactionsInput,
|
||||||
|
getSavedPaymentCardsInput,
|
||||||
|
staysInput,
|
||||||
|
} from "./input"
|
||||||
import {
|
import {
|
||||||
creditCardsSchema,
|
creditCardsSchema,
|
||||||
getFriendTransactionsSchema,
|
getFriendTransactionsSchema,
|
||||||
@@ -752,13 +756,26 @@ export const userQueryRouter = router({
|
|||||||
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
||||||
return await getCreditCards({ session: ctx.session })
|
return await getCreditCards({ session: ctx.session })
|
||||||
}),
|
}),
|
||||||
safeCreditCards: safeProtectedProcedure.query(async function ({ ctx }) {
|
safePaymentCards: safeProtectedProcedure
|
||||||
if (!ctx.session) {
|
.input(getSavedPaymentCardsInput)
|
||||||
return null
|
.query(async function ({ ctx, input }) {
|
||||||
}
|
if (!ctx.session) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return await getCreditCards({ session: ctx.session, onlyNonExpired: true })
|
const savedCards = await getCreditCards({
|
||||||
}),
|
session: ctx.session,
|
||||||
|
onlyNonExpired: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!savedCards) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return savedCards.filter((card) =>
|
||||||
|
input.supportedCards.includes(card.type)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
|
||||||
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
||||||
getProfileCounter.add(1)
|
getProfileCounter.add(1)
|
||||||
|
|||||||
@@ -33,8 +33,13 @@ export interface PaymentProps {
|
|||||||
user: SafeUser
|
user: SafeUser
|
||||||
roomPrice: { publicPrice: number; memberPrice: number | undefined }
|
roomPrice: { publicPrice: number; memberPrice: number | undefined }
|
||||||
otherPaymentOptions: PaymentMethodEnum[]
|
otherPaymentOptions: PaymentMethodEnum[]
|
||||||
savedCreditCards: CreditCard[] | null
|
|
||||||
mustBeGuaranteed: boolean
|
mustBeGuaranteed: boolean
|
||||||
|
supportedCards: PaymentMethodEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentClientProps
|
||||||
|
extends Omit<PaymentProps, "supportedCards"> {
|
||||||
|
savedCreditCards: CreditCard[] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SectionPageProps {
|
export interface SectionPageProps {
|
||||||
|
|||||||
Reference in New Issue
Block a user