@nuitee/booking-widget 1.0.8 → 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.8/dist/booking-widget.css">
98
- <script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.8/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.8/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'),
@@ -287,6 +287,20 @@ function createBookingApi(config = {}) {
287
287
 
288
288
  // Fetch get_properties first to get currency_code (e.g. 'EUR') and room details
289
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
+ };
290
304
  let propertyCurrency = currency;
291
305
  const propQuery = propertyKey
292
306
  ? `key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
@@ -302,7 +316,7 @@ function createBookingApi(config = {}) {
302
316
  const propRes = await _fetch(propFullUrl, getOpts);
303
317
  if (propRes.ok) {
304
318
  const propData = await propRes.json();
305
- propertyRooms = propData?.rooms ?? {};
319
+ propertyRooms = normalizeRoomsById(propData?.rooms);
306
320
  propertyCurrency = (typeof propData?.currency_code === 'string' && propData.currency_code.trim())
307
321
  ? propData.currency_code.trim()
308
322
  : currency;
@@ -361,18 +375,41 @@ function createBookingApi(config = {}) {
361
375
  return (a?.price ?? a?.total ?? r?.price ?? 0) || 0;
362
376
  };
363
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
+ };
364
387
 
365
388
  return availableRoomIds.map((roomId) => {
366
- const roomData = propertyRooms[roomId] ?? propertyRooms[String(roomId)] ?? {};
389
+ const roomData = propertyRooms[String(roomId)] ?? propertyRooms[roomId] ?? {};
367
390
  const roomRates = filteredRates.filter((r) => String(r.room_id) === String(roomId));
368
391
  const minPrice = roomRates.length
369
392
  ? Math.min(...roomRates.map(getPrice).filter((p) => p > 0)) || roomData.base_price || 0
370
393
  : roomData.base_price || 0;
371
394
 
372
- const photos = roomData.photos ?? [];
373
- const mainPhoto = photos.find((p) => p.main) ?? photos[0];
374
- const photoPath = mainPhoto?.path ?? '';
375
- 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;
376
413
 
377
414
  const sizeVal = roomData.size?.value ?? roomData.size_value;
378
415
  let sizeUnit = roomData.size?.unit ?? roomData.size_unit ?? 'm²';
@@ -986,20 +1023,20 @@ if (typeof window !== 'undefined') {
986
1023
  this.stripeInstance = null;
987
1024
  this.elementsInstance = null;
988
1025
 
989
- const baseSteps = [
1026
+ this.baseSteps = [
990
1027
  { key: 'dates', label: 'Dates & Guests', num: '01' },
991
1028
  { key: 'rooms', label: 'Room', num: '02' },
992
1029
  { key: 'rates', label: 'Rate', num: '03' },
993
1030
  { key: 'summary', label: 'Summary', num: '04' },
994
1031
  ];
995
- this.hasStripe = !!(this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function');
996
- this.STEPS = this.hasStripe ? [...baseSteps, { key: 'payment', label: 'Payment', num: '05' }] : baseSteps;
1032
+ this._recomputeSteps();
997
1033
 
998
1034
  this.ROOMS = [];
999
1035
  this.RATES = [];
1000
1036
  this.bookingApi = this.options.bookingApi || ((this.options.propertyKey || defaultApiBase) && typeof window.createBookingApi === 'function'
1001
1037
  ? window.createBookingApi({
1002
1038
  availabilityBaseUrl: defaultApiBase || '',
1039
+ s3BaseUrl: this.options.s3BaseUrl || undefined,
1003
1040
  propertyKey: this.options.propertyKey || undefined,
1004
1041
  mode: this.options.mode === 'sandbox' ? 'sandbox' : undefined,
1005
1042
  })
@@ -1222,6 +1259,34 @@ if (typeof window !== 'undefined') {
1222
1259
  };
1223
1260
  }
1224
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
+
1225
1290
  _fetchRuntimeConfig() {
1226
1291
  const self = this;
1227
1292
  const key = String(this.options.propertyKey).trim();
@@ -1232,6 +1297,8 @@ if (typeof window !== 'undefined') {
1232
1297
  if (__bwConfigCache[cacheKey]) {
1233
1298
  const cached = __bwConfigCache[cacheKey];
1234
1299
  this._applyApiColors(cached);
1300
+ this._applyApiS3BaseUrl(cached._s3BaseUrl);
1301
+ this._applyApiStripeKey(cached._stripePublishableKey);
1235
1302
  this._configState = 'loaded';
1236
1303
  this.applyColors();
1237
1304
  this._initPosthog(cached._posthogKey || '');
@@ -1248,15 +1315,36 @@ if (typeof window !== 'undefined') {
1248
1315
  return res.json();
1249
1316
  })
1250
1317
  .then(function (data) {
1318
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
1251
1319
  const apiColors = {};
1252
- if (data.widgetBackground) apiColors.background = data.widgetBackground;
1253
- if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
1254
- if (data.primaryColor) apiColors.primary = data.primaryColor;
1255
- if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
1256
- 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;
1257
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();
1258
1344
  __bwConfigCache[cacheKey] = apiColors;
1259
1345
  self._applyApiColors(apiColors);
1346
+ self._applyApiS3BaseUrl(apiColors._s3BaseUrl);
1347
+ self._applyApiStripeKey(apiColors._stripePublishableKey);
1260
1348
  self._configState = 'loaded';
1261
1349
  self.applyColors();
1262
1350
  self._initPosthog(apiColors._posthogKey);
@@ -208,14 +208,13 @@ class BookingWidget {
208
208
  guest: { firstName: '', lastName: '', email: '', phone: '', specialRequests: '' },
209
209
  };
210
210
 
211
- const baseSteps = [
211
+ this.baseSteps = [
212
212
  { key: 'dates', label: 'Dates & Guests', num: '01' },
213
213
  { key: 'rooms', label: 'Room', num: '02' },
214
214
  { key: 'rates', label: 'Rate', num: '03' },
215
215
  { key: 'summary', label: 'Summary', num: '04' },
216
216
  ];
217
- this.hasStripe = !!(this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function');
218
- this.STEPS = this.hasStripe ? [...baseSteps, { key: 'payment', label: 'Payment', num: '05' }] : baseSteps;
217
+ this._recomputeSteps();
219
218
 
220
219
  this.ROOMS = [];
221
220
  this.RATES = [];
@@ -489,6 +488,39 @@ class BookingWidget {
489
488
  };
490
489
  }
491
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
+
492
524
  /**
493
525
  * Fetch runtime styling config from /load-config.
494
526
  * Caches result in __bwConfigCache (module-level, shared across instances).
@@ -506,6 +538,8 @@ class BookingWidget {
506
538
  if (__bwConfigCache[cacheKey]) {
507
539
  const cached = __bwConfigCache[cacheKey];
508
540
  this._applyApiColors(cached);
541
+ this._applyApiS3BaseUrl(cached._s3BaseUrl);
542
+ this._applyApiStripeKey(cached._stripePublishableKey);
509
543
  this._configState = 'loaded';
510
544
  this.applyColors();
511
545
  this._initPosthog(cached._posthogKey || '');
@@ -522,15 +556,36 @@ class BookingWidget {
522
556
  return res.json();
523
557
  })
524
558
  .then(function (data) {
559
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
525
560
  const apiColors = {};
526
- if (data.widgetBackground) apiColors.background = data.widgetBackground;
527
- if (data.widgetTextColor) apiColors.text = data.widgetTextColor;
528
- if (data.primaryColor) apiColors.primary = data.primaryColor;
529
- if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
530
- 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;
531
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();
532
585
  __bwConfigCache[cacheKey] = apiColors;
533
586
  self._applyApiColors(apiColors);
587
+ self._applyApiS3BaseUrl(apiColors._s3BaseUrl);
588
+ self._applyApiStripeKey(apiColors._stripePublishableKey);
534
589
  self._configState = 'loaded';
535
590
  self.applyColors();
536
591
  self._initPosthog(apiColors._posthogKey);
@@ -286,6 +286,20 @@ function createBookingApi(config = {}) {
286
286
 
287
287
  // Fetch get_properties first to get currency_code (e.g. 'EUR') and room details
288
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
+ };
289
303
  let propertyCurrency = currency;
290
304
  const propQuery = propertyKey
291
305
  ? `key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
