@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('Widget opened'),
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.6/dist/booking-widget.css">
98
- <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.6/dist/booking-widget-standalone.js"></script>
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.6/dist/booking-widget.css',
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
- this._applyApiColors(__bwConfigCache[cacheKey]);
1227
+ const cached = __bwConfigCache[cacheKey];
1228
+ this._applyApiColors(cached);
1200
1229
  this._configState = 'loaded';
1201
1230
  this.applyColors();
1202
- return Promise.resolve(__bwConfigCache[cacheKey]);
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
- this.state.selectedRoom = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
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;
@@ -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
- this._applyApiColors(__bwConfigCache[cacheKey]);
506
+ const cached = __bwConfigCache[cacheKey];
507
+ this._applyApiColors(cached);
477
508
  this._configState = 'loaded';
478
509
  this.applyColors();
479
- return Promise.resolve(__bwConfigCache[cacheKey]);
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
- this.state.selectedRoom = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
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
- }, [isOpen, onOpen]);
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) setConfirmationDetails(data);
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
- setState(prev => ({ ...prev, selectedRoom: roomsList.find(r => r.id === id) }));
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 → raw API color object */
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
- _configCache.set(cacheKey, apiColors);
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
- this.updateState({ selectedRoom: this.roomsList.find(r => r.id === id) });
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.6",
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
  }