@nuitee/booking-widget 1.0.5 → 1.0.7
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 +88 -10
- package/dist/booking-widget.js +87 -6
- package/dist/core/booking-api.js +5 -4
- package/dist/react/BookingWidget.jsx +66 -6
- package/dist/utils/config-service.js +7 -6
- package/dist/vue/BookingWidget.vue +64 -4
- package/package.json +3 -2
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.7/dist/booking-widget.css">
|
|
98
|
+
<script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.7/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.7/dist/booking-widget.css',
|
|
106
106
|
propertyKey: 'your-property-key',
|
|
107
107
|
onOpen: () => console.log('Opened'),
|
|
108
108
|
onClose: () => console.log('Closed'),
|
|
@@ -230,6 +230,7 @@ function createBookingApi(config = {}) {
|
|
|
230
230
|
const url = `${(base || '').replace(/\/$/, '')}${path}`;
|
|
231
231
|
const headers = {
|
|
232
232
|
'Content-Type': 'application/json',
|
|
233
|
+
'Source': 'booking_engine',
|
|
233
234
|
...staticHeaders,
|
|
234
235
|
...getHeaders(),
|
|
235
236
|
};
|
|
@@ -291,8 +292,8 @@ function createBookingApi(config = {}) {
|
|
|
291
292
|
? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
|
|
292
293
|
: `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
|
|
293
294
|
try {
|
|
294
|
-
const getOpts = { method: 'GET' };
|
|
295
|
-
if (propFullUrl.includes('ngrok')) getOpts.headers
|
|
295
|
+
const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
|
|
296
|
+
if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
|
|
296
297
|
const propRes = await fetch(propFullUrl, getOpts);
|
|
297
298
|
if (propRes.ok) {
|
|
298
299
|
const propData = await propRes.json();
|
|
@@ -692,7 +693,7 @@ async function decryptPropertyId(options = {}) {
|
|
|
692
693
|
const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
|
|
693
694
|
const res = await fetch(url, {
|
|
694
695
|
method: 'POST',
|
|
695
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
696
|
+
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
|
|
696
697
|
body: JSON.stringify({ hash: propertyKey }),
|
|
697
698
|
});
|
|
698
699
|
if (!res.ok) throw new Error(res.statusText || 'Failed to decrypt property id');
|
|
@@ -719,7 +720,7 @@ async function fetchBookingEnginePref(options = {}) {
|
|
|
719
720
|
const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
|
|
720
721
|
const res = await fetch(url, {
|
|
721
722
|
method: 'GET',
|
|
722
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
723
|
+
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
|
|
723
724
|
});
|
|
724
725
|
if (!res.ok) throw new Error(res.statusText || 'Failed to fetch config');
|
|
725
726
|
|
|
@@ -906,7 +907,7 @@ if (typeof window !== 'undefined') {
|
|
|
906
907
|
const url = defaultApiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (options.mode === 'sandbox' ? '?sandbox=true' : '');
|
|
907
908
|
return fetch(url, {
|
|
908
909
|
method: 'POST',
|
|
909
|
-
headers: { 'Content-Type': 'application/json' },
|
|
910
|
+
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine' },
|
|
910
911
|
body: JSON.stringify(payload),
|
|
911
912
|
}).then(function (r) {
|
|
912
913
|
if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
|
|
@@ -935,6 +936,8 @@ if (typeof window !== 'undefined') {
|
|
|
935
936
|
mode: options.mode || null,
|
|
936
937
|
bookingApi: options.bookingApi || null,
|
|
937
938
|
cssUrl: options.cssUrl || null,
|
|
939
|
+
posthogKey: options.posthogKey || null,
|
|
940
|
+
posthogHost: options.posthogHost || 'https://us.i.posthog.com',
|
|
938
941
|
// Color customization: CONFIG defaults from load-config, installer colors override
|
|
939
942
|
colors: (function () {
|
|
940
943
|
const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
|
|
@@ -1045,6 +1048,25 @@ if (typeof window !== 'undefined') {
|
|
|
1045
1048
|
return this.STEPS.findIndex(s => s.key === key);
|
|
1046
1049
|
}
|
|
1047
1050
|
|
|
1051
|
+
_capture(eventName, properties) {
|
|
1052
|
+
if (typeof window !== 'undefined' && window.posthog && typeof window.posthog.capture === 'function') {
|
|
1053
|
+
try {
|
|
1054
|
+
const mode = this.options.mode === 'sandbox' ? 'sandbox' : 'live';
|
|
1055
|
+
const propertyKey = this.options.propertyKey != null ? String(this.options.propertyKey) : undefined;
|
|
1056
|
+
const propertyId = this.options.propertyId != null && this.options.propertyId !== '' ? String(this.options.propertyId) : undefined;
|
|
1057
|
+
window.posthog.capture(eventName, { ...(properties || {}), mode, propertyKey, propertyId });
|
|
1058
|
+
} catch (e) { /* noop */ }
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
_identify() {
|
|
1063
|
+
if (typeof window !== 'undefined' && this.options.propertyKey && window.posthog && typeof window.posthog.identify === 'function') {
|
|
1064
|
+
try {
|
|
1065
|
+
window.posthog.identify(String(this.options.propertyKey).trim());
|
|
1066
|
+
} catch (e) { /* noop */ }
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1048
1070
|
open() {
|
|
1049
1071
|
if (!this.container) this.init();
|
|
1050
1072
|
if (!this.overlay || !this.widget) return; // container element not found
|
|
@@ -1055,6 +1077,7 @@ if (typeof window !== 'undefined') {
|
|
|
1055
1077
|
this._fetchRuntimeConfig();
|
|
1056
1078
|
}
|
|
1057
1079
|
this.render();
|
|
1080
|
+
this._capture('widgetOpened');
|
|
1058
1081
|
if (this.options.onOpen) this.options.onOpen();
|
|
1059
1082
|
}
|
|
1060
1083
|
|
|
@@ -1111,6 +1134,12 @@ if (typeof window !== 'undefined') {
|
|
|
1111
1134
|
this.bookingApi.fetchRooms(params).then((rooms) => {
|
|
1112
1135
|
this.ROOMS = Array.isArray(rooms) ? rooms : [];
|
|
1113
1136
|
this.loadingRooms = false;
|
|
1137
|
+
this._capture('search', {
|
|
1138
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
1139
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
1140
|
+
rooms: this.state.rooms,
|
|
1141
|
+
occupancies: this.state.occupancies,
|
|
1142
|
+
});
|
|
1114
1143
|
this.render();
|
|
1115
1144
|
}).catch((err) => {
|
|
1116
1145
|
this.apiError = err.message || 'Failed to load rooms';
|
|
@@ -1195,17 +1224,19 @@ if (typeof window !== 'undefined') {
|
|
|
1195
1224
|
const cacheKey = isSandbox ? key + ':sandbox' : key;
|
|
1196
1225
|
|
|
1197
1226
|
if (__bwConfigCache[cacheKey]) {
|
|
1198
|
-
|
|
1227
|
+
const cached = __bwConfigCache[cacheKey];
|
|
1228
|
+
this._applyApiColors(cached);
|
|
1199
1229
|
this._configState = 'loaded';
|
|
1200
1230
|
this.applyColors();
|
|
1201
|
-
|
|
1231
|
+
this._initPosthog(cached._posthogKey || '');
|
|
1232
|
+
return Promise.resolve(cached);
|
|
1202
1233
|
}
|
|
1203
1234
|
|
|
1204
1235
|
this._configState = 'loading';
|
|
1205
1236
|
this.render();
|
|
1206
1237
|
|
|
1207
1238
|
const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
|
|
1208
|
-
this._configPromise = fetch(url)
|
|
1239
|
+
this._configPromise = fetch(url, { headers: { 'Source': 'booking_engine' } })
|
|
1209
1240
|
.then(function (res) {
|
|
1210
1241
|
if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
|
|
1211
1242
|
return res.json();
|
|
@@ -1217,10 +1248,12 @@ if (typeof window !== 'undefined') {
|
|
|
1217
1248
|
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
1218
1249
|
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
1219
1250
|
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
1251
|
+
apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
|
|
1220
1252
|
__bwConfigCache[cacheKey] = apiColors;
|
|
1221
1253
|
self._applyApiColors(apiColors);
|
|
1222
1254
|
self._configState = 'loaded';
|
|
1223
1255
|
self.applyColors();
|
|
1256
|
+
self._initPosthog(apiColors._posthogKey);
|
|
1224
1257
|
self.render();
|
|
1225
1258
|
return apiColors;
|
|
1226
1259
|
})
|
|
@@ -1233,6 +1266,26 @@ if (typeof window !== 'undefined') {
|
|
|
1233
1266
|
return this._configPromise;
|
|
1234
1267
|
}
|
|
1235
1268
|
|
|
1269
|
+
_initPosthog(configKey) {
|
|
1270
|
+
const key = this.options.posthogKey || configKey || '';
|
|
1271
|
+
if (!key || typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
1272
|
+
if (this._posthogInited) return;
|
|
1273
|
+
try {
|
|
1274
|
+
// Inject the posthog loader snippet if posthog has not been loaded yet.
|
|
1275
|
+
// This makes the standalone bundle self-sufficient — no CDN <script> needed from the host.
|
|
1276
|
+
if (!window.posthog || !window.posthog.__SV) {
|
|
1277
|
+
!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||[]);
|
|
1278
|
+
}
|
|
1279
|
+
window.posthog.init(key, { api_host: this.options.posthogHost || 'https://us.i.posthog.com' });
|
|
1280
|
+
this._posthogInited = true;
|
|
1281
|
+
this._identify();
|
|
1282
|
+
this._capture('widgetLoaded');
|
|
1283
|
+
if (this.overlay && this.overlay.classList.contains('active')) {
|
|
1284
|
+
this._capture('widgetOpened');
|
|
1285
|
+
}
|
|
1286
|
+
} catch (e) { /* noop */ }
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1236
1289
|
_retryConfigFetch() {
|
|
1237
1290
|
this._configState = 'idle';
|
|
1238
1291
|
this._configError = null;
|
|
@@ -1682,7 +1735,9 @@ if (typeof window !== 'undefined') {
|
|
|
1682
1735
|
const grid = this.widget.querySelector('.room-grid');
|
|
1683
1736
|
const scrollLeft = grid ? grid.scrollLeft : 0;
|
|
1684
1737
|
if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
|
|
1685
|
-
|
|
1738
|
+
const room = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1739
|
+
this.state.selectedRoom = room;
|
|
1740
|
+
this._capture('selectedRoom', { roomId: id, roomName: room && room.name });
|
|
1686
1741
|
this.render();
|
|
1687
1742
|
const restoreScroll = () => {
|
|
1688
1743
|
const newGrid = this.widget.querySelector('.room-grid');
|
|
@@ -1742,6 +1797,7 @@ if (typeof window !== 'undefined') {
|
|
|
1742
1797
|
|
|
1743
1798
|
selectRate(id) {
|
|
1744
1799
|
this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1800
|
+
this._capture('selectedRate', { rateId: id });
|
|
1745
1801
|
this.render();
|
|
1746
1802
|
}
|
|
1747
1803
|
|
|
@@ -1974,6 +2030,16 @@ if (typeof window !== 'undefined') {
|
|
|
1974
2030
|
.then((res) => {
|
|
1975
2031
|
this.confirmationCode = (res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code)) || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
|
|
1976
2032
|
this.state.step = 'confirmation';
|
|
2033
|
+
this._capture('booked', {
|
|
2034
|
+
confirmationCode: this.confirmationCode,
|
|
2035
|
+
guest: this.state.guest,
|
|
2036
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
2037
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
2038
|
+
roomName: this.state.selectedRoom && this.state.selectedRoom.name,
|
|
2039
|
+
roomId: this.state.selectedRoom && this.state.selectedRoom.id,
|
|
2040
|
+
rateId: this.state.selectedRate && this.state.selectedRate.id,
|
|
2041
|
+
currency: this.state.selectedRoom && this.state.selectedRoom.currency,
|
|
2042
|
+
});
|
|
1977
2043
|
this.render();
|
|
1978
2044
|
})
|
|
1979
2045
|
.catch((err) => {
|
|
@@ -1991,7 +2057,7 @@ if (typeof window !== 'undefined') {
|
|
|
1991
2057
|
if (!token) throw new Error('Missing confirmation token');
|
|
1992
2058
|
const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
|
|
1993
2059
|
const url = base + '/proxy/confirmation/' + encodeURIComponent(token) + (this.options.mode === 'sandbox' ? '?sandbox=true' : '');
|
|
1994
|
-
const res = await fetch(url, { method: 'POST' });
|
|
2060
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
|
|
1995
2061
|
if (!res.ok) throw new Error(await res.text());
|
|
1996
2062
|
return await res.json();
|
|
1997
2063
|
}
|
|
@@ -2017,6 +2083,18 @@ if (typeof window !== 'undefined') {
|
|
|
2017
2083
|
self.confirmationStatus = status || self.confirmationStatus || 'pending';
|
|
2018
2084
|
if (status === 'confirmed') {
|
|
2019
2085
|
self.confirmationDetails = data;
|
|
2086
|
+
self._capture('booked', {
|
|
2087
|
+
confirmationCode: data && (data.confirmationCode ?? data.confirmation_code ?? data.bookingId ?? data.booking_id),
|
|
2088
|
+
bookingId: data && (data.bookingId ?? data.booking_id),
|
|
2089
|
+
guest: self.state.guest,
|
|
2090
|
+
checkIn: self.state.checkIn && self.state.checkIn.toISOString ? self.state.checkIn.toISOString() : undefined,
|
|
2091
|
+
checkOut: self.state.checkOut && self.state.checkOut.toISOString ? self.state.checkOut.toISOString() : undefined,
|
|
2092
|
+
roomName: self.state.selectedRoom && self.state.selectedRoom.name,
|
|
2093
|
+
roomId: self.state.selectedRoom && self.state.selectedRoom.id,
|
|
2094
|
+
rateId: self.state.selectedRate && self.state.selectedRate.id,
|
|
2095
|
+
totalAmount: data && (data.totalAmount ?? data.total_amount),
|
|
2096
|
+
currency: (data && data.currency) || (self.state.selectedRoom && self.state.selectedRoom.currency),
|
|
2097
|
+
});
|
|
2020
2098
|
self.confirmationPolling = false;
|
|
2021
2099
|
self.render();
|
|
2022
2100
|
return;
|
package/dist/booking-widget.js
CHANGED
|
@@ -122,7 +122,7 @@ class BookingWidget {
|
|
|
122
122
|
const url = apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (builtInMode === 'sandbox' ? '?sandbox=true' : '');
|
|
123
123
|
return fetch(url, {
|
|
124
124
|
method: 'POST',
|
|
125
|
-
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine' },
|
|
126
126
|
body: JSON.stringify(payload),
|
|
127
127
|
}).then(function (r) {
|
|
128
128
|
if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
|
|
@@ -160,6 +160,10 @@ class BookingWidget {
|
|
|
160
160
|
s3BaseUrl: options.s3BaseUrl || null,
|
|
161
161
|
/** Base URL for confirmation status polling (same as API_BASE_URL). Default https://ai.thehotelplanet.com */
|
|
162
162
|
confirmationBaseUrl: options.confirmationBaseUrl || 'https://ai.thehotelplanet.com',
|
|
163
|
+
/** PostHog: project API key (host page must load posthog-js and set window.posthog). */
|
|
164
|
+
posthogKey: options.posthogKey || null,
|
|
165
|
+
/** PostHog: API host (e.g. https://us.i.posthog.com). */
|
|
166
|
+
posthogHost: options.posthogHost || 'https://us.i.posthog.com',
|
|
163
167
|
// Color customization: CONFIG defaults from load-config, installer colors override
|
|
164
168
|
colors: (function () {
|
|
165
169
|
const dc = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_DEFAULT_COLORS__) ? window.__BOOKING_WIDGET_DEFAULT_COLORS__ : {};
|
|
@@ -242,6 +246,25 @@ class BookingWidget {
|
|
|
242
246
|
this.widget = null;
|
|
243
247
|
}
|
|
244
248
|
|
|
249
|
+
_capture(eventName, properties) {
|
|
250
|
+
if (typeof window !== 'undefined' && window.posthog && typeof window.posthog.capture === 'function') {
|
|
251
|
+
try {
|
|
252
|
+
const mode = this.options.mode === 'sandbox' ? 'sandbox' : 'live';
|
|
253
|
+
const propertyKey = this.options.propertyKey != null ? String(this.options.propertyKey) : undefined;
|
|
254
|
+
const propertyId = this.options.propertyId != null && this.options.propertyId !== '' ? String(this.options.propertyId) : undefined;
|
|
255
|
+
window.posthog.capture(eventName, { ...(properties || {}), mode, propertyKey, propertyId });
|
|
256
|
+
} catch (e) { /* noop */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_identify() {
|
|
261
|
+
if (typeof window !== 'undefined' && this.options.propertyKey && window.posthog && typeof window.posthog.identify === 'function') {
|
|
262
|
+
try {
|
|
263
|
+
window.posthog.identify(String(this.options.propertyKey).trim());
|
|
264
|
+
} catch (e) { /* noop */ }
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
245
268
|
// ===== Helpers =====
|
|
246
269
|
getNights() {
|
|
247
270
|
if (!this.state.checkIn || !this.state.checkOut) return 0;
|
|
@@ -305,6 +328,7 @@ class BookingWidget {
|
|
|
305
328
|
this._fetchRuntimeConfig();
|
|
306
329
|
}
|
|
307
330
|
this.render();
|
|
331
|
+
this._capture('widgetOpened');
|
|
308
332
|
if (this.options.onOpen) this.options.onOpen();
|
|
309
333
|
}
|
|
310
334
|
|
|
@@ -374,6 +398,12 @@ class BookingWidget {
|
|
|
374
398
|
this.bookingApi.fetchRooms(params).then((rooms) => {
|
|
375
399
|
this.ROOMS = Array.isArray(rooms) ? rooms : [];
|
|
376
400
|
this.loadingRooms = false;
|
|
401
|
+
this._capture('search', {
|
|
402
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
403
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
404
|
+
rooms: this.state.rooms,
|
|
405
|
+
occupancies: this.state.occupancies,
|
|
406
|
+
});
|
|
377
407
|
this.render();
|
|
378
408
|
}).catch((err) => {
|
|
379
409
|
this.apiError = err.message || 'Failed to load rooms';
|
|
@@ -473,17 +503,19 @@ class BookingWidget {
|
|
|
473
503
|
const cacheKey = isSandbox ? key + ':sandbox' : key;
|
|
474
504
|
|
|
475
505
|
if (__bwConfigCache[cacheKey]) {
|
|
476
|
-
|
|
506
|
+
const cached = __bwConfigCache[cacheKey];
|
|
507
|
+
this._applyApiColors(cached);
|
|
477
508
|
this._configState = 'loaded';
|
|
478
509
|
this.applyColors();
|
|
479
|
-
|
|
510
|
+
this._initPosthog(cached._posthogKey || '');
|
|
511
|
+
return Promise.resolve(cached);
|
|
480
512
|
}
|
|
481
513
|
|
|
482
514
|
this._configState = 'loading';
|
|
483
515
|
this.render();
|
|
484
516
|
|
|
485
517
|
const url = 'https://ai.thehotelplanet.com/load-config?apikey=' + encodeURIComponent(key) + (isSandbox ? '&mode=sandbox' : '');
|
|
486
|
-
this._configPromise = fetch(url)
|
|
518
|
+
this._configPromise = fetch(url, { headers: { 'Source': 'booking_engine' } })
|
|
487
519
|
.then(function (res) {
|
|
488
520
|
if (!res.ok) throw new Error('Failed to load widget configuration (HTTP ' + res.status + ').');
|
|
489
521
|
return res.json();
|
|
@@ -495,10 +527,12 @@ class BookingWidget {
|
|
|
495
527
|
if (data.primaryColor) apiColors.primary = data.primaryColor;
|
|
496
528
|
if (data.buttonTextColor) apiColors.primaryText = data.buttonTextColor;
|
|
497
529
|
if (data.widgetCardColor) apiColors.card = data.widgetCardColor;
|
|
530
|
+
apiColors._posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
|
|
498
531
|
__bwConfigCache[cacheKey] = apiColors;
|
|
499
532
|
self._applyApiColors(apiColors);
|
|
500
533
|
self._configState = 'loaded';
|
|
501
534
|
self.applyColors();
|
|
535
|
+
self._initPosthog(apiColors._posthogKey);
|
|
502
536
|
self.render();
|
|
503
537
|
return apiColors;
|
|
504
538
|
})
|
|
@@ -511,6 +545,28 @@ class BookingWidget {
|
|
|
511
545
|
return this._configPromise;
|
|
512
546
|
}
|
|
513
547
|
|
|
548
|
+
_initPosthog(configKey) {
|
|
549
|
+
const key = this.options.posthogKey || configKey || '';
|
|
550
|
+
if (!key || typeof window === 'undefined' || typeof document === 'undefined') return;
|
|
551
|
+
if (this._posthogInited) return;
|
|
552
|
+
try {
|
|
553
|
+
// Inject the posthog loader snippet if posthog has not been loaded yet.
|
|
554
|
+
// This makes the standalone version self-sufficient — no CDN <script> needed from the host.
|
|
555
|
+
if (!window.posthog || !window.posthog.__SV) {
|
|
556
|
+
/* eslint-disable */
|
|
557
|
+
!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||[]);
|
|
558
|
+
/* eslint-enable */
|
|
559
|
+
}
|
|
560
|
+
window.posthog.init(key, { api_host: this.options.posthogHost || 'https://us.i.posthog.com' });
|
|
561
|
+
this._posthogInited = true;
|
|
562
|
+
this._identify();
|
|
563
|
+
this._capture('widgetLoaded');
|
|
564
|
+
if (this.overlay && this.overlay.classList.contains('active')) {
|
|
565
|
+
this._capture('widgetOpened');
|
|
566
|
+
}
|
|
567
|
+
} catch (e) { /* noop */ }
|
|
568
|
+
}
|
|
569
|
+
|
|
514
570
|
injectCSS() {
|
|
515
571
|
if (!document.getElementById('booking-widget-styles')) {
|
|
516
572
|
const link = document.createElement('link');
|
|
@@ -955,7 +1011,9 @@ class BookingWidget {
|
|
|
955
1011
|
const grid = this.widget.querySelector('.room-grid');
|
|
956
1012
|
const scrollLeft = grid ? grid.scrollLeft : 0;
|
|
957
1013
|
if (document.activeElement && document.activeElement.blur) document.activeElement.blur();
|
|
958
|
-
|
|
1014
|
+
const room = this.ROOMS.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1015
|
+
this.state.selectedRoom = room;
|
|
1016
|
+
this._capture('selectedRoom', { roomId: id, roomName: room && room.name });
|
|
959
1017
|
this.render();
|
|
960
1018
|
const restoreScroll = () => {
|
|
961
1019
|
const newGrid = this.widget.querySelector('.room-grid');
|
|
@@ -1049,6 +1107,7 @@ class BookingWidget {
|
|
|
1049
1107
|
|
|
1050
1108
|
selectRate(id) {
|
|
1051
1109
|
this.state.selectedRate = this.RATES.find(r => r.id == id || String(r.id) === String(id)) || null;
|
|
1110
|
+
this._capture('selectedRate', { rateId: id });
|
|
1052
1111
|
this.render();
|
|
1053
1112
|
}
|
|
1054
1113
|
|
|
@@ -1306,6 +1365,16 @@ class BookingWidget {
|
|
|
1306
1365
|
const code = res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code);
|
|
1307
1366
|
this.confirmationCode = code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
|
|
1308
1367
|
this.state.step = 'confirmation';
|
|
1368
|
+
this._capture('booked', {
|
|
1369
|
+
confirmationCode: this.confirmationCode,
|
|
1370
|
+
guest: this.state.guest,
|
|
1371
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
1372
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
1373
|
+
roomName: this.state.selectedRoom && this.state.selectedRoom.name,
|
|
1374
|
+
roomId: this.state.selectedRoom && this.state.selectedRoom.id,
|
|
1375
|
+
rateId: this.state.selectedRate && this.state.selectedRate.id,
|
|
1376
|
+
currency: this.state.selectedRoom && this.state.selectedRoom.currency,
|
|
1377
|
+
});
|
|
1309
1378
|
this.render();
|
|
1310
1379
|
})
|
|
1311
1380
|
.catch((err) => {
|
|
@@ -1407,7 +1476,7 @@ class BookingWidget {
|
|
|
1407
1476
|
if (!token) throw new Error('Missing confirmation token');
|
|
1408
1477
|
const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
|
|
1409
1478
|
const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}${this.options.mode === 'sandbox' ? '?sandbox=true' : ''}`;
|
|
1410
|
-
const res = await fetch(url, { method: 'POST' });
|
|
1479
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
|
|
1411
1480
|
if (!res.ok) throw new Error(await res.text());
|
|
1412
1481
|
return await res.json();
|
|
1413
1482
|
}
|
|
@@ -1431,6 +1500,18 @@ class BookingWidget {
|
|
|
1431
1500
|
this.confirmationStatus = status || this.confirmationStatus || 'pending';
|
|
1432
1501
|
if (status === 'confirmed') {
|
|
1433
1502
|
this.confirmationDetails = data;
|
|
1503
|
+
this._capture('booked', {
|
|
1504
|
+
confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
|
|
1505
|
+
bookingId: data?.bookingId ?? data?.booking_id,
|
|
1506
|
+
guest: this.state.guest,
|
|
1507
|
+
checkIn: this.state.checkIn && this.state.checkIn.toISOString ? this.state.checkIn.toISOString() : undefined,
|
|
1508
|
+
checkOut: this.state.checkOut && this.state.checkOut.toISOString ? this.state.checkOut.toISOString() : undefined,
|
|
1509
|
+
roomName: this.state.selectedRoom && this.state.selectedRoom.name,
|
|
1510
|
+
roomId: this.state.selectedRoom && this.state.selectedRoom.id,
|
|
1511
|
+
rateId: this.state.selectedRate && this.state.selectedRate.id,
|
|
1512
|
+
totalAmount: data?.totalAmount ?? data?.total_amount,
|
|
1513
|
+
currency: data?.currency || (this.state.selectedRoom && this.state.selectedRoom.currency),
|
|
1514
|
+
});
|
|
1434
1515
|
this.confirmationPolling = false;
|
|
1435
1516
|
this.render();
|
|
1436
1517
|
return;
|
package/dist/core/booking-api.js
CHANGED
|
@@ -229,6 +229,7 @@ function createBookingApi(config = {}) {
|
|
|
229
229
|
const url = `${(base || '').replace(/\/$/, '')}${path}`;
|
|
230
230
|
const headers = {
|
|
231
231
|
'Content-Type': 'application/json',
|
|
232
|
+
'Source': 'booking_engine',
|
|
232
233
|
...staticHeaders,
|
|
233
234
|
...getHeaders(),
|
|
234
235
|
};
|
|
@@ -290,8 +291,8 @@ function createBookingApi(config = {}) {
|
|
|
290
291
|
? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
|
|
291
292
|
: `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
|
|
292
293
|
try {
|
|
293
|
-
const getOpts = { method: 'GET' };
|
|
294
|
-
if (propFullUrl.includes('ngrok')) getOpts.headers
|
|
294
|
+
const getOpts = { method: 'GET', headers: { 'Source': 'booking_engine' } };
|
|
295
|
+
if (propFullUrl.includes('ngrok')) getOpts.headers['ngrok-skip-browser-warning'] = 'true';
|
|
295
296
|
const propRes = await fetch(propFullUrl, getOpts);
|
|
296
297
|
if (propRes.ok) {
|
|
297
298
|
const propData = await propRes.json();
|
|
@@ -691,7 +692,7 @@ async function decryptPropertyId(options = {}) {
|
|
|
691
692
|
const url = `${base}/${locale}/booking_engine/decrypt_property_id`;
|
|
692
693
|
const res = await fetch(url, {
|
|
693
694
|
method: 'POST',
|
|
694
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
695
|
+
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
|
|
695
696
|
body: JSON.stringify({ hash: propertyKey }),
|
|
696
697
|
});
|
|
697
698
|
if (!res.ok) throw new Error(res.statusText || 'Failed to decrypt property id');
|
|
@@ -718,7 +719,7 @@ async function fetchBookingEnginePref(options = {}) {
|
|
|
718
719
|
const url = `${base}/api/core/${locale}/booking_engine/booking_engine_pref?property_id=${encodeURIComponent(propertyId)}`;
|
|
719
720
|
const res = await fetch(url, {
|
|
720
721
|
method: 'GET',
|
|
721
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
722
|
+
headers: { 'Content-Type': 'application/json', 'Source': 'booking_engine', ...headers },
|
|
722
723
|
});
|
|
723
724
|
if (!res.ok) throw new Error(res.statusText || 'Failed to fetch config');
|
|
724
725
|
|
|
@@ -5,6 +5,7 @@ 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';
|
|
8
9
|
|
|
9
10
|
const BASE_STEPS = [
|
|
10
11
|
{ key: 'dates', label: 'Dates & Guests', num: '01' },
|
|
@@ -45,6 +46,10 @@ const BookingWidget = ({
|
|
|
45
46
|
createPaymentIntent: createPaymentIntentProp = null,
|
|
46
47
|
/** Stripe: deprecated (booking completion is fetched from confirmation endpoint instead). */
|
|
47
48
|
onBookingComplete: onBookingCompleteProp = null,
|
|
49
|
+
/** PostHog: optional override for analytics key (defaults to VITE_POSTHOG_KEY). */
|
|
50
|
+
posthogKey = '',
|
|
51
|
+
/** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
|
|
52
|
+
posthogHost = '',
|
|
48
53
|
}) => {
|
|
49
54
|
const stripePublishableKey = stripePublishableKeyProp || STRIPE_PUBLISHABLE_KEY;
|
|
50
55
|
const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
|
|
@@ -57,7 +62,7 @@ const BookingWidget = ({
|
|
|
57
62
|
if (!effectivePaymentIntentUrl) return null;
|
|
58
63
|
return async (payload) => {
|
|
59
64
|
const url = effectivePaymentIntentUrl + (isSandbox ? '?sandbox=true' : '');
|
|
60
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
65
|
+
const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
|
|
61
66
|
if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
|
|
62
67
|
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
|
|
63
68
|
if (!res.ok) throw new Error(await res.text());
|
|
@@ -134,9 +139,26 @@ const BookingWidget = ({
|
|
|
134
139
|
return null;
|
|
135
140
|
}, [bookingApiProp, effectiveApiBaseUrl, effectiveAvailabilityBaseUrl, effectivePropertyBaseUrl, effectiveS3BaseUrl, apiSecret, propertyId, propertyKey, mode]);
|
|
136
141
|
|
|
142
|
+
|
|
143
|
+
const analyticsMode = isSandbox ? 'sandbox' : 'live';
|
|
144
|
+
const analyticsContext = {
|
|
145
|
+
mode: analyticsMode,
|
|
146
|
+
propertyKey: propertyKey || undefined,
|
|
147
|
+
propertyId: propertyId != null && propertyId !== '' ? String(propertyId) : undefined,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Track widgetLoaded when config has loaded (widget is ready). Identify by propertyKey.
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (hasPropertyKey && configLoaded) {
|
|
153
|
+
identifyAnalytics(propertyKey);
|
|
154
|
+
captureEvent('widgetLoaded', analyticsContext);
|
|
155
|
+
}
|
|
156
|
+
}, [hasPropertyKey, configLoaded, propertyKey, propertyId, analyticsMode]);
|
|
157
|
+
|
|
137
158
|
useEffect(() => {
|
|
138
159
|
if (isOpen && onOpen) onOpen();
|
|
139
|
-
|
|
160
|
+
if (isOpen) captureEvent('widgetOpened', analyticsContext);
|
|
161
|
+
}, [isOpen, onOpen, analyticsMode]);
|
|
140
162
|
|
|
141
163
|
// Delay adding 'active' by one frame so the browser can paint the initial state and animate open
|
|
142
164
|
useEffect(() => {
|
|
@@ -205,13 +227,28 @@ const BookingWidget = ({
|
|
|
205
227
|
if (cancelled) return;
|
|
206
228
|
try {
|
|
207
229
|
const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}${isSandbox ? '?sandbox=true' : ''}`;
|
|
208
|
-
const res = await fetch(url, { method: 'POST' });
|
|
230
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
|
|
209
231
|
if (!res.ok) throw new Error(await res.text());
|
|
210
232
|
const data = await res.json();
|
|
211
233
|
const status = data?.status != null ? String(data.status) : '';
|
|
212
234
|
if (!cancelled) setConfirmationStatus(status || 'pending');
|
|
213
235
|
if (status === 'confirmed') {
|
|
214
|
-
if (!cancelled)
|
|
236
|
+
if (!cancelled) {
|
|
237
|
+
setConfirmationDetails(data);
|
|
238
|
+
captureEvent('booked', {
|
|
239
|
+
...analyticsContext,
|
|
240
|
+
confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
|
|
241
|
+
bookingId: data?.bookingId ?? data?.booking_id,
|
|
242
|
+
guest: state.guest,
|
|
243
|
+
checkIn: state.checkIn?.toISOString?.(),
|
|
244
|
+
checkOut: state.checkOut?.toISOString?.(),
|
|
245
|
+
roomName: state.selectedRoom?.name,
|
|
246
|
+
roomId: state.selectedRoom?.id,
|
|
247
|
+
rateId: state.selectedRate?.id,
|
|
248
|
+
totalAmount: data?.totalAmount ?? data?.total_amount,
|
|
249
|
+
currency: data?.currency ?? state.selectedRoom?.currency,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
215
252
|
return;
|
|
216
253
|
}
|
|
217
254
|
} catch (err) {
|
|
@@ -241,8 +278,10 @@ const BookingWidget = ({
|
|
|
241
278
|
setConfigError(null);
|
|
242
279
|
setConfigLoaded(false);
|
|
243
280
|
fetchRuntimeConfig(propertyKey, colors, mode)
|
|
244
|
-
.then(({ widgetStyles }) => {
|
|
281
|
+
.then(({ widgetStyles, posthogKey: configPosthogKey }) => {
|
|
245
282
|
if (cancelled) return;
|
|
283
|
+
const analyticsKey = posthogKey || configPosthogKey || '';
|
|
284
|
+
initAnalytics(analyticsKey || posthogHost ? { key: analyticsKey || undefined, host: posthogHost || undefined } : {});
|
|
246
285
|
setRuntimeWidgetStyles(widgetStyles);
|
|
247
286
|
setConfigLoaded(true);
|
|
248
287
|
setConfigLoading(false);
|
|
@@ -319,6 +358,13 @@ const BookingWidget = ({
|
|
|
319
358
|
api.fetchRooms(params).then((rooms) => {
|
|
320
359
|
setRoomsList(Array.isArray(rooms) ? rooms : []);
|
|
321
360
|
setLoadingRooms(false);
|
|
361
|
+
captureEvent('search', {
|
|
362
|
+
...analyticsContext,
|
|
363
|
+
checkIn: state.checkIn?.toISOString?.(),
|
|
364
|
+
checkOut: state.checkOut?.toISOString?.(),
|
|
365
|
+
rooms: state.rooms,
|
|
366
|
+
occupancies: state.occupancies,
|
|
367
|
+
});
|
|
322
368
|
}).catch((err) => {
|
|
323
369
|
setApiError(err.message || 'Failed to load rooms');
|
|
324
370
|
setLoadingRooms(false);
|
|
@@ -443,11 +489,14 @@ const BookingWidget = ({
|
|
|
443
489
|
};
|
|
444
490
|
|
|
445
491
|
const selectRoom = (id) => {
|
|
446
|
-
|
|
492
|
+
const room = roomsList.find(r => r.id === id);
|
|
493
|
+
setState(prev => ({ ...prev, selectedRoom: room }));
|
|
494
|
+
captureEvent('selectedRoom', { ...analyticsContext, roomId: id, roomName: room?.name });
|
|
447
495
|
};
|
|
448
496
|
|
|
449
497
|
const selectRate = (id) => {
|
|
450
498
|
setState(prev => ({ ...prev, selectedRate: ratesList.find(r => r.id === id) }));
|
|
499
|
+
captureEvent('selectedRate', { ...analyticsContext, rateId: id });
|
|
451
500
|
};
|
|
452
501
|
|
|
453
502
|
const updateGuest = (field, value) => {
|
|
@@ -502,6 +551,17 @@ const BookingWidget = ({
|
|
|
502
551
|
const code = res && (res.confirmationCode ?? res.confirmation_code);
|
|
503
552
|
setConfirmationCode(code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6)));
|
|
504
553
|
setState(prev => ({ ...prev, step: 'confirmation' }));
|
|
554
|
+
captureEvent('booked', {
|
|
555
|
+
...analyticsContext,
|
|
556
|
+
confirmationCode: code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6)),
|
|
557
|
+
guest: state.guest,
|
|
558
|
+
checkIn: state.checkIn?.toISOString?.(),
|
|
559
|
+
checkOut: state.checkOut?.toISOString?.(),
|
|
560
|
+
roomName: state.selectedRoom?.name,
|
|
561
|
+
roomId: state.selectedRoom?.id,
|
|
562
|
+
rateId: state.selectedRate?.id,
|
|
563
|
+
currency: state.selectedRoom?.currency,
|
|
564
|
+
});
|
|
505
565
|
})
|
|
506
566
|
.catch((err) => {
|
|
507
567
|
setApiError(err?.message || err || 'Booking failed');
|
|
@@ -12,7 +12,7 @@ import { deriveWidgetStyles } from '../core/color-utils.js';
|
|
|
12
12
|
|
|
13
13
|
const CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/load-config';
|
|
14
14
|
|
|
15
|
-
/** In-memory cache: propertyKey →
|
|
15
|
+
/** In-memory cache: propertyKey → { apiColors, posthogKey } */
|
|
16
16
|
const _configCache = new Map();
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -83,21 +83,22 @@ export async function fetchRuntimeConfig(propertyKey, installerColors = null, mo
|
|
|
83
83
|
const cacheKey = isSandbox ? `${key}:sandbox` : key;
|
|
84
84
|
|
|
85
85
|
if (_configCache.has(cacheKey)) {
|
|
86
|
-
const apiColors = _configCache.get(cacheKey);
|
|
86
|
+
const { apiColors, posthogKey } = _configCache.get(cacheKey);
|
|
87
87
|
const colors = mergeColors(apiColors, installerColors);
|
|
88
|
-
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
|
|
88
|
+
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
const url = `${CONFIG_BASE_URL}?apikey=${encodeURIComponent(key)}${isSandbox ? '&mode=sandbox' : ''}`;
|
|
92
|
-
const res = await fetch(url);
|
|
92
|
+
const res = await fetch(url, { headers: { 'Source': 'booking_engine' } });
|
|
93
93
|
if (!res.ok) {
|
|
94
94
|
throw new Error(`Failed to load widget configuration (HTTP ${res.status}).`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const data = await res.json();
|
|
98
98
|
const apiColors = mapApiColors(data);
|
|
99
|
-
|
|
99
|
+
const posthogKey = ((data.posthogKey || data.POSTHOG_KEY) ?? '').trim();
|
|
100
|
+
_configCache.set(cacheKey, { apiColors, posthogKey });
|
|
100
101
|
|
|
101
102
|
const colors = mergeColors(apiColors, installerColors);
|
|
102
|
-
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors) };
|
|
103
|
+
return { apiColors, colors, widgetStyles: deriveWidgetStyles(colors), posthogKey };
|
|
103
104
|
}
|
|
@@ -482,6 +482,7 @@ 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';
|
|
485
486
|
|
|
486
487
|
|
|
487
488
|
const BASE_STEPS = [
|
|
@@ -525,6 +526,10 @@ export default {
|
|
|
525
526
|
createPaymentIntent: { type: Function, default: null },
|
|
526
527
|
/** Stripe: deprecated (booking completion is fetched from confirmation endpoint instead). */
|
|
527
528
|
onBookingComplete: { type: Function, default: null },
|
|
529
|
+
/** PostHog: optional override for analytics key (defaults to VITE_POSTHOG_KEY). */
|
|
530
|
+
posthogKey: { type: String, default: '' },
|
|
531
|
+
/** PostHog: optional override for API host (defaults to VITE_POSTHOG_HOST). */
|
|
532
|
+
posthogHost: { type: String, default: '' },
|
|
528
533
|
},
|
|
529
534
|
emits: ['close', 'complete', 'open'],
|
|
530
535
|
data() {
|
|
@@ -604,7 +609,7 @@ export default {
|
|
|
604
609
|
if (!url) return null;
|
|
605
610
|
return async (payload) => {
|
|
606
611
|
const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
|
|
607
|
-
const headers = { 'Content-Type': 'application/json' };
|
|
612
|
+
const headers = { 'Content-Type': 'application/json', 'Source': 'booking_engine' };
|
|
608
613
|
if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
|
|
609
614
|
const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
|
|
610
615
|
if (!res.ok) throw new Error(await res.text());
|
|
@@ -722,6 +727,16 @@ export default {
|
|
|
722
727
|
isVisible() {
|
|
723
728
|
return this.isOpen && !this.isClosing && this.isReadyForOpen;
|
|
724
729
|
},
|
|
730
|
+
analyticsMode() {
|
|
731
|
+
return this.isSandbox ? 'sandbox' : 'live';
|
|
732
|
+
},
|
|
733
|
+
analyticsContext() {
|
|
734
|
+
return {
|
|
735
|
+
mode: this.analyticsMode,
|
|
736
|
+
propertyKey: this.propertyKey || undefined,
|
|
737
|
+
propertyId: this.propertyId != null && this.propertyId !== '' ? String(this.propertyId) : undefined,
|
|
738
|
+
};
|
|
739
|
+
},
|
|
725
740
|
},
|
|
726
741
|
created() {
|
|
727
742
|
this._initRuntimeConfig();
|
|
@@ -740,6 +755,7 @@ export default {
|
|
|
740
755
|
isOpen: {
|
|
741
756
|
handler(open) {
|
|
742
757
|
if (open && this.onOpen) this.onOpen();
|
|
758
|
+
if (open) captureEvent('widgetOpened', this.analyticsContext);
|
|
743
759
|
if (!open || this.isClosing) {
|
|
744
760
|
this.isReadyForOpen = false;
|
|
745
761
|
return;
|
|
@@ -753,6 +769,14 @@ export default {
|
|
|
753
769
|
},
|
|
754
770
|
immediate: true,
|
|
755
771
|
},
|
|
772
|
+
configLoaded: {
|
|
773
|
+
handler(loaded) {
|
|
774
|
+
if (loaded && this.hasPropertyKey) {
|
|
775
|
+
identifyAnalytics(this.propertyKey);
|
|
776
|
+
captureEvent('widgetLoaded', this.analyticsContext);
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
},
|
|
756
780
|
'state.step': function(step) {
|
|
757
781
|
if (step !== 'summary' && step !== 'payment') {
|
|
758
782
|
this.checkoutShowPaymentForm = false;
|
|
@@ -782,7 +806,9 @@ export default {
|
|
|
782
806
|
this.configError = null;
|
|
783
807
|
this.configLoaded = false;
|
|
784
808
|
try {
|
|
785
|
-
const { widgetStyles } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
|
|
809
|
+
const { widgetStyles, posthogKey: configPosthogKey } = await fetchRuntimeConfig(this.propertyKey, this.colors, this.mode);
|
|
810
|
+
const analyticsKey = this.posthogKey || configPosthogKey || '';
|
|
811
|
+
initAnalytics(analyticsKey || this.posthogHost ? { key: analyticsKey || undefined, host: this.posthogHost || undefined } : {});
|
|
786
812
|
this.runtimeWidgetStyles = widgetStyles;
|
|
787
813
|
this.configLoaded = true;
|
|
788
814
|
} catch (err) {
|
|
@@ -795,7 +821,7 @@ export default {
|
|
|
795
821
|
const t = String(token || '').trim();
|
|
796
822
|
if (!t) throw new Error('Missing confirmation token');
|
|
797
823
|
const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
|
|
798
|
-
const res = await fetch(url, { method: 'POST' });
|
|
824
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Source': 'booking_engine' } });
|
|
799
825
|
if (!res.ok) throw new Error(await res.text());
|
|
800
826
|
return await res.json();
|
|
801
827
|
},
|
|
@@ -812,6 +838,19 @@ export default {
|
|
|
812
838
|
this.confirmationStatus = status || this.confirmationStatus || 'pending';
|
|
813
839
|
if (status === 'confirmed') {
|
|
814
840
|
this.confirmationDetails = data;
|
|
841
|
+
captureEvent('booked', {
|
|
842
|
+
...this.analyticsContext,
|
|
843
|
+
confirmationCode: data?.confirmationCode ?? data?.confirmation_code ?? data?.bookingId ?? data?.booking_id,
|
|
844
|
+
bookingId: data?.bookingId ?? data?.booking_id,
|
|
845
|
+
guest: this.state.guest,
|
|
846
|
+
checkIn: this.state.checkIn?.toISOString?.(),
|
|
847
|
+
checkOut: this.state.checkOut?.toISOString?.(),
|
|
848
|
+
roomName: this.state.selectedRoom?.name,
|
|
849
|
+
roomId: this.state.selectedRoom?.id,
|
|
850
|
+
rateId: this.state.selectedRate?.id,
|
|
851
|
+
totalAmount: data?.totalAmount ?? data?.total_amount,
|
|
852
|
+
currency: data?.currency ?? this.state.selectedRoom?.currency,
|
|
853
|
+
});
|
|
815
854
|
return;
|
|
816
855
|
}
|
|
817
856
|
} catch (err) {
|
|
@@ -880,6 +919,13 @@ export default {
|
|
|
880
919
|
api.fetchRooms(params).then((rooms) => {
|
|
881
920
|
this.roomsList = Array.isArray(rooms) ? rooms : [];
|
|
882
921
|
this.loadingRooms = false;
|
|
922
|
+
captureEvent('search', {
|
|
923
|
+
...this.analyticsContext,
|
|
924
|
+
checkIn: this.state.checkIn?.toISOString?.(),
|
|
925
|
+
checkOut: this.state.checkOut?.toISOString?.(),
|
|
926
|
+
rooms: this.state.rooms,
|
|
927
|
+
occupancies: this.state.occupancies,
|
|
928
|
+
});
|
|
883
929
|
}).catch((err) => {
|
|
884
930
|
this.apiError = err.message || 'Failed to load rooms';
|
|
885
931
|
this.loadingRooms = false;
|
|
@@ -972,6 +1018,17 @@ export default {
|
|
|
972
1018
|
const code = res && (res.confirmationCode != null ? res.confirmationCode : res.confirmation_code);
|
|
973
1019
|
this.confirmationCode = code || ('LX' + Date.now().toString(36).toUpperCase().slice(-6));
|
|
974
1020
|
this.state.step = 'confirmation';
|
|
1021
|
+
captureEvent('booked', {
|
|
1022
|
+
...this.analyticsContext,
|
|
1023
|
+
confirmationCode: this.confirmationCode,
|
|
1024
|
+
guest: this.state.guest,
|
|
1025
|
+
checkIn: this.state.checkIn?.toISOString?.(),
|
|
1026
|
+
checkOut: this.state.checkOut?.toISOString?.(),
|
|
1027
|
+
roomName: this.state.selectedRoom?.name,
|
|
1028
|
+
roomId: this.state.selectedRoom?.id,
|
|
1029
|
+
rateId: this.state.selectedRate?.id,
|
|
1030
|
+
currency: this.state.selectedRoom?.currency,
|
|
1031
|
+
});
|
|
975
1032
|
})
|
|
976
1033
|
.catch((err) => {
|
|
977
1034
|
this.apiError = (err && err.message) || err || 'Booking failed';
|
|
@@ -1124,10 +1181,13 @@ export default {
|
|
|
1124
1181
|
}
|
|
1125
1182
|
},
|
|
1126
1183
|
selectRoom(id) {
|
|
1127
|
-
|
|
1184
|
+
const room = this.roomsList.find(r => r.id === id);
|
|
1185
|
+
this.updateState({ selectedRoom: room });
|
|
1186
|
+
captureEvent('selectedRoom', { ...this.analyticsContext, roomId: id, roomName: room?.name });
|
|
1128
1187
|
},
|
|
1129
1188
|
selectRate(id) {
|
|
1130
1189
|
this.updateState({ selectedRate: this.ratesList.find(r => r.id === id) });
|
|
1190
|
+
captureEvent('selectedRate', { ...this.analyticsContext, rateId: id });
|
|
1131
1191
|
},
|
|
1132
1192
|
updateGuest(field, value) {
|
|
1133
1193
|
this.updateState({ guest: { ...this.state.guest, [field]: value } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuitee/booking-widget",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
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",
|
|
@@ -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
|
}
|