Merged in feat/SW-1879-aa-tracking-bed-breakfastpayment (pull request #1789)

feat: SW-1879 Tracking enter-details sections

* feat: SW-1879 Tracking enter-details sections

* feat: SW-1879 removed onSelect to trigger in onSubmit

* feat: SW-1879 Removed onSelect and triggered inside onSubmit

* feat: SW-1879 Optimized to remove unnecessary useEffect triggers in every re-render

* feat: SW-1879 Updated breakfast package typings

* feat: SW-1879 Reverted RadioCardProps

* feat: SW-1879 Optimised code


Approved-by: Tobias Johansson
Approved-by: Christian Andolf
This commit is contained in:
Hrishikesh Vaipurkar
2025-04-16 14:06:15 +00:00
parent 4076f8d6d8
commit 8c0597727b
13 changed files with 127 additions and 27 deletions

View File

@@ -100,7 +100,7 @@ export default async function DetailsPage({
booking, booking,
hotel, hotel,
rooms, rooms,
!!breakfastPackages?.length, !!breakfastPackages.length,
searchParams.city, searchParams.city,
!!user, !!user,
lang lang

View File

@@ -12,6 +12,7 @@ import {
import RadioCard from "@/components/TempDesignSystem/Form/RadioCard" import RadioCard from "@/components/TempDesignSystem/Form/RadioCard"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import { trackBedSelection } from "@/utils/tracking"
import { bedTypeFormSchema } from "./schema" import { bedTypeFormSchema } from "./schema"
@@ -47,18 +48,17 @@ export default function BedType() {
roomTypeCode: matchingRoom.value, roomTypeCode: matchingRoom.value,
} }
updateBedType(bedType) updateBedType(bedType)
trackBedSelection(bedType.roomTypeCode)
} }
}, },
[bedTypes, updateBedType] [bedTypes, updateBedType]
) )
const selectedBedType = methods.watch("bedType")
const handleSubmit = methods.handleSubmit
useEffect(() => { useEffect(() => {
if (methods.formState.isSubmitting) { handleSubmit(onSubmit)()
return }, [selectedBedType, handleSubmit, onSubmit])
}
methods.watch(() => methods.handleSubmit(onSubmit)())
}, [methods, onSubmit])
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>

View File

