@nuitee/booking-widget 1.0.6 → 1.0.7
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/README.md
CHANGED
|
@@ -67,7 +67,7 @@ const widget = new BookingWidget({
|
|
|
67
67
|
primary: '#3b82f6',
|
|
68
68
|
primaryText: '#ffffff'
|
|
69
69
|
},
|
|
70
|
-
onOpen: () => console.log('
|
|
70
|
+
onOpen: () => console.log('widgetOpened'),
|
|
71
71
|
onClose: () => console.log('Widget closed'),
|
|
72
72
|
onComplete: (bookingData) => console.log('Booking completed', bookingData)
|
|
73
73
|
});
|
|
@@ -94,15 +94,15 @@ widget.open();
|
|
|
94
94
|
No bundler: load the script and CSS from the CDN, then create the widget.
|
|
95
95
|
|
|
96
96
|
```html
|
|
97
|
-
<link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.
|
|
98
|
-
<script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.
|
|
97
|
+
<link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.7/dist/booking-widget.css">
|
|
98
|
+
<script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.7/dist/booking-widget-standalone.js"></script>
|
|
99
99
|
|
|
100
100
|
<div id="booking-widget-container"></div>
|
|
101
101
|
|
|
102
102
|
<script>
|
|
103
103
|
const widget = new BookingWidget({
|
|
104
104
|
containerId: 'booking-widget-container',
|
|
105
|
-
cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.
|
|
105
|
+
cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.7/dist/booking-widget.css',
|
|
106
106
|
propertyKey: 'your-property-key',
|
|
107
107
|
onOpen: () => console.log('Opened'),
|
|
108
108
|
onClose: () => console.log('Closed'),
|
|
@@ -936,6 +936,8 @@ if (typeof window !== 'undefined') {
|
|
|
936
936
|
mode: options.mode || null,
|
|
937
937
|
bookingApi: options.bookingApi || null,
|
|
938
938
|
cssUrl: options.cssUrl || null,
|
|
939
|
+
posthogKey: options.posthogKey || null,
|
|
940
|
+
posthogHost: options.posthogHost || 'https://us.i.posthog.com',
|
|
939
941
|
// Color customization: CONFIG defaults from load-config, installer colors override
|
|
940
942
|
colors: (function () {
|
|
941
943
|
const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
|
|
@@ -1046,6 +1048,25 @@ if (typeof window !== 'undefined') {
|
|
|
1046
1048
|
return this.STEPS.findIndex(s => s.key === key);
|
|
1047
1049
|
}
|
|
1048
1050
|
|
|
1051
|
+
_capture(eventName, properties) {
|
|
1052
|
+
if (typeof window !== 'undefined' && window.posthog && typeof window.posthog.capture === 'function') {
|
|
1053
|
+
try {
|
|
1054
|
+
const mode = this.options.mode === 'sandbox' ? 'sandbox' : 'live';
|
|
1055
|
+
const propertyKey = this.options.propertyKey != null ? String(this.options.propertyKey) : undefined;
|
|
1056
|
+
const propertyId = this.options.propertyId != null && this.options.propertyId !== '' ? String(this.options.propertyId) : undefined;
|
|
1057
|
+
window.posthog.capture(eventName, { ...(properties || {}), mode, propertyKey, propertyId });
|
|
1058
|
+
} catch (e) { /* noop */ }
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
_identify() {
|
|
1063
|
+
if (typeof window !== 'undefined' && this.options.propertyKey && window.posthog && typeof window.posthog.identify === 'function') {
|
|
1064
|
+
try {
|
|
1065
|
+
window.posthog.identify(String(this.options.propertyKey).trim());
|
|
1066
|
+
} catch (e) { /* noop */ }
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1049
1070
|
open() {
|
|
1050
1071
|
if (!this.container) this.init();
|
|
1051
1072
|
if (!this.overlay || !this.widget) return; // container element not found
|
|
@@ -1056,6 +1077,7 @@ if (typeof window !== 'undefined') {
|
|
|
1056
1077
|
this._fetchRuntimeConfig();
|
|
1057
1078
|
}
|
|
1058
1079
|
this.render();
|
|
1080
|
+
this._capture('widgetOpened');
|
|
1059
1081
|
if (this.options.onOpen) this.options.onOpen();
|
|
1060
1082
|
}
|
|
1061
1083
|
|
|
@@ -1112,6 +1134,12 @@ if (typeof window !== 'undefined') {
|
|
|
1112
1134
|
this.bookingApi.fetchRooms(params).then((rooms) => {
|
|
1113
1135
|
this.ROOMS = Array.isArray(rooms) ? rooms : [];
|
|
1114
1136
|
this.loadingRooms = false;
|
|
1137
|
+
this._capture('search', {
|
|
1138
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
1139
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
1140
|
+
rooms: this.state.rooms,
|
|
1141
|
+
occupancies: this.state.occupancies,
|
|
1142
|
+
});
|
|
1115
1143
|
this.render();
|
|
1116
1144
|
}).catch((err) => {
|
|
1117
1145
|
this.apiError = err.message || 'Failed to load rooms';
|
|
@@ -1196,10 +1224,12 @@ if (typeof window !== 'undefined') {
|
|
|
1196
1224
|
const cacheKey = isSandbox ? key + ':sandbox' : key;
|
|
1197
1225
|
|
|
1198
1226
|
if (__bwConfigCache[cacheKey]) {
|
|
1199
|
-
|
|
1227
|
+
const cached = __bwConfigCache[cacheKey];
|
|
1228
|
+
this._applyApiColors(cached);
|
|
1200
1229
|
this._configState = 'loaded';
|
|
1201
1230
|
this.applyColors();
|
|
1202
|
-
|
|
1231
|
+
this._initPosthog(cached._posthogKey || '');
|
|
1232
|
+
return Promise.resolve(cached);
|
|
1203
1233
|
}
|
|
1204
1234
|
|
|
1205
1235
|
this._configState = 'loading';
|
|
@@ -1218,10 +1248,12 @@ if (typeof window !== 'undefined') {
|
|
|
1218
1248
|
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
1219
1249
|
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
1220
1250
|
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
1251
|
+
apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
|
|
1221
1252
|
__bwConfigCache[cacheKey] = apiColors;
|
|
1222
1253
|
self._applyApiColors(apiColors);
|
|
1223
1254
|
self._configState = 'loaded';
|
|
1224
1255
|
self.applyColors();
|
|
1256
|
+
self._initPosthog(apiColors._posthogKey);
|
|
1225
1257
|
self.render();
|
|
1226
1258
|
return apiColors;
|
|
1227
1259
|
})
|
|
@@ -1234,6 +1266,26 @@ if (typeof window !== 'undefined') {
|
|
|
1234
1266
|
return this._configPromise;
|
|
1235
1267
|
}
|
|
1236
1268
|
|
|
1269
|
+
_initPosthog(configKey) {
|
|
1270
|
+
const key = this.options.posthogKey || configKey || '';
|
|
1271
|
+
if (!key || typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
1272
|
+
if (this._posthogInited) return;
|
|
1273
|
+
try {
|
|
1274
|
+
// Inject the posthog loader snippet if posthog has not been loaded yet.
|
|
1275
|
+
// This makes the standalone bundle self-sufficient — no CDN <script> needed from the host.
|
|
1276
|
+
if (!window.posthog || !window.posthog.__SV) {
|
|
1277
|
+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split('.');2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement('script')).type='text/javascript',p.async=!0,p.src=s.api_host.replace('.i.posthog.com','-assets.i.posthog.com')+'/static/array.js',(r=t.getElementsByTagName('script')[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a='posthog',u.people=u.people||[],u.toString=function(t){var e='posthog';return'posthog'!==a&&(e+='.'+a),t||(e+=' (stub)'),e},u.people.toString=function(){return u.toString(1)+' (stub)'},o='capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId setPersonPropertiesForFlags'.split(' '),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
1278
|
+
}
|
|
1279
|
+
window.posthog.init(key, { api_host: this.options.posthogHost || 'https://us.i.posthog.com' });
|
|
1280
|
+
this._posthogInited = true;
|
|
1281
|
+
this._identify();
|
|
1282
|
+
this._capture('widgetLoaded');
|
|
1283
|
+
if (this.overlay && this.overlay.classList.contains('active')) {
|
|
1284
|
+
this._capture('widgetOpened');
|
|
1285
|
+
}
|
|
1286
|
+
} catch (e) { /* noop */ }
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1237
1289
|
_retryConfigFetch() {
|
|
1238
1290
|
this._configState = 'idle';
|
|
1239
1291
|
this._configError = null;
|
|
@@ -1683,7 +1735,9 @@ if (typeof window !== 'undefined') {
|
|
|
1683
1735
|
const grid = this.widget.querySelector('.room-grid');
|
|
1684
1736
|
const scrollLeft = grid ? grid.scrollLeft : 0;
|
|
1685
1737
|
if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
|
|
1686
|
-
|
|
1738
|
+
const room = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1739
|
+
this.state.selectedRoom = room;
|
|
1740
|
+
this._capture('selectedRoom', { roomId: id, roomName: room && room.name });
|
|
1687
1741
|
this.render();
|
|
1688
1742
|
const restoreScroll = () => {
|
|
1689
1743
|
const newGrid = this.widget.querySelector('.room-grid');
|
|
@@ -1743,6 +1797,7 @@ if (typeof window !== 'undefined') {
|
|
|
1743
1797
|
|
|
1744
1798
|
selectRate(id) {
|
|
1745
1799
|
this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1800
|
+
this._capture('selectedRate', { rateId: id });
|
|
1746
1801
|
this.render();
|
|
1747
1802
|
}
|
|
1748
1803
|
|
|
@@ -1975,6 +2030,16 @@ if (typeof window !== 'undefined') {
|
|
|
1975
2030
|
.then((res) => {
|
|
1976
2031
|
this.confirmationCode = (res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code)) || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
|
|
1977
2032
|
this.state.step = 'confirmation';
|
|
2033
|
+
this._capture('booked', {
|
|
2034
|
+
confirmationCode: this.confirmationCode,
|
|
2035
|
+
guest: this.state.guest,
|
|
2036
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
2037
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
2038
|
+
roomName: this.state.selectedRoom && this.state.selectedRoom.name,
|
|
2039
|
+
roomId: this.state.selectedRoom && this.state.selectedRoom.id,
|
|
2040
|
+
rateId: this.state.selectedRate && this.state.selectedRate.id,
|
|
2041
|
+
currency: this.state.selectedRoom && this.state.selectedRoom.currency,
|
|
2042
|
+
});
|
|
1978
2043
|
this.render();
|
|
1979
2044
|
})
|
|
1980
2045
|
.catch((err) => {
|
|
@@ -2018,6 +2083,18 @@ if (typeof window !== 'undefined') {
|
|
|
2018
2083
|
self.confirmationStatus = status || self.confirmationStatus || 'pending';
|
|
2019
2084
|
if (status === 'confirmed') {
|
|
2020
2085
|
self.confirmationDetails = data;
|
|
2086
|
+
self._capture('booked', {
|
|
2087
|
+
confirmationCode: data && (data.confirmationCode ?? data.confirmation_code ?? data.bookingId ?? data.booking_id),
|
|
2088
|
+
bookingId: data && (data.bookingId ?? data.booking_id),
|
|
2089
|
+
guest: self.state.guest,
|
|
2090
|
+
checkIn: self.state.checkIn && self.state.checkIn.toISOString ? self.state.checkIn.toISOString() : undefined,
|
|
2091
|
+
checkOut: self.state.checkOut && self.state.checkOut.toISOString ? self.state.checkOut.toISOString() : undefined,
|
|
2092
|
+
roomName: self.state.selectedRoom && self.state.selectedRoom.name,
|
|
2093
|
+
roomId: self.state.selectedRoom && self.state.selectedRoom.id,
|
|
2094
|
+
rateId: self.state.selectedRate && self.state.selectedRate.id,
|
|
2095
|
+
totalAmount: data && (data.totalAmount ?? data.total_amount),
|
|
2096
|
+
currency: (data && data.currency) || (self.state.selectedRoom && self.state.selectedRoom.currency),
|
|
2097
|
+
});
|
|
2021
2098
|
self.confirmationPolling = false;
|
|
2022
2099
|
self.render();
|
|
2023
2100
|
return;
|
package/dist/booking-widget.js
CHANGED
|
@@ -160,6 +160,10 @@ class BookingWidget {
|
|
|
160
160
|
s3BaseUrl: options.s3BaseUrl || null,
|
|
161
161
|
/** Base URL for confirmation status polling (same as API_BASE_URL). Default https://ai.thehotelplanet.com */
|
|
162
162
|
confirmationBaseUrl: options.confirmationBaseUrl || 'https://ai.thehotelplanet.com',
|
|
163
|
+
/** PostHog: project API key (host page must load posthog-js and set window.posthog). */
|
|
164
|
+
posthogKey: options.posthogKey || null,
|
|
165
|
+
/** PostHog: API host (e.g. https://us.i.posthog.com). */
|
|
166
|
+
posthogHost: options.posthogHost || 'https://us.i.posthog.com',
|
|
163
167
|
// Color customization: CONFIG defaults from load-config, installer colors override
|
|
164
168
|
colors: (function () {
|
|
165
169
|
const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
|
|
@@ -242,6 +246,25 @@ class BookingWidget {
|
|
|
242
246
|
this.widget = null;
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
_capture(eventName, properties) {
|
|
250
|
+
if (typeof window !== 'undefined' && window.posthog && typeof window.posthog.capture === 'function') {
|
|
251
|
+
try {
|
|
252
|
+
const mode = this.options.mode === 'sandbox' ? 'sandbox' : 'live';
|
|
253
|
+
const propertyKey = this.options.propertyKey != null ? String(this.options.propertyKey) : undefined;
|
|
254
|
+
const propertyId = this.options.propertyId != null && this.options.propertyId !== '' ? String(this.options.propertyId) : undefined;
|
|
255
|
+
window.posthog.capture(eventName, { ...(properties || {}), mode, propertyKey, propertyId });
|
|
256
|
+
} catch (e) { /* noop */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_identify() {
|
|
261
|
+
if (typeof window !== 'undefined' && this.options.propertyKey && window.posthog && typeof window.posthog.identify === 'function') {
|
|
262
|
+
try {
|
|
263
|
+
window.posthog.identify(String(this.options.propertyKey).trim());
|
|
264
|
+
} catch (e) { /* noop */ }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
245
268
|
// ===== Helpers =====
|
|
246
269
|
getNights() {
|
|
247
270
|
if (!this.state.checkIn || !this.state.checkOut) return 0;
|
|
@@ -305,6 +328,7 @@ class BookingWidget {
|
|
|
305
328
|
this._fetchRuntimeConfig();
|
|
306
329
|
}
|
|
307
330
|
this.render();
|
|
331
|
+
this._capture('widgetOpened');
|
|
308
332
|
if (this.options.onOpen) this.options.onOpen();
|
|
309
333
|
}
|
|
310
334
|
|
|
@@ -374,6 +398,12 @@ class BookingWidget {
|
|
|
374
398
|
this.bookingApi.fetchRooms(params).then((rooms) => {
|
|
375
399
|
this.ROOMS = Array.isArray(rooms) ? rooms : [];
|
|
376
400
|
this.loadingRooms = false;
|
|
401
|
+
this._capture('search', {
|
|
402
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
403
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
404
|
+
rooms: this.state.rooms,
|
|
405
|
+
occupancies: this.state.occupancies,
|
|
406
|
+
});
|
|
377
407
|
this.render();
|
|
378
408
|
}).catch((err) => {
|
|
379
409
|
this.apiError = err.message || 'Failed to load rooms';
|
|
@@ -473,10 +503,12 @@ class BookingWidget {
|
|
|
473
503
|
const cacheKey = isSandbox ? key + ':sandbox' : key;
|
|
474
504
|
|
|
475
505
|
if (__bwConfigCache[cacheKey]) {
|
|
476
|
-
|
|
506
|
+
const cached = __bwConfigCache[cacheKey];
|
|
507
|
+
this._applyApiColors(cached);
|
|
477
508
|
this._configState = 'loaded';
|
|
478
509
|
this.applyColors();
|
|
479
|
-
|
|
510
|
+
this._initPosthog(cached._posthogKey || '');
|
|
511
|
+
return Promise.resolve(cached);
|
|
480
512
|
}
|
|
481
513
|
|
|
482
514
|
this._configState = 'loading';
|
|
@@ -495,10 +527,12 @@ class BookingWidget {
|
|
|
495
527
|
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
496
528
|
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
497
529
|
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
530
|
+
apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
|
|
498
531
|
__bwConfigCache[cacheKey] = apiColors;
|
|
499
532
|
self._applyApiColors(apiColors);
|
|
500
533
|
self._configState = 'loaded';
|
|
501
534
|
self.applyColors();
|
|
535
|
+
self._initPosthog(apiColors._posthogKey);
|
|
502
536
|
self.render();
|
|
503
537
|
return apiColors;
|
|
504
538
|
})
|
|
@@ -511,6 +545,28 @@ class BookingWidget {
|
|
|
511
545
|
return this._configPromise;
|
|
512
546
|
}
|
|
513
547
|
|
|
548
|
+
_initPosthog(configKey) {
|
|
549
|
+
const key = this.options.posthogKey || configKey || '';
|
|
550
|
+
if (!key || typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
551
|
+
if (this._posthogInited) return;
|
|
552
|
+
try {
|
|
553
|
+
// Inject the posthog loader snippet if posthog has not been loaded yet.
|
|
554
|
+
// This makes the standalone version self-sufficient — no CDN <script> needed from the host.
|
|
555
|
+
if (!window.posthog || !window.posthog.__SV) {
|
|
556
|
+
/* eslint-disable */
|
|
557
|
+
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split('.');2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement('script')).type='text/javascript',p.async=!0,p.src=s.api_host.replace('.i.posthog.com','-assets.i.posthog.com')+'/static/array.js',(r=t.getElementsByTagName('script')[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a='posthog',u.people=u.people||[],u.toString=function(t){var e='posthog';return'posthog'!==a&&(e+='.'+a),t||(e+=' (stub)'),e},u.people.toString=function(){return u.toString(1)+' (stub)'},o='capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId setPersonPropertiesForFlags'.split(' '),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
558
|
+
/* eslint-enable */
|
|
559
|
+
}
|
|
560
|
+
window.posthog.init(key, { api_host: this.options.posthogHost || 'https://us.i.posthog.com' });
|
|
561
|
+
this._posthogInited = true;
|
|
562
|
+
this._identify();
|
|
563
|
+
this._capture('widgetLoaded');
|
|
564
|
+
if (this.overlay && this.overlay.classList.contains('active')) {
|
|
565
|
+
this._capture('widgetOpened');
|
|
566
|
+
}
|
|
567
|
+
} catch (e) { /* noop */ }
|
|
568
|
+
}
|
|
569
|
+
|
|
514
570
|
injectCSS() {
|
|
515
571
|
if (!document.getElementById('booking-widget-styles')) {
|
|
516
572
|
const link = document.createElement('link');
|
|
@@ -955,7 +1011,9 @@ class BookingWidget {
|
|
|
955
1011
|
const grid = this.widget.querySelector('.room-grid');
|
|
956
1012
|
const scrollLeft = grid ? grid.scrollLeft : 0;
|
|
957
1013
|
if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
|
|
958
|
-
|
|
1014
|
+
const room = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1015
|
+
this.state.selectedRoom = room;
|
|
1016
|
+
this._capture('selectedRoom', { roomId: id, roomName: room && room.name });
|
|
959
1017
|
this.render();
|
|
960
1018
|
const restoreScroll = () => {
|
|
961
1019
|
const newGrid = this.widget.querySelector('.room-grid');
|
|
@@ -1049,6 +1107,7 @@ class BookingWidget {
|
|
|
1049
1107
|
|
|
1050
1108
|
selectRate(id) {
|
|
1051
1109
|
this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1110
|
+
this._capture('selectedRate', { rateId: id });
|
|
1052
1111
|
this.render();
|
|
1053
1112
|
}
|
|
1054
1113
|
|
|
@@ -1306,6 +1365,16 @@ class BookingWidget {
|
|
|
1306
1365
|
const code = res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code);
|
|
1307
1366
|
this.confirmationCode = code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
|
|
1308
1367
|
this.state.step = 'confirmation';
|
|
1368
|
+
this._capture('booked', {
|
|
1369
|
+
confirmationCode: this.confirmationCode,
|
|
1370
|
+
guest: this.state.guest,
|
|
1371
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
1372
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
1373
|
+
roomName: this.state.selectedRoom && this.state.selectedRoom.name,
|
|
1374
|
+
roomId: this.state.selectedRoom && this.state.selectedRoom.id,
|
|
1375
|
+
rateId: this.state.selectedRate && this.state.selectedRate.id,
|
|
1376
|
+
currency: this.state.selectedRoom && this.state.selectedRoom.currency,
|
|
1377
|
+
});
|
|
1309
1378
|
this.render();
|
|
1310
1379
|
})
|
|
1311
1380
|
.catch((err) => {
|
|
@@ -1431,6 +1500,18 @@ class BookingWidget {
|
|
|
1431
1500
|
this.confirmationStatus = status || this.confirmationStatus || 'pending';
|
|
1432
1501
|
if (status === 'confirmed') {
|
|
1433
1502
|
this.confirmationDetails = data;
|
|
1503
|
+
this._capture('booked', {
|
|
1504
|
+
confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
|
|
1505
|
+
bookingId: data?.bookingId ?? data?.booking_id,
|
|
1506
|
+
guest: this.state.guest,
|
|
1507
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
1508
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
1509
|
+
roomName: this.state.selectedRoom && this.state.selectedRoom.name,
|
|
1510
|
+
roomId: this.state.selectedRoom && this.state.selectedRoom.id,
|
|
1511
|
+
rateId: this.state.selectedRate && this.state.selectedRate.id,
|
|
1512
|
+
totalAmount: data?.totalAmount ?? data?.total_amount,
|
|
1513
|
+
currency: data?.currency || (this.state.selectedRoom && this.state.selectedRoom.currency),
|
|
1514
|
+
});
|
|
1434
1515
|
this.confirmationPolling = false;
|
|
1435
1516
|
this.render();
|
|
1436
1517
|
return;
|
|
@@ -5,6 +5,7 @@ import '../core/styles.css';
|
|
|
5
5
|
import { createBookingApi, formatPrice, buildCheckoutPayload, buildPaymentIntentPayload } from '../core/booking-api.js';
|
|
6
6
|
import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
|
|
7
7
|
import { fetchRuntimeConfig } from '../utils/config-service.js';
|
|
8
|
+
import { init as initAnalytics, capture as captureEvent, identify as identifyAnalytics } from '../utils/analytics.js';
|
|
8
9
|
|
|
9
10
|
const BASE_STEPS = [
|
|
10
11
|
{ key: 'dates', label: 'Dates & Guests', num: '01' },
|
|
@@ -45,6 +46,10 @@ const BookingWidget = ({
|
|
|
45
46
|
createPaymentIntent: createPaymentIntentProp = null,
|
|
46
47
|
/** Stripe: deprecated (booking completion is fetched from confirmation endpoint instead). */
|
|
47
48
|
onBookingComplete: onBookingCompleteProp = null,
|
|
49
|
+
/** PostHog: optional override for analytics key (defaults to VITE_POSTHOG_KEY). */
|
|
50
|
+
posthogKey = '',
|
|
51
|
+
/** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
|
|
52
|
+
posthogHost = '',
|
|
48
53
|
}) => {
|
|
49
54
|
const stripePublishableKey = stripePublishableKeyProp || STRIPE_PUBLISHABLE_KEY;
|
|
50
55
|
const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
|
|
@@ -134,9 +139,26 @@ const BookingWidget = ({
|
|
|
134
139
|
return null;
|
|
135
140
|
}, [bookingApiProp, effectiveApiBaseUrl, effectiveAvailabilityBaseUrl, effectivePropertyBaseUrl, effectiveS3BaseUrl, apiSecret, propertyId, propertyKey, mode]);
|
|
136
141
|
|
|
142
|
+
|
|
143
|
+
const analyticsMode = isSandbox ? 'sandbox' : 'live';
|
|
144
|
+
const analyticsContext = {
|
|
145
|
+
mode: analyticsMode,
|
|
146
|
+
propertyKey: propertyKey || undefined,
|
|
147
|
+
propertyId: propertyId != null && propertyId !== '' ? String(propertyId) : undefined,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Track widgetLoaded when config has loaded (widget is ready). Identify by propertyKey.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (hasPropertyKey && configLoaded) {
|
|
153
|
+
identifyAnalytics(propertyKey);
|
|
154
|
+
captureEvent('widgetLoaded', analyticsContext);
|
|
155
|
+
}
|
|
156
|
+
}, [hasPropertyKey, configLoaded, propertyKey, propertyId, analyticsMode]);
|
|
157
|
+
|
|
137
158
|
useEffect(() => {
|
|
138
159
|
if (isOpen && onOpen) onOpen();
|
|
139
|
-
|
|
160
|
+
if (isOpen) captureEvent('widgetOpened', analyticsContext);
|
|
161
|
+
}, [isOpen, onOpen, analyticsMode]);
|
|
140
162
|
|
|
141
163
|
// Delay adding 'active' by one frame so the browser can paint the initial state and animate open
|
|
142
164
|
useEffect(() => {
|
|
@@ -211,7 +233,22 @@ const BookingWidget = ({
|
|
|
211
233
|
const status = data?.status != null ? String(data.status) : '';
|
|
212
234
|
if (!cancelled) setConfirmationStatus(status || 'pending');
|
|
213
235
|
if (status === 'confirmed') {
|
|
214
|
-
if (!cancelled)
|
|
236
|
+
if (!cancelled) {
|
|
237
|
+
setConfirmationDetails(data);
|
|
238
|
+
captureEvent('booked', {
|
|
239
|
+
...analyticsContext,
|
|
240
|
+
confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
|
|
241
|
+
bookingId: data?.bookingId ?? data?.booking_id,
|
|
242
|
+
guest: state.guest,
|
|
243
|
+
checkIn: state.checkIn?.toISOString?.(),
|
|
244
|
+
checkOut: state.checkOut?.toISOString?.(),
|
|
245
|
+
roomName: state.selectedRoom?.name,
|
|
246
|
+
roomId: state.selectedRoom?.id,
|
|
247
|
+
rateId: state.selectedRate?.id,
|
|
248
|
+
totalAmount: data?.totalAmount ?? data?.total_amount,
|
|
249
|
+
currency: data?.currency ?? state.selectedRoom?.currency,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
215
252
|
return;
|
|
216
253
|
}
|
|
217
254
|
} catch (err) {
|
|
@@ -241,8 +278,10 @@ const BookingWidget = ({
|
|
|
241
278
|
setConfigError(null);
|
|
242
279
|
setConfigLoaded(false);
|
|
243
280
|
fetchRuntimeConfig(propertyKey, colors, mode)
|
|
244
|
-
.then(({ widgetStyles }) => {
|
|
281
|
+
.then(({ widgetStyles, posthogKey: configPosthogKey }) => {
|
|
245
282
|
if (cancelled) return;
|
|
283
|
+
const analyticsKey = posthogKey || configPosthogKey || '';
|
|
284
|
+
initAnalytics(analyticsKey || posthogHost ? { key: analyticsKey || undefined, host: posthogHost || undefined } : {});
|
|
246
285
|
setRuntimeWidgetStyles(widgetStyles);
|
|
247
286
|
setConfigLoaded(true);
|
|
248
287
|
setConfigLoading(false);
|
|
@@ -319,6 +358,13 @@ const BookingWidget = ({
|
|
|
319
358
|
api.fetchRooms(params).then((rooms) => {
|
|
320
359
|
setRoomsList(Array.isArray(rooms) ? rooms : []);
|
|
321
360
|
setLoadingRooms(false);
|
|
361
|
+
captureEvent('search', {
|
|
362
|
+
...analyticsContext,
|
|
363
|
+
checkIn: state.checkIn?.toISOString?.(),
|
|
364
|
+
checkOut: state.checkOut?.toISOString?.(),
|
|
365
|
+
rooms: state.rooms,
|
|
366
|
+
occupancies: state.occupancies,
|
|
367
|
+
});
|
|
322
368
|
}).catch((err) => {
|
|
323
369
|
setApiError(err.message || 'Failed to load rooms');
|
|
324
370
|
setLoadingRooms(false);
|
|
@@ -443,11 +489,14 @@ const BookingWidget = ({
|
|
|
443
489
|
};
|
|
444
490
|
|
|
445
491
|
const selectRoom = (id) => {
|
|
446
|
-
|
|
492
|
+
const room = roomsList.find(r => r.id === id);
|
|
493
|
+
setState(prev => ({ ...prev, selectedRoom: room }));
|
|
494
|
+
captureEvent('selectedRoom', { ...analyticsContext, roomId: id, roomName: room?.name });
|
|
447
495
|
};
|
|
448
496
|
|
|
449
497
|
const selectRate = (id) => {
|
|
450
498
|
setState(prev => ({ ...prev, selectedRate: ratesList.find(r => r.id === id) }));
|
|
499
|
+
captureEvent('selectedRate', { ...analyticsContext, rateId: id });
|
|
451
500
|
};
|
|
452
501
|
|
|
453
502
|
const updateGuest = (field, value) => {
|
|
@@ -502,6 +551,17 @@ const BookingWidget = ({
|
|
|
502
551
|
const code = res && (res.confirmationCode ?? res.confirmation_code);
|
|
503
552
|
setConfirmationCode(code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6)));
|
|
504
553
|
setState(prev => ({ ...prev, step: 'confirmation' }));
|
|
554
|
+
captureEvent('booked', {
|
|
555
|
+
...analyticsContext,
|
|
556
|
+
confirmationCode: code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6)),
|
|
557
|
+
guest: state.guest,
|
|
558
|
+
checkIn: state.checkIn?.toISOString?.(),
|
|
559
|
+
checkOut: state.checkOut?.toISOString?.(),
|
|
560
|
+
roomName: state.selectedRoom?.name,
|
|
561
|
+
roomId: state.selectedRoom?.id,
|
|
562
|
+
rateId: state.selectedRate?.id,
|
|
563
|
+
currency: state.selectedRoom?.currency,
|
|
564
|
+
});
|
|
505
565
|
})
|
|
506
566
|
.catch((err) => {
|
|
507
567
|
setApiError(err?.message || err || 'Booking failed');
|
|
@@ -12,7 +12,7 @@ import { deriveWidgetStyles } from '../core/color-utils.js';
|
|
|
12
12
|
|
|
13
13
|
const CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/load-config';
|
|
14
14
|
|
|
15
|
-
/** In-memory cache: propertyKey →
|
|
15
|
+
/** In-memory cache: propertyKey → { apiColors, posthogKey } */
|
|
16
16
|
const _configCache = new Map();
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -83,9 +83,9 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
|
|
|
83
83
|
const cacheKey = isSandbox ? `${key}:sandbox` : key;
|
|
84
84
|
|
|
85
85
|
if (_configCache.has(cacheKey)) {
|
|
86
|
-
const apiColors = _configCache.get(cacheKey);
|
|
86
|
+
const { apiColors, posthogKey } = _configCache.get(cacheKey);
|
|
87
87
|
const colors = mergeColors(apiColors, installerColors);
|
|
88
|
-
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
|
|
88
|
+
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
|
|
@@ -96,8 +96,9 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
|
|
|
96
96
|
|
|
97
97
|
const data = await res.json();
|
|
98
98
|
const apiColors = mapApiColors(data);
|
|
99
|
-
|
|
99
|
+
const posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
|
|
100
|
+
_configCache.set(cacheKey, { apiColors, posthogKey });
|
|
100
101
|
|
|
101
102
|
const colors = mergeColors(apiColors, installerColors);
|
|
102
|
-
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
|
|
103
|
+
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
|
|
103
104
|
}
|
|
@@ -482,6 +482,7 @@ import '../core/styles.css';
|
|
|
482
482
|
import { createBookingApi, formatPrice, buildCheckoutPayload, buildPaymentIntentPayload } from '../core/booking-api.js';
|
|
483
483
|
import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
|
|
484
484
|
import { fetchRuntimeConfig } from '../utils/config-service.js';
|
|
485
|
+
import { init as initAnalytics, capture as captureEvent, identify as identifyAnalytics } from '../utils/analytics.js';
|
|
485
486
|
|
|
486
487
|
|
|
487
488
|
const BASE_STEPS = [
|
|
@@ -525,6 +526,10 @@ export default {
|
|
|
525
526
|
createPaymentIntent: { type: Function, default: null },
|
|
526
527
|
/** Stripe: deprecated (booking completion is fetched from confirmation endpoint instead). */
|
|
527
528
|
onBookingComplete: { type: Function, default: null },
|
|
529
|
+
/** PostHog: optional override for analytics key (defaults to VITE_POSTHOG_KEY). */
|
|
530
|
+
posthogKey: { type: String, default: '' },
|
|
531
|
+
/** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
|
|
532
|
+
posthogHost: { type: String, default: '' },
|
|
528
533
|
},
|
|
529
534
|
emits: ['close', 'complete', 'open'],
|
|
530
535
|
data() {
|
|
@@ -722,6 +727,16 @@ export default {
|
|
|
722
727
|
isVisible() {
|
|
723
728
|
return this.isOpen && !this.isClosing && this.isReadyForOpen;
|
|
724
729
|
},
|
|
730
|
+
analyticsMode() {
|
|
731
|
+
return this.isSandbox ? 'sandbox' : 'live';
|
|
732
|
+
},
|
|
733
|
+
analyticsContext() {
|
|
734
|
+
return {
|
|
735
|
+
mode: this.analyticsMode,
|
|
736
|
+
propertyKey: this.propertyKey || undefined,
|
|
737
|
+
propertyId: this.propertyId != null && this.propertyId !== '' ? String(this.propertyId) : undefined,
|
|
738
|
+
};
|
|
739
|
+
},
|
|
725
740
|
},
|
|
726
741
|
created() {
|
|
727
742
|
this._initRuntimeConfig();
|
|
@@ -740,6 +755,7 @@ export default {
|
|
|
740
755
|
isOpen: {
|
|
741
756
|
handler(open) {
|
|
742
757
|
if (open && this.onOpen) this.onOpen();
|
|
758
|
+
if (open) captureEvent('widgetOpened', this.analyticsContext);
|
|
743
759
|
if (!open || this.isClosing) {
|
|
744
760
|
this.isReadyForOpen = false;
|
|
745
761
|
return;
|
|
@@ -753,6 +769,14 @@ export default {
|
|
|
753
769
|
},
|
|
754
770
|
immediate: true,
|
|
755
771
|
},
|
|
772
|
+
configLoaded: {
|
|
773
|
+
handler(loaded) {
|
|
774
|
+
if (loaded && this.hasPropertyKey) {
|
|
775
|
+
identifyAnalytics(this.propertyKey);
|
|
776
|
+
captureEvent('widgetLoaded', this.analyticsContext);
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
},
|
|
756
780
|
'state.step': function(step) {
|
|
757
781
|
if (step !== 'summary' && step !== 'payment') {
|
|
758
782
|
this.checkoutShowPaymentForm = false;
|
|
@@ -782,7 +806,9 @@ export default {
|
|
|
782
806
|
this.configError = null;
|
|
783
807
|
this.configLoaded = false;
|
|
784
808
|
try {
|
|
785
|
-
const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
|
|
809
|
+
const { widgetStyles, posthogKey: configPosthogKey } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
|
|
810
|
+
const analyticsKey = this.posthogKey || configPosthogKey || '';
|
|
811
|
+
initAnalytics(analyticsKey || this.posthogHost ? { key: analyticsKey || undefined, host: this.posthogHost || undefined } : {});
|
|
786
812
|
this.runtimeWidgetStyles = widgetStyles;
|
|
787
813
|
this.configLoaded = true;
|
|
788
814
|
} catch (err) {
|
|
@@ -812,6 +838,19 @@ export default {
|
|
|
812
838
|
this.confirmationStatus = status || this.confirmationStatus || 'pending';
|
|
813
839
|
if (status === 'confirmed') {
|
|
814
840
|
this.confirmationDetails = data;
|
|
841
|
+
captureEvent('booked', {
|
|
842
|
+
...this.analyticsContext,
|
|
843
|
+
confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
|
|
844
|
+
bookingId: data?.bookingId ?? data?.booking_id,
|
|
845
|
+
guest: this.state.guest,
|
|
846
|
+
checkIn: this.state.checkIn?.toISOString?.(),
|
|
847
|
+
checkOut: this.state.checkOut?.toISOString?.(),
|
|
848
|
+
roomName: this.state.selectedRoom?.name,
|
|
849
|
+
roomId: this.state.selectedRoom?.id,
|
|
850
|
+
rateId: this.state.selectedRate?.id,
|
|
851
|
+
totalAmount: data?.totalAmount ?? data?.total_amount,
|
|
852
|
+
currency: data?.currency ?? this.state.selectedRoom?.currency,
|
|
853
|
+
});
|
|
815
854
|
return;
|
|
816
855
|
}
|
|
817
856
|
} catch (err) {
|
|
@@ -880,6 +919,13 @@ export default {
|
|
|
880
919
|
api.fetchRooms(params).then((rooms) => {
|
|
881
920
|
this.roomsList = Array.isArray(rooms) ? rooms : [];
|
|
882
921
|
this.loadingRooms = false;
|
|
922
|
+
captureEvent('search', {
|
|
923
|
+
...this.analyticsContext,
|
|
924
|
+
checkIn: this.state.checkIn?.toISOString?.(),
|
|
925
|
+
checkOut: this.state.checkOut?.toISOString?.(),
|
|
926
|
+
rooms: this.state.rooms,
|
|
927
|
+
occupancies: this.state.occupancies,
|
|
928
|
+
});
|
|
883
929
|
}).catch((err) => {
|
|
884
930
|
this.apiError = err.message || 'Failed to load rooms';
|
|
885
931
|
this.loadingRooms = false;
|
|
@@ -972,6 +1018,17 @@ export default {
|
|
|
972
1018
|
const code = res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code);
|
|
973
1019
|
this.confirmationCode = code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
|
|
974
1020
|
this.state.step = 'confirmation';
|
|
1021
|
+
captureEvent('booked', {
|
|
1022
|
+
...this.analyticsContext,
|
|
1023
|
+
confirmationCode: this.confirmationCode,
|
|
1024
|
+
guest: this.state.guest,
|
|
1025
|
+
checkIn: this.state.checkIn?.toISOString?.(),
|
|
1026
|
+
checkOut: this.state.checkOut?.toISOString?.(),
|
|
1027
|
+
roomName: this.state.selectedRoom?.name,
|
|
1028
|
+
roomId: this.state.selectedRoom?.id,
|
|
1029
|
+
rateId: this.state.selectedRate?.id,
|
|
1030
|
+
currency: this.state.selectedRoom?.currency,
|
|
1031
|
+
});
|
|
975
1032
|
})
|
|
976
1033
|
.catch((err) => {
|
|
977
1034
|
this.apiError = (err && err.message) || err || 'Booking failed';
|
|
@@ -1124,10 +1181,13 @@ export default {
|
|
|
1124
1181
|
}
|
|
1125
1182
|
},
|
|
1126
1183
|
selectRoom(id) {
|
|
1127
|
-
|
|
1184
|
+
const room = this.roomsList.find(r => r.id === id);
|
|
1185
|
+
this.updateState({ selectedRoom: room });
|
|
1186
|
+
captureEvent('selectedRoom', { ...this.analyticsContext, roomId: id, roomName: room?.name });
|
|
1128
1187
|
},
|
|
1129
1188
|
selectRate(id) {
|
|
1130
1189
|
this.updateState({ selectedRate: this.ratesList.find(r => r.id === id) });
|
|
1190
|
+
captureEvent('selectedRate', { ...this.analyticsContext, rateId: id });
|
|
1131
1191
|
},
|
|
1132
1192
|
updateGuest(field, value) {
|
|
1133
1193
|
this.updateState({ guest: { ...this.state.guest, [field]: value } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuitee/booking-widget",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "A beautiful, customizable booking widget modal that can be embedded in any website. Supports vanilla JavaScript, React, and Vue.js.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
},
|
|
94
94
|
"dependencies": {
|
|
95
95
|
"@stripe/stripe-js": "^4.8.0",
|
|
96
|
-
"lucide-react": "^0.263.1"
|
|
96
|
+
"lucide-react": "^0.263.1",
|
|
97
|
+
"posthog-js": "^1.360.1"
|
|
97
98
|
}
|
|
98
99
|
}
|