@bigz-app/booking-widget 1.3.1 → 1.3.3

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.
@@ -12134,121 +12134,145 @@
12134
12134
  }
12135
12135
 
12136
12136
  /**
12137
- * Google Ads Conversion Tracking Utility
12137
+ * Google Ads Tracking Utility
12138
12138
  *
12139
- * Simplified utility that waits 1500ms, checks/initializes gtag, and sends conversion.
12140
- */
12141
- /**
12142
- * Check if gtag is available in current or parent window
12139
+ * Handles pageview tracking (widget load) and conversion tracking (successful booking).
12140
+ * Supports both direct gtag.js and GTM dataLayer setups.
12143
12141
  */
12144
12142
  function isGtagAvailable() {
12145
- if (typeof window === "undefined") {
12143
+ if (typeof window === "undefined")
12146
12144
  return false;
12147
- }
12148
- // Check current window
12149
- if (typeof window.gtag === "function") {
12145
+ if (typeof window.gtag === "function")
12150
12146
  return true;
12151
- }
12152
- // Check parent window (for iframe/widget scenarios)
12153
12147
  if (window !== window.parent) {
12154
12148
  try {
12155
- if (typeof window.parent?.gtag === "function") {
12149
+ if (typeof window.parent?.gtag === "function")
12156
12150
  return true;
12157
- }
12158
12151
  }
12159
12152
  catch {
12160
- // Cannot access parent window (cross-origin)
12153
+ // Cross-origin
12161
12154
  }
12162
12155
  }
12163
12156
  return false;
12164
12157
  }
12165
- /**
12166
- * Initialize gtag if not already available
12167
- */
12168
12158
  function initializeGtag(tagId) {
12169
- if (typeof window === "undefined") {
12159
+ if (typeof window === "undefined" || isGtagAvailable())
12170
12160
  return;
12171
- }
12172
- // Skip if gtag already exists
12173
- if (isGtagAvailable()) {
12174
- return;
12175
- }
12176
- // Initialize dataLayer and gtag function
12177
12161
  window.dataLayer = window.dataLayer || [];
12178
12162
  window.gtag = (...args) => {
12179
12163
  window.dataLayer.push(args);
12180
12164
  };
12181
- // Set current timestamp
12182
12165
  window.gtag("js", new Date());
12183
- // Load gtag script
12184
12166
  const script = document.createElement("script");
12185
12167
  script.async = true;
12186
12168
  script.src = `https://www.googletagmanager.com/gtag/js?id=${tagId}`;
12187
12169
  document.head.appendChild(script);
12188
- // Configure the tag
12189
12170
  window.gtag("config", tagId, {
12190
12171
  anonymize_ip: true,
12191
12172
  allow_google_signals: false,
12192
12173
  allow_ad_personalization_signals: false,
12193
12174
  });
12194
12175
  }
