@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.cjs CHANGED
@@ -12028,121 +12028,145 @@ function BookingForm({ config, eventDetails, stripePromise, onSuccess, onError,
12028
12028
  }
12029
12029
 
12030
12030
  /**
12031
- * Google Ads Conversion Tracking Utility
12031
+ * Google Ads Tracking Utility
12032
12032
  *
12033
- * Simplified utility that waits 1500ms, checks/initializes gtag, and sends conversion.
12034
- */
12035
- /**
12036
- * Check if gtag is available in current or parent window
12033
+ * Handles pageview tracking (widget load) and conversion tracking (successful booking).
12034
+ * Supports both direct gtag.js and GTM dataLayer setups.
12037
12035
  */
12038
12036
  function isGtagAvailable() {
12039
- if (typeof window === "undefined") {
12037
+ if (typeof window === "undefined")
12040
12038
  return false;
12041
- }
12042
- // Check current window
12043
- if (typeof window.gtag === "function") {
12039
+ if (typeof window.gtag === "function")
12044
12040
  return true;
12045
- }
12046
- // Check parent window (for iframe/widget scenarios)
12047
12041
  if (window !== window.parent) {
12048
12042
  try {
12049
- if (typeof window.parent?.gtag === "function") {
12043
+ if (typeof window.parent?.gtag === "function")
12050
12044
  return true;
12051
- }
12052
12045
  }
12053
12046
  catch {
12054
- // Cannot access parent window (cross-origin)
12047
+ // Cross-origin
12055
12048
  }
12056
12049
  }
12057
12050
  return false;
12058
12051
  }
12059
- /**
12060
- * Initialize gtag if not already available
12061
- */
12062
12052
  function initializeGtag(tagId) {
12063
- if (typeof window === "undefined") {
12053
+ if (typeof window === "undefined" || isGtagAvailable())
12064
12054
  return;
12065
- }
12066
- // Skip if gtag already exists
12067
- if (isGtagAvailable()) {
12068
- return;
12069
- }
12070
- // Initialize dataLayer and gtag function
12071
12055
  window.dataLayer = window.dataLayer || [];
12072
12056
  window.gtag = (...args) => {
12073
12057
  window.dataLayer.push(args);
12074
12058
  };
12075
- // Set current timestamp
12076
12059
  window.gtag("js", new Date());
12077
- // Load gtag script
12078
12060
  const script = document.createElement("script");
12079
12061
  script.async = true;
12080
12062
  script.src = `https://www.googletagmanager.com/gtag/js?id=${tagId}`;
12081
12063
  document.head.appendChild(script);
12082
- // Configure the tag
12083
12064
  window.gtag("config", tagId, {
12084
12065
  anonymize_ip: true,
12085
12066
  allow_google_signals: false,
12086
12067
  allow_ad_personalization_signals: false,
12087
12068
  });
12088
12069
  }
12089
- /**
12090
- * Send conversion event using available gtag
12091
- */
12092
- function sendConversion(config) {
12093
- if (typeof window === "undefined") {
12094
- return;
12070
+ function getGtag() {
12071
+ if (typeof window === "undefined")
12072
+ return null;
12073
+ if (typeof window.gtag === "function") {
12074
+ return window.gtag;
12095
12075
  }
12096
- let gtag = window.gtag;
12097
- // Try parent window gtag if current window doesn't have it
12098
- if (typeof gtag !== "function" && window !== window.parent) {
12076
+ if (window !== window.parent) {
12099
12077
  try {
12100
- gtag = window.parent?.gtag;
12078
+ const parentGtag = window.parent?.gtag;
12079
+ if (typeof parentGtag === "function")
12080
+ return parentGtag;
12101
12081
  }
12102
12082
  catch {
12103
- // Cannot access parent window (cross-origin)
12083
+ // Cross-origin
12104
12084
  }
12105
12085
  }
12106
- if (typeof gtag !== "function") {
12086
+ return null;
12087
+ }
12088
+ /**
12089
+ * Push an event to the dataLayer for GTM visibility,
12090
+ * then also fire via gtag for direct Google Ads tracking.
12091
+ */
12092
+ function sendEvent(eventName, params) {
12093
+ if (typeof window === "undefined")
12107
12094
  return;
12095
+ // GTM dataLayer push (object format — visible in Tag Assistant)
12096
+ window.dataLayer = window.dataLayer || [];
12097
+ window.dataLayer.push({
12098
+ event: eventName,
12099
+ ...params,
12100
+ });
12101
+ // gtag call (array format — processed by gtag.js for Google Ads)
12102
+ const gtag = getGtag();
12103
+ if (gtag) {
12104
+ gtag("event", eventName, params);
12108
12105
  }
12109
- // Build conversion data
12110
- const conversionData = {
12106
+ }
12107
+ function sendConversion(config) {
12108
+ const params = {
12111
12109
  send_to: `${config.tagId}/${config.conversionId}`,
12112
12110
  };
12113
- // Add optional parameters
12114
12111
  if (config.conversionValue !== undefined) {
12115
- conversionData.value = config.conversionValue;
12112
+ params.value = config.conversionValue;
12116
12113
  }
12117
12114
  if (config.conversionCurrency) {
12118
- conversionData.currency = config.conversionCurrency;
12115
+ params.currency = config.conversionCurrency;
12119
12116
  }
12120
12117
  if (config.transactionId) {
12121
- conversionData.transaction_id = config.transactionId;
12118
+ params.transaction_id = config.transactionId;
12122
12119
  }
12123
- // Send conversion event
12124
- gtag("event", "conversion", conversionData);
12120
+ sendEvent("conversion", params);
12125
12121
  }
12126
12122
  /**
12127
- * Main function to handle Google Ads conversion tracking
12128
- * Waits 1500ms, checks/initializes gtag, then sends conversion
12123
+ * Track widget pageview (fired once on widget mount).
12129
12124
  */
12130
- function handleGoogleAdsConversion(config) {
12131
- // Validate required config
12132
- if (!config.tagId || !config.conversionId) {
12125
+ function handleGoogleAdsPageview(tagId, consent) {
12126
+ if (!tagId || false || typeof window === "undefined")
12133
12127
  return;
12128
+ if (!isGtagAvailable()) {
12129
+ initializeGtag(tagId);
12134
12130
  }
12135
- // Wait 1500ms before proceeding
12131
+ const fire = () => sendEvent("widget_pageview", {
12132
+ send_to: tagId,
12133
+ page_location: window.location.href,
12134
+ page_title: document.title,
12135
+ });
12136
+ if (isGtagAvailable()) {
12137
+ fire();
12138
+ return;
12139
+ }
12140
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${tagId}"]`);
12141
+ if (script) {
12142
+ script.addEventListener("load", fire, { once: true });
12143
+ }
12144
+ }
12145
+ /**
12146
+ * Handle Google Ads conversion tracking.
12147
+ * Waits 1500ms for the success page to settle, then fires.
12148
+ */
12149
+ function handleGoogleAdsConversion(config) {
12150
+ if (!config.tagId || !config.conversionId)
12151
+ return;
12136
12152
  setTimeout(() => {
12137
- // Check if gtag is available, initialize if not
12138
- if (!isGtagAvailable()) {
12139
- initializeGtag(config.tagId);
12153
+ if (isGtagAvailable()) {
12154
+ sendConversion(config);
12155
+ return;
12156
+ }
12157
+ initializeGtag(config.tagId);
12158
+ const script = document.querySelector(`script[src*="googletagmanager.com/gtag/js?id=${config.tagId}"]`);
12159
+ if (script) {
12160
+ script.addEventListener("load", () => sendConversion(config));
12161
+ script.addEventListener("error", () => sendConversion(config));
12162
+ }
12163
+ else {
12164
+ sendConversion(config);
12140
12165
  }
12141
- sendConversion(config);
12142
12166
  }, 1500);
12143
12167
  }
12144
12168
 
12145
- const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, }) => {
12169
+ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId, googleAdsConfig: googleAdsConfigProp, }) => {
12146
12170
  const t = useTranslations();
12147
12171
  const { locale } = useLocale();
12148
12172
  const timezone = useTimezone();
@@ -12191,20 +12215,16 @@ const BookingSuccessModal = ({ isOpen, onClose, config, onError, paymentIntentId
12191
12215
  });
12192
12216
  setPaymentStatus(data.stripePaymentIntent?.status || data.order.status);
12193
12217
  const finalPaymentStatus = data.stripePaymentIntent?.status || data.order.status;
12218
+ const adsConfig = googleAdsConfigProp ?? data.googleAdsConfig;
12194
12219
  if (finalPaymentStatus === "succeeded" &&
12195
- config.googleAds?.tagId &&
12196
- config.googleAds?.conversionId &&
12197
- config.googleAds?.consent !== false) {
12198
- // Prepare conversion tracking data
12199
- const conversionValue = data.order.total / 100;
12200
- const transactionId = data.order.id;
12201
- // Track the conversion
12220
+ adsConfig?.tagId &&
12221
+ adsConfig?.conversionId) {
12202
12222
  handleGoogleAdsConversion({
12203
- tagId: config.googleAds.tagId,
12204
- conversionId: config.googleAds.conversionId,
12205
- conversionValue,
12206
- conversionCurrency: config.googleAds.conversionCurrency || "EUR",
12207
- transactionId,
12223
+ tagId: adsConfig.tagId,
12224
+ conversionId: adsConfig.conversionId,
12225
+ conversionValue: data.order.total / 100,
12226
+ conversionCurrency: adsConfig.conversionCurrency || "EUR",
12227
+ transactionId: data.order.id,
12208
12228
  });
12209
12229
  }
12210
12230
  }
@@ -15390,6 +15410,88 @@ function UpsellsStep({ upsells, selectedUpsells, participantCount, isLoading, is
15390
15410
  return (jsxRuntime.jsx(Sidebar, { isOpen: isOpen, onClose: onClose, title: t("upsells.title"), footer: footerContent, children: jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", padding: "16px 16px" }, children: [isLoading && (jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "12px", padding: "40px 20px", ...textStyles.muted }, children: [spinner(), jsxRuntime.jsx("span", { children: t("upsells.loading") })] })), !isLoading && upsells.length === 0 && (jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "40px 20px", ...textStyles.muted }, children: jsxRuntime.jsx("p", { children: t("upsells.noExtras") }) })), !isLoading && upsells.length > 0 && (jsxRuntime.jsx("div", { style: { display: "flex", flexDirection: "column", gap: "12px", flex: 1, overflowY: "auto", paddingBottom: "16px" }, children: upsells.map((upsell) => (jsxRuntime.jsx(UpsellCard, { upsell: upsell, isSelected: isSelected(upsell.id), participantCount: participantCount, onSelect: () => selectUpsell(upsell.id) }, upsell.id))) })), selectedCount > 0 && (jsxRuntime.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: [jsxRuntime.jsx("span", { style: textStyles.muted, children: selectedCount === 1 ? t("upsells.selected", { count: selectedCount }) : t("upsells.selectedPlural", { count: selectedCount }) }), jsxRuntime.jsxs("span", { style: { fontWeight: 600, color: "var(--bw-highlight-color)", fontFamily: "var(--bw-font-family)" }, children: ["+", formatCurrency(selectedTotal)] })] }))] }) }));
15391
15411
  }
15392
15412
 
15413
+ /**
15414
+ * Widget analytics — server-side PostHog tracking via batched sendBeacon.
15415
+ *
15416
+ * Events are buffered locally and flushed every few seconds (or on page
15417
+ * unload) as a single POST to `/api/booking/track`. This avoids extra
15418
+ * network requests on every click while still capturing a complete funnel.
15419
+ */
15420
+ let buffer = [];
15421
+ let flushTimer = null;
15422
+ let currentApiBaseUrl = "";
15423
+ let currentOrganizationId = "";
15424
+ const FLUSH_INTERVAL_MS = 3000;
15425
+ function flush() {
15426
+ if (buffer.length === 0)
15427
+ return;
15428
+ const payload = JSON.stringify({
15429
+ organizationId: currentOrganizationId,
15430
+ events: buffer,
15431
+ });
15432
+ buffer = [];
15433
+ const url = getApiUrl(currentApiBaseUrl, "/booking/track");
15434
+ // Use fetch with keepalive as primary — works cross-origin with CORS.
15435
+ // sendBeacon silently fails cross-origin with application/json because
15436
+ // it can't do CORS preflight, so we only use it as a text/plain fallback
15437
+ // on page unload when fetch may be aborted.
15438
+ try {
15439
+ void fetch(url, {
15440
+ method: "POST",
15441
+ headers: { "Content-Type": "application/json" },
15442
+ body: payload,
15443
+ keepalive: true,
15444
+ });
15445
+ }
15446
+ catch {
15447
+ // fetch failed (e.g. during page unload) — try sendBeacon with text/plain
15448
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
15449
+ const blob = new Blob([payload], { type: "text/plain" });
15450
+ navigator.sendBeacon(url, blob);
15451
+ }
15452
+ }
15453
+ }
15454
+ function scheduleFlush() {
15455
+ if (flushTimer)
15456
+ return;
15457
+ flushTimer = setTimeout(() => {
15458
+ flushTimer = null;
15459
+ flush();
15460
+ }, FLUSH_INTERVAL_MS);
15461
+ }
15462
+ /**
15463
+ * Initialise the analytics module. Must be called once before `trackEvent`.
15464
+ */
15465
+ function initAnalytics(apiBaseUrl, organizationId) {
15466
+ currentApiBaseUrl = apiBaseUrl;
15467
+ currentOrganizationId = organizationId;
15468
+ if (typeof window !== "undefined") {
15469
+ window.addEventListener("pagehide", flush);
15470
+ window.addEventListener("visibilitychange", () => {
15471
+ if (document.visibilityState === "hidden")
15472
+ flush();
15473
+ });
15474
+ }
15475
+ }
15476
+ /**
15477
+ * Queue a widget event. Non-blocking, fire-and-forget.
15478
+ */
15479
+ function trackEvent(event, properties = {}) {
15480
+ if (typeof window === "undefined")
15481
+ return;
15482
+ if (!currentOrganizationId)
15483
+ return;
15484
+ buffer.push({
15485
+ event,
15486
+ properties,
15487
+ domain: window.location.hostname,
15488
+ url: window.location.href,
15489
+ referrer: document.referrer,
15490
+ timestamp: new Date().toISOString(),
15491
+ });
15492
+ scheduleFlush();
15493
+ }
15494
+
15393
15495
  // Main widget component
15394
15496
  function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onTimezone, }) {
15395
15497
  const t = useTranslations();
@@ -15469,6 +15571,13 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15469
15571
  const [shouldRenderInstanceSelection, setShouldRenderInstanceSelection] = React.useState(false);
15470
15572
  const [shouldRenderUpsells, setShouldRenderUpsells] = React.useState(false);
15471
15573
  const [shouldRenderBookingForm, setShouldRenderBookingForm] = React.useState(false);
15574
+ // Google Ads config (received from API, set once from the first API response)
15575
+ const [googleAdsConfig, setGoogleAdsConfig] = React.useState(null);
15576
+ const extractGoogleAdsConfig = (data) => {
15577
+ if (!googleAdsConfig && data?.googleAdsConfig?.tagId) {
15578
+ setGoogleAdsConfig(data.googleAdsConfig);
15579
+ }
15580
+ };
15472
15581
  // Promo dialog state
15473
15582
  const [showPromoDialog, setShowPromoDialog] = React.useState(false);
15474
15583
  const [widgetContainerRef, setWidgetContainerRef] = React.useState(null);
@@ -15552,6 +15661,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15552
15661
  image: resolvedImage,
15553
15662
  };
15554
15663
  setVoucherConfig(mergedConfig);
15664
+ extractGoogleAdsConfig(data);
15555
15665
  setVoucherEventTypes(data.eventTypes || []);
15556
15666
  // Set system config for payment processing
15557
15667
  if (data.paymentProvider) {
@@ -15574,6 +15684,29 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15574
15684
  setIsLoadingVoucherConfig(false);
15575
15685
  }
15576
15686
  };
15687
+ // Initialise analytics once on mount
15688
+ const analyticsInitRef = React.useRef(false);
15689
+ React.useEffect(() => {
15690
+ if (!analyticsInitRef.current && config.organizationId) {
15691
+ analyticsInitRef.current = true;
15692
+ initAnalytics(config.apiBaseUrl, config.organizationId);
15693
+ trackEvent("widget_loaded", {
15694
+ viewMode,
15695
+ eventTypeId: config.eventTypeId,
15696
+ categoryId: config.categoryId,
15697
+ eventInstanceId: config.eventInstanceId,
15698
+ isStandaloneVoucherMode,
15699
+ });
15700
+ }
15701
+ }, [config.organizationId, config.apiBaseUrl]);
15702
+ // Fire widget pageview once when Google Ads config is received from API
15703
+ const pageviewFiredRef = React.useRef(false);
15704
+ React.useEffect(() => {
15705
+ if (!pageviewFiredRef.current && googleAdsConfig?.tagId) {
15706
+ pageviewFiredRef.current = true;
15707
+ handleGoogleAdsPageview(googleAdsConfig.tagId);
15708
+ }
15709
+ }, [googleAdsConfig]);
15577
15710
  // Determine initial step and load data
15578
15711
  React.useEffect(() => {
15579
15712
  const initializeWidget = async () => {
@@ -15662,6 +15795,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15662
15795
  });
15663
15796
  const voucherData = await voucherResponse.json();
15664
15797
  if (voucherResponse.ok && voucherData.voucherResult) {
15798
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "stripe_redirect" });
15665
15799
  setVoucherPurchaseResult(voucherData.voucherResult);
15666
15800
  setIsSuccess(true);
15667
15801
  setSuccessPaymentId(null);
@@ -15671,6 +15805,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15671
15805
  catch {
15672
15806
  // Fall back to booking success flow if voucher lookup fails.
15673
15807
  }
15808
+ trackEvent("booking_completed", { paymentIntentId: stripeReturn.paymentIntent, source: "stripe_redirect" });
15674
15809
  setVoucherPurchaseResult(null);
15675
15810
  setSuccessPaymentId(stripeReturn.paymentIntent);
15676
15811
  setIsSuccess(true);
@@ -15708,6 +15843,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15708
15843
  });
15709
15844
  const voucherData = await voucherResponse.json();
15710
15845
  if (voucherResponse.ok && voucherData.voucherResult) {
15846
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "mollie_redirect" });
15711
15847
  setVoucherPurchaseResult(voucherData.voucherResult);
15712
15848
  setIsSuccess(true);
15713
15849
  setSuccessPaymentId(null);
@@ -15746,6 +15882,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15746
15882
  window[globalFlagKey] = true;
15747
15883
  const timer = setTimeout(() => {
15748
15884
  setShowPromoDialog(true);
15885
+ trackEvent("promo_dialog_shown", { discountCode: config.promo?.discountCode });
15749
15886
  }, 1000);
15750
15887
  return () => clearTimeout(timer);
15751
15888
  }, [config.promo?.enabled, config.promo?.discountCode]);
@@ -15755,6 +15892,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15755
15892
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
15756
15893
  };
