@cimplify/sdk 0.6.6 → 0.6.7

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/react.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback } from 'react';
1
+ import { createContext, useContext, useState, useEffect, useRef, useMemo, useCallback, useSyncExternalStore } from 'react';
2
2
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
3
3
 
4
4
  // src/react/index.tsx
@@ -404,9 +404,11 @@ function CimplifyCheckout({
404
404
  const elementsRef = useRef(null);
405
405
  const activeCheckoutRef = useRef(null);
406
406
  const initialAppearanceRef = useRef(appearance);
407
+ const hasWarnedInlineAppearanceRef = useRef(false);
407
408
  const isMountedRef = useRef(true);
408
409
  const demoRunRef = useRef(0);
409
410
  const isDemoCheckout = demoMode ?? client.getPublicKey().trim().length === 0;
411
+ const isTestMode = client.isTestMode();
410
412
  const emitStatus = useCallback(
411
413
  (nextStatus, context = {}) => {
412
414
  setStatus(nextStatus);
@@ -420,6 +422,14 @@ function CimplifyCheckout({
420
422
  setOrderType(resolvedOrderTypes[0] || "pickup");
421
423
  }
422
424
  }, [resolvedOrderTypes, orderType]);
425
+ useEffect(() => {
426
+ if (appearance && appearance !== initialAppearanceRef.current && !hasWarnedInlineAppearanceRef.current) {
427
+ hasWarnedInlineAppearanceRef.current = true;
428
+ console.warn(
429
+ "[Cimplify] `appearance` prop reference changed after mount. Elements keep the initial appearance to avoid iframe remount. Memoize appearance with useMemo() to remove this warning."
430
+ );
431
+ }
432
+ }, [appearance]);
423
433
  useEffect(() => {
424
434
  let cancelled = false;
425
435
  async function bootstrap() {
@@ -626,6 +636,19 @@ function CimplifyCheckout({
626
636
  return /* @__PURE__ */ jsx("div", { className, "data-cimplify-checkout": "", children: /* @__PURE__ */ jsx("p", { "data-cimplify-error": "", style: { fontSize: "14px", color: "#b91c1c" }, children: errorMessage || "Unable to initialize checkout. Please refresh and try again." }) });
627
637
  }
628
638
  return /* @__PURE__ */ jsxs("div", { className, "data-cimplify-checkout": "", children: [
639
+ isTestMode && !isDemoCheckout && /* @__PURE__ */ jsx(
640
+ "p",
641
+ {
642
+ "data-cimplify-test-mode": "",
643
+ style: {
644
+ marginBottom: "10px",
645
+ fontSize: "12px",
646
+ fontWeight: 600,
647
+ color: "#92400e"
648
+ },
649
+ children: "Test mode - no real charges"
650
+ }
651
+ ),
629
652
  /* @__PURE__ */ jsx("div", { "data-cimplify-section": "auth", children: /* @__PURE__ */ jsx("div", { ref: isDemoCheckout ? void 0 : authMountRef }) }),
630
653
  /* @__PURE__ */ jsx("div", { "data-cimplify-section": "order-type", style: { marginTop: "12px" }, children: /* @__PURE__ */ jsx(
631
654
  "div",
@@ -693,12 +716,74 @@ var ErrorCode = {
693
716
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
694
717
  NETWORK_ERROR: "NETWORK_ERROR",
695
718
  TIMEOUT: "TIMEOUT",
719
+ UNAUTHORIZED: "UNAUTHORIZED",
696
720
  NOT_FOUND: "NOT_FOUND"};
721
+ var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
722
+ function docsUrlForCode(code) {
723
+ return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
724
+ }
725
+ var ERROR_SUGGESTIONS = {
726
+ UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
727
+ NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
728
+ TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
729
+ UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
730
+ FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
731
+ NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
732
+ VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
733
+ CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
734
+ CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
735
+ CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
736
+ ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
737
+ VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
738
+ VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
739
+ ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
740
+ ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
741
+ CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
742
+ DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
743
+ CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
744
+ PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
745
+ PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
746
+ INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
747
+ CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
748
+ INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
749
+ OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
750
+ AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
751
+ PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
752
+ SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
753
+ BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
754
+ SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
755
+ OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
756
+ INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
757
+ BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
758
+ INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
759
+ ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
760
+ NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
761
+ PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
762
+ AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
763
+ AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
764
+ ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
765
+ CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
766
+ CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
767
+ REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
768
+ POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
769
+ FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
770
+ };
771
+ var ERROR_HINTS = Object.fromEntries(
772
+ Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
773
+ code,
774
+ {
775
+ docs_url: docsUrlForCode(code),
776
+ suggestion
777
+ }
778
+ ])
779
+ );
697
780
  var CimplifyError = class extends Error {
698
- constructor(code, message, retryable = false) {
781
+ constructor(code, message, retryable = false, docs_url, suggestion) {
699
782
  super(message);
700
783
  this.code = code;
701
784
  this.retryable = retryable;
785
+ this.docs_url = docs_url;
786
+ this.suggestion = suggestion;
702
787
  this.name = "CimplifyError";
703
788
  }
704
789
  /** User-friendly message safe to display */
@@ -706,6 +791,28 @@ var CimplifyError = class extends Error {
706
791
  return this.message;
707
792
  }
708
793
  };
794
+ function getErrorHint(code) {
795
+ return ERROR_HINTS[code];
796
+ }
797
+ function enrichError(error, options = {}) {
798
+ const hint = getErrorHint(error.code);
799
+ if (hint) {
800
+ if (!error.docs_url) {
801
+ error.docs_url = hint.docs_url;
802
+ }
803
+ if (!error.suggestion) {
804
+ error.suggestion = hint.suggestion;
805
+ }
806
+ } else if (!error.docs_url) {
807
+ error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
808
+ }
809
+ if (options.isTestMode && !error.message.includes("pk_test_")) {
810
+ error.message = `${error.message}
811
+
812
+ \u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
813
+ }
814
+ return error;
815
+ }
709
816
 
710
817
  // src/types/result.ts
711
818
  function ok(value) {
@@ -717,11 +824,11 @@ function err(error) {
717
824
 
718
825
  // src/catalogue.ts
719
826
  function toCimplifyError(error) {
720
- if (error instanceof CimplifyError) return error;
827
+ if (error instanceof CimplifyError) return enrichError(error);
721
828
  if (error instanceof Error) {
722
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
829
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
723
830
  }
724
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
831
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
725
832
  }
726
833
  async function safe(promise) {
727
834
  try {
@@ -1016,11 +1123,11 @@ var CatalogueQueries = class {
1016
1123
 
1017
1124
  // src/cart.ts
1018
1125
  function toCimplifyError2(error) {
1019
- if (error instanceof CimplifyError) return error;
1126
+ if (error instanceof CimplifyError) return enrichError(error);
1020
1127
  if (error instanceof Error) {
1021
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
1128
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
1022
1129
  }
1023
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
1130
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
1024
1131
  }
1025
1132
  async function safe2(promise) {
1026
1133
  try {
@@ -1180,6 +1287,19 @@ var ORDER_MUTATION = {
1180
1287
  UPDATE_CUSTOMER: "order.update_order_customer"
1181
1288
  };
1182
1289
 
1290
+ // src/utils/price.ts
1291
+ function parsePrice(value) {
1292
+ if (value === void 0 || value === null) {
1293
+ return 0;
1294
+ }
1295
+ if (typeof value === "number") {
1296
+ return isNaN(value) ? 0 : value;
1297
+ }
1298
+ const cleaned = value.replace(/[^\d.-]/g, "");
1299
+ const parsed = parseFloat(cleaned);
1300
+ return isNaN(parsed) ? 0 : parsed;
1301
+ }
1302
+
1183
1303
  // src/utils/payment.ts
1184
1304
  var PAYMENT_SUCCESS_STATUSES = /* @__PURE__ */ new Set([
1185
1305
  "success",
@@ -1859,11 +1979,11 @@ var CheckoutResolver = class {
1859
1979
 
1860
1980
  // src/checkout.ts
1861
1981
  function toCimplifyError3(error) {
1862
- if (error instanceof CimplifyError) return error;
1982
+ if (error instanceof CimplifyError) return enrichError(error);
1863
1983
  if (error instanceof Error) {
1864
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
1984
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
1865
1985
  }
1866
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
1986
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
1867
1987
  }
1868
1988
  async function safe3(promise) {
1869
1989
  try {
@@ -2039,11 +2159,11 @@ var CheckoutService = class {
2039
2159
 
2040
2160
  // src/orders.ts
2041
2161
  function toCimplifyError4(error) {
2042
- if (error instanceof CimplifyError) return error;
2162
+ if (error instanceof CimplifyError) return enrichError(error);
2043
2163
  if (error instanceof Error) {
2044
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
2164
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
2045
2165
  }
2046
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
2166
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
2047
2167
  }
2048
2168
  async function safe4(promise) {
2049
2169
  try {
@@ -2300,11 +2420,11 @@ var AuthService = class {
2300
2420
 
2301
2421
  // src/business.ts
2302
2422
  function toCimplifyError7(error) {
2303
- if (error instanceof CimplifyError) return error;
2423
+ if (error instanceof CimplifyError) return enrichError(error);
2304
2424
  if (error instanceof Error) {
2305
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
2425
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
2306
2426
  }
2307
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
2427
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
2308
2428
  }
2309
2429
  async function safe7(promise) {
2310
2430
  try {
@@ -2651,12 +2771,15 @@ var FxService = class {
2651
2771
 
2652
2772
  // src/elements.ts
2653
2773
  function toCheckoutError(code, message, recoverable) {
2774
+ const hint = getErrorHint(code);
2654
2775
  return {
2655
2776
  success: false,
2656
2777
  error: {
2657
2778
  code,
2658
2779
  message,
2659
- recoverable
2780
+ recoverable,
2781
+ docs_url: hint?.docs_url,
2782
+ suggestion: hint?.suggestion
2660
2783
  }
2661
2784
  };
2662
2785
  }
@@ -2697,6 +2820,7 @@ var CimplifyElements = class {
2697
2820
  this.paymentData = null;
2698
2821
  this.checkoutInProgress = false;
2699
2822
  this.activeCheckoutAbort = null;
2823
+ this.hasWarnedMissingAuthElement = false;
2700
2824
  this.businessIdResolvePromise = null;
2701
2825
  this.client = client;
2702
2826
  this.businessId = businessId ?? null;
@@ -2792,6 +2916,12 @@ var CimplifyElements = class {
2792
2916
  );
2793
2917
  }
2794
2918
  const authElement = this.elements.get(ELEMENT_TYPES.AUTH);
2919
+ if (!authElement && !this.hasWarnedMissingAuthElement) {
2920
+ this.hasWarnedMissingAuthElement = true;
2921
+ console.warn(
2922
+ "[Cimplify] processCheckout() called without AuthElement mounted. For best conversion and Link enrollment, mount <AuthElement> before checkout."
2923
+ );
2924
+ }
2795
2925
  if (authElement && !this.accessToken) {
2796
2926
  return toCheckoutError(
2797
2927
  "AUTH_INCOMPLETE",
@@ -3228,28 +3358,37 @@ function isRetryable(error) {
3228
3358
  }
3229
3359
  return false;
3230
3360
  }
3231
- function toNetworkError(error) {
3361
+ function toNetworkError(error, isTestMode) {
3232
3362
  if (error instanceof DOMException && error.name === "AbortError") {
3233
- return new CimplifyError(
3234
- ErrorCode.TIMEOUT,
3235
- "Request timed out. Please check your connection and try again.",
3236
- true
3363
+ return enrichError(
3364
+ new CimplifyError(
3365
+ ErrorCode.TIMEOUT,
3366
+ "Request timed out. Please check your connection and try again.",
3367
+ true
3368
+ ),
3369
+ { isTestMode }
3237
3370
  );
3238
3371
  }
3239
3372
  if (error instanceof TypeError && error.message.includes("fetch")) {
3240
- return new CimplifyError(
3241
- ErrorCode.NETWORK_ERROR,
3242
- "Network error. Please check your internet connection.",
3243
- true
3373
+ return enrichError(
3374
+ new CimplifyError(
3375
+ ErrorCode.NETWORK_ERROR,
3376
+ "Network error. Please check your internet connection.",
3377
+ true
3378
+ ),
3379
+ { isTestMode }
3244
3380
  );
3245
3381
  }
3246
3382
  if (error instanceof CimplifyError) {
3247
- return error;
3383
+ return enrichError(error, { isTestMode });
3248
3384
  }
3249
- return new CimplifyError(
3250
- ErrorCode.UNKNOWN_ERROR,
3251
- error instanceof Error ? error.message : "An unknown error occurred",
3252
- false
3385
+ return enrichError(
3386
+ new CimplifyError(
3387
+ ErrorCode.UNKNOWN_ERROR,
3388
+ error instanceof Error ? error.message : "An unknown error occurred",
3389
+ false
3390
+ ),
3391
+ { isTestMode }
3253
3392
  );
3254
3393
  }
3255
3394
  function deriveUrls() {
@@ -3311,6 +3450,9 @@ var CimplifyClient = class {
3311
3450
  getPublicKey() {
3312
3451
  return this.publicKey;
3313
3452
  }
3453
+ isTestMode() {
3454
+ return this.publicKey.trim().startsWith("pk_test_");
3455
+ }
3314
3456
  setAccessToken(token) {
3315
3457
  const previous = this.accessToken;
3316
3458
  this.accessToken = token;
@@ -3460,7 +3602,7 @@ var CimplifyClient = class {
3460
3602
  return response;
3461
3603
  } catch (error) {
3462
3604
  lastError = error;
3463
- const networkError = toNetworkError(error);
3605
+ const networkError = toNetworkError(error, this.isTestMode());
3464
3606
  const errorRetryable = isRetryable(error);
3465
3607
  if (!errorRetryable || attempt >= this.maxRetries) {
3466
3608
  this.hooks.onRequestError?.({
@@ -3483,7 +3625,7 @@ var CimplifyClient = class {
3483
3625
  await sleep(delay);
3484
3626
  }
3485
3627
  }
3486
- const finalError = toNetworkError(lastError);
3628
+ const finalError = toNetworkError(lastError, this.isTestMode());
3487
3629
  this.hooks.onRequestError?.({
3488
3630
  ...context,
3489
3631
  error: finalError,
@@ -3601,22 +3743,40 @@ var CimplifyClient = class {
3601
3743
  async handleRestResponse(response) {
3602
3744
  const json = await response.json();
3603
3745
  if (!response.ok) {
3604
- throw new CimplifyError(
3605
- json.error?.error_code || "API_ERROR",
3606
- json.error?.error_message || "An error occurred",
3607
- false
3746
+ const error = enrichError(
3747
+ new CimplifyError(
3748
+ json.error?.error_code || "API_ERROR",
3749
+ json.error?.error_message || "An error occurred",
3750
+ false
3751
+ ),
3752
+ { isTestMode: this.isTestMode() }
3608
3753
  );
3754
+ if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
3755
+ console.warn(
3756
+ "[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
3757
+ );
3758
+ }
3759
+ throw error;
3609
3760
  }
3610
3761
  return json.data;
3611
3762
  }
3612
3763
  async handleResponse(response) {
3613
3764
  const json = await response.json();
3614
3765
  if (!json.success || json.error) {
3615
- throw new CimplifyError(
3616
- json.error?.code || "UNKNOWN_ERROR",
3617
- json.error?.message || "An unknown error occurred",
3618
- json.error?.retryable || false
3766
+ const error = enrichError(
3767
+ new CimplifyError(
3768
+ json.error?.code || "UNKNOWN_ERROR",
3769
+ json.error?.message || "An unknown error occurred",
3770
+ json.error?.retryable || false
3771
+ ),
3772
+ { isTestMode: this.isTestMode() }
3619
3773
  );
3774
+ if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
3775
+ console.warn(
3776
+ "[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
3777
+ );
3778
+ }
3779
+ throw error;
3620
3780
  }
3621
3781
  return json.data;
3622
3782
  }
@@ -3803,6 +3963,9 @@ function CimplifyProvider({
3803
3963
  const nextLocations = locationsResult.ok && Array.isArray(locationsResult.value) ? locationsResult.value : [];
3804
3964
  const initialLocation = resolveInitialLocation(nextLocations);
3805
3965
  setBusiness(nextBusiness);
3966
+ if (nextBusiness?.id) {
3967
+ resolvedClient.setBusinessId(nextBusiness.id);
3968
+ }
3806
3969
  setLocations(nextLocations);
3807
3970
  if (initialLocation) {
3808
3971
  setCurrentLocationState(initialLocation);
@@ -3861,6 +4024,1065 @@ function useCimplify() {
3861
4024
  }
3862
4025
  return context;
3863
4026
  }
4027
+ function useOptionalCimplify() {
4028
+ return useContext(CimplifyContext);
4029
+ }
4030
+ var productsCache = /* @__PURE__ */ new Map();
4031
+ var productsInflight = /* @__PURE__ */ new Map();
4032
+ function buildProductsCacheKey(client, locationId, options) {
4033
+ return JSON.stringify({
4034
+ key: client.getPublicKey(),
4035
+ location_id: locationId || "__none__",
4036
+ options
4037
+ });
4038
+ }
4039
+ function useProducts(options = {}) {
4040
+ const context = useOptionalCimplify();
4041
+ const client = options.client ?? context?.client;
4042
+ if (!client) {
4043
+ throw new Error("useProducts must be used within CimplifyProvider or passed { client }.");
4044
+ }
4045
+ const enabled = options.enabled ?? true;
4046
+ const locationId = client.getLocationId();
4047
+ const previousLocationIdRef = useRef(locationId);
4048
+ const requestIdRef = useRef(0);
4049
+ const queryOptions = useMemo(
4050
+ () => ({
4051
+ category: options.category,
4052
+ collection: options.collection,
4053
+ search: options.search,
4054
+ featured: options.featured,
4055
+ limit: options.limit
4056
+ }),
4057
+ [options.category, options.collection, options.featured, options.limit, options.search]
4058
+ );
4059
+ const cacheKey = useMemo(
4060
+ () => buildProductsCacheKey(client, locationId, queryOptions),
4061
+ [client, locationId, queryOptions]
4062
+ );
4063
+ const cached = productsCache.get(cacheKey);
4064
+ const [products, setProducts] = useState(cached?.products ?? []);
4065
+ const [isLoading, setIsLoading] = useState(enabled && !cached);
4066
+ const [error, setError] = useState(null);
4067
+ useEffect(() => {
4068
+ if (previousLocationIdRef.current !== locationId) {
4069
+ productsCache.clear();
4070
+ productsInflight.clear();
4071
+ previousLocationIdRef.current = locationId;
4072
+ }
4073
+ }, [locationId]);
4074
+ const load = useCallback(
4075
+ async (force = false) => {
4076
+ if (!enabled) {
4077
+ setIsLoading(false);
4078
+ return;
4079
+ }
4080
+ const nextRequestId = ++requestIdRef.current;
4081
+ setError(null);
4082
+ if (!force) {
4083
+ const cacheEntry = productsCache.get(cacheKey);
4084
+ if (cacheEntry) {
4085
+ setProducts(cacheEntry.products);
4086
+ setIsLoading(false);
4087
+ return;
4088
+ }
4089
+ }
4090
+ setIsLoading(true);
4091
+ try {
4092
+ const existing = productsInflight.get(cacheKey);
4093
+ const promise = existing ?? (async () => {
4094
+ const result = await client.catalogue.getProducts(queryOptions);
4095
+ if (!result.ok) {
4096
+ throw result.error;
4097
+ }
4098
+ return result.value;
4099
+ })();
4100
+ if (!existing) {
4101
+ productsInflight.set(
4102
+ cacheKey,
4103
+ promise.finally(() => {
4104
+ productsInflight.delete(cacheKey);
4105
+ })
4106
+ );
4107
+ }
4108
+ const value = await promise;
4109
+ productsCache.set(cacheKey, { products: value });
4110
+ if (nextRequestId === requestIdRef.current) {
4111
+ setProducts(value);
4112
+ setError(null);
4113
+ }
4114
+ } catch (loadError) {
4115
+ if (nextRequestId === requestIdRef.current) {
4116
+ setError(loadError);
4117
+ }
4118
+ } finally {
4119
+ if (nextRequestId === requestIdRef.current) {
4120
+ setIsLoading(false);
4121
+ }
4122
+ }
4123
+ },
4124
+ [cacheKey, client, enabled, queryOptions]
4125
+ );
4126
+ useEffect(() => {
4127
+ void load(false);
4128
+ }, [load]);
4129
+ const refetch = useCallback(async () => {
4130
+ productsCache.delete(cacheKey);
4131
+ await load(true);
4132
+ }, [cacheKey, load]);
4133
+ return { products, isLoading, error, refetch };
4134
+ }
4135
+ var productCache = /* @__PURE__ */ new Map();
4136
+ var productInflight = /* @__PURE__ */ new Map();
4137
+ function isLikelySlug(value) {
4138
+ return /^[a-z0-9-]+$/.test(value);
4139
+ }
4140
+ function buildProductCacheKey(client, locationId, slugOrId) {
4141
+ return JSON.stringify({
4142
+ key: client.getPublicKey(),
4143
+ location_id: locationId || "__none__",
4144
+ slug_or_id: slugOrId
4145
+ });
4146
+ }
4147
+ function useProduct(slugOrId, options = {}) {
4148
+ const context = useOptionalCimplify();
4149
+ const client = options.client ?? context?.client;
4150
+ if (!client) {
4151
+ throw new Error("useProduct must be used within CimplifyProvider or passed { client }.");
4152
+ }
4153
+ const enabled = options.enabled ?? true;
4154
+ const locationId = client.getLocationId();
4155
+ const previousLocationIdRef = useRef(locationId);
4156
+ const requestIdRef = useRef(0);
4157
+ const normalizedSlugOrId = useMemo(() => (slugOrId || "").trim(), [slugOrId]);
4158
+ const cacheKey = useMemo(
4159
+ () => buildProductCacheKey(client, locationId, normalizedSlugOrId),
4160
+ [client, locationId, normalizedSlugOrId]
4161
+ );
4162
+ const cached = productCache.get(cacheKey);
4163
+ const [product, setProduct] = useState(cached?.product ?? null);
4164
+ const [isLoading, setIsLoading] = useState(
4165
+ enabled && normalizedSlugOrId.length > 0 && !cached
4166
+ );
4167
+ const [error, setError] = useState(null);
4168
+ useEffect(() => {
4169
+ if (previousLocationIdRef.current !== locationId) {
4170
+ productCache.clear();
4171
+ productInflight.clear();
4172
+ previousLocationIdRef.current = locationId;
4173
+ }
4174
+ }, [locationId]);
4175
+ const load = useCallback(
4176
+ async (force = false) => {
4177
+ if (!enabled || normalizedSlugOrId.length === 0) {
4178
+ setProduct(null);
4179
+ setIsLoading(false);
4180
+ return;
4181
+ }
4182
+ const nextRequestId = ++requestIdRef.current;
4183
+ setError(null);
4184
+ if (!force) {
4185
+ const cacheEntry = productCache.get(cacheKey);
4186
+ if (cacheEntry) {
4187
+ setProduct(cacheEntry.product);
4188
+ setIsLoading(false);
4189
+ return;
4190
+ }
4191
+ }
4192
+ setIsLoading(true);
4193
+ try {
4194
+ const existing = productInflight.get(cacheKey);
4195
+ const promise = existing ?? (async () => {
4196
+ const result = isLikelySlug(normalizedSlugOrId) ? await client.catalogue.getProductBySlug(normalizedSlugOrId) : await client.catalogue.getProduct(normalizedSlugOrId);
4197
+ if (!result.ok) {
4198
+ throw result.error;
4199
+ }
4200
+ return result.value;
4201
+ })();
4202
+ if (!existing) {
4203
+ productInflight.set(
4204
+ cacheKey,
4205
+ promise.finally(() => {
4206
+ productInflight.delete(cacheKey);
4207
+ })
4208
+ );
4209
+ }
4210
+ const value = await promise;
4211
+ productCache.set(cacheKey, { product: value });
4212
+ if (nextRequestId === requestIdRef.current) {
4213
+ setProduct(value);
4214
+ setError(null);
4215
+ }
4216
+ } catch (loadError) {
4217
+ if (nextRequestId === requestIdRef.current) {
4218
+ setError(loadError);
4219
+ }
4220
+ } finally {
4221
+ if (nextRequestId === requestIdRef.current) {
4222
+ setIsLoading(false);
4223
+ }
4224
+ }
4225
+ },
4226
+ [cacheKey, client, enabled, normalizedSlugOrId]
4227
+ );
4228
+ useEffect(() => {
4229
+ void load(false);
4230
+ }, [load]);
4231
+ const refetch = useCallback(async () => {
4232
+ productCache.delete(cacheKey);
4233
+ await load(true);
4234
+ }, [cacheKey, load]);
4235
+ return { product, isLoading, error, refetch };
4236
+ }
4237
+ var categoriesCache = /* @__PURE__ */ new Map();
4238
+ var categoriesInflight = /* @__PURE__ */ new Map();
4239
+ function buildCategoriesCacheKey(client) {
4240
+ return client.getPublicKey() || "__demo__";
4241
+ }
4242
+ function useCategories(options = {}) {
4243
+ const context = useOptionalCimplify();
4244
+ const client = options.client ?? context?.client;
4245
+ if (!client) {
4246
+ throw new Error("useCategories must be used within CimplifyProvider or passed { client }.");
4247
+ }
4248
+ const enabled = options.enabled ?? true;
4249
+ const cacheKey = useMemo(() => buildCategoriesCacheKey(client), [client]);
4250
+ const cached = categoriesCache.get(cacheKey);
4251
+ const [categories, setCategories] = useState(cached ?? []);
4252
+ const [isLoading, setIsLoading] = useState(enabled && !cached);
4253
+ const [error, setError] = useState(null);
4254
+ const load = useCallback(
4255
+ async (force = false) => {
4256
+ if (!enabled) {
4257
+ setIsLoading(false);
4258
+ return;
4259
+ }
4260
+ setError(null);
4261
+ if (!force) {
4262
+ const cachedCategories = categoriesCache.get(cacheKey);
4263
+ if (cachedCategories) {
4264
+ setCategories(cachedCategories);
4265
+ setIsLoading(false);
4266
+ return;
4267
+ }
4268
+ }
4269
+ setIsLoading(true);
4270
+ try {
4271
+ const existing = categoriesInflight.get(cacheKey);
4272
+ const promise = existing ?? (async () => {
4273
+ const result = await client.catalogue.getCategories();
4274
+ if (!result.ok) {
4275
+ throw result.error;
4276
+ }
4277
+ return result.value;
4278
+ })();
4279
+ if (!existing) {
4280
+ categoriesInflight.set(
4281
+ cacheKey,
4282
+ promise.finally(() => {
4283
+ categoriesInflight.delete(cacheKey);
4284
+ })
4285
+ );
4286
+ }
4287
+ const value = await promise;
4288
+ categoriesCache.set(cacheKey, value);
4289
+ setCategories(value);
4290
+ } catch (loadError) {
4291
+ setError(loadError);
4292
+ } finally {
4293
+ setIsLoading(false);
4294
+ }
4295
+ },
4296
+ [cacheKey, client, enabled]
4297
+ );
4298
+ useEffect(() => {
4299
+ void load(false);
4300
+ }, [load]);
4301
+ const refetch = useCallback(async () => {
4302
+ categoriesCache.delete(cacheKey);
4303
+ await load(true);
4304
+ }, [cacheKey, load]);
4305
+ return { categories, isLoading, error, refetch };
4306
+ }
4307
+ var CART_STORAGE_PREFIX = "cimplify:cart:v2";
4308
+ var DEFAULT_TAX_RATE = 0.08875;
4309
+ var cartStores = /* @__PURE__ */ new Map();
4310
+ function toNumber(value) {
4311
+ if (typeof value === "number" && Number.isFinite(value)) {
4312
+ return value;
4313
+ }
4314
+ if (typeof value === "string") {
4315
+ const parsed = Number.parseFloat(value);
4316
+ return Number.isFinite(parsed) ? parsed : 0;
4317
+ }
4318
+ return 0;
4319
+ }
4320
+ function roundMoney(value) {
4321
+ return Math.max(0, Number(value.toFixed(2)));
4322
+ }
4323
+ function clampQuantity(quantity) {
4324
+ if (!Number.isFinite(quantity)) {
4325
+ return 1;
4326
+ }
4327
+ return Math.max(1, Math.floor(quantity));
4328
+ }
4329
+ function normalizeOptionIds(value) {
4330
+ if (!value || value.length === 0) {
4331
+ return void 0;
4332
+ }
4333
+ const normalized = Array.from(
4334
+ new Set(
4335
+ value.map((entry) => entry.trim()).filter((entry) => entry.length > 0)
4336
+ )
4337
+ ).sort();
4338
+ return normalized.length > 0 ? normalized : void 0;
4339
+ }
4340
+ function buildLineKey(productId, options) {
4341
+ const parts = [productId];
4342
+ if (options.locationId) {
4343
+ parts.push(`l:${options.locationId}`);
4344
+ }
4345
+ if (options.variantId) {
4346
+ parts.push(`v:${options.variantId}`);
4347
+ }
4348
+ const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
4349
+ if (addOnOptionIds && addOnOptionIds.length > 0) {
4350
+ parts.push(`a:${addOnOptionIds.join(",")}`);
4351
+ }
4352
+ if (options.bundleSelections && options.bundleSelections.length > 0) {
4353
+ parts.push(`b:${JSON.stringify(options.bundleSelections)}`);
4354
+ }
4355
+ if (options.compositeSelections && options.compositeSelections.length > 0) {
4356
+ parts.push(`c:${JSON.stringify(options.compositeSelections)}`);
4357
+ }
4358
+ if (options.quoteId) {
4359
+ parts.push(`q:${options.quoteId}`);
4360
+ }
4361
+ return parts.join("|");
4362
+ }
4363
+ function calculateLineSubtotal(item) {
4364
+ let unitPrice = parsePrice(item.product.default_price);
4365
+ if (item.variant?.price_adjustment) {
4366
+ unitPrice += parsePrice(item.variant.price_adjustment);
4367
+ }
4368
+ for (const option of item.addOnOptions || []) {
4369
+ if (option.default_price) {
4370
+ unitPrice += parsePrice(option.default_price);
4371
+ }
4372
+ }
4373
+ return roundMoney(unitPrice * item.quantity);
4374
+ }
4375
+ function calculateSummary(items) {
4376
+ const subtotal = roundMoney(items.reduce((sum, item) => sum + calculateLineSubtotal(item), 0));
4377
+ const tax = roundMoney(subtotal * DEFAULT_TAX_RATE);
4378
+ return {
4379
+ subtotal,
4380
+ tax,
4381
+ total: roundMoney(subtotal + tax)
4382
+ };
4383
+ }
4384
+ function toProductFromServerItem(item, businessId) {
4385
+ return {
4386
+ id: item.item_id,
4387
+ business_id: businessId,
4388
+ category_id: item.category_id,
4389
+ name: item.name,
4390
+ slug: item.item_id,
4391
+ description: item.description,
4392
+ image_url: item.image_url,
4393
+ default_price: item.base_price,
4394
+ product_type: "product",
4395
+ inventory_type: "none",
4396
+ variant_strategy: "fetch_all",
4397
+ is_active: item.is_available,
4398
+ created_at: item.created_at,
4399
+ updated_at: item.updated_at,
4400
+ metadata: {
4401
+ from_sdk: true
4402
+ }
4403
+ };
4404
+ }
4405
+ function mapServerCart(serverCart) {
4406
+ const items = serverCart.items.map((item) => {
4407
+ const variant = item.variant_info || item.variant_name ? {
4408
+ id: item.variant_info?.id || item.variant_id || "",
4409
+ name: item.variant_info?.name || item.variant_name || "Variant",
4410
+ price_adjustment: item.variant_info?.price_adjustment
4411
+ } : void 0;
4412
+ const quoteId = typeof item.metadata?.quote_id === "string" ? item.metadata.quote_id : void 0;
4413
+ return {
4414
+ id: item.id,
4415
+ product: toProductFromServerItem(item, serverCart.business_id),
4416
+ quantity: clampQuantity(item.quantity),
4417
+ locationId: serverCart.location_id,
4418
+ variantId: item.variant_id,
4419
+ variant,
4420
+ quoteId,
4421
+ addOnOptionIds: item.add_on_option_ids || void 0,
4422
+ addOnOptions: (item.add_on_options || []).map((option) => ({
4423
+ id: option.id,
4424
+ name: option.name,
4425
+ default_price: option.price
4426
+ })),
4427
+ bundleSelections: item.bundle_selections || void 0,
4428
+ compositeSelections: item.composite_selections || void 0,
4429
+ specialInstructions: item.special_instructions
4430
+ };
4431
+ });
4432
+ return {
4433
+ items,
4434
+ subtotal: roundMoney(toNumber(serverCart.pricing.subtotal)),
4435
+ tax: roundMoney(toNumber(serverCart.pricing.tax_amount)),
4436
+ total: roundMoney(toNumber(serverCart.pricing.total_price)),
4437
+ currency: serverCart.pricing.currency || "USD"
4438
+ };
4439
+ }
4440
+ function buildStorageKey(storeKey) {
4441
+ return `${CART_STORAGE_PREFIX}:${storeKey}`;
4442
+ }
4443
+ function createEmptySnapshot(currency) {
4444
+ return {
4445
+ items: [],
4446
+ subtotal: 0,
4447
+ tax: 0,
4448
+ total: 0,
4449
+ currency,
4450
+ isLoading: true
4451
+ };
4452
+ }
4453
+ function createCartStore(params) {
4454
+ const { client, storeKey, locationId, isDemoMode, currency } = params;
4455
+ const storageKey = buildStorageKey(storeKey);
4456
+ const listeners = /* @__PURE__ */ new Set();
4457
+ let snapshot = createEmptySnapshot(currency);
4458
+ let initialized = false;
4459
+ let initializePromise = null;
4460
+ function emit() {
4461
+ listeners.forEach((listener) => {
4462
+ listener();
4463
+ });
4464
+ }
4465
+ function setSnapshot(nextSnapshot) {
4466
+ snapshot = nextSnapshot;
4467
+ persistSnapshot();
4468
+ emit();
4469
+ }
4470
+ function updateSnapshot(updater) {
4471
+ setSnapshot(updater(snapshot));
4472
+ }
4473
+ function persistSnapshot() {
4474
+ if (typeof window === "undefined" || !window.localStorage) {
4475
+ return;
4476
+ }
4477
+ try {
4478
+ window.localStorage.setItem(
4479
+ storageKey,
4480
+ JSON.stringify({
4481
+ items: snapshot.items,
4482
+ subtotal: snapshot.subtotal,
4483
+ tax: snapshot.tax,
4484
+ total: snapshot.total,
4485
+ currency: snapshot.currency
4486
+ })
4487
+ );
4488
+ } catch {
4489
+ }
4490
+ }
4491
+ function hydrateFromStorage() {
4492
+ if (typeof window === "undefined" || !window.localStorage) {
4493
+ return;
4494
+ }
4495
+ try {
4496
+ const raw = window.localStorage.getItem(storageKey);
4497
+ if (!raw) {
4498
+ return;
4499
+ }
4500
+ const parsed = JSON.parse(raw);
4501
+ if (!parsed || !Array.isArray(parsed.items)) {
4502
+ return;
4503
+ }
4504
+ snapshot = {
4505
+ items: parsed.items,
4506
+ subtotal: toNumber(parsed.subtotal),
4507
+ tax: toNumber(parsed.tax),
4508
+ total: toNumber(parsed.total),
4509
+ currency: typeof parsed.currency === "string" && parsed.currency ? parsed.currency : currency,
4510
+ isLoading: !isDemoMode
4511
+ };
4512
+ emit();
4513
+ } catch {
4514
+ }
4515
+ }
4516
+ async function sync() {
4517
+ if (isDemoMode) {
4518
+ updateSnapshot((current) => ({
4519
+ ...current,
4520
+ isLoading: false
4521
+ }));
4522
+ return;
4523
+ }
4524
+ updateSnapshot((current) => ({
4525
+ ...current,
4526
+ isLoading: true
4527
+ }));
4528
+ const result = await client.cart.get();
4529
+ if (!result.ok) {
4530
+ updateSnapshot((current) => ({
4531
+ ...current,
4532
+ isLoading: false
4533
+ }));
4534
+ throw result.error;
4535
+ }
4536
+ const next = mapServerCart(result.value);
4537
+ setSnapshot({
4538
+ ...next,
4539
+ isLoading: false
4540
+ });
4541
+ }
4542
+ async function maybeResolveQuoteId(product, quantity, options) {
4543
+ if (options.quoteId) {
4544
+ return options.quoteId;
4545
+ }
4546
+ const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
4547
+ const requiresQuote = Boolean(
4548
+ options.variantId || addOnOptionIds && addOnOptionIds.length > 0 || options.bundleSelections && options.bundleSelections.length > 0 || options.compositeSelections && options.compositeSelections.length > 0
4549
+ );
4550
+ if (!requiresQuote || isDemoMode) {
4551
+ return void 0;
4552
+ }
4553
+ const quoteResult = await client.catalogue.fetchQuote({
4554
+ product_id: product.id,
4555
+ quantity,
4556
+ location_id: options.locationId || locationId || void 0,
4557
+ variant_id: options.variantId,
4558
+ add_on_option_ids: addOnOptionIds,
4559
+ bundle_selections: options.bundleSelections,
4560
+ composite_selections: options.compositeSelections
4561
+ });
4562
+ if (!quoteResult.ok) {
4563
+ throw quoteResult.error;
4564
+ }
4565
+ return quoteResult.value.quote_id;
4566
+ }
4567
+ async function addItem(product, quantity = 1, options = {}) {
4568
+ const resolvedQuantity = clampQuantity(quantity);
4569
+ const normalizedOptions = {
4570
+ ...options,
4571
+ locationId: options.locationId || locationId || void 0,
4572
+ addOnOptionIds: normalizeOptionIds(options.addOnOptionIds)
4573
+ };
4574
+ const maybeProductWithVariants = product;
4575
+ if (Array.isArray(maybeProductWithVariants.variants) && maybeProductWithVariants.variants.length > 0 && !normalizedOptions.variantId) {
4576
+ console.warn(
4577
+ "[Cimplify] addItem() called without variantId for a product that has variants. Default variant pricing will be used."
4578
+ );
4579
+ }
4580
+ const previousSnapshot = snapshot;
4581
+ const lineKey = buildLineKey(product.id, normalizedOptions);
4582
+ const optimisticItem = {
4583
+ id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
4584
+ product,
4585
+ quantity: resolvedQuantity,
4586
+ locationId: normalizedOptions.locationId,
4587
+ variantId: normalizedOptions.variantId,
4588
+ variant: normalizedOptions.variant,
4589
+ quoteId: normalizedOptions.quoteId,
4590
+ addOnOptionIds: normalizedOptions.addOnOptionIds,
4591
+ addOnOptions: normalizedOptions.addOnOptions,
4592
+ bundleSelections: normalizedOptions.bundleSelections,
4593
+ compositeSelections: normalizedOptions.compositeSelections,
4594
+ specialInstructions: normalizedOptions.specialInstructions
4595
+ };
4596
+ updateSnapshot((current) => {
4597
+ const existingIndex = current.items.findIndex((item) => {
4598
+ const itemKey = buildLineKey(item.product.id, {
4599
+ locationId: item.locationId,
4600
+ variantId: item.variantId,
4601
+ quoteId: item.quoteId,
4602
+ addOnOptionIds: item.addOnOptionIds,
4603
+ bundleSelections: item.bundleSelections,
4604
+ compositeSelections: item.compositeSelections
4605
+ });
4606
+ return itemKey === lineKey;
4607
+ });
4608
+ const nextItems = [...current.items];
4609
+ if (existingIndex >= 0) {
4610
+ const existing = nextItems[existingIndex];
4611
+ nextItems[existingIndex] = {
4612
+ ...existing,
4613
+ quantity: existing.quantity + resolvedQuantity
4614
+ };
4615
+ } else {
4616
+ nextItems.push(optimisticItem);
4617
+ }
4618
+ const summary = calculateSummary(nextItems);
4619
+ return {
4620
+ ...current,
4621
+ items: nextItems,
4622
+ subtotal: summary.subtotal,
4623
+ tax: summary.tax,
4624
+ total: summary.total
4625
+ };
4626
+ });
4627
+ if (isDemoMode) {
4628
+ return;
4629
+ }
4630
+ try {
4631
+ const quoteId = await maybeResolveQuoteId(product, resolvedQuantity, normalizedOptions);
4632
+ const result = await client.cart.addItem({
4633
+ item_id: product.id,
4634
+ quantity: resolvedQuantity,
4635
+ variant_id: normalizedOptions.variantId,
4636
+ quote_id: quoteId,
4637
+ add_on_options: normalizedOptions.addOnOptionIds,
4638
+ special_instructions: normalizedOptions.specialInstructions,
4639
+ bundle_selections: normalizedOptions.bundleSelections,
4640
+ composite_selections: normalizedOptions.compositeSelections
4641
+ });
4642
+ if (!result.ok) {
4643
+ throw result.error;
4644
+ }
4645
+ await sync();
4646
+ } catch (error) {
4647
+ snapshot = previousSnapshot;
4648
+ persistSnapshot();
4649
+ emit();
4650
+ throw error;
4651
+ }
4652
+ }
4653
+ async function removeItem(itemId) {
4654
+ const previousSnapshot = snapshot;
4655
+ updateSnapshot((current) => {
4656
+ const nextItems = current.items.filter((item) => item.id !== itemId);
4657
+ const summary = calculateSummary(nextItems);
4658
+ return {
4659
+ ...current,
4660
+ items: nextItems,
4661
+ subtotal: summary.subtotal,
4662
+ tax: summary.tax,
4663
+ total: summary.total
4664
+ };
4665
+ });
4666
+ if (isDemoMode) {
4667
+ return;
4668
+ }
4669
+ try {
4670
+ const result = await client.cart.removeItem(itemId);
4671
+ if (!result.ok) {
4672
+ throw result.error;
4673
+ }
4674
+ await sync();
4675
+ } catch (error) {
4676
+ snapshot = previousSnapshot;
4677
+ persistSnapshot();
4678
+ emit();
4679
+ throw error;
4680
+ }
4681
+ }
4682
+ async function updateQuantity(itemId, quantity) {
4683
+ if (quantity <= 0) {
4684
+ await removeItem(itemId);
4685
+ return;
4686
+ }
4687
+ const resolvedQuantity = clampQuantity(quantity);
4688
+ const previousSnapshot = snapshot;
4689
+ updateSnapshot((current) => {
4690
+ const nextItems = current.items.map(
4691
+ (item) => item.id === itemId ? {
4692
+ ...item,
4693
+ quantity: resolvedQuantity
4694
+ } : item
4695
+ );
4696
+ const summary = calculateSummary(nextItems);
4697
+ return {
4698
+ ...current,
4699
+ items: nextItems,
4700
+ subtotal: summary.subtotal,
4701
+ tax: summary.tax,
4702
+ total: summary.total
4703
+ };
4704
+ });
4705
+ if (isDemoMode) {
4706
+ return;
4707
+ }
4708
+ try {
4709
+ const result = await client.cart.updateQuantity(itemId, resolvedQuantity);
4710
+ if (!result.ok) {
4711
+ throw result.error;
4712
+ }
4713
+ await sync();
4714
+ } catch (error) {
4715
+ snapshot = previousSnapshot;
4716
+ persistSnapshot();
4717
+ emit();
4718
+ throw error;
4719
+ }
4720
+ }
4721
+ async function clearCart() {
4722
+ const previousSnapshot = snapshot;
4723
+ updateSnapshot((current) => ({
4724
+ ...current,
4725
+ items: [],
4726
+ subtotal: 0,
4727
+ tax: 0,
4728
+ total: 0
4729
+ }));
4730
+ if (isDemoMode) {
4731
+ return;
4732
+ }
4733
+ try {
4734
+ const result = await client.cart.clear();
4735
+ if (!result.ok) {
4736
+ throw result.error;
4737
+ }
4738
+ await sync();
4739
+ } catch (error) {
4740
+ snapshot = previousSnapshot;
4741
+ persistSnapshot();
4742
+ emit();
4743
+ throw error;
4744
+ }
4745
+ }
4746
+ async function initialize() {
4747
+ if (initialized) {
4748
+ if (initializePromise) {
4749
+ await initializePromise;
4750
+ }
4751
+ return;
4752
+ }
4753
+ initialized = true;
4754
+ hydrateFromStorage();
4755
+ if (isDemoMode) {
4756
+ updateSnapshot((current) => ({
4757
+ ...current,
4758
+ isLoading: false
4759
+ }));
4760
+ return;
4761
+ }
4762
+ initializePromise = sync().catch(() => {
4763
+ updateSnapshot((current) => ({
4764
+ ...current,
4765
+ isLoading: false
4766
+ }));
4767
+ });
4768
+ await initializePromise;
4769
+ initializePromise = null;
4770
+ }
4771
+ return {
4772
+ subscribe(listener) {
4773
+ listeners.add(listener);
4774
+ return () => {
4775
+ listeners.delete(listener);
4776
+ };
4777
+ },
4778
+ getSnapshot() {
4779
+ return snapshot;
4780
+ },
4781
+ initialize,
4782
+ addItem,
4783
+ removeItem,
4784
+ updateQuantity,
4785
+ clearCart,
4786
+ sync
4787
+ };
4788
+ }
4789
+ function getStoreKey(client, locationId, isDemoMode) {
4790
+ return [
4791
+ client.getPublicKey() || "__demo__",
4792
+ locationId || "__no_location__",
4793
+ isDemoMode ? "demo" : "live"
4794
+ ].join(":");
4795
+ }
4796
+ function getOrCreateStore(params) {
4797
+ const { client, locationId, isDemoMode, currency } = params;
4798
+ const storeKey = getStoreKey(client, locationId, isDemoMode);
4799
+ const existing = cartStores.get(storeKey);
4800
+ if (existing) {
4801
+ return existing;
4802
+ }
4803
+ const created = createCartStore({
4804
+ client,
4805
+ storeKey,
4806
+ locationId,
4807
+ isDemoMode,
4808
+ currency
4809
+ });
4810
+ cartStores.set(storeKey, created);
4811
+ return created;
4812
+ }
4813
+ function useCart(options = {}) {
4814
+ const context = useOptionalCimplify();
4815
+ const client = options.client ?? context?.client;
4816
+ if (!client) {
4817
+ throw new Error("useCart must be used within CimplifyProvider or passed { client }.");
4818
+ }
4819
+ const locationId = options.locationId ?? client.getLocationId();
4820
+ const isDemoMode = options.demoMode ?? context?.isDemoMode ?? client.getPublicKey().trim().length === 0;
4821
+ const currency = options.currency ?? context?.currency ?? "USD";
4822
+ const store = useMemo(
4823
+ () => getOrCreateStore({
4824
+ client,
4825
+ locationId,
4826
+ isDemoMode,
4827
+ currency
4828
+ }),
4829
+ [client, currency, isDemoMode, locationId]
4830
+ );
4831
+ const snapshot = useSyncExternalStore(
4832
+ store.subscribe,
4833
+ store.getSnapshot,
4834
+ store.getSnapshot
4835
+ );
4836
+ useEffect(() => {
4837
+ void store.initialize();
4838
+ }, [store]);
4839
+ const addItem = useCallback(
4840
+ async (product, quantity, addOptions) => {
4841
+ await store.addItem(product, quantity, addOptions);
4842
+ },
4843
+ [store]
4844
+ );
4845
+ const removeItem = useCallback(
4846
+ async (itemId) => {
4847
+ await store.removeItem(itemId);
4848
+ },
4849
+ [store]
4850
+ );
4851
+ const updateQuantity = useCallback(
4852
+ async (itemId, quantity) => {
4853
+ await store.updateQuantity(itemId, quantity);
4854
+ },
4855
+ [store]
4856
+ );
4857
+ const clearCart = useCallback(async () => {
4858
+ await store.clearCart();
4859
+ }, [store]);
4860
+ const sync = useCallback(async () => {
4861
+ try {
4862
+ await store.sync();
4863
+ } catch (syncError) {
4864
+ throw syncError;
4865
+ }
4866
+ }, [store]);
4867
+ const itemCount = useMemo(
4868
+ () => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
4869
+ [snapshot.items]
4870
+ );
4871
+ return {
4872
+ items: snapshot.items,
4873
+ itemCount,
4874
+ subtotal: snapshot.subtotal,
4875
+ tax: snapshot.tax,
4876
+ total: snapshot.total,
4877
+ currency: snapshot.currency,
4878
+ isEmpty: itemCount === 0,
4879
+ isLoading: snapshot.isLoading,
4880
+ addItem,
4881
+ removeItem,
4882
+ updateQuantity,
4883
+ clearCart,
4884
+ sync
4885
+ };
4886
+ }
4887
+ var orderCache = /* @__PURE__ */ new Map();
4888
+ var orderInflight = /* @__PURE__ */ new Map();
4889
+ function buildOrderCacheKey(client, orderId) {
4890
+ return `${client.getPublicKey() || "__demo__"}:${orderId}`;
4891
+ }
4892
+ function useOrder(orderId, options = {}) {
4893
+ const context = useOptionalCimplify();
4894
+ const client = options.client ?? context?.client;
4895
+ if (!client) {
4896
+ throw new Error("useOrder must be used within CimplifyProvider or passed { client }.");
4897
+ }
4898
+ const normalizedOrderId = useMemo(() => (orderId || "").trim(), [orderId]);
4899
+ const enabled = options.enabled ?? true;
4900
+ const poll = options.poll ?? false;
4901
+ const pollInterval = options.pollInterval ?? 5e3;
4902
+ const requestIdRef = useRef(0);
4903
+ const cacheKey = useMemo(
4904
+ () => buildOrderCacheKey(client, normalizedOrderId),
4905
+ [client, normalizedOrderId]
4906
+ );
4907
+ const cached = orderCache.get(cacheKey);
4908
+ const [order, setOrder] = useState(cached?.order ?? null);
4909
+ const [isLoading, setIsLoading] = useState(
4910
+ enabled && normalizedOrderId.length > 0 && !cached
4911
+ );
4912
+ const [error, setError] = useState(null);
4913
+ const load = useCallback(
4914
+ async (force = false) => {
4915
+ if (!enabled || normalizedOrderId.length === 0) {
4916
+ setOrder(null);
4917
+ setIsLoading(false);
4918
+ return;
4919
+ }
4920
+ const nextRequestId = ++requestIdRef.current;
4921
+ setError(null);
4922
+ if (!force) {
4923
+ const cacheEntry = orderCache.get(cacheKey);
4924
+ if (cacheEntry) {
4925
+ setOrder(cacheEntry.order);
4926
+ setIsLoading(false);
4927
+ return;
4928
+ }
4929
+ }
4930
+ setIsLoading(true);
4931
+ try {
4932
+ const existing = orderInflight.get(cacheKey);
4933
+ const promise = existing ?? (async () => {
4934
+ const result = await client.orders.get(normalizedOrderId);
4935
+ if (!result.ok) {
4936
+ throw result.error;
4937
+ }
4938
+ return result.value;
4939
+ })();
4940
+ if (!existing) {
4941
+ orderInflight.set(
4942
+ cacheKey,
4943
+ promise.finally(() => {
4944
+ orderInflight.delete(cacheKey);
4945
+ })
4946
+ );
4947
+ }
4948
+ const value = await promise;
4949
+ orderCache.set(cacheKey, { order: value });
4950
+ if (nextRequestId === requestIdRef.current) {
4951
+ setOrder(value);
4952
+ setError(null);
4953
+ }
4954
+ } catch (loadError) {
4955
+ if (nextRequestId === requestIdRef.current) {
4956
+ setError(loadError);
4957
+ }
4958
+ } finally {
4959
+ if (nextRequestId === requestIdRef.current) {
4960
+ setIsLoading(false);
4961
+ }
4962
+ }
4963
+ },
4964
+ [cacheKey, client, enabled, normalizedOrderId]
4965
+ );
4966
+ useEffect(() => {
4967
+ void load(false);
4968
+ }, [load]);
4969
+ useEffect(() => {
4970
+ if (!poll || !enabled || normalizedOrderId.length === 0) {
4971
+ return;
4972
+ }
4973
+ const timer = window.setInterval(() => {
4974
+ void load(true);
4975
+ }, Math.max(1e3, pollInterval));
4976
+ return () => {
4977
+ window.clearInterval(timer);
4978
+ };
4979
+ }, [enabled, load, normalizedOrderId.length, poll, pollInterval]);
4980
+ const refetch = useCallback(async () => {
4981
+ orderCache.delete(cacheKey);
4982
+ await load(true);
4983
+ }, [cacheKey, load]);
4984
+ return { order, isLoading, error, refetch };
4985
+ }
4986
+ var LOCATION_STORAGE_KEY2 = "cimplify_location_id";
4987
+ function readStoredLocationId() {
4988
+ if (typeof window === "undefined" || !window.localStorage) {
4989
+ return null;
4990
+ }
4991
+ const value = window.localStorage.getItem(LOCATION_STORAGE_KEY2);
4992
+ if (!value) {
4993
+ return null;
4994
+ }
4995
+ const normalized = value.trim();
4996
+ return normalized.length > 0 ? normalized : null;
4997
+ }
4998
+ function writeStoredLocationId(locationId) {
4999
+ if (typeof window === "undefined" || !window.localStorage) {
5000
+ return;
5001
+ }
5002
+ if (!locationId) {
5003
+ window.localStorage.removeItem(LOCATION_STORAGE_KEY2);
5004
+ return;
5005
+ }
5006
+ window.localStorage.setItem(LOCATION_STORAGE_KEY2, locationId);
5007
+ }
5008
+ function resolveLocation(locations, preferredId) {
5009
+ if (locations.length === 0) {
5010
+ return null;
5011
+ }
5012
+ if (preferredId) {
5013
+ const found = locations.find((location) => location.id === preferredId);
5014
+ if (found) {
5015
+ return found;
5016
+ }
5017
+ }
5018
+ return locations[0];
5019
+ }
5020
+ function useLocations(options = {}) {
5021
+ const context = useOptionalCimplify();
5022
+ if (context && (!options.client || context.client === options.client)) {
5023
+ return {
5024
+ locations: context.locations,
5025
+ currentLocation: context.currentLocation,
5026
+ setCurrentLocation: context.setCurrentLocation,
5027
+ isLoading: !context.isReady
5028
+ };
5029
+ }
5030
+ const client = options.client;
5031
+ const [locations, setLocations] = useState([]);
5032
+ const [currentLocation, setCurrentLocationState] = useState(null);
5033
+ const [isLoading, setIsLoading] = useState(true);
5034
+ const setCurrentLocation = useCallback(
5035
+ (location) => {
5036
+ setCurrentLocationState(location);
5037
+ if (client) {
5038
+ client.setLocationId(location.id);
5039
+ }
5040
+ writeStoredLocationId(location.id);
5041
+ },
5042
+ [client]
5043
+ );
5044
+ useEffect(() => {
5045
+ if (!client) {
5046
+ setLocations([]);
5047
+ setCurrentLocationState(null);
5048
+ setIsLoading(false);
5049
+ return;
5050
+ }
5051
+ const activeClient = client;
5052
+ let cancelled = false;
5053
+ async function loadLocations() {
5054
+ setIsLoading(true);
5055
+ const result = await activeClient.business.getLocations();
5056
+ if (cancelled) {
5057
+ return;
5058
+ }
5059
+ if (!result.ok) {
5060
+ setLocations([]);
5061
+ setCurrentLocationState(null);
5062
+ setIsLoading(false);
5063
+ return;
5064
+ }
5065
+ const nextLocations = result.value;
5066
+ const preferredId = activeClient.getLocationId() ?? readStoredLocationId();
5067
+ const resolved = resolveLocation(nextLocations, preferredId);
5068
+ setLocations(nextLocations);
5069
+ setCurrentLocationState(resolved);
5070
+ activeClient.setLocationId(resolved?.id || null);
5071
+ writeStoredLocationId(resolved?.id || null);
5072
+ setIsLoading(false);
5073
+ }
5074
+ void loadLocations();
5075
+ return () => {
5076
+ cancelled = true;
5077
+ };
5078
+ }, [client]);
5079
+ return {
5080
+ locations,
5081
+ currentLocation,
5082
+ setCurrentLocation,
5083
+ isLoading
5084
+ };
5085
+ }
3864
5086
  var ElementsContext = createContext({
3865
5087
  elements: null,
3866
5088
  isReady: false
@@ -4021,4 +5243,4 @@ function useCheckout() {
4021
5243
  return { submit, process, isLoading };
4022
5244
  }
4023
5245
 
4024
- export { Ad, AdProvider, AddressElement, AuthElement, CimplifyCheckout, CimplifyProvider, ElementsProvider, PaymentElement, useAds, useCheckout, useCimplify, useElements, useElementsReady };
5246
+ export { Ad, AdProvider, AddressElement, AuthElement, CimplifyCheckout, CimplifyProvider, ElementsProvider, PaymentElement, useAds, useCart, useCategories, useCheckout, useCimplify, useElements, useElementsReady, useLocations, useOptionalCimplify, useOrder, useProduct, useProducts };