@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.
package/dist/index.esm.js CHANGED
@@ -12008,121 +12008,145 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
12008
12008
  }
12009
12009
 
12010
12010
  /**
12011
- * Google Ads Conversion Tracking Utility
12011
+ * Google Ads Tracking Utility
12012
12012
  *
12013
- * Simplified utility that waits 1500ms, checks/initializes gtag, and sends conversion.
12014
- */
12015
- /**
12016
- * Check if gtag is available in current or parent window
12013
+ * Handles pageview tracking (widget load) and conversion tracking (successful booking).
12014
+ * Supports both direct gtag.js and GTM dataLayer setups.
12017
12015
  */
12018
12016
  function isGtagAvailable() {
12019
- if (typeof window === "undefined") {
12017
+ if (typeof window === "undefined")
12020
12018
  return false;
12021
- }
12022
- // Check current window
12023
- if (typeof window.gtag === "function") {
12019
+ if (typeof window.gtag === "function")
12024
12020
  return true;
12025
- }
12026
- // Check parent window (for iframe/widget scenarios)
12027
12021
  if (window !== window.parent) {
12028
12022
  try {
12029
- if (typeof window.parent?.gtag === "function") {
12023
+ if (typeof window.parent?.gtag === "function")
12030
12024
  return true;
12031
- }
12032
12025
  }
12033
12026
  catch {
12034
- // Cannot access parent window (cross-origin)
12027
+ // Cross-origin
12035
12028
  }
12036
12029
  }
12037
12030
  return false;
12038
12031
  }
12039
- /**
12040
- * Initialize gtag if not already available
12041
- */
12042
12032
  function initializeGtag(tagId) {
12043
- if (typeof window === "undefined") {
12033
+ if (typeof window === "undefined" || isGtagAvailable())
12044
12034
  return;
12045
- }
12046
- // Skip if gtag already exists
12047
- if (isGtagAvailable()) {
12048
- return;
12049
- }
12050
- // Initialize dataLayer and gtag function
12051
12035
  window.dataLayer = window.dataLayer || [];
12052
12036
  window.gtag = (...args) => {
12053
12037
  window.dataLayer.push(args);
12054
12038
  };
12055
- // Set current timestamp
12056
12039
  window.gtag("js", new Date());
12057
- // Load gtag script
12058
12040
  const script = document.createElement("script");
12059
12041
  script.async = true;
12060
12042
  script.src = `https://www.googletagmanager.com/gtag/js?id=${tagId}`;
12061
12043
  document.head.appendChild(script);
12062
- // Configure the tag
12063
12044
  window.gtag("config", tagId, {
12064
12045
  anonymize_ip: true,
12065
12046
  allow_google_signals: false,
12066
12047
  allow_ad_personalization_signals: false,
12067
12048
  });
12068
12049
  }
