@ticketboothapp/booking 1.2.25 → 1.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -29
- package/src/components/booking/AddOnsSection.tsx +2 -2
- package/src/components/booking/AdminPaymentChoiceModal.tsx +1 -1
- package/src/components/booking/BookingDialog.tsx +31 -13
- package/src/components/booking/BookingFlow.tsx +32 -27
- package/src/components/booking/BookingFlowCollage.tsx +10 -6
- package/src/components/booking/BookingFlowPlaceholder.tsx +1 -1
- package/src/components/booking/BookingFlowPreview.tsx +18 -9
- package/src/components/booking/BookingProductGrid.tsx +55 -19
- package/src/components/booking/Calendar.module.css +19 -4
- package/src/components/booking/Calendar.tsx +13 -8
- package/src/components/booking/CancellationPolicySelector.tsx +2 -2
- package/src/components/booking/ChangeBookingDialog.tsx +22 -12
- package/src/components/booking/CheckoutForm.module.css +10 -0
- package/src/components/booking/CheckoutForm.tsx +10 -2
- package/src/components/booking/CheckoutModal.tsx +16 -14
- package/src/components/booking/DapFlowCollage.tsx +5 -2
- package/src/components/booking/DapTourDescription.tsx +4 -4
- package/src/components/booking/DependentAddOnBookingDialog.tsx +23 -16
- package/src/components/booking/DependentAddOnPaymentForm.tsx +10 -7
- package/src/components/booking/ItineraryBox.tsx +6 -6
- package/src/components/booking/ItineraryBuilder.tsx +1 -1
- package/src/components/booking/MealDrinkAddOnSelector.tsx +3 -3
- package/src/components/booking/PickupLocationSelector.tsx +20 -18
- package/src/components/booking/PickupTimeSelector.tsx +3 -3
- package/src/components/booking/PriceBreakdown.tsx +5 -5
- package/src/components/booking/PriceSummary.module.css +7 -0
- package/src/components/booking/PriceSummary.tsx +8 -7
- package/src/components/booking/PrivateShuttleBookingFlow.tsx +28 -19
- package/src/components/booking/PromoCodeInput.module.css +31 -25
- package/src/components/booking/PromoCodeInput.tsx +36 -24
- package/src/components/booking/ReturnTimeSelector.tsx +3 -3
- package/src/components/booking/TermsAcceptance.tsx +7 -2
- package/src/components/booking/TicketSelector.tsx +1 -1
- package/src/components/booking/TourDescription.tsx +11 -6
- package/src/components/booking/booking-flow.css +65 -4
- package/src/hooks/useBookingSourceMetadataFromLocation.ts +1 -1
- package/src/hooks/useIsBookingLaunchLive.ts +1 -1
- package/src/index.ts +26 -64
- package/src/providers/booking-dialog-provider.tsx +62 -53
- package/src/runtime/BookingHostContext.tsx +39 -0
- package/src/runtime/index.ts +13 -0
- package/src/runtime/types.ts +86 -0
- package/tsconfig.json +3 -5
- package/src/assets/icons/minus.svg +0 -7
- package/src/assets/icons/partner-logos/getyourguide.svg +0 -8
- package/src/assets/icons/plus.svg +0 -3
- package/src/colours.css +0 -23
- package/src/components/BookingDetails.module.css +0 -1591
- package/src/components/BookingDetails.tsx +0 -2264
- package/src/components/BookingWidget.tsx +0 -305
- package/src/components/ManageBookingView.tsx +0 -437
- package/src/components/PhoneInputWithCountry.module.css +0 -131
- package/src/components/PhoneInputWithCountry.tsx +0 -44
- package/src/components/PickupLocationDialog.module.css +0 -360
- package/src/components/PickupLocationDialog.tsx +0 -357
- package/src/components/PostBookingDependentAddOnUpsell.module.css +0 -174
- package/src/components/PostBookingDependentAddOnUpsell.tsx +0 -407
- package/src/components/button.css +0 -245
- package/src/components/button.tsx +0 -152
- package/src/components/colorable-svg.tsx +0 -29
- package/src/components/image.css +0 -29
- package/src/components/image.tsx +0 -113
- package/src/components/partner/PartnerBookingPage.module.css +0 -130
- package/src/components/partner/PartnerBookingPage.tsx +0 -390
- package/src/components/partner/PartnerBookingPageWithBrowserMetadata.tsx +0 -45
- package/src/components/product-tag.module.css +0 -30
- package/src/components/product-tag.tsx +0 -34
- package/src/components/product-theme-pages/image-modal.tsx +0 -248
- package/src/components/product-theme-pages/photo-gallery.module.css +0 -200
- package/src/components/terms/TermsContent.tsx +0 -178
- package/src/components/value-pill.module.css +0 -59
- package/src/components/value-pill.tsx +0 -46
- package/src/constants/images.ts +0 -556
- package/src/constants/pill-values.ts +0 -210
- package/src/constants/products.ts +0 -155
- package/src/contexts/AvailabilitiesCacheContext.tsx +0 -125
- package/src/contexts/CompanyContext.tsx +0 -70
- package/src/data/dap-descriptions/session-couples-families-friends.en.json +0 -61
- package/src/data/dap-descriptions/session-elopements.en.json +0 -60
- package/src/data/dap-descriptions/session-proposals.en.json +0 -60
- package/src/data/product-descriptions/afternoon-delight.en.json +0 -35
- package/src/data/product-descriptions/emerald-lake-escape.en.json +0 -68
- package/src/data/product-descriptions/lake-louise-adventure.en.json +0 -74
- package/src/data/product-descriptions/moraine-lake-adventure.en.json +0 -78
- package/src/data/product-descriptions/moraine-lake-sunrise-lake-louise-golden-hour.en.json +0 -65
- package/src/data/product-descriptions/moraine-lake-sunrise.en.json +0 -64
- package/src/data/product-descriptions/private-tour.en.json +0 -80
- package/src/data/product-descriptions/two-lakes-combo.en.json +0 -65
- package/src/data/products-config.json +0 -101
- package/src/lib/analytics.ts +0 -197
- package/src/lib/booking/booking-source.ts +0 -51
- package/src/lib/booking/checkout-breakdown.ts +0 -69
- package/src/lib/booking/correlation-id.ts +0 -46
- package/src/lib/booking/i18n/config.ts +0 -21
- package/src/lib/booking/i18n/index.tsx +0 -144
- package/src/lib/booking/i18n/messages/en.json +0 -236
- package/src/lib/booking/i18n/messages/fr.json +0 -236
- package/src/lib/booking/itinerary-display.ts +0 -36
- package/src/lib/booking/itinerary-labels.ts +0 -70
- package/src/lib/booking/location-calculations.ts +0 -43
- package/src/lib/booking/location-utils.ts +0 -165
- package/src/lib/booking/map-utils.ts +0 -153
- package/src/lib/booking/marker-icons.ts +0 -113
- package/src/lib/booking/normalize-booking-product-id.ts +0 -21
- package/src/lib/booking/pickup-location-types.ts +0 -25
- package/src/lib/booking/places-api.ts +0 -154
- package/src/lib/booking/pricing.ts +0 -466
- package/src/lib/booking/product-option-id.ts +0 -35
- package/src/lib/booking/source-metadata.ts +0 -226
- package/src/lib/booking/sunday-week.ts +0 -14
- package/src/lib/booking/theme.ts +0 -83
- package/src/lib/booking/trace-context.ts +0 -62
- package/src/lib/booking/utils.ts +0 -9
- package/src/lib/booking-api.ts +0 -1793
- package/src/lib/booking-constants.ts +0 -23
- package/src/lib/booking-ref.ts +0 -13
- package/src/lib/booking-types.ts +0 -36
- package/src/lib/currency.ts +0 -81
- package/src/lib/dap-descriptions.ts +0 -50
- package/src/lib/dap-itinerary-preview.ts +0 -315
- package/src/lib/dependent-add-on-api.ts +0 -434
- package/src/lib/env.ts +0 -96
- package/src/lib/firebase.ts +0 -20
- package/src/lib/job-application-api.ts +0 -83
- package/src/lib/manage-booking-embed-print.ts +0 -16
- package/src/lib/manage-booking-post-checkout.ts +0 -68
- package/src/lib/photo-dap-config.ts +0 -228
- package/src/lib/photo-packages.ts +0 -75
- package/src/lib/pickup/map-utils.ts +0 -56
- package/src/lib/pickup/marker-icons.ts +0 -19
- package/src/lib/product-descriptions.ts +0 -66
- package/src/lib/products-config.ts +0 -73
- package/src/providers/dependent-add-on-dialog-provider.tsx +0 -105
- package/src/radius.css +0 -5
- package/src/spacing.css +0 -7
- package/src/strings/en.json +0 -1774
- package/src/strings/es.json +0 -1573
- package/src/strings/fr.json +0 -1573
- package/src/strings/index.js +0 -23
- package/src/text-style.css +0 -56
- package/src/utils/currency-converter.ts +0 -101
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"shortDescription": "Visit <b>both Moraine Lake and Lake Louise in one day</b> on our <b>most popular Banff day tour</b>. With departures at <b>7:30 AM, 9 AM, and 1 PM</b> from <b>Canmore with pickups in Banff and Lake Louise</b>, sit back, relax, and take in the scenic views while our guides handle the driving and logistics. Spend <b>up to two hours at Moraine Lake</b> exploring the Rockpile and lakeshore, then continue to <b>Lake Louise for more exploration</b>, and to walk the shoreline, canoe, or hike to Fairview Lookout.",
|
|
3
|
-
"paragraphs": [
|
|
4
|
-
"<b>Visit two of the most iconic lakes</b> in the Canadian Rockies on this relaxed small-group tour to <b>Moraine Lake and Lake Louise</b>. Perfect for those who want to <b>experience the highlights</b> of Banff National Park <b>without the stress of driving or parking</b>, this tour gives you plenty of time to explore each location at your own pace.",
|
|
5
|
-
"Your day begins with departures at <b>7:30 AM, 9 AM, or 1 PM</b> from <b>Canmore, with convenient doorstep pickups in Banff and Lake Louise</b>. Sit back and enjoy the scenic drive through the mountains while our local guides handle the logistics and share stories about the area, its wildlife, and the history of the park along the way.",
|
|
6
|
-
"Our <b>first stop is Moraine Lake</b>, where you’ll have up to <b>two hours to explore</b> one of the most photographed lakes in the Canadian Rockies. <b>Walk</b> along the lakeshore, rent a <b>canoe, hike</b> the famous Rockpile for panoramic views of the Valley of the Ten Peaks, or <b>simply relax and take in the incredible alpine scenery</b>.",
|
|
7
|
-
"Afterward, we continue to <b>Lake Louise for up to two hours of free time</b>. Stroll along the shoreline, rent a canoe on the turquoise water, or hike to Fairview Lookout for sweeping views of the lake and surrounding peaks.",
|
|
8
|
-
"With <b>comfortable transportation, knowledgeable local driver-guides, and plenty of time to explore both lakes at your own pace</b>, this tour offers an easy and memorable way to experience two of the most beautiful places in Banff National Park."
|
|
9
|
-
],
|
|
10
|
-
"review": {
|
|
11
|
-
"text": "Our trip to Moraine Lake and Lake Louise with Via Via was wonderful and worry-free. The van was very comfortable and we loved the small group experience. Everything was well organized and we had plenty of time to explore both lakes.",
|
|
12
|
-
"name": "— Pam M."
|
|
13
|
-
},
|
|
14
|
-
"sections": [
|
|
15
|
-
{
|
|
16
|
-
"title": "What's included",
|
|
17
|
-
"content": [
|
|
18
|
-
"• Convenient pickup directly at your door in Canmore, Harvie Heights, Banff and Lake Louise 🏨",
|
|
19
|
-
"• Experienced local guides with tips and knowledge on the best photo spots, hikes, and view spots 🏔️",
|
|
20
|
-
"• Brand new comfortable & luxury shuttles 🚐",
|
|
21
|
-
"• Trailsnacks 🍫",
|
|
22
|
-
"• Water to refills - bring your water bottle 💧",
|
|
23
|
-
"• Phone chargers 🔋",
|
|
24
|
-
"• Moraine Lake Access Fee 🏞️",
|
|
25
|
-
"• Banff National Park Pass 🏞️"
|
|
26
|
-
]
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
"title": "Tour highlights",
|
|
30
|
-
"content": [
|
|
31
|
-
"• Visit both Moraine Lake and Lake Louise in one unforgettable day.",
|
|
32
|
-
"• Relax on a scenic drive through Banff National Park while our guides handle the logistics.",
|
|
33
|
-
"• Spend two full hours exploring Moraine Lake and the famous Rockpile viewpoint.",
|
|
34
|
-
"• Enjoy another two hours at Lake Louise to walk the shoreline, canoe, or hike to Fairview Lookout.",
|
|
35
|
-
"• Small-group experience for a more relaxed and personalized tour.",
|
|
36
|
-
"• Learn about the area’s wildlife, history, and hidden gems from our knowledgeable local guides."
|
|
37
|
-
]
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"title": "Additional information",
|
|
41
|
-
"content": [
|
|
42
|
-
"<strong>What to bring:</strong>",
|
|
43
|
-
"Even in the summer, temperatures and weather conditions can change quickly in the mountains and can be below freezing temperatures around sunrise.",
|
|
44
|
-
"✅ Make sure to bring enough layers, sunrise in the mountains can still be pretty chilly!",
|
|
45
|
-
"✅ Bring a beanie and gloves to keep you cozy and comfortable",
|
|
46
|
-
"✅ Bring water and snacks to stay energized throughout the day",
|
|
47
|
-
"✅ Be prepared for quick weather changes in the mountains",
|
|
48
|
-
"✅ Check the current weather conditions via the link in your booking confirmation email before departure!",
|
|
49
|
-
"<strong>Special requirements:</strong>",
|
|
50
|
-
"Children's safety seats are available for children aged 2 and up or weighing 40lbs (18kg) or more.",
|
|
51
|
-
"If your child is <strong>younger than 2</strong> or weighs <strong>less than 40lbs</strong>, a rear-facing safety seat is required, and <strong>customers must bring their own</strong>.",
|
|
52
|
-
"Please inform us in advance if you need a children's safety seat or if you will be bringing your own. The children's safety seat can stay in the shuttle during your lake visit.",
|
|
53
|
-
"<strong>Restrictions:</strong>",
|
|
54
|
-
"Small pets are welcome if they can fit in a carry-on cage on your lap.",
|
|
55
|
-
"Service animals of all sizes are allowed - <strong>please contact us beforehand if you're bringing a service animal</strong>.",
|
|
56
|
-
"<strong>Cancellations:</strong>",
|
|
57
|
-
"We offer standard, flexible, and premium cancellation policies.",
|
|
58
|
-
"<strong>Standard Cancellation:</strong> Guests can cancel their booking up to <strong>7 days prior to their trip</strong> for a <strong>full refund</strong>, cancel up to <strong>72 hours before departure</strong> for a <strong>50% refund</strong>.",
|
|
59
|
-
"<strong>Flexible Cancellation:</strong> Guests can cancel their booking up to <strong>7 days prior to their trip</strong> for a <strong>full refund</strong>, cancel up to <strong>24 hours before departure</strong> for a <strong>75% refund</strong>.",
|
|
60
|
-
"<strong>Premium Cancellation:</strong> Guests can cancel their booking up to <strong>12 hours before departure</strong> for a <strong>full refund</strong>.",
|
|
61
|
-
"Additionally, all guests may make changes to their booking date and time up to <strong>72 hours in advance of their trip</strong> (subject to availability). To cancel your booking, please contact us directly."
|
|
62
|
-
]
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "1",
|
|
3
|
-
"generatedAt": "2025-03-05T00:00:00Z",
|
|
4
|
-
"companyId": "c_LFU0Vx9hS5v3",
|
|
5
|
-
"products": [
|
|
6
|
-
{
|
|
7
|
-
"productId": "p_qa5keidRoV6H",
|
|
8
|
-
"optionIds": ["po_cSD2wlJfQqc0"],
|
|
9
|
-
"display": {
|
|
10
|
-
"path": "/moraine-lake-shuttle",
|
|
11
|
-
"slug": "moraine-lake-sunrise-lake-louise-golden-hour",
|
|
12
|
-
"imageIds": ["moraine-lake-canoes-sunrise.jpg"],
|
|
13
|
-
"collageImageIds": ["moraine-lake-canoes-sunrise.jpg", "golden-hour-lake-louise.jpg", "moraine-lake-sunrise-couple.jpg", "moraine-lake-sunrise-hotdrink.jpg"],
|
|
14
|
-
"shortName": "Moraine Lake Sunrise & Lake Louise Golden Hour",
|
|
15
|
-
"description": "Catch sunrise at Moraine Lake, then explore Lake Louise during golden hour. Two iconic lakes in one unforgettable morning.",
|
|
16
|
-
"themePage": "moraine-lake",
|
|
17
|
-
"mostPopular": true
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"productId": "p_YKQsLmKhfYFp",
|
|
22
|
-
"optionIds": ["po_82wA2gUZBZ9z"],
|
|
23
|
-
"display": {
|
|
24
|
-
"path": "/moraine-lake-shuttle",
|
|
25
|
-
"slug": "moraine-lake-sunrise",
|
|
26
|
-
"imageIds": ["moraine-lake-sunrise-cheers.jpg"],
|
|
27
|
-
"collageImageIds": ["moraine-lake-canoes-sunrise.jpg", "moraine-lake-sunrise-cheers.jpg", "ml-sunrise-gorp.jpg", "moraine-lake-sunrise-yellow-jacket.jpg"],
|
|
28
|
-
"shortName": "Moraine Lake Sunrise Shuttle",
|
|
29
|
-
"description": "Arrive at Moraine Lake before sunrise. Beat the crowds and capture the iconic turquoise waters at first light.",
|
|
30
|
-
"themePage": "moraine-lake"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"productId": "p_CUJ4sVvaOkjk",
|
|
35
|
-
"optionIds": ["po_ImWip6rbg0D6", "po_lOO7wz6uMus1", "po_fvrAfalgI673"],
|
|
36
|
-
"display": {
|
|
37
|
-
"path": "/moraine-lake-shuttle",
|
|
38
|
-
"slug": "two-lakes-combo",
|
|
39
|
-
"imageIds": ["moraine-lake-yellow-bikini.jpg"],
|
|
40
|
-
"collageImageIds": ["lake-louise-flora.jpg", "moraine-lake-yellow-bikini.jpg", "couple-moraine-turquoise-reflection.jpg", "lake-louise-canoe.jpg"],
|
|
41
|
-
"shortName": "Two Lakes Combo",
|
|
42
|
-
"description": "Visit both Moraine Lake and Lake Louise in one day. Perfect for those who want to see the best of both.",
|
|
43
|
-
"themePage": "moraine-lake",
|
|
44
|
-
"mostPopular": true
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
"productId": "p_wQHE15ITi7TS",
|
|
49
|
-
"optionIds": ["po_FTLSMsahWKjM", "po_nbp2pFQo9AH7", "po_nzlCuOVJU181"],
|
|
50
|
-
"display": {
|
|
51
|
-
"path": "/moraine-lake-shuttle",
|
|
52
|
-
"slug": "moraine-lake-adventure",
|
|
53
|
-
"imageIds": ["moraine-lake-canoe.jpg"],
|
|
54
|
-
"collageImageIds": ["moraine-lake-jump-in.jpg", "moraine-lake-hiking.jpg", "moraine-lake-canoe.jpg", "moraine-lake-rockpile-couple.jpg"],
|
|
55
|
-
"shortName": "Moraine Lake Adventure",
|
|
56
|
-
"description": "Our classic Moraine Lake shuttle. Flexible time at the lake for hiking, canoeing, or simply soaking in the views.",
|
|
57
|
-
"themePage": "moraine-lake"
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"productId": "p_aaD7mhbGaZOi",
|
|
62
|
-
"optionIds": ["po_BOw9d7qIWEhJ"],
|
|
63
|
-
"display": {
|
|
64
|
-
"path": "/lake-louise-shuttle",
|
|
65
|
-
"slug": "lake-louise-adventure",
|
|
66
|
-
"imageIds": ["lake-louise-flora.jpg"],
|
|
67
|
-
"collageImageIds": ["lake-louise-flora.jpg", "lake-louise-fairview-lookout.jpg", "lake-louise-lakefront-friends.jpg", "guests-ll-canada.jpg"],
|
|
68
|
-
"shortName": "Lake Louise Shuttle",
|
|
69
|
-
"description": "Daily shuttles to Lake Louise from Canmore and Banff. Hike, canoe, or explore the iconic chateau.",
|
|
70
|
-
"themePage": "lake-louise"
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
"productId": "p_C5jHohBUCwrd",
|
|
75
|
-
"optionIds": ["po_cVr23UWDaC9M"],
|
|
76
|
-
"display": {
|
|
77
|
-
"path": "/emerald-lake-shuttle",
|
|
78
|
-
"slug": "emerald-lake-escape",
|
|
79
|
-
"imageIds": ["emerald-lake-lodge-viewpoint.jpg"],
|
|
80
|
-
"collageImageIds": ["lake-louise-hiker.jpg", "emerald-lake-jump.jpg", "cilantro-on-the-lake-emerald.jpg", "moraine-lake-canoe.jpg"],
|
|
81
|
-
"shortName": "Big 3 Lakes Tour",
|
|
82
|
-
"description": "Full-day adventure to Lake Louise, Emerald Lake, and more. Lunch included. Our most comprehensive tour.",
|
|
83
|
-
"themePage": "emerald-lake"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"productId": "p_urmbvZepRegd",
|
|
88
|
-
"optionIds": ["po_uO9Ra6NHzwof", "po_44EQlZn24gXg", "po_vuzqpvGk6wVS", "po_nIToR7Qwau3E"],
|
|
89
|
-
"productType": "PRIVATE_SHUTTLE",
|
|
90
|
-
"display": {
|
|
91
|
-
"path": "/private-shuttle",
|
|
92
|
-
"slug": "private-tour",
|
|
93
|
-
"imageIds": ["lower-johnston-canyon-falls.jpg"],
|
|
94
|
-
"collageImageIds": ["takakkaw-falls.jpg", "family-sunrise-moraine-lake.jpg", "via-via-shuttle-view.jpg", "moraine-lake-jump-in.jpg"],
|
|
95
|
-
"shortName": "Private Shuttle",
|
|
96
|
-
"description": "Your own private shuttle. Custom itinerary, your pace, your group. Perfect for special occasions.",
|
|
97
|
-
"themePage": "private"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
]
|
|
101
|
-
}
|
package/src/lib/analytics.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Frontend analytics for GA4 and Meta Pixel.
|
|
3
|
-
* Events deduplicate with backend using transaction_id / eventID = Stripe paymentIntent.id.
|
|
4
|
-
* - development/staging: log to console only (no GA4/Meta)
|
|
5
|
-
* - production: send to GA4/Meta when consent is granted
|
|
6
|
-
*/
|
|
7
|
-
import { ENV, isLocalhost, isProduction, shouldLogPurchaseToConsole } from '@/lib/env';
|
|
8
|
-
|
|
9
|
-
const CONSENT_KEY = 'cookie-consent';
|
|
10
|
-
const PENDING_PURCHASE_KEY = 'pending_purchase';
|
|
11
|
-
|
|
12
|
-
declare global {
|
|
13
|
-
interface Window {
|
|
14
|
-
dataLayer?: unknown[];
|
|
15
|
-
gtag?: (...args: unknown[]) => void;
|
|
16
|
-
fbq?: (...args: unknown[]) => void;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Check if user has granted cookie consent. */
|
|
21
|
-
export function hasAnalyticsConsent(): boolean {
|
|
22
|
-
if (typeof window === 'undefined') return false;
|
|
23
|
-
try {
|
|
24
|
-
return localStorage.getItem(CONSENT_KEY) === 'granted';
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Mark consent as granted (call from cookie banner on Accept). */
|
|
31
|
-
export function setAnalyticsConsentGranted(): void {
|
|
32
|
-
if (typeof window === 'undefined') return;
|
|
33
|
-
try {
|
|
34
|
-
localStorage.setItem(CONSENT_KEY, 'granted');
|
|
35
|
-
} catch {
|
|
36
|
-
/* ignore */
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Store purchase data before Stripe redirect.
|
|
42
|
-
* Success page reads payment_intent from URL and fires purchase with transaction_id = payment_intent.
|
|
43
|
-
*/
|
|
44
|
-
export function storePendingPurchase(value: number, currency: string): void {
|
|
45
|
-
if (typeof window === 'undefined') return;
|
|
46
|
-
try {
|
|
47
|
-
sessionStorage.setItem(
|
|
48
|
-
PENDING_PURCHASE_KEY,
|
|
49
|
-
JSON.stringify({ value, currency })
|
|
50
|
-
);
|
|
51
|
-
} catch {
|
|
52
|
-
/* ignore */
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Read and clear pending purchase from sessionStorage. */
|
|
57
|
-
export function consumePendingPurchase(): { value: number; currency: string } | null {
|
|
58
|
-
if (typeof window === 'undefined') return null;
|
|
59
|
-
try {
|
|
60
|
-
const raw = sessionStorage.getItem(PENDING_PURCHASE_KEY);
|
|
61
|
-
sessionStorage.removeItem(PENDING_PURCHASE_KEY);
|
|
62
|
-
if (!raw) return null;
|
|
63
|
-
const parsed = JSON.parse(raw) as { value?: number; currency?: string };
|
|
64
|
-
if (typeof parsed?.value === 'number' && typeof parsed?.currency === 'string') {
|
|
65
|
-
return { value: parsed.value, currency: parsed.currency };
|
|
66
|
-
}
|
|
67
|
-
return null;
|
|
68
|
-
} catch {
|
|
69
|
-
return null;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function safeGtag(...args: unknown[]): void {
|
|
74
|
-
if (typeof window !== 'undefined' && window.gtag) {
|
|
75
|
-
window.gtag(...args);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function safeFbq(...args: unknown[]): void {
|
|
80
|
-
if (typeof window !== 'undefined' && window.fbq) {
|
|
81
|
-
window.fbq(...args);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Fire view_item when user selects or views a product. */
|
|
86
|
-
export function trackViewItem(
|
|
87
|
-
productId: string,
|
|
88
|
-
productName: string,
|
|
89
|
-
price: number,
|
|
90
|
-
currency: string
|
|
91
|
-
): void {
|
|
92
|
-
const data = { productId, productName, price, currency };
|
|
93
|
-
|
|
94
|
-
if (isLocalhost() || shouldLogPurchaseToConsole()) {
|
|
95
|
-
console.log('analytics view_item', data);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!hasAnalyticsConsent()) return;
|
|
100
|
-
|
|
101
|
-
if (ENV.GA4_MEASUREMENT_ID) {
|
|
102
|
-
safeGtag('event', 'view_item', {
|
|
103
|
-
items: [{ item_id: productId, item_name: productName, price, currency }],
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
if (ENV.META_PIXEL_ID) {
|
|
107
|
-
safeFbq('track', 'ViewContent', {
|
|
108
|
-
content_ids: [productId],
|
|
109
|
-
content_name: productName,
|
|
110
|
-
content_type: 'product',
|
|
111
|
-
value: price,
|
|
112
|
-
currency,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface BeginCheckoutItem {
|
|
118
|
-
id: string;
|
|
119
|
-
name: string;
|
|
120
|
-
qty: number;
|
|
121
|
-
price: number;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Fire begin_checkout when checkout modal opens. */
|
|
125
|
-
export function trackBeginCheckout(
|
|
126
|
-
value: number,
|
|
127
|
-
currency: string,
|
|
128
|
-
items: BeginCheckoutItem[]
|
|
129
|
-
): void {
|
|
130
|
-
const data = { value, currency, items };
|
|
131
|
-
|
|
132
|
-
if (isLocalhost() || shouldLogPurchaseToConsole()) {
|
|
133
|
-
console.log('analytics begin_checkout', data);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (!hasAnalyticsConsent()) return;
|
|
138
|
-
|
|
139
|
-
if (ENV.GA4_MEASUREMENT_ID) {
|
|
140
|
-
safeGtag('event', 'begin_checkout', {
|
|
141
|
-
value,
|
|
142
|
-
currency,
|
|
143
|
-
items: items.map((i) => ({
|
|
144
|
-
item_id: i.id,
|
|
145
|
-
item_name: i.name,
|
|
146
|
-
quantity: i.qty,
|
|
147
|
-
price: i.price,
|
|
148
|
-
})),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (ENV.META_PIXEL_ID) {
|
|
152
|
-
safeFbq('track', 'InitiateCheckout', {
|
|
153
|
-
value,
|
|
154
|
-
currency,
|
|
155
|
-
content_ids: items.map((i) => i.id),
|
|
156
|
-
content_type: 'product',
|
|
157
|
-
num_items: items.reduce((s, i) => s + i.qty, 0),
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Fire purchase on success page.
|
|
164
|
-
* transactionId MUST be Stripe paymentIntent.id (from URL param payment_intent) for dedup with backend.
|
|
165
|
-
*/
|
|
166
|
-
export function trackPurchase(
|
|
167
|
-
transactionId: string,
|
|
168
|
-
value: number,
|
|
169
|
-
currency: string,
|
|
170
|
-
items: Array<{ item_id: string; item_name: string; quantity: number; price: number }> = []
|
|
171
|
-
): void {
|
|
172
|
-
const data = { transaction_id: transactionId, value, currency, items };
|
|
173
|
-
|
|
174
|
-
if (isLocalhost() || shouldLogPurchaseToConsole()) {
|
|
175
|
-
console.log('analytics purchase', data);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (!hasAnalyticsConsent()) return;
|
|
180
|
-
|
|
181
|
-
if (ENV.GA4_MEASUREMENT_ID) {
|
|
182
|
-
safeGtag('event', 'purchase', {
|
|
183
|
-
transaction_id: transactionId,
|
|
184
|
-
value,
|
|
185
|
-
currency,
|
|
186
|
-
items,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
if (ENV.META_PIXEL_ID) {
|
|
190
|
-
safeFbq('track', 'Purchase', {
|
|
191
|
-
value,
|
|
192
|
-
currency,
|
|
193
|
-
order_id: transactionId,
|
|
194
|
-
eventID: transactionId,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Channel values the API persists on reservations/bookings (aligned with ticketbooth-be
|
|
3
|
-
* `BookingMarketingSource` + `SourceTrackingService` auto-detect: WEBSITE, AFFILIATE, DASHBOARD,
|
|
4
|
-
* GYG, VIATOR, plus dynamic uppercase `utm_source` strings when no fixed bucket applies).
|
|
5
|
-
*/
|
|
6
|
-
export enum KnownBookingSource {
|
|
7
|
-
WEBSITE = 'WEBSITE',
|
|
8
|
-
/** Main-site partner embed (e.g. `/partner/{slug}`); not the dedicated `booking.*` portal app. */
|
|
9
|
-
PUBLIC_PARTNER_WEBSITE = 'PUBLIC_PARTNER_WEBSITE',
|
|
10
|
-
PARTNER_PORTAL = 'PARTNER_PORTAL',
|
|
11
|
-
AFFILIATE = 'AFFILIATE',
|
|
12
|
-
DASHBOARD = 'DASHBOARD',
|
|
13
|
-
GYG = 'GYG',
|
|
14
|
-
VIATOR = 'VIATOR',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Default reserve/checkout `source` when the client does not send a more specific channel. */
|
|
18
|
-
export const DEFAULT_BOOKING_SOURCE = KnownBookingSource.WEBSITE;
|
|
19
|
-
|
|
20
|
-
/** Client `source` when booking through the signed-in partner org / marketing partner flows. */
|
|
21
|
-
export const PARTNER_PORTAL_BOOKING_SOURCE = KnownBookingSource.PARTNER_PORTAL;
|
|
22
|
-
|
|
23
|
-
const PUBLIC_BOOKING_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* True when metadata carries a partner org id or a normalized public marketing slug (same shape
|
|
27
|
-
* as backend `publicBookingSlug`), independent of hostname or URL path — use with route-level
|
|
28
|
-
* `canonicalAttribution.partnerSlug` so localhost and stripped URLs still attribute.
|
|
29
|
-
*/
|
|
30
|
-
export function mergedMetadataImpliesPartnerPortal(
|
|
31
|
-
merged: Partial<{ partnerId?: string; partnerSlug?: string }>,
|
|
32
|
-
): boolean {
|
|
33
|
-
const pid = typeof merged.partnerId === 'string' ? merged.partnerId.trim() : '';
|
|
34
|
-
if (pid.startsWith('par_')) return true;
|
|
35
|
-
const slug = typeof merged.partnerSlug === 'string' ? merged.partnerSlug.trim().toLowerCase() : '';
|
|
36
|
-
return slug.length > 0 && PUBLIC_BOOKING_SLUG_RE.test(slug);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Fixed client channel from product / option identifiers (GYG / Viator embeds). Everything else
|
|
41
|
-
* is treated as main-site {@link KnownBookingSource.WEBSITE}; AFFILIATE/DASHBOARD are set server-side.
|
|
42
|
-
*/
|
|
43
|
-
export function inferClientBookingSourceFromProductIds(
|
|
44
|
-
productId: string,
|
|
45
|
-
productOptionId?: string | null,
|
|
46
|
-
): KnownBookingSource {
|
|
47
|
-
const haystack = `${productId} ${productOptionId ?? ''}`.toLowerCase();
|
|
48
|
-
if (haystack.includes('gyg_')) return KnownBookingSource.GYG;
|
|
49
|
-
if (haystack.includes('viator_')) return KnownBookingSource.VIATOR;
|
|
50
|
-
return KnownBookingSource.WEBSITE;
|
|
51
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { CheckoutBreakdown, CheckoutReceiptLine } from '@/lib/booking-api';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Round to 2 decimal places. Used so breakdown amounts and totals are stable
|
|
5
|
-
* and pass backend validation (sum of line items ≈ totalAmount within 0.02).
|
|
6
|
-
*/
|
|
7
|
-
export function round2(n: number): number {
|
|
8
|
-
return Math.round(n * 100) / 100;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface CheckoutLineInput {
|
|
12
|
-
label: string;
|
|
13
|
-
amount: number;
|
|
14
|
-
type: string;
|
|
15
|
-
quantity?: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Line types for checkout breakdown. ADDITIONAL_HOURS reserved for future private shuttle extra hours. */
|
|
19
|
-
export const CheckoutLineType = {
|
|
20
|
-
TICKET: 'TICKET',
|
|
21
|
-
DEPOSIT: 'DEPOSIT',
|
|
22
|
-
FEE: 'FEE',
|
|
23
|
-
RETURN_OPTION: 'return',
|
|
24
|
-
CANCELLATION_UPGRADE: 'cancellation',
|
|
25
|
-
TAX: 'TAX',
|
|
26
|
-
PROMO_CODE: 'PROMO_CODE',
|
|
27
|
-
ROUNDING: 'ROUNDING',
|
|
28
|
-
ADDITIONAL_HOURS: 'ADDITIONAL_HOURS', // Future: private shuttle extra hours line
|
|
29
|
-
} as const;
|
|
30
|
-
|
|
31
|
-
export interface BuildCheckoutBreakdownParams {
|
|
32
|
-
/** Line items in display order (tickets, return, cancellation, fees, tax, promo, etc.) */
|
|
33
|
-
lines: CheckoutLineInput[];
|
|
34
|
-
/** Total amount to charge (will be rounded to 2 decimals). */
|
|
35
|
-
totalAmount: number;
|
|
36
|
-
currency: string;
|
|
37
|
-
/** Label for rounding line when added (e.g. "Rounding"). */
|
|
38
|
-
roundingLabel: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Builds a CheckoutBreakdown from the given lines and total.
|
|
43
|
-
* Rounds each line amount to 2 decimals; if the sum differs from total by more than 0.02,
|
|
44
|
-
* adds a rounding line so the backend validator accepts it.
|
|
45
|
-
* Reused by BookingFlow and PrivateShuttleBookingFlow so Stripe and /manage match the UI.
|
|
46
|
-
*/
|
|
47
|
-
export function buildCheckoutBreakdown(params: BuildCheckoutBreakdownParams): CheckoutBreakdown {
|
|
48
|
-
const { lines, totalAmount, currency, roundingLabel } = params;
|
|
49
|
-
const totalRounded = round2(totalAmount);
|
|
50
|
-
const lineItems: CheckoutReceiptLine[] = lines.map((line) => ({
|
|
51
|
-
label: line.label,
|
|
52
|
-
amount: round2(line.amount),
|
|
53
|
-
type: line.type,
|
|
54
|
-
quantity: line.quantity,
|
|
55
|
-
}));
|
|
56
|
-
const sumLines = lineItems.reduce((s, l) => s + l.amount, 0);
|
|
57
|
-
if (Math.abs(sumLines - totalRounded) > 0.02) {
|
|
58
|
-
lineItems.push({
|
|
59
|
-
label: roundingLabel,
|
|
60
|
-
amount: round2(totalRounded - sumLines),
|
|
61
|
-
type: 'ROUNDING',
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
return {
|
|
65
|
-
lineItems,
|
|
66
|
-
totalAmount: totalRounded,
|
|
67
|
-
currency,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One opaque id per browser tab/session for joining client telemetry → TicketBooth Lambda logs.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export const BOOKING_CORRELATION_HEADER = 'X-Correlation-Id';
|
|
6
|
-
|
|
7
|
-
const STORAGE_KEY = 'tb_booking_correlation_id';
|
|
8
|
-
|
|
9
|
-
function newCorrelationId(): string {
|
|
10
|
-
try {
|
|
11
|
-
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
12
|
-
return crypto.randomUUID();
|
|
13
|
-
}
|
|
14
|
-
} catch {
|
|
15
|
-
/* fall through */
|
|
16
|
-
}
|
|
17
|
-
return `${Date.now()}-${Math.random().toString(36).slice(2, 12)}`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Stable for the lifetime of this tab's sessionStorage (until the tab closes / storage cleared). */
|
|
21
|
-
export function getOrCreateBookingCorrelationId(): string {
|
|
22
|
-
if (typeof window === 'undefined') return '';
|
|
23
|
-
try {
|
|
24
|
-
let id = sessionStorage.getItem(STORAGE_KEY);
|
|
25
|
-
if (!id) {
|
|
26
|
-
id = newCorrelationId();
|
|
27
|
-
sessionStorage.setItem(STORAGE_KEY, id);
|
|
28
|
-
}
|
|
29
|
-
return id;
|
|
30
|
-
} catch {
|
|
31
|
-
return newCorrelationId();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Merge correlation header into outbound API headers (browser only). */
|
|
36
|
-
export function withBookingCorrelationId(
|
|
37
|
-
headers: Record<string, string>
|
|
38
|
-
): Record<string, string> {
|
|
39
|
-
if (typeof window === 'undefined') return headers;
|
|
40
|
-
const id = getOrCreateBookingCorrelationId();
|
|
41
|
-
if (!id) return headers;
|
|
42
|
-
return {
|
|
43
|
-
...headers,
|
|
44
|
-
[BOOKING_CORRELATION_HEADER]: id,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* i18n Configuration
|
|
3
|
-
*
|
|
4
|
-
* This file sets up the internationalization configuration.
|
|
5
|
-
* Currently supports English (en) as default, ready to add more languages.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export const locales = ['en', 'fr'] as const;
|
|
9
|
-
export type Locale = (typeof locales)[number];
|
|
10
|
-
|
|
11
|
-
export const defaultLocale: Locale = 'en';
|
|
12
|
-
|
|
13
|
-
// Language display names
|
|
14
|
-
export const languageNames: Record<Locale, string> = {
|
|
15
|
-
en: 'English',
|
|
16
|
-
fr: 'Français',
|
|
17
|
-
// Add more languages here:
|
|
18
|
-
// es: 'Español',
|
|
19
|
-
// de: 'Deutsch',
|
|
20
|
-
};
|
|
21
|
-
|