@cimplify/sdk 0.31.0 → 0.36.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.
Files changed (44) hide show
  1. package/dist/advanced.d.mts +6 -2
  2. package/dist/advanced.d.ts +6 -2
  3. package/dist/advanced.js +21 -21
  4. package/dist/advanced.mjs +2 -2
  5. package/dist/{chunk-46BDUSJX.mjs → chunk-2X6P5WJZ.mjs} +108 -32
  6. package/dist/{chunk-E3HC2YAE.mjs → chunk-6TVQQYQ5.mjs} +14 -1
  7. package/dist/{chunk-2CGA3MUW.js → chunk-HKVRUGM6.js} +142 -91
  8. package/dist/{chunk-3J4PPSMI.mjs → chunk-TFHKVQLL.mjs} +123 -72
  9. package/dist/{chunk-PEDLCL72.js → chunk-V2O5AO7I.js} +15 -0
  10. package/dist/{chunk-6HCBOIZ4.js → chunk-ZYACIPKO.js} +160 -84
  11. package/dist/{index-BpyFcJVv.d.ts → client-CkGlXZRd.d.mts} +121 -72
  12. package/dist/{index-m8zGQUda.d.mts → client-lvLvB1_K.d.ts} +121 -72
  13. package/dist/{index-D4oNCc37.d.mts → index-CC1JPhB2.d.mts} +2 -2
  14. package/dist/{index-BHVJgyqM.d.ts → index-CPcFayQg.d.ts} +2 -2
  15. package/dist/index.d.mts +6 -5
  16. package/dist/index.d.ts +6 -5
  17. package/dist/index.js +117 -109
  18. package/dist/index.mjs +3 -3
  19. package/dist/mock/cli.mjs +8086 -0
  20. package/dist/mock/library.d.mts +15 -0
  21. package/dist/mock/library.d.ts +15 -0
  22. package/dist/mock/library.js +7285 -0
  23. package/dist/mock/library.mjs +7278 -0
  24. package/dist/mock/msw.d.mts +14 -0
  25. package/dist/mock/msw.d.ts +14 -0
  26. package/dist/mock/msw.js +7280 -0
  27. package/dist/mock/msw.mjs +7278 -0
  28. package/dist/{payment-DnU_eLuT.d.ts → payment-DA1i5hhk.d.mts} +13 -12
  29. package/dist/{payment-DnU_eLuT.d.mts → payment-DA1i5hhk.d.ts} +13 -12
  30. package/dist/{price-DUVmRTaz.d.ts → price-Bv4G8aOO.d.ts} +1 -1
  31. package/dist/{price-D-hso3np.d.mts → price-s1XN0edJ.d.mts} +1 -1
  32. package/dist/react.d.mts +21 -4
  33. package/dist/react.d.ts +21 -4
  34. package/dist/react.js +835 -715
  35. package/dist/react.mjs +725 -605
  36. package/dist/server-B8TeE2OD.d.mts +915 -0
  37. package/dist/server-B8TeE2OD.d.ts +915 -0
  38. package/dist/styles.css +1 -1
  39. package/dist/utils.d.mts +3 -3
  40. package/dist/utils.d.ts +3 -3
  41. package/dist/utils.js +28 -28
  42. package/dist/utils.mjs +1 -1
  43. package/package.json +32 -12
  44. package/registry/cart-summary.json +1 -1
package/dist/react.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  "use client";
2
- import { DURATION_UNIT, getVariantDisplayName, INPUT_FIELD_TYPE, PRODUCT_TYPE, createCimplifyClient, RENDER_HINT } from './chunk-46BDUSJX.mjs';
3
- export { getVariantDisplayName } from './chunk-46BDUSJX.mjs';
4
- import { ELEMENT_TYPES, EVENT_TYPES } from './chunk-3J4PPSMI.mjs';
5
- import { isSupportedCurrency, formatPrice, parsePrice, getUnitPriceAtQuantity, isOnSale, getDiscountPercentage, getBasePrice, getPriceRange, formatPriceRange } from './chunk-E3HC2YAE.mjs';
6
- import React10, { createContext, useRef, useState, useCallback, useEffect, useContext, useMemo, useSyncExternalStore, useId } from 'react';
2
+ import { DURATION_UNIT, getVariantDisplayName, INPUT_FIELD_TYPE, PRODUCT_TYPE, createCimplifyClient, RENDER_HINT } from './chunk-2X6P5WJZ.mjs';
3
+ export { getVariantDisplayName } from './chunk-2X6P5WJZ.mjs';
4
+ import { ELEMENT_TYPES, EVENT_TYPES } from './chunk-TFHKVQLL.mjs';
5
+ import { isSupportedCurrency, formatPrice, parsePrice, getUnitPriceAtQuantity, isOnSale, getDiscountPercentage, getBasePrice, getPriceRange, formatPriceRange } from './chunk-6TVQQYQ5.mjs';
6
+ import React10, { createContext, useRef, useState, useCallback, useEffect, useContext, useMemo, useId, useSyncExternalStore } from 'react';
7
7
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
8
8
  import { clsx } from 'clsx';
9
9
  import { twMerge } from 'tailwind-merge';
@@ -398,19 +398,114 @@ function transformToCheckoutCart(cart, fx) {
398
398
  currency: fx?.displayCurrency ?? cart.pricing.currency
399
399
  };
400
400
  }