12069
- /**
12070
- * Send conversion event using available gtag
12071
- */
12072
- function sendConversion(config) {
12073
- if (typeof window === "undefined") {
12074
- return;
12050
+ function getGtag() {
12051
+ if (typeof window === "undefined")
12052
+ return null;
12053
+ if (typeof window.gtag === "function") {
12054
+ return window.gtag;
12075
12055
  }
12076
- let gtag = window.gtag;
12077
- // Try parent window gtag if current window doesn't have it
12078
- if (typeof gtag !== "function" && window !== window.parent) {
12056
+ if (window !== window.parent) {
12079
12057
  try {
12080
- gtag = window.parent?.gtag;
12058
+ const parentGtag = window.parent?.gtag;
12059
+ if (typeof parentGtag === "function")
12060
+ return parentGtag;
12081
12061
  }
12082
12062
  catch {
12083
- // Cannot access parent window (cross-origin)
12063
+ // Cross-origin
12084
12064
  }
12085
12065
  }
12086
- if (typeof gtag !== "function") {
12066
+ return null;
12067
+ }
12068
+ /**
12069
+ * Push an event to the dataLayer for GTM visibility,
12070
+ * then also fire via gtag for direct Google Ads tracking.
12071
+ */
12072
+ function sendEvent(eventName, params) {
12073
+ if (typeof window === "undefined")
12087
12074
  return;
12075
+ // GTM dataLayer push (object format — visible in Tag Assistant)
12076
+ window.dataLayer = window.dataLayer || [];
12077
+ window.dataLayer.push({
12078
+ event: eventName,
12079
+ ...params,
12080
+ });
12081
+ // gtag call (array format — processed by gtag.js for Google Ads)
12082
+ const gtag = getGtag();
12083
+ if (gtag) {
12084
+ gtag("event", eventName, params);
12088
12085
  }
12089
- // Build conversion data
12090
- const conversionData = {
12086
+ }
12087
+ function sendConversion(config) {
12088
+ const params = {
12091
12089
  send_to: `${config.tagId}/${config.conversionId}`,
12092
12090
  };
12093
- // Add optional parameters
12094
12091
  if (config.conversionValue !== undefined) {
12095
- conversionData.value = config.conversionValue;
12092
+ params.value = config.conversionValue;
12096
12093
  }
12097
12094
  if (config.conversionCurrency) {
12098
- conversionData.currency = config.conversionCurrency;
12095
+ params.currency = config.conversionCurrency;
12099
12096
  }
12100
12097
  if (config.transactionId) {
12101
- conversionData.transaction_id = config.transactionId;
12098
+ params.transaction_id = config.transactionId;
12102
12099
  }
12103
- // Send conversion event
12104
- gtag("event", "conversion", conversionData);
12100
+ sendEvent("conversion", params);
12105
12101
  }
12106
12102
  /**
12107
- * Main function to handle Google Ads conversion tracking
12108
- * Waits 1500ms, checks/initializes gtag, then sends conversion
12103
+ * Track widget pageview (fired once on widget mount).
12109
12104
  */
12110
- function handleGoogleAdsConversion(config) {
12111
- // Validate required config
12112
- if (!config.tagId || !config.conversionId) {
12105
+ function handleGoogleAdsPageview(tagId, consent) {
12106
+ if (!tagId || false || typeof window === "undefined")
12113
12107
  return;
12108
+ if (!isGtagAvailable()) {
12109
+ initializeGtag(tagId);
12114
12110
  }
12115
- // Wait 1500ms before proceeding
12111
+ const fire = () => sendEvent("widget_pageview", {
12112
+ send_to: tagId,
12113
+ page_location: window.location.href,
12114
+ page_title: document.title,
12115
+ });
12116
+ if (isGtagAvailable()) {
12117
+ fire();
12118
+ return;
12119
+ }
12120
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${tagId}"]`);
12121
+ if (script) {
12122
+ script.addEventListener("load", fire, { once: true });
12123
+ }
12124
+ }
12125
+ /**
12126
+ * Handle Google Ads conversion tracking.
12127
+ * Waits 1500ms for the success page to settle, then fires.
12128
+ */
12129
+ function handleGoogleAdsConversion(config) {
12130
+ if (!config.tagId || !config.conversionId)
12131
+ return;
12116
12132
  setTimeout(() => {
12117
- // Check if gtag is available, initialize if not
12118
- if (!isGtagAvailable()) {
12119
- initializeGtag(config.tagId);
12133
+ if (isGtagAvailable()) {
12134
+ sendConversion(config);
12135
+ return;
12136
+ }
12137
+ initializeGtag(config.tagId);
12138
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${config.tagId}"]`);
12139
+ if (script) {
12140
+ script.addEventListener("load", () => sendConversion(config));
12141
+ script.addEventListener("error", () => sendConversion(config));
12142
+ }
12143
+ else {
12144
+ sendConversion(config);
12120
12145
  }
12121
- sendConversion(config);
12122
12146
  }, 1500);
12123
12147
  }
12124
12148
 
12125
- const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, }) => {
12149
+ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, googleAdsConfig: googleAdsConfigProp, }) => {
12126
12150
  const t = useTranslations();
12127
12151
  const { locale } = useLocale();
12128
12152
  const timezone = useTimezone();
@@ -12171,20 +12195,16 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12171
12195
  });
12172
12196
  setPaymentStatus(data.stripePaymentIntent?.status || data.order.status);
12173
12197
  const finalPaymentStatus = data.stripePaymentIntent?.status || data.order.status;
12198
+ const adsConfig = googleAdsConfigProp ?? data.googleAdsConfig;
12174
12199
  if (finalPaymentStatus === "succeeded" &&
12175
- config.googleAds?.tagId &&
12176
- config.googleAds?.conversionId &&
12177
- config.googleAds?.consent !== false) {
12178
- // Prepare conversion tracking data
12179
- const conversionValue = data.order.total / 100;
12180
- const transactionId = data.order.id;
12181
- // Track the conversion
12200
+ adsConfig?.tagId &&
12201
+ adsConfig?.conversionId) {
12182
12202
  handleGoogleAdsConversion({
12183
- tagId: config.googleAds.tagId,
12184
- conversionId: config.googleAds.conversionId,
12185
- conversionValue,
12186
- conversionCurrency: config.googleAds.conversionCurrency || "EUR",
12187
- transactionId,
12203
+ tagId: adsConfig.tagId,
12204
+ conversionId: adsConfig.conversionId,
12205
+ conversionValue: data.order.total / 100,
12206
+ conversionCurrency: adsConfig.conversionCurrency || "EUR",
12207
+ transactionId: data.order.id,
12188
12208
  });
12189
12209
  }
12190
12210
  }
@@ -15370,6 +15390,88 @@ function UpsellsStep({ upsells, selectedUpsells, participantCount, isLoading, is
15370
15390
  return (jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: t("upsells.title"), footer: footerContent, children: jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", padding: "16px 16px" }, children: [isLoading && (jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "12px", padding: "40px 20px", ...textStyles.muted }, children: [spinner(), jsx("span", { children: t("upsells.loading") })] })), !isLoading && upsells.length === 0 && (jsx("div", { style: { textAlign: "center", padding: "40px 20px", ...textStyles.muted }, children: jsx("p", { children: t("upsells.noExtras") }) })), !isLoading && upsells.length > 0 && (jsx("div", { style: { display: "flex", flexDirection: "column", gap: "12px", flex: 1, overflowY: "auto", paddingBottom: "16px" }, children: upsells.map((upsell) => (jsx(UpsellCard, { upsell: upsell, isSelected: isSelected(upsell.id), participantCount: participantCount, onSelect: () => selectUpsell(upsell.id) }, upsell.id))) })), selectedCount > 0 && (jsxs("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: [jsx("span", { style: textStyles.muted, children: selectedCount === 1 ? t("upsells.selected", { count: selectedCount }) : t("upsells.selectedPlural", { count: selectedCount }) }), jsxs("span", { style: { fontWeight: 600, color: "var(--bw-highlight-color)", fontFamily: "var(--bw-font-family)" }, children: ["+", formatCurrency(selectedTotal)] })] }))] }) }));