12195
- /**
12196
- * Send conversion event using available gtag
12197
- */
12198
- function sendConversion(config) {
12199
- if (typeof window === "undefined") {
12200
- return;
12176
+ function getGtag() {
12177
+ if (typeof window === "undefined")
12178
+ return null;
12179
+ if (typeof window.gtag === "function") {
12180
+ return window.gtag;
12201
12181
  }
12202
- let gtag = window.gtag;
12203
- // Try parent window gtag if current window doesn't have it
12204
- if (typeof gtag !== "function" && window !== window.parent) {
12182
+ if (window !== window.parent) {
12205
12183
  try {
12206
- gtag = window.parent?.gtag;
12184
+ const parentGtag = window.parent?.gtag;
12185
+ if (typeof parentGtag === "function")
12186
+ return parentGtag;
12207
12187
  }
12208
12188
  catch {
12209
- // Cannot access parent window (cross-origin)
12189
+ // Cross-origin
12210
12190
  }
12211
12191
  }
12212
- if (typeof gtag !== "function") {
12192
+ return null;
12193
+ }
12194
+ /**
12195
+ * Push an event to the dataLayer for GTM visibility,
12196
+ * then also fire via gtag for direct Google Ads tracking.
12197
+ */
12198
+ function sendEvent(eventName, params) {
12199
+ if (typeof window === "undefined")
12213
12200
  return;
12201
+ // GTM dataLayer push (object format — visible in Tag Assistant)
12202
+ window.dataLayer = window.dataLayer || [];
12203
+ window.dataLayer.push({
12204
+ event: eventName,
12205
+ ...params,
12206
+ });
12207
+ // gtag call (array format — processed by gtag.js for Google Ads)
12208
+ const gtag = getGtag();
12209
+ if (gtag) {
12210
+ gtag("event", eventName, params);
12214
12211
  }
12215
- // Build conversion data
12216
- const conversionData = {
12212
+ }
12213
+ function sendConversion(config) {
12214
+ const params = {
12217
12215
  send_to: `${config.tagId}/${config.conversionId}`,
12218
12216
  };
12219
- // Add optional parameters
12220
12217
  if (config.conversionValue !== undefined) {
12221
- conversionData.value = config.conversionValue;
12218
+ params.value = config.conversionValue;
12222
12219
  }
12223
12220
  if (config.conversionCurrency) {
12224
- conversionData.currency = config.conversionCurrency;
12221
+ params.currency = config.conversionCurrency;
12225
12222
  }
12226
12223
  if (config.transactionId) {
12227
- conversionData.transaction_id = config.transactionId;
12224
+ params.transaction_id = config.transactionId;
12228
12225
  }
12229
- // Send conversion event
12230
- gtag("event", "conversion", conversionData);
12226
+ sendEvent("conversion", params);
12231
12227
  }
12232
12228
  /**
12233
- * Main function to handle Google Ads conversion tracking
12234
- * Waits 1500ms, checks/initializes gtag, then sends conversion
12229
+ * Track widget pageview (fired once on widget mount).
12235
12230
  */
12236
- function handleGoogleAdsConversion(config) {
12237
- // Validate required config
12238
- if (!config.tagId || !config.conversionId) {
12231
+ function handleGoogleAdsPageview(tagId, consent) {
12232
+ if (!tagId || false || typeof window === "undefined")
12239
12233
  return;
12234
+ if (!isGtagAvailable()) {
12235
+ initializeGtag(tagId);
12240
12236
  }
12241
- // Wait 1500ms before proceeding
12237
+ const fire = () => sendEvent("widget_pageview", {
12238
+ send_to: tagId,
12239
+ page_location: window.location.href,
12240
+ page_title: document.title,
12241
+ });
12242
+ if (isGtagAvailable()) {
12243
+ fire();
12244
+ return;
12245
+ }
12246
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${tagId}"]`);
12247
+ if (script) {
12248
+ script.addEventListener("load", fire, { once: true });
12249
+ }
12250
+ }
12251
+ /**
12252
+ * Handle Google Ads conversion tracking.
12253
+ * Waits 1500ms for the success page to settle, then fires.
12254
+ */
12255
+ function handleGoogleAdsConversion(config) {
12256
+ if (!config.tagId || !config.conversionId)
12257
+ return;
12242
12258
  setTimeout(() => {
12243
- // Check if gtag is available, initialize if not
12244
- if (!isGtagAvailable()) {
12245
- initializeGtag(config.tagId);
12259
+ if (isGtagAvailable()) {
12260
+ sendConversion(config);
12261
+ return;
12262
+ }
12263
+ initializeGtag(config.tagId);
12264
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${config.tagId}"]`);
12265
+ if (script) {
12266
+ script.addEventListener("load", () => sendConversion(config));
12267
+ script.addEventListener("error", () => sendConversion(config));
12268
+ }
12269
+ else {
12270
+ sendConversion(config);
12246
12271
  }
12247
- sendConversion(config);
12248
12272
  }, 1500);
12249
12273
  }
12250
12274
 
12251
- const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, }) => {
12275
+ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, googleAdsConfig: googleAdsConfigProp, }) => {
12252
12276
  const t = useTranslations();
12253
12277
  const { locale } = useLocale();
12254
12278
  const timezone = useTimezone();
@@ -12297,20 +12321,16 @@
12297
12321
  });
12298
12322
  setPaymentStatus(data.stripePaymentIntent?.status || data.order.status);
12299
12323
  const finalPaymentStatus = data.stripePaymentIntent?.status || data.order.status;
12324
+ const adsConfig = googleAdsConfigProp ?? data.googleAdsConfig;
12300
12325
  if (finalPaymentStatus === "succeeded" &&
12301
- config.googleAds?.tagId &&
12302
- config.googleAds?.conversionId &&
12303
- config.googleAds?.consent !== false) {
12304
- // Prepare conversion tracking data
12305
- const conversionValue = data.order.total / 100;
12306
- const transactionId = data.order.id;
12307
- // Track the conversion
12326
+ adsConfig?.tagId &&
12327
+ adsConfig?.conversionId) {
12308
12328
  handleGoogleAdsConversion({
12309
- tagId: config.googleAds.tagId,
12310
- conversionId: config.googleAds.conversionId,
12311
- conversionValue,
12312
- conversionCurrency: config.googleAds.conversionCurrency || "EUR",
12313
- transactionId,
12329
+ tagId: adsConfig.tagId,
12330
+ conversionId: adsConfig.conversionId,
12331
+ conversionValue: data.order.total / 100,
12332
+ conversionCurrency: adsConfig.conversionCurrency || "EUR",
12333
+ transactionId: data.order.id,
12314
12334
  });
12315
12335
  }
12316
12336
  }
@@ -15496,6 +15516,88 @@
15496
15516
  return (u$2(Sidebar, { isOpen: isOpen, onClose: onClose, title: t("upsells.title"), footer: footerContent, children: u$2("div", { style: { display: "flex", flexDirection: "column", height: "100%", padding: "16px 16px" }, children: [isLoading && (u$2("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "12px", padding: "40px 20px", ...textStyles.muted }, children: [spinner(), u$2("span", { children: t("upsells.loading") })] })), !isLoading && upsells.length === 0 && (u$2("div", { style: { textAlign: "center", padding: "40px 20px", ...textStyles.muted }, children: u$2("p", { children: t("upsells.noExtras") }) })), !isLoading && upsells.length > 0 && (u$2("div", { style: { display: "flex", flexDirection: "column", gap: "12px", flex: 1, overflowY: "auto", paddingBottom: "16px" }, children: upsells.map((upsell) => (u$2(UpsellCard, { upsell: upsell, isSelected: isSelected(upsell.id), participantCount: participantCount, onSelect: () => selectUpsell(upsell.id) }, upsell.id))) })), selectedCount > 0 && (u$2("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: "16px", paddingBottom: "16px", paddingTop: "16px", borderTop: "1px solid var(--bw-border-color)", fontSize: "14px" }, children: [u$2("span", { style: textStyles.muted, children: selectedCount === 1 ? t("upsells.selected", { count: selectedCount }) : t("upsells.selectedPlural", { count: selectedCount }) }), u$2("span", { style: { fontWeight: 600, color: "var(--bw-highlight-color)", fontFamily: "var(--bw-font-family)" }, children: ["+", formatCurrency(selectedTotal)] })] }))] }) }));
15497
15517
  }