401
- var CART_STORAGE_PREFIX = "cimplify:cart:v2";
402
- var DEFAULT_TAX_RATE = 0;
403
- var cartStores = /* @__PURE__ */ new Map();
404
- function clearAllCartStores() {
405
- for (const [, store] of cartStores) {
406
- void store.clearCart();
401
+
402
+ // src/internal/collection.ts
403
+ function newOpId() {
404
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
405
+ return `op_${crypto.randomUUID()}`;
407
406
  }
408
- cartStores.clear();
407
+ return `op_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
409
408
  }
410
- function toNumber(value) {
411
- if (typeof value === "number" && Number.isFinite(value)) {
412
- return value;
409
+ function createCollection(config) {
410
+ let base = config.initialBase ?? null;
411
+ let ops = [];
412
+ const listeners = /* @__PURE__ */ new Set();
413
+ let cachedView;
414
+ let cachedBaseRef;
415
+ let cachedOpsVersion = -1;
416
+ let opsVersion = 0;
417
+ function notify() {
418
+ for (const listener of listeners) {
419
+ listener();
420
+ }
413
421
  }
422
+ function recompute() {
423
+ if (cachedView !== void 0 && cachedBaseRef === base && cachedOpsVersion === opsVersion) {
424
+ return cachedView;
425
+ }
426
+ const view = config.toView(base, ops);
427
+ cachedView = view;
428
+ cachedBaseRef = base;
429
+ cachedOpsVersion = opsVersion;
430
+ return view;
431
+ }
432
+ return {
433
+ subscribe(listener) {
434
+ listeners.add(listener);
435
+ return () => {
436
+ listeners.delete(listener);
437
+ };
438
+ },
439
+ getSnapshot() {
440
+ return recompute();
441
+ },
442
+ setBase(next) {
443
+ base = next;
444
+ opsVersion++;
445
+ notify();
446
+ },
447
+ enqueue(patch, idempotencyKey) {
448
+ const op = {
449
+ id: newOpId(),
450
+ idempotencyKey,
451
+ patch,
452
+ status: "pending",
453
+ attemptCount: 0,
454
+ enqueuedAt: Date.now()
455
+ };
456
+ ops = [...ops, op];
457
+ opsVersion++;
458
+ notify();
459
+ return op.id;
460
+ },
461
+ confirm(opId, newBase) {
462
+ const idx = ops.findIndex((op) => op.id === opId);
463
+ if (idx === -1 && newBase === void 0) {
464
+ return;
465
+ }
466
+ if (idx !== -1) {
467
+ ops = ops.filter((op) => op.id !== opId);
468
+ }
469
+ if (newBase !== void 0) {
470
+ base = newBase;
471
+ }
472
+ opsVersion++;
473
+ notify();
474
+ },
475
+ fail(opId, error) {
476
+ const idx = ops.findIndex((op) => op.id === opId);
477
+ if (idx === -1) return;
478
+ ops = ops.map(
479
+ (op, i) => i === idx ? {
480
+ ...op,
481
+ status: "failed",
482
+ attemptCount: op.attemptCount + 1,
483
+ lastError: error
484
+ } : op
485
+ );
486
+ opsVersion++;
487
+ notify();
488
+ },
489
+ remove(opId) {
490
+ const next = ops.filter((op) => op.id !== opId);
491
+ if (next.length === ops.length) return;
492
+ ops = next;
493
+ opsVersion++;
494
+ notify();
495
+ },
496
+ getOps() {
497
+ return ops;
498
+ },
499
+ getBase() {
500
+ return base;
501
+ }
502
+ };
503
+ }
504
+
505
+ // src/internal/cart-patches.ts
506
+ var DEFAULT_TAX_RATE = 0;
507
+ function toNumber(value) {
508
+ if (typeof value === "number" && Number.isFinite(value)) return value;
414
509
  if (typeof value === "string") {
415
510
  const parsed = Number.parseFloat(value);
416
511
  return Number.isFinite(parsed) ? parsed : 0;
@@ -421,30 +516,20 @@ function roundMoney(value) {
421
516
  return Math.max(0, Number(value.toFixed(2)));
422
517
  }
423
518
  function clampQuantity(quantity) {
424
- if (!Number.isFinite(quantity)) {
425
- return 1;
426
- }
519
+ if (!Number.isFinite(quantity)) return 1;
427
520
  return Math.max(1, Math.floor(quantity));
428
521
  }
429
522
  function normalizeOptionIds(value) {
430
- if (!value || value.length === 0) {
431
- return void 0;
432
- }
523
+ if (!value || value.length === 0) return void 0;
433
524
  const normalized = Array.from(
434
- new Set(
435
- value.map((entry) => entry.trim()).filter((entry) => entry.length > 0)
436
- )
525
+ new Set(value.map((entry) => entry.trim()).filter((entry) => entry.length > 0))
437
526
  ).sort();
438
527
  return normalized.length > 0 ? normalized : void 0;
439
528
  }
440
529
  function buildLineKey(productId, options) {
441
530
  const parts = [productId];
442
- if (options.locationId) {
443
- parts.push(`l:${options.locationId}`);
444
- }
445
- if (options.variantId) {
446
- parts.push(`v:${options.variantId}`);
447
- }
531
+ if (options.locationId) parts.push(`l:${options.locationId}`);
532
+ if (options.variantId) parts.push(`v:${options.variantId}`);
448
533
  const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
449
534
  if (addOnOptionIds && addOnOptionIds.length > 0) {
450
535
  parts.push(`a:${addOnOptionIds.join(",")}`);
@@ -455,61 +540,122 @@ function buildLineKey(productId, options) {
455
540
  if (options.compositeSelections && options.compositeSelections.length > 0) {
456
541
  parts.push(`c:${JSON.stringify(options.compositeSelections)}`);
457
542
  }
458
- if (options.quoteId) {
459
- parts.push(`q:${options.quoteId}`);
460
- }
461
- if (options.scheduledStart) {
462
- parts.push(`ss:${options.scheduledStart}`);
463
- }
464
- if (options.staffId) {
465
- parts.push(`st:${options.staffId}`);
466
- }
467
- if (options.resourceId) {
468
- parts.push(`r:${options.resourceId}`);
469
- }
543
+ if (options.quoteId) parts.push(`q:${options.quoteId}`);
544
+ if (options.scheduledStart) parts.push(`ss:${options.scheduledStart}`);
545
+ if (options.staffId) parts.push(`st:${options.staffId}`);
546
+ if (options.resourceId) parts.push(`r:${options.resourceId}`);
470
547
  return parts.join("|");
471
548
  }
549
+ function lineKeyFromItem(item) {
550
+ return buildLineKey(item.product.id, {
551
+ locationId: item.locationId,
552
+ variantId: item.variantId,
553
+ quoteId: item.quoteId,
554
+ addOnOptionIds: item.addOnOptionIds,
555
+ bundleSelections: item.bundleSelections,
556
+ compositeSelections: item.compositeSelections,
557
+ scheduledStart: item.scheduledStart,
558
+ staffId: item.staffId
559
+ });
560
+ }
561
+ function attachInternalItemMetadata(item) {
562
+ return { ...item, _lineKey: lineKeyFromItem(item) };
563
+ }
472
564
  function calculateLineSubtotal(item) {
473
565
  let unitPrice = parsePrice(item.product.default_price);
474
566
  if (item.variant?.price_adjustment) {
475
567
  unitPrice += parsePrice(item.variant.price_adjustment);
476
568
  }
477
569
  for (const option of item.addOnOptions || []) {
478
- if (option.default_price) {
479
- unitPrice += parsePrice(option.default_price);
480
- }
570
+ if (option.default_price) unitPrice += parsePrice(option.default_price);
481
571
  }
482
572
  return roundMoney(unitPrice * item.quantity);
483
573
  }
484
574
  function calculateSummary(items) {
485
575
  const subtotal = roundMoney(items.reduce((sum, item) => sum + calculateLineSubtotal(item), 0));
486
576
  const tax = roundMoney(subtotal * DEFAULT_TAX_RATE);
487
- return {
488
- subtotal,
489
- tax,
490
- total: roundMoney(subtotal + tax)
491
- };
577
+ return { subtotal, tax, total: roundMoney(subtotal + tax) };
492
578
  }
493
- function lineKeyFromItem(item) {
494
- return buildLineKey(item.product.id, {
495
- locationId: item.locationId,
496
- variantId: item.variantId,
497
- quoteId: item.quoteId,
498
- addOnOptionIds: item.addOnOptionIds,
499
- bundleSelections: item.bundleSelections,
500
- compositeSelections: item.compositeSelections,
501
- scheduledStart: item.scheduledStart,
502
- staffId: item.staffId
503
- });
579
+ function applyCartPatch(base, patch) {
580
+ switch (patch.kind) {
581
+ case "add_item": {
582
+ const existingIndex = base.items.findIndex(
583
+ (item) => (item._lineKey ?? lineKeyFromItem(item)) === patch.lineKey
584
+ );
585
+ const nextItems = [...base.items];
586
+ if (existingIndex >= 0) {
587
+ const existing = nextItems[existingIndex];
588
+ nextItems[existingIndex] = {
589
+ ...existing,
590
+ quantity: existing.quantity + patch.tempItem.quantity
591
+ };
592
+ } else {
593
+ nextItems.push(patch.tempItem);
594
+ }
595
+ return { ...base, items: nextItems, ...calculateSummary(nextItems) };
596
+ }
597
+ case "remove_item": {
598
+ const nextItems = base.items.filter((item) => item.id !== patch.itemId);
599
+ return { ...base, items: nextItems, ...calculateSummary(nextItems) };
600
+ }
601
+ case "set_quantity": {
602
+ const resolved = clampQuantity(patch.quantity);
603
+ const nextItems = base.items.map(
604
+ (item) => item.id === patch.itemId ? { ...item, quantity: resolved } : item
605
+ );
606
+ return { ...base, items: nextItems, ...calculateSummary(nextItems) };
607
+ }
608
+ case "clear": {
609
+ return { ...base, items: [], subtotal: 0, tax: 0, total: 0 };
610
+ }
611
+ }
504
612
  }
505
- function attachInternalItemMetadata(item) {
506
- return {
507
- ...item,
508
- _lineKey: lineKeyFromItem(item)
509
- };
613
+ function useCollectionStore(collection) {
614
+ return useSyncExternalStore(
615
+ (listener) => collection.subscribe(listener),
616
+ () => collection.getSnapshot(),
617
+ () => collection.getSnapshot()
618
+ );
619
+ }
620
+
621
+ // src/react/hooks/use-cart.ts
622
+ var CART_STORAGE_PREFIX = "cimplify:cart:v3";
623
+ var cartStores = /* @__PURE__ */ new Map();
624
+ var ROLLBACK_VISIBLE_DELAY_MS = 3e3;
625
+ function clearAllCartStores() {
626
+ for (const [, entry] of cartStores) {
627
+ entry.collection.setBase(emptyBase(entry.currency));
628
+ for (const handle of entry.failedOpRemovers) {
629
+ clearTimeout(handle);
630
+ }
631
+ entry.failedOpRemovers.clear();
632
+ if (entry.persistTimer) {
633
+ clearTimeout(entry.persistTimer);
634
+ entry.persistTimer = null;
635
+ }
636
+ }
637
+ cartStores.clear();
638
+ }
639
+ function emptyBase(currency, isLoading = true) {
640
+ return { items: [], subtotal: 0, tax: 0, total: 0, currency, isLoading };
641
+ }
642
+ function advanceBaseWithPatch(entry, patch) {
643
+ const current = entry.collection.getBase() ?? emptyBase(entry.currency, false);
644
+ entry.collection.setBase({ ...applyCartPatch(current, patch), isLoading: false });
645
+ persistBase(entry);
646
+ }
647
+ function buildStoreKey(client, locationId, isDemoMode) {
648
+ return [
649
+ client.getPublicKey() || "__demo__",
650
+ locationId || "__no_location__",
651
+ isDemoMode ? "demo" : "live"
652
+ ].join(":");
653
+ }
654
+ function buildStorageKey(storeKey) {
655
+ return `${CART_STORAGE_PREFIX}:${storeKey}`;
510
656
  }
511
657
  function stripInternalItemMetadata(item) {
512
- const { _lineKey, ...rest } = item;
658
+ const { _lineKey, isOptimistic, ...rest } = item;
513
659
  return rest;
514
660
  }
515
661
  function toProductFromServerItem(item, businessId) {
@@ -528,12 +674,10 @@ function toProductFromServerItem(item, businessId) {
528
674
  is_active: item.is_available,
529
675
  created_at: item.created_at,
530
676
  updated_at: item.updated_at,
531
- metadata: {
532
- from_sdk: true
533
- }
677
+ metadata: { from_sdk: true }
534
678
  };
535
679
  }
536
- function mapServerCart(serverCart) {
680
+ function mapServerCart(serverCart, fallbackCurrency) {
537
681
  const items = serverCart.items.map((item) => {
538
682
  const variant = item.variant_info || item.variant_name ? {
539
683
  id: item.variant_info?.id || item.variant_id || "",
@@ -574,207 +718,219 @@ function mapServerCart(serverCart) {
574
718
  subtotal: roundMoney(toNumber(serverCart.pricing.subtotal)),
575
719
  tax: roundMoney(toNumber(serverCart.pricing.tax_amount)),
576
720
  total: roundMoney(toNumber(serverCart.pricing.total_price)),
577
- currency: serverCart.pricing.currency || "USD"
721
+ currency: serverCart.pricing.currency || fallbackCurrency,
722
+ isLoading: false
578
723
  };
579
724
  }
580
- function buildStorageKey(storeKey) {
581
- return `${CART_STORAGE_PREFIX}:${storeKey}`;
582
- }
583
- function createEmptySnapshot(currency) {
725
+ function annotateOptimisticItems(base, ops) {
726
+ if (ops.length === 0) return base;
727
+ const optimisticIds = /* @__PURE__ */ new Set();
728
+ for (const op of ops) {
729
+ if (op.patch.kind === "add_item") {
730
+ optimisticIds.add(op.patch.tempItem.id);
731
+ }
732
+ }
733
+ if (optimisticIds.size === 0) return base;
584
734
  return {
585
- items: [],
586
- subtotal: 0,
587
- tax: 0,
588
- total: 0,
589
- currency,
590
- isLoading: true
735
+ ...base,
736
+ items: base.items.map(
737
+ (item) => optimisticIds.has(item.id) ? { ...item, isOptimistic: true } : item
738
+ )
591
739
  };
592
740
  }
593
- function createCartStore(params) {
594
- const { client, storeKey, locationId, isDemoMode, currency } = params;
595
- const storageKey = buildStorageKey(storeKey);
596
- const listeners = /* @__PURE__ */ new Set();
597
- let snapshot = createEmptySnapshot(currency);
598
- let initialized = false;
599
- let initializePromise = null;
600
- let persistTimer = null;
601
- function emit() {
602
- listeners.forEach((listener) => {
603
- listener();
604
- });
605
- }
606
- function setSnapshot(nextSnapshot) {
607
- snapshot = nextSnapshot;
608
- schedulePersist();
609
- emit();
610
- }
611
- function updateSnapshot(updater) {
612
- setSnapshot(updater(snapshot));
741
+ function deriveView(base, ops) {
742
+ const start = base ?? emptyBase("USD");
743
+ let folded = start;
744
+ for (const op of ops) {
745
+ folded = applyCartPatch(folded, op.patch);
613
746
  }
614
- function persistSnapshot() {
615
- if (typeof window === "undefined" || !window.localStorage) {
616
- return;
617
- }
747
+ folded = annotateOptimisticItems(folded, ops);
748
+ return {
749
+ ...folded,
750
+ isOptimistic: ops.length > 0,
751
+ pendingOpCount: ops.length
752
+ };
753
+ }
754
+ function persistBase(entry) {
755
+ if (typeof window === "undefined" || !window.localStorage) return;
756
+ if (entry.persistTimer) return;
757
+ entry.persistTimer = setTimeout(() => {
758
+ entry.persistTimer = null;
759
+ if (typeof window === "undefined" || !window.localStorage) return;
760
+ const base = entry.collection.getBase();
761
+ if (!base) return;
618
762
  try {
619
763
  window.localStorage.setItem(
620
- storageKey,
764
+ entry.storageKey,
621
765
  JSON.stringify({
622
- items: snapshot.items.map((item) => stripInternalItemMetadata(item)),
623
- subtotal: snapshot.subtotal,
624
- tax: snapshot.tax,
625
- total: snapshot.total,
626
- currency: snapshot.currency
766
+ items: base.items.map((item) => stripInternalItemMetadata(item)),
767
+ subtotal: base.subtotal,
768
+ tax: base.tax,
769
+ total: base.total,
770
+ currency: base.currency
627
771
  })
628
772
  );
629
773
  } catch {
630
774
  }
775
+ }, 100);
776
+ }
777
+ function hydrateFromStorage(entry) {
778
+ if (typeof window === "undefined" || !window.localStorage) return;
779
+ try {
780
+ const raw = window.localStorage.getItem(entry.storageKey);
781
+ if (!raw) return;
782
+ const parsed = JSON.parse(raw);
783
+ if (!parsed || !Array.isArray(parsed.items)) return;
784
+ const items = parsed.items.map((item) => attachInternalItemMetadata(item));
785
+ entry.collection.setBase({
786
+ items,
787
+ subtotal: toNumber(parsed.subtotal),
788
+ tax: toNumber(parsed.tax),
789
+ total: toNumber(parsed.total),
790
+ currency: typeof parsed.currency === "string" && parsed.currency ? parsed.currency : entry.currency,
791
+ isLoading: !entry.isDemoMode
792
+ });
793
+ } catch {
631
794
  }
632
- function schedulePersist() {
633
- if (persistTimer) {
634
- return;
635
- }
636
- persistTimer = setTimeout(() => {
637
- persistTimer = null;
638
- persistSnapshot();
639
- }, 100);
795
+ }
796
+ async function syncFromServer(entry) {
797
+ if (entry.isDemoMode) {
798
+ const base = entry.collection.getBase() ?? emptyBase(entry.currency);
799
+ entry.collection.setBase({ ...base, isLoading: false });
800
+ return;
640
801
  }
641
- function hydrateFromStorage() {
642
- if (typeof window === "undefined" || !window.localStorage) {
643
- return;
644
- }
645
- try {
646
- const raw = window.localStorage.getItem(storageKey);
647
- if (!raw) {
648
- return;
649
- }
650
- const parsed = JSON.parse(raw);
651
- if (!parsed || !Array.isArray(parsed.items)) {
652
- return;
653
- }
654
- snapshot = {
655
- items: parsed.items.map((item) => attachInternalItemMetadata(item)),
656
- subtotal: toNumber(parsed.subtotal),
657
- tax: toNumber(parsed.tax),
658
- total: toNumber(parsed.total),
659
- currency: typeof parsed.currency === "string" && parsed.currency ? parsed.currency : currency,
660
- isLoading: !isDemoMode
661
- };
662
- emit();
663
- } catch {
664
- }
802
+ if (entry.syncing) return;
803
+ entry.syncing = true;
804
+ try {
805
+ const result = await entry.client.cart.get();
806
+ if (!result.ok) throw result.error;
807
+ entry.collection.setBase(mapServerCart(result.value, entry.currency));
808
+ persistBase(entry);
809
+ } finally {
810
+ entry.syncing = false;
811
+ }
812
+ }
813
+ function emitMutationError(entry, operation, opId, idempotencyKey, error) {
814
+ entry.client.events.emit("mutation:error", {
815
+ service: "cart",
816
+ operation,
817
+ opId,
818
+ idempotencyKey,
819
+ error
820
+ });
821
+ }
822
+ function scheduleFailedOpRemoval(entry, opId) {
823
+ const handle = setTimeout(() => {
824
+ entry.failedOpRemovers.delete(handle);
825
+ entry.collection.remove(opId);
826
+ }, ROLLBACK_VISIBLE_DELAY_MS);
827
+ entry.failedOpRemovers.add(handle);
828
+ }
829
+ function getOrCreateStore(params) {
830
+ const { client, locationId, isDemoMode, currency, optimisticEnabled } = params;
831
+ const storeKey = buildStoreKey(client, locationId, isDemoMode);
832
+ const existing = cartStores.get(storeKey);
833
+ if (existing) {
834
+ existing.optimisticEnabled = optimisticEnabled;
835
+ return existing;
665
836
  }
666
- async function sync() {
667
- if (isDemoMode) {
668
- updateSnapshot((current) => ({
669
- ...current,
670
- isLoading: false
671
- }));
672
- return;
673
- }
674
- updateSnapshot((current) => ({
675
- ...current,
676
- isLoading: true
677
- }));
678
- const result = await client.cart.get();
679
- if (!result.ok) {
680
- updateSnapshot((current) => ({
681
- ...current,
682
- isLoading: false
683
- }));
684
- throw result.error;
685
- }
686
- const next = mapServerCart(result.value);
687
- setSnapshot({
688
- ...next,
689
- isLoading: false
690
- });
837
+ const collection = createCollection({
838
+ applyPatch: applyCartPatch,
839
+ toView: (base, ops) => deriveView(base, ops),
840
+ initialBase: emptyBase(currency)
841
+ });
842
+ const entry = {
843
+ collection,
844
+ client,
845
+ storageKey: buildStorageKey(storeKey),
846
+ locationId,
847
+ currency,
848
+ isDemoMode,
849
+ optimisticEnabled,
850
+ initialized: false,
851
+ initializePromise: null,
852
+ persistTimer: null,
853
+ syncing: false,
854
+ failedOpRemovers: /* @__PURE__ */ new Set()
855
+ };
856
+ cartStores.set(storeKey, entry);
857
+ return entry;
858
+ }
859
+ async function ensureInitialized(entry) {
860
+ if (entry.initialized) {
861
+ if (entry.initializePromise) await entry.initializePromise;
862
+ return;
691
863
  }
692
- async function maybeResolveQuoteId(product, quantity, options) {
693
- if (options.quoteId) {
694
- return options.quoteId;
695
- }
696
- const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
697
- const requiresQuote = Boolean(
698
- options.variantId || addOnOptionIds && addOnOptionIds.length > 0 || options.bundleSelections && options.bundleSelections.length > 0 || options.compositeSelections && options.compositeSelections.length > 0
699
- );
700
- if (!requiresQuote || isDemoMode) {
701
- return void 0;
702
- }
703
- const quoteResult = await client.catalogue.fetchQuote({
704
- product_id: product.id,
705
- quantity,
706
- location_id: options.locationId || locationId || void 0,
707
- variant_id: options.variantId,
708
- add_on_option_ids: addOnOptionIds,
709
- bundle_selections: options.bundleSelections,
710
- composite_selections: options.compositeSelections
711
- });
712
- if (!quoteResult.ok) {
713
- throw quoteResult.error;
714
- }
715
- return quoteResult.value.quote_id;
864
+ entry.initialized = true;
865
+ hydrateFromStorage(entry);
866
+ if (entry.isDemoMode) {
867
+ const base = entry.collection.getBase() ?? emptyBase(entry.currency);
868
+ entry.collection.setBase({ ...base, isLoading: false });
869
+ return;
716
870
  }
717
- async function addItem(product, quantity = 1, options = {}) {
718
- const resolvedQuantity = clampQuantity(quantity);
719
- const normalizedOptions = {
720
- ...options,
721
- locationId: options.locationId || locationId || void 0,
722
- addOnOptionIds: normalizeOptionIds(options.addOnOptionIds)
723
- };
724
- const maybeProductWithVariants = product;
725
- if (Array.isArray(maybeProductWithVariants.variants) && maybeProductWithVariants.variants.length > 0 && !normalizedOptions.variantId) {
726
- console.warn(
727
- "[Cimplify] addItem() called without variantId for a product that has variants. Default variant pricing will be used."
728
- );
729
- }
730
- const previousSnapshot = snapshot;
731
- const lineKey = buildLineKey(product.id, normalizedOptions);
732
- const optimisticItem = attachInternalItemMetadata({
733
- id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
734
- product,
735
- quantity: resolvedQuantity,
736
- locationId: normalizedOptions.locationId,
737
- variantId: normalizedOptions.variantId,
738
- variant: normalizedOptions.variant,
739
- quoteId: normalizedOptions.quoteId,
740
- addOnOptionIds: normalizedOptions.addOnOptionIds,
741
- addOnOptions: normalizedOptions.addOnOptions,
742
- bundleSelections: normalizedOptions.bundleSelections,
743
- compositeSelections: normalizedOptions.compositeSelections,
744
- specialInstructions: normalizedOptions.specialInstructions,
745
- scheduledStart: normalizedOptions.scheduledStart,
746
- scheduledEnd: normalizedOptions.scheduledEnd,
747
- staffId: normalizedOptions.staffId
748
- });
749
- updateSnapshot((current) => {
750
- const existingIndex = current.items.findIndex(
751
- (item) => (item._lineKey ?? lineKeyFromItem(item)) === lineKey
752
- );
753
- const nextItems = [...current.items];
754
- if (existingIndex >= 0) {
755
- const existing = nextItems[existingIndex];
756
- nextItems[existingIndex] = {
757
- ...existing,
758
- quantity: existing.quantity + resolvedQuantity
759
- };
760
- } else {
761
- nextItems.push(optimisticItem);
762
- }
763
- const summary = calculateSummary(nextItems);
764
- return {
765
- ...current,
766
- items: nextItems,
767
- subtotal: summary.subtotal,
768
- tax: summary.tax,
769
- total: summary.total
770
- };
771
- });
772
- if (isDemoMode) {
773
- return;
774
- }
775
- try {
776
- const quoteId = await maybeResolveQuoteId(product, resolvedQuantity, normalizedOptions);
777
- const result = await client.cart.addItem({
871
+ entry.initializePromise = syncFromServer(entry).catch(() => {
872
+ const base = entry.collection.getBase() ?? emptyBase(entry.currency);
873
+ entry.collection.setBase({ ...base, isLoading: false });
874
+ });
875
+ await entry.initializePromise;
876
+ entry.initializePromise = null;
877
+ }
878
+ async function maybeResolveQuoteId(entry, product, quantity, options) {
879
+ if (options.quoteId) return options.quoteId;
880
+ const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
881
+ const requiresQuote = Boolean(
882
+ options.variantId || addOnOptionIds && addOnOptionIds.length > 0 || options.bundleSelections && options.bundleSelections.length > 0 || options.compositeSelections && options.compositeSelections.length > 0
883
+ );
884
+ if (!requiresQuote || entry.isDemoMode) return void 0;
885
+ const quoteResult = await entry.client.catalogue.fetchQuote({
886
+ product_id: product.id,
887
+ quantity,
888
+ location_id: options.locationId || entry.locationId || void 0,
889
+ variant_id: options.variantId,
890
+ add_on_option_ids: addOnOptionIds,
891
+ bundle_selections: options.bundleSelections,
892
+ composite_selections: options.compositeSelections
893
+ });
894
+ if (!quoteResult.ok) throw quoteResult.error;
895
+ return quoteResult.value.quote_id;
896
+ }
897
+ async function performAddItem(entry, product, quantity, options) {
898
+ const resolvedQuantity = clampQuantity(quantity);
899
+ const normalizedOptions = {
900
+ ...options,
901
+ locationId: options.locationId || entry.locationId || void 0,
902
+ addOnOptionIds: normalizeOptionIds(options.addOnOptionIds)
903
+ };
904
+ const lineKey = buildLineKey(product.id, normalizedOptions);
905
+ const tempItem = attachInternalItemMetadata({
906
+ id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
907
+ product,
908
+ quantity: resolvedQuantity,
909
+ locationId: normalizedOptions.locationId,
910
+ variantId: normalizedOptions.variantId,
911
+ variant: normalizedOptions.variant,
912
+ quoteId: normalizedOptions.quoteId,
913
+ addOnOptionIds: normalizedOptions.addOnOptionIds,
914
+ addOnOptions: normalizedOptions.addOnOptions,
915
+ bundleSelections: normalizedOptions.bundleSelections,
916
+ compositeSelections: normalizedOptions.compositeSelections,
917
+ specialInstructions: normalizedOptions.specialInstructions,
918
+ scheduledStart: normalizedOptions.scheduledStart,
919
+ scheduledEnd: normalizedOptions.scheduledEnd,
920
+ staffId: normalizedOptions.staffId
921
+ });
922
+ const idempotencyKey = `idem_${typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`}`;
923
+ const patch = { kind: "add_item", tempItem, lineKey };
924
+ if (entry.isDemoMode) {
925
+ advanceBaseWithPatch(entry, patch);
926
+ return;
927
+ }
928
+ const useOptimistic = entry.optimisticEnabled;
929
+ const opId = useOptimistic ? entry.collection.enqueue(patch, idempotencyKey) : null;
930
+ try {
931
+ const quoteId = await maybeResolveQuoteId(entry, product, resolvedQuantity, normalizedOptions);
932
+ const result = await entry.client.cart.addItem(
933
+ {
778
934
  item_id: product.id,
779
935
  quantity: resolvedQuantity,
780
936
  variant_id: normalizedOptions.variantId,
@@ -788,186 +944,103 @@ function createCartStore(params) {
788
944
  staff_id: normalizedOptions.staffId,
789
945
  resource_id: normalizedOptions.resourceId,
790
946
  customer_inputs: normalizedOptions.customerInputs
791
- });
792
- if (!result.ok) {
793
- throw result.error;
794
- }
795
- await sync();
796
- } catch (error) {
797
- snapshot = previousSnapshot;
798
- persistSnapshot();
799
- emit();
800
- throw error;
801
- }
802
- }
803
- async function removeItem(itemId) {
804
- const previousSnapshot = snapshot;
805
- updateSnapshot((current) => {
806
- const nextItems = current.items.filter((item) => item.id !== itemId);
807
- const summary = calculateSummary(nextItems);
808
- return {
809
- ...current,
810
- items: nextItems,
811
- subtotal: summary.subtotal,
812
- tax: summary.tax,
813
- total: summary.total
814
- };
815
- });
816
- if (isDemoMode) {
817
- return;
818
- }
819
- try {
820
- const result = await client.cart.removeItem(itemId);
821
- if (!result.ok) {
822
- throw result.error;
823
- }
824
- await sync();
825
- } catch (error) {
826
- snapshot = previousSnapshot;
827
- persistSnapshot();
828
- emit();
829
- throw error;
830
- }
947
+ },
948
+ { idempotencyKey }
949
+ );
950
+ if (!result.ok) throw result.error;
951
+ await syncFromServer(entry);
952
+ if (opId) entry.collection.confirm(opId);
953
+ } catch (error) {
954
+ const cimplifyError = error;
955
+ if (opId) {
956
+ entry.collection.fail(opId, cimplifyError);
957
+ emitMutationError(entry, "add_item", opId, idempotencyKey, cimplifyError);
958
+ scheduleFailedOpRemoval(entry, opId);
959
+ }
960
+ throw cimplifyError;
961
+ }
962
+ }
963
+ async function performRemoveItem(entry, itemId) {
964
+ const idempotencyKey = "";
965
+ const patch = { kind: "remove_item", itemId };
966
+ if (entry.isDemoMode) {
967
+ advanceBaseWithPatch(entry, patch);
968
+ return;
831
969
  }
832
- async function updateQuantity(itemId, quantity) {
833
- if (quantity <= 0) {
834
- await removeItem(itemId);
835
- return;
836
- }
837
- const resolvedQuantity = clampQuantity(quantity);
838
- const previousSnapshot = snapshot;
839
- updateSnapshot((current) => {
840
- const nextItems = current.items.map(
841
- (item) => item.id === itemId ? {
842
- ...item,
843
- quantity: resolvedQuantity
844
- } : item
845
- );
846
- const summary = calculateSummary(nextItems);
847
- return {
848
- ...current,
849
- items: nextItems,
850
- subtotal: summary.subtotal,
851
- tax: summary.tax,
852
- total: summary.total
853
- };
854
- });
855
- if (isDemoMode) {
856
- return;
857
- }
858
- try {
859
- const result = await client.cart.updateQuantity(itemId, resolvedQuantity);
860
- if (!result.ok) {
861
- throw result.error;
862
- }
863
- await sync();
864
- } catch (error) {
865
- snapshot = previousSnapshot;
866
- persistSnapshot();
867
- emit();
868
- throw error;
869
- }
870
- }
871
- async function clearCart() {
872
- const previousSnapshot = snapshot;
873
- updateSnapshot((current) => ({
874
- ...current,
875
- items: [],
876
- subtotal: 0,
877
- tax: 0,
878
- total: 0
879
- }));
880
- if (isDemoMode) {
881
- return;
882
- }
883
- try {
884
- const result = await client.cart.clear();
885
- if (!result.ok) {
886
- throw result.error;
887
- }
888
- await sync();
889
- } catch (error) {
890
- snapshot = previousSnapshot;
891
- persistSnapshot();
892
- emit();
893
- throw error;
894
- }
970
+ const useOptimistic = entry.optimisticEnabled;
971
+ const opId = useOptimistic ? entry.collection.enqueue(patch, idempotencyKey) : null;
972
+ try {
973
+ const result = await entry.client.cart.removeItem(itemId);
974
+ if (!result.ok) throw result.error;
975
+ await syncFromServer(entry);
976
+ if (opId) entry.collection.confirm(opId);
977
+ } catch (error) {
978
+ const cimplifyError = error;
979
+ if (opId) {
980
+ entry.collection.fail(opId, cimplifyError);
981
+ emitMutationError(entry, "remove_item", opId, idempotencyKey, cimplifyError);
982
+ scheduleFailedOpRemoval(entry, opId);
983
+ }
984
+ throw cimplifyError;
985
+ }
986
+ }
987
+ async function performUpdateQuantity(entry, itemId, quantity) {
988
+ if (quantity <= 0) {
989
+ return performRemoveItem(entry, itemId);
990
+ }
991
+ const resolved = clampQuantity(quantity);
992
+ const idempotencyKey = "";
993
+ const patch = { kind: "set_quantity", itemId, quantity: resolved };
994
+ if (entry.isDemoMode) {
995
+ advanceBaseWithPatch(entry, patch);
996
+ return;
895
997
  }
896
- async function initialize() {
897
- if (initialized) {
898
- if (initializePromise) {
899
- await initializePromise;
900
- }
901
- return;
902
- }
903
- initialized = true;
904
- hydrateFromStorage();
905
- if (isDemoMode) {
906
- updateSnapshot((current) => ({
907
- ...current,
908
- isLoading: false
909
- }));
910
- return;
911
- }
912
- initializePromise = sync().catch(() => {
913
- updateSnapshot((current) => ({
914
- ...current,
915
- isLoading: false
916
- }));
917
- });
918
- await initializePromise;
919
- initializePromise = null;
998
+ const useOptimistic = entry.optimisticEnabled;
999
+ const opId = useOptimistic ? entry.collection.enqueue(patch, idempotencyKey) : null;
1000
+ try {
1001
+ const result = await entry.client.cart.updateQuantity(itemId, resolved);
1002
+ if (!result.ok) throw result.error;
1003
+ await syncFromServer(entry);
1004
+ if (opId) entry.collection.confirm(opId);
1005
+ } catch (error) {
1006
+ const cimplifyError = error;
1007
+ if (opId) {
1008
+ entry.collection.fail(opId, cimplifyError);
1009
+ emitMutationError(entry, "update_quantity", opId, idempotencyKey, cimplifyError);
1010
+ scheduleFailedOpRemoval(entry, opId);
1011
+ }
1012
+ throw cimplifyError;
1013
+ }
1014
+ }
1015
+ async function performClear(entry) {
1016
+ const idempotencyKey = "";
1017
+ const patch = { kind: "clear" };
1018
+ if (entry.isDemoMode) {
1019
+ advanceBaseWithPatch(entry, patch);
1020
+ return;
920
1021
  }
921
- async function reorder(orderId) {
922
- const result = await client.cart.reorderFromOrder(orderId);
923
- if (!result.ok) {
924
- throw result.error;
1022
+ const useOptimistic = entry.optimisticEnabled;
1023
+ const opId = useOptimistic ? entry.collection.enqueue(patch, idempotencyKey) : null;
1024
+ try {
1025
+ const result = await entry.client.cart.clear();
1026
+ if (!result.ok) throw result.error;
1027
+ await syncFromServer(entry);
1028
+ if (opId) entry.collection.confirm(opId);
1029
+ } catch (error) {
1030
+ const cimplifyError = error;
1031
+ if (opId) {
1032
+ entry.collection.fail(opId, cimplifyError);
1033
+ emitMutationError(entry, "clear", opId, idempotencyKey, cimplifyError);
1034
+ scheduleFailedOpRemoval(entry, opId);
925
1035
  }
926
- await sync();
927
- return result.value;
1036
+ throw cimplifyError;
928
1037
  }
929
- return {
930
- subscribe(listener) {
931
- listeners.add(listener);
932
- return () => {
933
- listeners.delete(listener);
934
- };
935
- },
936
- getSnapshot() {
937
- return snapshot;
938
- },
939
- initialize,
940
- addItem,
941
- removeItem,
942
- updateQuantity,
943
- clearCart,
944
- sync,
945
- reorder
946
- };
947
1038
  }
948
- function getStoreKey(client, locationId, isDemoMode) {
949
- return [
950
- client.getPublicKey() || "__demo__",
951
- locationId || "__no_location__",
952
- isDemoMode ? "demo" : "live"
953
- ].join(":");
954
- }
955
- function getOrCreateStore(params) {
956
- const { client, locationId, isDemoMode, currency } = params;
957
- const storeKey = getStoreKey(client, locationId, isDemoMode);
958
- const existing = cartStores.get(storeKey);
959
- if (existing) {
960
- return existing;
961
- }
962
- const created = createCartStore({
963
- client,
964
- storeKey,
965
- locationId,
966
- isDemoMode,
967
- currency
968
- });
969
- cartStores.set(storeKey, created);
970
- return created;
1039
+ async function performReorder(entry, orderId) {
1040
+ const result = await entry.client.cart.reorderFromOrder(orderId);
1041
+ if (!result.ok) throw result.error;
1042
+ await syncFromServer(entry);
1043
+ return result.value;
971
1044
  }
972
1045
  function useCart(options = {}) {
973
1046
  const context = useOptionalCimplifyClient();
@@ -978,77 +1051,92 @@ function useCart(options = {}) {
978
1051
  const locationId = options.locationId ?? client.getLocationId();
979
1052
  const isDemoMode = options.demoMode ?? context?.isDemoMode ?? client.getPublicKey().trim().length === 0;
980
1053
  const currency = options.currency ?? context?.currency ?? "USD";
981
- const store = useMemo(
1054
+ const optimisticEnabled = context?.optimistic ?? true;
1055
+ const entry = useMemo(
982
1056
  () => getOrCreateStore({
983
1057
  client,
984
1058
  locationId,
985
1059
  isDemoMode,
986
- currency
1060
+ currency,
1061
+ optimisticEnabled
987
1062
  }),
988
- [client, currency, isDemoMode, locationId]
989
- );
990
- const snapshot = useSyncExternalStore(
991
- store.subscribe,
992
- store.getSnapshot,
993
- store.getSnapshot
1063
+ [client, currency, isDemoMode, locationId, optimisticEnabled]
994
1064
  );
1065
+ const view = useCollectionStore(entry.collection);
995
1066
  useEffect(() => {
996
- void store.initialize();
997
- }, [store]);
1067
+ void ensureInitialized(entry);
1068
+ }, [entry]);
998
1069
  const addItem = useCallback(
999
1070
  async (product, quantity, addOptions) => {
1000
- await store.addItem(product, quantity, addOptions);
1071
+ await performAddItem(entry, product, quantity ?? 1, addOptions ?? {});
1001
1072
  },
1002
- [store]
1073
+ [entry]
1003
1074
  );
1004
1075
  const removeItem = useCallback(
1005
1076
  async (itemId) => {
1006
- await store.removeItem(itemId);
1077
+ await performRemoveItem(entry, itemId);
1007
1078
  },
1008
- [store]
1079
+ [entry]
1009
1080
  );
1010
1081
  const updateQuantity = useCallback(
1011
1082
  async (itemId, quantity) => {
1012
- await store.updateQuantity(itemId, quantity);
1083
+ await performUpdateQuantity(entry, itemId, quantity);
1013
1084
  },
1014
- [store]
1085
+ [entry]
1015
1086
  );
1016
1087
  const clearCart = useCallback(async () => {
1017
- await store.clearCart();
1018
- }, [store]);
1088
+ await performClear(entry);
1089
+ }, [entry]);
1019
1090
  const sync = useCallback(async () => {
1020
- try {
1021
- await store.sync();
1022
- } catch (syncError) {
1023
- throw syncError;
1024
- }
1025
- }, [store]);
1091
+ await syncFromServer(entry);
1092
+ }, [entry]);
1026
1093
  const reorder = useCallback(
1027
1094
  async (orderId) => {
1028
- return store.reorder(orderId);
1095
+ return performReorder(entry, orderId);
1029
1096
  },
1030
- [store]
1097
+ [entry]
1031
1098
  );
1032
1099
  const itemCount = useMemo(
1033
- () => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
1034
- [snapshot.items]
1100
+ () => view.items.reduce((sum, item) => sum + item.quantity, 0),
1101
+ [view.items]
1102
+ );
1103
+ return useMemo(
1104
+ () => ({
1105
+ items: view.items,
1106
+ itemCount,
1107
+ subtotal: view.subtotal,
1108
+ tax: view.tax,
1109
+ total: view.total,
1110
+ currency: view.currency,
1111
+ isEmpty: itemCount === 0,
1112
+ isLoading: view.isLoading,
1113
+ isOptimistic: view.isOptimistic,
1114
+ pendingOpCount: view.pendingOpCount,
1115
+ addItem,
1116
+ removeItem,
1117
+ updateQuantity,
1118
+ clearCart,
1119
+ sync,
1120
+ reorder
1121
+ }),
1122
+ [
1123
+ view.items,
1124
+ itemCount,
1125
+ view.subtotal,
1126
+ view.tax,
1127
+ view.total,
1128
+ view.currency,
1129
+ view.isLoading,
1130
+ view.isOptimistic,
1131
+ view.pendingOpCount,
1132
+ addItem,
1133
+ removeItem,
1134
+ updateQuantity,
1135
+ clearCart,
1136
+ sync,
1137
+ reorder
1138
+ ]
1035
1139
  );
1036
- return useMemo(() => ({
1037
- items: snapshot.items,
1038
- itemCount,
1039
- subtotal: snapshot.subtotal,
1040
- tax: snapshot.tax,
1041
- total: snapshot.total,
1042
- currency: snapshot.currency,
1043
- isEmpty: itemCount === 0,
1044
- isLoading: snapshot.isLoading,
1045
- addItem,
1046
- removeItem,
1047
- updateQuantity,
1048
- clearCart,
1049
- sync,
1050
- reorder
1051
- }), [snapshot.items, itemCount, snapshot.subtotal, snapshot.tax, snapshot.total, snapshot.currency, snapshot.isLoading, addItem, removeItem, updateQuantity, clearCart, sync, reorder]);
1052
1140
  }
1053
1141
  var LOCATION_STORAGE_KEY = "cimplify_location_id";
1054
1142
  var DISPLAY_CURRENCY_STORAGE_KEY = "cimplify_display_currency";
@@ -1120,7 +1208,8 @@ var CimplifyStateContext = createContext(null);
1120
1208
  function CimplifyProvider({
1121
1209
  client,
1122
1210
  children,
1123
- onLocationChange
1211
+ onLocationChange,
1212
+ optimistic = true
1124
1213
  }) {
1125
1214
  const resolvedClient = useMemo(() => client ?? createDefaultClient(), [client]);
1126
1215
  const onLocationChangeRef = useRef(onLocationChange);
@@ -1264,7 +1353,8 @@ function CimplifyProvider({
1264
1353
  locations,
1265
1354
  isReady,
1266
1355
  isDemoMode,
1267
- baseCurrency
1356
+ baseCurrency,
1357
+ optimistic
1268
1358
  }),
1269
1359
  [
1270
1360
  resolvedClient,
@@ -1272,7 +1362,8 @@ function CimplifyProvider({
1272
1362
  baseCurrency,
1273
1363
  locations,
1274
1364
  isReady,
1275
- isDemoMode
1365
+ isDemoMode,
1366
+ optimistic
1276
1367
  ]
1277
1368
  );
1278
1369
  const stateContextValue = useMemo(
@@ -6350,85 +6441,94 @@ function CartLineItemRow({
6350
6441
  });
6351
6442
  const hasComposite = item.compositeSelections && item.compositeSelections.length > 0;
6352
6443
  const hasBundle = item.bundleSelections && item.bundleSelections.length > 0;
6353
- return /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-item": true, className: classNames?.item, children: [
6354
- /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-item-info": true, children: [
6355
- /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-name": true, className: classNames?.itemName, children: item.product.name }),
6356
- item.variant && /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-variant": true, children: getVariantDisplayName(item.variant, item.product.name) }),
6357
- item.addOnOptions && item.addOnOptions.length > 0 && /* @__PURE__ */ jsxs("span", { "data-cimplify-cart-item-addons": true, children: [
6358
- "+ ",
6359
- item.addOnOptions.map((opt) => opt.name).join(", ")
6360
- ] }),
6361
- (hasComposite || hasBundle) && /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-badge": true, children: hasComposite ? "Custom" : "Bundle" }),
6362
- item.specialInstructions && /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-instructions": true, className: "text-xs text-muted-foreground", children: item.specialInstructions }),
6363
- item.customerInputs && item.customerInputs.length > 0 && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-customer-inputs": true, className: classNames?.customerInputs, children: item.customerInputs.map((input) => /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-input": true, children: [
6364
- /* @__PURE__ */ jsx("span", { "data-cimplify-cart-input-label": true, children: input.field_name }),
6365
- /* @__PURE__ */ jsx("span", { "data-cimplify-cart-input-value": true, children: input.field_type === INPUT_FIELD_TYPE.File || input.field_type === INPUT_FIELD_TYPE.Image ? String(input.value).split("/").pop() || "Uploaded file" : String(input.value) })
6366
- ] }, input.field_id)) }),
6367
- (item.lineType === "service" || item.product.type === "service") && item.scheduledStart && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-service-info": true, className: classNames?.serviceInfo, children: [
6368
- /* @__PURE__ */ jsxs("span", { children: [
6369
- "\u{1F4C5}",
6370
- " ",
6371
- new Date(item.scheduledStart).toLocaleDateString(),
6372
- " at ",
6373
- new Date(item.scheduledStart).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
6374
- ] }),
6375
- item.confirmationCode && /* @__PURE__ */ jsxs("span", { children: [
6376
- "Confirmation: ",
6377
- item.confirmationCode
6378
- ] }),
6379
- item.serviceStatus && /* @__PURE__ */ jsx("span", { "data-status": item.serviceStatus, children: item.serviceStatus })
6380
- ] }),
6381
- (item.lineType === "digital" || item.product.type === "digital") && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-digital-info": true, className: classNames?.digitalInfo, children: /* @__PURE__ */ jsxs("span", { children: [
6382
- "\u26A1",
6383
- " Instant digital delivery"
6384
- ] }) }),
6385
- item.bundleResolved && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-bundle-breakdown": true, className: classNames?.bundleBreakdown, children: item.bundleResolved.selections.map((sel) => /* @__PURE__ */ jsxs("div", { "data-product-type": sel.product_type, children: [
6386
- /* @__PURE__ */ jsxs("span", { children: [
6387
- sel.quantity,
6388
- "\xD7 ",
6389
- sel.product_name
6390
- ] }),
6391
- sel.product_type === "service" && sel.scheduling?.scheduled_start && /* @__PURE__ */ jsxs("span", { children: [
6392
- " \u2014 ",
6393
- new Date(sel.scheduling.scheduled_start).toLocaleDateString()
6394
- ] }),
6395
- sel.product_type === "digital" && /* @__PURE__ */ jsx("span", { children: " \u2014 Digital" })
6396
- ] }, sel.component_id)) }),
6397
- item.compositeResolved && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-composite-breakdown": true, className: classNames?.compositeBreakdown, children: item.compositeResolved.selections.map((sel) => /* @__PURE__ */ jsxs("div", { "data-product-type": sel.product_type, children: [
6398
- /* @__PURE__ */ jsxs("span", { children: [
6399
- sel.quantity,
6400
- "\xD7 ",
6401
- sel.component_name
6444
+ const isOptimistic = item.isOptimistic === true;
6445
+ return /* @__PURE__ */ jsxs(
6446
+ "div",
6447
+ {
6448
+ "data-cimplify-cart-item": true,
6449
+ "data-optimistic": isOptimistic ? "true" : void 0,
6450
+ className: classNames?.item,
6451
+ children: [
6452
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-item-info": true, children: [
6453
+ /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-name": true, className: classNames?.itemName, children: item.product.name }),
6454
+ item.variant && /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-variant": true, children: getVariantDisplayName(item.variant, item.product.name) }),
6455
+ item.addOnOptions && item.addOnOptions.length > 0 && /* @__PURE__ */ jsxs("span", { "data-cimplify-cart-item-addons": true, children: [
6456
+ "+ ",
6457
+ item.addOnOptions.map((opt) => opt.name).join(", ")
6458
+ ] }),
6459
+ (hasComposite || hasBundle) && /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-badge": true, children: hasComposite ? "Custom" : "Bundle" }),
6460
+ item.specialInstructions && /* @__PURE__ */ jsx("span", { "data-cimplify-cart-item-instructions": true, className: "text-xs text-muted-foreground", children: item.specialInstructions }),
6461
+ item.customerInputs && item.customerInputs.length > 0 && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-customer-inputs": true, className: classNames?.customerInputs, children: item.customerInputs.map((input) => /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-input": true, children: [
6462
+ /* @__PURE__ */ jsx("span", { "data-cimplify-cart-input-label": true, children: input.field_name }),
6463
+ /* @__PURE__ */ jsx("span", { "data-cimplify-cart-input-value": true, children: input.field_type === INPUT_FIELD_TYPE.File || input.field_type === INPUT_FIELD_TYPE.Image ? String(input.value).split("/").pop() || "Uploaded file" : String(input.value) })
6464
+ ] }, input.field_id)) }),
6465
+ (item.lineType === "service" || item.product.type === "service") && item.scheduledStart && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-service-info": true, className: classNames?.serviceInfo, children: [
6466
+ /* @__PURE__ */ jsxs("span", { children: [
6467
+ "\u{1F4C5}",
6468
+ " ",
6469
+ new Date(item.scheduledStart).toLocaleDateString(),
6470
+ " at ",
6471
+ new Date(item.scheduledStart).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
6472
+ ] }),
6473
+ item.confirmationCode && /* @__PURE__ */ jsxs("span", { children: [
6474
+ "Confirmation: ",
6475
+ item.confirmationCode
6476
+ ] }),
6477
+ item.serviceStatus && /* @__PURE__ */ jsx("span", { "data-status": item.serviceStatus, children: item.serviceStatus })
6478
+ ] }),
6479
+ (item.lineType === "digital" || item.product.type === "digital") && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-digital-info": true, className: classNames?.digitalInfo, children: /* @__PURE__ */ jsxs("span", { children: [
6480
+ "\u26A1",
6481
+ " Instant digital delivery"
6482
+ ] }) }),
6483
+ item.bundleResolved && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-bundle-breakdown": true, className: classNames?.bundleBreakdown, children: item.bundleResolved.selections.map((sel) => /* @__PURE__ */ jsxs("div", { "data-product-type": sel.product_type, children: [
6484
+ /* @__PURE__ */ jsxs("span", { children: [
6485
+ sel.quantity,
6486
+ "\xD7 ",
6487
+ sel.product_name
6488
+ ] }),
6489
+ sel.product_type === "service" && sel.scheduling?.scheduled_start && /* @__PURE__ */ jsxs("span", { children: [
6490
+ " \u2014 ",
6491
+ new Date(sel.scheduling.scheduled_start).toLocaleDateString()
6492
+ ] }),
6493
+ sel.product_type === "digital" && /* @__PURE__ */ jsx("span", { children: " \u2014 Digital" })
6494
+ ] }, sel.component_id)) }),
6495
+ item.compositeResolved && /* @__PURE__ */ jsx("div", { "data-cimplify-cart-composite-breakdown": true, className: classNames?.compositeBreakdown, children: item.compositeResolved.selections.map((sel) => /* @__PURE__ */ jsxs("div", { "data-product-type": sel.product_type, children: [
6496
+ /* @__PURE__ */ jsxs("span", { children: [
6497
+ sel.quantity,
6498
+ "\xD7 ",
6499
+ sel.component_name
6500
+ ] }),
6501
+ sel.product_type === "service" && sel.scheduling?.scheduled_start && /* @__PURE__ */ jsxs("span", { children: [
6502
+ " \u2014 ",
6503
+ new Date(sel.scheduling.scheduled_start).toLocaleDateString()
6504
+ ] })
6505
+ ] }, sel.component_id)) }),
6506
+ /* @__PURE__ */ jsx(Price, { amount: unitPrice, className: classNames?.itemPrice })
6402
6507
  ] }),
