@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 +3 -3
- package/dist/booking-widget-standalone.js +114 -20
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +66 -10
- package/dist/core/booking-api.js +52 -10
- package/dist/core/color-utils.js +1 -1
- package/dist/core/styles.css +1 -1
- package/dist/react/BookingWidget.jsx +24 -7
- package/dist/react/styles.css +1 -1
- package/dist/utils/analytics.js +88 -0
- package/dist/utils/config-service.js +58 -12
- package/dist/vue/BookingWidget.vue +25 -9
- package/dist/vue/styles.css +1 -1
- package/package.json +2 -2
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.
|
|
98
|
-
<script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.
|
|
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.
|
|
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
|
|
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
|
|
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[
|
|
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
|
|
369
|
-
const
|
|
370
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
876
|
-
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
|
-
|
|
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.
|
|
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)
|
|
1248
|
-
if (data.primaryColor)
|
|
1249
|
-
if (data.buttonTextColor)
|
|
1250
|
-
if (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);
|
package/dist/booking-widget.css
CHANGED
package/dist/booking-widget.js
CHANGED
|
@@ -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
|
-
|
|
87
|
-
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
|
-
|
|
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.
|
|
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)
|
|
527
|
-
if (data.primaryColor)
|
|
528
|
-
if (data.buttonTextColor)
|
|
529
|
-
if (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);
|
package/dist/core/booking-api.js
CHANGED
|
@@ -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
|
|
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
|
|
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[
|
|
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
|
|
368
|
-
const
|
|
369
|
-
const
|
|
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
|
|
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
|
|
762
|
+
const res = await _fetch(url, {
|
|
721
763
|
method: 'GET',
|
|
722
764
|
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
|
|
723
765
|
});
|
package/dist/core/color-utils.js
CHANGED
|
@@ -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 +
|
|
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)}%)`;
|
package/dist/core/styles.css
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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; };
|
package/dist/react/styles.css
CHANGED
|
@@ -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)
|
|
33
|
-
if (data.primaryColor)
|
|
34
|
-
if (data.buttonTextColor)
|
|
35
|
-
if (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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
package/dist/vue/styles.css
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuitee/booking-widget",
|
|
3
|
-
"version": "1.0.
|
|
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",
|