@nuitee/booking-widget 1.0.7 → 1.0.9

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
@@ -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.7/dist/booking-widget.css">
98
- <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.7/dist/booking-widget-standalone.js"></script>
97
+ <link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.9/dist/booking-widget.css">
98
+ <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.9/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.7/dist/booking-widget.css',
105
+ cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.9/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 (_) {}
@@ -282,6 +287,20 @@ function createBookingApi(config = {}) {
282
287
 
283
288
  // Fetch get_properties first to get currency_code (e.g. 'EUR') and room details
284
289
  let propertyRooms = {};
290
+ const normalizeRoomsById = (rooms) => {
291
+ if (!rooms) return {};
292
+ if (Array.isArray(rooms)) {
293
+ return rooms.reduce((acc, room) => {
294
+ if (!room || typeof room !== 'object') return acc;
295
+ const key = room.id ?? room.room_id;
296
+ if (key == null) return acc;
297
+ acc[String(key)] = room;
298
+ return acc;
299
+ }, {});
300
+ }
301
+ if (typeof rooms === 'object') return rooms;
302
+ return {};
303
+ };
285
304
  let propertyCurrency = currency;
286
305
  const propQuery = propertyKey
287
306
  ? `key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
@@ -294,10 +313,10 @@ function createBookingApi(config = {}) {
294
313
  try {
295
314
  const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
296
315
  if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
297
- const propRes = await fetch(propFullUrl, getOpts);
316
+ const propRes = await _fetch(propFullUrl, getOpts);
298
317
  if (propRes.ok) {
299
318
  const propData = await propRes.json();
300
- propertyRooms = propData?.rooms ?? {};
319
+ propertyRooms = normalizeRoomsById(propData?.rooms);
301
320
  propertyCurrency = (typeof propData?.currency_code === 'string' && propData.currency_code.trim())
302
321
  ? propData.currency_code.trim()
303
322
  : currency;
@@ -356,18 +375,41 @@ function createBookingApi(config = {}) {
356
375
  return (a?.price ?? a?.total ?? r?.price ?? 0) || 0;
357
376
  };
358
377
  const fallbackImage = 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=800&q=80';
378
+ const toImageUrl = (value) => {
379
+ if (typeof value !== 'string') return '';
380
+ const v = value.trim();
381
+ if (!v) return '';
382
+ if (/^https?:\/\//i.test(v)) return v;
383
+ if (v.startsWith('//')) return `https:${v}`;
384
+ if (s3BaseUrl) return `${s3BaseUrl}/${v.replace(/^\//, '')}`;
385
+ return '';
386
+ };
359
387
 
360
388
  return availableRoomIds.map((roomId) => {
361
- const roomData = propertyRooms[roomId] ?? propertyRooms[String(roomId)] ?? {};
389
+ const roomData = propertyRooms[String(roomId)] ?? propertyRooms[roomId] ?? {};
362
390
  const roomRates = filteredRates.filter((r) => String(r.room_id) === String(roomId));
363
391
  const minPrice = roomRates.length
364
392
  ? Math.min(...roomRates.map(getPrice).filter((p) => p > 0)) || roomData.base_price || 0
365
393
  : roomData.base_price || 0;
366
394
 
367
- const photos = roomData.photos ?? [];
368
- const mainPhoto = photos.find((p) => p.main) ?? photos[0];
369
- const photoPath = mainPhoto?.path ?? '';
370
- const image = s3BaseUrl && photoPath ? `${s3BaseUrl}/${photoPath.replace(/^\//, '')}` : fallbackImage;
395
+ const photos = Array.isArray(roomData.photos) ? roomData.photos : [];
396
+ const mainPhoto = photos.find((p) => (typeof p === 'object' && p?.main)) ?? photos[0] ?? {};
397
+ const photoValue = typeof mainPhoto === 'string' ? mainPhoto : '';
398
+ const imageCandidates = [
399
+ photoValue,
400
+ mainPhoto.path,
401
+ mainPhoto.url,
402
+ mainPhoto.src,
403
+ mainPhoto.image,
404
+ mainPhoto.image_url,
405
+ mainPhoto.secure_url,
406
+ roomData.image,
407
+ roomData.image_url,
408
+ roomData.imageUrl,
409
+ roomData.thumbnail,
410
+ roomData.thumbnail_url,
411
+ ];
412
+ const image = imageCandidates.map(toImageUrl).find(Boolean) || fallbackImage;
371
413
 
372
414
  const sizeVal = roomData.size?.value ?? roomData.size_value;
373
415
  let sizeUnit = roomData.size?.unit ?? roomData.size_unit ?? 'm²';
@@ -691,7 +733,7 @@ async function decryptPropertyId(options = {}) {
691
733
  if (!propertyKey) return null;
692
734
  const base = (baseUrl || '').replace(/\/$/, '');
693
735
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
694
- const res = await fetch(url, {
736
+ const res = await _fetch(url, {
695
737
  method: 'POST',
696
738
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
697
739
  body: JSON.stringify({ hash: propertyKey }),
@@ -718,7 +760,7 @@ async function fetchBookingEnginePref(options = {}) {
718
760
  }
719
761
  const base = (baseUrl || '').replace(/\/$/, '');
720
762
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
721
- const res = await fetch(url, {
763
+ const res = await _fetch(url, {
722
764
  method: 'GET',
723
765
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
724
766
  });
@@ -872,8 +914,9 @@ if (typeof window !== 'undefined') {
872
914
  var primaryRgb = hexToRgb(primary), bgHsl = hexToHsl(bg), fgHsl = hexToHsl(fg);
873
915
  var styles = { '--primary': primary, '--primary-fg': primaryFg, '--bg': bg, '--fg': fg, '--card-fg': fg };
874
916
  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'];
917
+ var _cardBase = c.card || (bgHsl ? 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 2) + '%)' : bg);
918
+ styles['--card'] = c.card ? _cardBase + '40' : _cardBase;
919
+ styles['--card-solid'] = _cardBase;
877
920
  if (bgHsl) {
878
921
  styles['--secondary'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 10) + '%)';
879
922
  styles['--border'] = 'hsl(' + bgHsl[0] + ', ' + bgHsl[1] + '%, ' + Math.min(95, bgHsl[2] + 14) + '%)';
@@ -980,20 +1023,20 @@ if (typeof window !== 'undefined') {
980
1023
  this.stripeInstance = null;
981
1024
  this.elementsInstance = null;
982
1025
 
983
- const baseSteps = [
1026
+ this.baseSteps = [
984
1027
  { key: 'dates', label: 'Dates & Guests', num: '01' },
985
1028
  { key: 'rooms', label: 'Room', num: '02' },
986
1029
  { key: 'rates', label: 'Rate', num: '03' },
987
1030
  { key: 'summary', label: 'Summary', num: '04' },
988
1031
  ];
989
- this.hasStripe = !!(this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function');
990
- this.STEPS = this.hasStripe ? [...baseSteps, { key: 'payment', label: 'Payment', num: '05' }] : baseSteps;
1032
+ this._recomputeSteps();
991
1033
 
992
1034
  this.ROOMS = [];
993
1035
  this.RATES = [];
994
1036
  this.bookingApi = this.options.bookingApi || ((this.options.propertyKey || defaultApiBase) && typeof window.createBookingApi === 'function'
995
1037
  ? window.createBookingApi({
996
1038
  availabilityBaseUrl: defaultApiBase || '',
1039
+ s3BaseUrl: this.options.s3BaseUrl || undefined,
997
1040
  propertyKey: this.options.propertyKey || undefined,
998
1041
  mode: this.options.mode === 'sandbox' ? 'sandbox' : undefined,
999
1042
  })
@@ -1216,6 +1259,34 @@ if (typeof window !== 'undefined') {
1216
1259
  };
1217
1260
  }
1218
1261
 
1262
+ _applyApiS3BaseUrl(value) {
1263
+ const s3 = (typeof value === 'string' ? value.trim() : '').replace(/\/$/, '');
1264
+ if (!s3) return;
1265
+ if (!this.options.s3BaseUrl) this.options.s3BaseUrl = s3;
1266
+ if (this.options.bookingApi || typeof window.createBookingApi !== 'function') return;
1267
+ const defaultApiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
1268
+ this.bookingApi = window.createBookingApi({
1269
+ availabilityBaseUrl: defaultApiBase || '',
1270
+ s3BaseUrl: this.options.s3BaseUrl || undefined,
1271
+ propertyKey: this.options.propertyKey || undefined,
1272
+ mode: this.options.mode === 'sandbox' ? 'sandbox' : undefined,
1273
+ });
1274
+ }
1275
+
1276
+ _applyApiStripeKey(value) {
1277
+ const key = (typeof value === 'string' ? value.trim() : '');
1278
+ if (!key || this.options.stripePublishableKey) return;
1279
+ this.options.stripePublishableKey = key;
1280
+ this._recomputeSteps();
1281
+ }
1282
+
1283
+ _recomputeSteps() {
1284
+ this.hasStripe = !!(this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function');
1285
+ this.STEPS = this.hasStripe
1286
+ ? this.baseSteps.concat([{ key: 'payment', label: 'Payment', num: '05' }])
1287
+ : this.baseSteps.slice();
1288
+ }
1289
+
1219
1290
  _fetchRuntimeConfig() {
1220
1291
  const self = this;
1221
1292
  const key = String(this.options.propertyKey).trim();
@@ -1226,6 +1297,8 @@ if (typeof window !== 'undefined') {
1226
1297
  if (__bwConfigCache[cacheKey]) {
1227
1298
  const cached = __bwConfigCache[cacheKey];
1228
1299
  this._applyApiColors(cached);
1300
+ this._applyApiS3BaseUrl(cached._s3BaseUrl);
1301
+ this._applyApiStripeKey(cached._stripePublishableKey);
1229
1302
  this._configState = 'loaded';
1230
1303
  this.applyColors();
1231
1304
  this._initPosthog(cached._posthogKey || '');
@@ -1242,15 +1315,36 @@ if (typeof window !== 'undefined') {
1242
1315
  return res.json();
1243
1316
  })
1244
1317
  .then(function (data) {
1318
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
1245
1319
  const apiColors = {};
1246
- if (data.widgetBackground) apiColors.background = data.widgetBackground;
1247
- if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
1248
- if (data.primaryColor) apiColors.primary = data.primaryColor;
1249
- if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
1250
- if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
1320
+ if (data.widgetBackground || cfg.widgetBackground) apiColors.background = data.widgetBackground || cfg.widgetBackground;
1321
+ if (data.widgetTextColor || cfg.widgetTextColor) apiColors.text = data.widgetTextColor || cfg.widgetTextColor;
1322
+ if (data.primaryColor || cfg.primaryColor) apiColors.primary = data.primaryColor || cfg.primaryColor;
1323
+ if (data.buttonTextColor || cfg.buttonTextColor) apiColors.primaryText = data.buttonTextColor || cfg.buttonTextColor;
1324
+ if (data.widgetCardColor || cfg.widgetCardColor) apiColors.card = data.widgetCardColor || cfg.widgetCardColor;
1251
1325
  apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
1326
+ apiColors._s3BaseUrl = String(
1327
+ data.VITE_AWS_S3_PATH
1328
+ || data.vite_aws_s3_path
1329
+ || data.s3BaseUrl
1330
+ || data.s3_base_url
1331
+ || data.CONFIG?.VITE_AWS_S3_PATH
1332
+ || ''
1333
+ ).trim();
1334
+ apiColors._stripePublishableKey = String(
1335
+ data.STRIPE_PUBLISHABLE_KEY
1336
+ || data.STRIPE_PUBLICED_KEY
1337
+ || data.stripePublishableKey
1338
+ || data.stripe_publishable_key
1339
+ || data.CONFIG?.STRIPE_PUBLISHABLE_KEY
1340
+ || data.CONFIG?.STRIPE_PUBLICED_KEY
1341
+ || data.CONFIG?.stripePublishableKey
1342
+ || ''
1343
+ ).trim();
1252
1344
  __bwConfigCache[cacheKey] = apiColors;
1253
1345
  self._applyApiColors(apiColors);
1346
+ self._applyApiS3BaseUrl(apiColors._s3BaseUrl);
1347
+ self._applyApiStripeKey(apiColors._stripePublishableKey);
1254
1348
  self._configState = 'loaded';
1255
1349
  self.applyColors();
1256
1350
  self._initPosthog(apiColors._posthogKey);
@@ -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) + '%)';
@@ -207,14 +208,13 @@ class BookingWidget {
207
208
  guest: { firstName: '', lastName: '', email: '', phone: '', specialRequests: '' },
208
209
  };
209
210
 
210
- const baseSteps = [
211
+ this.baseSteps = [
211
212
  { key: 'dates', label: 'Dates & Guests', num: '01' },
212
213
  { key: 'rooms', label: 'Room', num: '02' },
213
214
  { key: 'rates', label: 'Rate', num: '03' },
214
215
  { key: 'summary', label: 'Summary', num: '04' },
215
216
  ];
216
- this.hasStripe = !!(this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function');
217
- this.STEPS = this.hasStripe ? [...baseSteps, { key: 'payment', label: 'Payment', num: '05' }] : baseSteps;
217
+ this._recomputeSteps();
218
218
 
219
219
  this.ROOMS = [];
220
220
  this.RATES = [];
@@ -488,6 +488,39 @@ class BookingWidget {
488
488
  };
489
489
  }
490
490
 
491
+ _applyApiS3BaseUrl(value) {
492
+ const s3 = (typeof value === 'string' ? value.trim() : '').replace(/\/$/, '');
493
+ if (!s3) return;
494
+ if (!this.options.s3BaseUrl) this.options.s3BaseUrl = s3;
495
+ if (this.options.bookingApi || typeof window.createBookingApi !== 'function') return;
496
+ const defaultApiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
497
+ const opts = this.options;
498
+ this.bookingApi = window.createBookingApi({
499
+ baseUrl: opts.apiBaseUrl || opts.availabilityBaseUrl || defaultApiBase || '',
500
+ availabilityBaseUrl: opts.availabilityBaseUrl || defaultApiBase || undefined,
501
+ propertyBaseUrl: opts.propertyBaseUrl || undefined,
502
+ s3BaseUrl: opts.s3BaseUrl || undefined,
503
+ propertyId: opts.propertyId != null && opts.propertyId !== '' ? String(opts.propertyId) : undefined,
504
+ propertyKey: opts.propertyKey || undefined,
505
+ mode: opts.mode === 'sandbox' ? 'sandbox' : undefined,
506
+ headers: opts.apiSecret ? { 'X-API-Key': opts.apiSecret } : undefined,
507
+ });
508
+ }
509
+
510
+ _applyApiStripeKey(value) {
511
+ const key = (typeof value === 'string' ? value.trim() : '');
512
+ if (!key || this.options.stripePublishableKey) return;
513
+ this.options.stripePublishableKey = key;
514
+ this._recomputeSteps();
515
+ }
516
+
517
+ _recomputeSteps() {
518
+ this.hasStripe = !!(this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function');
519
+ this.STEPS = this.hasStripe
520
+ ? [...this.baseSteps, { key: 'payment', label: 'Payment', num: '05' }]
521
+ : [...this.baseSteps];
522
+ }
523
+
491
524
  /**
492
525
  * Fetch runtime styling config from /load-config.
493
526
  * Caches result in __bwConfigCache (module-level, shared across instances).
@@ -505,6 +538,8 @@ class BookingWidget {
505
538
  if (__bwConfigCache[cacheKey]) {
506
539
  const cached = __bwConfigCache[cacheKey];
507
540
  this._applyApiColors(cached);
541
+ this._applyApiS3BaseUrl(cached._s3BaseUrl);
542
+ this._applyApiStripeKey(cached._stripePublishableKey);
508
543
  this._configState = 'loaded';
509
544
  this.applyColors();
510
545
  this._initPosthog(cached._posthogKey || '');
@@ -521,15 +556,36 @@ class BookingWidget {
521
556
  return res.json();
522
557
  })
523
558
  .then(function (data) {
559
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
524
560
  const apiColors = {};
525
- if (data.widgetBackground) apiColors.background = data.widgetBackground;
526
- if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
527
- if (data.primaryColor) apiColors.primary = data.primaryColor;
528
- if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
529
- if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
561
+ if (data.widgetBackground || cfg.widgetBackground) apiColors.background = data.widgetBackground || cfg.widgetBackground;
562
+ if (data.widgetTextColor || cfg.widgetTextColor) apiColors.text = data.widgetTextColor || cfg.widgetTextColor;
563
+ if (data.primaryColor || cfg.primaryColor) apiColors.primary = data.primaryColor || cfg.primaryColor;
564
+ if (data.buttonTextColor || cfg.buttonTextColor) apiColors.primaryText = data.buttonTextColor || cfg.buttonTextColor;
565
+ if (data.widgetCardColor || cfg.widgetCardColor) apiColors.card = data.widgetCardColor || cfg.widgetCardColor;
530
566
  apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
567
+ apiColors._s3BaseUrl = String(
568
+ data.VITE_AWS_S3_PATH
569
+ || data.vite_aws_s3_path
570
+ || data.s3BaseUrl
571
+ || data.s3_base_url
572
+ || data.CONFIG?.VITE_AWS_S3_PATH
573
+ || ''
574
+ ).trim();
575
+ apiColors._stripePublishableKey = String(
576
+ data.STRIPE_PUBLISHABLE_KEY
577
+ || data.STRIPE_PUBLICED_KEY
578
+ || data.stripePublishableKey
579
+ || data.stripe_publishable_key
580
+ || data.CONFIG?.STRIPE_PUBLISHABLE_KEY
581
+ || data.CONFIG?.STRIPE_PUBLICED_KEY
582
+ || data.CONFIG?.stripePublishableKey
583
+ || ''
584
+ ).trim();
531
585
  __bwConfigCache[cacheKey] = apiColors;
532
586
  self._applyApiColors(apiColors);
587
+ self._applyApiS3BaseUrl(apiColors._s3BaseUrl);
588
+ self._applyApiStripeKey(apiColors._stripePublishableKey);
533
589
  self._configState = 'loaded';
534
590
  self.applyColors();
535
591
  self._initPosthog(apiColors._posthogKey);
@@ -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 (_) {}
@@ -281,6 +286,20 @@ function createBookingApi(config = {}) {
281
286
 
282
287
  // Fetch get_properties first to get currency_code (e.g. 'EUR') and room details
283
288
  let propertyRooms = {};
289
+ const normalizeRoomsById = (rooms) => {
290
+ if (!rooms) return {};
291
+ if (Array.isArray(rooms)) {
292
+ return rooms.reduce((acc, room) => {
293
+ if (!room || typeof room !== 'object') return acc;
294
+ const key = room.id ?? room.room_id;
295
+ if (key == null) return acc;
296
+ acc[String(key)] = room;
297
+ return acc;
298
+ }, {});
299
+ }
300
+ if (typeof rooms === 'object') return rooms;
301
+ return {};
302
+ };
284
303
  let propertyCurrency = currency;
285
304
  const propQuery = propertyKey
286
305
  ? `key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
@@ -293,10 +312,10 @@ function createBookingApi(config = {}) {
293
312
  try {
294
313
  const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
295
314
  if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
296
- const propRes = await fetch(propFullUrl, getOpts);
315
+ const propRes = await _fetch(propFullUrl, getOpts);
297
316
  if (propRes.ok) {
298
317
  const propData = await propRes.json();
299
- propertyRooms = propData?.rooms ?? {};
318
+ propertyRooms = normalizeRoomsById(propData?.rooms);
300
319
  propertyCurrency = (typeof propData?.currency_code === 'string' && propData.currency_code.trim())
301
320
  ? propData.currency_code.trim()
302
321
  : currency;
@@ -355,18 +374,41 @@ function createBookingApi(config = {}) {
355
374
  return (a?.price ?? a?.total ?? r?.price ?? 0) || 0;
356
375
  };
357
376
  const fallbackImage = 'https://images.unsplash.com/photo-1618773928121-c32242e63f39?w=800&q=80';
377
+ const toImageUrl = (value) => {
378
+ if (typeof value !== 'string') return '';
379
+ const v = value.trim();
380
+ if (!v) return '';
381
+ if (/^https?:\/\//i.test(v)) return v;
382
+ if (v.startsWith('//')) return `https:${v}`;
383
+ if (s3BaseUrl) return `${s3BaseUrl}/${v.replace(/^\//, '')}`;
384
+ return '';
385
+ };
358
386
 
359
387
  return availableRoomIds.map((roomId) => {
360
- const roomData = propertyRooms[roomId] ?? propertyRooms[String(roomId)] ?? {};
388
+ const roomData = propertyRooms[String(roomId)] ?? propertyRooms[roomId] ?? {};
361
389
  const roomRates = filteredRates.filter((r) => String(r.room_id) === String(roomId));
362
390
  const minPrice = roomRates.length
363
391
  ? Math.min(...roomRates.map(getPrice).filter((p) => p > 0)) || roomData.base_price || 0
364
392
  : roomData.base_price || 0;
365
393
 
366
- const photos = roomData.photos ?? [];
367
- const mainPhoto = photos.find((p) => p.main) ?? photos[0];
368
- const photoPath = mainPhoto?.path ?? '';
369
- const image = s3BaseUrl && photoPath ? `${s3BaseUrl}/${photoPath.replace(/^\//, '')}` : fallbackImage;
394
+ const photos = Array.isArray(roomData.photos) ? roomData.photos : [];
395
+ const mainPhoto = photos.find((p) => (typeof p === 'object' && p?.main)) ?? photos[0] ?? {};
396
+ const photoValue = typeof mainPhoto === 'string' ? mainPhoto : '';
397
+ const imageCandidates = [
398
+ photoValue,
399
+ mainPhoto.path,
400
+ mainPhoto.url,
401
+ mainPhoto.src,
402
+ mainPhoto.image,
403
+ mainPhoto.image_url,
404
+ mainPhoto.secure_url,
405
+ roomData.image,
406
+ roomData.image_url,
407
+ roomData.imageUrl,
408
+ roomData.thumbnail,
409
+ roomData.thumbnail_url,
410
+ ];
411
+ const image = imageCandidates.map(toImageUrl).find(Boolean) || fallbackImage;
370
412
 
371
413
  const sizeVal = roomData.size?.value ?? roomData.size_value;
372
414
  let sizeUnit = roomData.size?.unit ?? roomData.size_unit ?? 'm²';
@@ -690,7 +732,7 @@ async function decryptPropertyId(options = {}) {
690
732
  if (!propertyKey) return null;
691
733
  const base = (baseUrl || '').replace(/\/$/, '');
692
734
  const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
693
- const res = await fetch(url, {
735
+ const res = await _fetch(url, {
694
736
  method: 'POST',
695
737
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
696
738
  body: JSON.stringify({ hash: propertyKey }),
@@ -717,7 +759,7 @@ async function fetchBookingEnginePref(options = {}) {
717
759
  }
718
760
  const base = (baseUrl || '').replace(/\/$/, '');
719
761
  const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
720
- const res = await fetch(url, {
762
+ const res = await _fetch(url, {
721
763
  method: 'GET',
722
764
  headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
723
765
  });
@@ -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;
@@ -7,6 +7,11 @@ import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
7
7
  import { fetchRuntimeConfig } from '../utils/config-service.js';
8
8
  import { init as initAnalytics, capture as captureEvent, identify as identifyAnalytics } from '../utils/analytics.js';
9
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;
14
+
10
15
  const BASE_STEPS = [
11
16
  { key: 'dates', label: 'Dates & Guests', num: '01' },
12
17
  { key: 'rooms', label: 'Room', num: '02' },
@@ -51,9 +56,11 @@ const BookingWidget = ({
51
56
  /** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
52
57
  posthogHost = '',
53
58
  }) => {
54
- const stripePublishableKey = stripePublishableKeyProp || STRIPE_PUBLISHABLE_KEY;
59
+ const [runtimeStripePublishableKey, setRuntimeStripePublishableKey] = useState('');
60
+ const [runtimeApiBaseUrl, setRuntimeApiBaseUrl] = useState('');
61
+ const stripePublishableKey = stripePublishableKeyProp || runtimeStripePublishableKey || STRIPE_PUBLISHABLE_KEY;
55
62
  const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
56
- const apiBase = (env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
63
+ const apiBase = (runtimeApiBaseUrl || env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
57
64
  const effectivePaymentIntentUrl = (apiBase ? apiBase + '/proxy/create-payment-intent' : '').trim();
58
65
  const isSandbox = mode === 'sandbox';
59
66
  const hasPropertyKey = !!(propertyKey && String(propertyKey).trim());
@@ -64,7 +71,7 @@ const BookingWidget = ({
64
71
  const url = effectivePaymentIntentUrl + (isSandbox ? '?sandbox=true' : '');
65
72
  const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
66
73
  if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
67
- const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
74
+ const res = await _nativeFetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
68
75
  if (!res.ok) throw new Error(await res.text());
69
76
  const data = await res.json();
70
77
  const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
@@ -108,6 +115,7 @@ const BookingWidget = ({
108
115
  const [configLoaded, setConfigLoaded] = useState(false);
109
116
  const [configError, setConfigError] = useState(null);
110
117
  const [runtimeWidgetStyles, setRuntimeWidgetStyles] = useState({});
118
+ const [runtimeS3BaseUrl, setRuntimeS3BaseUrl] = useState('');
111
119
  const [configRetryCount, setConfigRetryCount] = useState(0);
112
120
  const widgetRef = useRef(null);
113
121
  const stripeRef = useRef(null);
@@ -116,11 +124,11 @@ const BookingWidget = ({
116
124
 
117
125
  // Use package .env when props not passed (e.g. examples with envDir pointing at root)
118
126
  const effectiveApiBaseUrl = apiBaseUrl || env.VITE_API_URL || '';
119
- const effectiveConfirmationBaseUrl = (env.VITE_API_BASE_URL || API_BASE_URL || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
127
+ const effectiveConfirmationBaseUrl = (runtimeApiBaseUrl || env.VITE_API_BASE_URL || API_BASE_URL || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
120
128
  const apiBaseForAvailability = (env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
121
129
  const effectiveAvailabilityBaseUrl = availabilityBaseUrl !== '' ? availabilityBaseUrl : apiBaseForAvailability;
122
130
  const effectivePropertyBaseUrl = propertyBaseUrl || env.VITE_PROPERTY_BASE_URL || '';
123
- const effectiveS3BaseUrl = s3BaseUrl || env.VITE_AWS_S3_PATH || '';
131
+ const effectiveS3BaseUrl = s3BaseUrl || runtimeS3BaseUrl || env.VITE_AWS_S3_PATH || '';
124
132
 
125
133
  const bookingApiRef = useMemo(() => {
126
134
  if (bookingApiProp && typeof bookingApiProp.fetchRooms === 'function') return bookingApiProp;
@@ -227,7 +235,7 @@ const BookingWidget = ({
227
235
  if (cancelled) return;
228
236
  try {
229
237
  const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}${isSandbox ? '?sandbox=true' : ''}`;
230
- const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
238
+ const res = await _nativeFetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
231
239
  if (!res.ok) throw new Error(await res.text());
232
240
  const data = await res.json();
233
241
  const status = data?.status != null ? String(data.status) : '';
@@ -271,6 +279,9 @@ const BookingWidget = ({
271
279
  setConfigLoading(false);
272
280
  setConfigLoaded(false);
273
281
  setRuntimeWidgetStyles({});
282
+ setRuntimeS3BaseUrl('');
283
+ setRuntimeStripePublishableKey('');
284
+ setRuntimeApiBaseUrl('');
274
285
  return;
275
286
  }
276
287
  let cancelled = false;
@@ -278,17 +289,23 @@ const BookingWidget = ({
278
289
  setConfigError(null);
279
290
  setConfigLoaded(false);
280
291
  fetchRuntimeConfig(propertyKey, colors, mode)
281
- .then(({ widgetStyles, posthogKey: configPosthogKey }) => {
292
+ .then(({ widgetStyles, posthogKey: configPosthogKey, s3BaseUrl: configS3BaseUrl, stripePublishableKey: configStripeKey, apiBaseUrl: configApiBaseUrl }) => {
282
293
  if (cancelled) return;
283
294
  const analyticsKey = posthogKey || configPosthogKey || '';
284
295
  initAnalytics(analyticsKey || posthogHost ? { key: analyticsKey || undefined, host: posthogHost || undefined } : {});
285
296
  setRuntimeWidgetStyles(widgetStyles);
297
+ setRuntimeS3BaseUrl(configS3BaseUrl || '');
298
+ setRuntimeStripePublishableKey(configStripeKey || '');
299
+ setRuntimeApiBaseUrl(configApiBaseUrl || '');
286
300
  setConfigLoaded(true);
287
301
  setConfigLoading(false);
288
302
  })
289
303
  .catch((err) => {
290
304
  if (cancelled) return;
291
305
  setConfigError(err?.message || 'Failed to load widget configuration.');
306
+ setRuntimeS3BaseUrl('');
307
+ setRuntimeStripePublishableKey('');
308
+ setRuntimeApiBaseUrl('');
292
309
  setConfigLoading(false);
293
310
  });
294
311
  return () => { cancelled = true; };
@@ -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,11 +10,53 @@
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 → { apiColors, posthogKey } */
19
+ /** In-memory cache: propertyKey → { apiColors, posthogKey, s3BaseUrl, stripePublishableKey } */
16
20
  const _configCache = new Map();
17
21
 
22
+ function extractS3BaseUrl(data) {
23
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
24
+ return String(
25
+ data?.VITE_AWS_S3_PATH
26
+ || data?.vite_aws_s3_path
27
+ || data?.s3BaseUrl
28
+ || data?.s3_base_url
29
+ || cfg?.VITE_AWS_S3_PATH
30
+ || ''
31
+ ).trim();
32
+ }
33
+
34
+ function extractStripePublishableKey(data) {
35
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
36
+ return String(
37
+ data?.STRIPE_PUBLISHABLE_KEY
38
+ || data?.STRIPE_PUBLICED_KEY
39
+ || data?.stripePublishableKey
40
+ || data?.stripe_publishable_key
41
+ || cfg?.STRIPE_PUBLISHABLE_KEY
42
+ || cfg?.STRIPE_PUBLICED_KEY
43
+ || cfg?.stripePublishableKey
44
+ || ''
45
+ ).trim();
46
+ }
47
+
48
+ function extractApiBaseUrl(data) {
49
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
50
+ return String(
51
+ data?.API_BASE_URL
52
+ || data?.apiBaseUrl
53
+ || data?.api_base_url
54
+ || cfg?.API_BASE_URL
55
+ || cfg?.apiBaseUrl
56
+ || ''
57
+ ).trim().replace(/\/$/, '');
58
+ }
59
+
18
60
  /**
19
61
  * Map the /load-config response field names to the internal color keys used
20
62
  * throughout the widget.
@@ -27,12 +69,13 @@ const _configCache = new Map();
27
69
  * widgetCardColor → card
28
70
  */
29
71
  function mapApiColors(data) {
72
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
30
73
  const mapped = {};
31
- if (data.widgetBackground) mapped.background = data.widgetBackground;
32
- if (data.widgetTextColor) mapped.text = data.widgetTextColor;
33
- if (data.primaryColor) mapped.primary = data.primaryColor;
34
- if (data.buttonTextColor) mapped.primaryText = data.buttonTextColor;
35
- if (data.widgetCardColor) mapped.card = data.widgetCardColor;
74
+ if (data.widgetBackground || cfg.widgetBackground) mapped.background = data.widgetBackground || cfg.widgetBackground;
75
+ if (data.widgetTextColor || cfg.widgetTextColor) mapped.text = data.widgetTextColor || cfg.widgetTextColor;
76
+ if (data.primaryColor || cfg.primaryColor) mapped.primary = data.primaryColor || cfg.primaryColor;
77
+ if (data.buttonTextColor || cfg.buttonTextColor) mapped.primaryText = data.buttonTextColor || cfg.buttonTextColor;
78
+ if (data.widgetCardColor || cfg.widgetCardColor) mapped.card = data.widgetCardColor || cfg.widgetCardColor;
36
79
  return mapped;
37
80
  }
38
81
 
@@ -70,7 +113,7 @@ export function mergeColors(mappedApiColors, installerColors) {
70
113
  * @param {string} propertyKey - The hotel property key / API key.
71
114
  * @param {object|null} installerColors - Optional installer color overrides.
72
115
  * @param {string} [mode] - Pass 'sandbox' to append &mode=sandbox to the request.
73
- * @returns {Promise<{ apiColors: object, colors: object, widgetStyles: object }>}
116
+ * @returns {Promise<{ apiColors: object, colors: object, widgetStyles: object, posthogKey: string, s3BaseUrl: string, stripePublishableKey: string, apiBaseUrl: string }>}
74
117
  */
75
118
  export async function fetchRuntimeConfig(propertyKey, installerColors = null, mode = null) {
76
119
  if (!propertyKey || !String(propertyKey).trim()) {
@@ -83,13 +126,13 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
83
126
  const cacheKey = isSandbox ? `${key}:sandbox` : key;
84
127
 
85
128
  if (_configCache.has(cacheKey)) {
86
- const { apiColors, posthogKey } = _configCache.get(cacheKey);
129
+ const { apiColors, posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl } = _configCache.get(cacheKey);
87
130
  const colors = mergeColors(apiColors, installerColors);
88
- return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
131
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl };
89
132
  }
90
133
 
91
134
  const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
92
- const res = await fetch(url, { headers: { 'Source': 'booking_engine' } });
135
+ const res = await _nativeFetch(url, { headers: { 'Source': 'booking_engine' } });
93
136
  if (!res.ok) {
94
137
  throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
95
138
  }
@@ -97,8 +140,11 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
97
140
  const data = await res.json();
98
141
  const apiColors = mapApiColors(data);
99
142
  const posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
100
- _configCache.set(cacheKey, { apiColors, posthogKey });
143
+ const s3BaseUrl = extractS3BaseUrl(data);
144
+ const stripePublishableKey = extractStripePublishableKey(data);
145
+ const apiBaseUrl = extractApiBaseUrl(data);
146
+ _configCache.set(cacheKey, { apiColors, posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl });
101
147
 
102
148
  const colors = mergeColors(apiColors, installerColors);
103
- return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
149
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl };
104
150
  }
@@ -484,6 +484,11 @@ import { STRIPE_PUBLISHABLE_KEY, API_BASE_URL } from '../core/stripe-config.js';
484
484
  import { fetchRuntimeConfig } from '../utils/config-service.js';
485
485
  import { init as initAnalytics, capture as captureEvent, identify as identifyAnalytics } from '../utils/analytics.js';
486
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;
491
+
487
492
 
488
493
  const BASE_STEPS = [
489
494
  { key: 'dates', label: 'Dates & Guests', num: '01' },
@@ -544,6 +549,8 @@ export default {
544
549
  configLoaded: false,
545
550
  configError: null,
546
551
  runtimeWidgetStyles: {},
552
+ runtimeS3BaseUrl: '',
553
+ runtimeStripePublishableKey: '',
547
554
  calendarMonth: new Date().getMonth(),
548
555
  calendarYear: new Date().getFullYear(),
549
556
  pickState: 0,
@@ -585,7 +592,10 @@ export default {
585
592
  return this.propertyBaseUrl || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_PROPERTY_BASE_URL) || '';
586
593
  },
587
594
  effectiveS3BaseUrl() {
588
- return this.s3BaseUrl || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_AWS_S3_PATH) || '';
595
+ return this.s3BaseUrl || this.runtimeS3BaseUrl || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_AWS_S3_PATH) || '';
596
+ },
597
+ effectiveStripePublishableKey() {
598
+ return this.stripePublishableKey || this.runtimeStripePublishableKey || '';
589
599
  },
590
600
  effectivePaymentIntentUrl() {
591
601
  const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
@@ -598,7 +608,7 @@ export default {
598
608
  return String(base).replace(/\/$/, '');
599
609
  },
600
610
  hasStripe() {
601
- return !!(this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function');
611
+ return !!(this.effectiveStripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function');
602
612
  },
603
613
  STEPS() {
604
614
  return this.hasStripe ? [...BASE_STEPS, { key: 'payment', label: 'Payment', num: '05' }] : BASE_STEPS;
@@ -611,7 +621,7 @@ export default {
611
621
  const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
612
622
  const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
613
623
  if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
614
- const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
624
+ const res = await _nativeFetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
615
625
  if (!res.ok) throw new Error(await res.text());
616
626
  const data = await res.json();
617
627
  const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
@@ -800,19 +810,25 @@ export default {
800
810
  this.configLoading = false;
801
811
  this.configLoaded = false;
802
812
  this.runtimeWidgetStyles = {};
813
+ this.runtimeS3BaseUrl = '';
814
+ this.runtimeStripePublishableKey = '';
803
815
  return;
804
816
  }
805
817
  this.configLoading = true;
806
818
  this.configError = null;
807
819
  this.configLoaded = false;
808
820
  try {
809
- const { widgetStyles, posthogKey: configPosthogKey } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
821
+ const { widgetStyles, posthogKey: configPosthogKey, s3BaseUrl: configS3BaseUrl, stripePublishableKey: configStripeKey } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
810
822
  const analyticsKey = this.posthogKey || configPosthogKey || '';
811
823
  initAnalytics(analyticsKey || this.posthogHost ? { key: analyticsKey || undefined, host: this.posthogHost || undefined } : {});
812
824
  this.runtimeWidgetStyles = widgetStyles;
825
+ this.runtimeS3BaseUrl = configS3BaseUrl || '';
826
+ this.runtimeStripePublishableKey = configStripeKey || '';
813
827
  this.configLoaded = true;
814
828
  } catch (err) {
815
829
  this.configError = err?.message || 'Failed to load widget configuration.';
830
+ this.runtimeS3BaseUrl = '';
831
+ this.runtimeStripePublishableKey = '';
816
832
  } finally {
817
833
  this.configLoading = false;
818
834
  }
@@ -821,7 +837,7 @@ export default {
821
837
  const t = String(token || '').trim();
822
838
  if (!t) throw new Error('Missing confirmation token');
823
839
  const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
824
- const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
840
+ const res = await _nativeFetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
825
841
  if (!res.ok) throw new Error(await res.text());
826
842
  return await res.json();
827
843
  },
@@ -861,7 +877,7 @@ export default {
861
877
  pollOnce();
862
878
  },
863
879
  async loadStripePaymentElement() {
864
- if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.stripePublishableKey || typeof this.effectiveCreatePaymentIntent !== 'function') return;
880
+ if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.effectiveStripePublishableKey || typeof this.effectiveCreatePaymentIntent !== 'function') return;
865
881
  const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
866
882
  this.paymentElementReady = false;
867
883
  try {
@@ -873,7 +889,7 @@ export default {
873
889
  this.apiError = 'Payment setup failed: no client secret returned';
874
890
  return;
875
891
  }
876
- const stripe = await loadStripe(this.stripePublishableKey);
892
+ const stripe = await loadStripe(this.effectiveStripePublishableKey);
877
893
  if (!stripe || this.state.step !== 'payment') return;
878
894
  this.stripeInstance = stripe;
879
895
  const elements = stripe.elements({ clientSecret, appearance: { theme: 'flat', variables: { borderRadius: '8px' } } });
@@ -982,14 +998,14 @@ export default {
982
998
  if (!this.canSubmit) return;
983
999
  const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
984
1000
  // Summary step with Stripe: go to payment step (form loads there)
985
- if (this.state.step === 'summary' && this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
1001
+ if (this.state.step === 'summary' && this.effectiveStripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
986
1002
  this.apiError = null;
987
1003
  this.goToStep('payment');
988
1004
  return;
989
1005
  }
990
1006
 
991
1007
  // Payment step: confirm payment
992
- if (this.state.step === 'payment' && this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function' && this.stripeInstance && this.elementsInstance) {
1008
+ if (this.state.step === 'payment' && this.effectiveStripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function' && this.stripeInstance && this.elementsInstance) {
993
1009
  this.apiError = null;
994
1010
  this.stripeInstance.confirmPayment({
995
1011
  elements: this.elementsInstance,
@@ -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.7",
3
+ "version": "1.0.9",
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",