15371
15391
  }
15372
15392
 
15393
+ /**
15394
+ * Widget analytics — server-side PostHog tracking via batched sendBeacon.
15395
+ *
15396
+ * Events are buffered locally and flushed every few seconds (or on page
15397
+ * unload) as a single POST to `/api/booking/track`. This avoids extra
15398
+ * network requests on every click while still capturing a complete funnel.
15399
+ */
15400
+ let buffer = [];
15401
+ let flushTimer = null;
15402
+ let currentApiBaseUrl = "";
15403
+ let currentOrganizationId = "";
15404
+ const FLUSH_INTERVAL_MS = 3000;
15405
+ function flush() {
15406
+ if (buffer.length === 0)
15407
+ return;
15408
+ const payload = JSON.stringify({
15409
+ organizationId: currentOrganizationId,
15410
+ events: buffer,
15411
+ });
15412
+ buffer = [];
15413
+ const url = getApiUrl(currentApiBaseUrl, "/booking/track");
15414
+ // Use fetch with keepalive as primary — works cross-origin with CORS.
15415
+ // sendBeacon silently fails cross-origin with application/json because
15416
+ // it can't do CORS preflight, so we only use it as a text/plain fallback
15417
+ // on page unload when fetch may be aborted.
15418
+ try {
15419
+ void fetch(url, {
15420
+ method: "POST",
15421
+ headers: { "Content-Type": "application/json" },
15422
+ body: payload,
15423
+ keepalive: true,
15424
+ });
15425
+ }
15426
+ catch {
15427
+ // fetch failed (e.g. during page unload) — try sendBeacon with text/plain
15428
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
15429
+ const blob = new Blob([payload], { type: "text/plain" });
15430
+ navigator.sendBeacon(url, blob);
15431
+ }
15432
+ }
15433
+ }
15434
+ function scheduleFlush() {
15435
+ if (flushTimer)
15436
+ return;
15437
+ flushTimer = setTimeout(() => {
15438
+ flushTimer = null;
15439
+ flush();
15440
+ }, FLUSH_INTERVAL_MS);
15441
+ }
15442
+ /**
15443
+ * Initialise the analytics module. Must be called once before `trackEvent`.
15444
+ */
15445
+ function initAnalytics(apiBaseUrl, organizationId) {
15446
+ currentApiBaseUrl = apiBaseUrl;
15447
+ currentOrganizationId = organizationId;
15448
+ if (typeof window !== "undefined") {
15449
+ window.addEventListener("pagehide", flush);
15450
+ window.addEventListener("visibilitychange", () => {
15451
+ if (document.visibilityState === "hidden")
15452
+ flush();
15453
+ });
15454
+ }
15455
+ }
15456
+ /**
15457
+ * Queue a widget event. Non-blocking, fire-and-forget.
15458
+ */
15459
+ function trackEvent(event, properties = {}) {
15460
+ if (typeof window === "undefined")
15461
+ return;
15462
+ if (!currentOrganizationId)
15463
+ return;
15464
+ buffer.push({
15465
+ event,
15466
+ properties,
15467
+ domain: window.location.hostname,
15468
+ url: window.location.href,
15469
+ referrer: document.referrer,
15470
+ timestamp: new Date().toISOString(),
15471
+ });
15472
+ scheduleFlush();
15473
+ }
15474
+
15373
15475
  // Main widget component
