@seatlayer/js 0.8.0 → 0.9.0
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 +159 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +73 -3
- package/dist/index.d.ts +73 -3
- package/dist/index.js +159 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.cts
CHANGED
|
@@ -271,6 +271,40 @@ declare class EmbeddedDesigner {
|
|
|
271
271
|
* so plain host CSS can restyle too.
|
|
272
272
|
*/
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Stable checkout-handoff contract (P4). Passed as the THIRD argument to
|
|
276
|
+
* `onCheckout(hold, seats, handoff)` — additive, so the legacy `(hold, seats)`
|
|
277
|
+
* shape used by DesiPass web-v2 (SDK 0.7.3+) is untouched. This is the object to
|
|
278
|
+
* build your order against: it is self-contained (holdId, expiry, currency, and
|
|
279
|
+
* per-line tier + price) and never changes shape across minor releases.
|
|
280
|
+
*/
|
|
281
|
+
interface CheckoutLineItem {
|
|
282
|
+
/** Seat label (or GA synthetic-unit label). */
|
|
283
|
+
label: string;
|
|
284
|
+
/** Chart object id (row/booth/GA area) the unit belongs to. */
|
|
285
|
+
objectId: string;
|
|
286
|
+
objectType: 'seat' | 'booth' | 'ga';
|
|
287
|
+
categoryKey: string;
|
|
288
|
+
/** Chosen ticket tier id (Adult/Child/…), or null when the category has no tiers. */
|
|
289
|
+
tierId: string | null;
|
|
290
|
+
/** Unit price in MAJOR currency units (e.g. 45 = 45.00). Server-authoritative. */
|
|
291
|
+
unitPrice: number;
|
|
292
|
+
/** ISO-4217, resolved server-side (per-event override → org → USD). */
|
|
293
|
+
currency: string;
|
|
294
|
+
quantity: number;
|
|
295
|
+
}
|
|
296
|
+
interface CheckoutHandoff {
|
|
297
|
+
/** Server hold id — pass this to YOUR book call. */
|
|
298
|
+
holdId: string;
|
|
299
|
+
/** Epoch ms the hold expires (after any extensions). */
|
|
300
|
+
expiresAt: number;
|
|
301
|
+
/** ISO-4217 currency for the whole order. */
|
|
302
|
+
currency: string;
|
|
303
|
+
/** Priced line items (tier + unit price + currency), server-authoritative. */
|
|
304
|
+
lineItems: CheckoutLineItem[];
|
|
305
|
+
/** Convenience total in major units (Σ unitPrice × quantity). */
|
|
306
|
+
total: number;
|
|
307
|
+
}
|
|
274
308
|
/** Host theme overrides — any subset; unset keys fall back to the org's chart theme, then defaults. */
|
|
275
309
|
interface SeatPickerTheme {
|
|
276
310
|
/** Brand accent (CTA, active chips, hold pill). */
|
|
@@ -332,9 +366,18 @@ interface SeatPickerOptions {
|
|
|
332
366
|
seatView?: boolean;
|
|
333
367
|
/**
|
|
334
368
|
* Buyer pressed the CTA and the hold succeeded — hand off to YOUR checkout.
|
|
335
|
-
*
|
|
369
|
+
* `hold` and `seats` are the legacy args (unchanged since 0.6). `handoff` (P4)
|
|
370
|
+
* is the stable, self-contained {@link CheckoutHandoff} to build your order
|
|
371
|
+
* against — holdId, expiry, currency and priced line items. Prefer it.
|
|
336
372
|
*/
|
|
337
|
-
onCheckout?: (hold: HoldResult, seats: PickerSeat[]) => void;
|
|
373
|
+
onCheckout?: (hold: HoldResult, seats: PickerSeat[], handoff: CheckoutHandoff) => void;
|
|
374
|
+
/**
|
|
375
|
+
* The held seats were BOOKED (P4) — your server completed payment and the
|
|
376
|
+
* booking landed over the realtime channel while the widget was still open.
|
|
377
|
+
* The widget shows a success state; use this to advance your own UI (receipt,
|
|
378
|
+
* redirect). Fires once per hold.
|
|
379
|
+
*/
|
|
380
|
+
onBooked?: (handoff: CheckoutHandoff) => void;
|
|
338
381
|
/** Selection changed (tap or best-available). */
|
|
339
382
|
onSelectionChange?: (seats: PickerSeat[]) => void;
|
|
340
383
|
/** The open hold expired server-side (widget already reset itself). */
|
|
@@ -356,6 +399,14 @@ declare class SeatPicker {
|
|
|
356
399
|
private toastTimer;
|
|
357
400
|
private currency;
|
|
358
401
|
private hold;
|
|
402
|
+
/** Latest server expiry for the open hold (moves on extend). */
|
|
403
|
+
private holdExpiresAt;
|
|
404
|
+
/** True once we handed off to checkout — arms booked-confirmation detection. */
|
|
405
|
+
private handedOff;
|
|
406
|
+
/** Guards single onBooked + single success overlay per hold. */
|
|
407
|
+
private bookedShown;
|
|
408
|
+
private extendEl;
|
|
409
|
+
private bookedEl;
|
|
359
410
|
private gaQty;
|
|
360
411
|
private tipEl;
|
|
361
412
|
private tipPos;
|
|
@@ -386,6 +437,10 @@ declare class SeatPicker {
|
|
|
386
437
|
static open(options: Omit<SeatPickerOptions, 'container'>): Promise<SeatPicker>;
|
|
387
438
|
constructor(options: SeatPickerOptions);
|
|
388
439
|
render(): Promise<this>;
|
|
440
|
+
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
441
|
+
private buildExtendPrompt;
|
|
442
|
+
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
443
|
+
private buildBookedOverlay;
|
|
389
444
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
390
445
|
private buildArenaChrome;
|
|
391
446
|
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
@@ -418,6 +473,21 @@ declare class SeatPicker {
|
|
|
418
473
|
private handleCta;
|
|
419
474
|
private startHoldTimer;
|
|
420
475
|
private stopHoldTimer;
|
|
476
|
+
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
477
|
+
private setExtendPrompt;
|
|
478
|
+
private handleExtend;
|
|
479
|
+
/**
|
|
480
|
+
* Fire the booked-confirmation state once the buyer's held seats settle to
|
|
481
|
+
* booked. The controller clears its own hold the moment every held label reads
|
|
482
|
+
* 'booked' over the realtime channel (clearBookedHoldIfSettled), and this runs
|
|
483
|
+
* on the same onStatusChange — so `currentHold() === null` while we still hold
|
|
484
|
+
* a checkout handoff means "sold", not expired (expiry clears via onHoldExpired
|
|
485
|
+
* on a different path, which nulls this.hold first).
|
|
486
|
+
*/
|
|
487
|
+
private detectBooked;
|
|
488
|
+
private showBooked;
|
|
489
|
+
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
490
|
+
private buildHandoff;
|
|
421
491
|
private toast;
|
|
422
492
|
private placeTooltip;
|
|
423
493
|
private updateTooltip;
|
|
@@ -427,4 +497,4 @@ declare class SeatPicker {
|
|
|
427
497
|
destroy(): void;
|
|
428
498
|
}
|
|
429
499
|
|
|
430
|
-
export { ApiError, type BestAvailableResult, EmbeddedDesigner, type EmbeddedDesignerEventType, type EmbeddedDesignerMessage, type EmbeddedDesignerOptions, type GAAreaAvailability, type HoldConflict, type HoldLineItem, type HoldResult, SeatPicker, type SeatPickerOptions, type SeatPickerTheme, SeatingChart, type SeatingChartOptions, type SelectedSeat };
|
|
500
|
+
export { ApiError, type BestAvailableResult, type CheckoutHandoff, type CheckoutLineItem, EmbeddedDesigner, type EmbeddedDesignerEventType, type EmbeddedDesignerMessage, type EmbeddedDesignerOptions, type GAAreaAvailability, type HoldConflict, type HoldLineItem, type HoldResult, SeatPicker, type SeatPickerOptions, type SeatPickerTheme, SeatingChart, type SeatingChartOptions, type SelectedSeat };
|
package/dist/index.d.ts
CHANGED
|
@@ -271,6 +271,40 @@ declare class EmbeddedDesigner {
|
|
|
271
271
|
* so plain host CSS can restyle too.
|
|
272
272
|
*/
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Stable checkout-handoff contract (P4). Passed as the THIRD argument to
|
|
276
|
+
* `onCheckout(hold, seats, handoff)` — additive, so the legacy `(hold, seats)`
|
|
277
|
+
* shape used by DesiPass web-v2 (SDK 0.7.3+) is untouched. This is the object to
|
|
278
|
+
* build your order against: it is self-contained (holdId, expiry, currency, and
|
|
279
|
+
* per-line tier + price) and never changes shape across minor releases.
|
|
280
|
+
*/
|
|
281
|
+
interface CheckoutLineItem {
|
|
282
|
+
/** Seat label (or GA synthetic-unit label). */
|
|
283
|
+
label: string;
|
|
284
|
+
/** Chart object id (row/booth/GA area) the unit belongs to. */
|
|
285
|
+
objectId: string;
|
|
286
|
+
objectType: 'seat' | 'booth' | 'ga';
|
|
287
|
+
categoryKey: string;
|
|
288
|
+
/** Chosen ticket tier id (Adult/Child/…), or null when the category has no tiers. */
|
|
289
|
+
tierId: string | null;
|
|
290
|
+
/** Unit price in MAJOR currency units (e.g. 45 = 45.00). Server-authoritative. */
|
|
291
|
+
unitPrice: number;
|
|
292
|
+
/** ISO-4217, resolved server-side (per-event override → org → USD). */
|
|
293
|
+
currency: string;
|
|
294
|
+
quantity: number;
|
|
295
|
+
}
|
|
296
|
+
interface CheckoutHandoff {
|
|
297
|
+
/** Server hold id — pass this to YOUR book call. */
|
|
298
|
+
holdId: string;
|
|
299
|
+
/** Epoch ms the hold expires (after any extensions). */
|
|
300
|
+
expiresAt: number;
|
|
301
|
+
/** ISO-4217 currency for the whole order. */
|
|
302
|
+
currency: string;
|
|
303
|
+
/** Priced line items (tier + unit price + currency), server-authoritative. */
|
|
304
|
+
lineItems: CheckoutLineItem[];
|
|
305
|
+
/** Convenience total in major units (Σ unitPrice × quantity). */
|
|
306
|
+
total: number;
|
|
307
|
+
}
|
|
274
308
|
/** Host theme overrides — any subset; unset keys fall back to the org's chart theme, then defaults. */
|
|
275
309
|
interface SeatPickerTheme {
|
|
276
310
|
/** Brand accent (CTA, active chips, hold pill). */
|
|
@@ -332,9 +366,18 @@ interface SeatPickerOptions {
|
|
|
332
366
|
seatView?: boolean;
|
|
333
367
|
/**
|
|
334
368
|
* Buyer pressed the CTA and the hold succeeded — hand off to YOUR checkout.
|
|
335
|
-
*
|
|
369
|
+
* `hold` and `seats` are the legacy args (unchanged since 0.6). `handoff` (P4)
|
|
370
|
+
* is the stable, self-contained {@link CheckoutHandoff} to build your order
|
|
371
|
+
* against — holdId, expiry, currency and priced line items. Prefer it.
|
|
336
372
|
*/
|
|
337
|
-
onCheckout?: (hold: HoldResult, seats: PickerSeat[]) => void;
|
|
373
|
+
onCheckout?: (hold: HoldResult, seats: PickerSeat[], handoff: CheckoutHandoff) => void;
|
|
374
|
+
/**
|
|
375
|
+
* The held seats were BOOKED (P4) — your server completed payment and the
|
|
376
|
+
* booking landed over the realtime channel while the widget was still open.
|
|
377
|
+
* The widget shows a success state; use this to advance your own UI (receipt,
|
|
378
|
+
* redirect). Fires once per hold.
|
|
379
|
+
*/
|
|
380
|
+
onBooked?: (handoff: CheckoutHandoff) => void;
|
|
338
381
|
/** Selection changed (tap or best-available). */
|
|
339
382
|
onSelectionChange?: (seats: PickerSeat[]) => void;
|
|
340
383
|
/** The open hold expired server-side (widget already reset itself). */
|
|
@@ -356,6 +399,14 @@ declare class SeatPicker {
|
|
|
356
399
|
private toastTimer;
|
|
357
400
|
private currency;
|
|
358
401
|
private hold;
|
|
402
|
+
/** Latest server expiry for the open hold (moves on extend). */
|
|
403
|
+
private holdExpiresAt;
|
|
404
|
+
/** True once we handed off to checkout — arms booked-confirmation detection. */
|
|
405
|
+
private handedOff;
|
|
406
|
+
/** Guards single onBooked + single success overlay per hold. */
|
|
407
|
+
private bookedShown;
|
|
408
|
+
private extendEl;
|
|
409
|
+
private bookedEl;
|
|
359
410
|
private gaQty;
|
|
360
411
|
private tipEl;
|
|
361
412
|
private tipPos;
|
|
@@ -386,6 +437,10 @@ declare class SeatPicker {
|
|
|
386
437
|
static open(options: Omit<SeatPickerOptions, 'container'>): Promise<SeatPicker>;
|
|
387
438
|
constructor(options: SeatPickerOptions);
|
|
388
439
|
render(): Promise<this>;
|
|
440
|
+
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
441
|
+
private buildExtendPrompt;
|
|
442
|
+
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
443
|
+
private buildBookedOverlay;
|
|
389
444
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
390
445
|
private buildArenaChrome;
|
|
391
446
|
/** Reflect the engine's current LOD rung onto the pill group. */
|
|
@@ -418,6 +473,21 @@ declare class SeatPicker {
|
|
|
418
473
|
private handleCta;
|
|
419
474
|
private startHoldTimer;
|
|
420
475
|
private stopHoldTimer;
|
|
476
|
+
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
477
|
+
private setExtendPrompt;
|
|
478
|
+
private handleExtend;
|
|
479
|
+
/**
|
|
480
|
+
* Fire the booked-confirmation state once the buyer's held seats settle to
|
|
481
|
+
* booked. The controller clears its own hold the moment every held label reads
|
|
482
|
+
* 'booked' over the realtime channel (clearBookedHoldIfSettled), and this runs
|
|
483
|
+
* on the same onStatusChange — so `currentHold() === null` while we still hold
|
|
484
|
+
* a checkout handoff means "sold", not expired (expiry clears via onHoldExpired
|
|
485
|
+
* on a different path, which nulls this.hold first).
|
|
486
|
+
*/
|
|
487
|
+
private detectBooked;
|
|
488
|
+
private showBooked;
|
|
489
|
+
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
490
|
+
private buildHandoff;
|
|
421
491
|
private toast;
|
|
422
492
|
private placeTooltip;
|
|
423
493
|
private updateTooltip;
|
|
@@ -427,4 +497,4 @@ declare class SeatPicker {
|
|
|
427
497
|
destroy(): void;
|
|
428
498
|
}
|
|
429
499
|
|
|
430
|
-
export { ApiError, type BestAvailableResult, EmbeddedDesigner, type EmbeddedDesignerEventType, type EmbeddedDesignerMessage, type EmbeddedDesignerOptions, type GAAreaAvailability, type HoldConflict, type HoldLineItem, type HoldResult, SeatPicker, type SeatPickerOptions, type SeatPickerTheme, SeatingChart, type SeatingChartOptions, type SelectedSeat };
|
|
500
|
+
export { ApiError, type BestAvailableResult, type CheckoutHandoff, type CheckoutLineItem, EmbeddedDesigner, type EmbeddedDesignerEventType, type EmbeddedDesignerMessage, type EmbeddedDesignerOptions, type GAAreaAvailability, type HoldConflict, type HoldLineItem, type HoldResult, SeatPicker, type SeatPickerOptions, type SeatPickerTheme, SeatingChart, type SeatingChartOptions, type SelectedSeat };
|
package/dist/index.js
CHANGED
|
@@ -57,6 +57,14 @@ var PubApi = class {
|
|
|
57
57
|
body: { labels, holdId }
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
/** P4 "need more time?": push an active hold's expiry out. Throws ApiError 409
|
|
61
|
+
* (reason: expired | extend_limit | not_found | not_active) if it can't. */
|
|
62
|
+
extend(key, holdId, ttlMs) {
|
|
63
|
+
return request(this.base, `/pub/events/${encodeURIComponent(key)}/extend`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
body: { holdId, ...ttlMs ? { ttlMs } : {} }
|
|
66
|
+
});
|
|
67
|
+
}
|
|
60
68
|
socketUrl(key) {
|
|
61
69
|
const wsBase = this.base.replace(/^http/, "ws");
|
|
62
70
|
return `${wsBase}/pub/events/${encodeURIComponent(key)}/subscribe`;
|
|
@@ -386,6 +394,7 @@ import {
|
|
|
386
394
|
} from "@seatlayer/core";
|
|
387
395
|
var DEFAULT_API_BASE2 = "https://api.seatlayer.io";
|
|
388
396
|
var DEFAULT_MAX_SELECTION2 = 10;
|
|
397
|
+
var EXTEND_PROMPT_MS = 6e4;
|
|
389
398
|
function resolveContainer3(container) {
|
|
390
399
|
if (typeof container === "string") {
|
|
391
400
|
const el = document.querySelector(container);
|
|
@@ -499,6 +508,30 @@ var CSS = `
|
|
|
499
508
|
.sl-boot-retry{margin-top:4px;padding:9px 20px;border-radius:var(--sl-r-sm);background:var(--sl-accent);
|
|
500
509
|
color:var(--sl-accent-ink);font-weight:700;font-size:13px}
|
|
501
510
|
|
|
511
|
+
/* "Need more time?" extend prompt (bottom-center over the map, above the toast) */
|
|
512
|
+
.sl-extend{position:absolute;left:50%;bottom:16px;transform:translateX(-50%) translateY(6px);z-index:9;
|
|
513
|
+
display:none;align-items:center;gap:12px;max-width:92%;background:var(--sl-surface);border:1px solid var(--sl-line);
|
|
514
|
+
color:var(--sl-text);border-radius:14px;padding:10px 12px 10px 16px;box-shadow:0 18px 50px -18px rgba(0,0,0,.6);
|
|
515
|
+
opacity:0;transition:opacity .2s,transform .2s}
|
|
516
|
+
.sl-extend.on{display:flex;opacity:1;transform:translateX(-50%) translateY(0)}
|
|
517
|
+
.sl-extend-txt{font-size:12.5px;font-weight:600;line-height:1.35}
|
|
518
|
+
.sl-extend-txt b{font-variant-numeric:tabular-nums}
|
|
519
|
+
.sl-extend-btn{flex:none;padding:8px 14px;border-radius:999px;font-weight:800;font-size:12.5px;
|
|
520
|
+
background:var(--sl-accent);color:var(--sl-accent-ink);transition:filter .15s,opacity .15s}
|
|
521
|
+
.sl-extend-btn:hover{filter:brightness(1.08)}
|
|
522
|
+
.sl-extend-btn:disabled{opacity:.5;cursor:not-allowed}
|
|
523
|
+
|
|
524
|
+
/* booked confirmation overlay (covers the widget once the held seats are sold) */
|
|
525
|
+
.sl-booked{position:absolute;inset:0;z-index:11;display:none;flex-direction:column;align-items:center;
|
|
526
|
+
justify-content:center;gap:12px;text-align:center;padding:28px;background:var(--sl-bg)}
|
|
527
|
+
.sl-booked.on{display:flex}
|
|
528
|
+
.sl-booked-badge{width:60px;height:60px;border-radius:999px;display:flex;align-items:center;justify-content:center;
|
|
529
|
+
background:var(--sl-accent);color:var(--sl-accent-ink)}
|
|
530
|
+
.sl-booked-badge svg{width:30px;height:30px;stroke:currentColor;stroke-width:2.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
|
|
531
|
+
.sl-booked-title{font-weight:800;font-size:19px;color:var(--sl-text)}
|
|
532
|
+
.sl-booked-sub{font-size:13px;color:var(--sl-muted);line-height:1.5;max-width:320px}
|
|
533
|
+
.sl-booked-seats{font-weight:700;color:var(--sl-text)}
|
|
534
|
+
|
|
502
535
|
/* a11y filter chips (over the map, top-left) */
|
|
503
536
|
.sl-chips{position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;flex-wrap:wrap;max-width:70%}
|
|
504
537
|
.sl-chip-f{display:inline-flex;align-items:center;gap:6px;padding:7px 12px;border-radius:999px;font-size:12px;font-weight:700;
|
|
@@ -646,6 +679,14 @@ var SeatPicker = class _SeatPicker {
|
|
|
646
679
|
// state
|
|
647
680
|
this.currency = "USD";
|
|
648
681
|
this.hold = null;
|
|
682
|
+
/** Latest server expiry for the open hold (moves on extend). */
|
|
683
|
+
this.holdExpiresAt = 0;
|
|
684
|
+
/** True once we handed off to checkout — arms booked-confirmation detection. */
|
|
685
|
+
this.handedOff = false;
|
|
686
|
+
/** Guards single onBooked + single success overlay per hold. */
|
|
687
|
+
this.bookedShown = false;
|
|
688
|
+
this.extendEl = null;
|
|
689
|
+
this.bookedEl = null;
|
|
649
690
|
this.gaQty = /* @__PURE__ */ new Map();
|
|
650
691
|
this.tipEl = null;
|
|
651
692
|
this.tipPos = { x: 0, y: 0 };
|
|
@@ -684,9 +725,12 @@ var SeatPicker = class _SeatPicker {
|
|
|
684
725
|
onStatusChange: () => {
|
|
685
726
|
this.syncPrices();
|
|
686
727
|
this.evictTakenSelections();
|
|
728
|
+
this.detectBooked();
|
|
687
729
|
},
|
|
688
730
|
onHoldExpired: () => {
|
|
689
731
|
this.hold = null;
|
|
732
|
+
this.handedOff = false;
|
|
733
|
+
this.bookedShown = false;
|
|
690
734
|
this.stopHoldTimer();
|
|
691
735
|
this.gaQty.clear();
|
|
692
736
|
this.toast(t2("picker.holdExpired", void 0) || "Your hold expired \u2014 seats released. Pick again.");
|
|
@@ -895,10 +939,36 @@ var SeatPicker = class _SeatPicker {
|
|
|
895
939
|
this.srEl.setAttribute("aria-live", "polite");
|
|
896
940
|
root.appendChild(this.srEl);
|
|
897
941
|
this.buildArenaChrome();
|
|
942
|
+
this.buildExtendPrompt();
|
|
943
|
+
this.buildBookedOverlay();
|
|
898
944
|
this.syncPrices();
|
|
899
945
|
this.syncTray();
|
|
900
946
|
return this;
|
|
901
947
|
}
|
|
948
|
+
/** The "Need more time?" prompt shown in the hold's final EXTEND_PROMPT_MS. */
|
|
949
|
+
buildExtendPrompt() {
|
|
950
|
+
const el = document.createElement("div");
|
|
951
|
+
el.className = "sl-extend";
|
|
952
|
+
el.setAttribute("role", "status");
|
|
953
|
+
el.innerHTML = `<span class="sl-extend-txt" data-ref="extendTxt"></span><button type="button" class="sl-extend-btn" data-ref="extendBtn"></button>`;
|
|
954
|
+
this.els.map.appendChild(el);
|
|
955
|
+
this.extendEl = el;
|
|
956
|
+
this.els.extendTxt = el.querySelector('[data-ref="extendTxt"]');
|
|
957
|
+
this.els.extendBtn = el.querySelector('[data-ref="extendBtn"]');
|
|
958
|
+
this.els.extendBtn.textContent = "Add time";
|
|
959
|
+
this.els.extendBtn.addEventListener("click", () => void this.handleExtend());
|
|
960
|
+
}
|
|
961
|
+
/** Success overlay + onBooked fire when the held seats settle to booked. */
|
|
962
|
+
buildBookedOverlay() {
|
|
963
|
+
const el = document.createElement("div");
|
|
964
|
+
el.className = "sl-booked";
|
|
965
|
+
el.setAttribute("role", "status");
|
|
966
|
+
el.setAttribute("aria-live", "polite");
|
|
967
|
+
el.innerHTML = `<div class="sl-booked-badge"><svg viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg></div><div class="sl-booked-title">You're all set</div><div class="sl-booked-sub" data-ref="bookedSub"></div>`;
|
|
968
|
+
this.root.appendChild(el);
|
|
969
|
+
this.bookedEl = el;
|
|
970
|
+
this.els.bookedSub = el.querySelector('[data-ref="bookedSub"]');
|
|
971
|
+
}
|
|
902
972
|
// ---- arena / multi-floor chrome -------------------------------------------
|
|
903
973
|
/** Build the rung pills (charts with sections) and floor switcher (>1 floor). */
|
|
904
974
|
buildArenaChrome() {
|
|
@@ -1277,7 +1347,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1277
1347
|
async handleCta() {
|
|
1278
1348
|
const cta = this.els.cta;
|
|
1279
1349
|
if (this.hold && !this.controller.getSelection().some((s) => !(this.hold.items ?? []).some((i) => i.label === s.label))) {
|
|
1280
|
-
|
|
1350
|
+
const seats = this.hold.seats ?? this.controller.getSelection();
|
|
1351
|
+
this.handedOff = true;
|
|
1352
|
+
this.opts.onCheckout?.(this.hold, seats, this.buildHandoff(this.hold));
|
|
1281
1353
|
return;
|
|
1282
1354
|
}
|
|
1283
1355
|
cta.disabled = true;
|
|
@@ -1300,8 +1372,9 @@ var SeatPicker = class _SeatPicker {
|
|
|
1300
1372
|
return;
|
|
1301
1373
|
}
|
|
1302
1374
|
this.hold = hold;
|
|
1375
|
+
this.handedOff = true;
|
|
1303
1376
|
this.startHoldTimer(hold.expiresAt);
|
|
1304
|
-
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? []);
|
|
1377
|
+
this.opts.onCheckout?.(hold, chosenSeats.length ? chosenSeats : hold.seats ?? [], this.buildHandoff(hold));
|
|
1305
1378
|
} catch (err) {
|
|
1306
1379
|
this.opts.onError?.(err);
|
|
1307
1380
|
this.toast("One or more seats were just taken. Please pick again.");
|
|
@@ -1311,13 +1384,15 @@ var SeatPicker = class _SeatPicker {
|
|
|
1311
1384
|
}
|
|
1312
1385
|
startHoldTimer(expiresAt) {
|
|
1313
1386
|
this.stopHoldTimer();
|
|
1387
|
+
this.holdExpiresAt = expiresAt;
|
|
1314
1388
|
const pill = this.els.hold;
|
|
1315
1389
|
const tick = () => {
|
|
1316
|
-
const ms = Math.max(0,
|
|
1390
|
+
const ms = Math.max(0, this.holdExpiresAt - Date.now());
|
|
1317
1391
|
const m = Math.floor(ms / 6e4);
|
|
1318
1392
|
const s = String(Math.floor(ms % 6e4 / 1e3)).padStart(2, "0");
|
|
1319
1393
|
pill.textContent = `Held ${m}:${s}`;
|
|
1320
1394
|
pill.classList.add("on");
|
|
1395
|
+
this.setExtendPrompt(ms > 0 && ms <= EXTEND_PROMPT_MS, ms);
|
|
1321
1396
|
if (ms <= 0) this.stopHoldTimer();
|
|
1322
1397
|
};
|
|
1323
1398
|
tick();
|
|
@@ -1327,6 +1402,83 @@ var SeatPicker = class _SeatPicker {
|
|
|
1327
1402
|
if (this.holdTimer) clearInterval(this.holdTimer);
|
|
1328
1403
|
this.holdTimer = null;
|
|
1329
1404
|
this.els.hold?.classList.remove("on");
|
|
1405
|
+
this.setExtendPrompt(false, 0);
|
|
1406
|
+
}
|
|
1407
|
+
/** Show/refresh (or hide) the "Need more time?" prompt with the live seconds left. */
|
|
1408
|
+
setExtendPrompt(show, ms) {
|
|
1409
|
+
if (!this.extendEl) return;
|
|
1410
|
+
if (show && this.controller.currentHold() && !this.bookedShown) {
|
|
1411
|
+
const secs = Math.ceil(ms / 1e3);
|
|
1412
|
+
this.els.extendTxt.innerHTML = `Your seats are held for <b>0:${String(secs).padStart(2, "0")}</b>. Need more time?`;
|
|
1413
|
+
this.extendEl.classList.add("on");
|
|
1414
|
+
} else {
|
|
1415
|
+
this.extendEl.classList.remove("on");
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
async handleExtend() {
|
|
1419
|
+
const btn = this.els.extendBtn;
|
|
1420
|
+
btn.disabled = true;
|
|
1421
|
+
const prev = btn.textContent;
|
|
1422
|
+
btn.textContent = "Adding\u2026";
|
|
1423
|
+
try {
|
|
1424
|
+
const h = await this.controller.extendHold(this.opts.holdTtlMs);
|
|
1425
|
+
if (h) {
|
|
1426
|
+
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1427
|
+
this.holdExpiresAt = h.expiresAt;
|
|
1428
|
+
this.extendEl?.classList.remove("on");
|
|
1429
|
+
this.toast("More time added \u2014 your seats are still held.");
|
|
1430
|
+
} else {
|
|
1431
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1432
|
+
}
|
|
1433
|
+
} catch (err) {
|
|
1434
|
+
this.opts.onError?.(err);
|
|
1435
|
+
this.toast("Couldn't add more time \u2014 please head to checkout now.");
|
|
1436
|
+
} finally {
|
|
1437
|
+
btn.disabled = false;
|
|
1438
|
+
btn.textContent = prev;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
/**
|
|
1442
|
+
* Fire the booked-confirmation state once the buyer's held seats settle to
|
|
1443
|
+
* booked. The controller clears its own hold the moment every held label reads
|
|
1444
|
+
* 'booked' over the realtime channel (clearBookedHoldIfSettled), and this runs
|
|
1445
|
+
* on the same onStatusChange — so `currentHold() === null` while we still hold
|
|
1446
|
+
* a checkout handoff means "sold", not expired (expiry clears via onHoldExpired
|
|
1447
|
+
* on a different path, which nulls this.hold first).
|
|
1448
|
+
*/
|
|
1449
|
+
detectBooked() {
|
|
1450
|
+
if (this.bookedShown || !this.handedOff || !this.hold) return;
|
|
1451
|
+
if (this.controller.currentHold() !== null) return;
|
|
1452
|
+
this.showBooked();
|
|
1453
|
+
}
|
|
1454
|
+
showBooked() {
|
|
1455
|
+
if (this.bookedShown || !this.hold) return;
|
|
1456
|
+
this.bookedShown = true;
|
|
1457
|
+
const handoff = this.buildHandoff(this.hold);
|
|
1458
|
+
this.stopHoldTimer();
|
|
1459
|
+
const n = handoff.lineItems.reduce((sum, i) => sum + i.quantity, 0);
|
|
1460
|
+
if (this.els.bookedSub) {
|
|
1461
|
+
this.els.bookedSub.innerHTML = `<span class="sl-booked-seats">${n} ${n === 1 ? "ticket" : "tickets"}</span> confirmed. A confirmation is on its way.`;
|
|
1462
|
+
}
|
|
1463
|
+
this.bookedEl?.classList.add("on");
|
|
1464
|
+
this.opts.onBooked?.(handoff);
|
|
1465
|
+
}
|
|
1466
|
+
/** Assemble the stable {@link CheckoutHandoff} from a hold's server line items. */
|
|
1467
|
+
buildHandoff(hold) {
|
|
1468
|
+
const items = hold.items ?? [];
|
|
1469
|
+
const lineItems = items.map((it) => ({
|
|
1470
|
+
label: it.label,
|
|
1471
|
+
objectId: it.objectId,
|
|
1472
|
+
objectType: it.objectType,
|
|
1473
|
+
categoryKey: it.categoryKey,
|
|
1474
|
+
tierId: it.tierId,
|
|
1475
|
+
unitPrice: it.unitPrice,
|
|
1476
|
+
currency: it.currency ?? this.currency,
|
|
1477
|
+
quantity: it.quantity ?? 1
|
|
1478
|
+
}));
|
|
1479
|
+
const currency = lineItems[0]?.currency ?? this.currency;
|
|
1480
|
+
const total = lineItems.reduce((sum, i) => sum + i.unitPrice * i.quantity, 0);
|
|
1481
|
+
return { holdId: hold.holdId, expiresAt: hold.expiresAt, currency, lineItems, total };
|
|
1330
1482
|
}
|
|
1331
1483
|
toast(msg) {
|
|
1332
1484
|
const el = this.els.toast;
|
|
@@ -1368,6 +1520,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1368
1520
|
const h = await this.controller.bestAvailable(qty, categoryKey);
|
|
1369
1521
|
if (h) {
|
|
1370
1522
|
this.hold = { holdId: h.holdId, expiresAt: h.expiresAt, seats: h.seats, items: h.items };
|
|
1523
|
+
this.handedOff = false;
|
|
1524
|
+
this.bookedShown = false;
|
|
1371
1525
|
this.startHoldTimer(h.expiresAt);
|
|
1372
1526
|
this.syncTray();
|
|
1373
1527
|
return this.hold;
|
|
@@ -1381,6 +1535,8 @@ var SeatPicker = class _SeatPicker {
|
|
|
1381
1535
|
async release() {
|
|
1382
1536
|
await this.controller.release();
|
|
1383
1537
|
this.hold = null;
|
|
1538
|
+
this.handedOff = false;
|
|
1539
|
+
this.bookedShown = false;
|
|
1384
1540
|
this.stopHoldTimer();
|
|
1385
1541
|
this.gaQty.clear();
|
|
1386
1542
|
this.syncTray();
|