From f56a1ece0f06f07ea82dd94a5986eb4e9219eb86 Mon Sep 17 00:00:00 2001 From: Tobias Johansson Date: Wed, 23 Apr 2025 10:32:31 +0200 Subject: [PATCH] feat(SW-1255): Added loading state to submit button in enter details --- .../EnterDetails/Payment/PaymentClient.tsx | 64 ++++++++++--------- .../Summary/Mobile/BottomSheet/index.tsx | 32 ++++++---- .../scandic-web/stores/enter-details/index.ts | 8 +++ .../scandic-web/types/stores/enter-details.ts | 2 + 4 files changed, 63 insertions(+), 43 deletions(-) diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx index 09eec17e0..cfbd31848 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Payment/PaymentClient.tsx @@ -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 - } - const paymentGuarantee = intl.formatMessage({ defaultMessage: "Payment Guarantee", }) @@ -480,7 +479,9 @@ export default function PaymentClient({ }) return ( -
+
{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", diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx index 71b017e28..0b6f62234 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx @@ -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({ diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts index 81a7e40fd..df025a65a 100644 --- a/apps/scandic-web/stores/enter-details/index.ts +++ b/apps/scandic-web/stores/enter-details/index.ts @@ -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) => { diff --git a/apps/scandic-web/types/stores/enter-details.ts b/apps/scandic-web/types/stores/enter-details.ts index da801fa19..72697e347 100644 --- a/apps/scandic-web/types/stores/enter-details.ts +++ b/apps/scandic-web/types/stores/enter-details.ts @@ -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[]