@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 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
- return !!selection.startTime && !!selection.endTime;
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
- return !needTerms && !needWaiver;
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
- if (promoterEnabled && promoterMode.autoAdvanceAvailability && step === "availability") {
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.jsx(
3969
- TimeSlotPicker,
3970
- {
3971
- timeSlots: availability?.timeSlots ?? [],
3972
- selectedDate: slotDate,
3973
- onDateChange: handleSlotDateChange,
3974
- selectedSlot: selection.startTime,
3975
- onSlotSelect: handleSlotSelect,
3976
- partySize: selection.partySize,
3977
- onPartySizeChange: handlePartySizeChange,
3978
- showPartySize: true,
3979
- showDuration: domain === "watersport" || domain === "service",
3980
- selectedDuration: activeDurationMinutes,
3981
- onDurationChange: handleDurationChange,
3982
- minPartySizeOverride: selectedProductPartyBounds.min,
3983
- maxPartySizeOverride: selectedProductPartyBounds.max,
3984
- durationPricing: activeRiderDurationPricing ?? selectedProduct?.durationPricing,
3985
- currency,
3986
- showRemainingSpots
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