@@ -301,7 +315,7 @@ function createBookingApi(config = {}) {
301
315
  const propRes = await _fetch(propFullUrl, getOpts);
302
316
  if (propRes.ok) {
303
317
  const propData = await propRes.json();
304
- propertyRooms = propData?.rooms ?? {};
318
+ propertyRooms = normalizeRoomsById(propData?.rooms);
305
319
  propertyCurrency = (typeof propData?.currency_code === 'string' && propData.currency_code.trim())
306
320
  ? propData.currency_code.trim()
307
321
  : currency;
@@ -360,18 +374,41 @@ function createBookingApi(config = {}) {
360
374
  return (a?.price ?? a?.total ?? r?.price ?? 0) || 0;
361
375
  };
362
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
+ };
363
386
 
364
387
  return availableRoomIds.map((roomId) => {
365
- const roomData = propertyRooms[roomId] ?? propertyRooms[String(roomId)] ?? {};
388
+ const roomData = propertyRooms[String(roomId)] ?? propertyRooms[roomId] ?? {};
366
389
  const roomRates = filteredRates.filter((r) => String(r.room_id) === String(roomId));
367
390
  const minPrice = roomRates.length
368
391
  ? Math.min(...roomRates.map(getPrice).filter((p) => p > 0)) || roomData.base_price || 0
369
392
  : roomData.base_price || 0;
370
393
 
371
- const photos = roomData.photos ?? [];
372
- const mainPhoto = photos.find((p) => p.main) ?? photos[0];
373
- const photoPath = mainPhoto?.path ?? '';
374
- 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;
375
412
 
376
413
  const sizeVal = roomData.size?.value ?? roomData.size_value;
377
414
  let sizeUnit = roomData.size?.unit ?? roomData.size_unit ?? 'm²';
@@ -56,9 +56,11 @@ const BookingWidget = ({
56
56
  /** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
57
57
  posthogHost = '',
58
58
  }) => {
59
- const stripePublishableKey = stripePublishableKeyProp || STRIPE_PUBLISHABLE_KEY;
59
+ const [runtimeStripePublishableKey, setRuntimeStripePublishableKey] = useState('');
60
+ const [runtimeApiBaseUrl, setRuntimeApiBaseUrl] = useState('');
61
+ const stripePublishableKey = stripePublishableKeyProp || runtimeStripePublishableKey || STRIPE_PUBLISHABLE_KEY;
60
62
  const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
61
- const apiBase = (env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
63
+ const apiBase = (runtimeApiBaseUrl || env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
62
64
  const effectivePaymentIntentUrl = (apiBase ? apiBase + '/proxy/create-payment-intent' : '').trim();
63
65
  const isSandbox = mode === 'sandbox';
64
66
  const hasPropertyKey = !!(propertyKey && String(propertyKey).trim());
@@ -113,6 +115,7 @@ const BookingWidget = ({
113
115
  const [configLoaded, setConfigLoaded] = useState(false);
114
116
  const [configError, setConfigError] = useState(null);
115
117
  const [runtimeWidgetStyles, setRuntimeWidgetStyles] = useState({});
118
+ const [runtimeS3BaseUrl, setRuntimeS3BaseUrl] = useState('');
116
119
  const [configRetryCount, setConfigRetryCount] = useState(0);
117
120
  const widgetRef = useRef(null);
118
121
  const stripeRef = useRef(null);
@@ -121,11 +124,11 @@ const BookingWidget = ({
121
124
 
122
125
  // Use package .env when props not passed (e.g. examples with envDir pointing at root)
123
126
  const effectiveApiBaseUrl = apiBaseUrl || env.VITE_API_URL || '';
124
- 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(/\/$/, '');
125
128
  const apiBaseForAvailability = (env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
126
129
  const effectiveAvailabilityBaseUrl = availabilityBaseUrl !== '' ? availabilityBaseUrl : apiBaseForAvailability;
127
130
  const effectivePropertyBaseUrl = propertyBaseUrl || env.VITE_PROPERTY_BASE_URL || '';
128
- const effectiveS3BaseUrl = s3BaseUrl || env.VITE_AWS_S3_PATH || '';
131
+ const effectiveS3BaseUrl = s3BaseUrl || runtimeS3BaseUrl || env.VITE_AWS_S3_PATH || '';
129
132
 
130
133
  const bookingApiRef = useMemo(() => {
131
134
  if (bookingApiProp && typeof bookingApiProp.fetchRooms === 'function') return bookingApiProp;
@@ -276,6 +279,9 @@ const BookingWidget = ({
276
279
  setConfigLoading(false);
277
280
  setConfigLoaded(false);
278
281
  setRuntimeWidgetStyles({});
282
+ setRuntimeS3BaseUrl('');
283
+ setRuntimeStripePublishableKey('');
284
+ setRuntimeApiBaseUrl('');
279
285
  return;
280
286
  }
281
287
  let cancelled = false;
@@ -283,17 +289,23 @@ const BookingWidget = ({
283
289
  setConfigError(null);
284
290
  setConfigLoaded(false);
285
291
  fetchRuntimeConfig(propertyKey, colors, mode)
286
- .then(({ widgetStyles, posthogKey: configPosthogKey }) => {
292
+ .then(({ widgetStyles, posthogKey: configPosthogKey, s3BaseUrl: configS3BaseUrl, stripePublishableKey: configStripeKey, apiBaseUrl: configApiBaseUrl }) => {
287
293
  if (cancelled) return;
288
294
  const analyticsKey = posthogKey || configPosthogKey || '';
289
295
  initAnalytics(analyticsKey || posthogHost ? { key: analyticsKey || undefined, host: posthogHost || undefined } : {});
290
296
  setRuntimeWidgetStyles(widgetStyles);
297
+ setRuntimeS3BaseUrl(configS3BaseUrl || '');
298
+ setRuntimeStripePublishableKey(configStripeKey || '');
299
+ setRuntimeApiBaseUrl(configApiBaseUrl || '');
291
300
  setConfigLoaded(true);
292
301
  setConfigLoading(false);
293
302
  })
294
303
  .catch((err) => {
295
304
  if (cancelled) return;
296
305
  setConfigError(err?.message || 'Failed to load widget configuration.');
306
+ setRuntimeS3BaseUrl('');
307
+ setRuntimeStripePublishableKey('');
308
+ setRuntimeApiBaseUrl('');
297
309
  setConfigLoading(false);
298
310
  });
299
311
  return () => { cancelled = true; };
@@ -16,9 +16,47 @@ const _nativeFetch = typeof fetch === 'function' ? fetch : /* istanbul ignore ne
16
16
 
17
17
  const CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/load-config';
18
18
 
19
- /** In-memory cache: propertyKey → { apiColors, posthogKey } */
19
+ /** In-memory cache: propertyKey → { apiColors, posthogKey, s3BaseUrl, stripePublishableKey } */
20
20
  const _configCache = new Map();
21
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
+
22
60
  /**
23
61
  * Map the /load-config response field names to the internal color keys used
24
62
  * throughout the widget.
@@ -31,12 +69,13 @@ const _configCache = new Map();
31
69
  * widgetCardColor → card
32
70
  */
33
71
  function mapApiColors(data) {
72
+ const cfg = (data && typeof data.CONFIG === 'object' && data.CONFIG) ? data.CONFIG : {};
34
73
  const mapped = {};
35
- if (data.widgetBackground) mapped.background = data.widgetBackground;
36
- if (data.widgetTextColor) mapped.text = data.widgetTextColor;
37
- if (data.primaryColor) mapped.primary = data.primaryColor;
38
- if (data.buttonTextColor) mapped.primaryText = data.buttonTextColor;
39
- 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;
40
79
  return mapped;
41
80
  }
42
81
 
@@ -74,7 +113,7 @@ export function mergeColors(mappedApiColors, installerColors) {
74
113
  * @param {string} propertyKey - The hotel property key / API key.
75
114
  * @param {object|null} installerColors - Optional installer color overrides.
76
115
  * @param {string} [mode] - Pass 'sandbox' to append &mode=sandbox to the request.
77
- * @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 }>}
78
117
  */
79
118
  export async function fetchRuntimeConfig(propertyKey, installerColors = null, mode = null) {
80
119
  if (!propertyKey || !String(propertyKey).trim()) {
@@ -87,9 +126,9 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
87
126
  const cacheKey = isSandbox ? `${key}:sandbox` : key;
88
127
 
89
128
  if (_configCache.has(cacheKey)) {
90
- const { apiColors, posthogKey } = _configCache.get(cacheKey);
129
+ const { apiColors, posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl } = _configCache.get(cacheKey);
91
130
  const colors = mergeColors(apiColors, installerColors);
92
- return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
131
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl };
93
132
  }
94
133
 
95
134
  const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
@@ -101,8 +140,11 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
101
140
  const data = await res.json();
102
141
  const apiColors = mapApiColors(data);
103
142
  const posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
104
- _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 });
105
147
 
106
148
  const colors = mergeColors(apiColors, installerColors);
107
- return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
149
+ return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey, s3BaseUrl, stripePublishableKey, apiBaseUrl };
108
150
  }
@@ -549,6 +549,8 @@ export default {
549
549
  configLoaded: false,
550
550
  configError: null,
551
551
  runtimeWidgetStyles: {},
552
+ runtimeS3BaseUrl: '',
553
+ runtimeStripePublishableKey: '',
552
554
  calendarMonth: new Date().getMonth(),
553
555
  calendarYear: new Date().getFullYear(),
554
556
  pickState: 0,
@@ -590,7 +592,10 @@ export default {
590
592
  return this.propertyBaseUrl || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_PROPERTY_BASE_URL) || '';
591
593
  },
592
594
  effectiveS3BaseUrl() {
593
- 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 || '';
594
599
  },
595
600
  effectivePaymentIntentUrl() {
596
601
  const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
@@ -603,7 +608,7 @@ export default {
603
608
  return String(base).replace(/\/$/, '');
604
609
  },
605
610
  hasStripe() {
606
- return !!(this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function');
611
+ return !!(this.effectiveStripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function');
607
612
  },
608
613
  STEPS() {
609
614
  return this.hasStripe ? [...BASE_STEPS, { key: 'payment', label: 'Payment', num: '05' }] : BASE_STEPS;
@@ -805,19 +810,25 @@ export default {
805
810
  this.configLoading = false;
806
811
  this.configLoaded = false;
807
812
  this.runtimeWidgetStyles = {};
813
+ this.runtimeS3BaseUrl = '';
814
+ this.runtimeStripePublishableKey = '';
808
815
  return;
809
816
  }
810
817
  this.configLoading = true;
811
818
  this.configError = null;
812
819
  this.configLoaded = false;
813
820
  try {
814
- 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);
815
822
  const analyticsKey = this.posthogKey || configPosthogKey || '';
816
823
  initAnalytics(analyticsKey || this.posthogHost ? { key: analyticsKey || undefined, host: this.posthogHost || undefined } : {});
817
824
  this.runtimeWidgetStyles = widgetStyles;
825
+ this.runtimeS3BaseUrl = configS3BaseUrl || '';
826
+ this.runtimeStripePublishableKey = configStripeKey || '';
818
827
  this.configLoaded = true;
819
828
  } catch (err) {
820
829
  this.configError = err?.message || 'Failed to load widget configuration.';
830
+ this.runtimeS3BaseUrl = '';
831
+ this.runtimeStripePublishableKey = '';
821
832
  } finally {
822
833
  this.configLoading = false;
823
834
  }
@@ -866,7 +877,7 @@ export default {
866
877
  pollOnce();
867
878
  },
868
879
  async loadStripePaymentElement() {
869
- 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;
870
881
  const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
871
882
  this.paymentElementReady = false;
872
883
  try {
@@ -878,7 +889,7 @@ export default {
878
889
  this.apiError = 'Payment setup failed: no client secret returned';
879
890
  return;
880
891
  }
881
- const stripe = await loadStripe(this.stripePublishableKey);
892
+ const stripe = await loadStripe(this.effectiveStripePublishableKey);
882
893
  if (!stripe || this.state.step !== 'payment') return;
883
894
  this.stripeInstance = stripe;
884
895
  const elements = stripe.elements({ clientSecret, appearance: { theme: 'flat', variables: { borderRadius: '8px' } } });
@@ -987,14 +998,14 @@ export default {
987
998
  if (!this.canSubmit) return;
988
999
  const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
989
1000
  // Summary step with Stripe: go to payment step (form loads there)
990
- if (this.state.step === 'summary' && this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
1001
+ if (this.state.step === 'summary' && this.effectiveStripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
991
1002
  this.apiError = null;
992
1003
  this.goToStep('payment');
993
1004
  return;
994
1005
  }
995
1006
 
996
1007
  // Payment step: confirm payment
997
- 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) {
998
1009
  this.apiError = null;
999
1010
  this.stripeInstance.confirmPayment({
1000
1011
  elements: this.elementsInstance,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuitee/booking-widget",
3
- "version": "1.0.8",
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",