Merged in chore/refactor-paymentclient (pull request #3219)
chore: Refactor PaymentClient * Extract function mustGuaranteeBooking * Break apart PaymentClient Approved-by: Joakim Jäderberg
This commit is contained in:
@@ -32,7 +32,6 @@ import { env } from "../../../../env/client"
|
||||
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
|
||||
import { clearBookingWidgetState } from "../../../hooks/useBookingWidgetState"
|
||||
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
|
||||
import { useIsLoggedIn } from "../../../hooks/useIsLoggedIn"
|
||||
import useLang from "../../../hooks/useLang"
|
||||
import { useEnterDetailsStore } from "../../../stores/enter-details"
|
||||
import ConfirmBooking from "../Confirm"
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
hasFlexibleRate,
|
||||
hasPrepaidRate,
|
||||
isPaymentMethodEnum,
|
||||
mustGuaranteeBooking,
|
||||
writePaymentInfoToSessionStorage,
|
||||
} from "./helpers"
|
||||
import { type PaymentFormData, paymentSchema } from "./schema"
|
||||
@@ -51,6 +51,7 @@ import { getPaymentHeadingConfig } from "./utils"
|
||||
|
||||
import styles from "./payment.module.css"
|
||||
|
||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
|
||||
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
|
||||
|
||||
@@ -74,10 +75,13 @@ export default function PaymentClient({
|
||||
const intl = useIntl()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const isUserLoggedIn = useIsLoggedIn()
|
||||
const { getTopOffset } = useStickyPosition({})
|
||||
const { user } = useBookingFlowContext()
|
||||
|
||||
const { user, isLoggedIn } = useBookingFlowContext()
|
||||
const [refId, setRefId] = useState("")
|
||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||
useState(false)
|
||||
const [priceChangeData, setPriceChangeData] =
|
||||
useState<PriceChangeData | null>(null)
|
||||
const [showBookingAlert, setShowBookingAlert] = useState(false)
|
||||
|
||||
const {
|
||||
@@ -96,35 +100,16 @@ export default function PaymentClient({
|
||||
runPreSubmitCallbacks: state.actions.runPreSubmitCallbacks,
|
||||
}))
|
||||
|
||||
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
||||
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (
|
||||
(room.guest.join || room.guest.membershipNo) &&
|
||||
booking.rooms[idx].counterRateCode
|
||||
) {
|
||||
return room.memberMustBeGuaranteed
|
||||
}
|
||||
|
||||
return room.mustBeGuaranteed
|
||||
const bookingMustBeGuaranteed = mustGuaranteeBooking({
|
||||
isUserLoggedIn: isLoggedIn,
|
||||
booking,
|
||||
rooms,
|
||||
})
|
||||
|
||||
const [refId, setRefId] = useState("")
|
||||
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
||||
useState(false)
|
||||
|
||||
const [priceChangeData, setPriceChangeData] =
|
||||
useState<PriceChangeData | null>(null)
|
||||
|
||||
const { toDate, fromDate, hotelId } = booking
|
||||
|
||||
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
||||
const hasFlexRates = rooms.some(hasFlexibleRate)
|
||||
const hasOnlyFlexRates = rooms.every(hasFlexibleRate)
|
||||
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
||||
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
|
||||
|
||||
const methods = useForm<PaymentFormData>({
|
||||
defaultValues: {
|
||||
@@ -171,9 +156,9 @@ export default function PaymentClient({
|
||||
|
||||
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
|
||||
if (hasPriceChange) {
|
||||
const priceChangeData = booking.rooms.map(
|
||||
(room) => room.priceChangedMetadata || null
|
||||
)
|
||||
const priceChangeData = booking.rooms
|
||||
.map((room) => room.priceChangedMetadata || null)
|
||||
.filter(isNotNull)
|
||||
setPriceChangeData(priceChangeData)
|
||||
} else {
|
||||
setIsPollingForBookingStatus(true)
|
||||
@@ -204,19 +189,9 @@ export default function PaymentClient({
|
||||
},
|
||||
})
|
||||
|
||||
const bookingStatus = useHandleBookingStatus({
|
||||
refId,
|
||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
enabled: isPollingForBookingStatus,
|
||||
})
|
||||
|
||||
const handlePaymentError = useCallback(
|
||||
const { toDate, fromDate, hotelId } = booking
|
||||
const trackPaymentError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
setShowBookingAlert(true)
|
||||
setIsSubmitting(false)
|
||||
|
||||
const currentPaymentMethod = methods.getValues("paymentMethod")
|
||||
const smsEnable = methods.getValues("smsConfirmation")
|
||||
const guarantee = methods.getValues("guarantee")
|
||||
@@ -255,52 +230,30 @@ export default function PaymentClient({
|
||||
}
|
||||
},
|
||||
[
|
||||
methods,
|
||||
savedCreditCards,
|
||||
hotelId,
|
||||
bookingMustBeGuaranteed,
|
||||
hasOnlyFlexRates,
|
||||
setIsSubmitting,
|
||||
hotelId,
|
||||
methods,
|
||||
savedCreditCards,
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (bookingStatus?.data?.booking.paymentUrl) {
|
||||
router.push(bookingStatus.data.booking.paymentUrl)
|
||||
} else if (
|
||||
bookingStatus?.data?.booking.reservationStatus ===
|
||||
BookingStatusEnum.BookingCompleted
|
||||
) {
|
||||
const mainRoom = bookingStatus.data.booking.rooms[0]
|
||||
clearBookingWidgetState()
|
||||
// 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) {
|
||||
handlePaymentError("Timeout")
|
||||
}
|
||||
}, [
|
||||
bookingStatus.data,
|
||||
bookingStatus.isTimeout,
|
||||
router,
|
||||
intl,
|
||||
lang,
|
||||
handlePaymentError,
|
||||
])
|
||||
const handlePaymentError = useCallback(
|
||||
(errorMessage: string) => {
|
||||
setShowBookingAlert(true)
|
||||
setIsSubmitting(false)
|
||||
|
||||
const getPaymentMethod = useCallback(
|
||||
(paymentMethod: string | null | undefined): PaymentMethodEnum => {
|
||||
if (hasFlexRates) {
|
||||
return PaymentMethodEnum.card
|
||||
}
|
||||
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
||||
? paymentMethod
|
||||
: PaymentMethodEnum.card
|
||||
trackPaymentError(errorMessage)
|
||||
},
|
||||
[hasFlexRates]
|
||||
[setIsSubmitting, trackPaymentError]
|
||||
)
|
||||
|
||||
useBookingStatusRedirect({
|
||||
refId,
|
||||
enabled: isPollingForBookingStatus,
|
||||
onError: handlePaymentError,
|
||||
})
|
||||
|
||||
const scrollToInvalidField = useCallback(async (): Promise<boolean> => {
|
||||
// If any room is not complete/valid, scroll to the first invalid field, this is needed as rooms and other fields are in separate forms
|
||||
|
||||
@@ -308,20 +261,12 @@ export default function PaymentClient({
|
||||
const errorNames = Object.keys(methods.formState.errors)
|
||||
const firstIncompleteRoomIndex = rooms.findIndex((room) => !room.isComplete)
|
||||
|
||||
const scrollToElement = (el: HTMLElement) => {
|
||||
const offset = getTopOffset()
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - offset - 20
|
||||
window.scrollTo({ top, behavior: "smooth" })
|
||||
const input = el.querySelector<HTMLElement>("input")
|
||||
input?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
if (invalidField) {
|
||||
scrollToElement(invalidField)
|
||||
scrollToElement(invalidField, getTopOffset())
|
||||
} else if (errorNames.length > 0) {
|
||||
const firstErrorEl = document.querySelector(`[name="${errorNames[0]}"]`)
|
||||
if (firstErrorEl) {
|
||||
scrollToElement(firstErrorEl as HTMLElement)
|
||||
scrollToElement(firstErrorEl as HTMLElement, getTopOffset())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,63 +283,32 @@ export default function PaymentClient({
|
||||
return
|
||||
}
|
||||
|
||||
const paymentMethod = getPaymentMethod(data.paymentMethod)
|
||||
const paymentMethod = getPaymentMethod(data.paymentMethod, hasFlexRates)
|
||||
|
||||
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`
|
||||
const guarantee = data.guarantee
|
||||
const useSavedCard = savedCreditCard
|
||||
? {
|
||||
card: {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
},
|
||||
}
|
||||
: {}
|
||||
|
||||
const shouldUsePayment =
|
||||
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
||||
const payment = shouldUsePayment
|
||||
? {
|
||||
paymentMethod: paymentMethod,
|
||||
...useSavedCard,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
}
|
||||
? getPaymentData({ paymentMethod, savedCreditCard, lang })
|
||||
: undefined
|
||||
|
||||
const paymentMethodType = savedCreditCard
|
||||
? savedCreditCard.type
|
||||
: paymentMethod
|
||||
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
||||
const lateArrivalGuarantee = guarantee ? "yes" : "mandatory"
|
||||
writeGlaToSessionStorage(
|
||||
lateArrivalGuarantee,
|
||||
hotelId,
|
||||
paymentMethodType,
|
||||
!!savedCreditCard
|
||||
)
|
||||
trackGlaSaveCardAttempt({
|
||||
hotelId,
|
||||
hasSavedCreditCard: !!savedCreditCard,
|
||||
creditCardType: savedCreditCard?.cardType,
|
||||
lateArrivalGuarantee,
|
||||
})
|
||||
} else if (!hasOnlyFlexRates) {
|
||||
trackPaymentEvent({
|
||||
event: "paymentAttemptStart",
|
||||
hotelId,
|
||||
method: paymentMethodType,
|
||||
isSavedCreditCard: !!savedCreditCard,
|
||||
smsEnable: data.smsConfirmation,
|
||||
status: "attempt",
|
||||
})
|
||||
}
|
||||
writePaymentInfoToSessionStorage(paymentMethodType, !!savedCreditCard)
|
||||
trackPaymentEvents({
|
||||
isSavedCreditCard: !!savedCreditCard,
|
||||
paymentMethodType,
|
||||
guarantee,
|
||||
smsEnable: data.smsConfirmation,
|
||||
bookingMustBeGuaranteed,
|
||||
hasOnlyFlexRates,
|
||||
hotelId,
|
||||
})
|
||||
|
||||
const payload: CreateBookingInput = {
|
||||
checkInDate: fromDate,
|
||||
@@ -406,7 +320,7 @@ export default function PaymentClient({
|
||||
({ room }, idx): CreateBookingInput["rooms"][number] => {
|
||||
const isMainRoom = idx === 0
|
||||
let rateCode = ""
|
||||
if (isMainRoom && isUserLoggedIn) {
|
||||
if (isMainRoom && isLoggedIn) {
|
||||
rateCode = booking.rooms[idx].rateCode
|
||||
} else if (
|
||||
(room.guest.join || room.guest.membershipNo) &&
|
||||
@@ -500,17 +414,17 @@ export default function PaymentClient({
|
||||
[
|
||||
setIsSubmitting,
|
||||
scrollToInvalidField,
|
||||
getPaymentMethod,
|
||||
hasFlexRates,
|
||||
savedCreditCards,
|
||||
lang,
|
||||
bookingMustBeGuaranteed,
|
||||
hasOnlyFlexRates,
|
||||
lang,
|
||||
fromDate,
|
||||
toDate,
|
||||
hotelId,
|
||||
rooms,
|
||||
initiateBooking,
|
||||
isUserLoggedIn,
|
||||
isLoggedIn,
|
||||
booking.rooms,
|
||||
user?.data?.partnerLoyaltyNumber,
|
||||
]
|
||||
@@ -526,6 +440,7 @@ export default function PaymentClient({
|
||||
const { preHeading, heading, subHeading, showLearnMore } =
|
||||
getPaymentHeadingConfig(intl, bookingMustBeGuaranteed, hasOnlyFlexRates)
|
||||
|
||||
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
|
||||
return (
|
||||
<section
|
||||
className={cx(styles.paymentSection, {
|
||||
@@ -599,3 +514,148 @@ export default function PaymentClient({
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const scrollToElement = (el: HTMLElement, offset: number) => {
|
||||
const top = el.getBoundingClientRect().top + window.scrollY - offset - 20
|
||||
window.scrollTo({ top, behavior: "smooth" })
|
||||
const input = el.querySelector<HTMLElement>("input")
|
||||
input?.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
const getPaymentMethod = (
|
||||
paymentMethod: string | null | undefined,
|
||||
hasFlexRates: boolean
|
||||
): PaymentMethodEnum => {
|
||||
if (hasFlexRates) {
|
||||
return PaymentMethodEnum.card
|
||||
}
|
||||
return paymentMethod && isPaymentMethodEnum(paymentMethod)
|
||||
? paymentMethod
|
||||
: PaymentMethodEnum.card
|
||||
}
|
||||
|
||||
function createPaymentCallbackUrl(lang: Lang) {
|
||||
return `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
||||
}
|
||||
|
||||
function useBookingStatusRedirect({
|
||||
refId,
|
||||
enabled,
|
||||
onError,
|
||||
}: {
|
||||
refId: string
|
||||
enabled: boolean
|
||||
onError: (errorMessage: string) => void
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const bookingStatus = useHandleBookingStatus({
|
||||
refId,
|
||||
expectedStatuses: [BookingStatusEnum.BookingCompleted],
|
||||
maxRetries,
|
||||
retryInterval,
|
||||
enabled,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (bookingStatus?.data?.booking.paymentUrl) {
|
||||
router.push(bookingStatus.data.booking.paymentUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
bookingStatus?.data?.booking.reservationStatus ===
|
||||
BookingStatusEnum.BookingCompleted
|
||||
) {
|
||||
const mainRoom = bookingStatus.data.booking.rooms[0]
|
||||
clearBookingWidgetState()
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
||||
if (bookingStatus.isTimeout) {
|
||||
onError("Timeout")
|
||||
}
|
||||
}, [bookingStatus.data, bookingStatus.isTimeout, router, intl, lang, onError])
|
||||
}
|
||||
|
||||
function getPaymentData({
|
||||
paymentMethod,
|
||||
savedCreditCard,
|
||||
lang,
|
||||
}: {
|
||||
paymentMethod: PaymentMethodEnum
|
||||
savedCreditCard?: CreditCard
|
||||
lang: Lang
|
||||
}) {
|
||||
const paymentRedirectUrl = createPaymentCallbackUrl(lang)
|
||||
|
||||
return {
|
||||
paymentMethod: paymentMethod,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
card: savedCreditCard
|
||||
? {
|
||||
alias: savedCreditCard.alias,
|
||||
expiryDate: savedCreditCard.expirationDate,
|
||||
cardType: savedCreditCard.cardType,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function isNotNull<T>(value: T | null): value is T {
|
||||
return value !== null
|
||||
}
|
||||
|
||||
function trackPaymentEvents(data: {
|
||||
isSavedCreditCard: boolean
|
||||
paymentMethodType: string
|
||||
guarantee: boolean
|
||||
smsEnable: boolean
|
||||
bookingMustBeGuaranteed: boolean
|
||||
hasOnlyFlexRates: boolean
|
||||
hotelId: string
|
||||
}) {
|
||||
const {
|
||||
isSavedCreditCard,
|
||||
paymentMethodType,
|
||||
guarantee,
|
||||
smsEnable,
|
||||
bookingMustBeGuaranteed,
|
||||
hasOnlyFlexRates,
|
||||
hotelId,
|
||||
} = data
|
||||
|
||||
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
||||
const lateArrivalGuarantee = guarantee ? "yes" : "mandatory"
|
||||
writeGlaToSessionStorage(
|
||||
lateArrivalGuarantee,
|
||||
hotelId,
|
||||
paymentMethodType,
|
||||
isSavedCreditCard
|
||||
)
|
||||
trackGlaSaveCardAttempt({
|
||||
hotelId,
|
||||
hasSavedCreditCard: isSavedCreditCard,
|
||||
creditCardType: isSavedCreditCard ? paymentMethodType : undefined,
|
||||
lateArrivalGuarantee,
|
||||
})
|
||||
} else if (!hasOnlyFlexRates) {
|
||||
trackPaymentEvent({
|
||||
event: "paymentAttemptStart",
|
||||
hotelId,
|
||||
method: paymentMethodType,
|
||||
isSavedCreditCard,
|
||||
smsEnable,
|
||||
status: "attempt",
|
||||
})
|
||||
}
|
||||
writePaymentInfoToSessionStorage(paymentMethodType, isSavedCreditCard)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user