feat(SW-1255): Added loading state to submit button in enter details
This commit is contained in:
committed by
Simon Emanuelsson
parent
89468bc37f
commit
f56a1ece0f
@@ -8,6 +8,7 @@ import { FormProvider, useForm } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BOOKING_CONFIRMATION_NUMBER,
|
BOOKING_CONFIRMATION_NUMBER,
|
||||||
@@ -25,8 +26,6 @@ import { trpc } from "@/lib/trpc/client"
|
|||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import PaymentOption from "@/components/HotelReservation/PaymentOption"
|
import PaymentOption from "@/components/HotelReservation/PaymentOption"
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
|
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
|
||||||
@@ -76,14 +75,18 @@ export default function PaymentClient({
|
|||||||
|
|
||||||
const [showBookingAlert, setShowBookingAlert] = useState(false)
|
const [showBookingAlert, setShowBookingAlert] = useState(false)
|
||||||
|
|
||||||
const { booking, rooms, totalPrice, preSubmitCallbacks } =
|
const { booking, rooms, totalPrice, isSubmitting, preSubmitCallbacks, setIsSubmitting } =
|
||||||
useEnterDetailsStore((state) => ({
|
useEnterDetailsStore((state) => ({
|
||||||
booking: state.booking,
|
booking: state.booking,
|
||||||
rooms: state.rooms,
|
rooms: state.rooms,
|
||||||
totalPrice: state.totalPrice,
|
totalPrice: state.totalPrice,
|
||||||
preSubmitCallbacks: state.preSubmitCallbacks,
|
preSubmitCallbacks: state.preSubmitCallbacks,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
setIsSubmitting: state.actions.setIsSubmitting,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
const allRoomsComplete = rooms.every((r) => r.isComplete)
|
||||||
|
|
||||||
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
|
||||||
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
|
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
|
||||||
return true
|
return true
|
||||||
@@ -202,6 +205,7 @@ export default function PaymentClient({
|
|||||||
const handlePaymentError = useCallback(
|
const handlePaymentError = useCallback(
|
||||||
(errorMessage: string) => {
|
(errorMessage: string) => {
|
||||||
setShowBookingAlert(true)
|
setShowBookingAlert(true)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
|
||||||
const currentPaymentMethod = methods.getValues("paymentMethod")
|
const currentPaymentMethod = methods.getValues("paymentMethod")
|
||||||
const smsEnable = methods.getValues("smsConfirmation")
|
const smsEnable = methods.getValues("smsConfirmation")
|
||||||
@@ -243,6 +247,7 @@ export default function PaymentClient({
|
|||||||
hotelId,
|
hotelId,
|
||||||
bookingMustBeGuaranteed,
|
bookingMustBeGuaranteed,
|
||||||
hasOnlyFlexRates,
|
hasOnlyFlexRates,
|
||||||
|
setIsSubmitting,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -284,6 +289,8 @@ export default function PaymentClient({
|
|||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(data: PaymentFormData) => {
|
(data: PaymentFormData) => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
Object.values(preSubmitCallbacks).forEach((callback) => {
|
Object.values(preSubmitCallbacks).forEach((callback) => {
|
||||||
callback()
|
callback()
|
||||||
})
|
})
|
||||||
@@ -321,24 +328,24 @@ export default function PaymentClient({
|
|||||||
const guarantee = data.guarantee
|
const guarantee = data.guarantee
|
||||||
const useSavedCard = savedCreditCard
|
const useSavedCard = savedCreditCard
|
||||||
? {
|
? {
|
||||||
card: {
|
card: {
|
||||||
alias: savedCreditCard.alias,
|
alias: savedCreditCard.alias,
|
||||||
expiryDate: savedCreditCard.expirationDate,
|
expiryDate: savedCreditCard.expirationDate,
|
||||||
cardType: savedCreditCard.cardType,
|
cardType: savedCreditCard.cardType,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
const shouldUsePayment =
|
const shouldUsePayment =
|
||||||
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
guarantee || bookingMustBeGuaranteed || !hasOnlyFlexRates
|
||||||
const payment = shouldUsePayment
|
const payment = shouldUsePayment
|
||||||
? {
|
? {
|
||||||
paymentMethod: paymentMethod,
|
paymentMethod: paymentMethod,
|
||||||
...useSavedCard,
|
...useSavedCard,
|
||||||
success: `${paymentRedirectUrl}/success`,
|
success: `${paymentRedirectUrl}/success`,
|
||||||
error: `${paymentRedirectUrl}/error`,
|
error: `${paymentRedirectUrl}/error`,
|
||||||
cancel: `${paymentRedirectUrl}/cancel`,
|
cancel: `${paymentRedirectUrl}/cancel`,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
|
||||||
@@ -457,18 +464,10 @@ export default function PaymentClient({
|
|||||||
preSubmitCallbacks,
|
preSubmitCallbacks,
|
||||||
isUserLoggedIn,
|
isUserLoggedIn,
|
||||||
getTopOffset,
|
getTopOffset,
|
||||||
|
setIsSubmitting,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
|
||||||
initiateBooking.isPending ||
|
|
||||||
(isPollingForBookingStatus &&
|
|
||||||
!bookingStatus.data?.paymentUrl &&
|
|
||||||
!bookingStatus.isTimeout)
|
|
||||||
) {
|
|
||||||
return <LoadingSpinner />
|
|
||||||
}
|
|
||||||
|
|
||||||
const paymentGuarantee = intl.formatMessage({
|
const paymentGuarantee = intl.formatMessage({
|
||||||
defaultMessage: "Payment Guarantee",
|
defaultMessage: "Payment Guarantee",
|
||||||
})
|
})
|
||||||
@@ -480,7 +479,9 @@ export default function PaymentClient({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.paymentSection}>
|
<section
|
||||||
|
className={`${styles.paymentSection} ${allRoomsComplete && !isSubmitting ? "" : styles.disabled}`}
|
||||||
|
>
|
||||||
<header>
|
<header>
|
||||||
<Title level="h2" as="h4">
|
<Title level="h2" as="h4">
|
||||||
{hasOnlyFlexRates && bookingMustBeGuaranteed
|
{hasOnlyFlexRates && bookingMustBeGuaranteed
|
||||||
@@ -551,7 +552,7 @@ export default function PaymentClient({
|
|||||||
value={savedCreditCard.id}
|
value={savedCreditCard.id}
|
||||||
label={
|
label={
|
||||||
PAYMENT_METHOD_TITLES[
|
PAYMENT_METHOD_TITLES[
|
||||||
savedCreditCard.cardType as PaymentMethodEnum
|
savedCreditCard.cardType as PaymentMethodEnum
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
cardNumber={savedCreditCard.truncatedNumber}
|
cardNumber={savedCreditCard.truncatedNumber}
|
||||||
@@ -580,7 +581,7 @@ export default function PaymentClient({
|
|||||||
value={paymentMethod}
|
value={paymentMethod}
|
||||||
label={
|
label={
|
||||||
PAYMENT_METHOD_TITLES[
|
PAYMENT_METHOD_TITLES[
|
||||||
paymentMethod as PaymentMethodEnum
|
paymentMethod as PaymentMethodEnum
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -601,11 +602,12 @@ export default function PaymentClient({
|
|||||||
)}
|
)}
|
||||||
<div className={styles.submitButton}>
|
<div className={styles.submitButton}>
|
||||||
<Button
|
<Button
|
||||||
intent="primary"
|
|
||||||
theme="base"
|
|
||||||
size="small"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={methods.formState.isSubmitting}
|
isDisabled={
|
||||||
|
!methods.formState.isValid || methods.formState.isSubmitting
|
||||||
|
}
|
||||||
|
isPending={isSubmitting}
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
defaultMessage: "Complete booking",
|
defaultMessage: "Complete booking",
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { useSearchParams } from "next/navigation"
|
|||||||
import { type PropsWithChildren, useEffect, useRef } from "react"
|
import { type PropsWithChildren, useEffect, useRef } from "react"
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
|
||||||
import { useEnterDetailsStore } from "@/stores/enter-details"
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient"
|
import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient"
|
||||||
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"
|
||||||
import { formatPrice } from "@/utils/numberFormatting"
|
import { formatPrice } from "@/utils/numberFormatting"
|
||||||
@@ -20,13 +21,19 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
|
|||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const errorCode = searchParams.get("errorCode")
|
const errorCode = searchParams.get("errorCode")
|
||||||
|
|
||||||
const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } =
|
const {
|
||||||
useEnterDetailsStore((state) => ({
|
isSummaryOpen,
|
||||||
isSummaryOpen: state.isSummaryOpen,
|
toggleSummaryOpen,
|
||||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
totalPrice,
|
||||||
totalPrice: state.totalPrice,
|
isSubmittingDisabled,
|
||||||
isSubmittingDisabled: state.isSubmittingDisabled,
|
isSubmitting,
|
||||||
}))
|
} = useEnterDetailsStore((state) => ({
|
||||||
|
isSummaryOpen: state.isSummaryOpen,
|
||||||
|
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||||
|
totalPrice: state.totalPrice,
|
||||||
|
isSubmittingDisabled: state.isSubmittingDisabled,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
}))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSummaryOpen) {
|
if (isSummaryOpen) {
|
||||||
@@ -82,11 +89,12 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
|
|||||||
</Caption>
|
</Caption>
|
||||||
</button>
|
</button>
|
||||||
<Button
|
<Button
|
||||||
intent="primary"
|
variant="Primary"
|
||||||
theme="base"
|
size="Large"
|
||||||
size="large"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isSubmittingDisabled}
|
isDisabled={isSubmittingDisabled}
|
||||||
|
isPending={isSubmitting}
|
||||||
|
typography="Body/Supporting text (caption)/smBold"
|
||||||
form={formId}
|
form={formId}
|
||||||
>
|
>
|
||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export function createDetailsStore(
|
|||||||
breakfastPackages,
|
breakfastPackages,
|
||||||
canProceedToPayment: false,
|
canProceedToPayment: false,
|
||||||
isSubmittingDisabled: false,
|
isSubmittingDisabled: false,
|
||||||
|
isSubmitting: false,
|
||||||
isSummaryOpen: false,
|
isSummaryOpen: false,
|
||||||
lastRoom: initialState.booking.rooms.length - 1,
|
lastRoom: initialState.booking.rooms.length - 1,
|
||||||
rooms: initialState.rooms.map((room, idx) => {
|
rooms: initialState.rooms.map((room, idx) => {
|
||||||
@@ -365,6 +366,13 @@ export function createDetailsStore(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
setIsSubmitting(isSubmitting) {
|
||||||
|
return set(
|
||||||
|
produce((state: DetailsState) => {
|
||||||
|
state.isSubmitting = isSubmitting
|
||||||
|
})
|
||||||
|
)
|
||||||
|
},
|
||||||
setTotalPrice(totalPrice) {
|
setTotalPrice(totalPrice) {
|
||||||
return set(
|
return set(
|
||||||
produce((state: DetailsState) => {
|
produce((state: DetailsState) => {
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export type InitialState = {
|
|||||||
export interface DetailsState {
|
export interface DetailsState {
|
||||||
actions: {
|
actions: {
|
||||||
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
|
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
|
||||||
|
setIsSubmitting: (isSubmitting: boolean) => void
|
||||||
setTotalPrice: (totalPrice: Price) => void
|
setTotalPrice: (totalPrice: Price) => void
|
||||||
toggleSummaryOpen: () => void
|
toggleSummaryOpen: () => void
|
||||||
updateSeachParamString: (searchParamString: string) => void
|
updateSeachParamString: (searchParamString: string) => void
|
||||||
@@ -93,6 +94,7 @@ export interface DetailsState {
|
|||||||
breakfastPackages: BreakfastPackages
|
breakfastPackages: BreakfastPackages
|
||||||
canProceedToPayment: boolean
|
canProceedToPayment: boolean
|
||||||
isSubmittingDisabled: boolean
|
isSubmittingDisabled: boolean
|
||||||
|
isSubmitting: boolean
|
||||||
isSummaryOpen: boolean
|
isSummaryOpen: boolean
|
||||||
lastRoom: number
|
lastRoom: number
|
||||||
rooms: RoomState[]
|
rooms: RoomState[]
|
||||||
|
|||||||
Reference in New Issue
Block a user