@nuitee/booking-widget 1.0.6 → 1.0.8

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.8/dist/booking-widget.css">
98
+ <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.8/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.8/dist/booking-widget.css',
106
106
  propertyKey: 'your-property-key',
107
107
  onOpen: () => console.log('Opened'),
108
108
  onClose: () => console.log('Closed'),
@@ -20,6 +20,11 @@ const DEFAULT_ROOMS = [];
20
20
 
21
21
  const DEFAULT_RATES = [];
22
22
 
23
+ // Capture the native browser fetch at module-load time — before PostHog (or any other
24
+ // library) has a chance to wrap window.fetch for session recording. PostHog's wrapped
25
+ // fetch drops non-standard request headers such as 'Source', breaking API auth.
26
+ const _fetch = typeof fetch === 'function' ? fetch : /* istanbul ignore next */ undefined;
27
+
23
28
  /**
24
29
  * Returns date as YYYY-MM-DD using local date (no timezone shift).
25
30
  * Calendar dates are created at local midnight; using toISOString() would convert to UTC and can shift the day.
@@ -237,7 +242,7 @@ function createBookingApi(config = {}) {
237
242
  if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
238
243
  const options = { method, headers };
239
244
  if (body && (method === 'POST' || method === 'PUT')) options.body = JSON.stringify(body);
240
- const res = await fetch(url, options);
245
+ const res = await _fetch(url, options);
241
246
  if (!res.ok) {
242
247
  let body;
243
248
  try { body = await res.json(); } catch (_) {}
@@ -294,7 +299,7 @@ function createBookingApi(config = {}) {
294
299
  try {
295
300
  const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
296
301
  if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
297
- const propRes = await fetch(propFullUrl, getOpts);
302
+ const propRes = await _fetch(propFullUrl, getOpts);
298
303
  if (propRes.ok) {
299
304
  const propData = await propRes.json();
300
305
  propertyRooms = propData?.rooms ?? {};
@@ -691,7 +696,7 @@ async function decryptPropertyId(options = {}) {
691
696
  if (!propertyKey) return null;
692
697
  const base = (baseUrl || '').replace(/\/$/, '');
693
698
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
694
- const res = await fetch(url, {
699
+ const res = await _fetch(url, {
695
700
  method: 'POST',
696
701
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
697
702
  body: JSON.stringify({ hash: propertyKey }),
@@ -718,7 +723,7 @@ async function fetchBookingEnginePref(options = {}) {
718
723
  }
719
724
  const base = (baseUrl || '').replace(/\/$/, '');
720
725
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
721
- const res = await fetch(url, {
726
+ const res = await _fetch(url, {
722
727
  method: 'GET',
723
728
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
724
729
  });
@@ -872,8 +877,9 @@ if (typeof window !== 'undefined') {
872
877
  var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
873
878
  var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
874
879
  if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
875
- styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
876
- styles['--card-solid'] = styles['--card'];
880
+ var _cardBase = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
881
+ styles['--card'] = c.card ? _cardBase + '40' : _cardBase;
882
+ styles['--card-solid'] = _cardBase;
877
883
  if (bgHsl) {
878
884
  styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
879
885
  styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
@@ -936,6 +942,8 @@ if (typeof window !== 'undefined') {
936
942
  mode: options.mode || null,
937
943
  bookingApi: options.bookingApi || null,
938
944
  cssUrl: options.cssUrl || null,
945
+ posthogKey: options.posthogKey || null,
946
+ posthogHost: options.posthogHost || 'https://us.i.posthog.com',
939
947
  // Color customization: CONFIG defaults from load-config, installer colors override
940
948
  colors: (function () {
941
949
  const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
@@ -1046,6 +1054,25 @@ if (typeof window !== 'undefined') {
1046
1054
  return this.STEPS.findIndex(s => s.key === key);
1047
1055
  }
1048
1056
 
1057
+ _capture(eventName, properties) {
1058
+ if (typeof window !== 'undefined' && window.posthog && typeof window.posthog.capture === 'function') {
1059
+ try {
1060
+ const mode = this.options.mode === 'sandbox' ? 'sandbox' : 'live';
1061
+ const propertyKey = this.options.propertyKey != null ? String(this.options.propertyKey) : undefined;
1062
+ const propertyId = this.options.propertyId != null && this.options.propertyId !== '' ? String(this.options.propertyId) : undefined;
1063
+ window.posthog.capture(eventName, { ...(properties || {}), mode, propertyKey, propertyId });
1064
+ } catch (e) { /* noop */ }
1065
+ }
1066
+ }
1067
+
1068
+ _identify() {
1069
+ if (typeof window !== 'undefined' && this.options.propertyKey && window.posthog && typeof window.posthog.identify === 'function') {
1070
+ try {
1071
+ window.posthog.identify(String(this.options.propertyKey).trim());
1072
+ } catch (e) { /* noop */ }
1073
+ }
1074
+ }
1075
+
1049
1076
  open() {
1050
1077
  if (!this.container) this.init();
1051
1078
  if (!this.overlay || !this.widget) return; // container element not found
@@ -1056,6 +1083,7 @@ if (typeof window !== 'undefined') {
1056
1083
  this._fetchRuntimeConfig();
1057
1084
  }
1058
1085
  this.render();
1086
+ this._capture('widgetOpened');
1059
1087
  if (this.options.onOpen) this.options.onOpen();
1060
1088
  }
1061
1089
 
