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