@resira/ui 0.4.10 → 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 +36 -2
- package/dist/index.cjs +230 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +230 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# @resira/ui v0.
|
|
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@
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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.
|
|
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:
|
|
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.
|
|
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) {
|
|
@@ -3359,18 +3546,14 @@ function ResiraBookingWidget() {
|
|
|
3359
3546
|
const result = await createPayment(paymentPayload);
|
|
3360
3547
|
if (!result) return;
|
|
3361
3548
|
if (result.amountNow === 0) {
|
|
3362
|
-
if (result.reservationId) {
|
|
3363
|
-
const confirmed = await confirmPayment(result.paymentIntentId, result.reservationId);
|
|
3364
|
-
if (!confirmed) return;
|
|
3365
|
-
}
|
|
3366
3549
|
setStep("confirmation");
|
|
3367
3550
|
}
|
|
3368
|
-
}, [createPayment, paymentPayload
|
|
3551
|
+
}, [createPayment, paymentPayload]);
|
|
3369
3552
|
const handlePaymentError = react.useCallback((msg) => {
|
|
3370
3553
|
onError?.("payment_error", msg);
|
|
3371
3554
|
}, [onError]);
|
|
3372
3555
|
const handleCheckoutSubmit = react.useCallback(async () => {
|
|
3373
|
-
const contactMode = "email-required";
|
|
3556
|
+
const contactMode = promoterEnabled ? promoterMode.contactMode : "email-required";
|
|
3374
3557
|
const errors = validateGuestForm(guest, {
|
|
3375
3558
|
required: locale.required,
|
|
3376
3559
|
invalidEmail: locale.invalidEmail,
|
|
@@ -3393,15 +3576,11 @@ function ResiraBookingWidget() {
|
|
|
3393
3576
|
const result = await createPayment(paymentPayload);
|
|
3394
3577
|
if (!result) return;
|
|
3395
3578
|
if (result.amountNow === 0) {
|
|
3396
|
-
if (result.reservationId) {
|
|
3397
|
-
const confirmed = await confirmPayment(result.paymentIntentId, result.reservationId);
|
|
3398
|
-
if (!confirmed) return;
|
|
3399
|
-
}
|
|
3400
3579
|
setStep("confirmation");
|
|
3401
3580
|
} else {
|
|
3402
3581
|
setStep("payment");
|
|
3403
3582
|
}
|
|
3404
|
-
}, [guest, locale, checkoutSession, termsAccepted, createPayment, paymentPayload, confirmPayment]);
|
|
3583
|
+
}, [guest, locale, checkoutSession, termsAccepted, createPayment, paymentPayload, confirmPayment, promoterEnabled, promoterMode.contactMode]);
|
|
3405
3584
|
const handleSubmitNoPayment = react.useCallback(async () => {
|
|
3406
3585
|
if (showTerms && !termsAccepted) {
|
|
3407
3586
|
setTermsError(locale.termsRequired);
|
|
@@ -3527,8 +3706,8 @@ function ResiraBookingWidget() {
|
|
|
3527
3706
|
step === "resource" && isServiceBased && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3528
3707
|
ProductSelector,
|
|
3529
3708
|
{
|
|
3530
|
-
products:
|
|
3531
|
-
resources,
|
|
3709
|
+
products: displayProducts.filter((p) => p.active),
|
|
3710
|
+
resources: displayResources,
|
|
3532
3711
|
selectedId: selectedProduct?.id,
|
|
3533
3712
|
onSelect: handleProductSelect,
|
|
3534
3713
|
loading: productsLoading,
|
|
@@ -3538,7 +3717,7 @@ function ResiraBookingWidget() {
|
|
|
3538
3717
|
step === "resource" && !isServiceBased && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3539
3718
|
ResourcePicker,
|
|
3540
3719
|
{
|
|
3541
|
-
resources,
|
|
3720
|
+
resources: displayResources,
|
|
3542
3721
|
selectedIds: selectedResourceIds,
|
|
3543
3722
|
onSelect: handleResourceSelect,
|
|
3544
3723
|
allowMultiSelect,
|
|
@@ -3673,7 +3852,9 @@ function ResiraBookingWidget() {
|
|
|
3673
3852
|
discountCode,
|
|
3674
3853
|
onDiscountCodeChange: setDiscountCode,
|
|
3675
3854
|
onPromoValidated: handlePromoValidated,
|
|
3676
|
-
error: termsError ?? void 0
|
|
3855
|
+
error: termsError ?? void 0,
|
|
3856
|
+
disablePromoValidation: promoterEnabled && promoterMode.disablePromoValidation,
|
|
3857
|
+
hidePromoInput: promoterEnabled && promoterMode.hidePromoInput
|
|
3677
3858
|
}
|
|
3678
3859
|
) }),
|
|
3679
3860
|
paymentError && hasPayment && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", style: { padding: "12px 0" }, children: [
|
|
@@ -3759,7 +3940,9 @@ function ResiraBookingWidget() {
|
|
|
3759
3940
|
},
|
|
3760
3941
|
onPromoValidated: () => {
|
|
3761
3942
|
},
|
|
3762
|
-
error: termsError ?? void 0
|
|
3943
|
+
error: termsError ?? void 0,
|
|
3944
|
+
disablePromoValidation: promoterEnabled && promoterMode.disablePromoValidation,
|
|
3945
|
+
hidePromoInput: promoterEnabled && promoterMode.hidePromoInput
|
|
3763
3946
|
}
|
|
3764
3947
|
) }),
|
|
3765
3948
|
paymentError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", style: { padding: "12px 0" }, children: [
|