@nuitee/booking-widget 1.0.0 → 1.0.2
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/USAGE.md +5 -5
- package/dist/booking-widget-standalone.js +82 -39
- package/dist/booking-widget.js +38 -7
- package/dist/core/booking-api.js +19 -8
- package/dist/core/stripe-config.js +1 -1
- package/dist/react/BookingWidget.jsx +22 -10
- package/dist/vue/BookingWidget.vue +20 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,18 +91,18 @@ widget.open();
|
|
|
91
91
|
|
|
92
92
|
### Vanilla JavaScript (standalone script)
|
|
93
93
|
|
|
94
|
-
No bundler: load the script and CSS, then create the widget.
|
|
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="
|
|
98
|
-
<script src="
|
|
97
|
+
<link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget.css">
|
|
98
|
+
<script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.0/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: '
|
|
105
|
+
cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget.css',
|
|
106
106
|
propertyKey: 'your-property-key',
|
|
107
107
|
onOpen: () => console.log('Opened'),
|
|
108
108
|
onClose: () => console.log('Closed'),
|
package/USAGE.md
CHANGED
|
@@ -170,22 +170,22 @@ Use the **standalone bundle** when you are not using a bundler: load the script
|
|
|
170
170
|
|
|
171
171
|
### 1. Load assets
|
|
172
172
|
|
|
173
|
+
Load the script and CSS from the CDN:
|
|
174
|
+
|
|
173
175
|
```html
|
|
174
|
-
<link rel="stylesheet" href="
|
|
175
|
-
<script src="
|
|
176
|
+
<link rel="stylesheet" href="https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget.css">
|
|
177
|
+
<script src="https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget-standalone.js"></script>
|
|
176
178
|
|
|
177
179
|
<div id="booking-widget-container"></div>
|
|
178
180
|
```
|
|
179
181
|
|
|
180
|
-
Replace `node_modules/...` with your actual path or a CDN URL if you host the built files.
|
|
181
|
-
|
|
182
182
|
### 2. Create the widget
|
|
183
183
|
|
|
184
184
|
```html
|
|
185
185
|
<script>
|
|
186
186
|
const widget = new BookingWidget({
|
|
187
187
|
containerId: 'booking-widget-container',
|
|
188
|
-
cssUrl: '
|
|
188
|
+
cssUrl: 'https://cdn.thehotelplanet.com/booking-widget/v1.0.0/dist/booking-widget.css',
|
|
189
189
|
propertyKey: 'your-property-key',
|
|
190
190
|
onOpen: () => console.log('Opened'),
|
|
191
191
|
onClose: () => console.log('Closed'),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='
|
|
1
|
+
(function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='pk_live_51T0SdzEj1UJpIPAMQQUH55w3Dj1E07CihP3iTOVO1IM4mMVAJvE86BiUmzGQKuNPPl05btyY39lRca8PSzRzZ9K700CncmOGQ0';})();
|
|
2
2
|
/**
|
|
3
3
|
* Shared Booking API layer for the @nuitee/booking-widget.
|
|
4
4
|
* Used by Vanilla JS, Vue, and React variants so integration lives in one place.
|
|
@@ -198,6 +198,7 @@ function deriveRatePolicyDetail(avRate) {
|
|
|
198
198
|
* @param {string} [config.s3BaseUrl] - Base URL for room images (e.g. from VITE_AWS_S3_PATH).
|
|
199
199
|
* @param {string|number} [config.propertyId] - Property ID (legacy). Prefer propertyKey for proxy endpoints.
|
|
200
200
|
* @param {string} [config.propertyKey] - Property key for proxy endpoints (e.g. VITE_PROPERTY_KEY). Used in /proxy/availability and /proxy/ari-properties?key=.
|
|
201
|
+
* @param {string} [config.mode] - 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it.
|
|
201
202
|
* @param {string} [config.currency] - Currency code (e.g. 'MAD'). Default 'MAD'.
|
|
202
203
|
* @param {Object} [config.headers] - Optional. Static headers for each request (e.g. { 'X-API-Key': '...' }).
|
|
203
204
|
* @param {function(): Object} [config.getHeaders] - Optional. Return headers for each request (merged after headers). Use for dynamic headers.
|
|
@@ -216,6 +217,7 @@ function createBookingApi(config = {}) {
|
|
|
216
217
|
if (rawKey && !propertyKey && typeof console !== 'undefined' && console.warn) {
|
|
217
218
|
console.warn('[booking-api] Property key must start with "book_engine_". Ignoring invalid value. Set VITE_PROPERTY_KEY in your .env (see .env.example).');
|
|
218
219
|
}
|
|
220
|
+
const sandbox = config.mode === 'sandbox';
|
|
219
221
|
const currency = config.currency || 'MAD';
|
|
220
222
|
const staticHeaders = config.headers || {};
|
|
221
223
|
const getHeaders = config.getHeaders || (() => ({}));
|
|
@@ -280,8 +282,11 @@ function createBookingApi(config = {}) {
|
|
|
280
282
|
// Fetch get_properties first to get currency_code (e.g. 'EUR') and room details
|
|
281
283
|
let propertyRooms = {};
|
|
282
284
|
let propertyCurrency = currency;
|
|
285
|
+
const propQuery = propertyKey
|
|
286
|
+
? `key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
|
|
287
|
+
: '';
|
|
283
288
|
const propFullUrl = propertyKey
|
|
284
|
-
? `${availabilityBaseUrl}/proxy/ari-properties
|
|
289
|
+
? `${availabilityBaseUrl}/proxy/ari-properties?${propQuery}`
|
|
285
290
|
: propertyBaseUrl
|
|
286
291
|
? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
|
|
287
292
|
: `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
|
|
@@ -308,7 +313,7 @@ function createBookingApi(config = {}) {
|
|
|
308
313
|
};
|
|
309
314
|
|
|
310
315
|
const availabilityPath = propertyKey
|
|
311
|
-
? `/proxy/availability?key=${encodeURIComponent(propertyKey)}`
|
|
316
|
+
? `/proxy/availability?key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
|
|
312
317
|
: `/api/calendar/booking_engine/${propertyId}/availability`;
|
|
313
318
|
const data = await request('POST', availabilityPath, body, availabilityBaseUrl);
|
|
314
319
|
if (data && typeof data === 'object' && (data.error || data.message)) {
|
|
@@ -521,14 +526,15 @@ function computeCheckoutTotal(state, nights) {
|
|
|
521
526
|
* Use this when integrating Stripe (amount/currency for Payment Intent) and your booking API (external_booking + internal_booking; add stripe_token after payment success).
|
|
522
527
|
*
|
|
523
528
|
* @param {Object} state - Widget state: checkIn, checkOut, rooms, occupancies, selectedRoom, selectedRate, guest
|
|
524
|
-
* @param {Object} [options] - { propertyId: string|number, propertyKey: string, clientBookingReference?: string }
|
|
525
|
-
* @returns {{ stripe: { amount: number, currency: string }, external_booking: Object, internal_booking: Object }}
|
|
529
|
+
* @param {Object} [options] - { propertyId: string|number, propertyKey: string, clientBookingReference?: string, sandbox?: boolean }
|
|
530
|
+
* @returns {{ stripe: { amount: number, currency: string }, external_booking: Object, internal_booking: Object, sandbox?: boolean }}
|
|
526
531
|
*/
|
|
527
532
|
function buildCheckoutPayload(state, options = {}) {
|
|
528
533
|
const propertyId = options.propertyId != null ? String(options.propertyId) : '';
|
|
529
534
|
const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
|
|
530
535
|
const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
|
|
531
536
|
const clientBookingReference = options.clientBookingReference || 'nuitee-booking-widget';
|
|
537
|
+
const sandbox = options.sandbox === true;
|
|
532
538
|
|
|
533
539
|
const checkIn = state.checkIn;
|
|
534
540
|
const checkOut = state.checkOut;
|
|
@@ -592,7 +598,7 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
592
598
|
room_id: state.selectedRoom?.id ?? '',
|
|
593
599
|
};
|
|
594
600
|
|
|
595
|
-
|
|
601
|
+
const out = {
|
|
596
602
|
stripe: {
|
|
597
603
|
amount: total,
|
|
598
604
|
currency: currency.toLowerCase(),
|
|
@@ -600,6 +606,8 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
600
606
|
external_booking,
|
|
601
607
|
internal_booking,
|
|
602
608
|
};
|
|
609
|
+
if (sandbox) out.sandbox = true;
|
|
610
|
+
return out;
|
|
603
611
|
}
|
|
604
612
|
|
|
605
613
|
/**
|
|
@@ -608,12 +616,13 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
608
616
|
* Response: { clientSecret, confirmationToken }
|
|
609
617
|
*
|
|
610
618
|
* @param {Object} state - Widget state: checkIn, checkOut, rooms, occupancies, selectedRoom, selectedRate, guest
|
|
611
|
-
* @param {Object} [options] - { propertyKey: string }
|
|
612
|
-
* @returns {{ rate_identifier: string, key: string, metadata: Object }}
|
|
619
|
+
* @param {Object} [options] - { propertyKey: string, sandbox?: boolean }
|
|
620
|
+
* @returns {{ rate_identifier: string, key: string, metadata: Object, sandbox?: boolean }}
|
|
613
621
|
*/
|
|
614
622
|
function buildPaymentIntentPayload(state, options = {}) {
|
|
615
623
|
const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
|
|
616
624
|
const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
|
|
625
|
+
const sandbox = options.sandbox === true;
|
|
617
626
|
const checkIn = state.checkIn;
|
|
618
627
|
const checkOut = state.checkOut;
|
|
619
628
|
const nights = checkIn && checkOut
|
|
@@ -633,7 +642,7 @@ function buildPaymentIntentPayload(state, options = {}) {
|
|
|
633
642
|
...(Array.isArray(o.childrenAges) && o.childrenAges.length ? { child_ages: o.childrenAges } : {}),
|
|
634
643
|
}));
|
|
635
644
|
|
|
636
|
-
|
|
645
|
+
const out = {
|
|
637
646
|
rate_identifier: rateIdentifier,
|
|
638
647
|
key: propertyKey,
|
|
639
648
|
metadata: {
|
|
@@ -651,6 +660,8 @@ function buildPaymentIntentPayload(state, options = {}) {
|
|
|
651
660
|
source_transaction: 'booking engine',
|
|
652
661
|
},
|
|
653
662
|
};
|
|
663
|
+
if (sandbox) out.sandbox = true;
|
|
664
|
+
return out;
|
|
654
665
|
}
|
|
655
666
|
|
|
656
667
|
/**
|
|
@@ -819,28 +830,30 @@ if (typeof window !== 'undefined') {
|
|
|
819
830
|
|
|
820
831
|
// Copy the BookingWidget class from core/widget.js
|
|
821
832
|
// This is a simplified version that will be bundled
|
|
822
|
-
const apiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
|
|
823
833
|
const builtInStripeKey = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_STRIPE_KEY__) ? window.__BOOKING_WIDGET_STRIPE_KEY__ : '';
|
|
824
|
-
const builtInCreatePaymentIntent = (apiBase && typeof window !== 'undefined')
|
|
825
|
-
? function (payload) {
|
|
826
|
-
return fetch(apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent', {
|
|
827
|
-
method: 'POST',
|
|
828
|
-
headers: { 'Content-Type': 'application/json' },
|
|
829
|
-
body: JSON.stringify(payload),
|
|
830
|
-
}).then(function (r) {
|
|
831
|
-
if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
|
|
832
|
-
return r.json();
|
|
833
|
-
}).then(function (data) {
|
|
834
|
-
return {
|
|
835
|
-
clientSecret: data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret,
|
|
836
|
-
confirmationToken: data.confirmationToken ?? data.confirmation_token ?? data.data?.confirmationToken ?? data.data?.confirmation_token,
|
|
837
|
-
};
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
: null;
|
|
841
834
|
|
|
842
835
|
class BookingWidget {
|
|
843
836
|
constructor(options = {}) {
|
|
837
|
+
const defaultApiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
|
|
838
|
+
const builtInCreatePaymentIntent = (!options.createPaymentIntent && defaultApiBase && typeof window !== 'undefined')
|
|
839
|
+
? function (payload) {
|
|
840
|
+
const url = defaultApiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (options.mode === 'sandbox' ? '?sandbox=true' : '');
|
|
841
|
+
return fetch(url, {
|
|
842
|
+
method: 'POST',
|
|
843
|
+
headers: { 'Content-Type': 'application/json' },
|
|
844
|
+
body: JSON.stringify(payload),
|
|
845
|
+
}).then(function (r) {
|
|
846
|
+
if (!r.ok) throw new Error(r.statusText || 'Payment intent failed');
|
|
847
|
+
return r.json();
|
|
848
|
+
}).then(function (data) {
|
|
849
|
+
return {
|
|
850
|
+
clientSecret: data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret,
|
|
851
|
+
confirmationToken: data.confirmationToken ?? data.confirmation_token ?? data.data?.confirmationToken ?? data.data?.confirmation_token,
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
: null;
|
|
856
|
+
|
|
844
857
|
this.options = {
|
|
845
858
|
containerId: options.containerId || 'booking-widget-container',
|
|
846
859
|
onOpen: options.onOpen || null,
|
|
@@ -849,10 +862,11 @@ if (typeof window !== 'undefined') {
|
|
|
849
862
|
onBeforeConfirm: options.onBeforeConfirm || null,
|
|
850
863
|
createPaymentIntent: options.createPaymentIntent || builtInCreatePaymentIntent,
|
|
851
864
|
onBookingComplete: options.onBookingComplete || null,
|
|
852
|
-
confirmationBaseUrl: options.confirmationBaseUrl ||
|
|
865
|
+
confirmationBaseUrl: options.confirmationBaseUrl || defaultApiBase || 'https://ai.thehotelplanet.com',
|
|
853
866
|
stripePublishableKey: options.stripePublishableKey || builtInStripeKey || null,
|
|
854
867
|
propertyId: options.propertyId != null && options.propertyId !== '' ? options.propertyId : null,
|
|
855
868
|
propertyKey: options.propertyKey || null,
|
|
869
|
+
mode: options.mode || null,
|
|
856
870
|
bookingApi: options.bookingApi || null,
|
|
857
871
|
cssUrl: options.cssUrl || null,
|
|
858
872
|
// Color customization options
|
|
@@ -902,11 +916,11 @@ if (typeof window !== 'undefined') {
|
|
|
902
916
|
|
|
903
917
|
this.ROOMS = [];
|
|
904
918
|
this.RATES = [];
|
|
905
|
-
const defaultApiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
|
|
906
919
|
this.bookingApi = this.options.bookingApi || ((this.options.propertyKey || defaultApiBase) && typeof window.createBookingApi === 'function'
|
|
907
920
|
? window.createBookingApi({
|
|
908
921
|
availabilityBaseUrl: defaultApiBase || '',
|
|
909
922
|
propertyKey: this.options.propertyKey || undefined,
|
|
923
|
+
mode: this.options.mode === 'sandbox' ? 'sandbox' : undefined,
|
|
910
924
|
})
|
|
911
925
|
: null);
|
|
912
926
|
|
|
@@ -1042,6 +1056,7 @@ if (typeof window !== 'undefined') {
|
|
|
1042
1056
|
this.widget.innerHTML = `
|
|
1043
1057
|
<button class="booking-widget-close">${iconHTML('x', '1.5em')}</button>
|
|
1044
1058
|
<div class="booking-widget-step-indicator"></div>
|
|
1059
|
+
<div class="booking-widget-property-key-message" role="alert" style="display:none;margin:0 1.5em 1em;padding:0.75em 1em;background:var(--destructive,#ef4444);color:#fff;border-radius:8px;font-size:0.9em;"></div>
|
|
1045
1060
|
<div class="booking-widget-step-content"></div>
|
|
1046
1061
|
`;
|
|
1047
1062
|
this.widget.querySelector('.booking-widget-close').addEventListener('click', () => this.close());
|
|
@@ -1098,12 +1113,29 @@ if (typeof window !== 'undefined') {
|
|
|
1098
1113
|
|
|
1099
1114
|
render() {
|
|
1100
1115
|
this.renderStepIndicator();
|
|
1116
|
+
this.renderPropertyKeyMessage();
|
|
1101
1117
|
this.renderStepContent();
|
|
1102
1118
|
}
|
|
1103
1119
|
|
|
1120
|
+
renderPropertyKeyMessage() {
|
|
1121
|
+
const el = this.widget.querySelector('.booking-widget-property-key-message');
|
|
1122
|
+
if (!el) return;
|
|
1123
|
+
const key = this.options.propertyKey;
|
|
1124
|
+
const missing = !key || (typeof key === 'string' && !key.trim());
|
|
1125
|
+
if (missing) {
|
|
1126
|
+
el.style.display = 'block';
|
|
1127
|
+
el.innerHTML = 'The propertyKey is missing. Please provide it as a prop to load rooms from the API.';
|
|
1128
|
+
} else {
|
|
1129
|
+
el.style.display = 'none';
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1104
1133
|
renderStepIndicator() {
|
|
1105
1134
|
const el = this.widget.querySelector('.booking-widget-step-indicator');
|
|
1106
|
-
if (
|
|
1135
|
+
if (!el) return;
|
|
1136
|
+
const key = this.options.propertyKey;
|
|
1137
|
+
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
1138
|
+
if (!hasPropertyKey || this.state.step === 'confirmation') {
|
|
1107
1139
|
el.innerHTML = '';
|
|
1108
1140
|
return;
|
|
1109
1141
|
}
|
|
@@ -1125,6 +1157,13 @@ if (typeof window !== 'undefined') {
|
|
|
1125
1157
|
|
|
1126
1158
|
renderStepContent() {
|
|
1127
1159
|
const el = this.widget.querySelector('.booking-widget-step-content');
|
|
1160
|
+
if (!el) return;
|
|
1161
|
+
const key = this.options.propertyKey;
|
|
1162
|
+
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
1163
|
+
if (!hasPropertyKey) {
|
|
1164
|
+
el.innerHTML = '';
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1128
1167
|
switch (this.state.step) {
|
|
1129
1168
|
case 'dates':
|
|
1130
1169
|
el.innerHTML = this.renderDatesStep();
|
|
@@ -1472,7 +1511,7 @@ if (typeof window !== 'undefined') {
|
|
|
1472
1511
|
<span class="rate-name">${[r.policy, ...(r.benefits || [])].filter(Boolean).join(' ')}</span>
|
|
1473
1512
|
<div class="rate-benefits"><span class="amenity-tag">${r.rate_code ?? r.name}</span></div>
|
|
1474
1513
|
</div>
|
|
1475
|
-
<div class="rate-price"><strong>$
|
|
1514
|
+
<div class="rate-price"><strong>${(typeof window.formatPrice === 'function' ? window.formatPrice(total, this.state.selectedRoom?.currency) : (this.state.selectedRoom?.currency || 'USD') + ' ' + total.toLocaleString())}</strong><small>total</small></div>
|
|
1476
1515
|
</div>
|
|
1477
1516
|
<p class="rate-desc">${r.description}</p>
|
|
1478
1517
|
</div>`;
|
|
@@ -1536,18 +1575,20 @@ if (typeof window !== 'undefined') {
|
|
|
1536
1575
|
const roomTotal = Math.round(this.state.selectedRoom.basePrice * this.state.selectedRate.priceModifier * nights * rooms);
|
|
1537
1576
|
const fees = this.state.selectedRate.fees ?? [];
|
|
1538
1577
|
const allIncluded = fees.length > 0 && fees.every(f => f.included) && (!this.state.selectedRate.vat || this.state.selectedRate.vat.included);
|
|
1578
|
+
const currency = this.state.selectedRoom?.currency || 'USD';
|
|
1579
|
+
const fmt = (a) => (typeof window.formatPrice === 'function' ? window.formatPrice(a, currency) : currency + ' ' + Math.round(Number(a) || 0).toLocaleString());
|
|
1539
1580
|
let rows = fees.map(f => {
|
|
1540
1581
|
const amt = f.perNight ? f.amount * nights * rooms : f.amount;
|
|
1541
1582
|
const badge = f.included ? '<span class="summary-fee-badge">Included</span>' : '<span class="summary-fee-badge summary-fee-badge--excluded">Excluded</span>';
|
|
1542
|
-
return `<div class="summary-row summary-row--fee"><span class="summary-fee-label">${f.name} ${badge}</span><span>$
|
|
1583
|
+
return `<div class="summary-row summary-row--fee"><span class="summary-fee-label">${f.name} ${badge}</span><span>${fmt(amt)}</span></div>`;
|
|
1543
1584
|
}).join('');
|
|
1544
1585
|
const vat = this.state.selectedRate.vat;
|
|
1545
1586
|
if (vat) {
|
|
1546
1587
|
const showVatBadge = vat.value !== 0 && vat.value != null;
|
|
1547
1588
|
const vatBadge = showVatBadge ? (vat.included ? '<span class="summary-fee-badge">Included</span>' : '<span class="summary-fee-badge summary-fee-badge--excluded">Excluded</span>') : '';
|
|
1548
|
-
rows += `<div class="summary-row summary-row--fee"><span class="summary-fee-label">VAT${vatBadge ? ' ' + vatBadge : ''}</span><span>$
|
|
1589
|
+
rows += `<div class="summary-row summary-row--fee"><span class="summary-fee-label">VAT${vatBadge ? ' ' + vatBadge : ''}</span><span>${fmt(vat.value || 0)}</span></div>`;
|
|
1549
1590
|
}
|
|
1550
|
-
return `<div class="summary-row"><span>Room total</span><span>$
|
|
1591
|
+
return `<div class="summary-row"><span>Room total</span><span>${fmt(roomTotal)}</span></div>
|
|
1551
1592
|
<div class="summary-fees">
|
|
1552
1593
|
<p class="summary-fees-heading">Fees & taxes</p>
|
|
1553
1594
|
${allIncluded ? '<p class="summary-fees-note">Included in your rate</p>' : ''}
|
|
@@ -1556,7 +1597,7 @@ if (typeof window !== 'undefined') {
|
|
|
1556
1597
|
})() : ''}
|
|
1557
1598
|
<div class="summary-total">
|
|
1558
1599
|
<span class="summary-total-label">Total</span>
|
|
1559
|
-
<span class="summary-total-price">$
|
|
1600
|
+
<span class="summary-total-price">${(typeof window.formatPrice === 'function' ? window.formatPrice(total, this.state.selectedRoom?.currency) : (this.state.selectedRoom?.currency || 'USD') + ' ' + total.toLocaleString())}</span>
|
|
1560
1601
|
</div>
|
|
1561
1602
|
${this.apiError ? `<p style="color:var(--destructive, #ef4444);font-size:0.85em;margin-top:0.5em;">${this.apiError}</p>` : ''}
|
|
1562
1603
|
<button type="button" class="btn-primary" style="max-width:100%;margin-top:1em;" ${!canSubmit ? 'disabled' : ''} onclick="window.bookingWidgetInstance.confirmReservation(event)">${iconHTML('creditCard', '1.2em')} ${buttonLabel}</button>
|
|
@@ -1588,7 +1629,7 @@ if (typeof window !== 'undefined') {
|
|
|
1588
1629
|
<h3>Amount due</h3>
|
|
1589
1630
|
<div class="payment-total-row">
|
|
1590
1631
|
<span class="payment-total-label">Total</span>
|
|
1591
|
-
<span class="payment-total-amount">$
|
|
1632
|
+
<span class="payment-total-amount">${(typeof window.formatPrice === 'function' ? window.formatPrice(total, this.state.selectedRoom?.currency) : (this.state.selectedRoom?.currency || 'USD') + ' ' + total.toLocaleString())}</span>
|
|
1592
1633
|
</div>
|
|
1593
1634
|
<button type="button" class="btn-primary" ${buttonDisabled} onclick="window.bookingWidgetInstance.confirmReservation(event)">${iconHTML('creditCard', '1.2em')} ${buttonLabel}</button>
|
|
1594
1635
|
<p class="secure-note">${iconHTML('lock', '1em')} Secure & encrypted booking</p>
|
|
@@ -1606,7 +1647,8 @@ if (typeof window !== 'undefined') {
|
|
|
1606
1647
|
if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.options.stripePublishableKey || typeof this.options.createPaymentIntent !== 'function') return;
|
|
1607
1648
|
const buildPaymentIntentPayload = typeof window !== 'undefined' && window.buildPaymentIntentPayload;
|
|
1608
1649
|
const buildCheckoutPayload = typeof window !== 'undefined' && window.buildCheckoutPayload;
|
|
1609
|
-
const
|
|
1650
|
+
const sandbox = this.options.mode === 'sandbox';
|
|
1651
|
+
const paymentIntentPayload = buildPaymentIntentPayload ? buildPaymentIntentPayload(this.state, { propertyKey: this.options.propertyKey ?? undefined, sandbox }) : (buildCheckoutPayload ? buildCheckoutPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined, sandbox }) : null);
|
|
1610
1652
|
if (!paymentIntentPayload) return;
|
|
1611
1653
|
const self = this;
|
|
1612
1654
|
this.paymentElementReady = false;
|
|
@@ -1685,7 +1727,8 @@ if (typeof window !== 'undefined') {
|
|
|
1685
1727
|
const canSubmit = this.state.guest.firstName && this.state.guest.lastName && this.state.guest.email;
|
|
1686
1728
|
if (!canSubmit) return;
|
|
1687
1729
|
const buildPayload = typeof window !== 'undefined' && window.buildCheckoutPayload;
|
|
1688
|
-
const
|
|
1730
|
+
const sandbox = this.options.mode === 'sandbox';
|
|
1731
|
+
const payload = buildPayload ? buildPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined, sandbox }) : null;
|
|
1689
1732
|
const hasStripe = this.hasStripe;
|
|
1690
1733
|
|
|
1691
1734
|
if (this.state.step === 'summary' && hasStripe) {
|
|
@@ -1745,7 +1788,7 @@ if (typeof window !== 'undefined') {
|
|
|
1745
1788
|
const token = String(confirmationToken || '').trim();
|
|
1746
1789
|
if (!token) throw new Error('Missing confirmation token');
|
|
1747
1790
|
const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
|
|
1748
|
-
const url = base + '/proxy/confirmation/' + encodeURIComponent(token);
|
|
1791
|
+
const url = base + '/proxy/confirmation/' + encodeURIComponent(token) + (this.options.mode === 'sandbox' ? '?sandbox=true' : '');
|
|
1749
1792
|
const res = await fetch(url, { method: 'POST' });
|
|
1750
1793
|
if (!res.ok) throw new Error(await res.text());
|
|
1751
1794
|
return await res.json();
|
package/dist/booking-widget.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
(function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='
|
|
1
|
+
(function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='pk_live_51T0SdzEj1UJpIPAMQQUH55w3Dj1E07CihP3iTOVO1IM4mMVAJvE86BiUmzGQKuNPPl05btyY39lRca8PSzRzZ9K700CncmOGQ0';})();
|
|
2
2
|
// ===== Icons (Lucide/shadcn) =====
|
|
3
3
|
const icons = {
|
|
4
4
|
calendar: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect><line x1="16" y1="2" x2="16" y2="6"></line><line x1="8" y1="2" x2="8" y2="6"></line><line x1="3" y1="10" x2="21" y2="10"></line></svg>`,
|
|
@@ -64,9 +64,11 @@ class BookingWidget {
|
|
|
64
64
|
constructor(options = {}) {
|
|
65
65
|
const apiBase = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) ? window.__BOOKING_WIDGET_API_BASE_URL__ : '';
|
|
66
66
|
const builtInStripeKey = (typeof window !== 'undefined' && window.__BOOKING_WIDGET_STRIPE_KEY__) ? window.__BOOKING_WIDGET_STRIPE_KEY__ : '';
|
|
67
|
+
const builtInMode = options.mode;
|
|
67
68
|
const builtInCreatePaymentIntent = (apiBase && typeof window !== 'undefined')
|
|
68
69
|
? function (payload) {
|
|
69
|
-
|
|
70
|
+
const url = apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent' + (builtInMode === 'sandbox' ? '?sandbox=true' : '');
|
|
71
|
+
return fetch(url, {
|
|
70
72
|
method: 'POST',
|
|
71
73
|
headers: { 'Content-Type': 'application/json' },
|
|
72
74
|
body: JSON.stringify(payload),
|
|
@@ -99,6 +101,8 @@ class BookingWidget {
|
|
|
99
101
|
apiSecret: options.apiSecret || null,
|
|
100
102
|
propertyId: options.propertyId != null && options.propertyId !== '' ? options.propertyId : null,
|
|
101
103
|
propertyKey: options.propertyKey || null,
|
|
104
|
+
/** 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it. */
|
|
105
|
+
mode: options.mode || null,
|
|
102
106
|
availabilityBaseUrl: options.availabilityBaseUrl || null,
|
|
103
107
|
propertyBaseUrl: options.propertyBaseUrl || null,
|
|
104
108
|
s3BaseUrl: options.s3BaseUrl || null,
|
|
@@ -125,6 +129,7 @@ class BookingWidget {
|
|
|
125
129
|
s3BaseUrl: opts.s3BaseUrl || undefined,
|
|
126
130
|
propertyId: opts.propertyId != null && opts.propertyId !== '' ? String(opts.propertyId) : undefined,
|
|
127
131
|
propertyKey: opts.propertyKey || undefined,
|
|
132
|
+
mode: opts.mode === 'sandbox' ? 'sandbox' : undefined,
|
|
128
133
|
headers: opts.apiSecret ? { 'X-API-Key': opts.apiSecret } : undefined,
|
|
129
134
|
})
|
|
130
135
|
: null);
|
|
@@ -334,6 +339,7 @@ class BookingWidget {
|
|
|
334
339
|
this.widget.innerHTML = `
|
|
335
340
|
<button class="booking-widget-close">${iconHTML('x', '1.5em')}</button>
|
|
336
341
|
<div class="booking-widget-step-indicator"></div>
|
|
342
|
+
<div class="booking-widget-property-key-message" role="alert" style="display:none;margin:0 1.5em 1em;padding:0.75em 1em;background:var(--destructive,#ef4444);color:#fff;border-radius:8px;font-size:0.9em;"></div>
|
|
337
343
|
<div class="booking-widget-step-content"></div>
|
|
338
344
|
`;
|
|
339
345
|
this.widget.querySelector('.booking-widget-close').addEventListener('click', () => this.close());
|
|
@@ -392,6 +398,7 @@ class BookingWidget {
|
|
|
392
398
|
window.__bookingWidgetRendering = true;
|
|
393
399
|
try {
|
|
394
400
|
this.renderStepIndicator();
|
|
401
|
+
this.renderPropertyKeyMessage();
|
|
395
402
|
this.renderStepContent();
|
|
396
403
|
} finally {
|
|
397
404
|
window.__bookingWidgetRendering = false;
|
|
@@ -402,7 +409,9 @@ class BookingWidget {
|
|
|
402
409
|
renderStepIndicator() {
|
|
403
410
|
const el = this.widget.querySelector('.booking-widget-step-indicator');
|
|
404
411
|
if (!el) return;
|
|
405
|
-
|
|
412
|
+
const key = this.options.propertyKey;
|
|
413
|
+
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
414
|
+
if (!hasPropertyKey || this.state.step === 'confirmation') {
|
|
406
415
|
el.innerHTML = '';
|
|
407
416
|
return;
|
|
408
417
|
}
|
|
@@ -422,9 +431,29 @@ class BookingWidget {
|
|
|
422
431
|
el.innerHTML = html;
|
|
423
432
|
}
|
|
424
433
|
|
|
434
|
+
renderPropertyKeyMessage() {
|
|
435
|
+
const el = this.widget.querySelector('.booking-widget-property-key-message');
|
|
436
|
+
if (!el) return;
|
|
437
|
+
const key = this.options.propertyKey;
|
|
438
|
+
const missing = !key || (typeof key === 'string' && !key.trim());
|
|
439
|
+
if (missing) {
|
|
440
|
+
el.style.display = 'block';
|
|
441
|
+
el.innerHTML = 'The propertyKey is missing. Please provide it as a prop to load rooms from the API.';
|
|
442
|
+
} else {
|
|
443
|
+
el.style.display = 'none';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
425
447
|
// ===== Step Renderers =====
|
|
426
448
|
renderStepContent() {
|
|
427
449
|
const el = this.widget.querySelector('.booking-widget-step-content');
|
|
450
|
+
if (!el) return;
|
|
451
|
+
const key = this.options.propertyKey;
|
|
452
|
+
const hasPropertyKey = key != null && key !== '' && (typeof key !== 'string' || key.trim() !== '');
|
|
453
|
+
if (!hasPropertyKey) {
|
|
454
|
+
el.innerHTML = '';
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
428
457
|
switch (this.state.step) {
|
|
429
458
|
case 'dates':
|
|
430
459
|
el.innerHTML = this.renderDatesStep();
|
|
@@ -949,9 +978,10 @@ class BookingWidget {
|
|
|
949
978
|
if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.options.stripePublishableKey || typeof this.options.createPaymentIntent !== 'function') return;
|
|
950
979
|
const buildPaymentIntentPayload = typeof window !== 'undefined' && window.buildPaymentIntentPayload;
|
|
951
980
|
const buildCheckoutPayload = typeof window !== 'undefined' && window.buildCheckoutPayload;
|
|
981
|
+
const sandbox = this.options.mode === 'sandbox';
|
|
952
982
|
const paymentIntentPayload = buildPaymentIntentPayload
|
|
953
|
-
? buildPaymentIntentPayload(this.state, { propertyKey: this.options.propertyKey ?? undefined })
|
|
954
|
-
: (buildCheckoutPayload ? buildCheckoutPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined }) : null);
|
|
983
|
+
? buildPaymentIntentPayload(this.state, { propertyKey: this.options.propertyKey ?? undefined, sandbox })
|
|
984
|
+
: (buildCheckoutPayload ? buildCheckoutPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined, sandbox }) : null);
|
|
955
985
|
if (!paymentIntentPayload) return;
|
|
956
986
|
const self = this;
|
|
957
987
|
this.paymentElementReady = false;
|
|
@@ -1030,7 +1060,8 @@ class BookingWidget {
|
|
|
1030
1060
|
const canSubmit = this.state.guest.firstName && this.state.guest.lastName && this.state.guest.email;
|
|
1031
1061
|
if (!canSubmit) return;
|
|
1032
1062
|
const buildPayload = typeof window !== 'undefined' && window.buildCheckoutPayload;
|
|
1033
|
-
const
|
|
1063
|
+
const sandbox = this.options.mode === 'sandbox';
|
|
1064
|
+
const payload = buildPayload ? buildPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined, sandbox }) : null;
|
|
1034
1065
|
const hasStripe = this.options.stripePublishableKey && typeof this.options.createPaymentIntent === 'function';
|
|
1035
1066
|
|
|
1036
1067
|
// Summary step with Stripe: go to payment step (form loads there)
|
|
@@ -1177,7 +1208,7 @@ class BookingWidget {
|
|
|
1177
1208
|
const token = String(confirmationToken || '').trim();
|
|
1178
1209
|
if (!token) throw new Error('Missing confirmation token');
|
|
1179
1210
|
const base = String(this.options.confirmationBaseUrl || 'https://ai.thehotelplanet.com').replace(/\/$/, '');
|
|
1180
|
-
const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}`;
|
|
1211
|
+
const url = `${base}/proxy/confirmation/${encodeURIComponent(token)}${this.options.mode === 'sandbox' ? '?sandbox=true' : ''}`;
|
|
1181
1212
|
const res = await fetch(url, { method: 'POST' });
|
|
1182
1213
|
if (!res.ok) throw new Error(await res.text());
|
|
1183
1214
|
return await res.json();
|
package/dist/core/booking-api.js
CHANGED
|
@@ -197,6 +197,7 @@ function deriveRatePolicyDetail(avRate) {
|
|
|
197
197
|
* @param {string} [config.s3BaseUrl] - Base URL for room images (e.g. from VITE_AWS_S3_PATH).
|
|
198
198
|
* @param {string|number} [config.propertyId] - Property ID (legacy). Prefer propertyKey for proxy endpoints.
|
|
199
199
|
* @param {string} [config.propertyKey] - Property key for proxy endpoints (e.g. VITE_PROPERTY_KEY). Used in /proxy/availability and /proxy/ari-properties?key=.
|
|
200
|
+
* @param {string} [config.mode] - 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it.
|
|
200
201
|
* @param {string} [config.currency] - Currency code (e.g. 'MAD'). Default 'MAD'.
|
|
201
202
|
* @param {Object} [config.headers] - Optional. Static headers for each request (e.g. { 'X-API-Key': '...' }).
|
|
202
203
|
* @param {function(): Object} [config.getHeaders] - Optional. Return headers for each request (merged after headers). Use for dynamic headers.
|
|
@@ -215,6 +216,7 @@ function createBookingApi(config = {}) {
|
|
|
215
216
|
if (rawKey && !propertyKey && typeof console !== 'undefined' && console.warn) {
|
|
216
217
|
console.warn('[booking-api] Property key must start with "book_engine_". Ignoring invalid value. Set VITE_PROPERTY_KEY in your .env (see .env.example).');
|
|
217
218
|
}
|
|
219
|
+
const sandbox = config.mode === 'sandbox';
|
|
218
220
|
const currency = config.currency || 'MAD';
|
|
219
221
|
const staticHeaders = config.headers || {};
|
|
220
222
|
const getHeaders = config.getHeaders || (() => ({}));
|
|
@@ -279,8 +281,11 @@ function createBookingApi(config = {}) {
|
|
|
279
281
|
// Fetch get_properties first to get currency_code (e.g. 'EUR') and room details
|
|
280
282
|
let propertyRooms = {};
|
|
281
283
|
let propertyCurrency = currency;
|
|
284
|
+
const propQuery = propertyKey
|
|
285
|
+
? `key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
|
|
286
|
+
: '';
|
|
282
287
|
const propFullUrl = propertyKey
|
|
283
|
-
? `${availabilityBaseUrl}/proxy/ari-properties
|
|
288
|
+
? `${availabilityBaseUrl}/proxy/ari-properties?${propQuery}`
|
|
284
289
|
: propertyBaseUrl
|
|
285
290
|
? `${propertyBaseUrl}/ari/get_properties?id=${encodeURIComponent(propertyId)}`
|
|
286
291
|
: `/ari/get_properties?id=${encodeURIComponent(propertyId)}`;
|
|
@@ -307,7 +312,7 @@ function createBookingApi(config = {}) {
|
|
|
307
312
|
};
|
|
308
313
|
|
|
309
314
|
const availabilityPath = propertyKey
|
|
310
|
-
? `/proxy/availability?key=${encodeURIComponent(propertyKey)}`
|
|
315
|
+
? `/proxy/availability?key=${encodeURIComponent(propertyKey)}${sandbox ? '&sandbox=true' : ''}`
|
|
311
316
|
: `/api/calendar/booking_engine/${propertyId}/availability`;
|
|
312
317
|
const data = await request('POST', availabilityPath, body, availabilityBaseUrl);
|
|
313
318
|
if (data && typeof data === 'object' && (data.error || data.message)) {
|
|
@@ -520,14 +525,15 @@ function computeCheckoutTotal(state, nights) {
|
|
|
520
525
|
* Use this when integrating Stripe (amount/currency for Payment Intent) and your booking API (external_booking + internal_booking; add stripe_token after payment success).
|
|
521
526
|
*
|
|
522
527
|
* @param {Object} state - Widget state: checkIn, checkOut, rooms, occupancies, selectedRoom, selectedRate, guest
|
|
523
|
-
* @param {Object} [options] - { propertyId: string|number, propertyKey: string, clientBookingReference?: string }
|
|
524
|
-
* @returns {{ stripe: { amount: number, currency: string }, external_booking: Object, internal_booking: Object }}
|
|
528
|
+
* @param {Object} [options] - { propertyId: string|number, propertyKey: string, clientBookingReference?: string, sandbox?: boolean }
|
|
529
|
+
* @returns {{ stripe: { amount: number, currency: string }, external_booking: Object, internal_booking: Object, sandbox?: boolean }}
|
|
525
530
|
*/
|
|
526
531
|
function buildCheckoutPayload(state, options = {}) {
|
|
527
532
|
const propertyId = options.propertyId != null ? String(options.propertyId) : '';
|
|
528
533
|
const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
|
|
529
534
|
const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
|
|
530
535
|
const clientBookingReference = options.clientBookingReference || 'nuitee-booking-widget';
|
|
536
|
+
const sandbox = options.sandbox === true;
|
|
531
537
|
|
|
532
538
|
const checkIn = state.checkIn;
|
|
533
539
|
const checkOut = state.checkOut;
|
|
@@ -591,7 +597,7 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
591
597
|
room_id: state.selectedRoom?.id ?? '',
|
|
592
598
|
};
|
|
593
599
|
|
|
594
|
-
|
|
600
|
+
const out = {
|
|
595
601
|
stripe: {
|
|
596
602
|
amount: total,
|
|
597
603
|
currency: currency.toLowerCase(),
|
|
@@ -599,6 +605,8 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
599
605
|
external_booking,
|
|
600
606
|
internal_booking,
|
|
601
607
|
};
|
|
608
|
+
if (sandbox) out.sandbox = true;
|
|
609
|
+
return out;
|
|
602
610
|
}
|
|
603
611
|
|
|
604
612
|
/**
|
|
@@ -607,12 +615,13 @@ function buildCheckoutPayload(state, options = {}) {
|
|
|
607
615
|
* Response: { clientSecret, confirmationToken }
|
|
608
616
|
*
|
|
609
617
|
* @param {Object} state - Widget state: checkIn, checkOut, rooms, occupancies, selectedRoom, selectedRate, guest
|
|
610
|
-
* @param {Object} [options] - { propertyKey: string }
|
|
611
|
-
* @returns {{ rate_identifier: string, key: string, metadata: Object }}
|
|
618
|
+
* @param {Object} [options] - { propertyKey: string, sandbox?: boolean }
|
|
619
|
+
* @returns {{ rate_identifier: string, key: string, metadata: Object, sandbox?: boolean }}
|
|
612
620
|
*/
|
|
613
621
|
function buildPaymentIntentPayload(state, options = {}) {
|
|
614
622
|
const rawKey = options.propertyKey != null && options.propertyKey !== '' ? String(options.propertyKey).trim() : '';
|
|
615
623
|
const propertyKey = rawKey && rawKey.startsWith('book_engine_') ? rawKey : '';
|
|
624
|
+
const sandbox = options.sandbox === true;
|
|
616
625
|
const checkIn = state.checkIn;
|
|
617
626
|
const checkOut = state.checkOut;
|
|
618
627
|
const nights = checkIn && checkOut
|
|
@@ -632,7 +641,7 @@ function buildPaymentIntentPayload(state, options = {}) {
|
|
|
632
641
|
...(Array.isArray(o.childrenAges) && o.childrenAges.length ? { child_ages: o.childrenAges } : {}),
|
|
633
642
|
}));
|
|
634
643
|
|
|
635
|
-
|
|
644
|
+
const out = {
|
|
636
645
|
rate_identifier: rateIdentifier,
|
|
637
646
|
key: propertyKey,
|
|
638
647
|
metadata: {
|
|
@@ -650,6 +659,8 @@ function buildPaymentIntentPayload(state, options = {}) {
|
|
|
650
659
|
source_transaction: 'booking engine',
|
|
651
660
|
},
|
|
652
661
|
};
|
|
662
|
+
if (sandbox) out.sandbox = true;
|
|
663
|
+
return out;
|
|
653
664
|
}
|
|
654
665
|
|
|
655
666
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Config from CONFIG_URL (default https://ai.thehotelplanet.com/load-config). No .env used.
|
|
3
3
|
* Property key is NOT from load-config; installers must set it (VITE_PROPERTY_KEY in app .env or propertyKey option).
|
|
4
4
|
*/
|
|
5
|
-
export const STRIPE_PUBLISHABLE_KEY = '
|
|
5
|
+
export const STRIPE_PUBLISHABLE_KEY = 'pk_live_51T0SdzEj1UJpIPAMQQUH55w3Dj1E07CihP3iTOVO1IM4mMVAJvE86BiUmzGQKuNPPl05btyY39lRca8PSzRzZ9K700CncmOGQ0';
|
|
6
6
|
export const API_BASE_URL = 'https://ai.thehotelplanet.com';
|
|
7
7
|
export const VITE_CONFIG_BASE_URL = 'https://ai.thehotelplanet.com/api';
|
|
8
8
|
export const VITE_AWS_S3_PATH = 'https://nuitee-s3-temp.s3.us-west-1.amazonaws.com';
|
|
@@ -31,6 +31,8 @@ const BookingWidget = ({
|
|
|
31
31
|
apiSecret = '',
|
|
32
32
|
propertyId = '',
|
|
33
33
|
propertyKey = '',
|
|
34
|
+
/** 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it. */
|
|
35
|
+
mode = '',
|
|
34
36
|
availabilityBaseUrl = '',
|
|
35
37
|
propertyBaseUrl = '',
|
|
36
38
|
s3BaseUrl = '',
|
|
@@ -47,20 +49,23 @@ const BookingWidget = ({
|
|
|
47
49
|
const env = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env : {};
|
|
48
50
|
const apiBase = (env.VITE_API_BASE_URL || API_BASE_URL || '').replace(/\/$/, '');
|
|
49
51
|
const effectivePaymentIntentUrl = (apiBase ? apiBase + '/proxy/create-payment-intent' : '').trim();
|
|
52
|
+
const isSandbox = mode === 'sandbox';
|
|
53
|
+
const hasPropertyKey = !!(propertyKey && String(propertyKey).trim());
|
|
50
54
|
const createPaymentIntent = useMemo(() => {
|
|
51
55
|
if (typeof createPaymentIntentProp === 'function') return createPaymentIntentProp;
|
|
52
56
|
if (!effectivePaymentIntentUrl) return null;
|
|
53
57
|
return async (payload) => {
|
|
58
|
+
const url = effectivePaymentIntentUrl + (isSandbox ? '?sandbox=true' : '');
|
|
54
59
|
const headers = { 'Content-Type': 'application/json' };
|
|
55
|
-
if (
|
|
56
|
-
const res = await fetch(
|
|
60
|
+
if (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
|
|
61
|
+
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
|
|
57
62
|
if (!res.ok) throw new Error(await res.text());
|
|
58
63
|
const data = await res.json();
|
|
59
64
|
const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
|
|
60
65
|
const confirmationToken = data.confirmationToken ?? data.confirmation_token ?? data.data?.confirmationToken ?? data.data?.confirmation_token;
|
|
61
66
|
return { clientSecret, confirmationToken };
|
|
62
67
|
};
|
|
63
|
-
}, [createPaymentIntentProp, effectivePaymentIntentUrl]);
|
|
68
|
+
}, [createPaymentIntentProp, effectivePaymentIntentUrl, isSandbox]);
|
|
64
69
|
const onBookingComplete = useMemo(() => onBookingCompleteProp ?? null, [onBookingCompleteProp]);
|
|
65
70
|
const hasStripe = Boolean(stripePublishableKey && typeof createPaymentIntent === 'function');
|
|
66
71
|
const STEPS = useMemo(() => buildSteps(hasStripe), [hasStripe]);
|
|
@@ -116,11 +121,12 @@ const BookingWidget = ({
|
|
|
116
121
|
s3BaseUrl: effectiveS3BaseUrl || undefined,
|
|
117
122
|
propertyId: propertyId || undefined,
|
|
118
123
|
propertyKey: propertyKey || undefined,
|
|
124
|
+
mode: mode === 'sandbox' ? 'sandbox' : undefined,
|
|
119
125
|
headers: apiSecret ? { 'X-API-Key': apiSecret } : undefined,
|
|
120
126
|
});
|
|
121
127
|
}
|
|
122
128
|
return null;
|
|
123
|
-
}, [bookingApiProp, effectiveApiBaseUrl, effectiveAvailabilityBaseUrl, effectivePropertyBaseUrl, effectiveS3BaseUrl, apiSecret, propertyId, propertyKey]);
|
|
129
|
+
}, [bookingApiProp, effectiveApiBaseUrl, effectiveAvailabilityBaseUrl, effectivePropertyBaseUrl, effectiveS3BaseUrl, apiSecret, propertyId, propertyKey, mode]);
|
|
124
130
|
|
|
125
131
|
useEffect(() => {
|
|
126
132
|
if (isOpen && onOpen) onOpen();
|
|
@@ -145,7 +151,7 @@ const BookingWidget = ({
|
|
|
145
151
|
setPaymentElementReady(false);
|
|
146
152
|
return;
|
|
147
153
|
}
|
|
148
|
-
const paymentIntentPayload = buildPaymentIntentPayload(state, { propertyKey: propertyKey || undefined });
|
|
154
|
+
const paymentIntentPayload = buildPaymentIntentPayload(state, { propertyKey: propertyKey || undefined, sandbox: isSandbox });
|
|
149
155
|
let mounted = true;
|
|
150
156
|
setPaymentElementReady(false);
|
|
151
157
|
setApiError(null);
|
|
@@ -182,7 +188,7 @@ const BookingWidget = ({
|
|
|
182
188
|
}
|
|
183
189
|
stripeRef.current = null;
|
|
184
190
|
};
|
|
185
|
-
}, [state.step, checkoutShowPaymentForm, stripePublishableKey, createPaymentIntent, onBookingComplete, state.checkIn, state.checkOut, state.rooms, state.occupancies, state.selectedRoom?.id, state.selectedRate?.id, state.guest, propertyId, propertyKey]);
|
|
191
|
+
}, [state.step, checkoutShowPaymentForm, stripePublishableKey, createPaymentIntent, onBookingComplete, state.checkIn, state.checkOut, state.rooms, state.occupancies, state.selectedRoom?.id, state.selectedRate?.id, state.guest, propertyId, propertyKey, isSandbox]);
|
|
186
192
|
|
|
187
193
|
// After Stripe confirms payment, poll confirmation endpoint every 2s until status === 'confirmed'
|
|
188
194
|
useEffect(() => {
|
|
@@ -192,7 +198,7 @@ const BookingWidget = ({
|
|
|
192
198
|
const pollOnce = async () => {
|
|
193
199
|
if (cancelled) return;
|
|
194
200
|
try {
|
|
195
|
-
const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}`;
|
|
201
|
+
const url = `${effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(String(confirmationToken).trim())}${isSandbox ? '?sandbox=true' : ''}`;
|
|
196
202
|
const res = await fetch(url, { method: 'POST' });
|
|
197
203
|
if (!res.ok) throw new Error(await res.text());
|
|
198
204
|
const data = await res.json();
|
|
@@ -212,7 +218,7 @@ const BookingWidget = ({
|
|
|
212
218
|
cancelled = true;
|
|
213
219
|
if (timer) clearTimeout(timer);
|
|
214
220
|
};
|
|
215
|
-
}, [state.step, confirmationToken, effectiveConfirmationBaseUrl]);
|
|
221
|
+
}, [state.step, confirmationToken, effectiveConfirmationBaseUrl, isSandbox]);
|
|
216
222
|
|
|
217
223
|
// Apply custom colors
|
|
218
224
|
useEffect(() => {
|
|
@@ -431,7 +437,7 @@ const BookingWidget = ({
|
|
|
431
437
|
const confirmReservation = () => {
|
|
432
438
|
const canSubmit = state.guest.firstName && state.guest.lastName && state.guest.email;
|
|
433
439
|
if (!canSubmit) return;
|
|
434
|
-
const payload = buildCheckoutPayload(state, { propertyId: propertyId || undefined, propertyKey: propertyKey || undefined });
|
|
440
|
+
const payload = buildCheckoutPayload(state, { propertyId: propertyId || undefined, propertyKey: propertyKey || undefined, sandbox: isSandbox });
|
|
435
441
|
// Summary step with Stripe: go to payment step (payment form loads there)
|
|
436
442
|
if (state.step === 'summary' && stripePublishableKey && typeof createPaymentIntent === 'function') {
|
|
437
443
|
setApiError(null);
|
|
@@ -1175,7 +1181,12 @@ const BookingWidget = ({
|
|
|
1175
1181
|
onTransitionEnd={handleTransitionEnd}
|
|
1176
1182
|
>
|
|
1177
1183
|
<button className="booking-widget-close" onClick={requestClose}><X size={24} /></button>
|
|
1178
|
-
{renderStepIndicator()}
|
|
1184
|
+
{hasPropertyKey && renderStepIndicator()}
|
|
1185
|
+
{(!propertyKey || (typeof propertyKey === 'string' && !propertyKey.trim())) ? (
|
|
1186
|
+
<div className="booking-widget-property-key-message" role="alert" style={{ margin: '1.5em', padding: '1em', background: 'var(--destructive, #ef4444)', color: '#fff', borderRadius: '8px', fontSize: '0.9em' }}>
|
|
1187
|
+
The propertyKey is missing. Please provide it as a prop to load rooms from the API.
|
|
1188
|
+
</div>
|
|
1189
|
+
) : (
|
|
1179
1190
|
<div className="booking-widget-step-content">
|
|
1180
1191
|
{state.step === 'dates' && renderDatesStep()}
|
|
1181
1192
|
{state.step === 'rooms' && renderRoomsStep()}
|
|
@@ -1184,6 +1195,7 @@ const BookingWidget = ({
|
|
|
1184
1195
|
{state.step === 'payment' && renderPaymentStep()}
|
|
1185
1196
|
{state.step === 'confirmation' && renderConfirmationStep()}
|
|
1186
1197
|
</div>
|
|
1198
|
+
)}
|
|
1187
1199
|
</div>
|
|
1188
1200
|
</>
|
|
1189
1201
|
);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<button class="booking-widget-close" @click="requestClose">
|
|
11
11
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
12
12
|
</button>
|
|
13
|
-
<div v-if="state.step !== 'confirmation'" class="booking-widget-step-indicator">
|
|
13
|
+
<div v-if="hasPropertyKey && state.step !== 'confirmation'" class="booking-widget-step-indicator">
|
|
14
14
|
<template v-for="(step, i) in STEPS" :key="step.key">
|
|
15
15
|
<div class="step-item">
|
|
16
16
|
<span
|
|
@@ -34,7 +34,10 @@
|
|
|
34
34
|
</span>
|
|
35
35
|
</template>
|
|
36
36
|
</div>
|
|
37
|
-
<div class="booking-widget-
|
|
37
|
+
<div v-if="!propertyKey || !String(propertyKey).trim()" class="booking-widget-property-key-message" role="alert" style="margin: 1.5em; padding: 1em; background: var(--destructive, #ef4444); color: #fff; border-radius: 8px; font-size: 0.9em;">
|
|
38
|
+
The propertyKey is missing. Please provide it as a prop to load rooms from the API.
|
|
39
|
+
</div>
|
|
40
|
+
<div v-else class="booking-widget-step-content">
|
|
38
41
|
<!-- Dates Step -->
|
|
39
42
|
<div v-if="state.step === 'dates'">
|
|
40
43
|
<h2 class="step-title">Plan Your Stay</h2>
|
|
@@ -467,6 +470,8 @@ export default {
|
|
|
467
470
|
propertyId: { type: [String, Number], default: '' },
|
|
468
471
|
/** Property key/hash for decrypt/pref (pass from your app instead of env). */
|
|
469
472
|
propertyKey: { type: String, default: '' },
|
|
473
|
+
/** 'sandbox' to pass sandbox=true on all proxy/API calls; omit or 'live' to not pass it. */
|
|
474
|
+
mode: { type: String, default: '' },
|
|
470
475
|
availabilityBaseUrl: { type: String, default: '' },
|
|
471
476
|
propertyBaseUrl: { type: String, default: '' },
|
|
472
477
|
s3BaseUrl: { type: String, default: '' },
|
|
@@ -556,9 +561,10 @@ export default {
|
|
|
556
561
|
const url = this.effectivePaymentIntentUrl;
|
|
557
562
|
if (!url) return null;
|
|
558
563
|
return async (payload) => {
|
|
564
|
+
const requestUrl = url + (this.isSandbox ? '?sandbox=true' : '');
|
|
559
565
|
const headers = { 'Content-Type': 'application/json' };
|
|
560
|
-
if (
|
|
561
|
-
const res = await fetch(
|
|
566
|
+
if (requestUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
|
|
567
|
+
const res = await fetch(requestUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
|
|
562
568
|
if (!res.ok) throw new Error(await res.text());
|
|
563
569
|
const data = await res.json();
|
|
564
570
|
const clientSecret = data.clientSecret ?? data.client_secret ?? data.data?.clientSecret ?? data.data?.client_secret ?? data.paymentIntent?.client_secret;
|
|
@@ -566,6 +572,12 @@ export default {
|
|
|
566
572
|
return { clientSecret, confirmationToken };
|
|
567
573
|
};
|
|
568
574
|
},
|
|
575
|
+
isSandbox() {
|
|
576
|
+
return this.mode === 'sandbox';
|
|
577
|
+
},
|
|
578
|
+
hasPropertyKey() {
|
|
579
|
+
return !!(this.propertyKey != null && this.propertyKey !== '' && String(this.propertyKey).trim() !== '');
|
|
580
|
+
},
|
|
569
581
|
bookingApiRef() {
|
|
570
582
|
if (this.bookingApi && typeof this.bookingApi.fetchRooms === 'function') return this.bookingApi;
|
|
571
583
|
if ((this.effectiveApiBaseUrl || this.propertyKey) && typeof createBookingApi === 'function') {
|
|
@@ -576,6 +588,7 @@ export default {
|
|
|
576
588
|
s3BaseUrl: this.effectiveS3BaseUrl || undefined,
|
|
577
589
|
propertyId: this.propertyId != null && this.propertyId !== '' ? String(this.propertyId) : undefined,
|
|
578
590
|
propertyKey: this.propertyKey || undefined,
|
|
591
|
+
mode: this.mode === 'sandbox' ? 'sandbox' : undefined,
|
|
579
592
|
headers: this.apiSecret ? { 'X-API-Key': this.apiSecret } : undefined,
|
|
580
593
|
});
|
|
581
594
|
}
|
|
@@ -721,7 +734,7 @@ export default {
|
|
|
721
734
|
async fetchConfirmationDetails(token) {
|
|
722
735
|
const t = String(token || '').trim();
|
|
723
736
|
if (!t) throw new Error('Missing confirmation token');
|
|
724
|
-
const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}`;
|
|
737
|
+
const url = `${this.effectiveConfirmationBaseUrl}/proxy/confirmation/${encodeURIComponent(t)}${this.isSandbox ? '?sandbox=true' : ''}`;
|
|
725
738
|
const res = await fetch(url, { method: 'POST' });
|
|
726
739
|
if (!res.ok) throw new Error(await res.text());
|
|
727
740
|
return await res.json();
|
|
@@ -750,7 +763,7 @@ export default {
|
|
|
750
763
|
},
|
|
751
764
|
async loadStripePaymentElement() {
|
|
752
765
|
if (this.state.step !== 'payment' || !this.checkoutShowPaymentForm || !this.stripePublishableKey || typeof this.effectiveCreatePaymentIntent !== 'function') return;
|
|
753
|
-
const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined });
|
|
766
|
+
const paymentIntentPayload = buildPaymentIntentPayload(this.state, { propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
|
|
754
767
|
this.paymentElementReady = false;
|
|
755
768
|
try {
|
|
756
769
|
await this.$nextTick();
|
|
@@ -859,7 +872,7 @@ export default {
|
|
|
859
872
|
},
|
|
860
873
|
confirmReservation() {
|
|
861
874
|
if (!this.canSubmit) return;
|
|
862
|
-
const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined });
|
|
875
|
+
const payload = buildCheckoutPayload(this.state, { propertyId: this.propertyId != null && this.propertyId !== '' ? this.propertyId : undefined, propertyKey: this.propertyKey || undefined, sandbox: this.isSandbox });
|
|
863
876
|
// Summary step with Stripe: go to payment step (form loads there)
|
|
864
877
|
if (this.state.step === 'summary' && this.stripePublishableKey && typeof this.effectiveCreatePaymentIntent === 'function') {
|
|
865
878
|
this.apiError = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuitee/booking-widget",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|