@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 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="node_modules/@nuitee/booking-widget/dist/booking-widget.css">
98
- <script src="node_modules/@nuitee/booking-widget/dist/booking-widget-standalone.js"></script>
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: 'node_modules/@nuitee/booking-widget/dist/booking-widget.css',
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="node_modules/@nuitee/booking-widget/dist/booking-widget.css">
175
- <script src="node_modules/@nuitee/booking-widget/dist/booking-widget-standalone.js"></script>
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: 'node_modules/@nuitee/booking-widget/dist/booking-widget.css',
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__='pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';})();
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?key=${encodeURIComponent(propertyKey)}`
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
- return {
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
- return {
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 || (typeof window !== 'undefined' && window.__BOOKING_WIDGET_API_BASE_URL__) || 'https://ai.thehotelplanet.com',
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 (this.state.step === 'confirmation') {
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>$ ${total.toLocaleString()}</strong><small>total</small></div>
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>$ ` + Math.round(amt).toLocaleString() + `</span></div>`;
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>$ ` + Math.round(vat.value || 0).toLocaleString() + `</span></div>`;
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>$ ` + Math.round(roomTotal).toLocaleString() + `</span></div>
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 &amp; 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">$ ${total.toLocaleString()}</span>
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')}&nbsp; ${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">$ ${total.toLocaleString()}</span>
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')}&nbsp; ${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 paymentIntentPayload = buildPaymentIntentPayload ? buildPaymentIntentPayload(this.state, { propertyKey: this.options.propertyKey ?? undefined }) : (buildCheckoutPayload ? buildCheckoutPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined }) : null);
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 payload = buildPayload ? buildPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined }) : null;
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();
@@ -1,4 +1,4 @@
1
- (function(){'use strict';window.__BOOKING_WIDGET_API_BASE_URL__='https://ai.thehotelplanet.com';window.__BOOKING_WIDGET_STRIPE_KEY__='pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';})();
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
- return fetch(apiBase.replace(/\/$/, '') + '/proxy/create-payment-intent', {
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
- if (this.state.step === 'confirmation') {
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 payload = buildPayload ? buildPayload(this.state, { propertyId: this.options.propertyId ?? undefined, propertyKey: this.options.propertyKey ?? undefined }) : null;
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();
@@ -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?key=${encodeURIComponent(propertyKey)}`
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
- return {
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
- return {
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 = 'pk_test_51T0Se5EvQaQIixshRA8gbsI40NubMGwHlSWZmQM2LAx3jpVO0ThzVifqdTiMeGOyBfLNe8V9G8POGu8SjkvWOStM00H0i1U3uz';
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 (effectivePaymentIntentUrl.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
56
- const res = await fetch(effectivePaymentIntentUrl, { method: 'POST', headers, body: JSON.stringify(payload) });
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-step-content">
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 (url.includes('ngrok')) headers['ngrok-skip-browser-warning'] = 'true';
561
- const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(payload) });
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.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",