6403
- sel.product_type === "service" && sel.scheduling?.scheduled_start && /* @__PURE__ */ jsxs("span", { children: [
6404
- " \u2014 ",
6405
- new Date(sel.scheduling.scheduled_start).toLocaleDateString()
6508
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-item-controls": true, className: classNames?.quantity, children: [
6509
+ /* @__PURE__ */ jsx(
6510
+ QuantitySelector,
6511
+ {
6512
+ value: item.quantity,
6513
+ onChange: (qty) => onQuantityChange(item.id, qty),
6514
+ min: 0
6515
+ }
6516
+ ),
6517
+ /* @__PURE__ */ jsx(
6518
+ "button",
6519
+ {
6520
+ type: "button",
6521
+ onClick: () => onRemove(item.id),
6522
+ "data-cimplify-cart-item-remove": true,
6523
+ className: classNames?.removeButton,
6524
+ "aria-label": `Remove ${item.product.name}`,
6525
+ children: "Remove"
6526
+ }
6527
+ )
6406
6528
  ] })
6407
- ] }, sel.component_id)) }),
6408
- /* @__PURE__ */ jsx(Price, { amount: unitPrice, className: classNames?.itemPrice })
6409
- ] }),
6410
- /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-item-controls": true, className: classNames?.quantity, children: [
6411
- /* @__PURE__ */ jsx(
6412
- QuantitySelector,
6413
- {
6414
- value: item.quantity,
6415
- onChange: (qty) => onQuantityChange(item.id, qty),
6416
- min: 0
6417
- }
6418
- ),
6419
- /* @__PURE__ */ jsx(
6420
- "button",
6421
- {
6422
- type: "button",
6423
- onClick: () => onRemove(item.id),
6424
- "data-cimplify-cart-item-remove": true,
6425
- className: classNames?.removeButton,
6426
- "aria-label": `Remove ${item.product.name}`,
6427
- children: "Remove"
6428
- }
6429
- )
6430
- ] })
6431
- ] });
6529
+ ]
6530
+ }
6531
+ );
6432
6532
  }
