@bigz-app/booking-widget 1.3.2 → 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
@@ -15390,6 +15390,88 @@ function UpsellsStep({ upsells, selectedUpsells, participantCount, isLoading, is
15390
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)] })] }))] }) }));
15391
15391
  }
15392
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
+
15393
15475
  // Main widget component
15394
15476
  function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onTimezone, }) {
15395
15477
  const t = useTranslations();
@@ -15582,6 +15664,21 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15582
15664
  setIsLoadingVoucherConfig(false);
15583
15665
  }
15584
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]);
15585
15682
  // Fire widget pageview once when Google Ads config is received from API
15586
15683
  const pageviewFiredRef = useRef(false);
15587
15684
  useEffect(() => {
@@ -15678,6 +15775,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15678
15775
  });
15679
15776
  const voucherData = await voucherResponse.json();
15680
15777
  if (voucherResponse.ok && voucherData.voucherResult) {
15778
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "stripe_redirect" });
15681
15779
  setVoucherPurchaseResult(voucherData.voucherResult);
15682
15780
  setIsSuccess(true);
15683
15781
  setSuccessPaymentId(null);
@@ -15687,6 +15785,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15687
15785
  catch {
15688
15786
  // Fall back to booking success flow if voucher lookup fails.
15689
15787
  }
15788
+ trackEvent("booking_completed", { paymentIntentId: stripeReturn.paymentIntent, source: "stripe_redirect" });
15690
15789
  setVoucherPurchaseResult(null);
15691
15790
  setSuccessPaymentId(stripeReturn.paymentIntent);
15692
15791
  setIsSuccess(true);
@@ -15724,6 +15823,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15724
15823
  });
15725
15824
  const voucherData = await voucherResponse.json();
15726
15825
  if (voucherResponse.ok && voucherData.voucherResult) {
15826
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "mollie_redirect" });
15727
15827
  setVoucherPurchaseResult(voucherData.voucherResult);
15728
15828
  setIsSuccess(true);
15729
15829
  setSuccessPaymentId(null);
@@ -15762,6 +15862,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15762
15862
  window[globalFlagKey] = true;
15763
15863
  const timer = setTimeout(() => {
15764
15864
  setShowPromoDialog(true);
15865
+ trackEvent("promo_dialog_shown", { discountCode: config.promo?.discountCode });
15765
15866
  }, 1000);
15766
15867
  return () => clearTimeout(timer);
15767
15868
  }, [config.promo?.enabled, config.promo?.discountCode]);
@@ -15771,6 +15872,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15771
15872
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
15772
15873
  };
15773
15874
  const handlePromoCtaClick = () => {
15875
+ trackEvent("promo_cta_clicked", { discountCode: config.promo?.discountCode });
15774
15876
  setShowPromoDialog(false);
15775
15877
  const promoId = config.promo?.discountCode || "default";
15776
15878
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
@@ -15805,6 +15907,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
15805
15907
  }
15806
15908
  extractGoogleAdsConfig(data);
15807
15909
  setEventTypes(data.eventTypes);
15910
+ trackEvent("event_types_loaded", { count: data.eventTypes.length });
15808
15911
  if (isSingleEventTypeMode && data.eventTypes.length === 1) {
15809
15912
  setSelectedEventType(data.eventTypes[0]);
15810
15913
  await loadEventInstances(data.eventTypes[0].id);
@@ -16060,6 +16163,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16060
16163
  }
16061
16164
  // Event type selection handlers
16062
16165
  const handleEventTypeSelect = async (eventType) => {
16166
+ trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16063
16167
  setSelectedEventType(eventType);
16064
16168
  setCurrentStep("eventInstances");
16065
16169
  setShouldRenderInstanceSelection(true);
@@ -16073,6 +16177,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16073
16177
  };
16074
16178
  // Event instance selection handlers