15498
15518
 
15519
+ /**
15520
+ * Widget analytics — server-side PostHog tracking via batched sendBeacon.
15521
+ *
15522
+ * Events are buffered locally and flushed every few seconds (or on page
15523
+ * unload) as a single POST to `/api/booking/track`. This avoids extra
15524
+ * network requests on every click while still capturing a complete funnel.
15525
+ */
15526
+ let buffer = [];
15527
+ let flushTimer = null;
15528
+ let currentApiBaseUrl = "";
15529
+ let currentOrganizationId = "";
15530
+ const FLUSH_INTERVAL_MS = 3000;
15531
+ function flush() {
15532
+ if (buffer.length === 0)
15533
+ return;
15534
+ const payload = JSON.stringify({
15535
+ organizationId: currentOrganizationId,
15536
+ events: buffer,
15537
+ });
15538
+ buffer = [];
15539
+ const url = getApiUrl(currentApiBaseUrl, "/booking/track");
15540
+ // Use fetch with keepalive as primary — works cross-origin with CORS.
15541
+ // sendBeacon silently fails cross-origin with application/json because
15542
+ // it can't do CORS preflight, so we only use it as a text/plain fallback
15543
+ // on page unload when fetch may be aborted.
15544
+ try {
15545
+ void fetch(url, {
15546
+ method: "POST",
15547
+ headers: { "Content-Type": "application/json" },
15548
+ body: payload,
15549
+ keepalive: true,
15550
+ });
15551
+ }
15552
+ catch {
15553
+ // fetch failed (e.g. during page unload) — try sendBeacon with text/plain
15554
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
15555
+ const blob = new Blob([payload], { type: "text/plain" });
15556
+ navigator.sendBeacon(url, blob);
15557
+ }
15558
+ }
15559
+ }
15560
+ function scheduleFlush() {
15561
+ if (flushTimer)
15562
+ return;
15563
+ flushTimer = setTimeout(() => {
15564
+ flushTimer = null;
15565
+ flush();
15566
+ }, FLUSH_INTERVAL_MS);
15567
+ }
15568
+ /**
15569
+ * Initialise the analytics module. Must be called once before `trackEvent`.
15570
+ */
15571
+ function initAnalytics(apiBaseUrl, organizationId) {
15572
+ currentApiBaseUrl = apiBaseUrl;
15573
+ currentOrganizationId = organizationId;
15574
+ if (typeof window !== "undefined") {
15575
+ window.addEventListener("pagehide", flush);
15576
+ window.addEventListener("visibilitychange", () => {
15577
+ if (document.visibilityState === "hidden")
15578
+ flush();
15579
+ });
15580
+ }
15581
+ }
15582
+ /**
15583
+ * Queue a widget event. Non-blocking, fire-and-forget.
15584
+ */
15585
+ function trackEvent(event, properties = {}) {
15586
+ if (typeof window === "undefined")
15587
+ return;
15588
+ if (!currentOrganizationId)
15589
+ return;
15590
+ buffer.push({
15591
+ event,
15592
+ properties,
15593
+ domain: window.location.hostname,
15594
+ url: window.location.href,
15595
+ referrer: document.referrer,
15596
+ timestamp: new Date().toISOString(),
15597
+ });
15598
+ scheduleFlush();
15599
+ }
15600
+
15499
15601
  // Main widget component
