@resira/ui 0.4.11 → 0.4.12

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
@@ -1,11 +1,11 @@
1
- # @resira/ui v0.3.2
1
+ # @resira/ui v0.4.x
2
2
 
3
3
  React booking UI for Resira. It includes a ready-to-embed widget, modal flow, provider, hooks, and lower-level components for custom booking experiences.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @resira/ui@0.3.2 @resira/sdk@0.3.1
8
+ npm install @resira/ui@latest @resira/sdk@latest
9
9
  ```
10
10
 
11
11
  Peer dependencies:
@@ -139,6 +139,40 @@ The provider loads public configuration such as:
139
139
 
140
140
  You can override those values locally through `config` when needed.
141
141
 
142
+ ## Promoter mode (fast / low-bandwidth)
143
+
144
+ Use promoter mode when staff are creating bookings on-the-spot in unstable mobile networks.
145
+
146
+ ```tsx
147
+ <ResiraProvider
148
+ apiKey="resira_live_..."
149
+ domain="watersport"
150
+ config={{
151
+ promoterMode: {
152
+ enabled: true,
153
+ contactMode: "phone-required",
154
+ disableImages: true,
155
+ disablePromoValidation: true,
156
+ cacheTtlMs: 300000,
157
+ useStaleDataOnError: true,
158
+ autoAdvanceAvailability: true,
159
+ hidePromoInput: false,
160
+ },
161
+ }}
162
+ >
163
+ <ResiraBookingWidget />
164
+ </ResiraProvider>
165
+ ```
166
+
167
+ Promoter mode defaults are optimized for speed:
168
+
169
+ - Hides step indicator for a cleaner, faster flow
170
+ - Hides heavy service/resource images
171
+ - Caches resources/products/availability responses
172
+ - Falls back to stale cached data if network fails
173
+ - Uses phone-first guest validation by default
174
+ - Skips promo live-validation API call until payment/booking submit
175
+
142
176
  ## Hooks and components
143
177
 
144
178
  Main exports:
package/dist/index.cjs CHANGED
@@ -168,6 +168,17 @@ var DOMAIN_DEFAULTS = {
168
168
  availableDurations: [30, 45, 60, 90]
169
169
  }
170
170
  };
171
+ var DEFAULT_PROMOTER_MODE = {
172
+ enabled: false,
173
+ contactMode: "phone-required",
174
+ disableImages: true,
175
+ disablePromoValidation: true,
176
+ hidePromoInput: false,
177
+ cacheTtlMs: 5 * 60 * 1e3,
178
+ useStaleDataOnError: true,
179
+ autoAdvanceAvailability: true,
180
+ showStepIndicator: false
181
+ };
171
182
  function ResiraProvider({
172
183
  apiKey,
173
184
  resourceId,
@@ -225,7 +236,11 @@ function ResiraProvider({
225
236
  const visibleServiceCount = config?.visibleServiceCount ?? 4;
226
237
  const groupServicesByCategory = config?.groupServicesByCategory ?? true;
227
238
  const renderServiceCard = config?.renderServiceCard;
228
- const showStepIndicator = config?.showStepIndicator ?? true;
239
+ const promoterMode = react.useMemo(
240
+ () => ({ ...DEFAULT_PROMOTER_MODE, ...config?.promoterMode }),
241
+ [config?.promoterMode]
242
+ );
243
+ const showStepIndicator = config?.showStepIndicator ?? (promoterMode.enabled ? promoterMode.showStepIndicator : true);
229
244
  const deeplink = config?.deeplink;
230
245
  const deeplinkGuest = config?.deeplinkGuest;
231
246
  const onStepChange = config?.onStepChange;
@@ -264,12 +279,31 @@ function ResiraProvider({
264
279
  onStepChange,
265
280
  onBookingComplete,
266
281
  onError,
267
- checkoutSessionToken
282
+ checkoutSessionToken,
283
+ promoterMode
268
284
  }),
269
- [client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard, showStepIndicator, deeplink, deeplinkGuest, onStepChange, onBookingComplete, onError, checkoutSessionToken]
285
+ [client, resourceId, activeResourceId, setActiveResourceId, catalogMode, allowMultiSelect, domain, theme, locale, domainConfig, stripePublishableKey, termsText, waiverText, showWaiver, showTerms, showRemainingSpots, depositPercent, refundPolicy, onClose, classNames, serviceLayout, visibleServiceCount, groupServicesByCategory, renderServiceCard, showStepIndicator, deeplink, deeplinkGuest, onStepChange, onBookingComplete, onError, checkoutSessionToken, promoterMode]
270
286
  );
271
287
  return /* @__PURE__ */ jsxRuntime.jsx(ResiraContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-root", style: cssVars, children }) });
272
288
  }
289
+ var availabilityCache = /* @__PURE__ */ new Map();
290
+ var resourcesCache = /* @__PURE__ */ new Map();
291
+ var productsCache = /* @__PURE__ */ new Map();
292
+ function readCache(cache, key, now = Date.now()) {
293
+ const entry = cache.get(key);
294
+ if (!entry) return null;
295
+ if (entry.expiresAt <= now) return null;
296
+ return entry;
297
+ }
298
+ function readStaleCache(cache, key) {
299
+ return cache.get(key) ?? null;
300
+ }
301
+ function writeCache(cache, key, value, ttlMs) {
302
+ cache.set(key, {
303
+ value,
304
+ expiresAt: Date.now() + Math.max(1e3, ttlMs)
305
+ });
306
+ }
273
307
  async function getDishCompat(client, dishId) {
274
308
  const maybeClient = client;
275
309
  if (typeof maybeClient.getDish === "function") {
@@ -291,7 +325,7 @@ async function listDishesCompat(client) {
291
325
  throw new Error("Installed @resira/sdk does not support dish endpoints. Upgrade to @resira/sdk >= 0.3.0.");
292
326
  }
293
327
  function useAvailability(params, productId) {
294
- const { client, activeResourceId } = useResira();
328
+ const { client, activeResourceId, promoterMode } = useResira();
295
329
  const resourceId = activeResourceId ?? "";
296
330
  const [data, setData] = react.useState(null);
297
331
  const [loading, setLoading] = react.useState(false);
@@ -300,10 +334,23 @@ function useAvailability(params, productId) {
300
334
  const useProduct = !!productId;
301
335
  const enabled = useProduct ? productId.length > 0 : resourceId.length > 0;
302
336
  const paramsKey = JSON.stringify(params ?? null);
337
+ const cachePrefix = client.getBaseUrl();
338
+ const targetKey = useProduct ? `product:${productId}` : `resource:${resourceId}`;
339
+ const availabilityCacheKey = `${cachePrefix}:${targetKey}:${paramsKey}`;
303
340
  const completedKeyRef = react.useRef(null);
304
341
  const refetch = react.useCallback(
305
342
  async (overrideParams) => {
306
343
  if (!enabled) return;
344
+ if (promoterMode.enabled && overrideParams === void 0) {
345
+ const cached = readCache(availabilityCache, availabilityCacheKey);
346
+ if (cached) {
347
+ setData(cached.value);
348
+ setLoading(false);
349
+ setError(null);
350
+ completedKeyRef.current = paramsKey;
351
+ return;
352
+ }
353
+ }
307
354
  const fetchId = ++abortRef.current;
308
355
  completedKeyRef.current = null;
309
356
  setLoading(true);
@@ -314,9 +361,21 @@ function useAvailability(params, productId) {
314
361
  if (fetchId === abortRef.current) {
315
362
  setData(result);
316
363
  completedKeyRef.current = paramsKey;
364
+ if (promoterMode.enabled) {
365
+ writeCache(availabilityCache, availabilityCacheKey, result, promoterMode.cacheTtlMs);
366
+ }
317
367
  }
318
368
  } catch (err) {
319
369
  if (fetchId === abortRef.current) {
370
+ if (promoterMode.enabled && promoterMode.useStaleDataOnError) {
371
+ const stale = readStaleCache(availabilityCache, availabilityCacheKey);
372
+ if (stale) {
373
+ setData(stale.value);
374
+ setError(null);
375
+ completedKeyRef.current = paramsKey;
376
+ return;
377
+ }
378
+ }
320
379
  setError(
321
380
  err instanceof Error ? err.message : "Failed to load availability"
322
381
  );
@@ -329,7 +388,7 @@ function useAvailability(params, productId) {
329
388
  }
330
389
  },
331
390
  // eslint-disable-next-line react-hooks/exhaustive-deps
332
- [client, resourceId, productId, useProduct, paramsKey, enabled]
391
+ [client, resourceId, productId, useProduct, paramsKey, enabled, promoterMode, availabilityCacheKey]
333
392
  );
334
393
  react.useEffect(() => {
335
394
  if (!enabled) {
@@ -339,6 +398,16 @@ function useAvailability(params, productId) {
339
398
  completedKeyRef.current = null;
340
399
  return;
341
400
  }
401
+ if (promoterMode.enabled) {
402
+ const cached = readCache(availabilityCache, availabilityCacheKey);
403
+ if (cached) {
404
+ setData(cached.value);
405
+ setLoading(false);
406
+ setError(null);
407
+ completedKeyRef.current = paramsKey;
408
+ return;
409
+ }
410
+ }
342
411
  if (completedKeyRef.current === paramsKey) {
343
412
  return;
344
413
  }
@@ -353,9 +422,21 @@ function useAvailability(params, productId) {
353
422
  if (!cancelled && fetchId === abortRef.current) {
354
423
  setData(result);
355
424
  completedKeyRef.current = paramsKey;
425
+ if (promoterMode.enabled) {
426
+ writeCache(availabilityCache, availabilityCacheKey, result, promoterMode.cacheTtlMs);
427
+ }
356
428
  }
357
429
  } catch (err) {
358
430
  if (!cancelled && fetchId === abortRef.current) {
431
+ if (promoterMode.enabled && promoterMode.useStaleDataOnError) {
432
+ const stale = readStaleCache(availabilityCache, availabilityCacheKey);
433
+ if (stale) {
434
+ setData(stale.value);
435
+ setError(null);
436
+ completedKeyRef.current = paramsKey;
437
+ return;
438
+ }
439
+ }
359
440
  setError(
360
441
  err instanceof Error ? err.message : "Failed to load availability"
361
442
  );
@@ -371,7 +452,7 @@ function useAvailability(params, productId) {
371
452
  return () => {
372
453
  cancelled = true;
373
454
  };
374
- }, [client, resourceId, productId, useProduct, paramsKey, enabled]);
455
+ }, [client, resourceId, productId, useProduct, paramsKey, enabled, promoterMode, availabilityCacheKey]);
375
456
  return { data, loading, error, refetch };
376
457
  }
377
458
  function useReservation() {
@@ -409,22 +490,47 @@ function useReservation() {
409
490
  return { reservation, submitting, error, submit, reset };
410
491
  }
411
492
  function useResources() {
412
- const { client } = useResira();
493
+ const { client, promoterMode } = useResira();
413
494
  const [resources, setResources] = react.useState([]);
414
495
  const [loading, setLoading] = react.useState(true);
415
496
  const [error, setError] = react.useState(null);
416
497
  react.useEffect(() => {
417
498
  let cancelled = false;
499
+ const cacheKey = `${client.getBaseUrl()}:resources`;
500
+ if (promoterMode.enabled) {
501
+ const cached = readCache(resourcesCache, cacheKey);
502
+ if (cached) {
503
+ setResources(cached.value);
504
+ setLoading(false);
505
+ setError(null);
506
+ return () => {
507
+ cancelled = true;
508
+ };
509
+ }
510
+ }
418
511
  async function fetchResources() {
419
512
  try {
420
513
  setLoading(true);
421
514
  setError(null);
422
515
  const data = await client.listResources();
423
516
  if (!cancelled) {
424
- setResources(data.resources ?? []);
517
+ const nextResources = data.resources ?? [];
518
+ setResources(nextResources);
519
+ if (promoterMode.enabled) {
520
+ writeCache(resourcesCache, cacheKey, nextResources, promoterMode.cacheTtlMs);
521
+ }
425
522
  }
426
523
  } catch (err) {
427
524
  if (!cancelled) {
525
+ if (promoterMode.enabled && promoterMode.useStaleDataOnError) {
526
+ const stale = readStaleCache(resourcesCache, cacheKey);
527
+ if (stale) {
528
+ setResources(stale.value);
529
+ setError(null);
530
+ setLoading(false);
531
+ return;
532
+ }
533
+ }
428
534
  const message = err instanceof Error ? err.message : "Failed to load resources";
429
535
  setError(message);
430
536
  }
@@ -438,26 +544,51 @@ function useResources() {
438
544
  return () => {
439
545
  cancelled = true;
440
546
  };
441
- }, [client]);
547
+ }, [client, promoterMode]);
442
548
  return { resources, loading, error };
443
549
  }
444
550
  function useProducts() {
445
- const { client } = useResira();
551
+ const { client, promoterMode } = useResira();
446
552
  const [products, setProducts] = react.useState([]);
447
553
  const [loading, setLoading] = react.useState(true);
448
554
  const [error, setError] = react.useState(null);
449
555
  react.useEffect(() => {
450
556
  let cancelled = false;
557
+ const cacheKey = `${client.getBaseUrl()}:products`;
558
+ if (promoterMode.enabled) {
559
+ const cached = readCache(productsCache, cacheKey);
560
+ if (cached) {
561
+ setProducts(cached.value);
562
+ setLoading(false);
563
+ setError(null);
564
+ return () => {
565
+ cancelled = true;
566
+ };
567
+ }
568
+ }
451
569
  async function fetchProducts() {
452
570
  try {
453
571
  setLoading(true);
454
572
  setError(null);
455
573
  const data = await client.listProducts();
456
574
  if (!cancelled) {
457
- setProducts(data.products ?? []);
575
+ const nextProducts = data.products ?? [];
576
+ setProducts(nextProducts);
577
+ if (promoterMode.enabled) {
578
+ writeCache(productsCache, cacheKey, nextProducts, promoterMode.cacheTtlMs);
579
+ }
458
580
  }
459
581
  } catch (err) {
460
582
  if (!cancelled) {
583
+ if (promoterMode.enabled && promoterMode.useStaleDataOnError) {
584
+ const stale = readStaleCache(productsCache, cacheKey);
585
+ if (stale) {
586
+ setProducts(stale.value);
587
+ setError(null);
588
+ setLoading(false);
589
+ return;
590
+ }
591
+ }
461
592
  const message = err instanceof Error ? err.message : "Failed to load services";
462
593
  setError(message);
463
594
  }
@@ -471,7 +602,7 @@ function useProducts() {
471
602
  return () => {
472
603
  cancelled = true;
473
604
  };
474
- }, [client]);
605
+ }, [client, promoterMode]);
475
606
  return { products, loading, error };
476
607
  }
477
608
  function usePaymentIntent() {
@@ -2183,7 +2314,14 @@ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2183
2314
  function validateGuestForm(values, labels, contactMode = "email-required") {
2184
2315
  const errors = {};
2185
2316
  if (!values.guestName.trim()) errors.guestName = labels.required;
2186
- if (contactMode === "either") {
2317
+ if (contactMode === "phone-required") {
2318
+ if (!values.guestPhone.trim()) {
2319
+ errors.guestPhone = labels.required;
2320
+ }
2321
+ if (values.guestEmail.trim() && !EMAIL_RE.test(values.guestEmail.trim())) {
2322
+ errors.guestEmail = labels.invalidEmail;
2323
+ }
2324
+ } else if (contactMode === "either") {
2187
2325
  const hasPhone = values.guestPhone.trim().length > 0;
2188
2326
  const hasEmail = values.guestEmail.trim().length > 0;
2189
2327
  if (!hasPhone && !hasEmail) {
@@ -2203,8 +2341,9 @@ function validateGuestForm(values, labels, contactMode = "email-required") {
2203
2341
  return errors;
2204
2342
  }
2205
2343
  function GuestForm({ values, onChange, errors = {} }) {
2206
- const { locale, domain } = useResira();
2344
+ const { locale, domain, promoterMode } = useResira();
2207
2345
  const isRestaurant = domain === "restaurant";
2346
+ const isPromoterPhoneMode = promoterMode.enabled && promoterMode.contactMode === "phone-required";
2208
2347
  const update = react.useCallback(
2209
2348
  (field, value) => {
2210
2349
  onChange({ ...values, [field]: value });
@@ -2320,7 +2459,7 @@ function GuestForm({ values, onChange, errors = {} }) {
2320
2459
  ] }),
2321
2460
  errors.guestName && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-field-error", children: errors.guestName })
2322
2461
  ] }),
2323
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field resira-field--half", children: [
2462
+ !isPromoterPhoneMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field resira-field--half", children: [
2324
2463
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "resira-field-label", children: [
2325
2464
  locale.email,
2326
2465
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "*" })
@@ -2343,21 +2482,43 @@ function GuestForm({ values, onChange, errors = {} }) {
2343
2482
  ] })
2344
2483
  ] }),
2345
2484
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field", children: [
2346
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "resira-field-label", children: locale.phone }),
2485
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "resira-field-label", children: [
2486
+ locale.phone,
2487
+ isPromoterPhoneMode && /* @__PURE__ */ jsxRuntime.jsx("span", { children: "*" })
2488
+ ] }),
2347
2489
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field-input-wrap", children: [
2348
2490
  /* @__PURE__ */ jsxRuntime.jsx(PhoneIcon, { size: 15, className: "resira-field-input-icon" }),
2349
2491
  /* @__PURE__ */ jsxRuntime.jsx(
2350
2492
  "input",
2351
2493
  {
2352
2494
  type: "tel",
2353
- className: "resira-field-input resira-field-input--sm",
2495
+ className: `resira-field-input resira-field-input--sm${errors.guestPhone ? " resira-field-input--error" : ""}`,
2354
2496
  value: values.guestPhone,
2355
2497
  onChange: (e) => update("guestPhone", e.target.value),
2356
2498
  placeholder: locale.phone,
2357
2499
  autoComplete: "tel"
2358
2500
  }
2359
2501
  )
2360
- ] })
2502
+ ] }),
2503
+ errors.guestPhone && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-field-error", children: errors.guestPhone })
2504
+ ] }),
2505
+ isPromoterPhoneMode && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field", children: [
2506
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "resira-field-label", children: locale.email }),
2507
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field-input-wrap", children: [
2508
+ /* @__PURE__ */ jsxRuntime.jsx(MailIcon, { size: 15, className: "resira-field-input-icon" }),
2509
+ /* @__PURE__ */ jsxRuntime.jsx(
2510
+ "input",
2511
+ {
2512
+ type: "email",
2513
+ className: `resira-field-input resira-field-input--sm${errors.guestEmail ? " resira-field-input--error" : ""}`,
2514
+ value: values.guestEmail,
2515
+ onChange: (e) => update("guestEmail", e.target.value),
2516
+ placeholder: locale.email,
2517
+ autoComplete: "email"
2518
+ }
2519
+ )
2520
+ ] }),
2521
+ errors.guestEmail && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "resira-field-error", children: errors.guestEmail })
2361
2522
  ] }),
2362
2523
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-field", children: [
2363
2524
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "resira-field-label", children: locale.notes }),
@@ -2385,7 +2546,9 @@ function WaiverConsent({
2385
2546
  discountCode,
2386
2547
  onDiscountCodeChange,
2387
2548
  onPromoValidated,
2388
- error
2549
+ error,
2550
+ disablePromoValidation = false,
2551
+ hidePromoInput = false
2389
2552
  }) {
2390
2553
  const { client, locale, showTerms, showWaiver, termsText, waiverText, refundPolicy } = useResira();
2391
2554
  const [promoValidating, setPromoValidating] = react.useState(false);
@@ -2393,6 +2556,12 @@ function WaiverConsent({
2393
2556
  const handleApplyPromo = react.useCallback(async () => {
2394
2557
  const code = discountCode.trim();
2395
2558
  if (!code) return;
2559
+ if (disablePromoValidation) {
2560
+ const deferredResult = { valid: true };
2561
+ setPromoResult(deferredResult);
2562
+ onPromoValidated?.(deferredResult);
2563
+ return;
2564
+ }
2396
2565
  setPromoValidating(true);
2397
2566
  setPromoResult(null);
2398
2567
  try {
@@ -2406,7 +2575,7 @@ function WaiverConsent({
2406
2575
  } finally {
2407
2576
  setPromoValidating(false);
2408
2577
  }
2409
- }, [client, discountCode, locale.discountInvalid, onPromoValidated]);
2578
+ }, [client, discountCode, locale.discountInvalid, onPromoValidated, disablePromoValidation]);
2410
2579
  const handleCodeChange = react.useCallback(
2411
2580
  (code) => {
2412
2581
  onDiscountCodeChange(code);
@@ -2450,7 +2619,7 @@ function WaiverConsent({
2450
2619
  ] }, i)) })
2451
2620
  ] }),
2452
2621
  error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-waiver-error", children: error }),
2453
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-waiver-promo", children: [
2622
+ !hidePromoInput && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-waiver-promo", children: [
2454
2623
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-waiver-promo-row", children: [
2455
2624
  /* @__PURE__ */ jsxRuntime.jsx(TagIcon, { size: 14 }),
2456
2625
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -2481,11 +2650,7 @@ function WaiverConsent({
2481
2650
  className: `resira-waiver-discount-result ${promoResult.valid ? "resira-waiver-discount-result--valid" : "resira-waiver-discount-result--invalid"}`,
2482
2651
  children: promoResult.valid ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2483
2652
  /* @__PURE__ */ jsxRuntime.jsx(CheckCircleIcon, { size: 13 }),
2484
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
2485
- locale.discountApplied,
2486
- " ",
2487
- promoResult.discountType === "percent" ? `(${promoResult.discountValue}% off)` : promoResult.discountType === "fixed" && promoResult.discountValue ? `(${(promoResult.discountValue / 100).toFixed(2)} ${promoResult.currency ?? ""} off)` : ""
2488
- ] })
2653
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: disablePromoValidation ? "Code saved. Final validation happens at payment." : `${locale.discountApplied} ${promoResult.discountType === "percent" ? `(${promoResult.discountValue}% off)` : promoResult.discountType === "fixed" && promoResult.discountValue ? `(${(promoResult.discountValue / 100).toFixed(2)} ${promoResult.currency ?? ""} off)` : ""}` })
2489
2654
  ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2490
2655
  /* @__PURE__ */ jsxRuntime.jsx(AlertCircleIcon, { size: 13 }),
2491
2656
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: promoResult.error ?? locale.discountInvalid })
@@ -2976,12 +3141,14 @@ function ResiraBookingWidget() {
2976
3141
  onStepChange,
2977
3142
  onBookingComplete,
2978
3143
  onError,
2979
- checkoutSessionToken
3144
+ checkoutSessionToken,
3145
+ promoterMode
2980
3146
  } = useResira();
2981
3147
  const isDateBased = domain === "rental";
2982
3148
  const isTimeBased = domain === "restaurant" || domain === "watersport" || domain === "service";
2983
3149
  const isServiceBased = domain === "watersport" || domain === "service";
2984
3150
  const hasPayment = !!stripePublishableKey;
3151
+ const promoterEnabled = promoterMode.enabled;
2985
3152
  const isCheckoutMode = !!checkoutSessionToken;
2986
3153
  const {
2987
3154
  session: checkoutSession,
@@ -3014,6 +3181,14 @@ function ResiraBookingWidget() {
3014
3181
  error: productsError
3015
3182
  } = useProducts();
3016
3183
  const [selectedProduct, setSelectedProduct] = react.useState(null);
3184
+ const displayResources = react.useMemo(
3185
+ () => promoterEnabled && promoterMode.disableImages ? resources.map((resource) => ({ ...resource, imageUrl: void 0, images: void 0 })) : resources,
3186
+ [resources, promoterEnabled, promoterMode.disableImages]
3187
+ );
3188
+ const displayProducts = react.useMemo(
3189
+ () => promoterEnabled && promoterMode.disableImages ? products.map((product) => ({ ...product, imageUrl: void 0, images: void 0 })) : products,
3190
+ [products, promoterEnabled, promoterMode.disableImages]
3191
+ );
3017
3192
  const [selection, setSelection] = react.useState({
3018
3193
  partySize: domainConfig.defaultPartySize ?? 2,
3019
3194
  duration: domainConfig.defaultDuration
@@ -3203,8 +3378,14 @@ function ResiraBookingWidget() {
3203
3378
  const handleDateSelect = react.useCallback(
3204
3379
  (start, end) => {
3205
3380
  setSelection((prev) => ({ ...prev, startDate: start, endDate: end }));
3381
+ if (promoterEnabled && promoterMode.autoAdvanceAvailability && step === "availability" && end) {
3382
+ const nextIdx = stepIndex(step, STEPS) + 1;
3383
+ if (nextIdx < STEPS.length) {
3384
+ setStep(STEPS[nextIdx]);
3385
+ }
3386
+ }
3206
3387
  },
3207
- []
3388
+ [promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS]
3208
3389
  );
3209
3390
  const handleSlotSelect = react.useCallback(
3210
3391
  (start, end) => {
@@ -3215,8 +3396,14 @@ function ResiraBookingWidget() {
3215
3396
  startTime: start,
3216
3397
  endTime: end
3217
3398
  }));
3399
+ if (promoterEnabled && promoterMode.autoAdvanceAvailability && step === "availability") {
3400
+ const nextIdx = stepIndex(step, STEPS) + 1;
3401
+ if (nextIdx < STEPS.length) {
3402
+ setStep(STEPS[nextIdx]);
3403
+ }
3404
+ }
3218
3405
  },
3219
- [slotDate]
3406
+ [slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS]
3220
3407
  );
3221
3408
  const handlePartySizeChange = react.useCallback((size) => {
3222
3409
  setSelection((prev) => ({ ...prev, partySize: size }));
@@ -3310,7 +3497,7 @@ function ResiraBookingWidget() {
3310
3497
  }
3311
3498
  }, [STEPS, currentIndex, step, resetPaymentIntent]);
3312
3499
  const handleDetailsSubmit = react.useCallback(async () => {
3313
- const contactMode = domain === "restaurant" ? "either" : "email-required";
3500
+ const contactMode = promoterEnabled ? promoterMode.contactMode : domain === "restaurant" ? "either" : "email-required";
3314
3501
  const errors = validateGuestForm(guest, {
3315
3502
  required: locale.required,
3316
3503
  invalidEmail: locale.invalidEmail,
@@ -3344,7 +3531,7 @@ function ResiraBookingWidget() {
3344
3531
  return;
3345
3532
  }
3346
3533
  setStep(STEPS[currentIndex + 1]);
3347
- }, [guest, locale, STEPS, currentIndex, domain, hasPayment, activeResourceId, selection, submit]);
3534
+ }, [guest, locale, STEPS, currentIndex, domain, hasPayment, activeResourceId, selection, submit, termsAccepted, waiverAccepted, promoterEnabled, promoterMode.contactMode]);
3348
3535
  const handlePaymentSuccess = react.useCallback(
3349
3536
  async (paymentIntentId) => {
3350
3537
  if (paymentIntent?.reservationId) {
@@ -3366,7 +3553,7 @@ function ResiraBookingWidget() {
3366
3553
  onError?.("payment_error", msg);
3367
3554
  }, [onError]);
3368
3555
  const handleCheckoutSubmit = react.useCallback(async () => {
3369
- const contactMode = "email-required";
3556
+ const contactMode = promoterEnabled ? promoterMode.contactMode : "email-required";
3370
3557
  const errors = validateGuestForm(guest, {
3371
3558
  required: locale.required,
3372
3559
  invalidEmail: locale.invalidEmail,
@@ -3393,7 +3580,7 @@ function ResiraBookingWidget() {
3393
3580
  } else {
3394
3581
  setStep("payment");
3395
3582
  }
3396
- }, [guest, locale, checkoutSession, termsAccepted, createPayment, paymentPayload, confirmPayment]);
3583
+ }, [guest, locale, checkoutSession, termsAccepted, createPayment, paymentPayload, confirmPayment, promoterEnabled, promoterMode.contactMode]);
3397
3584
  const handleSubmitNoPayment = react.useCallback(async () => {
3398
3585
  if (showTerms && !termsAccepted) {
3399
3586
  setTermsError(locale.termsRequired);
@@ -3519,8 +3706,8 @@ function ResiraBookingWidget() {
3519
3706
  step === "resource" && isServiceBased && /* @__PURE__ */ jsxRuntime.jsx(
3520
3707
  ProductSelector,
3521
3708
  {
3522
- products: products.filter((p) => p.active),
3523
- resources,
3709
+ products: displayProducts.filter((p) => p.active),
3710
+ resources: displayResources,
3524
3711
  selectedId: selectedProduct?.id,
3525
3712
  onSelect: handleProductSelect,
3526
3713
  loading: productsLoading,
@@ -3530,7 +3717,7 @@ function ResiraBookingWidget() {
3530
3717
  step === "resource" && !isServiceBased && /* @__PURE__ */ jsxRuntime.jsx(
3531
3718
  ResourcePicker,
3532
3719
  {
3533
- resources,
3720
+ resources: displayResources,
3534
3721
  selectedIds: selectedResourceIds,
3535
3722
  onSelect: handleResourceSelect,
3536
3723
  allowMultiSelect,
@@ -3665,7 +3852,9 @@ function ResiraBookingWidget() {
3665
3852
  discountCode,
3666
3853
  onDiscountCodeChange: setDiscountCode,
3667
3854
  onPromoValidated: handlePromoValidated,
3668
- error: termsError ?? void 0
3855
+ error: termsError ?? void 0,
3856
+ disablePromoValidation: promoterEnabled && promoterMode.disablePromoValidation,
3857
+ hidePromoInput: promoterEnabled && promoterMode.hidePromoInput
3669
3858
  }
3670
3859
  ) }),
3671
3860
  paymentError && hasPayment && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", style: { padding: "12px 0" }, children: [
@@ -3751,7 +3940,9 @@ function ResiraBookingWidget() {
3751
3940
  },
3752
3941
  onPromoValidated: () => {
3753
3942
  },
3754
- error: termsError ?? void 0
3943
+ error: termsError ?? void 0,
3944
+ disablePromoValidation: promoterEnabled && promoterMode.disablePromoValidation,
3945
+ hidePromoInput: promoterEnabled && promoterMode.hidePromoInput
3755
3946
  }
3756
3947
  ) }),
3757
3948
  paymentError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", style: { padding: "12px 0" }, children: [