15374
15476
  function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onTimezone, }) {
15375
15477
  const t = useTranslations();
@@ -15449,6 +15551,13 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15449
15551
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = useState(false);
15450
15552
  const [shouldRenderUpsells, setShouldRenderUpsells] = useState(false);
15451
15553
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = useState(false);
15554
+ // Google Ads config (received from API, set once from the first API response)
15555
+ const [googleAdsConfig, setGoogleAdsConfig] = useState(null);
15556
+ const extractGoogleAdsConfig = (data) => {
15557
+ if (!googleAdsConfig && data?.googleAdsConfig?.tagId) {
15558
+ setGoogleAdsConfig(data.googleAdsConfig);
15559
+ }
15560
+ };
15452
15561
  // Promo dialog state
15453
15562
  const [showPromoDialog, setShowPromoDialog] = useState(false);
15454
15563
  const [widgetContainerRef, setWidgetContainerRef] = useState(null);
@@ -15532,6 +15641,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15532
15641
  image: resolvedImage,
15533
15642
  };
15534
15643
  setVoucherConfig(mergedConfig);
15644
+ extractGoogleAdsConfig(data);
15535
15645
  setVoucherEventTypes(data.eventTypes || []);
15536
15646
  // Set system config for payment processing
15537
15647
  if (data.paymentProvider) {
@@ -15554,6 +15664,29 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15554
15664
  setIsLoadingVoucherConfig(false);
15555
15665
  }
15556
15666
  };
15667
+ // Initialise analytics once on mount
15668
+ const analyticsInitRef = useRef(false);
15669
+ useEffect(() => {
15670
+ if (!analyticsInitRef.current && config.organizationId) {
15671
+ analyticsInitRef.current = true;
15672
+ initAnalytics(config.apiBaseUrl, config.organizationId);
15673
+ trackEvent("widget_loaded", {
15674
+ viewMode,
15675
+ eventTypeId: config.eventTypeId,
15676
+ categoryId: config.categoryId,
15677
+ eventInstanceId: config.eventInstanceId,
15678
+ isStandaloneVoucherMode,
15679
+ });
15680
+ }
15681
+ }, [config.organizationId, config.apiBaseUrl]);
15682
+ // Fire widget pageview once when Google Ads config is received from API
15683
+ const pageviewFiredRef = useRef(false);
15684
+ useEffect(() => {
15685
+ if (!pageviewFiredRef.current && googleAdsConfig?.tagId) {
15686
+ pageviewFiredRef.current = true;
15687
+ handleGoogleAdsPageview(googleAdsConfig.tagId);
15688
+ }
15689
+ }, [googleAdsConfig]);
15557
15690
  // Determine initial step and load data