15500
15602
  function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onTimezone, }) {
15501
15603
  const t = useTranslations();
@@ -15575,6 +15677,13 @@
15575
15677
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = d$1(false);
15576
15678
  const [shouldRenderUpsells, setShouldRenderUpsells] = d$1(false);
15577
15679
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = d$1(false);
15680
+ // Google Ads config (received from API, set once from the first API response)
15681
+ const [googleAdsConfig, setGoogleAdsConfig] = d$1(null);
15682
+ const extractGoogleAdsConfig = (data) => {
15683
+ if (!googleAdsConfig && data?.googleAdsConfig?.tagId) {
15684
+ setGoogleAdsConfig(data.googleAdsConfig);
15685
+ }
15686
+ };
15578
15687
  // Promo dialog state
15579
15688
  const [showPromoDialog, setShowPromoDialog] = d$1(false);
15580
15689
  const [widgetContainerRef, setWidgetContainerRef] = d$1(null);
@@ -15658,6 +15767,7 @@
15658
15767
  image: resolvedImage,
15659
15768
  };
15660
15769
  setVoucherConfig(mergedConfig);
15770
+ extractGoogleAdsConfig(data);
15661
15771
  setVoucherEventTypes(data.eventTypes || []);
15662
15772
  // Set system config for payment processing