16075
16179
  const handleEventInstanceSelect = async (eventInstance) => {
16180
+ trackEvent("event_instance_selected", { eventInstanceId: eventInstance.id, eventInstanceName: eventInstance.name });
16076
16181
  setSelectedEventInstance(eventInstance);
16077
16182
  bookingReturnStep.current = "eventInstances";
16078
16183
  // Set default participant count for upsell calculations
@@ -16085,9 +16190,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16085
16190
  try {
16086
16191
  const availableUpsells = await loadUpsells(selectedEventType.id, eventInstance.id, defaultParticipantCount);
16087
16192
  if (availableUpsells.length > 0) {
16088
- // Show upsells step
16193
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16089
16194
  setUpsells(availableUpsells);
16090
- // Pre-select default-checked upsells
16091
16195
  const defaultSelections = availableUpsells
16092
16196
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16093
16197
  .map((upsell) => ({
@@ -16097,7 +16201,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16097
16201
  setSelectedUpsells(defaultSelections);
16098
16202
  setCurrentStep("upsells");
16099
16203
  setIsLoadingUpsells(false);
16100
- return; // Don't proceed to booking yet
16204
+ return;
16101
16205
  }
16102
16206
  }
16103
16207
  catch (err) {
@@ -16107,7 +16211,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16107
16211
  setIsLoadingUpsells(false);
16108
16212
  }
16109
16213
  }
16110
- // No upsells available, go directly to booking
16214
+ trackEvent("booking_form_opened", { fromUpsells: false });
16111
16215
  setCurrentStep("booking");
16112
16216
  setShouldRenderBookingForm(true);
16113
16217
  setIsLoadingEventDetails(true);
@@ -16130,6 +16234,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16130
16234
  setEventDetails(null);
16131
16235
  };
16132
16236
  const handleBookingSuccess = (result) => {
16237
+ trackEvent("booking_completed", { paymentIntentId: result.paymentIntent?.id });
16133
16238
  setIsSuccess(true);
16134
16239
  setSuccessPaymentId(result.paymentIntent.id);
16135
16240
  setSidebarOpen(false);
@@ -16145,7 +16250,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16145
16250
  setSelectedUpsells(selections);
16146
16251
  };
16147
16252
  const handleUpsellsContinue = async () => {
16148
- // Move to booking step
16253
+ trackEvent("booking_form_opened", { fromUpsells: true });
16149
16254
  setCurrentStep("booking");
16150
16255
  setShouldRenderBookingForm(true);
16151
16256
  setIsLoadingEventDetails(true);
@@ -16165,8 +16270,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16165
16270
  };
16166
16271
  // Voucher purchase handlers
16167
16272
  const handleVoucherCardClick = async () => {
16273
+ trackEvent("voucher_card_clicked");
16168
16274
  setPreselectedVoucherEventTypeId(null);
16169
- // Ensure voucher config and event types are loaded before opening the form
16170
16275
  if (!voucherConfig || voucherEventTypes.length === 0) {
16171
16276
  await loadVoucherConfig();
16172
16277
  }
@@ -16185,6 +16290,7 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16185
16290
  setPreselectedVoucherEventTypeId(null);
16186
16291
  };
16187
16292
  const handleVoucherSuccess = (result) => {
16293
+ trackEvent("voucher_purchased", { voucherType: result.voucherType });
16188
16294
  setVoucherPurchaseResult(result);
16189
16295
  setIsVoucherFormOpen(false);
16190
16296
  setIsSuccess(true);
@@ -16267,8 +16373,8 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16267
16373
  try {
16268
16374
  const availableUpsells = await loadUpsells(eventTypeForUpsells.id, eventInstanceId, defaultParticipantCount);
16269
16375
  if (availableUpsells.length > 0) {
16376
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16270
16377
  setUpsells(availableUpsells);
16271
- // Pre-select default-checked upsells
16272
16378
  const defaultSelections = availableUpsells
16273
16379
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16274
16380
  .map((upsell) => ({
@@ -16278,7 +16384,6 @@ function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onT
16278
16384
  setSelectedUpsells(defaultSelections);
16279
16385
  setCurrentStep("upsells");
16280
16386
  setIsLoadingUpsells(false);
16281
- // Load event details in background for when user continues past upsells
16282
16387
  void loadEventDetails(eventInstanceId);
16283
16388
  return;
16284
16389
  }