@bigz-app/booking-widget 1.3.2 → 1.3.4

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.
@@ -15516,6 +15516,88 @@
15516
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)] })] }))] }) }));
15517
15517
  }
15518
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
+
15519
15601
  // Main widget component
15520
15602
  function UniversalBookingWidgetInner({ config: baseConfig, onWidgetLanguage, onTimezone, }) {
15521
15603
  const t = useTranslations();
@@ -15708,6 +15790,21 @@
15708
15790
  setIsLoadingVoucherConfig(false);
15709
15791
  }
15710
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]);
15711
15808
  // Fire widget pageview once when Google Ads config is received from API
15712
15809
  const pageviewFiredRef = A$2(false);
15713
15810
  y$1(() => {
@@ -15804,6 +15901,7 @@
15804
15901
  });
15805
15902
  const voucherData = await voucherResponse.json();
15806
15903
  if (voucherResponse.ok && voucherData.voucherResult) {
15904
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "stripe_redirect" });
15807
15905
  setVoucherPurchaseResult(voucherData.voucherResult);
15808
15906
  setIsSuccess(true);
15809
15907
  setSuccessPaymentId(null);
@@ -15813,6 +15911,7 @@
15813
15911
  catch {
15814
15912
  // Fall back to booking success flow if voucher lookup fails.
15815
15913
  }
15914
+ trackEvent("booking_completed", { paymentIntentId: stripeReturn.paymentIntent, source: "stripe_redirect" });
15816
15915
  setVoucherPurchaseResult(null);
15817
15916
  setSuccessPaymentId(stripeReturn.paymentIntent);
15818
15917
  setIsSuccess(true);
@@ -15850,6 +15949,7 @@
15850
15949
  });
15851
15950
  const voucherData = await voucherResponse.json();
15852
15951
  if (voucherResponse.ok && voucherData.voucherResult) {
15952
+ trackEvent("voucher_purchased", { voucherType: voucherData.voucherResult.voucherType, source: "mollie_redirect" });
15853
15953
  setVoucherPurchaseResult(voucherData.voucherResult);
15854
15954
  setIsSuccess(true);
15855
15955
  setSuccessPaymentId(null);
@@ -15888,6 +15988,7 @@
15888
15988
  window[globalFlagKey] = true;
15889
15989
  const timer = setTimeout(() => {
15890
15990
  setShowPromoDialog(true);
15991
+ trackEvent("promo_dialog_shown", { discountCode: config.promo?.discountCode });
15891
15992
  }, 1000);
15892
15993
  return () => clearTimeout(timer);
15893
15994
  }, [config.promo?.enabled, config.promo?.discountCode]);
@@ -15897,6 +15998,7 @@
15897
15998
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
15898
15999
  };
15899
16000
  const handlePromoCtaClick = () => {
16001
+ trackEvent("promo_cta_clicked", { discountCode: config.promo?.discountCode });
15900
16002
  setShowPromoDialog(false);
15901
16003
  const promoId = config.promo?.discountCode || "default";
15902
16004
  localStorage.setItem(`bigz-promo-${promoId}-shown`, "true");
@@ -15931,6 +16033,7 @@
15931
16033
  }
15932
16034
  extractGoogleAdsConfig(data);
15933
16035
  setEventTypes(data.eventTypes);
16036
+ trackEvent("event_types_loaded", { count: data.eventTypes.length });
15934
16037
  if (isSingleEventTypeMode && data.eventTypes.length === 1) {
15935
16038
  setSelectedEventType(data.eventTypes[0]);
15936
16039
  await loadEventInstances(data.eventTypes[0].id);
@@ -16186,6 +16289,7 @@
16186
16289
  }
16187
16290
  // Event type selection handlers
16188
16291
  const handleEventTypeSelect = async (eventType) => {
16292
+ trackEvent("event_type_selected", { eventTypeId: eventType.id, eventTypeName: eventType.name });
16189
16293
  setSelectedEventType(eventType);
16190
16294
  setCurrentStep("eventInstances");
16191
16295
  setShouldRenderInstanceSelection(true);
@@ -16199,6 +16303,7 @@
16199
16303
  };
16200
16304
  // Event instance selection handlers