15663
15773
  if (data.paymentProvider) {
@@ -15680,6 +15790,29 @@
15680
15790
  setIsLoadingVoucherConfig(false);
15681
15791
  }
15682
15792
  };
15793
+ // Initialise analytics once on mount
15794
+ const analyticsInitRef = A$2(false);
15795
+ y$1(() => {
15796
+ if (!analyticsInitRef.current && config.organizationId) {
15797
+ analyticsInitRef.current = true;
15798
+ initAnalytics(config.apiBaseUrl, config.organizationId);
15799
+ trackEvent("widget_loaded", {
15800
+ viewMode,
15801
+ eventTypeId: config.eventTypeId,
15802
+ categoryId: config.categoryId,
15803
+ eventInstanceId: config.eventInstanceId,
15804
+ isStandaloneVoucherMode,
15805
+ });
15806
+ }
15807
+ }, [config.organizationId, config.apiBaseUrl]);
15808
+ // Fire widget pageview once when Google Ads config is received from API
15809
+ const pageviewFiredRef = A$2(false);
15810
+ y$1(() => {
15811
+ if (!pageviewFiredRef.current && googleAdsConfig?.tagId) {
15812
+ pageviewFiredRef.current = true;
15813
+ handleGoogleAdsPageview(googleAdsConfig.tagId);
15814
+ }
15815
+ }, [googleAdsConfig]);
15683
15816
  // Determine initial step and load data
15684
15817
  y$1(() => {
15685
15818
  const initializeWidget = async () => {
@@ -15768,6 +15901,7 @@
15768
15901
  });
15769
15902
  const voucherData = await voucherResponse.json();
15770
15903
  if (voucherResponse.ok && voucherData.voucherResult) {
15904
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "stripe_redirect" });
15771
15905
  setVoucherPurchaseResult(voucherData.voucherResult);
15772
15906
  setIsSuccess(true);
15773
15907
  setSuccessPaymentId(null);
@@ -15777,6 +15911,7 @@
15777
15911
  catch {
15778
15912
  // Fall back to booking success flow if voucher lookup fails.
15779
15913
  }
15914
+ trackEvent("booking_completed", { paymentIntentId: stripeReturn.paymentIntent, source: "stripe_redirect" });
15780
15915
  setVoucherPurchaseResult(null);
15781
15916
  setSuccessPaymentId(stripeReturn.paymentIntent);
15782
15917
  setIsSuccess(true);
@@ -15814,6 +15949,7 @@
15814
15949
  });
15815
15950
  const voucherData = await voucherResponse.json();
15816
15951
  if (voucherResponse.ok && voucherData.voucherResult) {
15952
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "mollie_redirect" });
15817
15953
  setVoucherPurchaseResult(voucherData.voucherResult);
15818
15954
  setIsSuccess(true);
15819
15955
  setSuccessPaymentId(null);
@@ -15852,6 +15988,7 @@
15852
15988
  window[globalFlagKey] = true;
15853
15989
  const timer = setTimeout(() => {
15854
15990
  setShowPromoDialog(true);
15991
+ trackEvent("promo_dialog_shown", { discountCode: config.promo?.discountCode });
15855
15992
  }, 1000);
15856
15993
  return () => clearTimeout(timer);
15857
15994
  }, [config.promo?.enabled, config.promo?.discountCode]);
@@ -15861,6 +15998,7 @@
15861
15998
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
15862
15999
  };
15863
16000
  const handlePromoCtaClick = () => {
16001
+ trackEvent("promo_cta_clicked", { discountCode: config.promo?.discountCode });
15864
16002
  setShowPromoDialog(false);
15865
16003
  const promoId = config.promo?.discountCode || "default";
15866
16004
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
@@ -15893,7 +16031,9 @@
15893
16031
  onWidgetLanguage?.(wl);
15894
16032
  onTimezone?.(wl.timezone);
15895
16033
  }
