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