15558
15691
  useEffect(() => {
15559
15692
  const initializeWidget = async () => {
@@ -15642,6 +15775,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15642
15775
  });
15643
15776
  const voucherData = await voucherResponse.json();
15644
15777
  if (voucherResponse.ok && voucherData.voucherResult) {
15778
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "stripe_redirect" });
15645
15779
  setVoucherPurchaseResult(voucherData.voucherResult);
15646
15780
  setIsSuccess(true);
15647
15781
  setSuccessPaymentId(null);
@@ -15651,6 +15785,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15651
15785
  catch {
15652
15786
  // Fall back to booking success flow if voucher lookup fails.
15653
15787
  }
15788
+ trackEvent("booking_completed", { paymentIntentId: stripeReturn.paymentIntent, source: "stripe_redirect" });
15654
15789
  setVoucherPurchaseResult(null);
15655
15790
  setSuccessPaymentId(stripeReturn.paymentIntent);
15656
15791
  setIsSuccess(true);
@@ -15688,6 +15823,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15688
15823
  });
15689
15824
  const voucherData = await voucherResponse.json();
15690
15825
  if (voucherResponse.ok && voucherData.voucherResult) {
15826
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "mollie_redirect" });
15691
15827
  setVoucherPurchaseResult(voucherData.voucherResult);
15692
15828
  setIsSuccess(true);
15693
15829
  setSuccessPaymentId(null);
@@ -15726,6 +15862,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15726
15862
  window[globalFlagKey] = true;
15727
15863
  const timer = setTimeout(() => {
15728
15864
  setShowPromoDialog(true);
15865
+ trackEvent("promo_dialog_shown", { discountCode: config.promo?.discountCode });
15729
15866
  }, 1000);
15730
15867
  return () => clearTimeout(timer);
15731
15868
  }, [config.promo?.enabled, config.promo?.discountCode]);
@@ -15735,6 +15872,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15735
15872
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
15736
15873
  };
15737
15874
  const handlePromoCtaClick = () => {
15875
+ trackEvent("promo_cta_clicked", { discountCode: config.promo?.discountCode });
15738
15876
  setShowPromoDialog(false);
15739
15877
  const promoId = config.promo?.discountCode || "default";
15740
15878
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
@@ -15767,7 +15905,9 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15767
15905
  onWidgetLanguage?.(wl);
15768
15906
  onTimezone?.(wl.timezone);
15769
15907
  }
15908
+ extractGoogleAdsConfig(data);
15770
15909
  setEventTypes(data.eventTypes);
15910
+ trackEvent("event_types_loaded", { count: data.eventTypes.length });
15771
15911
  if (isSingleEventTypeMode && data.eventTypes.length === 1) {
15772
15912
  setSelectedEventType(data.eventTypes[0]);
15773
15913
  await loadEventInstances(data.eventTypes[0].id);
@@ -15803,6 +15943,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15803
15943
  onWidgetLanguage?.(wl);
15804
15944
  onTimezone?.(wl.timezone);
15805
15945
  }
15946
+ extractGoogleAdsConfig(data);
15806
15947
  setUpcomingEvents(data.upcomingEvents || []);
15807
15948
  }
15808
15949
  else {
@@ -15838,6 +15979,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15838
15979
  onWidgetLanguage?.(wl);
15839
15980
  onTimezone?.(wl.timezone);
15840
15981
  }
15982
+ extractGoogleAdsConfig(data);
15841
15983
  setSpecials(data.specials || []);
15842
15984
  }
15843
15985
  else {
@@ -15864,6 +16006,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15864
16006
  onWidgetLanguage?.(wl);
15865
16007
  onTimezone?.(wl.timezone);
15866
16008
  }
16009
+ extractGoogleAdsConfig(data);
15867
16010
  setEventInstances(data.eventInstances);