16034
+ extractGoogleAdsConfig(data);
15896
16035
  setEventTypes(data.eventTypes);
16036
+ trackEvent("event_types_loaded", { count: data.eventTypes.length });
15897
16037
  if (isSingleEventTypeMode && data.eventTypes.length === 1) {
15898
16038
  setSelectedEventType(data.eventTypes[0]);
15899
16039
  await loadEventInstances(data.eventTypes[0].id);
@@ -15929,6 +16069,7 @@
15929
16069
  onWidgetLanguage?.(wl);
15930
16070
  onTimezone?.(wl.timezone);
15931
16071
  }
16072
+ extractGoogleAdsConfig(data);
15932
16073
  setUpcomingEvents(data.upcomingEvents || []);
15933
16074
  }
15934
16075
  else {
@@ -15964,6 +16105,7 @@
15964
16105
  onWidgetLanguage?.(wl);
15965
16106
  onTimezone?.(wl.timezone);
15966
16107
  }
16108
+ extractGoogleAdsConfig(data);
15967
16109
  setSpecials(data.specials || []);
15968
16110
  }
15969
16111
  else {
@@ -15990,6 +16132,7 @@
15990
16132
  onWidgetLanguage?.(wl);
15991
16133
  onTimezone?.(wl.timezone);
15992
16134
  }
16135
+ extractGoogleAdsConfig(data);
15993
16136
  setEventInstances(data.eventInstances);
