feat(SW-1255): Added loading state to submit button in enter details

This commit is contained in:
Tobias Johansson
2025-04-23 10:32:31 +02:00
committed by Simon Emanuelsson
parent 89468bc37f
commit f56a1ece0f
4 changed files with 63 additions and 43 deletions

View File

@@ -8,6 +8,7 @@ import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { Button } from "@scandic-hotels/design-system/Button"
import {
BOOKING_CONFIRMATION_NUMBER,
@@ -25,8 +26,6 @@ import { trpc } from "@/lib/trpc/client"
import { useEnterDetailsStore } from "@/stores/enter-details"
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 Title from "@/components/TempDesignSystem/Text/Title"
import { useAvailablePaymentOptions } from "@/hooks/booking/useAvailablePaymentOptions"
@@ -76,14 +75,18 @@ export default function PaymentClient({
const [showBookingAlert, setShowBookingAlert] = useState(false)
const { booking, rooms, totalPrice, preSubmitCallbacks } =
const { booking, rooms, totalPrice, isSubmitting, preSubmitCallbacks, setIsSubmitting } =
useEnterDetailsStore((state) => ({
booking: state.booking,
rooms: state.rooms,
totalPrice: state.totalPrice,
preSubmitCallbacks: state.preSubmitCallbacks,
isSubmitting: state.isSubmitting,
setIsSubmitting: state.actions.setIsSubmitting,
}))
const allRoomsComplete = rooms.every((r) => r.isComplete)
const bookingMustBeGuaranteed = rooms.some(({ room }, idx) => {
if (idx === 0 && isUserLoggedIn && room.memberMustBeGuaranteed) {
return true
@@ -202,6 +205,7 @@ export default function PaymentClient({
const handlePaymentError = useCallback(
(errorMessage: string) => {
setShowBookingAlert(true)
setIsSubmitting(false)
const currentPaymentMethod = methods.getValues("paymentMethod")
const smsEnable = methods.getValues("smsConfirmation")
@@ -243,6 +247,7 @@ export default function PaymentClient({
hotelId,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
setIsSubmitting,
]
)
@@ -284,6 +289,8 @@ export default function PaymentClient({
const handleSubmit = useCallback(
(data: PaymentFormData) => {
setIsSubmitting(true)
Object.values(preSubmitCallbacks).forEach((callback) => {
callback()
})
@@ -321,24 +328,24 @@ export default function PaymentClient({
const guarantee = data.guarantee
const useSavedCard = savedCreditCard
? {
card: {
alias: savedCreditCard.alias,
expiryDate: savedCreditCard.expirationDate,
cardType: savedCreditCard.cardType,
},
}
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`,
}
paymentMethod: paymentMethod,
...useSavedCard,
success: `${paymentRedirectUrl}/success`,
error: `${paymentRedirectUrl}/error`,
cancel: `${paymentRedirectUrl}/cancel`,
}
: undefined
if (guarantee || (bookingMustBeGuaranteed && hasOnlyFlexRates)) {
@@ -457,18 +464,10 @@ export default function PaymentClient({
preSubmitCallbacks,
isUserLoggedIn,
getTopOffset,
setIsSubmitting,
]
)
if (
initiateBooking.isPending ||
(isPollingForBookingStatus &&
!bookingStatus.data?.paymentUrl &&
!bookingStatus.isTimeout)
) {
return <LoadingSpinner />
}
const paymentGuarantee = intl.formatMessage({
defaultMessage: "Payment Guarantee",
})
@@ -480,7 +479,9 @@ export default function PaymentClient({
})
return (
<section className={styles.paymentSection}>
<section
className={`${styles.paymentSection} ${allRoomsComplete && !isSubmitting ? "" : styles.disabled}`}
>
<header>
<Title level="h2" as="h4">
{hasOnlyFlexRates && bookingMustBeGuaranteed
@@ -551,7 +552,7 @@ export default function PaymentClient({
value={savedCreditCard.id}
label={
PAYMENT_METHOD_TITLES[
savedCreditCard.cardType as PaymentMethodEnum
savedCreditCard.cardType as PaymentMethodEnum
]
}
cardNumber={savedCreditCard.truncatedNumber}
@@ -580,7 +581,7 @@ export default function PaymentClient({
value={paymentMethod}
label={
PAYMENT_METHOD_TITLES[
paymentMethod as PaymentMethodEnum
paymentMethod as PaymentMethodEnum
]
}
/>
@@ -601,11 +602,12 @@ export default function PaymentClient({
)}
<div className={styles.submitButton}>
<Button
intent="primary"
theme="base"
size="small"
type="submit"
disabled={methods.formState.isSubmitting}
isDisabled={
!methods.formState.isValid || methods.formState.isSubmitting
}
isPending={isSubmitting}
typography="Body/Supporting text (caption)/smBold"
>
{intl.formatMessage({
defaultMessage: "Complete booking",

View File

@@ -4,10 +4,11 @@ import { useSearchParams } from "next/navigation"
import { type PropsWithChildren, useEffect, useRef } from "react"
import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { formId } from "@/components/HotelReservation/EnterDetails/Payment/PaymentClient"
import Button from "@/components/TempDesignSystem/Button"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { formatPrice } from "@/utils/numberFormatting"
@@ -20,13 +21,19 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
const searchParams = useSearchParams()
const errorCode = searchParams.get("errorCode")
const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } =
useEnterDetailsStore((state) => ({
isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.actions.toggleSummaryOpen,
totalPrice: state.totalPrice,
isSubmittingDisabled: state.isSubmittingDisabled,
}))
const {
isSummaryOpen,
toggleSummaryOpen,
totalPrice,
isSubmittingDisabled,
isSubmitting,
} = useEnterDetailsStore((state) => ({
isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.actions.toggleSummaryOpen,
totalPrice: state.totalPrice,
isSubmittingDisabled: state.isSubmittingDisabled,
isSubmitting: state.isSubmitting,
}))
useEffect(() => {
if (isSummaryOpen) {
@@ -82,11 +89,12 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) {
</Caption>
</button>
<Button
intent="primary"
theme="base"
size="large"
variant="Primary"
size="Large"
type="submit"
disabled={isSubmittingDisabled}
isDisabled={isSubmittingDisabled}
isPending={isSubmitting}
typography="Body/Supporting text (caption)/smBold"
form={formId}
>
{intl.formatMessage({

View File

@@ -143,6 +143,7 @@ export function createDetailsStore(
breakfastPackages,
canProceedToPayment: false,
isSubmittingDisabled: false,
isSubmitting: false,
isSummaryOpen: false,
lastRoom: initialState.booking.rooms.length - 1,
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) {
return set(
produce((state: DetailsState) => {

View File

@@ -84,6 +84,7 @@ export type InitialState = {
export interface DetailsState {
actions: {
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
setIsSubmitting: (isSubmitting: boolean) => void
setTotalPrice: (totalPrice: Price) => void
toggleSummaryOpen: () => void
updateSeachParamString: (searchParamString: string) => void
@@ -93,6 +94,7 @@ export interface DetailsState {
breakfastPackages: BreakfastPackages
canProceedToPayment: boolean
isSubmittingDisabled: boolean
isSubmitting: boolean
isSummaryOpen: boolean
lastRoom: number
rooms: RoomState[]