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 { 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()
}) })
@@ -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
@@ -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",

View File

@@ -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,12 +21,18 @@ 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,
toggleSummaryOpen,
totalPrice,
isSubmittingDisabled,
isSubmitting,
} = useEnterDetailsStore((state) => ({
isSummaryOpen: state.isSummaryOpen, isSummaryOpen: state.isSummaryOpen,
toggleSummaryOpen: state.actions.toggleSummaryOpen, toggleSummaryOpen: state.actions.toggleSummaryOpen,
totalPrice: state.totalPrice, totalPrice: state.totalPrice,
isSubmittingDisabled: state.isSubmittingDisabled, isSubmittingDisabled: state.isSubmittingDisabled,
isSubmitting: state.isSubmitting,
})) }))
useEffect(() => { useEffect(() => {
@@ -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({

View File

@@ -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) => {

View File

@@ -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[]