15994
16137
  if (data.paymentProvider) {
15995
16138
  setSystemConfig({
@@ -16050,6 +16193,7 @@
16050
16193
  onWidgetLanguage?.(wl);
16051
16194
  onTimezone?.(wl.timezone);
16052
16195
  }
16196
+ extractGoogleAdsConfig(data);
16053
16197
  setEventDetails(data.eventDetails);
16054
16198
  setSystemConfig({
16055
16199
  paymentProvider: data.paymentProvider,
@@ -16119,6 +16263,7 @@
16119
16263
  onWidgetLanguage?.(wl);
16120
16264
  onTimezone?.(wl.timezone);
16121
16265
  }
16266
+ extractGoogleAdsConfig(data);
16122
16267
  return data.upsells || [];
16123
16268
  }
16124
16269
  else {
@@ -16144,6 +16289,7 @@
16144
16289
  }
16145
16290
  // Event type selection handlers
16146
16291
  const handleEventTypeSelect = async (eventType) => {
16292
+ trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16147
16293
  setSelectedEventType(eventType);
16148
16294
  setCurrentStep("eventInstances");
16149
16295
  setShouldRenderInstanceSelection(true);
@@ -16157,6 +16303,7 @@
16157
16303
  };
16158
16304
  // Event instance selection handlers
16159
16305
  const handleEventInstanceSelect = async (eventInstance) => {
16306
+ trackEvent("event_instance_selected", { eventInstanceId: eventInstance.id, eventInstanceName: eventInstance.name });
16160
16307
  setSelectedEventInstance(eventInstance);
16161
16308
  bookingReturnStep.current = "eventInstances";
16162
16309
  // Set default participant count for upsell calculations
@@ -16169,9 +16316,8 @@
16169
16316
  try {
16170
16317
  const availableUpsells = await loadUpsells(selectedEventType.id, eventInstance.id, defaultParticipantCount);
16171
16318
  if (availableUpsells.length > 0) {
16172
- // Show upsells step
16319
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16173
16320
  setUpsells(availableUpsells);
16174
- // Pre-select default-checked upsells
16175
16321
  const defaultSelections = availableUpsells
16176
16322
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16177
16323
  .map((upsell) => ({
@@ -16181,7 +16327,7 @@
16181
16327
  setSelectedUpsells(defaultSelections);
16182
16328
  setCurrentStep("upsells");
16183
16329
  setIsLoadingUpsells(false);
16184
- return; // Don't proceed to booking yet
16330
+ return;
16185
16331
  }
16186
16332
  }
16187
16333
  catch (err) {
@@ -16191,7 +16337,7 @@
16191
16337
  setIsLoadingUpsells(false);
16192
16338
  }
16193
16339
  }
16194
- // No upsells available, go directly to booking
16340
+ trackEvent("booking_form_opened", { fromUpsells: false });
16195
16341
  setCurrentStep("booking");
16196
16342
  setShouldRenderBookingForm(true);
16197
16343
  setIsLoadingEventDetails(true);
@@ -16214,6 +16360,7 @@
16214
16360
  setEventDetails(null);
16215
16361
  };
16216
16362
  const handleBookingSuccess = (result) => {
16363
+ trackEvent("booking_completed", { paymentIntentId: result.paymentIntent?.id });
16217
16364
  setIsSuccess(true);
16218
16365
  setSuccessPaymentId(result.paymentIntent.id);
16219
16366
  setSidebarOpen(false);
@@ -16229,7 +16376,7 @@
16229
16376
  setSelectedUpsells(selections);
16230
16377
  };
16231
16378
  const handleUpsellsContinue = async () => {
16232
- // Move to booking step
16379
+ trackEvent("booking_form_opened", { fromUpsells: true });
16233
16380
  setCurrentStep("booking");
16234
16381
  setShouldRenderBookingForm(true);
16235
16382
  setIsLoadingEventDetails(true);
@@ -16249,8 +16396,8 @@
16249
16396
  };
16250
16397
  // Voucher purchase handlers
16251
16398
  const handleVoucherCardClick = async () => {
16399
+ trackEvent("voucher_card_clicked");
16252
16400
  setPreselectedVoucherEventTypeId(null);
16253
- // Ensure voucher config and event types are loaded before opening the form
16254
16401
  if (!voucherConfig || voucherEventTypes.length === 0) {
16255
16402
  await loadVoucherConfig();
16256
16403
  }
@@ -16269,6 +16416,7 @@
16269
16416
  setPreselectedVoucherEventTypeId(null);
16270
16417
  };
16271
16418
  const handleVoucherSuccess = (result) => {
16419
+ trackEvent("voucher_purchased", { voucherType: result.voucherType });
16272
16420
  setVoucherPurchaseResult(result);
16273
16421
  setIsVoucherFormOpen(false);
16274
16422
  setIsSuccess(true);
@@ -16351,8 +16499,8 @@
16351
16499
  try {
16352
16500
  const availableUpsells = await loadUpsells(eventTypeForUpsells.id, eventInstanceId, defaultParticipantCount);
16353
16501
  if (availableUpsells.length > 0) {
16502
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16354
16503
  setUpsells(availableUpsells);
16355
- // Pre-select default-checked upsells
16356
16504
  const defaultSelections = availableUpsells
16357
16505
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16358
16506
  .map((upsell) => ({
@@ -16362,7 +16510,6 @@
16362
16510
  setSelectedUpsells(defaultSelections);
16363
16511
  setCurrentStep("upsells");
16364
16512
  setIsLoadingUpsells(false);
16365
- // Load event details in background for when user continues past upsells
16366
16513
  void loadEventDetails(eventInstanceId);
16367
16514
  return;
16368
16515
  }
@@ -16532,7 +16679,7 @@
16532
16679
  url.searchParams.delete("mollie_payment_id");
16533
16680
  url.searchParams.delete("mollie_status");
16534
16681
  window.history.replaceState({}, "", url.toString());
16535
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16682
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16536
16683
  }
16537
16684
  if (viewMode === "specials" && showingPreview) {
16538
16685
  return (u$2(StyleProvider, { config: config, children: [u$2("div", { ref: setWidgetContainerRef, children: [u$2(SpecialsView, { specials: specials, onEventSelect: handleUpcomingEventSelect, isLoading: isLoadingSpecials, showSavingsAmount: config.specialsSettings?.showSavingsAmount ?? true, showSavingsPercent: config.specialsSettings?.showSavingsPercent ?? false, emptyStateText: config.specialsSettings?.emptyStateText }), shouldRenderBookingForm && eventDetails && (u$2(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, onBackToEventInstances: () => {
@@ -16557,7 +16704,7 @@
16557
16704
  setShouldRenderBookingForm(false);
16558
16705
  setSelectedUpsells([]);
16559
16706
  setUpsells([]);
16560
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16707
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16561
16708
  }
16562
16709
  if (viewMode === "next-events" && !showingPreview && currentStep === "eventInstances") {
16563
16710
  return (u$2(StyleProvider, { config: config, children: [u$2("div", { ref: setWidgetContainerRef, children: [shouldRenderInstanceSelection && (u$2(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => {
@@ -16580,7 +16727,7 @@
16580
16727
  url.searchParams.delete("mollie_payment_id");
16581
16728
  url.searchParams.delete("mollie_status");
16582
16729
  window.history.replaceState({}, "", url.toString());
16583
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16730
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16584
16731
  }
16585
16732
  if (viewMode === "button" && (isSingleEventTypeMode || isDirectInstanceMode)) {
16586
16733
  return (u$2(StyleProvider, { config: config, children: [u$2("div", { ref: setWidgetContainerRef, style: {
@@ -16626,7 +16773,7 @@
16626
16773
  url.searchParams.delete("mollie_payment_id");
16627
16774
  url.searchParams.delete("mollie_status");
16628
16775
  window.history.replaceState({}, "", url.toString());
16629
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16776
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (u$2(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16630
16777
  }
16631
16778
  // Cards mode (default) - show event type selection with optional voucher card
16632
16779
  const cardsView = (u$2(k$3, { children: [hasEventSelection && (u$2(EventTypeSelection, { eventTypes: eventTypes, onEventTypeSelect: handleEventTypeSelect, onInstancePreview: (instanceId, eventTypeId) => void handleUpcomingEventSelect(instanceId, eventTypeId), isLoading: isLoading, skeletonCount: getSkeletonCount(), showVoucherAttachment: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled && !isStandaloneVoucherMode), onVoucherClick: handleVoucherAttachmentClick })), isStandaloneVoucherMode && (u$2(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: false, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: null, voucherPurchaseResult: null, isSuccess: false, showStandaloneCard: Boolean(voucherConfig?.enabled && voucherCardIntegrationEnabled), onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => { } })), isStandaloneVoucherMode && isLoading && !voucherConfig && (u$2("div", { style: { padding: "24px", textAlign: "center" }, children: u$2("div", { style: {
@@ -16682,7 +16829,7 @@
16682
16829
  url.searchParams.delete("mollie_payment_id");
16683
16830
  url.searchParams.delete("mollie_status");
16684
16831
  window.history.replaceState({}, "", url.toString());
16685
- }, config: config, onError: setError, paymentIntentId: successPaymentId }), u$2(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: isVoucherFormOpen, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: preselectedVoucherEventTypeId, voucherPurchaseResult: voucherPurchaseResult, isSuccess: isSuccess, showStandaloneCard: false, onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => {
16832
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), u$2(VoucherIntegration, { config: config, voucherConfig: voucherConfig, eventTypes: voucherEventTypes, systemConfig: systemConfig, isFormOpen: isVoucherFormOpen, isLoadingConfig: isLoadingVoucherConfig, preselectedEventTypeId: preselectedVoucherEventTypeId, voucherPurchaseResult: voucherPurchaseResult, isSuccess: isSuccess, showStandaloneCard: false, onCardClick: handleVoucherCardClick, onFormClose: handleVoucherFormClose, onSuccess: handleVoucherSuccess, onError: handleVoucherError, onSuccessModalClose: () => {
16686
16833
  setIsSuccess(false);
16687
16834
  setVoucherPurchaseResult(null);
16688
16835
  const url = new URL(window.location.href);