@@ -1112,6 +1140,12 @@ if (typeof window !== 'undefined') {
1112
1140
  this.bookingApi.fetchRooms(params).then((rooms) => {
1113
1141
  this.ROOMS = Array.isArray(rooms) ? rooms : [];
1114
1142
  this.loadingRooms = false;
1143
+ this._capture('search', {
1144
+ checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
1145
+ checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
1146
+ rooms: this.state.rooms,
1147
+ occupancies: this.state.occupancies,
1148
+ });
1115
1149
  this.render();
1116
1150
  }).catch((err) => {
1117
1151
  this.apiError = err.message || 'Failed to load rooms';
@@ -1196,10 +1230,12 @@ if (typeof window !== 'undefined') {
1196
1230
  const cacheKey = isSandbox ? key + ':sandbox' : key;
1197
1231
 
1198
1232
  if (__bwConfigCache[cacheKey]) {
1199
- this._applyApiColors(__bwConfigCache[cacheKey]);
1233
+ const cached = __bwConfigCache[cacheKey];
1234
+ this._applyApiColors(cached);
1200
1235
  this._configState = 'loaded';
1201
1236
  this.applyColors();
1202
- return Promise.resolve(__bwConfigCache[cacheKey]);
1237
+ this._initPosthog(cached._posthogKey || '');
1238
+ return Promise.resolve(cached);
1203
1239
  }
1204
1240
 
1205
1241
  this._configState = 'loading';
@@ -1218,10 +1254,12 @@ if (typeof window !== 'undefined') {
1218
1254
  if (data.primaryColor) apiColors.primary = data.primaryColor;
1219
1255
  if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
1220
1256
  if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
1257
+ apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
1221
1258
  __bwConfigCache[cacheKey] = apiColors;
1222
1259
  self._applyApiColors(apiColors);
1223
1260
  self._configState = 'loaded';
1224
1261
  self.applyColors();
1262
+ self._initPosthog(apiColors._posthogKey);
1225
1263
  self.render();
1226
1264
  return apiColors;
1227
1265
  })
@@ -1234,6 +1272,26 @@ if (typeof window !== 'undefined') {
1234
1272
  return this._configPromise;
1235
1273
  }
1236
1274
 
1275
+ _initPosthog(configKey) {
1276
+ const key = this.options.posthogKey || configKey || '';
1277
+ if (!key || typeof window === 'undefined' || typeof document === 'undefined') return;
1278
+ if (this._posthogInited) return;
1279
+ try {
1280
+ // Inject the posthog loader snippet if posthog has not been loaded yet.
1281
+ // This makes the standalone bundle self-sufficient — no CDN <script> needed from the host.
1282
+ if (!window.posthog || !window.posthog.__SV) {
1283
+ !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||[]);
1284
+ }
1285
+ window.posthog.init(key, { api_host: this.options.posthogHost || 'https://us.i.posthog.com' });
1286
+ this._posthogInited = true;
1287
+ this._identify();
1288
+ this._capture('widgetLoaded');
1289
+ if (this.overlay && this.overlay.classList.contains('active')) {
1290
+ this._capture('widgetOpened');
1291
+ }
1292
+ } catch (e) { /* noop */ }
1293
+ }
1294
+
1237
1295
  _retryConfigFetch() {
1238
1296
  this._configState = 'idle';
1239
1297
  this._configError = null;
@@ -1683,7 +1741,9 @@ if (typeof window !== 'undefined') {
1683
1741
  const grid = this.widget.querySelector('.room-grid');
1684
1742
  const scrollLeft = grid ? grid.scrollLeft : 0;
1685
1743
  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;
1744
+ const room = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
1745
+ this.state.selectedRoom = room;
1746
+ this._capture('selectedRoom', { roomId: id, roomName: room && room.name });
1687
1747
  this.render();
1688
1748
  const restoreScroll = () => {
1689
1749
  const newGrid = this.widget.querySelector('.room-grid');
@@ -1743,6 +1803,7 @@ if (typeof window !== 'undefined') {
1743
1803
 
1744
1804
  selectRate(id) {
1745
1805
  this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
1806
+ this._capture('selectedRate', { rateId: id });
1746
1807
  this.render();
1747
1808
  }
1748
1809
 
@@ -1975,6 +2036,16 @@ if (typeof window !== 'undefined') {
1975
2036
  .then((res) => {
1976
2037
  this.confirmationCode = (res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code)) || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
1977
2038
  this.state.step = 'confirmation';
2039
+ this._capture('booked', {
2040
+ confirmationCode: this.confirmationCode,
2041
+ guest: this.state.guest,
2042
+ checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
2043
+ checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
2044
+ roomName: this.state.selectedRoom && this.state.selectedRoom.name,
2045
+ roomId: this.state.selectedRoom && this.state.selectedRoom.id,
2046
+ rateId: this.state.selectedRate && this.state.selectedRate.id,
2047
+ currency: this.state.selectedRoom && this.state.selectedRoom.currency,
2048
+ });
1978
2049
  this.render();
1979
2050
  })
1980
2051
  .catch((err) => {
@@ -2018,6 +2089,18 @@ if (typeof window !== 'undefined') {
2018
2089
  self.confirmationStatus = status || self.confirmationStatus || 'pending';
2019
2090
  if (status === 'confirmed') {
2020
2091
  self.confirmationDetails = data;
2092
+ self._capture('booked', {
2093
+ confirmationCode: data && (data.confirmationCode ?? data.confirmation_code ?? data.bookingId ?? data.booking_id),
2094
+ bookingId: data && (data.bookingId ?? data.booking_id),
2095
+ guest: self.state.guest,
2096
+ checkIn: self.state.checkIn && self.state.checkIn.toISOString ? self.state.checkIn.toISOString() : undefined,
2097
+ checkOut: self.state.checkOut && self.state.checkOut.toISOString ? self.state.checkOut.toISOString() : undefined,
2098
+ roomName: self.state.selectedRoom && self.state.selectedRoom.name,
2099
+ roomId: self.state.selectedRoom && self.state.selectedRoom.id,
2100
+ rateId: self.state.selectedRate && self.state.selectedRate.id,
2101
+ totalAmount: data && (data.totalAmount ?? data.total_amount),
2102
+ currency: (data && data.currency) || (self.state.selectedRoom && self.state.selectedRoom.currency),
2103
+ });
2021
2104
  self.confirmationPolling = false;
2022
2105
  self.render();
2023
2106
  return;
@@ -612,7 +612,7 @@
612
612
  max-width: calc(100vw - 2em);
613
613
  box-sizing: border-box;
614
614
  z-index: 10;
615
- background: var(--card-solid);
615
+ background: var(--bg);
616
616
  border: 1px solid var(--border);
617
617
  border-radius: var(--radius);
618
618
  padding: 1em;
@@ -83,8 +83,9 @@ function deriveWidgetStyles(c) {
83
83
  var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
84
84
  var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
85
85
  if (primaryRgb) styles['--primary-rgb'] = primaryRgb[0] + ', ' + primaryRgb[1] + ', ' + primaryRgb[2];
86
- styles['--card'] = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
87
- styles['--card-solid'] = styles['--card'];
86
+ var _cardBase = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
87
+ styles['--card'] = c.card ? _cardBase + '40' : _cardBase;
88
+ styles['--card-solid'] = _cardBase;
88
89
  if (bgHsl) {
89
90
  styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
90
91
  styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
@@ -160,6 +161,10 @@ class BookingWidget {
160
161
  s3BaseUrl: options.s3BaseUrl || null,
161
162
  /** Base URL for confirmation status polling (same as API_BASE_URL). Default https://ai.thehotelplanet.com */
162
163
  confirmationBaseUrl: options.confirmationBaseUrl || 'https://ai.thehotelplanet.com',
164
+ /** PostHog: project API key (host page must load posthog-js and set window.posthog). */
165
+ posthogKey: options.posthogKey || null,
166
+ /** PostHog: API host (e.g. https://us.i.posthog.com). */
167
+ posthogHost: options.posthogHost || 'https://us.i.posthog.com',
163
168
  // Color customization: CONFIG defaults from load-config, installer colors override
164
169
  colors: (function () {
165
170
  const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
@@ -242,6 +247,25 @@ class BookingWidget {
242
247
  this.widget = null;
243
248
  }
244
249
 
250
+ _capture(eventName, properties) {
251
+ if (typeof window !== 'undefined' && window.posthog && typeof window.posthog.capture === 'function') {
252
+ try {
253
+ const mode = this.options.mode === 'sandbox' ? 'sandbox' : 'live';
254
+ const propertyKey = this.options.propertyKey != null ? String(this.options.propertyKey) : undefined;
255
+ const propertyId = this.options.propertyId != null && this.options.propertyId !== '' ? String(this.options.propertyId) : undefined;
256
+ window.posthog.capture(eventName, { ...(properties || {}), mode, propertyKey, propertyId });
257
+ } catch (e) { /* noop */ }
258
+ }
259
+ }
260
+
261
+ _identify() {
262
+ if (typeof window !== 'undefined' && this.options.propertyKey && window.posthog && typeof window.posthog.identify === 'function') {
263
+ try {
264
+ window.posthog.identify(String(this.options.propertyKey).trim());
265
+ } catch (e) { /* noop */ }
266
+ }
267
+ }
268
+
245
269
  // ===== Helpers =====
246
270
  getNights() {
247
271
  if (!this.state.checkIn || !this.state.checkOut) return 0;
@@ -305,6 +329,7 @@ class BookingWidget {
305
329
  this._fetchRuntimeConfig();
306
330
  }
307
331
  this.render();
332
+ this._capture('widgetOpened');
308
333
  if (this.options.onOpen) this.options.onOpen();
309
334
  }
310
335
 
@@ -374,6 +399,12 @@ class BookingWidget {
374
399
  this.bookingApi.fetchRooms(params).then((rooms) => {
375
400
  this.ROOMS = Array.isArray(rooms) ? rooms : [];
376
401
  this.loadingRooms = false;
402
+ this._capture('search', {
403
+ checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
404
+ checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
405
+ rooms: this.state.rooms,
406
+ occupancies: this.state.occupancies,
407
+ });
377
408
  this.render();
378
409
  }).catch((err) => {
379
410
  this.apiError = err.message || 'Failed to load rooms';
@@ -473,10 +504,12 @@ class BookingWidget {
473
504
  const cacheKey = isSandbox ? key + ':sandbox' : key;
474
505
 
475
506
  if (__bwConfigCache[cacheKey]) {
476
- this._applyApiColors(__bwConfigCache[cacheKey]);
507
+ const cached = __bwConfigCache[cacheKey];
508
+ this._applyApiColors(cached);
477
509
  this._configState = 'loaded';
478
510
  this.applyColors();
479
- return Promise.resolve(__bwConfigCache[cacheKey]);
511
+ this._initPosthog(cached._posthogKey || '');
512
+ return Promise.resolve(cached);
480
513
  }
481
514
 
482
515
  this._configState = 'loading';
@@ -495,10 +528,12 @@ class BookingWidget {
495
528
  if (data.primaryColor) apiColors.primary = data.primaryColor;
496
529
  if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
497
530
  if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
531
+ apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
498
532
  __bwConfigCache[cacheKey] = apiColors;
499
533
  self._applyApiColors(apiColors);
500
534
  self._configState = 'loaded';
501
535
  self.applyColors();
536
+ self._initPosthog(apiColors._posthogKey);
502
537
  self.render();
503
538
  return apiColors;
504
539
  })
@@ -511,6 +546,28 @@ class BookingWidget {
511
546
  return this._configPromise;
512
547
  }
513
548
 
549
+ _initPosthog(configKey) {
550
+ const key = this.options.posthogKey || configKey || '';
551
+ if (!key || typeof window === 'undefined' || typeof document === 'undefined') return;
552
+ if (this._posthogInited) return;
553
+ try {
554
+ // Inject the posthog loader snippet if posthog has not been loaded yet.
555
+ // This makes the standalone version self-sufficient — no CDN <script> needed from the host.
556
+ if (!window.posthog || !window.posthog.__SV) {
557
+ /* eslint-disable */
558
+ !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||[]);
559
+ /* eslint-enable */
560
+ }
561
+ window.posthog.init(key, { api_host: this.options.posthogHost || 'https://us.i.posthog.com' });
562
+ this._posthogInited = true;
563
+ this._identify();
564
+ this._capture('widgetLoaded');
565
+ if (this.overlay && this.overlay.classList.contains('active')) {
566
+ this._capture('widgetOpened');
567
+ }
568
+ } catch (e) { /* noop */ }
569
+ }
570
+
514
571
  injectCSS() {
515
572
  if (!document.getElementById('booking-widget-styles')) {
516
573
  const link = document.createElement('link');
@@ -955,7 +1012,9 @@ class BookingWidget {
955
1012
  const grid = this.widget.querySelector('.room-grid');
956
1013
  const scrollLeft = grid ? grid.scrollLeft : 0;
957
1014
  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;
1015
+ const room = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
1016
+ this.state.selectedRoom = room;
1017
+ this._capture('selectedRoom', { roomId: id, roomName: room && room.name });
959
1018
  this.render();
960
1019
  const restoreScroll = () => {
961
1020
  const newGrid = this.widget.querySelector('.room-grid');
@@ -1049,6 +1108,7 @@ class BookingWidget {
1049
1108
 
1050
1109
  selectRate(id) {
1051
1110
  this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
1111
+ this._capture('selectedRate', { rateId: id });
1052
1112
  this.render();
1053
1113
  }
1054
1114
 
@@ -1306,6 +1366,16 @@ class BookingWidget {
1306
1366
  const code = res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code);
1307
1367
  this.confirmationCode = code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
1308
1368
  this.state.step = 'confirmation';
1369
+ this._capture('booked', {
1370
+ confirmationCode: this.confirmationCode,
1371
+ guest: this.state.guest,
1372
+ checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
1373
+ checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
1374
+ roomName: this.state.selectedRoom && this.state.selectedRoom.name,
1375
+ roomId: this.state.selectedRoom && this.state.selectedRoom.id,
1376
+ rateId: this.state.selectedRate && this.state.selectedRate.id,
1377
+ currency: this.state.selectedRoom && this.state.selectedRoom.currency,
1378
+ });
1309
1379
  this.render();
1310
1380
  })
1311
1381
  .catch((err) => {
@@ -1431,6 +1501,18 @@ class BookingWidget {
1431
1501
  this.confirmationStatus = status || this.confirmationStatus || 'pending';
1432
1502
  if (status === 'confirmed') {
1433
1503
  this.confirmationDetails = data;
1504
+ this._capture('booked', {
1505
+ confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
1506
+ bookingId: data?.bookingId ?? data?.booking_id,
1507
+ guest: this.state.guest,
1508
+ checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
1509
+ checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
1510
+ roomName: this.state.selectedRoom && this.state.selectedRoom.name,
1511
+ roomId: this.state.selectedRoom && this.state.selectedRoom.id,
1512
+ rateId: this.state.selectedRate && this.state.selectedRate.id,
1513
+ totalAmount: data?.totalAmount ?? data?.total_amount,
1514
+ currency: data?.currency || (this.state.selectedRoom && this.state.selectedRoom.currency),
1515
+ });
1434
1516
  this.confirmationPolling = false;
1435
1517
  this.render();
1436
1518
  return;
@@ -19,6 +19,11 @@ const DEFAULT_ROOMS = [];
19
19
 
20
20
  const DEFAULT_RATES = [];
21
21
 
22
+ // Capture the native browser fetch at module-load time — before PostHog (or any other
23
+ // library) has a chance to wrap window.fetch for session recording. PostHog's wrapped
24
+ // fetch drops non-standard request headers such as 'Source', breaking API auth.
25
+ const _fetch = typeof fetch === 'function' ? fetch : /* istanbul ignore next */ undefined;
26
+
22
27
  /**
23
28
  * Returns date as YYYY-MM-DD using local date (no timezone shift).
24
29
  * Calendar dates are created at local midnight; using toISOString() would convert to UTC and can shift the day.
@@ -236,7 +241,7 @@ function createBookingApi(config = {}) {
236
241
  if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
237
242
  const options = { method, headers };
238
243
  if (body && (method === 'POST' || method === 'PUT')) options.body = JSON.stringify(body);
239
- const res = await fetch(url, options);
244
+ const res = await _fetch(url, options);
240
245
  if (!res.ok) {
241
246
  let body;
242
247
  try { body = await res.json(); } catch (_) {}
@@ -293,7 +298,7 @@ function createBookingApi(config = {}) {
293
298
  try {
294
299
  const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
295
300
  if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
296
- const propRes = await fetch(propFullUrl, getOpts);
301
+ const propRes = await _fetch(propFullUrl, getOpts);
297
302
  if (propRes.ok) {
298
303
  const propData = await propRes.json();
299
304
  propertyRooms = propData?.rooms ?? {};
@@ -690,7 +695,7 @@ async function decryptPropertyId(options = {}) {
690
695
  if (!propertyKey) return null;
691
696
  const base = (baseUrl || '').replace(/\/$/, '');
692
697
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
693
- const res = await fetch(url, {
698
+ const res = await _fetch(url, {
694
699
  method: 'POST',
695
700
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
696
701
  body: JSON.stringify({ hash: propertyKey }),
@@ -717,7 +722,7 @@ async function fetchBookingEnginePref(options = {}) {
717
722
  }
718
723
  const base = (baseUrl || '').replace(/\/$/, '');
719
724
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
720
- const res = await fetch(url, {
725
+ const res = await _fetch(url, {
721
726
  method: 'GET',
722
727
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
723
728
  });
@@ -73,7 +73,7 @@ export function deriveWidgetStyles(c) {
73
73
  if (primaryRgb) styles['--primary-rgb'] = `${primaryRgb[0]}, ${primaryRgb[1]}, ${primaryRgb[2]}`;
74
74
 
75
75
  if (cardExplicit) {
76
- styles['--card'] = cardExplicit + "40";
76
+ styles['--card'] = cardExplicit + '40';
77
77
  styles['--card-solid'] = cardExplicit;
78
78
  } else if (bgHsl) {
79
79
  const cardVal = `hsl(${bgHsl[0]}, ${bgHsl[1]}%, ${Math.min(95, bgHsl[2] + 2)}%)`;
@@ -612,7 +612,7 @@
612
612
  max-width: calc(100vw - 2em);
613
613
  box-sizing: border-box;
614
614
  z-index: 10;
615
- background: var(--card-solid);
615
+ background: var(--bg);
616
616
  border: 1px solid var(--border);
617
617
  border-radius: var(--radius);
618
618
  padding: 1em;
@@ -5,6 +5,12 @@ 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';
9
+
10
+ // Capture the native browser fetch at module-load time — before PostHog (or any other
11
+ // library) wraps window.fetch for session recording. The wrapped version drops
12
+ // non-standard request headers such as 'Source'.
13
+ const _nativeFetch = typeof fetch === 'function' ? fetch : /* istanbul ignore next */ undefined;
8
14
 
9
15
  const BASE_STEPS = [
10
16
  { key: 'dates', label: 'Dates & Guests', num: '01' },
@@ -45,6 +51,10 @@ const BookingWidget = ({
45
51
  createPaymentIntent: createPaymentIntentProp = null,
46
52
  /** Stripe: deprecated (booking completion is fetched from confirmation endpoint instead). */
47
53
  onBookingComplete: onBookingCompleteProp = null,
54
+ /** PostHog: optional override for analytics key (defaults to VITE_POSTHOG_KEY). */
55
+ posthogKey = '',
56
+ /** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
57
+ posthogHost = '',
48
58
  }) => {
49
59
  const stripePublishableKey = stripePublishableKeyProp || STRIPE_PUBLISHABLE_KEY;
50
60
  const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
@@ -59,7 +69,7 @@ const BookingWidget = ({
59
69
  const url = effectivePaymentIntentUrl + (isSandbox ? '?sandbox=true' : '');
60
70
  const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
61
71
  if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
62
- const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
72
+ const res = await _nativeFetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
63
73
  if (!res.ok) throw new Error(await res.text());
64
74
  const data = await res.json();
65
75
  const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
@@ -134,9 +144,26 @@ const BookingWidget = ({
134
144
  return null;
135
145
  }, [bookingApiProp, effectiveApiBaseUrl, effectiveAvailabilityBaseUrl, effectivePropertyBaseUrl, effectiveS3BaseUrl, apiSecret, propertyId, propertyKey, mode]);
136
146
 
147
+
148
+ const analyticsMode = isSandbox ? 'sandbox' : 'live';
149
+ const analyticsContext = {
150
+ mode: analyticsMode,
151
+ propertyKey: propertyKey || undefined,
152
+ propertyId: propertyId != null && propertyId !== '' ? String(propertyId) : undefined,
153
+ };
154
+
155
+ // Track widgetLoaded when config has loaded (widget is ready). Identify by propertyKey.
156
+ useEffect(() => {
157
+ if (hasPropertyKey && configLoaded) {
158
+ identifyAnalytics(propertyKey);
159
+ captureEvent('widgetLoaded', analyticsContext);
160
+ }
161
+ }, [hasPropertyKey, configLoaded, propertyKey, propertyId, analyticsMode]);
162
+
137
163
  useEffect(() => {
138
164
  if (isOpen && onOpen) onOpen();
139
- }, [isOpen, onOpen]);
165
+ if (isOpen) captureEvent('widgetOpened', analyticsContext);
166
+ }, [isOpen, onOpen, analyticsMode]);
140
167
 
141
168
  // Delay adding 'active' by one frame so the browser can paint the initial state and animate open
142
169
  useEffect(() => {
@@ -205,13 +232,28 @@ const BookingWidget = ({
205
232
  if (cancelled) return;
206
233
  try {
207
234
  const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}${isSandbox ? '?sandbox=true' : ''}`;
208
- const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
235
+ const res = await _nativeFetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
209
236
  if (!res.ok) throw new Error(await res.text());
210
237
  const data = await res.json();
211
238
  const status = data?.status != null ? String(data.status) : '';
212
239
  if (!cancelled) setConfirmationStatus(status || 'pending');
213
240
  if (status === 'confirmed') {
214
- if (!cancelled) setConfirmationDetails(data);
241
+ if (!cancelled) {
242
+ setConfirmationDetails(data);
243
+ captureEvent('booked', {
244
+ ...analyticsContext,
245
+ confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
246
+ bookingId: data?.bookingId ?? data?.booking_id,
247
+ guest: state.guest,
248
+ checkIn: state.checkIn?.toISOString?.(),
249
+ checkOut: state.checkOut?.toISOString?.(),
250
+ roomName: state.selectedRoom?.name,
251
+ roomId: state.selectedRoom?.id,
252
+ rateId: state.selectedRate?.id,
253
+ totalAmount: data?.totalAmount ?? data?.total_amount,
254
+ currency: data?.currency ?? state.selectedRoom?.currency,
255
+ });
256
+ }
215
257
  return;
216
258
  }
217
259
  } catch (err) {
@@ -241,8 +283,10 @@ const BookingWidget = ({
241
283
  setConfigError(null);
242
284
  setConfigLoaded(false);
243
285
  fetchRuntimeConfig(propertyKey, colors, mode)
244
- .then(({ widgetStyles }) => {
286
+ .then(({ widgetStyles, posthogKey: configPosthogKey }) => {
245
287
  if (cancelled) return;
288
+ const analyticsKey = posthogKey || configPosthogKey || '';
289
+ initAnalytics(analyticsKey || posthogHost ? { key: analyticsKey || undefined, host: posthogHost || undefined } : {});
246
290
  setRuntimeWidgetStyles(widgetStyles);
247
291
  setConfigLoaded(true);
248
292
  setConfigLoading(false);
@@ -319,6 +363,13 @@ const BookingWidget = ({
319
363
  api.fetchRooms(params).then((rooms) => {
320
364
  setRoomsList(Array.isArray(rooms) ? rooms : []);
321
365
  setLoadingRooms(false);
366
+ captureEvent('search', {
367
+ ...analyticsContext,
368
+ checkIn: state.checkIn?.toISOString?.(),
369
+ checkOut: state.checkOut?.toISOString?.(),
370
+ rooms: state.rooms,
371
+ occupancies: state.occupancies,
372
+ });
322
373
  }).catch((err) => {
323
374
  setApiError(err.message || 'Failed to load rooms');
324
375
  setLoadingRooms(false);
@@ -443,11 +494,14 @@ const BookingWidget = ({
443
494
  };
444
495
 
445
496
  const selectRoom = (id) => {
446
- setState(prev => ({ ...prev, selectedRoom: roomsList.find(r => r.id === id) }));
497
+ const room = roomsList.find(r => r.id === id);
498
+ setState(prev => ({ ...prev, selectedRoom: room }));
499
+ captureEvent('selectedRoom', { ...analyticsContext, roomId: id, roomName: room?.name });
447
500
  };
448
501
 
449
502
  const selectRate = (id) => {
450
503
  setState(prev => ({ ...prev, selectedRate: ratesList.find(r => r.id === id) }));
504
+ captureEvent('selectedRate', { ...analyticsContext, rateId: id });
451
505
  };
452
506
 
453
507
  const updateGuest = (field, value) => {
@@ -502,6 +556,17 @@ const BookingWidget = ({
502
556
  const code = res && (res.confirmationCode ?? res.confirmation_code);
503
557
  setConfirmationCode(code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6)));
504
558
  setState(prev => ({ ...prev, step: 'confirmation' }));
559
+ captureEvent('booked', {
560
+ ...analyticsContext,
561
+ confirmationCode: code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6)),
562
+ guest: state.guest,
563
+ checkIn: state.checkIn?.toISOString?.(),
564
+ checkOut: state.checkOut?.toISOString?.(),
565
+ roomName: state.selectedRoom?.name,
566
+ roomId: state.selectedRoom?.id,
567
+ rateId: state.selectedRate?.id,
568
+ currency: state.selectedRoom?.currency,
569
+ });
505
570
  })
506
571
  .catch((err) => {
507
572
  setApiError(err?.message || err || 'Booking failed');
@@ -612,7 +612,7 @@
612
612
  max-width: calc(100vw - 2em);
613
613
  box-sizing: border-box;
614
614
  z-index: 10;
615
- background: var(--card-solid);
615
+ background: var(--bg);
616
616
  border: 1px solid var(--border);
617
617
  border-radius: var(--radius);
618
618
  padding: 1em;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * PostHog analytics for the booking widget.
3
+ * The key is sourced from /load-config at runtime; VITE_POSTHOG_KEY/.env or prop overrides are
4
+ * also supported but the config-fetched key is preferred so no hardcoded key is needed.
5
+ * Used by React and Vue; core/standalone use window.posthog when host provides it.
6
+ */
7
+ import posthog from 'posthog-js';
8
+
9
+ let initialized = false;
10
+
11
+ function getConfig(overrides = {}) {
12
+ if (overrides.key != null || overrides.host != null) {
13
+ return {
14
+ key: (overrides.key && String(overrides.key).trim()) || '',
15
+ host: overrides.host || 'https://us.i.posthog.com',
16
+ };
17
+ }
18
+ if (typeof import.meta !== 'undefined' && import.meta.env) {
19
+ const key = import.meta.env.VITE_POSTHOG_KEY || import.meta.env.POSTHOG_KEY || '';
20
+ const host = import.meta.env.VITE_POSTHOG_HOST || import.meta.env.POSTHOG_HOST || 'https://us.i.posthog.com';
21
+ return { key: (key && String(key).trim()) || '', host };
22
+ }
23
+ if (typeof window !== 'undefined') {
24
+ const key = (window.__BOOKING_WIDGET_POSTHOG_KEY__ || '').trim();
25
+ const host = window.__BOOKING_WIDGET_POSTHOG_HOST__ || 'https://us.i.posthog.com';
26
+ return { key, host };
27
+ }
28
+ return { key: '', host: 'https://us.i.posthog.com' };
29
+ }
30
+
31
+ /**
32
+ * Initialize PostHog when key is available. Call once at app/widget load.
33
+ * @param {object} [overrides] - Optional { key, host } to override env.
34
+ */
35
+ export function init(overrides = {}) {
36
+ if (initialized) return posthog;
37
+ const { key, host } = getConfig(overrides);
38
+ if (!key) return null;
39
+ try {
40
+ // Snapshot the native fetch/XHR before posthog.init() wraps them for session recording.
41
+ // PostHog's wrapped fetch drops non-standard headers (e.g. 'Source'), so we restore the
42
+ // originals immediately after init so all subsequent widget API calls are unaffected.
43
+ const _savedFetch = typeof window !== 'undefined' ? window.fetch : undefined;
44
+ const _savedXHROpen = typeof window !== 'undefined' && window.XMLHttpRequest
45
+ ? window.XMLHttpRequest.prototype.open
46
+ : undefined;
47
+
48
+ posthog.init(key, { api_host: host });
49
+
50
+ if (_savedFetch && typeof window !== 'undefined') window.fetch = _savedFetch;
51
+ if (_savedXHROpen && typeof window !== 'undefined') window.XMLHttpRequest.prototype.open = _savedXHROpen;
52
+
53
+ initialized = true;
54
+ return posthog;
55
+ } catch (e) {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Identify the current context (e.g. property/tenant) so events are associated with it.
62
+ * Call with propertyKey when the widget has a known property.
63
+ * @param {string} id - Distinct ID (e.g. propertyKey).
64
+ */
65
+ export function identify(id) {
66
+ try {
67
+ if (initialized && id != null && String(id).trim() && typeof posthog.identify === 'function') {
68
+ posthog.identify(String(id).trim());
69
+ }
70
+ } catch (e) {
71
+ // ignore
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Capture an event. No-op if PostHog is not initialized.
77
+ * @param {string} eventName - Event name (e.g. 'widgetLoaded', 'search').
78
+ * @param {object} [properties] - Optional event properties.
79
+ */
80
+ export function capture(eventName, properties = {}) {
81
+ try {
82
+ if (initialized && typeof posthog.capture === 'function') {
83
+ posthog.capture(eventName, properties);
84
+ }
85
+ } catch (e) {
86
+ // ignore
87
+ }
88
+ }
@@ -10,9 +10,13 @@
10
10
  import { DEFAULT_COLORS } from '../core/stripe-config.js';
11
11
  import { deriveWidgetStyles } from '../core/color-utils.js';
12
12
 
13
+ // Capture native fetch before PostHog can wrap it (PostHog session recording
14
+ // wraps window.fetch and may strip non-standard headers such as 'Source').
15
+ const _nativeFetch = typeof fetch === 'function' ? fetch : /* istanbul ignore next */ undefined;
16
+
13
17
  const CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/load-config';
14
18
 
15
- /** In-memory cache: propertyKey → raw API color object */
19
+ /** In-memory cache: propertyKey → { apiColors, posthogKey } */
16
20
  const _configCache = new Map();
17
21
 
18
22
  /**
@@ -83,21 +87,22 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
83
87
  const cacheKey = isSandbox ? `${key}:sandbox` : key;
84
88
 
85
89
  if (_configCache.has(cacheKey)) {
86
- const apiColors = _configCache.get(cacheKey);
90
+ const { apiColors, posthogKey } = _configCache.get(cacheKey);
87
91
  const colors = mergeColors(apiColors, installerColors);
88
- return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
92
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
89
93
  }
90
94
 
91
95
  const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
92
- const res = await fetch(url, { headers: { 'Source': 'booking_engine' } });
96
+ const res = await _nativeFetch(url, { headers: { 'Source': 'booking_engine' } });
93
97
  if (!res.ok) {
94
98
  throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
95
99
  }
96
100
 
97
101
  const data = await res.json();
98
102
  const apiColors = mapApiColors(data);
99
- _configCache.set(cacheKey, apiColors);
103
+ const posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
104
+ _configCache.set(cacheKey, { apiColors, posthogKey });
100
105
 
101
106
  const colors = mergeColors(apiColors, installerColors);
102
- return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
107
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
103
108
  }
@@ -482,6 +482,12 @@ 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';
486
+
487
+ // Capture the native browser fetch at module-load time — before PostHog (or any other
488
+ // library) wraps window.fetch for session recording. The wrapped version drops
489
+ // non-standard request headers such as 'Source'.
490
+ const _nativeFetch = typeof fetch === 'function' ? fetch : /* istanbul ignore next */ undefined;
485
491
 
486
492
 
487
493
  const BASE_STEPS = [
@@ -525,6 +531,10 @@ export default {
525
531
  createPaymentIntent: { type: Function, default: null },
526
532
  /** Stripe: deprecated (booking completion is fetched from confirmation endpoint instead). */
527
533
  onBookingComplete: { type: Function, default: null },
534
+ /** PostHog: optional override for analytics key (defaults to VITE_POSTHOG_KEY). */
535
+ posthogKey: { type: String, default: '' },
536
+ /** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
537
+ posthogHost: { type: String, default: '' },
528
538
  },
529
539
  emits: ['close', 'complete', 'open'],
530
540
  data() {
@@ -606,7 +616,7 @@ export default {
606
616
  const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
607
617
  const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
608
618
  if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
609
- const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
619
+ const res = await _nativeFetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
610
620
  if (!res.ok) throw new Error(await res.text());
611
621
  const data = await res.json();
612
622
  const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
@@ -722,6 +732,16 @@ export default {
722
732
  isVisible() {
723
733
  return this.isOpen && !this.isClosing && this.isReadyForOpen;
724
734
  },
735
+ analyticsMode() {
736
+ return this.isSandbox ? 'sandbox' : 'live';
737
+ },
738
+ analyticsContext() {
739
+ return {
740
+ mode: this.analyticsMode,
741
+ propertyKey: this.propertyKey || undefined,
742
+ propertyId: this.propertyId != null && this.propertyId !== '' ? String(this.propertyId) : undefined,
743
+ };
744
+ },
725
745
  },
726
746
  created() {
727
747
  this._initRuntimeConfig();
@@ -740,6 +760,7 @@ export default {
740
760
  isOpen: {
741
761
  handler(open) {
742
762
  if (open && this.onOpen) this.onOpen();
763
+ if (open) captureEvent('widgetOpened', this.analyticsContext);
743
764
  if (!open || this.isClosing) {
744
765
  this.isReadyForOpen = false;
745
766
  return;
@@ -753,6 +774,14 @@ export default {
753
774
  },
754
775
  immediate: true,
755
776
  },
777
+ configLoaded: {
778
+ handler(loaded) {
779
+ if (loaded && this.hasPropertyKey) {
780
+ identifyAnalytics(this.propertyKey);
781
+ captureEvent('widgetLoaded', this.analyticsContext);
782
+ }
783
+ },
784
+ },
756
785
  'state.step': function(step) {
757
786
  if (step !== 'summary' && step !== 'payment') {
758
787
  this.checkoutShowPaymentForm = false;
@@ -782,7 +811,9 @@ export default {
782
811
  this.configError = null;
783
812
  this.configLoaded = false;
784
813
  try {
785
- const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
814
+ const { widgetStyles, posthogKey: configPosthogKey } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
815
+ const analyticsKey = this.posthogKey || configPosthogKey || '';
816
+ initAnalytics(analyticsKey || this.posthogHost ? { key: analyticsKey || undefined, host: this.posthogHost || undefined } : {});
786
817
  this.runtimeWidgetStyles = widgetStyles;
787
818
  this.configLoaded = true;
788
819
  } catch (err) {
@@ -795,7 +826,7 @@ export default {
795
826
  const t = String(token || '').trim();
796
827
  if (!t) throw new Error('Missing confirmation token');
797
828
  const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
798
- const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
829
+ const res = await _nativeFetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
799
830
  if (!res.ok) throw new Error(await res.text());
800
831
  return await res.json();
801
832
  },
@@ -812,6 +843,19 @@ export default {
812
843
  this.confirmationStatus = status || this.confirmationStatus || 'pending';
813
844
  if (status === 'confirmed') {
814
845
  this.confirmationDetails = data;
846
+ captureEvent('booked', {
847
+ ...this.analyticsContext,
848
+ confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
849
+ bookingId: data?.bookingId ?? data?.booking_id,
850
+ guest: this.state.guest,
851
+ checkIn: this.state.checkIn?.toISOString?.(),
852
+ checkOut: this.state.checkOut?.toISOString?.(),
853
+ roomName: this.state.selectedRoom?.name,
854
+ roomId: this.state.selectedRoom?.id,
855
+ rateId: this.state.selectedRate?.id,
856
+ totalAmount: data?.totalAmount ?? data?.total_amount,
857
+ currency: data?.currency ?? this.state.selectedRoom?.currency,
858
+ });
815
859
  return;
816
860
  }
817
861
  } catch (err) {
@@ -880,6 +924,13 @@ export default {
880
924
  api.fetchRooms(params).then((rooms) => {
881
925
  this.roomsList = Array.isArray(rooms) ? rooms : [];
882
926
  this.loadingRooms = false;
927
+ captureEvent('search', {
928
+ ...this.analyticsContext,
929
+ checkIn: this.state.checkIn?.toISOString?.(),
930
+ checkOut: this.state.checkOut?.toISOString?.(),
931
+ rooms: this.state.rooms,
932
+ occupancies: this.state.occupancies,
933
+ });
883
934
  }).catch((err) => {
884
935
  this.apiError = err.message || 'Failed to load rooms';
885
936
  this.loadingRooms = false;
@@ -972,6 +1023,17 @@ export default {
972
1023
  const code = res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code);
973
1024
  this.confirmationCode = code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
974
1025
  this.state.step = 'confirmation';
1026
+ captureEvent('booked', {
1027
+ ...this.analyticsContext,
1028
+ confirmationCode: this.confirmationCode,
1029
+ guest: this.state.guest,
1030
+ checkIn: this.state.checkIn?.toISOString?.(),
1031
+ checkOut: this.state.checkOut?.toISOString?.(),
1032
+ roomName: this.state.selectedRoom?.name,
1033
+ roomId: this.state.selectedRoom?.id,
1034
+ rateId: this.state.selectedRate?.id,
1035
+ currency: this.state.selectedRoom?.currency,
1036
+ });
975
1037
  })
976
1038
  .catch((err) => {
977
1039
  this.apiError = (err && err.message) || err || 'Booking failed';
@@ -1124,10 +1186,13 @@ export default {
1124
1186
  }
1125
1187
  },
1126
1188
  selectRoom(id) {
1127
- this.updateState({ selectedRoom: this.roomsList.find(r => r.id === id) });
1189
+ const room = this.roomsList.find(r => r.id === id);
1190
+ this.updateState({ selectedRoom: room });
1191
+ captureEvent('selectedRoom', { ...this.analyticsContext, roomId: id, roomName: room?.name });
1128
1192
  },
1129
1193
  selectRate(id) {
1130
1194
  this.updateState({ selectedRate: this.ratesList.find(r => r.id === id) });
1195
+ captureEvent('selectedRate', { ...this.analyticsContext, rateId: id });
1131
1196
  },
1132
1197
  updateGuest(field, value) {
1133
1198
  this.updateState({ guest: { ...this.state.guest, [field]: value } });
@@ -612,7 +612,7 @@
612
612
  max-width: calc(100vw - 2em);
613
613
  box-sizing: border-box;
614
614
  z-index: 10;
615
- background: var(--card-solid);
615
+ background: var(--bg);
616
616
  border: 1px solid var(--border);
617
617
  border-radius: var(--radius);
618
618
  padding: 1em;
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.8",
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",
@@ -34,7 +34,7 @@
34
34
  "build": "node scripts/generate-stripe-config.js && npm run build:css && npm run build:js && npm run build:react && npm run build:vue && npm run build:types",
35
35
  "build:stripe-config": "node scripts/generate-stripe-config.js",
36
36
  "build:css": "mkdir -p dist dist/core && cp src/core/styles.css dist/booking-widget.css && cp src/core/styles.css dist/core/styles.css",
37
- "build:js": "mkdir -p dist dist/core dist/utils && cp src/core/widget.js dist/booking-widget.js && cp src/core/booking-api.js dist/core/booking-api.js && cp src/core/stripe-config.js dist/core/stripe-config.js && cp src/core/color-utils.js dist/core/color-utils.js && cp src/utils/config-service.js dist/utils/config-service.js && node scripts/inject-widget-bootstrap.js && cp src/standalone/bundle.js dist/booking-widget-standalone.js && node scripts/build-standalone.js && npm run build:entry",
37
+ "build:js": "mkdir -p dist dist/core dist/utils && cp src/core/widget.js dist/booking-widget.js && cp src/core/booking-api.js dist/core/booking-api.js && cp src/core/stripe-config.js dist/core/stripe-config.js && cp src/core/color-utils.js dist/core/color-utils.js && cp src/utils/config-service.js dist/utils/config-service.js && cp src/utils/analytics.js dist/utils/analytics.js && node scripts/inject-widget-bootstrap.js && cp src/standalone/bundle.js dist/booking-widget-standalone.js && node scripts/build-standalone.js && npm run build:entry",
38
38
  "build:react": "mkdir -p dist/react && cp src/react/BookingWidget.jsx dist/react/BookingWidget.jsx && cp src/core/styles.css dist/react/styles.css",
39
39
  "build:vue": "mkdir -p dist/vue && cp src/vue/BookingWidget.vue dist/vue/BookingWidget.vue && cp src/core/styles.css dist/vue/styles.css",
40
40
  "build:types": "npm run build:types:main && npm run build:types:react && npm run build:types:vue",
@@ -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
  }