6433
6533
  function CartSummary({
6434
6534
  cart: serverCart,
@@ -6439,7 +6539,18 @@ function CartSummary({
6439
6539
  className,
6440
6540
  classNames
6441
6541
  }) {
6442
- const { items, itemCount, subtotal, tax, total, isEmpty, removeItem, updateQuantity } = useCart();
6542
+ const {
6543
+ items,
6544
+ itemCount,
6545
+ subtotal,
6546
+ tax,
6547
+ total,
6548
+ isEmpty,
6549
+ isOptimistic,
6550
+ pendingOpCount,
6551
+ removeItem,
6552
+ updateQuantity
6553
+ } = useCart();
6443
6554
  const deliveryFee = serverCart?.delivery_fee;
6444
6555
  const serviceCharge = serverCart?.service_charge;
6445
6556
  const totalDiscounts = serverCart?.total_discounts;
@@ -6457,60 +6568,69 @@ function CartSummary({
6457
6568
  void updateQuantity(itemId, qty);
6458
6569
  }
6459
6570
  };
6460
- return /* @__PURE__ */ jsx("div", { "data-cimplify-cart-summary": true, className: cn(className, classNames?.root), children: isEmpty ? /* @__PURE__ */ jsx("div", { "data-cimplify-cart-empty": true, className: classNames?.empty, children: /* @__PURE__ */ jsx("p", { children: emptyMessage }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6461
- /* @__PURE__ */ jsx("div", { "data-cimplify-cart-items": true, children: items.map((item) => /* @__PURE__ */ jsx(
6462
- CartLineItemRow,
6463
- {
6464
- item,
6465
- onRemove: handleRemove,
6466
- onQuantityChange: handleQuantityChange,
6467
- classNames
6468
- },
6469
- item.id
6470
- )) }),
6471
- /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-totals": true, className: classNames?.totals, children: [
6472
- /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-subtotal": true, className: classNames?.subtotal, children: [
6473
- /* @__PURE__ */ jsxs("span", { children: [
6474
- "Subtotal (",
6475
- itemCount,
6476
- " ",
6477
- itemCount === 1 ? "item" : "items",
6478
- ")"
6571
+ return /* @__PURE__ */ jsx(
6572
+ "div",
6573
+ {
6574
+ "data-cimplify-cart-summary": true,
6575
+ "data-optimistic": isOptimistic ? "true" : void 0,
6576
+ "data-pending-ops": pendingOpCount > 0 ? pendingOpCount : void 0,
6577
+ className: cn(className, classNames?.root),
6578
+ children: isEmpty ? /* @__PURE__ */ jsx("div", { "data-cimplify-cart-empty": true, className: classNames?.empty, children: /* @__PURE__ */ jsx("p", { children: emptyMessage }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
6579
+ /* @__PURE__ */ jsx("div", { "data-cimplify-cart-items": true, children: items.map((item) => /* @__PURE__ */ jsx(
6580
+ CartLineItemRow,
6581
+ {
6582
+ item,
6583
+ onRemove: handleRemove,
6584
+ onQuantityChange: handleQuantityChange,
6585
+ classNames
6586
+ },
6587
+ item.id
6588
+ )) }),
6589
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-totals": true, className: classNames?.totals, children: [
6590
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-subtotal": true, className: classNames?.subtotal, children: [
6591
+ /* @__PURE__ */ jsxs("span", { children: [
6592
+ "Subtotal (",
6593
+ itemCount,
6594
+ " ",
6595
+ itemCount === 1 ? "item" : "items",
6596
+ ")"
6597
+ ] }),
6598
+ /* @__PURE__ */ jsx(Price, { amount: subtotal })
6599
+ ] }),
6600
+ totalDiscounts != null && parsePrice(totalDiscounts) > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-discount": true, className: classNames?.discount, children: [
6601
+ /* @__PURE__ */ jsx("span", { children: "Discount" }),
6602
+ /* @__PURE__ */ jsx(Price, { amount: totalDiscounts, prefix: "-" })
6603
+ ] }),
6604
+ deliveryFee != null && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-delivery-fee": true, className: classNames?.deliveryFee, children: [
6605
+ /* @__PURE__ */ jsx("span", { children: "Delivery fee" }),
6606
+ parsePrice(deliveryFee) > 0 ? /* @__PURE__ */ jsx(Price, { amount: deliveryFee }) : /* @__PURE__ */ jsx("span", { children: "Free delivery" })
6607
+ ] }),
6608
+ serviceCharge != null && parsePrice(serviceCharge) > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-service-charge": true, className: classNames?.serviceCharge, children: [
6609
+ /* @__PURE__ */ jsx("span", { children: "Service charge" }),
6610
+ /* @__PURE__ */ jsx(Price, { amount: serviceCharge })
6611
+ ] }),
6612
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-tax": true, className: classNames?.tax, children: [
6613
+ /* @__PURE__ */ jsx("span", { children: "Tax" }),
6614
+ /* @__PURE__ */ jsx(Price, { amount: tax })
6615
+ ] }),
6616
+ /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-total": true, className: classNames?.total, children: [
6617
+ /* @__PURE__ */ jsx("span", { children: "Total" }),
6618
+ /* @__PURE__ */ jsx(Price, { amount: total })
6619
+ ] })
6479
6620
  ] }),
