@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.
- package/dist/booking-widget.js +113 -8
- package/dist/booking-widget.js.map +1 -1
- package/dist/components/UniversalBookingWidget.d.ts.map +1 -1
- package/dist/index.cjs +113 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +113 -8
- package/dist/index.esm.js.map +1 -1
- package/dist/utils/analytics.d.ts +17 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/package.json +1 -1
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|