15868
16011
  if (data.paymentProvider) {
15869
16012
  setSystemConfig({
@@ -15924,6 +16067,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15924
16067
  onWidgetLanguage?.(wl);
15925
16068
  onTimezone?.(wl.timezone);
15926
16069
  }
16070
+ extractGoogleAdsConfig(data);
15927
16071
  setEventDetails(data.eventDetails);
15928
16072
  setSystemConfig({
15929
16073
  paymentProvider: data.paymentProvider,
@@ -15993,6 +16137,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15993
16137
  onWidgetLanguage?.(wl);
15994
16138
  onTimezone?.(wl.timezone);
15995
16139
  }
16140
+ extractGoogleAdsConfig(data);
15996
16141
  return data.upsells || [];
15997
16142
  }
15998
16143
  else {
@@ -16018,6 +16163,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16018
16163
  }
16019
16164
  // Event type selection handlers
16020
16165
  const handleEventTypeSelect = async (eventType) => {
16166
+ trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16021
16167
  setSelectedEventType(eventType);
16022
16168
  setCurrentStep("eventInstances");
16023
16169
  setShouldRenderInstanceSelection(true);
@@ -16031,6 +16177,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16031
16177
  };
16032
16178
  // Event instance selection handlers
16033
16179
  const handleEventInstanceSelect = async (eventInstance) => {
16180
+ trackEvent("event_instance_selected", { eventInstanceId: eventInstance.id, eventInstanceName: eventInstance.name });
16034
16181
  setSelectedEventInstance(eventInstance);
16035
16182
  bookingReturnStep.current = "eventInstances";
16036
16183
  // Set default participant count for upsell calculations
@@ -16043,9 +16190,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16043
16190
  try {
16044
16191
  const availableUpsells = await loadUpsells(selectedEventType.id, eventInstance.id, defaultParticipantCount);
16045
16192
  if (availableUpsells.length > 0) {
16046
- // Show upsells step
16193
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16047
16194
  setUpsells(availableUpsells);
16048
- // Pre-select default-checked upsells
16049
16195
  const defaultSelections = availableUpsells
16050
16196
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16051
16197
  .map((upsell) => ({
@@ -16055,7 +16201,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16055
16201
  setSelectedUpsells(defaultSelections);
16056
16202
  setCurrentStep("upsells");
16057
16203
  setIsLoadingUpsells(false);
16058
- return; // Don't proceed to booking yet
16204
+ return;
16059
16205
  }
16060
16206
  }
16061
16207
  catch (err) {
@@ -16065,7 +16211,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16065
16211
  setIsLoadingUpsells(false);
16066
16212
  }
16067
16213
  }
16068
- // No upsells available, go directly to booking
16214
+ trackEvent("booking_form_opened", { fromUpsells: false });
16069
16215
  setCurrentStep("booking");
16070
16216
  setShouldRenderBookingForm(true);
16071
16217
  setIsLoadingEventDetails(true);
@@ -16088,6 +16234,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16088
16234
  setEventDetails(null);
16089
16235
  };
16090
16236
  const handleBookingSuccess = (result) => {
16237
+ trackEvent("booking_completed", { paymentIntentId: result.paymentIntent?.id });
16091
16238
  setIsSuccess(true);
16092
16239
  setSuccessPaymentId(result.paymentIntent.id);
16093
16240
  setSidebarOpen(false);
@@ -16103,7 +16250,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16103
16250
  setSelectedUpsells(selections);
16104
16251
  };
16105
16252
  const handleUpsellsContinue = async () => {
16106
- // Move to booking step
16253
+ trackEvent("booking_form_opened", { fromUpsells: true });
16107
16254
  setCurrentStep("booking");
16108
16255
  setShouldRenderBookingForm(true);
16109
16256
  setIsLoadingEventDetails(true);
@@ -16123,8 +16270,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16123
16270
  };
16124
16271
  // Voucher purchase handlers
16125
16272
  const handleVoucherCardClick = async () => {
16273
+ trackEvent("voucher_card_clicked");
16126
16274
  setPreselectedVoucherEventTypeId(null);
16127
- // Ensure voucher config and event types are loaded before opening the form
16128
16275
  if (!voucherConfig || voucherEventTypes.length === 0) {
16129
16276
  await loadVoucherConfig();
16130
16277
  }
@@ -16143,6 +16290,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16143
16290
  setPreselectedVoucherEventTypeId(null);
16144
16291
  };
16145
16292
  const handleVoucherSuccess = (result) => {
16293
+ trackEvent("voucher_purchased", { voucherType: result.voucherType });
16146
16294
  setVoucherPurchaseResult(result);
16147
16295
  setIsVoucherFormOpen(false);
16148
16296
  setIsSuccess(true);
@@ -16225,8 +16373,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16225
16373
  try {
16226
16374
  const availableUpsells = await loadUpsells(eventTypeForUpsells.id, eventInstanceId, defaultParticipantCount);
16227
16375
  if (availableUpsells.length > 0) {
16376
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16228
16377
  setUpsells(availableUpsells);
16229
- // Pre-select default-checked upsells
16230
16378
  const defaultSelections = availableUpsells
16231
16379
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16232
16380
  .map((upsell) => ({
@@ -16236,7 +16384,6 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16236
16384
  setSelectedUpsells(defaultSelections);
16237
16385
  setCurrentStep("upsells");
16238
16386
  setIsLoadingUpsells(false);
16239
- // Load event details in background for when user continues past upsells
16240
16387
  void loadEventDetails(eventInstanceId);
16241
16388
  return;
16242
16389
  }
@@ -16406,7 +16553,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16406
16553
  url.searchParams.delete("mollie_payment_id");
16407
16554
  url.searchParams.delete("mollie_status");
16408
16555
  window.history.replaceState({}, "", url.toString());
16409
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16556
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16410
16557
  }
16411
16558
  if (viewMode === "specials" && showingPreview) {
16412
16559
  return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [jsx(SpecialsView, { specials: specials, onEventSelect: handleUpcomingEventSelect, isLoading: isLoadingSpecials, showSavingsAmount: config.specialsSettings?.showSavingsAmount ?? true, showSavingsPercent: config.specialsSettings?.showSavingsPercent ?? false, emptyStateText: config.specialsSettings?.emptyStateText }), shouldRenderBookingForm && eventDetails && (jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, onBackToEventInstances: () => {
@@ -16431,7 +16578,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16431
16578
  setShouldRenderBookingForm(false);
16432
16579
  setSelectedUpsells([]);
16433
16580
  setUpsells([]);
16434
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16581
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16435
16582
  }
16436
16583
  if (viewMode === "next-events" && !showingPreview && currentStep === "eventInstances") {
16437
16584
  return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, children: [shouldRenderInstanceSelection && (jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => {
@@ -16454,7 +16601,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16454
16601
  url.searchParams.delete("mollie_payment_id");
16455
16602
  url.searchParams.delete("mollie_status");
16456
16603
  window.history.replaceState({}, "", url.toString());
16457
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16604
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16458
16605
  }
16459
16606
  if (viewMode === "button" && (isSingleEventTypeMode || isDirectInstanceMode)) {
16460
16607
  return (jsxs(StyleProvider, { config: config, children: [jsxs("div", { ref: setWidgetContainerRef, style: {
@@ -16500,7 +16647,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16500
16647
  url.searchParams.delete("mollie_payment_id");
16501
16648
  url.searchParams.delete("mollie_status");
16502
16649
  window.history.replaceState({}, "", url.toString());
16503
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16650
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16504
16651
  }
16505
16652
  // Cards mode (default) - show event type selection with optional voucher card
16506
16653
  const cardsView = (jsxs(Fragment, { children: [hasEventSelection && (jsx(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 && (jsx(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 && (jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsx("div", { style: {
@@ -16556,7 +16703,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16556
16703
  url.searchParams.delete("mollie_payment_id");
16557
16704
  url.searchParams.delete("mollie_status");
16558
16705
  window.history.replaceState({}, "", url.toString());
16559
- }, config: config, onError: setError, paymentIntentId: successPaymentId }), jsx(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: () => {
16706
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsx(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: () => {
16560
16707
  setIsSuccess(false);
16561
16708
  setVoucherPurchaseResult(null);
16562
16709
  const url = new URL(window.location.href);