16201
16305
  const handleEventInstanceSelect = async (eventInstance) => {
16306
+ trackEvent("event_instance_selected", { eventInstanceId: eventInstance.id, eventInstanceName: eventInstance.name });
16202
16307
  setSelectedEventInstance(eventInstance);
16203
16308
  bookingReturnStep.current = "eventInstances";
16204
16309
  // Set default participant count for upsell calculations
@@ -16211,9 +16316,8 @@
16211
16316
  try {
16212
16317
  const availableUpsells = await loadUpsells(selectedEventType.id, eventInstance.id, defaultParticipantCount);
16213
16318
  if (availableUpsells.length > 0) {
16214
- // Show upsells step
16319
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16215
16320
  setUpsells(availableUpsells);
16216
- // Pre-select default-checked upsells
16217
16321
  const defaultSelections = availableUpsells
16218
16322
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16219
16323
  .map((upsell) => ({
@@ -16223,7 +16327,7 @@
16223
16327
  setSelectedUpsells(defaultSelections);
16224
16328
  setCurrentStep("upsells");
16225
16329
  setIsLoadingUpsells(false);
16226
- return; // Don't proceed to booking yet
16330
+ return;
16227
16331
  }
16228
16332
  }
16229
16333
  catch (err) {
@@ -16233,7 +16337,7 @@
16233
16337
  setIsLoadingUpsells(false);
16234
16338
  }
16235
16339
  }
16236
- // No upsells available, go directly to booking
16340
+ trackEvent("booking_form_opened", { fromUpsells: false });
16237
16341
  setCurrentStep("booking");
16238
16342
  setShouldRenderBookingForm(true);
16239
16343
  setIsLoadingEventDetails(true);
@@ -16256,6 +16360,7 @@
16256
16360
  setEventDetails(null);
16257
16361
  };
16258
16362
  const handleBookingSuccess = (result) => {
16363
+ trackEvent("booking_completed", { paymentIntentId: result.paymentIntent?.id });
16259
16364
  setIsSuccess(true);
16260
16365
  setSuccessPaymentId(result.paymentIntent.id);
16261
16366
  setSidebarOpen(false);
@@ -16271,7 +16376,7 @@
16271
16376
  setSelectedUpsells(selections);
16272
16377
  };
16273
16378
  const handleUpsellsContinue = async () => {
16274
- // Move to booking step
16379
+ trackEvent("booking_form_opened", { fromUpsells: true });
16275
16380
  setCurrentStep("booking");
16276
16381
  setShouldRenderBookingForm(true);
16277
16382
  setIsLoadingEventDetails(true);
@@ -16291,8 +16396,8 @@
16291
16396
  };
16292
16397
  // Voucher purchase handlers
16293
16398
  const handleVoucherCardClick = async () => {
16399
+ trackEvent("voucher_card_clicked");
16294
16400
  setPreselectedVoucherEventTypeId(null);
16295
- // Ensure voucher config and event types are loaded before opening the form
16296
16401
  if (!voucherConfig || voucherEventTypes.length === 0) {
16297
16402
  await loadVoucherConfig();
16298
16403
  }
@@ -16311,6 +16416,7 @@
16311
16416
  setPreselectedVoucherEventTypeId(null);
16312
16417
  };
16313
16418
  const handleVoucherSuccess = (result) => {
16419
+ trackEvent("voucher_purchased", { voucherType: result.voucherType });
16314
16420
  setVoucherPurchaseResult(result);
16315
16421
  setIsVoucherFormOpen(false);
16316
16422
  setIsSuccess(true);
@@ -16393,8 +16499,8 @@
16393
16499
  try {
16394
16500
  const availableUpsells = await loadUpsells(eventTypeForUpsells.id, eventInstanceId, defaultParticipantCount);
16395
16501
  if (availableUpsells.length > 0) {
16502
+ trackEvent("upsell_step_viewed", { count: availableUpsells.length });
16396
16503
  setUpsells(availableUpsells);
16397
- // Pre-select default-checked upsells
16398
16504
  const defaultSelections = availableUpsells
16399
16505
  .filter((upsell) => upsell.defaultChecked && upsell.available)
16400
16506
  .map((upsell) => ({
@@ -16404,7 +16510,6 @@
16404
16510
  setSelectedUpsells(defaultSelections);
16405
16511
  setCurrentStep("upsells");
16406
16512
  setIsLoadingUpsells(false);
16407
- // Load event details in background for when user continues past upsells
16408
16513
  void loadEventDetails(eventInstanceId);
16409
16514
  return;
16410
16515
  }