fix(BOOK-659): use unique ids for multiroom booking input fields * fix(BOOK-659): use unique ids for multiroom booking input fields Approved-by: Bianca Widstam Approved-by: Linus Flood
630 lines
20 KiB
TypeScript
630 lines
20 KiB
TypeScript
"use client"
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
|
import { cx } from "class-variance-authority"
|
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
|
import { useCallback, useEffect, useState } from "react"
|
|
import { FormProvider, useForm } from "react-hook-form"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
|
|
import {
|
|
bookingConfirmation,
|
|
selectRate,
|
|
} from "@scandic-hotels/common/constants/routes/hotelReservation"
|
|
import useStickyPosition from "@scandic-hotels/common/hooks/useStickyPosition"
|
|
import { logger } from "@scandic-hotels/common/logger"
|
|
import { formatPhoneNumber } from "@scandic-hotels/common/utils/phone"
|
|
import { Button } from "@scandic-hotels/design-system/Button"
|
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
|
import { trackEvent } from "@scandic-hotels/tracking/base"
|
|
import {
|
|
trackGlaSaveCardAttempt,
|
|
trackPaymentEvent,
|
|
} from "@scandic-hotels/tracking/payment"
|
|
import { trpc } from "@scandic-hotels/trpc/client"
|
|
import { bedTypeMap } from "@scandic-hotels/trpc/constants/bedTypeMap"
|
|
import { SEARCH_TYPE_REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
|
|
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
|
|
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
|
|
|
import { useBookingFlowConfig } from "../../../bookingFlowConfig/bookingFlowConfigContext"
|
|
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
|
|
import { clearBookingWidgetState } from "../../../hooks/useBookingWidgetState"
|
|
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
|
|
import useLang from "../../../hooks/useLang"
|
|
import { useEnterDetailsStore } from "../../../stores/enter-details"
|
|
import ConfirmBooking from "../Confirm"
|
|
import PriceChangeDialog from "../PriceChangeDialog"
|
|
import { writeGlaToSessionStorage } from "./PaymentCallback/helpers"
|
|
import BookingAlert from "./BookingAlert"
|
|
import { GuaranteeInfo } from "./GuaranteeInfo"
|
|
import {
|
|
getPaymentData,
|
|
getPaymentMethod,
|
|
hasFlexibleRate,
|
|
hasPrepaidRate,
|
|
mustGuaranteeBooking,
|
|
writePaymentInfoToSessionStorage,
|
|
} from "./helpers"
|
|
import { type PaymentFormData, paymentSchema } from "./schema"
|
|
import { getPaymentHeadingConfig } from "./utils"
|
|
|
|
import styles from "./payment.module.css"
|
|
|
|
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
|
|
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
|
|
|
|
import type { PriceChangeData } from "../PriceChangeData"
|
|
|
|
const maxRetries = 15
|
|
const retryInterval = 2000
|
|
|
|
type PaymentClientProps = {
|
|
otherPaymentOptions: PaymentMethodEnum[]
|
|
savedCreditCards: CreditCard[] | null
|
|
}
|
|
|
|
export const formId = "submit-booking"
|
|
export default function PaymentClient({
|
|
otherPaymentOptions,
|
|
savedCreditCards,
|
|
}: PaymentClientProps) {
|
|
const router = useRouter()
|
|
const lang = useLang()
|
|
const intl = useIntl()
|
|
const pathname = usePathname()
|
|
const searchParams = useSearchParams()
|
|
const { getTopOffset } = useStickyPosition({})
|
|
const { user, isLoggedIn } = useBookingFlowContext()
|
|
const { redemptionType } = useBookingFlowConfig()
|
|
const [refId, setRefId] = useState("")
|
|
const [isPollingForBookingStatus, setIsPollingForBookingStatus] =
|
|
useState(false)
|
|
const [priceChangeData, setPriceChangeData] =
|
|
useState<PriceChangeData | null>(null)
|
|
const [showBookingAlert, setShowBookingAlert] = useState(false)
|
|
|
|
const {
|
|
booking,
|
|
rooms,
|
|
totalPrice,
|
|
isSubmitting,
|
|
setIsSubmitting,
|
|
runPreSubmitCallbacks,
|
|
} = useEnterDetailsStore((state) => ({
|
|
booking: state.booking,
|
|
rooms: state.rooms,
|
|
totalPrice: state.totalPrice,
|
|
isSubmitting: state.isSubmitting,
|
|
setIsSubmitting: state.actions.setIsSubmitting,
|
|
runPreSubmitCallbacks: state.actions.runPreSubmitCallbacks,
|
|
}))
|
|
|
|
const bookingMustBeGuaranteed = mustGuaranteeBooking({
|
|
isUserLoggedIn: isLoggedIn,
|
|
booking,
|
|
rooms,
|
|
})
|
|
|
|
const hasPrepaidRates = rooms.some(hasPrepaidRate)
|
|
const hasFlexRates = rooms.some(hasFlexibleRate)
|
|
const hasOnlyFlexRates = rooms.every(hasFlexibleRate)
|
|
const hasMixedRates = hasPrepaidRates && hasFlexRates
|
|
|
|
const methods = useForm<PaymentFormData>({
|
|
defaultValues: {
|
|
paymentMethod: savedCreditCards?.length
|
|
? savedCreditCards[0].id
|
|
: PaymentMethodEnum.card,
|
|
smsConfirmation: false,
|
|
termsAndConditions: false,
|
|
guarantee: false,
|
|
},
|
|
mode: "all",
|
|
reValidateMode: "onChange",
|
|
resolver: zodResolver(paymentSchema),
|
|
})
|
|
|
|
const initiateBooking = trpc.booking.create.useMutation({
|
|
onSuccess: (result) => {
|
|
if (result) {
|
|
if ("error" in result) {
|
|
const queryParams = new URLSearchParams(searchParams.toString())
|
|
queryParams.set("errorCode", result.cause)
|
|
window.history.replaceState(
|
|
{},
|
|
"",
|
|
`${pathname}?${queryParams.toString()}`
|
|
)
|
|
handlePaymentError(result.cause)
|
|
return
|
|
}
|
|
|
|
const { booking } = result
|
|
const mainRoom = booking.rooms[0]
|
|
|
|
if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) {
|
|
clearBookingWidgetState()
|
|
// Cookie is used by Booking Confirmation page to validate that the user came from payment callback
|
|
// eslint-disable-next-line react-hooks/immutability
|
|
document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
|
|
const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
|
|
router.push(confirmationUrl)
|
|
return
|
|
}
|
|
|
|
setRefId(mainRoom.refId)
|
|
|
|
const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
|
|
if (hasPriceChange) {
|
|
const priceChangeData = booking.rooms
|
|
.map((room) => room.priceChangedMetadata || null)
|
|
.filter(isNotNull)
|
|
setPriceChangeData(priceChangeData)
|
|
} else {
|
|
setIsPollingForBookingStatus(true)
|
|
}
|
|
} else {
|
|
handlePaymentError("No confirmation number")
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
logger.error("Booking error", error)
|
|
handlePaymentError(error.message)
|
|
},
|
|
})
|
|
|
|
const priceChange = trpc.booking.priceChange.useMutation({
|
|
onSuccess: (result) => {
|
|
if (result?.id) {
|
|
setIsPollingForBookingStatus(true)
|
|
} else {
|
|
handlePaymentError("No confirmation number")
|
|
}
|
|
setPriceChangeData(null)
|
|
},
|
|
onError: (error) => {
|
|
logger.error("Price change error", error)
|
|
setPriceChangeData(null)
|
|
handlePaymentError(error.message)
|
|
},
|
|
})
|
|
|
|
const { toDate, fromDate, hotelId } = booking
|
|
const trackPaymentError = useCallback(
|
|
(errorMessage: string) => {
|
|
const currentPaymentMethod = methods.getValues("paymentMethod")
|
|
const smsEnable = methods.getValues("smsConfirmation")
|
|
const guarantee = methods.getValues("guarantee")
|
|
const savedCreditCard = savedCreditCards?.find(
|
|
(card) => card.id === currentPaymentMethod
|
|
)
|
|
|
|
const isSavedCreditCard = !!savedCreditCard
|
|
|
|
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
|
const lateArrivalGuarantee = guarantee ? "yes" : "mandatory"
|
|
trackEvent({
|
|
event: "glaCardSaveFailed",
|
|
hotelInfo: {
|
|
hotelId,
|
|
lateArrivalGuarantee,
|
|
guaranteedProduct: "room",
|
|
},
|
|
paymentInfo: {
|
|
isSavedCreditCard,
|
|
hotelId,
|
|
status: "glacardsavefailed",
|
|
type: savedCreditCard ? savedCreditCard.type : currentPaymentMethod,
|
|
},
|
|
})
|
|
} else {
|
|
trackPaymentEvent({
|
|
event: "paymentFail",
|
|
hotelId,
|
|
method: savedCreditCard ? savedCreditCard.type : currentPaymentMethod,
|
|
isSavedCreditCard,
|
|
smsEnable,
|
|
errorMessage,
|
|
status: "failed",
|
|
})
|
|
}
|
|
},
|
|
[
|
|
bookingMustBeGuaranteed,
|
|
hasOnlyFlexRates,
|
|
hotelId,
|
|
methods,
|
|
savedCreditCards,
|
|
]
|
|
)
|
|
|
|
const handlePaymentError = useCallback(
|
|
(errorMessage: string) => {
|
|
setShowBookingAlert(true)
|
|
setIsSubmitting(false)
|
|
|
|
trackPaymentError(errorMessage)
|
|
},
|
|
[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
|
|
|
|
const invalidField = await runPreSubmitCallbacks()
|
|
const errorNames = Object.keys(methods.formState.errors)
|
|
const firstIncompleteRoomIndex = rooms.findIndex((room) => !room.isComplete)
|
|
|
|
if (invalidField) {
|
|
scrollToElement(invalidField, getTopOffset())
|
|
} else if (errorNames.length > 0) {
|
|
const firstErrorEl = document.querySelector(`[name="${errorNames[0]}"]`)
|
|
if (firstErrorEl) {
|
|
scrollToElement(firstErrorEl as HTMLElement, getTopOffset())
|
|
}
|
|
}
|
|
|
|
return firstIncompleteRoomIndex !== -1
|
|
}, [runPreSubmitCallbacks, rooms, methods.formState.errors, getTopOffset])
|
|
|
|
const isRedemptionBooking = booking.searchType === SEARCH_TYPE_REDEMPTION
|
|
const handleSubmit = useCallback(
|
|
async (data: PaymentFormData) => {
|
|
setIsSubmitting(true)
|
|
|
|
const isRoomInvalid = await scrollToInvalidField()
|
|
if (isRoomInvalid) {
|
|
setIsSubmitting(false)
|
|
return
|
|
}
|
|
|
|
const savedCreditCard = savedCreditCards?.find(
|
|
(card) => card.id === data.paymentMethod
|
|
)
|
|
|
|
const guarantee = data.guarantee
|
|
const paymentMethod = getPaymentMethod({
|
|
paymentMethod: data.paymentMethod,
|
|
hasFlexRates,
|
|
isRedemptionBooking,
|
|
redemptionType,
|
|
})
|
|
const payment = getPaymentData({
|
|
guarantee,
|
|
bookingMustBeGuaranteed,
|
|
hasOnlyFlexRates,
|
|
paymentMethod,
|
|
savedCreditCard,
|
|
isRedemptionBooking,
|
|
lang,
|
|
})
|
|
|
|
const paymentMethodType = savedCreditCard
|
|
? savedCreditCard.type
|
|
: paymentMethod
|
|
trackPaymentEvents({
|
|
isSavedCreditCard: !!savedCreditCard,
|
|
paymentMethodType,
|
|
guarantee,
|
|
smsEnable: data.smsConfirmation,
|
|
bookingMustBeGuaranteed,
|
|
hasOnlyFlexRates,
|
|
hotelId,
|
|
})
|
|
|
|
const payload: CreateBookingInput = {
|
|
checkInDate: fromDate,
|
|
checkOutDate: toDate,
|
|
hotelId,
|
|
language: lang,
|
|
payment: payment ?? undefined,
|
|
rooms: rooms.map(
|
|
({ room }, idx): CreateBookingInput["rooms"][number] => {
|
|
const isMainRoom = idx === 0
|
|
let rateCode = ""
|
|
if (isMainRoom && isLoggedIn) {
|
|
rateCode = booking.rooms[idx].rateCode
|
|
} else if (
|
|
(room.guest.join || room.guest.membershipNo) &&
|
|
booking.rooms[idx].counterRateCode
|
|
) {
|
|
rateCode = booking.rooms[idx].counterRateCode
|
|
} else {
|
|
rateCode = booking.rooms[idx].rateCode
|
|
}
|
|
|
|
const phoneNumber = formatPhoneNumber(
|
|
room.guest.phoneNumber,
|
|
room.guest.phoneNumberCC
|
|
)
|
|
|
|
const guest: CreateBookingInput["rooms"][number]["guest"] = {
|
|
becomeMember: room.guest.join,
|
|
countryCode: room.guest.countryCode,
|
|
email: room.guest.email,
|
|
firstName: room.guest.firstName,
|
|
lastName: room.guest.lastName,
|
|
membershipNumber: room.guest.membershipNo,
|
|
phoneNumber,
|
|
partnerLoyaltyNumber: null,
|
|
}
|
|
|
|
if (isMainRoom) {
|
|
// Only valid for main room
|
|
guest.partnerLoyaltyNumber =
|
|
user?.data?.partnerLoyaltyNumber || null
|
|
guest.dateOfBirth =
|
|
"dateOfBirth" in room.guest && room.guest.dateOfBirth
|
|
? room.guest.dateOfBirth
|
|
: undefined
|
|
guest.postalCode =
|
|
"zipCode" in room.guest && room.guest.zipCode
|
|
? room.guest.zipCode
|
|
: undefined
|
|
}
|
|
|
|
const packages: CreateBookingInput["rooms"][number]["packages"] = {
|
|
accessibility:
|
|
room.roomFeatures?.some(
|
|
(feature) =>
|
|
feature.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
|
) ?? false,
|
|
allergyFriendly:
|
|
room.roomFeatures?.some(
|
|
(feature) => feature.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
|
) ?? false,
|
|
breakfast: !!(room.breakfast && room.breakfast.code),
|
|
petFriendly:
|
|
room.roomFeatures?.some(
|
|
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
|
) ?? false,
|
|
}
|
|
|
|
return {
|
|
adults: room.adults,
|
|
bookingCode: room.roomRate.bookingCode,
|
|
childrenAges: room.childrenInRoom?.map((child) => ({
|
|
age: child.age,
|
|
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
|
})),
|
|
guest,
|
|
packages,
|
|
rateCode,
|
|
roomPrice: {
|
|
memberPrice:
|
|
"member" in room.roomRate
|
|
? room.roomRate.member?.localPrice.pricePerStay
|
|
: undefined,
|
|
publicPrice:
|
|
"public" in room.roomRate
|
|
? room.roomRate.public?.localPrice.pricePerStay
|
|
: undefined,
|
|
},
|
|
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
|
smsConfirmationRequested: data.smsConfirmation,
|
|
specialRequest: {
|
|
comment: room.specialRequest.comment
|
|
? room.specialRequest.comment
|
|
: undefined,
|
|
},
|
|
}
|
|
}
|
|
),
|
|
}
|
|
initiateBooking.mutate(payload)
|
|
},
|
|
[
|
|
setIsSubmitting,
|
|
scrollToInvalidField,
|
|
savedCreditCards,
|
|
hasFlexRates,
|
|
redemptionType,
|
|
bookingMustBeGuaranteed,
|
|
hasOnlyFlexRates,
|
|
lang,
|
|
isRedemptionBooking,
|
|
hotelId,
|
|
fromDate,
|
|
toDate,
|
|
rooms,
|
|
initiateBooking,
|
|
isLoggedIn,
|
|
booking.rooms,
|
|
user?.data?.partnerLoyaltyNumber,
|
|
]
|
|
)
|
|
|
|
const handleInvalidSubmit = async () => {
|
|
const valid = await methods.trigger()
|
|
if (!valid) {
|
|
await scrollToInvalidField()
|
|
}
|
|
}
|
|
|
|
const { preHeading, heading, subHeading, showLearnMore } =
|
|
getPaymentHeadingConfig(intl, bookingMustBeGuaranteed, hasOnlyFlexRates)
|
|
|
|
return (
|
|
<section
|
|
className={cx(styles.paymentSection, {
|
|
[styles.isSubmitting]: isSubmitting,
|
|
})}
|
|
>
|
|
<header className={styles.header}>
|
|
<div>
|
|
{preHeading ? (
|
|
<Typography variant="Title/Overline/sm">
|
|
<p>{preHeading}</p>
|
|
</Typography>
|
|
) : null}
|
|
<Typography variant="Title/Subtitle/md">
|
|
<h2>{heading}</h2>
|
|
</Typography>
|
|
{subHeading ? (
|
|
<Typography variant="Body/Paragraph/mdBold">
|
|
<p>{subHeading}</p>
|
|
</Typography>
|
|
) : null}
|
|
</div>
|
|
{showLearnMore ? <GuaranteeInfo /> : null}
|
|
</header>
|
|
<BookingAlert isVisible={showBookingAlert} />
|
|
<FormProvider {...methods}>
|
|
<form
|
|
className={styles.paymentForm}
|
|
onSubmit={methods.handleSubmit(handleSubmit, handleInvalidSubmit)}
|
|
id={formId}
|
|
>
|
|
<ConfirmBooking
|
|
savedCreditCards={savedCreditCards}
|
|
otherPaymentOptions={otherPaymentOptions}
|
|
hasOnlyFlexRates={hasOnlyFlexRates}
|
|
hasMixedRates={hasMixedRates}
|
|
isRedemptionBooking={isRedemptionBooking}
|
|
bookingMustBeGuaranteed={bookingMustBeGuaranteed}
|
|
/>
|
|
|
|
<Button
|
|
className={styles.submitButton}
|
|
type="submit"
|
|
isDisabled={isSubmitting}
|
|
isPending={isSubmitting}
|
|
size="md"
|
|
>
|
|
{intl.formatMessage({
|
|
id: "enterDetails.completeBooking",
|
|
defaultMessage: "Complete booking",
|
|
})}
|
|
</Button>
|
|
</form>
|
|
</FormProvider>
|
|
{priceChangeData ? (
|
|
<PriceChangeDialog
|
|
isOpen={!!priceChangeData}
|
|
priceChangeData={priceChangeData}
|
|
prevTotalPrice={totalPrice.local.price}
|
|
currency={totalPrice.local.currency}
|
|
onCancel={() => {
|
|
const allSearchParams = searchParams.size
|
|
? `?${searchParams.toString()}`
|
|
: ""
|
|
router.push(`${selectRate(lang)}${allSearchParams}`)
|
|
}}
|
|
onAccept={() => priceChange.mutate({ refId })}
|
|
/>
|
|
) : null}
|
|
</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 })
|
|
}
|
|
|
|
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 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)
|
|
}
|