15757
15894
  const handlePromoCtaClick = () => {
15895
+ trackEvent("promo_cta_clicked", { discountCode: config.promo?.discountCode });
15758
15896
  setShowPromoDialog(false);
15759
15897
  const promoId = config.promo?.discountCode || "default";
15760
15898
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
@@ -15787,7 +15925,9 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15787
15925
  onWidgetLanguage?.(wl);
15788
15926
  onTimezone?.(wl.timezone);
15789
15927
  }
15928
+ extractGoogleAdsConfig(data);
15790
15929
  setEventTypes(data.eventTypes);
15930
+ trackEvent("event_types_loaded", { count: data.eventTypes.length });
15791
15931
  if (isSingleEventTypeMode && data.eventTypes.length === 1) {
15792
15932
  setSelectedEventType(data.eventTypes[0]);
15793
15933
  await loadEventInstances(data.eventTypes[0].id);
@@ -15823,6 +15963,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15823
15963
  onWidgetLanguage?.(wl);
15824
15964
  onTimezone?.(wl.timezone);
15825
15965
  }
15966
+ extractGoogleAdsConfig(data);
15826
15967
  setUpcomingEvents(data.upcomingEvents || []);
15827
15968
  }
15828
15969
  else {
@@ -15858,6 +15999,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15858
15999
  onWidgetLanguage?.(wl);
15859
16000
  onTimezone?.(wl.timezone);
15860
16001
  }