@@ -14,6 +14,7 @@ import RadioCard from "@/components/TempDesignSystem/Form/RadioCard"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import { formatPrice } from "@/utils/numberFormatting" import { formatPrice } from "@/utils/numberFormatting"
import { trackBreakfastSelection } from "@/utils/tracking"
import { breakfastFormSchema } from "./schema" import { breakfastFormSchema } from "./schema"
@@ -25,6 +26,7 @@ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Breakfast() { export default function Breakfast() {
const intl = useIntl() const intl = useIntl()
const packages = useEnterDetailsStore((state) => state.breakfastPackages) const packages = useEnterDetailsStore((state) => state.breakfastPackages)
const hotelId = useEnterDetailsStore((state) => state.booking.hotelId)
const { const {
actions: { updateBreakfast }, actions: { updateBreakfast },
room, room,
@@ -49,22 +51,26 @@ export default function Breakfast() {
const onSubmit = useCallback( const onSubmit = useCallback(
(values: BreakfastFormSchema) => { (values: BreakfastFormSchema) => {
const pkg = packages?.find((p) => p.code === values.breakfast) const pkg = packages.find((p) => p.code === values.breakfast)
if (pkg) { if (pkg) {
updateBreakfast(pkg) updateBreakfast(pkg)
} else { } else {
updateBreakfast(false) updateBreakfast(false)
} }
trackBreakfastSelection({
breakfastPackage: pkg ?? packages[0],
hotelId,
units: pkg ? room.adults : 0,
})
}, },
[packages, updateBreakfast] [packages, hotelId, room.adults, updateBreakfast]
) )
const selectedBreakfast = methods.watch("breakfast")
const handleSubmit = methods.handleSubmit
useEffect(() => { useEffect(() => {
if (methods.formState.isSubmitting) { handleSubmit(onSubmit)()
return }, [selectedBreakfast, handleSubmit, onSubmit])
}
methods.watch(() => methods.handleSubmit(onSubmit)())
}, [methods, onSubmit])
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>

View File

@@ -13,6 +13,7 @@ import { PaymentMethodEnum } from "@/constants/booking"
import Modal from "@/components/Modal" import Modal from "@/components/Modal"
import Divider from "@/components/TempDesignSystem/Divider" import Divider from "@/components/TempDesignSystem/Divider"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import { trackPaymentSectionOpen } from "@/utils/tracking/booking"
import MySavedCards from "../Payment/MySavedCards" import MySavedCards from "../Payment/MySavedCards"
import PaymentOption from "../Payment/PaymentOption" import PaymentOption from "../Payment/PaymentOption"
@@ -40,7 +41,16 @@ export default function ConfirmBooking({
<div className={styles.guaranteeContainer}> <div className={styles.guaranteeContainer}>
<div className={styles.title}> <div className={styles.title}>
<div className={styles.checkbox}> <div className={styles.checkbox}>
<Checkbox name="guarantee" /> <Checkbox
name="guarantee"
registerOptions={{
onChange: (e) => {
if (e.target.value) {
trackPaymentSectionOpen()
}
},
}}
/>
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<p> <p>
{intl.formatMessage({ {intl.formatMessage({

View File

@@ -13,7 +13,9 @@ import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone" import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import { trackPaymentSectionOpen } from "@/utils/tracking/booking"
import { hasPrepaidRate } from "../../Payment/helpers"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import { multiroomDetailsSchema } from "./schema" import { multiroomDetailsSchema } from "./schema"
@@ -25,10 +27,13 @@ const formID = "enter-details"
export default function Details() { export default function Details() {
const intl = useIntl() const intl = useIntl()
const { canProceedToPayment, lastRoom } = useEnterDetailsStore((state) => ({ const { canProceedToPayment, lastRoom, rooms } = useEnterDetailsStore(
canProceedToPayment: state.canProceedToPayment, (state) => ({
lastRoom: state.lastRoom, canProceedToPayment: state.canProceedToPayment,
})) lastRoom: state.lastRoom,
rooms: state.rooms,
})
)
const { const {
actions: { updateDetails }, actions: { updateDetails },
@@ -61,6 +66,8 @@ export default function Details() {
const guestIsGoingToJoin = methods.watch("join") const guestIsGoingToJoin = methods.watch("join")
const guestIsMember = methods.watch("membershipNo") const guestIsMember = methods.watch("membershipNo")
const hasPrepaidRates = rooms.some(hasPrepaidRate)
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form <form
@@ -144,6 +151,11 @@ export default function Details() {
typography="Body/Paragraph/mdBold" typography="Body/Paragraph/mdBold"
size="Medium" size="Medium"
type="submit" type="submit"
onPress={
isPaymentNext && canProceedToPayment && hasPrepaidRates
? trackPaymentSectionOpen
: undefined
}
> >
{isPaymentNext {isPaymentNext
? intl.formatMessage({ ? intl.formatMessage({

View File

@@ -14,6 +14,7 @@ import Input from "@/components/TempDesignSystem/Form/Input"
import Phone from "@/components/TempDesignSystem/Form/Phone" import Phone from "@/components/TempDesignSystem/Form/Phone"
import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/Details/Room" import { useRoomContext } from "@/contexts/Details/Room"
import { trackPaymentSectionOpen } from "@/utils/tracking/booking"
import JoinScandicFriendsCard from "./JoinScandicFriendsCard" import JoinScandicFriendsCard from "./JoinScandicFriendsCard"
import MemberPriceModal from "./MemberPriceModal" import MemberPriceModal from "./MemberPriceModal"
@@ -162,6 +163,11 @@ export default function Details({ user }: DetailsProps) {
typography="Body/Paragraph/mdBold" typography="Body/Paragraph/mdBold"
size="Medium" size="Medium"
type="submit" type="submit"
onPress={
isPaymentNext && canProceedToPayment && !room.isFlexRate
? trackPaymentSectionOpen
: undefined
}
> >
{isPaymentNext {isPaymentNext
? intl.formatMessage({ ? intl.formatMessage({

View File

@@ -26,7 +26,7 @@ export default function Multiroom() {
})) }))
const showBreakfastStep = const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages?.length !room.breakfastIncluded && !!breakfastPackages.length
const arePreviousRoomsValid = rooms.slice(0, idx).every((r) => r.isComplete) const arePreviousRoomsValid = rooms.slice(0, idx).every((r) => r.isComplete)

View File

@@ -40,7 +40,7 @@ export default function RoomOne({ user }: { user: SafeUser }) {
) )
const showBreakfastStep = const showBreakfastStep =
!room.breakfastIncluded && !!breakfastPackages?.length !room.breakfastIncluded && !!breakfastPackages.length
return ( return (
<section> <section>

View File

@@ -49,7 +49,7 @@ export function createDetailsStore(
initialState: InitialState, initialState: InitialState,
searchParams: string, searchParams: string,
user: SafeUser, user: SafeUser,
breakfastPackages: BreakfastPackages | null breakfastPackages: BreakfastPackages
) { ) {
const isMember = !!user const isMember = !!user
const isRedemption = const isRedemption =
@@ -134,7 +134,7 @@ export function createDetailsStore(
}, },
} }
if (room.breakfastIncluded || !breakfastPackages?.length) { if (room.breakfastIncluded || !breakfastPackages.length) {
delete steps[StepEnum.breakfast] delete steps[StepEnum.breakfast]
} }
@@ -318,7 +318,7 @@ export function createDetailsStore(
childrenInRoom: initialState.booking.rooms[idx].childrenInRoom, childrenInRoom: initialState.booking.rooms[idx].childrenInRoom,
bedType: room.bedType, bedType: room.bedType,
breakfast: breakfast:
!breakfastPackages?.length || room.breakfastIncluded !breakfastPackages.length || room.breakfastIncluded
? false ? false
: undefined, : undefined,
guest: guest:

View File

@@ -5,7 +5,7 @@ import type { SelectRateSearchParams } from "../components/hotelReservation/sele
export interface DetailsProviderProps extends React.PropsWithChildren { export interface DetailsProviderProps extends React.PropsWithChildren {
booking: SelectRateSearchParams booking: SelectRateSearchParams
breakfastPackages: BreakfastPackages | null breakfastPackages: BreakfastPackages
rooms: Room[] rooms: Room[]
searchParamsStr: string searchParamsStr: string
user: SafeUser user: SafeUser

View File

@@ -87,7 +87,7 @@ export interface DetailsState {
updateSeachParamString: (searchParamString: string) => void updateSeachParamString: (searchParamString: string) => void
} }
booking: SelectRateSearchParams booking: SelectRateSearchParams
breakfastPackages: BreakfastPackages | null breakfastPackages: BreakfastPackages
canProceedToPayment: boolean canProceedToPayment: boolean
isSubmittingDisabled: boolean isSubmittingDisabled: boolean
isSummaryOpen: boolean isSummaryOpen: boolean

View File

@@ -1,5 +1,6 @@
import { trackEvent } from "./base" import { trackEvent } from "./base"
import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast"
import type { LowestRoomPriceEvent } from "@/types/components/tracking" import type { LowestRoomPriceEvent } from "@/types/components/tracking"
export function trackLowestRoomPrice(event: LowestRoomPriceEvent) { export function trackLowestRoomPrice(event: LowestRoomPriceEvent) {
@@ -16,3 +17,64 @@ export function trackLowestRoomPrice(event: LowestRoomPriceEvent) {
}, },
}) })
} }
// Tracking for sections of booking flow enter-details page
export function trackBedSelection(bedType: string) {
trackEvent({
event: "bedSelection",
selection: {
name: "bed options selection click",
bedType: bedType,
},
pageInfo: {
pageName: "hotelreservation|bed",
pageType: "bookingbedtypepage",
},
})
}
export function trackBreakfastSelection({
breakfastPackage,
hotelId,
units,
}: {
breakfastPackage: BreakfastPackages[number]
hotelId: string
units: number
}) {
trackEvent({
event: "breakfastSelection",
selection: {
name: "breakfast options selection click",
},
ancillaries: [
{
hotelId: hotelId,
productCategory: "",
productId: breakfastPackage.code,
productUnits: units,
productPrice: breakfastPackage.localPrice.price,
productPoints: 0,
productType: "food",
productName: breakfastPackage.packageType,
},
],
pageInfo: {
pageName: "hotelreservation|breakfast",
pageType: "bookingbreakfastpage",
},
})
}
export function trackPaymentSectionOpen() {
trackEvent({
event: "paymentSectionOpen",
selection: {
name: "payment section open",
},
pageInfo: {
pageName: "hotelreservation|payment",
pageType: "bookingpaymentpage",
},
})
}

View File

@@ -1,5 +1,9 @@
export { trackClick } from "./base" export { trackClick } from "./base"
export { trackLowestRoomPrice } from "./booking" export {
trackBedSelection,
trackBreakfastSelection,
trackLowestRoomPrice,
} from "./booking"
export { trackAccordionClick, trackOpenSidePeekEvent } from "./componentEvents" export { trackAccordionClick, trackOpenSidePeekEvent } from "./componentEvents"
export { trackHotelMapClick, trackHotelTabClick } from "./hotelPage" export { trackHotelMapClick, trackHotelTabClick } from "./hotelPage"
export { trackCancelStay, trackMyStayPageLink } from "./myStay" export { trackCancelStay, trackMyStayPageLink } from "./myStay"