@resira/ui 0.4.18 → 0.4.20
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/dist/index.cjs +313 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -2
- package/dist/index.d.ts +57 -2
- package/dist/index.js +313 -39
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -62,7 +62,10 @@ var DEFAULT_LOCALE = {
|
|
|
62
62
|
contactHint: "Please provide at least a phone number or email",
|
|
63
63
|
restaurantNotesPlaceholder: "Allergies, special requests...",
|
|
64
64
|
reserveTable: "Reserve a table",
|
|
65
|
-
reservationFor: "Reservation for"
|
|
65
|
+
reservationFor: "Reservation for",
|
|
66
|
+
slotHoldReserved: "Reserved for you",
|
|
67
|
+
slotSoldOut: "This slot just sold out. Pick another time.",
|
|
68
|
+
slotHoldExpired: "Your hold expired. Please choose a time again."
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
// src/theme.ts
|
|
@@ -240,6 +243,7 @@ function ResiraProvider({
|
|
|
240
243
|
() => ({ ...DEFAULT_PROMOTER_MODE, ...config?.promoterMode }),
|
|
241
244
|
[config?.promoterMode]
|
|
242
245
|
);
|
|
246
|
+
const slotHoldsEnabled = config?.slotHoldsEnabled ?? true;
|
|
243
247
|
const showStepIndicator = config?.showStepIndicator ?? (promoterMode.enabled ? promoterMode.showStepIndicator : true);
|
|
244
248
|
const deeplink = config?.deeplink;
|
|
245
249
|
const deeplinkGuest = config?.deeplinkGuest;
|
|
@@ -280,9 +284,10 @@ function ResiraProvider({
|
|
|
280
284
|
onBookingComplete,
|
|
281
285
|
onError,
|
|
282
286
|
checkoutSessionToken,
|
|
283
|
-
promoterMode
|
|
287
|
+
promoterMode,
|
|
288
|
+
slotHoldsEnabled
|
|
284
289
|
}),
|
|
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]
|
|
290
|
+
[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, slotHoldsEnabled]
|
|
286
291
|
);
|
|
287
292
|
return /* @__PURE__ */ jsxRuntime.jsx(ResiraContext.Provider, { value, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-root", style: cssVars, children }) });
|
|
288
293
|
}
|
|
@@ -3203,6 +3208,175 @@ function clampPartySize(value, bounds) {
|
|
|
3203
3208
|
if (!Number.isFinite(value)) return bounds.min;
|
|
3204
3209
|
return Math.min(bounds.max, Math.max(bounds.min, Math.round(value)));
|
|
3205
3210
|
}
|
|
3211
|
+
function parseSlotHoldError(err) {
|
|
3212
|
+
if (err instanceof sdk.ResiraApiError) {
|
|
3213
|
+
const raw = `${err.body.code ?? ""} ${err.body.error ?? ""}`.toUpperCase();
|
|
3214
|
+
const message2 = err.body.error || err.message;
|
|
3215
|
+
if (raw.includes("SLOT_UNAVAILABLE")) return { code: "SLOT_UNAVAILABLE", message: message2 };
|
|
3216
|
+
if (raw.includes("HOLD_EXPIRED")) return { code: "HOLD_EXPIRED", message: message2 };
|
|
3217
|
+
if (raw.includes("HOLD_MISMATCH")) return { code: "HOLD_MISMATCH", message: message2 };
|
|
3218
|
+
if (raw.includes("HOLD_NOT_FOUND")) return { code: "HOLD_NOT_FOUND", message: message2 };
|
|
3219
|
+
if (raw.includes("HOLD_NOT_ACTIVE")) return { code: "HOLD_NOT_ACTIVE", message: message2 };
|
|
3220
|
+
if (err.status === 409) return { code: "SLOT_UNAVAILABLE", message: message2 };
|
|
3221
|
+
return { code: "UNKNOWN", message: message2 };
|
|
3222
|
+
}
|
|
3223
|
+
const message = err instanceof Error ? err.message : "Slot hold failed";
|
|
3224
|
+
return { code: "UNKNOWN", message };
|
|
3225
|
+
}
|
|
3226
|
+
function useSlotHold(options) {
|
|
3227
|
+
const {
|
|
3228
|
+
client,
|
|
3229
|
+
enabled,
|
|
3230
|
+
productId,
|
|
3231
|
+
startTime,
|
|
3232
|
+
durationMinutes,
|
|
3233
|
+
partySize,
|
|
3234
|
+
resourceId,
|
|
3235
|
+
sessionToken,
|
|
3236
|
+
onSlotUnavailable,
|
|
3237
|
+
onHoldExpired
|
|
3238
|
+
} = options;
|
|
3239
|
+
const [lastHold, setLastHold] = react.useState(null);
|
|
3240
|
+
const [creating, setCreating] = react.useState(false);
|
|
3241
|
+
const [error, setError] = react.useState(null);
|
|
3242
|
+
const [parsedCode, setParsedCode] = react.useState(null);
|
|
3243
|
+
const [remainingSeconds, setRemainingSeconds] = react.useState(null);
|
|
3244
|
+
const holdIdRef = react.useRef(null);
|
|
3245
|
+
const serverOffsetMs = react.useRef(0);
|
|
3246
|
+
const expiredFired = react.useRef(false);
|
|
3247
|
+
const onHoldExpiredRef = react.useRef(onHoldExpired);
|
|
3248
|
+
onHoldExpiredRef.current = onHoldExpired;
|
|
3249
|
+
const onSlotUnavailableRef = react.useRef(onSlotUnavailable);
|
|
3250
|
+
onSlotUnavailableRef.current = onSlotUnavailable;
|
|
3251
|
+
const slotKey = react.useMemo(
|
|
3252
|
+
() => enabled && productId && startTime && durationMinutes && partySize ? `${productId}|${startTime}|${durationMinutes}|${partySize}|${resourceId ?? ""}|${sessionToken ?? ""}` : "",
|
|
3253
|
+
[enabled, productId, startTime, durationMinutes, partySize, resourceId, sessionToken]
|
|
3254
|
+
);
|
|
3255
|
+
const release = react.useCallback(async () => {
|
|
3256
|
+
const id = holdIdRef.current;
|
|
3257
|
+
if (!id) return;
|
|
3258
|
+
holdIdRef.current = null;
|
|
3259
|
+
try {
|
|
3260
|
+
await client.releaseSlotHold(id);
|
|
3261
|
+
} catch {
|
|
3262
|
+
}
|
|
3263
|
+
setLastHold(null);
|
|
3264
|
+
setRemainingSeconds(null);
|
|
3265
|
+
}, [client]);
|
|
3266
|
+
react.useEffect(() => {
|
|
3267
|
+
expiredFired.current = false;
|
|
3268
|
+
if (!enabled || !slotKey) {
|
|
3269
|
+
void release();
|
|
3270
|
+
setError(null);
|
|
3271
|
+
setParsedCode(null);
|
|
3272
|
+
setCreating(false);
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
let cancelled = false;
|
|
3276
|
+
async function run() {
|
|
3277
|
+
setCreating(true);
|
|
3278
|
+
setError(null);
|
|
3279
|
+
setParsedCode(null);
|
|
3280
|
+
try {
|
|
3281
|
+
if (holdIdRef.current) {
|
|
3282
|
+
const prev = holdIdRef.current;
|
|
3283
|
+
holdIdRef.current = null;
|
|
3284
|
+
try {
|
|
3285
|
+
await client.releaseSlotHold(prev);
|
|
3286
|
+
} catch {
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
const res = await client.createSlotHold(
|
|
3290
|
+
productId,
|
|
3291
|
+
{
|
|
3292
|
+
startTime,
|
|
3293
|
+
durationMinutes,
|
|
3294
|
+
partySize,
|
|
3295
|
+
...resourceId ? { resourceId } : {},
|
|
3296
|
+
...sessionToken ? { sessionToken } : {}
|
|
3297
|
+
}
|
|
3298
|
+
);
|
|
3299
|
+
if (cancelled) return;
|
|
3300
|
+
holdIdRef.current = res.holdId;
|
|
3301
|
+
serverOffsetMs.current = new Date(res.serverNow).getTime() - Date.now();
|
|
3302
|
+
setLastHold(res);
|
|
3303
|
+
const rem = Math.max(
|
|
3304
|
+
0,
|
|
3305
|
+
Math.floor(
|
|
3306
|
+
(new Date(res.expiresAt).getTime() - (Date.now() + serverOffsetMs.current)) / 1e3
|
|
3307
|
+
)
|
|
3308
|
+
);
|
|
3309
|
+
setRemainingSeconds(rem);
|
|
3310
|
+
} catch (err) {
|
|
3311
|
+
if (cancelled) return;
|
|
3312
|
+
holdIdRef.current = null;
|
|
3313
|
+
setLastHold(null);
|
|
3314
|
+
setRemainingSeconds(null);
|
|
3315
|
+
const parsed = parseSlotHoldError(err);
|
|
3316
|
+
setParsedCode(parsed.code);
|
|
3317
|
+
setError(parsed.message);
|
|
3318
|
+
if (parsed.code === "SLOT_UNAVAILABLE" || err instanceof sdk.ResiraApiError && err.status === 409) {
|
|
3319
|
+
onSlotUnavailableRef.current?.();
|
|
3320
|
+
}
|
|
3321
|
+
} finally {
|
|
3322
|
+
if (!cancelled) setCreating(false);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
void run();
|
|
3326
|
+
return () => {
|
|
3327
|
+
cancelled = true;
|
|
3328
|
+
};
|
|
3329
|
+
}, [client, enabled, slotKey, productId, startTime, durationMinutes, partySize, resourceId, sessionToken, release]);
|
|
3330
|
+
react.useEffect(() => {
|
|
3331
|
+
if (!lastHold?.expiresAt) {
|
|
3332
|
+
setRemainingSeconds(null);
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
const tick = () => {
|
|
3336
|
+
const end = new Date(lastHold.expiresAt).getTime();
|
|
3337
|
+
const now = Date.now() + serverOffsetMs.current;
|
|
3338
|
+
const rem = Math.max(0, Math.floor((end - now) / 1e3));
|
|
3339
|
+
setRemainingSeconds(rem);
|
|
3340
|
+
if (rem <= 0 && !expiredFired.current) {
|
|
3341
|
+
expiredFired.current = true;
|
|
3342
|
+
holdIdRef.current = null;
|
|
3343
|
+
onHoldExpiredRef.current?.();
|
|
3344
|
+
}
|
|
3345
|
+
};
|
|
3346
|
+
tick();
|
|
3347
|
+
const id = window.setInterval(tick, 1e3);
|
|
3348
|
+
return () => window.clearInterval(id);
|
|
3349
|
+
}, [lastHold]);
|
|
3350
|
+
react.useEffect(() => {
|
|
3351
|
+
return () => {
|
|
3352
|
+
const id = holdIdRef.current;
|
|
3353
|
+
if (id) {
|
|
3354
|
+
holdIdRef.current = null;
|
|
3355
|
+
void client.releaseSlotHold(id).catch(() => {
|
|
3356
|
+
});
|
|
3357
|
+
}
|
|
3358
|
+
};
|
|
3359
|
+
}, [client]);
|
|
3360
|
+
const holdReady = react.useMemo(() => {
|
|
3361
|
+
if (!enabled) return true;
|
|
3362
|
+
if (creating) return false;
|
|
3363
|
+
if (error) return false;
|
|
3364
|
+
return !!lastHold?.holdId;
|
|
3365
|
+
}, [enabled, creating, error, lastHold?.holdId]);
|
|
3366
|
+
return {
|
|
3367
|
+
holdId: lastHold?.holdId ?? null,
|
|
3368
|
+
holdExpiresAt: lastHold?.expiresAt ?? null,
|
|
3369
|
+
heldResourceId: lastHold?.resourceId ?? null,
|
|
3370
|
+
remainingSeconds,
|
|
3371
|
+
creating,
|
|
3372
|
+
error,
|
|
3373
|
+
parsedErrorCode: parsedCode,
|
|
3374
|
+
holdReady,
|
|
3375
|
+
release,
|
|
3376
|
+
serverNow: lastHold?.serverNow ?? null,
|
|
3377
|
+
lastHold
|
|
3378
|
+
};
|
|
3379
|
+
}
|
|
3206
3380
|
function todayStr() {
|
|
3207
3381
|
const d = /* @__PURE__ */ new Date();
|
|
3208
3382
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
@@ -3221,6 +3395,12 @@ function normalizeDurationMinutes(value) {
|
|
|
3221
3395
|
}
|
|
3222
3396
|
return void 0;
|
|
3223
3397
|
}
|
|
3398
|
+
function formatHoldCountdown(totalSeconds) {
|
|
3399
|
+
const s = Math.max(0, Math.floor(totalSeconds));
|
|
3400
|
+
const m = Math.floor(s / 60);
|
|
3401
|
+
const r = s % 60;
|
|
3402
|
+
return `${m}:${String(r).padStart(2, "0")}`;
|
|
3403
|
+
}
|
|
3224
3404
|
function buildSteps(domain, hasPayment, catalogMode) {
|
|
3225
3405
|
const steps = [];
|
|
3226
3406
|
if (domain === "watersport" || domain === "service" || catalogMode && domain !== "restaurant") {
|
|
@@ -3251,6 +3431,7 @@ var STEP_LABELS = {
|
|
|
3251
3431
|
};
|
|
3252
3432
|
function ResiraBookingWidget() {
|
|
3253
3433
|
const {
|
|
3434
|
+
client,
|
|
3254
3435
|
domain,
|
|
3255
3436
|
locale,
|
|
3256
3437
|
domainConfig,
|
|
@@ -3271,7 +3452,8 @@ function ResiraBookingWidget() {
|
|
|
3271
3452
|
onBookingComplete,
|
|
3272
3453
|
onError,
|
|
3273
3454
|
checkoutSessionToken,
|
|
3274
|
-
promoterMode
|
|
3455
|
+
promoterMode,
|
|
3456
|
+
slotHoldsEnabled
|
|
3275
3457
|
} = useResira();
|
|
3276
3458
|
const isDateBased = domain === "rental";
|
|
3277
3459
|
const isTimeBased = domain === "restaurant" || domain === "watersport" || domain === "service";
|
|
@@ -3403,6 +3585,26 @@ function ResiraBookingWidget() {
|
|
|
3403
3585
|
confirming: confirmingPayment,
|
|
3404
3586
|
reset: resetPaymentIntent
|
|
3405
3587
|
} = usePaymentIntent();
|
|
3588
|
+
const slotHold = useSlotHold({
|
|
3589
|
+
client,
|
|
3590
|
+
enabled: slotHoldsEnabled && isServiceBased && !isCheckoutMode && !!selectedProduct?.id && !!selection.startTime && !!selection.endTime && !!activeDurationMinutes,
|
|
3591
|
+
productId: selectedProduct?.id,
|
|
3592
|
+
startTime: selection.startTime,
|
|
3593
|
+
durationMinutes: activeDurationMinutes ?? void 0,
|
|
3594
|
+
partySize: selection.partySize,
|
|
3595
|
+
resourceId: activeResourceId ?? selectedProduct?.equipmentIds?.[0],
|
|
3596
|
+
onSlotUnavailable: () => {
|
|
3597
|
+
void refetch();
|
|
3598
|
+
setSelection((s) => ({ ...s, startTime: void 0, endTime: void 0 }));
|
|
3599
|
+
onError?.("slot_unavailable", locale.slotSoldOut);
|
|
3600
|
+
},
|
|
3601
|
+
onHoldExpired: () => {
|
|
3602
|
+
setStep("availability");
|
|
3603
|
+
resetPaymentIntent();
|
|
3604
|
+
void refetch();
|
|
3605
|
+
onError?.("hold_expired", locale.slotHoldExpired);
|
|
3606
|
+
}
|
|
3607
|
+
});
|
|
3406
3608
|
const [paymentSubmitRequestId, setPaymentSubmitRequestId] = react.useState(0);
|
|
3407
3609
|
const [paymentFormReady, setPaymentFormReady] = react.useState(false);
|
|
3408
3610
|
const [paymentFormSubmitting, setPaymentFormSubmitting] = react.useState(false);
|
|
@@ -3414,7 +3616,8 @@ function ResiraBookingWidget() {
|
|
|
3414
3616
|
guestEmail: guest.guestEmail.trim() || checkoutSession.guestEmail || void 0,
|
|
3415
3617
|
guestPhone: guest.guestPhone.trim() || void 0,
|
|
3416
3618
|
notes: guest.notes.trim() || void 0,
|
|
3417
|
-
termsAccepted: termsAccepted || void 0
|
|
3619
|
+
termsAccepted: termsAccepted || void 0,
|
|
3620
|
+
...checkoutSession.holdId ? { holdId: checkoutSession.holdId } : {}
|
|
3418
3621
|
};
|
|
3419
3622
|
}
|
|
3420
3623
|
const resourceId = activeResourceId ?? selectedProduct?.equipmentIds?.[0] ?? "";
|
|
@@ -3432,9 +3635,10 @@ function ResiraBookingWidget() {
|
|
|
3432
3635
|
notes: guest.notes.trim() || void 0,
|
|
3433
3636
|
promoCode: discountCode.trim() || void 0,
|
|
3434
3637
|
termsAccepted: termsAccepted || void 0,
|
|
3435
|
-
waiverAccepted: waiverAccepted || void 0
|
|
3638
|
+
waiverAccepted: waiverAccepted || void 0,
|
|
3639
|
+
...slotHold.holdId ? { holdId: slotHold.holdId } : {}
|
|
3436
3640
|
};
|
|
3437
|
-
}, [activeResourceId, selectedProduct, selection, guest, discountCode, termsAccepted, waiverAccepted, isCheckoutMode, checkoutSession, checkoutSessionToken, activeDurationMinutes]);
|
|
3641
|
+
}, [activeResourceId, selectedProduct, selection, guest, discountCode, termsAccepted, waiverAccepted, isCheckoutMode, checkoutSession, checkoutSessionToken, activeDurationMinutes, slotHold.holdId]);
|
|
3438
3642
|
const blockedDates = react.useMemo(() => {
|
|
3439
3643
|
const dates = calendarData?.dates?.blockedDates ?? availability?.dates?.blockedDates ?? [];
|
|
3440
3644
|
return new Set(dates);
|
|
@@ -3523,15 +3727,24 @@ function ResiraBookingWidget() {
|
|
|
3523
3727
|
}
|
|
3524
3728
|
if (step === "availability") {
|
|
3525
3729
|
if (isDateBased) return !!selection.startDate && !!selection.endDate;
|
|
3526
|
-
|
|
3730
|
+
const base = !!selection.startTime && !!selection.endTime;
|
|
3731
|
+
if (!base) return false;
|
|
3732
|
+
if (slotHoldsEnabled && isServiceBased) {
|
|
3733
|
+
return slotHold.holdReady && !slotHold.creating;
|
|
3734
|
+
}
|
|
3735
|
+
return true;
|
|
3527
3736
|
}
|
|
3528
3737
|
if (step === "terms") {
|
|
3529
3738
|
const needTerms = showTerms && !termsAccepted;
|
|
3530
3739
|
const needWaiver = showWaiver && !waiverAccepted;
|
|
3531
|
-
|
|
3740
|
+
if (needTerms || needWaiver) return false;
|
|
3741
|
+
if (slotHoldsEnabled && isServiceBased && !isCheckoutMode) {
|
|
3742
|
+
return !!slotHold.holdId && slotHold.remainingSeconds != null && slotHold.remainingSeconds > 0;
|
|
3743
|
+
}
|
|
3744
|
+
return true;
|
|
3532
3745
|
}
|
|
3533
3746
|
return true;
|
|
3534
|
-
}, [step, isDateBased, isServiceBased, selection, selectedResourceIds, selectedProduct, termsAccepted, waiverAccepted, showTerms, showWaiver]);
|
|
3747
|
+
}, [step, isDateBased, isServiceBased, selection, selectedResourceIds, selectedProduct, termsAccepted, waiverAccepted, showTerms, showWaiver, slotHoldsEnabled, slotHold, isCheckoutMode]);
|
|
3535
3748
|
const handleDateSelect = react.useCallback(
|
|
3536
3749
|
(start, end) => {
|
|
3537
3750
|
setSelection((prev) => ({ ...prev, startDate: start, endDate: end }));
|
|
@@ -3562,14 +3775,15 @@ function ResiraBookingWidget() {
|
|
|
3562
3775
|
}
|
|
3563
3776
|
return next;
|
|
3564
3777
|
});
|
|
3565
|
-
|
|
3778
|
+
const deferPromoterAdvance = slotHoldsEnabled && isServiceBased;
|
|
3779
|
+
if (promoterEnabled && promoterMode.autoAdvanceAvailability && !deferPromoterAdvance && step === "availability") {
|
|
3566
3780
|
const nextIdx = stepIndex(step, STEPS) + 1;
|
|
3567
3781
|
if (nextIdx < STEPS.length) {
|
|
3568
3782
|
setStep(STEPS[nextIdx]);
|
|
3569
3783
|
}
|
|
3570
3784
|
}
|
|
3571
3785
|
},
|
|
3572
|
-
[slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS, selectedProduct?.id]
|
|
3786
|
+
[slotDate, promoterEnabled, promoterMode.autoAdvanceAvailability, step, STEPS, selectedProduct?.id, slotHoldsEnabled, isServiceBased]
|
|
3573
3787
|
);
|
|
3574
3788
|
const handlePartySizeChange = react.useCallback((size) => {
|
|
3575
3789
|
const clamped = clampPartySize(size, selectedProductPartyBounds);
|
|
@@ -3624,11 +3838,14 @@ function ResiraBookingWidget() {
|
|
|
3624
3838
|
persistedPartySize ?? prev.partySize,
|
|
3625
3839
|
bounds
|
|
3626
3840
|
);
|
|
3841
|
+
const hasSavedSlot = !!saved.startTime && !!saved.endTime;
|
|
3627
3842
|
const defaultDuration = saved.duration ?? product.durationPricing?.[0]?.durationMinutes ?? product.durationMinutes ?? prev.duration;
|
|
3628
3843
|
return {
|
|
3629
|
-
...prev,
|
|
3630
|
-
...saved,
|
|
3631
3844
|
productId: product.id,
|
|
3845
|
+
startDate: saved.startDate ?? slotDate,
|
|
3846
|
+
endDate: saved.endDate ?? slotDate,
|
|
3847
|
+
startTime: hasSavedSlot ? saved.startTime : void 0,
|
|
3848
|
+
endTime: hasSavedSlot ? saved.endTime : void 0,
|
|
3632
3849
|
duration: defaultDuration,
|
|
3633
3850
|
partySize: nextPartySize
|
|
3634
3851
|
};
|
|
@@ -3649,7 +3866,7 @@ function ResiraBookingWidget() {
|
|
|
3649
3866
|
}
|
|
3650
3867
|
}
|
|
3651
3868
|
},
|
|
3652
|
-
[setActiveResourceId, domainConfig, partySizeByProductId, selection.partySize, step, isServiceBased, STEPS, selectedProduct?.id]
|
|
3869
|
+
[setActiveResourceId, domainConfig, partySizeByProductId, selection.partySize, step, isServiceBased, STEPS, selectedProduct?.id, slotDate]
|
|
3653
3870
|
);
|
|
3654
3871
|
const handleResourceSelect = react.useCallback(
|
|
3655
3872
|
(resourceId) => {
|
|
@@ -3965,27 +4182,36 @@ function ResiraBookingWidget() {
|
|
|
3965
4182
|
minStay: domainConfig.minStay
|
|
3966
4183
|
}
|
|
3967
4184
|
),
|
|
3968
|
-
isTimeBased && /* @__PURE__ */ jsxRuntime.
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
4185
|
+
isTimeBased && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4186
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4187
|
+
TimeSlotPicker,
|
|
4188
|
+
{
|
|
4189
|
+
timeSlots: availability?.timeSlots ?? [],
|
|
4190
|
+
selectedDate: slotDate,
|
|
4191
|
+
onDateChange: handleSlotDateChange,
|
|
4192
|
+
selectedSlot: selection.startTime,
|
|
4193
|
+
onSlotSelect: handleSlotSelect,
|
|
4194
|
+
partySize: selection.partySize,
|
|
4195
|
+
onPartySizeChange: handlePartySizeChange,
|
|
4196
|
+
showPartySize: true,
|
|
4197
|
+
showDuration: domain === "watersport" || domain === "service",
|
|
4198
|
+
selectedDuration: activeDurationMinutes,
|
|
4199
|
+
onDurationChange: handleDurationChange,
|
|
4200
|
+
minPartySizeOverride: selectedProductPartyBounds.min,
|
|
4201
|
+
maxPartySizeOverride: selectedProductPartyBounds.max,
|
|
4202
|
+
durationPricing: activeRiderDurationPricing ?? selectedProduct?.durationPricing,
|
|
4203
|
+
currency,
|
|
4204
|
+
showRemainingSpots
|
|
4205
|
+
}
|
|
4206
|
+
),
|
|
4207
|
+
slotHoldsEnabled && isServiceBased && selection.startTime && selection.endTime && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4208
|
+
slotHold.creating && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { marginTop: 10, fontSize: 14, opacity: 0.85 }, role: "status", children: locale.loading }),
|
|
4209
|
+
slotHold.error && !slotHold.creating && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", style: { marginTop: 10 }, role: "alert", children: [
|
|
4210
|
+
/* @__PURE__ */ jsxRuntime.jsx(AlertCircleIcon, { size: 18 }),
|
|
4211
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: slotHold.error })
|
|
4212
|
+
] })
|
|
4213
|
+
] })
|
|
4214
|
+
] }),
|
|
3989
4215
|
isServiceBased && selectedProduct && computedPrice && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-price-preview", children: [
|
|
3990
4216
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-price-preview-row", children: [
|
|
3991
4217
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
@@ -4048,6 +4274,30 @@ function ResiraBookingWidget() {
|
|
|
4048
4274
|
] })
|
|
4049
4275
|
] }),
|
|
4050
4276
|
step === "terms" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4277
|
+
slotHoldsEnabled && isServiceBased && !isCheckoutMode && slotHold.holdId && slotHold.remainingSeconds != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4278
|
+
"div",
|
|
4279
|
+
{
|
|
4280
|
+
style: {
|
|
4281
|
+
marginBottom: 14,
|
|
4282
|
+
padding: "10px 12px",
|
|
4283
|
+
borderRadius: 8,
|
|
4284
|
+
fontSize: 14,
|
|
4285
|
+
background: slotHold.remainingSeconds <= 60 ? "rgba(220, 38, 38, 0.08)" : "rgba(59, 130, 246, 0.08)",
|
|
4286
|
+
border: `1px solid ${slotHold.remainingSeconds <= 60 ? "rgba(220, 38, 38, 0.22)" : "rgba(59, 130, 246, 0.2)"}`
|
|
4287
|
+
},
|
|
4288
|
+
role: "status",
|
|
4289
|
+
"aria-live": "polite",
|
|
4290
|
+
children: [
|
|
4291
|
+
locale.slotHoldReserved,
|
|
4292
|
+
" ",
|
|
4293
|
+
/* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
|
|
4294
|
+
"(",
|
|
4295
|
+
formatHoldCountdown(slotHold.remainingSeconds),
|
|
4296
|
+
")"
|
|
4297
|
+
] })
|
|
4298
|
+
]
|
|
4299
|
+
}
|
|
4300
|
+
),
|
|
4051
4301
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4052
4302
|
SummaryPreview,
|
|
4053
4303
|
{
|
|
@@ -4167,6 +4417,30 @@ function ResiraBookingWidget() {
|
|
|
4167
4417
|
] })
|
|
4168
4418
|
] }),
|
|
4169
4419
|
step === "payment" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4420
|
+
slotHoldsEnabled && isServiceBased && !isCheckoutMode && slotHold.holdId && slotHold.remainingSeconds != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4421
|
+
"div",
|
|
4422
|
+
{
|
|
4423
|
+
style: {
|
|
4424
|
+
marginBottom: 14,
|
|
4425
|
+
padding: "10px 12px",
|
|
4426
|
+
borderRadius: 8,
|
|
4427
|
+
fontSize: 14,
|
|
4428
|
+
background: slotHold.remainingSeconds <= 60 ? "rgba(220, 38, 38, 0.08)" : "rgba(59, 130, 246, 0.08)",
|
|
4429
|
+
border: `1px solid ${slotHold.remainingSeconds <= 60 ? "rgba(220, 38, 38, 0.22)" : "rgba(59, 130, 246, 0.2)"}`
|
|
4430
|
+
},
|
|
4431
|
+
role: "status",
|
|
4432
|
+
"aria-live": "polite",
|
|
4433
|
+
children: [
|
|
4434
|
+
locale.slotHoldReserved,
|
|
4435
|
+
" ",
|
|
4436
|
+
/* @__PURE__ */ jsxRuntime.jsxs("strong", { children: [
|
|
4437
|
+
"(",
|
|
4438
|
+
formatHoldCountdown(slotHold.remainingSeconds),
|
|
4439
|
+
")"
|
|
4440
|
+
] })
|
|
4441
|
+
]
|
|
4442
|
+
}
|
|
4443
|
+
),
|
|
4170
4444
|
paymentError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "resira-error", children: [
|
|
4171
4445
|
/* @__PURE__ */ jsxRuntime.jsx(AlertCircleIcon, { size: 24 }),
|
|
4172
4446
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "resira-error-message", children: paymentError })
|
|
@@ -4337,7 +4611,7 @@ function ResiraBookingWidget() {
|
|
|
4337
4611
|
onClick: () => {
|
|
4338
4612
|
void handleStartPayment();
|
|
4339
4613
|
},
|
|
4340
|
-
disabled: footerBusy,
|
|
4614
|
+
disabled: footerBusy || slotHoldsEnabled && isServiceBased && !isCheckoutMode && (!slotHold.holdId || (slotHold.remainingSeconds ?? 0) <= 0),
|
|
4341
4615
|
children: creatingPayment ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4342
4616
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "resira-spinner", style: { width: 16, height: 16 } }),
|
|
4343
4617
|
locale.processingPayment
|
|
@@ -4879,6 +5153,7 @@ exports.ViewfinderIcon = ViewfinderIcon;
|
|
|
4879
5153
|
exports.WaiverConsent = WaiverConsent;
|
|
4880
5154
|
exports.XIcon = XIcon;
|
|
4881
5155
|
exports.fetchServices = fetchServices;
|
|
5156
|
+
exports.parseSlotHoldError = parseSlotHoldError;
|
|
4882
5157
|
exports.resolveServicePrice = resolveServicePrice;
|
|
4883
5158
|
exports.resolveTheme = resolveTheme;
|
|
4884
5159
|
exports.themeToCSS = themeToCSS;
|
|
@@ -4892,6 +5167,7 @@ exports.useReservation = useReservation;
|
|
|
4892
5167
|
exports.useResira = useResira;
|
|
4893
5168
|
exports.useResources = useResources;
|
|
4894
5169
|
exports.useServices = useServices;
|
|
5170
|
+
exports.useSlotHold = useSlotHold;
|
|
4895
5171
|
exports.validateGuestForm = validateGuestForm;
|
|
4896
5172
|
//# sourceMappingURL=index.cjs.map
|
|
4897
5173
|
//# sourceMappingURL=index.cjs.map
|