6480
- /* @__PURE__ */ jsx(Price, { amount: subtotal })
6481
- ] }),
6482
- totalDiscounts != null && parsePrice(totalDiscounts) > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-discount": true, className: classNames?.discount, children: [
6483
- /* @__PURE__ */ jsx("span", { children: "Discount" }),
6484
- /* @__PURE__ */ jsx(Price, { amount: totalDiscounts, prefix: "-" })
6485
- ] }),
6486
- deliveryFee != null && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-delivery-fee": true, className: classNames?.deliveryFee, children: [
6487
- /* @__PURE__ */ jsx("span", { children: "Delivery fee" }),
6488
- parsePrice(deliveryFee) > 0 ? /* @__PURE__ */ jsx(Price, { amount: deliveryFee }) : /* @__PURE__ */ jsx("span", { children: "Free delivery" })
6489
- ] }),
6490
- serviceCharge != null && parsePrice(serviceCharge) > 0 && /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-service-charge": true, className: classNames?.serviceCharge, children: [
6491
- /* @__PURE__ */ jsx("span", { children: "Service charge" }),
6492
- /* @__PURE__ */ jsx(Price, { amount: serviceCharge })
6493
- ] }),
6494
- /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-tax": true, className: classNames?.tax, children: [
6495
- /* @__PURE__ */ jsx("span", { children: "Tax" }),
6496
- /* @__PURE__ */ jsx(Price, { amount: tax })
6497
- ] }),
6498
- /* @__PURE__ */ jsxs("div", { "data-cimplify-cart-total": true, className: classNames?.total, children: [
6499
- /* @__PURE__ */ jsx("span", { children: "Total" }),
6500
- /* @__PURE__ */ jsx(Price, { amount: total })
6621
+ onCheckout && /* @__PURE__ */ jsx(
6622
+ "button",
6623
+ {
6624
+ type: "button",
6625
+ onClick: onCheckout,
6626
+ "data-cimplify-cart-checkout": true,
6627
+ className: classNames?.checkout,
6628
+ children: "Proceed to Checkout"
6629
+ }
6630
+ )
6501
6631
  ] })
6502
- ] }),
6503
- onCheckout && /* @__PURE__ */ jsx(
6504
- "button",
6505
- {
6506
- type: "button",
6507
- onClick: onCheckout,
6508
- "data-cimplify-cart-checkout": true,
6509
- className: classNames?.checkout,
6510
- children: "Proceed to Checkout"
6511
- }
6512
- )
6513
- ] }) });
6632
+ }
6633
+ );
6514
6634
  }
6515
6635
  function AvailabilityBadge({
6516
6636
  product,