16002
+ extractGoogleAdsConfig(data);
15861
16003
  setSpecials(data.specials || []);
15862
16004
  }
15863
16005
  else {
@@ -15884,6 +16026,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15884
16026
  onWidgetLanguage?.(wl);
15885
16027
  onTimezone?.(wl.timezone);
15886
16028
  }
16029
+ extractGoogleAdsConfig(data);
15887
16030
  setEventInstances(data.eventInstances);
15888
16031
  if (data.paymentProvider) {
15889
16032
  setSystemConfig({
@@ -15944,6 +16087,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15944
16087
  onWidgetLanguage?.(wl);
15945
16088
  onTimezone?.(wl.timezone);
15946
16089
  }
16090
+ extractGoogleAdsConfig(data);
15947
16091
  setEventDetails(data.eventDetails);
15948
16092
  setSystemConfig({
15949
16093
  paymentProvider: data.paymentProvider,
@@ -16013,6 +16157,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16013
16157
  onWidgetLanguage?.(wl);
16014
16158
  onTimezone?.(wl.timezone);
16015
16159
  }
16160
+ extractGoogleAdsConfig(data);
16016
16161
  return data.upsells || [];
16017
16162
  }
16018
16163
  else {
@@ -16038,6 +16183,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16038
16183
  }
16039
16184
  // Event type selection handlers
16040
16185
  const handleEventTypeSelect = async (eventType) => {
16186
+ trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16041
16187
  setSelectedEventType(eventType);
16042
16188
  setCurrentStep("eventInstances");
16043
16189
  setShouldRenderInstanceSelection(true);
@@ -16051,6 +16197,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16051
16197
  };
16052
16198
  // Event instance selection handlers
16053
16199
  const handleEventInstanceSelect = async (eventInstance) => {
16200
+ trackEvent("event_instance_selected", { eventInstanceId: eventInstance.id, eventInstanceName: eventInstance.name });
16054
16201
  setSelectedEventInstance(eventInstance);
16055
16202
  bookingReturnStep.current = "eventInstances";
16056
16203
  // Set default participant count for upsell calculations
@@ -16063,9 +16210,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16063
16210
  try {
16064
16211
  const availableUpsells = await loadUpsells(selectedEventType.id, eventInstance.id, defaultParticipantCount);
16065
16212
  if (availableUpsells.length > 0) {
16066
- // Show upsells step
16213
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16067
16214
  setUpsells(availableUpsells);
16068
- // Pre-select default-checked upsells
16069
16215
  const defaultSelections = availableUpsells
16070
16216
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16071
16217
  .map((upsell) => ({
@@ -16075,7 +16221,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16075
16221
  setSelectedUpsells(defaultSelections);
16076
16222
  setCurrentStep("upsells");
16077
16223
  setIsLoadingUpsells(false);
16078
- return; // Don't proceed to booking yet
16224
+ return;
16079
16225
  }
16080
16226
  }
16081
16227
  catch (err) {
@@ -16085,7 +16231,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16085
16231
  setIsLoadingUpsells(false);
16086
16232
  }
16087
16233
  }
16088
- // No upsells available, go directly to booking
16234
+ trackEvent("booking_form_opened", { fromUpsells: false });
16089
16235
  setCurrentStep("booking");
16090
16236
  setShouldRenderBookingForm(true);
16091
16237
  setIsLoadingEventDetails(true);
@@ -16108,6 +16254,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16108
16254
  setEventDetails(null);
16109
16255
  };
16110
16256
  const handleBookingSuccess = (result) => {
16257
+ trackEvent("booking_completed", { paymentIntentId: result.paymentIntent?.id });
16111
16258
  setIsSuccess(true);
16112
16259
  setSuccessPaymentId(result.paymentIntent.id);
16113
16260
  setSidebarOpen(false);
@@ -16123,7 +16270,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16123
16270
  setSelectedUpsells(selections);
16124
16271
  };
16125
16272
  const handleUpsellsContinue = async () => {
16126
- // Move to booking step
16273
+ trackEvent("booking_form_opened", { fromUpsells: true });
16127
16274
  setCurrentStep("booking");
16128
16275
  setShouldRenderBookingForm(true);
16129
16276
  setIsLoadingEventDetails(true);
@@ -16143,8 +16290,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16143
16290
  };
16144
16291
  // Voucher purchase handlers
16145
16292
  const handleVoucherCardClick = async () => {
16293
+ trackEvent("voucher_card_clicked");
16146
16294
  setPreselectedVoucherEventTypeId(null);
16147
- // Ensure voucher config and event types are loaded before opening the form
16148
16295
  if (!voucherConfig || voucherEventTypes.length === 0) {
16149
16296
  await loadVoucherConfig();
16150
16297
  }
@@ -16163,6 +16310,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16163
16310
  setPreselectedVoucherEventTypeId(null);
16164
16311
  };
16165
16312
  const handleVoucherSuccess = (result) => {
16313
+ trackEvent("voucher_purchased", { voucherType: result.voucherType });
16166
16314
  setVoucherPurchaseResult(result);
16167
16315
  setIsVoucherFormOpen(false);
16168
16316
  setIsSuccess(true);
@@ -16245,8 +16393,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16245
16393
  try {
16246
16394
  const availableUpsells = await loadUpsells(eventTypeForUpsells.id, eventInstanceId, defaultParticipantCount);
16247
16395
  if (availableUpsells.length > 0) {
16396
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16248
16397
  setUpsells(availableUpsells);
16249
- // Pre-select default-checked upsells
16250
16398
  const defaultSelections = availableUpsells
16251
16399
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16252
16400
  .map((upsell) => ({
@@ -16256,7 +16404,6 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16256
16404
  setSelectedUpsells(defaultSelections);
16257
16405
  setCurrentStep("upsells");
16258
16406
  setIsLoadingUpsells(false);
16259
- // Load event details in background for when user continues past upsells
16260
16407
  void loadEventDetails(eventInstanceId);
16261
16408
  return;
16262
16409
  }
@@ -16426,7 +16573,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16426
16573
  url.searchParams.delete("mollie_payment_id");
16427
16574
  url.searchParams.delete("mollie_status");
16428
16575
  window.history.replaceState({}, "", url.toString());
16429
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16576
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16430
16577
  }
16431
16578
  if (viewMode === "specials" && showingPreview) {
16432
16579
  return (jsxRuntime.jsxs(StyleProvider, { config: config, children: [jsxRuntime.jsxs("div", { ref: setWidgetContainerRef, children: [jsxRuntime.jsx(SpecialsView, { specials: specials, onEventSelect: handleUpcomingEventSelect, isLoading: isLoadingSpecials, showSavingsAmount: config.specialsSettings?.showSavingsAmount ?? true, showSavingsPercent: config.specialsSettings?.showSavingsPercent ?? false, emptyStateText: config.specialsSettings?.emptyStateText }), shouldRenderBookingForm && eventDetails && (jsxRuntime.jsx(BookingForm, { config: config, eventDetails: eventDetails, stripePromise: stripePromise, onSuccess: handleBookingSuccess, onError: handleBookingError, onBackToEventInstances: () => {
@@ -16451,7 +16598,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16451
16598
  setShouldRenderBookingForm(false);
16452
16599
  setSelectedUpsells([]);
16453
16600
  setUpsells([]);
16454
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16601
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16455
16602
  }
16456
16603
  if (viewMode === "next-events" && !showingPreview && currentStep === "eventInstances") {
16457
16604
  return (jsxRuntime.jsxs(StyleProvider, { config: config, children: [jsxRuntime.jsxs("div", { ref: setWidgetContainerRef, children: [shouldRenderInstanceSelection && (jsxRuntime.jsx(EventInstanceSelection, { eventInstances: eventInstances, selectedEventType: selectedEventType, onEventInstanceSelect: handleEventInstanceSelect, onBackToEventTypes: () => {
@@ -16474,7 +16621,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16474
16621
  url.searchParams.delete("mollie_payment_id");
16475
16622
  url.searchParams.delete("mollie_status");
16476
16623
  window.history.replaceState({}, "", url.toString());
16477
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16624
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16478
16625
  }
16479
16626
  if (viewMode === "button" && (isSingleEventTypeMode || isDirectInstanceMode)) {
16480
16627
  return (jsxRuntime.jsxs(StyleProvider, { config: config, children: [jsxRuntime.jsxs("div", { ref: setWidgetContainerRef, style: {
@@ -16520,7 +16667,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16520
16667
  url.searchParams.delete("mollie_payment_id");
16521
16668
  url.searchParams.delete("mollie_status");
16522
16669
  window.history.replaceState({}, "", url.toString());
16523
- }, config: config, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16670
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId })] }), showPromoDialog && config.promo && (jsxRuntime.jsx(PromoDialog, { config: config.promo, onClose: handlePromoDialogClose, onCtaClick: handlePromoCtaClick }))] }));
16524
16671
  }
16525
16672
  // Cards mode (default) - show event type selection with optional voucher card
16526
16673
  const cardsView = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [hasEventSelection && (jsxRuntime.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 && (jsxRuntime.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 && (jsxRuntime.jsx("div", { style: { padding: "24px", textAlign: "center" }, children: jsxRuntime.jsx("div", { style: {
@@ -16576,7 +16723,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16576
16723
  url.searchParams.delete("mollie_payment_id");
16577
16724
  url.searchParams.delete("mollie_status");
16578
16725
  window.history.replaceState({}, "", url.toString());
16579
- }, config: config, onError: setError, paymentIntentId: successPaymentId }), jsxRuntime.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: () => {
16726
+ }, config: config, googleAdsConfig: googleAdsConfig, onError: setError, paymentIntentId: successPaymentId }), jsxRuntime.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: () => {
16580
16727
  setIsSuccess(false);
16581
16728
  setVoucherPurchaseResult(null);
16582
16729
  const url = new URL(window.location.href);