@nuitee/booking-widget 1.0.5 → 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.5/dist/booking-widget.css">
98
- <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.5/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.5/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'),
@@ -230,6 +230,7 @@ function createBookingApi(config = {}) {
230
230
  const url = `${(base || '').replace(/\/$/, '')}${path}`;
231
231
  const headers = {
232
232
  'Content-Type': 'application/json',
233
+ 'Source': 'booking_engine',
233
234
  ...staticHeaders,
234
235
  ...getHeaders(),
235
236
  };
@@ -291,8 +292,8 @@ function createBookingApi(config = {}) {
291
292
  ? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
292
293
  : `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
293
294
  try {
294
- const getOpts = { method: 'GET' };
295
- if (propFullUrl.includes('ngrok')) getOpts.headers = { 'ngrok-skip-browser-warning': 'true' };
295
+ const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
296
+ if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
296
297
  const propRes = await fetch(propFullUrl, getOpts);
297
298
  if (propRes.ok) {
298
299
  const propData = await propRes.json();
@@ -692,7 +693,7 @@ async function decryptPropertyId(options = {}) {
692
693
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
693
694
  const res = await fetch(url, {
694
695
  method: 'POST',
695
- headers: { 'Content-Type': 'application/json', ...headers },
696
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
696
697
  body: JSON.stringify({ hash: propertyKey }),
697
698
  });
698
699
  if (!res.ok) throw new Error(res.statusText || 'Failed to decrypt property id');
@@ -719,7 +720,7 @@ async function fetchBookingEnginePref(options = {}) {
719
720
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
720
721
  const res = await fetch(url, {
721
722
  method: 'GET',
722
- headers: { 'Content-Type': 'application/json', ...headers },
723
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
723
724
  });
724
725
  if (!res.ok) throw new Error(res.statusText || 'Failed to fetch config');
725
726
 
@@ -906,7 +907,7 @@ if (typeof window !== 'undefined') {
906
907
  const url = defaultApiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (options.mode === 'sandbox' ? '?sandbox=true' : '');
907
908
  return fetch(url, {
908
909
  method: 'POST',
909
- headers: { 'Content-Type': 'application/json' },
910
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine' },
910
911
  body: JSON.stringify(payload),
911
912
  }).then(function (r) {
912
913
  if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
@@ -935,6 +936,8 @@ if (typeof window !== 'undefined') {
935
936
  mode: options.mode || null,
936
937
  bookingApi: options.bookingApi || null,
937
938
  cssUrl: options.cssUrl || null,
939
+ posthogKey: options.posthogKey || null,
940
+ posthogHost: options.posthogHost || 'https://us.i.posthog.com',
938
941
  // Color customization: CONFIG defaults from load-config, installer colors override
939
942
  colors: (function () {
940
943
  const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
@@ -1045,6 +1048,25 @@ if (typeof window !== 'undefined') {
1045
1048
  return this.STEPS.findIndex(s => s.key === key);
1046
1049
  }
1047
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
+
1048
1070
  open() {
1049
1071
  if (!this.container) this.init();
1050
1072
  if (!this.overlay || !this.widget) return; // container element not found
@@ -1055,6 +1077,7 @@ if (typeof window !== 'undefined') {
1055
1077
  this._fetchRuntimeConfig();
1056
1078
  }
1057
1079
  this.render();
1080
+ this._capture('widgetOpened');
1058
1081
  if (this.options.onOpen) this.options.onOpen();
1059
1082
  }
1060
1083
 
@@ -1111,6 +1134,12 @@ if (typeof window !== 'undefined') {
1111
1134
  this.bookingApi.fetchRooms(params).then((rooms) => {
1112
1135
  this.ROOMS = Array.isArray(rooms) ? rooms : [];
1113
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
+ });
1114
1143
  this.render();
1115
1144
  }).catch((err) => {
1116
1145
  this.apiError = err.message || 'Failed to load rooms';
@@ -1195,17 +1224,19 @@ if (typeof window !== 'undefined') {
1195
1224
  const cacheKey = isSandbox ? key + ':sandbox' : key;
1196
1225
 
1197
1226
  if (__bwConfigCache[cacheKey]) {
1198
- this._applyApiColors(__bwConfigCache[cacheKey]);
1227
+ const cached = __bwConfigCache[cacheKey];
1228
+ this._applyApiColors(cached);
1199
1229
  this._configState = 'loaded';
1200
1230
  this.applyColors();
1201
- return Promise.resolve(__bwConfigCache[cacheKey]);
1231
+ this._initPosthog(cached._posthogKey || '');
1232
+ return Promise.resolve(cached);
1202
1233
  }
1203
1234
 
1204
1235
  this._configState = 'loading';
1205
1236
  this.render();
1206
1237
 
1207
1238
  const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
1208
- this._configPromise = fetch(url)
1239
+ this._configPromise = fetch(url, { headers: { 'Source': 'booking_engine' } })
1209
1240
  .then(function (res) {
1210
1241
  if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
1211
1242
  return res.json();
@@ -1217,10 +1248,12 @@ if (typeof window !== 'undefined') {
1217
1248
  if (data.primaryColor) apiColors.primary = data.primaryColor;
1218
1249
  if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
1219
1250
  if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
1251
+ apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
1220
1252
  __bwConfigCache[cacheKey] = apiColors;
1221
1253
  self._applyApiColors(apiColors);
1222
1254
  self._configState = 'loaded';
1223
1255
  self.applyColors();
1256
+ self._initPosthog(apiColors._posthogKey);
1224
1257
  self.render();
1225
1258
  return apiColors;
1226
1259
  })
@@ -1233,6 +1266,26 @@ if (typeof window !== 'undefined') {
1233
1266
  return this._configPromise;
1234
1267
  }
1235
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
+
1236
1289
  _retryConfigFetch() {
1237
1290
  this._configState = 'idle';
1238
1291
  this._configError = null;
@@ -1682,7 +1735,9 @@ if (typeof window !== 'undefined') {
1682
1735
  const grid = this.widget.querySelector('.room-grid');
1683
1736
  const scrollLeft = grid ? grid.scrollLeft : 0;
1684
1737
  if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
1685
- 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 });
1686
1741
  this.render();
1687
1742
  const restoreScroll = () => {
1688
1743
  const newGrid = this.widget.querySelector('.room-grid');
@@ -1742,6 +1797,7 @@ if (typeof window !== 'undefined') {
1742
1797
 
1743
1798
  selectRate(id) {
1744
1799
  this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
1800
+ this._capture('selectedRate', { rateId: id });
1745
1801
  this.render();
1746
1802
  }
1747
1803
 
@@ -1974,6 +2030,16 @@ if (typeof window !== 'undefined') {
1974
2030
  .then((res) => {
1975
2031
  this.confirmationCode = (res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code)) || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
1976
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
+ });
1977
2043
  this.render();
1978
2044
  })
1979
2045
  .catch((err) => {
@@ -1991,7 +2057,7 @@ if (typeof window !== 'undefined') {
1991
2057
  if (!token) throw new Error('Missing confirmation token');
1992
2058
  const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
1993
2059
  const url = base + '/proxy/confirmation/' + encodeURIComponent(token) + (this.options.mode === 'sandbox' ? '?sandbox=true' : '');
1994
- const res = await fetch(url, { method: 'POST' });
2060
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
1995
2061
  if (!res.ok) throw new Error(await res.text());
1996
2062
  return await res.json();
1997
2063
  }
@@ -2017,6 +2083,18 @@ if (typeof window !== 'undefined') {
2017
2083
  self.confirmationStatus = status || self.confirmationStatus || 'pending';
2018
2084
  if (status === 'confirmed') {
2019
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
+ });
2020
2098
  self.confirmationPolling = false;
2021
2099
  self.render();
2022
2100
  return;
@@ -122,7 +122,7 @@ class BookingWidget {
122
122
  const url = apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (builtInMode === 'sandbox' ? '?sandbox=true' : '');
123
123
  return fetch(url, {
124
124
  method: 'POST',
125
- headers: { 'Content-Type': 'application/json' },
125
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine' },
126
126
  body: JSON.stringify(payload),
127
127
  }).then(function (r) {
128
128
  if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
@@ -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,17 +503,19 @@ 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';
483
515
  this.render();
484
516
 
485
517
  const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
486
- this._configPromise = fetch(url)
518
+ this._configPromise = fetch(url, { headers: { 'Source': 'booking_engine' } })
487
519
  .then(function (res) {
488
520
  if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
489
521
  return res.json();
@@ -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) => {
@@ -1407,7 +1476,7 @@ class BookingWidget {
1407
1476
  if (!token) throw new Error('Missing confirmation token');
1408
1477
  const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
1409
1478
  const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}${this.options.mode === 'sandbox' ? '?sandbox=true' : ''}`;
1410
- const res = await fetch(url, { method: 'POST' });
1479
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
1411
1480
  if (!res.ok) throw new Error(await res.text());
1412
1481
  return await res.json();
1413
1482
  }
@@ -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;
@@ -229,6 +229,7 @@ function createBookingApi(config = {}) {
229
229
  const url = `${(base || '').replace(/\/$/, '')}${path}`;
230
230
  const headers = {
231
231
  'Content-Type': 'application/json',
232
+ 'Source': 'booking_engine',
232
233
  ...staticHeaders,
233
234
  ...getHeaders(),
234
235
  };
@@ -290,8 +291,8 @@ function createBookingApi(config = {}) {
290
291
  ? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
291
292
  : `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
292
293
  try {
293
- const getOpts = { method: 'GET' };
294
- if (propFullUrl.includes('ngrok')) getOpts.headers = { 'ngrok-skip-browser-warning': 'true' };
294
+ const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
295
+ if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
295
296
  const propRes = await fetch(propFullUrl, getOpts);
296
297
  if (propRes.ok) {
297
298
  const propData = await propRes.json();
@@ -691,7 +692,7 @@ async function decryptPropertyId(options = {}) {
691
692
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
692
693
  const res = await fetch(url, {
693
694
  method: 'POST',
694
- headers: { 'Content-Type': 'application/json', ...headers },
695
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
695
696
  body: JSON.stringify({ hash: propertyKey }),
696
697
  });
697
698
  if (!res.ok) throw new Error(res.statusText || 'Failed to decrypt property id');
@@ -718,7 +719,7 @@ async function fetchBookingEnginePref(options = {}) {
718
719
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
719
720
  const res = await fetch(url, {
720
721
  method: 'GET',
721
- headers: { 'Content-Type': 'application/json', ...headers },
722
+ headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
722
723
  });
723
724
  if (!res.ok) throw new Error(res.statusText || 'Failed to fetch config');
724
725
 
@@ -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 : {};
@@ -57,7 +62,7 @@ const BookingWidget = ({
57
62
  if (!effectivePaymentIntentUrl) return null;
58
63
  return async (payload) => {
59
64
  const url = effectivePaymentIntentUrl + (isSandbox ? '?sandbox=true' : '');
60
- const headers = { 'Content-Type': 'application/json' };
65
+ const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
61
66
  if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
62
67
  const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
63
68
  if (!res.ok) throw new Error(await res.text());
@@ -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(() => {
@@ -205,13 +227,28 @@ const BookingWidget = ({
205
227
  if (cancelled) return;
206
228
  try {
207
229
  const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}${isSandbox ? '?sandbox=true' : ''}`;
208
- const res = await fetch(url, { method: 'POST' });
230
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
209
231
  if (!res.ok) throw new Error(await res.text());
210
232
  const data = await res.json();
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,21 +83,22 @@ 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' : ''}`;
92
- const res = await fetch(url);
92
+ const res = await fetch(url, { headers: { 'Source': 'booking_engine' } });
93
93
  if (!res.ok) {
94
94
  throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
95
95
  }
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() {
@@ -604,7 +609,7 @@ export default {
604
609
  if (!url) return null;
605
610
  return async (payload) => {
606
611
  const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
607
- const headers = { 'Content-Type': 'application/json' };
612
+ const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
608
613
  if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
609
614
  const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
610
615
  if (!res.ok) throw new Error(await res.text());
@@ -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) {
@@ -795,7 +821,7 @@ export default {
795
821
  const t = String(token || '').trim();
796
822
  if (!t) throw new Error('Missing confirmation token');
797
823
  const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
798
- const res = await fetch(url, { method: 'POST' });
824
+ const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
799
825
  if (!res.ok) throw new Error(await res.text());
800
826
  return await res.json();
801
827
  },
@@ -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.5",
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
  }