@cimplify/sdk 0.6.6 → 0.6.8

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",
@@ -688,17 +711,88 @@ function CimplifyCheckout({
688
711
  }
689
712
 
690
713
  // src/types/common.ts
714
+ function money(value) {
715
+ return value;
716
+ }
717
+ function moneyFromNumber(value) {
718
+ return value.toFixed(2);
719
+ }
720
+ function currencyCode(value) {
721
+ return value;
722
+ }
691
723
  var ErrorCode = {
692
724
  // General
693
725
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
694
726
  NETWORK_ERROR: "NETWORK_ERROR",
695
727
  TIMEOUT: "TIMEOUT",
728
+ UNAUTHORIZED: "UNAUTHORIZED",
696
729
  NOT_FOUND: "NOT_FOUND"};
730
+ var DOCS_ERROR_BASE_URL = "https://docs.cimplify.io/reference/error-codes";
731
+ function docsUrlForCode(code) {
732
+ return `${DOCS_ERROR_BASE_URL}#${code.toLowerCase().replace(/_/g, "-")}`;
733
+ }
734
+ var ERROR_SUGGESTIONS = {
735
+ UNKNOWN_ERROR: "An unexpected error occurred. Capture the request/response payload and retry with exponential backoff.",
736
+ NETWORK_ERROR: "Check the shopper's connection and retry. If this persists, inspect CORS, DNS, and API reachability.",
737
+ TIMEOUT: "The request exceeded the timeout. Retry once, then poll order status before charging again.",
738
+ UNAUTHORIZED: "Authentication is missing or expired. Ensure a valid access token is set and refresh the session if needed.",
739
+ FORBIDDEN: "The key/session lacks permission for this resource. Verify business ownership and API key scope.",
740
+ NOT_FOUND: "The requested resource does not exist or is not visible in this environment.",
741
+ VALIDATION_ERROR: "One or more fields are invalid. Validate required fields and enum values before retrying.",
742
+ CART_EMPTY: "The cart has no items. Redirect back to menu/catalogue and require at least one line item.",
743
+ CART_EXPIRED: "This cart is no longer active. Recreate a new cart and re-add shopper selections.",
744
+ CART_NOT_FOUND: "Cart could not be located. It may have expired or belongs to a different key/location.",
745
+ ITEM_UNAVAILABLE: "The selected item is unavailable at this location/time. Prompt the shopper to pick an alternative.",
746
+ VARIANT_NOT_FOUND: "The requested variant no longer exists. Refresh product data and require re-selection.",
747
+ VARIANT_OUT_OF_STOCK: "The selected variant is out of stock. Show in-stock variants and block checkout for this line.",
748
+ ADDON_REQUIRED: "A required add-on is missing. Ensure required modifier groups are completed before add-to-cart.",
749
+ ADDON_MAX_EXCEEDED: "Too many add-ons were selected. Enforce max selections client-side before submission.",
750
+ CHECKOUT_VALIDATION_FAILED: "Checkout payload failed validation. Verify customer, order type, and address fields are complete.",
751
+ DELIVERY_ADDRESS_REQUIRED: "Delivery orders require an address. Collect and pass address info before processing checkout.",
752
+ CUSTOMER_INFO_REQUIRED: "Customer details are required. Ensure name/email/phone are available before checkout.",
753
+ PAYMENT_FAILED: "Payment provider rejected or failed processing. Show retry/change-method options to the shopper.",
754
+ PAYMENT_CANCELLED: "Payment was cancelled by the shopper or provider flow. Allow a safe retry path.",
755
+ INSUFFICIENT_FUNDS: "Payment method has insufficient funds. Prompt shopper to use another method.",
756
+ CARD_DECLINED: "Card was declined. Ask shopper to retry or switch payment method.",
757
+ INVALID_OTP: "Authorization code is invalid. Let shopper re-enter OTP/PIN and retry.",
758
+ OTP_EXPIRED: "Authorization code expired. Request a new OTP and re-submit authorization.",
759
+ AUTHORIZATION_FAILED: "Additional payment authorization failed. Retry authorization or change payment method.",
760
+ PAYMENT_ACTION_NOT_COMPLETED: "Required payment action was not completed. Resume provider flow and poll for status.",
761
+ SLOT_UNAVAILABLE: "Selected schedule slot is unavailable. Refresh available slots and ask shopper to reselect.",
762
+ BOOKING_CONFLICT: "The requested booking conflicts with an existing reservation. Pick another slot/resource.",
763
+ SERVICE_NOT_FOUND: "Requested service no longer exists. Refresh service catalogue and retry selection.",
764
+ OUT_OF_STOCK: "Inventory is depleted for this item. Remove it or reduce quantity before checkout.",
765
+ INSUFFICIENT_QUANTITY: "Requested quantity exceeds available inventory. Reduce quantity and retry.",
766
+ BUSINESS_ID_REQUIRED: "Business context could not be resolved. Verify the public key and business bootstrap call.",
767
+ INVALID_CART: "Cart is invalid for checkout. Sync cart state, ensure items exist, then retry.",
768
+ ORDER_TYPE_REQUIRED: "Order type is required. Provide one of delivery, pickup, or dine_in before checkout.",
769
+ NO_PAYMENT_ELEMENT: "PaymentElement is required for processCheckout(). Mount it before triggering checkout.",
770
+ PAYMENT_NOT_MOUNTED: "PaymentElement iframe is not mounted. Mount it in the DOM before processCheckout().",
771
+ AUTH_INCOMPLETE: "AuthElement has not completed authentication. Wait for AUTHENTICATED before checkout.",
772
+ AUTH_LOST: "Session was cleared during checkout. Re-authenticate and restart checkout safely.",
773
+ ALREADY_PROCESSING: "Checkout is already in progress. Disable duplicate submits until completion.",
774
+ CHECKOUT_NOT_READY: "Checkout elements are still initializing. Wait for readiness before submit.",
775
+ CANCELLED: "Checkout was cancelled. Preserve cart state and allow shopper to retry.",
776
+ REQUEST_TIMEOUT: "Provider call timed out. Poll payment/order status before issuing another charge attempt.",
777
+ POPUP_BLOCKED: "Browser blocked provider popup. Ask shopper to enable popups and retry.",
778
+ FX_QUOTE_FAILED: "Failed to lock FX quote. Retry currency quote or fallback to base currency."
779
+ };
780
+ var ERROR_HINTS = Object.fromEntries(
781
+ Object.entries(ERROR_SUGGESTIONS).map(([code, suggestion]) => [
782
+ code,
783
+ {
784
+ docs_url: docsUrlForCode(code),
785
+ suggestion
786
+ }
787
+ ])
788
+ );
697
789
  var CimplifyError = class extends Error {
698
- constructor(code, message, retryable = false) {
790
+ constructor(code, message, retryable = false, docs_url, suggestion) {
699
791
  super(message);
700
792
  this.code = code;
701
793
  this.retryable = retryable;
794
+ this.docs_url = docs_url;
795
+ this.suggestion = suggestion;
702
796
  this.name = "CimplifyError";
703
797
  }
704
798
  /** User-friendly message safe to display */
@@ -706,6 +800,28 @@ var CimplifyError = class extends Error {
706
800
  return this.message;
707
801
  }
708
802
  };
803
+ function getErrorHint(code) {
804
+ return ERROR_HINTS[code];
805
+ }
806
+ function enrichError(error, options = {}) {
807
+ const hint = getErrorHint(error.code);
808
+ if (hint) {
809
+ if (!error.docs_url) {
810
+ error.docs_url = hint.docs_url;
811
+ }
812
+ if (!error.suggestion) {
813
+ error.suggestion = hint.suggestion;
814
+ }
815
+ } else if (!error.docs_url) {
816
+ error.docs_url = docsUrlForCode(error.code || ErrorCode.UNKNOWN_ERROR);
817
+ }
818
+ if (options.isTestMode && !error.message.includes("pk_test_")) {
819
+ error.message = `${error.message}
820
+
821
+ \u2139 Your API key is a test-mode key (pk_test_...). Verify test data/session before retrying.`;
822
+ }
823
+ return error;
824
+ }
709
825
 
710
826
  // src/types/result.ts
711
827
  function ok(value) {
@@ -715,13 +831,18 @@ function err(error) {
715
831
  return { ok: false, error };
716
832
  }
717
833
 
834
+ // src/query/builder.ts
835
+ function escapeQueryValue(value) {
836
+ return value.replace(/'/g, "\\'");
837
+ }
838
+
718
839
  // src/catalogue.ts
719
840
  function toCimplifyError(error) {
720
- if (error instanceof CimplifyError) return error;
841
+ if (error instanceof CimplifyError) return enrichError(error);
721
842
  if (error instanceof Error) {
722
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
843
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
723
844
  }
724
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
845
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
725
846
  }
726
847
  async function safe(promise) {
727
848
  try {
@@ -795,7 +916,7 @@ var CatalogueQueries = class {
795
916
  let query = "products";
796
917
  const filters = [];
797
918
  if (options?.category) {
798
- filters.push(`@.category_id=='${options.category}'`);
919
+ filters.push(`@.category_id=='${escapeQueryValue(options.category)}'`);
799
920
  }
800
921
  if (options?.featured !== void 0) {
801
922
  filters.push(`@.featured==${options.featured}`);
@@ -804,7 +925,7 @@ var CatalogueQueries = class {
804
925
  filters.push(`@.in_stock==${options.in_stock}`);
805
926
  }
806
927
  if (options?.search) {
807
- filters.push(`@.name contains '${options.search}'`);
928
+ filters.push(`@.name contains '${escapeQueryValue(options.search)}'`);
808
929
  }
809
930
  if (options?.min_price !== void 0) {
810
931
  filters.push(`@.price>=${options.min_price}`);
@@ -835,7 +956,9 @@ var CatalogueQueries = class {
835
956
  }
836
957
  async getProductBySlug(slug) {
837
958
  const filteredResult = await safe(
838
- this.client.query(`products[?(@.slug=='${slug}')]`)
959
+ this.client.query(
960
+ `products[?(@.slug=='${escapeQueryValue(slug)}')]`
961
+ )
839
962
  );
840
963
  if (!filteredResult.ok) return filteredResult;
841
964
  const exactMatch = findProductBySlug(filteredResult.value, slug);
@@ -887,7 +1010,7 @@ var CatalogueQueries = class {
887
1010
  }
888
1011
  async getCategoryBySlug(slug) {
889
1012
  const result = await safe(
890
- this.client.query(`categories[?(@.slug=='${slug}')]`)
1013
+ this.client.query(`categories[?(@.slug=='${escapeQueryValue(slug)}')]`)
891
1014
  );
892
1015
  if (!result.ok) return result;
893
1016
  if (!result.value.length) {
@@ -896,7 +1019,11 @@ var CatalogueQueries = class {
896
1019
  return ok(result.value[0]);
897
1020
  }
898
1021
  async getCategoryProducts(categoryId) {
899
- return safe(this.client.query(`products[?(@.category_id=='${categoryId}')]`));
1022
+ return safe(
1023
+ this.client.query(
1024
+ `products[?(@.category_id=='${escapeQueryValue(categoryId)}')]`
1025
+ )
1026
+ );
900
1027
  }
901
1028
  async getCollections() {
902
1029
  return safe(this.client.query("collections"));
@@ -906,7 +1033,9 @@ var CatalogueQueries = class {
906
1033
  }
907
1034
  async getCollectionBySlug(slug) {
908
1035
  const result = await safe(
909
- this.client.query(`collections[?(@.slug=='${slug}')]`)
1036
+ this.client.query(
1037
+ `collections[?(@.slug=='${escapeQueryValue(slug)}')]`
1038
+ )
910
1039
  );
911
1040
  if (!result.ok) return result;
912
1041
  if (!result.value.length) {
@@ -919,7 +1048,9 @@ var CatalogueQueries = class {
919
1048
  }
920
1049
  async searchCollections(query, limit = 20) {
921
1050
  return safe(
922
- this.client.query(`collections[?(@.name contains '${query}')]#limit(${limit})`)
1051
+ this.client.query(
1052
+ `collections[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
1053
+ )
923
1054
  );
924
1055
  }
925
1056
  async getBundles() {
@@ -930,7 +1061,9 @@ var CatalogueQueries = class {
930
1061
  }
931
1062
  async getBundleBySlug(slug) {
932
1063
  const result = await safe(
933
- this.client.query(`bundles[?(@.slug=='${slug}')]`)
1064
+ this.client.query(
1065
+ `bundles[?(@.slug=='${escapeQueryValue(slug)}')]`
1066
+ )
934
1067
  );
935
1068
  if (!result.ok) return result;
936
1069
  if (!result.value.length) {
@@ -940,7 +1073,9 @@ var CatalogueQueries = class {
940
1073
  }
941
1074
  async searchBundles(query, limit = 20) {
942
1075
  return safe(
943
- this.client.query(`bundles[?(@.name contains '${query}')]#limit(${limit})`)
1076
+ this.client.query(
1077
+ `bundles[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
1078
+ )
944
1079
  );
945
1080
  }
946
1081
  async getComposites(options) {
@@ -980,9 +1115,9 @@ var CatalogueQueries = class {
980
1115
  }
981
1116
  async search(query, options) {
982
1117
  const limit = options?.limit ?? 20;
983
- let searchQuery = `products[?(@.name contains '${query}')]`;
1118
+ let searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}')]`;
984
1119
  if (options?.category) {
985
- searchQuery = `products[?(@.name contains '${query}' && @.category_id=='${options.category}')]`;
1120
+ searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}' && @.category_id=='${escapeQueryValue(options.category)}')]`;
986
1121
  }
987
1122
  searchQuery += `#limit(${limit})`;
988
1123
  return safe(this.client.query(searchQuery));
@@ -999,7 +1134,7 @@ var CatalogueQueries = class {
999
1134
  async getMenu(options) {
1000
1135
  let query = "menu";
1001
1136
  if (options?.category) {
1002
- query = `menu[?(@.category=='${options.category}')]`;
1137
+ query = `menu[?(@.category=='${escapeQueryValue(options.category)}')]`;
1003
1138
  }
1004
1139
  if (options?.limit) {
1005
1140
  query += `#limit(${options.limit})`;
@@ -1016,11 +1151,11 @@ var CatalogueQueries = class {
1016
1151
 
1017
1152
  // src/cart.ts
1018
1153
  function toCimplifyError2(error) {
1019
- if (error instanceof CimplifyError) return error;
1154
+ if (error instanceof CimplifyError) return enrichError(error);
1020
1155
  if (error instanceof Error) {
1021
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
1156
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
1022
1157
  }
1023
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
1158
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
1024
1159
  }
1025
1160
  async function safe2(promise) {
1026
1161
  try {
@@ -1180,6 +1315,19 @@ var ORDER_MUTATION = {
1180
1315
  UPDATE_CUSTOMER: "order.update_order_customer"
1181
1316
  };
1182
1317
 
1318
+ // src/utils/price.ts
1319
+ function parsePrice(value) {
1320
+ if (value === void 0 || value === null) {
1321
+ return 0;
1322
+ }
1323
+ if (typeof value === "number") {
1324
+ return isNaN(value) ? 0 : value;
1325
+ }
1326
+ const cleaned = value.replace(/[^\d.-]/g, "");
1327
+ const parsed = parseFloat(cleaned);
1328
+ return isNaN(parsed) ? 0 : parsed;
1329
+ }
1330
+
1183
1331
  // src/utils/payment.ts
1184
1332
  var PAYMENT_SUCCESS_STATUSES = /* @__PURE__ */ new Set([
1185
1333
  "success",
@@ -1280,6 +1428,8 @@ function normalizeStatusResponse(response) {
1280
1428
  }
1281
1429
  const res = response;
1282
1430
  const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
1431
+ const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) : void 0;
1432
+ const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ? currencyCode(res.currency) : void 0;
1283
1433
  const paidValue = res.paid === true;
1284
1434
  const derivedPaid = paidValue || [
1285
1435
  "success",
@@ -1292,8 +1442,8 @@ function normalizeStatusResponse(response) {
1292
1442
  return {
1293
1443
  status: normalizedStatus,
1294
1444
  paid: derivedPaid,
1295
- amount: res.amount,
1296
- currency: res.currency,
1445
+ amount: normalizedAmount,
1446
+ currency: normalizedCurrency,
1297
1447
  reference: res.reference,
1298
1448
  message: res.message || ""
1299
1449
  };
@@ -1859,11 +2009,11 @@ var CheckoutResolver = class {
1859
2009
 
1860
2010
  // src/checkout.ts
1861
2011
  function toCimplifyError3(error) {
1862
- if (error instanceof CimplifyError) return error;
2012
+ if (error instanceof CimplifyError) return enrichError(error);
1863
2013
  if (error instanceof Error) {
1864
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
2014
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
1865
2015
  }
1866
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
2016
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
1867
2017
  }
1868
2018
  async function safe3(promise) {
1869
2019
  try {
@@ -1985,14 +2135,17 @@ var CheckoutService = class {
1985
2135
  pay_currency: data.pay_currency,
1986
2136
  fx_quote_id: data.fx_quote_id
1987
2137
  };
1988
- const baseCurrency = (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase();
2138
+ const baseCurrency = currencyCode(
2139
+ (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase()
2140
+ );
1989
2141
  const payCurrency = data.pay_currency?.trim().toUpperCase();
2142
+ const payCurrencyCode = payCurrency ? currencyCode(payCurrency) : void 0;
1990
2143
  const cartTotalAmount = Number.parseFloat(cart.pricing.total_price || "0");
1991
- if (payCurrency && payCurrency !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
2144
+ if (payCurrencyCode && payCurrencyCode !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
1992
2145
  const fxQuoteResult = await this.client.fx.lockQuote({
1993
2146
  from: baseCurrency,
1994
- to: payCurrency,
1995
- amount: cartTotalAmount
2147
+ to: payCurrencyCode,
2148
+ amount: cart.pricing.total_price
1996
2149
  });
1997
2150
  if (!fxQuoteResult.ok) {
1998
2151
  return ok(
@@ -2003,7 +2156,7 @@ var CheckoutService = class {
2003
2156
  )
2004
2157
  );
2005
2158
  }
2006
- checkoutData.pay_currency = payCurrency;
2159
+ checkoutData.pay_currency = payCurrencyCode;
2007
2160
  checkoutData.fx_quote_id = fxQuoteResult.value.id;
2008
2161
  }
2009
2162
  data.on_status_change?.("processing", {});
@@ -2039,11 +2192,11 @@ var CheckoutService = class {
2039
2192
 
2040
2193
  // src/orders.ts
2041
2194
  function toCimplifyError4(error) {
2042
- if (error instanceof CimplifyError) return error;
2195
+ if (error instanceof CimplifyError) return enrichError(error);
2043
2196
  if (error instanceof Error) {
2044
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
2197
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
2045
2198
  }
2046
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
2199
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
2047
2200
  }
2048
2201
  async function safe4(promise) {
2049
2202
  try {
@@ -2300,11 +2453,11 @@ var AuthService = class {
2300
2453
 
2301
2454
  // src/business.ts
2302
2455
  function toCimplifyError7(error) {
2303
- if (error instanceof CimplifyError) return error;
2456
+ if (error instanceof CimplifyError) return enrichError(error);
2304
2457
  if (error instanceof Error) {
2305
- return new CimplifyError("UNKNOWN_ERROR", error.message, false);
2458
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, error.message, false));
2306
2459
  }
2307
- return new CimplifyError("UNKNOWN_ERROR", String(error), false);
2460
+ return enrichError(new CimplifyError(ErrorCode.UNKNOWN_ERROR, String(error), false));
2308
2461
  }
2309
2462
  async function safe7(promise) {
2310
2463
  try {
@@ -2651,12 +2804,15 @@ var FxService = class {
2651
2804
 
2652
2805
  // src/elements.ts
2653
2806
  function toCheckoutError(code, message, recoverable) {
2807
+ const hint = getErrorHint(code);
2654
2808
  return {
2655
2809
  success: false,
2656
2810
  error: {
2657
2811
  code,
2658
2812
  message,
2659
- recoverable
2813
+ recoverable,
2814
+ docs_url: hint?.docs_url,
2815
+ suggestion: hint?.suggestion
2660
2816
  }
2661
2817
  };
2662
2818
  }
@@ -2697,6 +2853,7 @@ var CimplifyElements = class {
2697
2853
  this.paymentData = null;
2698
2854
  this.checkoutInProgress = false;
2699
2855
  this.activeCheckoutAbort = null;
2856
+ this.hasWarnedMissingAuthElement = false;
2700
2857
  this.businessIdResolvePromise = null;
2701
2858
  this.client = client;
2702
2859
  this.businessId = businessId ?? null;
@@ -2792,6 +2949,12 @@ var CimplifyElements = class {
2792
2949
  );
2793
2950
  }
2794
2951
  const authElement = this.elements.get(ELEMENT_TYPES.AUTH);
2952
+ if (!authElement && !this.hasWarnedMissingAuthElement) {
2953
+ this.hasWarnedMissingAuthElement = true;
2954
+ console.warn(
2955
+ "[Cimplify] processCheckout() called without AuthElement mounted. For best conversion and Link enrollment, mount <AuthElement> before checkout."
2956
+ );
2957
+ }
2795
2958
  if (authElement && !this.accessToken) {
2796
2959
  return toCheckoutError(
2797
2960
  "AUTH_INCOMPLETE",
@@ -3228,28 +3391,37 @@ function isRetryable(error) {
3228
3391
  }
3229
3392
  return false;
3230
3393
  }
3231
- function toNetworkError(error) {
3394
+ function toNetworkError(error, isTestMode) {
3232
3395
  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
3396
+ return enrichError(
3397
+ new CimplifyError(
3398
+ ErrorCode.TIMEOUT,
3399
+ "Request timed out. Please check your connection and try again.",
3400
+ true
3401
+ ),
3402
+ { isTestMode }
3237
3403
  );
3238
3404
  }
3239
3405
  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
3406
+ return enrichError(
3407
+ new CimplifyError(
3408
+ ErrorCode.NETWORK_ERROR,
3409
+ "Network error. Please check your internet connection.",
3410
+ true
3411
+ ),
3412
+ { isTestMode }
3244
3413
  );
3245
3414
  }
3246
3415
  if (error instanceof CimplifyError) {
3247
- return error;
3416
+ return enrichError(error, { isTestMode });
3248
3417
  }
3249
- return new CimplifyError(
3250
- ErrorCode.UNKNOWN_ERROR,
3251
- error instanceof Error ? error.message : "An unknown error occurred",
3252
- false
3418
+ return enrichError(
3419
+ new CimplifyError(
3420
+ ErrorCode.UNKNOWN_ERROR,
3421
+ error instanceof Error ? error.message : "An unknown error occurred",
3422
+ false
3423
+ ),
3424
+ { isTestMode }
3253
3425
  );
3254
3426
  }
3255
3427
  function deriveUrls() {
@@ -3311,6 +3483,9 @@ var CimplifyClient = class {
3311
3483
  getPublicKey() {
3312
3484
  return this.publicKey;
3313
3485
  }
3486
+ isTestMode() {
3487
+ return this.publicKey.trim().startsWith("pk_test_");
3488
+ }
3314
3489
  setAccessToken(token) {
3315
3490
  const previous = this.accessToken;
3316
3491
  this.accessToken = token;
@@ -3432,7 +3607,7 @@ var CimplifyClient = class {
3432
3607
  signal: controller.signal
3433
3608
  });
3434
3609
  clearTimeout(timeoutId);
3435
- if (response.ok || response.status >= 400 && response.status < 500) {
3610
+ if (response.ok) {
3436
3611
  this.hooks.onRequestSuccess?.({
3437
3612
  ...context,
3438
3613
  status: response.status,
@@ -3440,6 +3615,21 @@ var CimplifyClient = class {
3440
3615
  });
3441
3616
  return response;
3442
3617
  }
3618
+ if (response.status >= 400 && response.status < 500) {
3619
+ this.hooks.onRequestError?.({
3620
+ ...context,
3621
+ error: new CimplifyError(
3622
+ `HTTP_${response.status}`,
3623
+ `Request failed with status ${response.status}`,
3624
+ false
3625
+ ),
3626
+ status: response.status,
3627
+ durationMs: Date.now() - startTime,
3628
+ retryCount,
3629
+ retryable: false
3630
+ });
3631
+ return response;
3632
+ }
3443
3633
  if (response.status >= 500 && attempt < this.maxRetries) {
3444
3634
  retryCount++;
3445
3635
  const delay = this.retryDelay * Math.pow(2, attempt);
@@ -3452,15 +3642,22 @@ var CimplifyClient = class {
3452
3642
  await sleep(delay);
3453
3643
  continue;
3454
3644
  }
3455
- this.hooks.onRequestSuccess?.({
3645
+ this.hooks.onRequestError?.({
3456
3646
  ...context,
3647
+ error: new CimplifyError(
3648
+ "SERVER_ERROR",
3649
+ `Server error ${response.status} after ${retryCount} retries`,
3650
+ false
3651
+ ),
3457
3652
  status: response.status,
3458
- durationMs: Date.now() - startTime
3653
+ durationMs: Date.now() - startTime,
3654
+ retryCount,
3655
+ retryable: false
3459
3656
  });
3460
3657
  return response;
3461
3658
  } catch (error) {
3462
3659
  lastError = error;
3463
- const networkError = toNetworkError(error);
3660
+ const networkError = toNetworkError(error, this.isTestMode());
3464
3661
  const errorRetryable = isRetryable(error);
3465
3662
  if (!errorRetryable || attempt >= this.maxRetries) {
3466
3663
  this.hooks.onRequestError?.({
@@ -3483,7 +3680,7 @@ var CimplifyClient = class {
3483
3680
  await sleep(delay);
3484
3681
  }
3485
3682
  }
3486
- const finalError = toNetworkError(lastError);
3683
+ const finalError = toNetworkError(lastError, this.isTestMode());
3487
3684
  this.hooks.onRequestError?.({
3488
3685
  ...context,
3489
3686
  error: finalError,
@@ -3601,22 +3798,40 @@ var CimplifyClient = class {
3601
3798
  async handleRestResponse(response) {
3602
3799
  const json = await response.json();
3603
3800
  if (!response.ok) {
3604
- throw new CimplifyError(
3605
- json.error?.error_code || "API_ERROR",
3606
- json.error?.error_message || "An error occurred",
3607
- false
3801
+ const error = enrichError(
3802
+ new CimplifyError(
3803
+ json.error?.error_code || "API_ERROR",
3804
+ json.error?.error_message || "An error occurred",
3805
+ false
3806
+ ),
3807
+ { isTestMode: this.isTestMode() }
3608
3808
  );
3809
+ if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
3810
+ console.warn(
3811
+ "[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
3812
+ );
3813
+ }
3814
+ throw error;
3609
3815
  }
3610
3816
  return json.data;
3611
3817
  }
3612
3818
  async handleResponse(response) {
3613
3819
  const json = await response.json();
3614
3820
  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
3821
+ const error = enrichError(
3822
+ new CimplifyError(
3823
+ json.error?.code || "UNKNOWN_ERROR",
3824
+ json.error?.message || "An unknown error occurred",
3825
+ json.error?.retryable || false
3826
+ ),
3827
+ { isTestMode: this.isTestMode() }
3619
3828
  );
3829
+ if (response.status === 401 || error.code === ErrorCode.UNAUTHORIZED) {
3830
+ console.warn(
3831
+ "[Cimplify] Received 401 Unauthorized. Access token may be missing/expired. Refresh authentication and retry the request."
3832
+ );
3833
+ }
3834
+ throw error;
3620
3835
  }
3621
3836
  return json.data;
3622
3837
  }
@@ -3803,6 +4018,9 @@ function CimplifyProvider({
3803
4018
  const nextLocations = locationsResult.ok && Array.isArray(locationsResult.value) ? locationsResult.value : [];
3804
4019
  const initialLocation = resolveInitialLocation(nextLocations);
3805
4020
  setBusiness(nextBusiness);
4021
+ if (nextBusiness?.id) {
4022
+ resolvedClient.setBusinessId(nextBusiness.id);
4023
+ }
3806
4024
  setLocations(nextLocations);
3807
4025
  if (initialLocation) {
3808
4026
  setCurrentLocationState(initialLocation);
@@ -3861,6 +4079,1809 @@ function useCimplify() {
3861
4079
  }
3862
4080
  return context;
3863
4081
  }
4082
+ function useOptionalCimplify() {
4083
+ return useContext(CimplifyContext);
4084
+ }
4085
+ var productsCache = /* @__PURE__ */ new Map();
4086
+ var productsInflight = /* @__PURE__ */ new Map();
4087
+ function buildProductsCacheKey(client, locationId, options) {
4088
+ return JSON.stringify({
4089
+ key: client.getPublicKey(),
4090
+ location_id: locationId || "__none__",
4091
+ options
4092
+ });
4093
+ }
4094
+ function useProducts(options = {}) {
4095
+ const context = useOptionalCimplify();
4096
+ const client = options.client ?? context?.client;
4097
+ if (!client) {
4098
+ throw new Error("useProducts must be used within CimplifyProvider or passed { client }.");
4099
+ }
4100
+ const enabled = options.enabled ?? true;
4101
+ const locationId = client.getLocationId();
4102
+ const previousLocationIdRef = useRef(locationId);
4103
+ const requestIdRef = useRef(0);
4104
+ const queryOptions = useMemo(
4105
+ () => ({
4106
+ category: options.category,
4107
+ collection: options.collection,
4108
+ search: options.search,
4109
+ featured: options.featured,
4110
+ limit: options.limit
4111
+ }),
4112
+ [options.category, options.collection, options.featured, options.limit, options.search]
4113
+ );
4114
+ const cacheKey = useMemo(
4115
+ () => buildProductsCacheKey(client, locationId, queryOptions),
4116
+ [client, locationId, queryOptions]
4117
+ );
4118
+ const cached = productsCache.get(cacheKey);
4119
+ const [products, setProducts] = useState(cached?.products ?? []);
4120
+ const [isLoading, setIsLoading] = useState(enabled && !cached);
4121
+ const [error, setError] = useState(null);
4122
+ useEffect(() => {
4123
+ if (previousLocationIdRef.current !== locationId) {
4124
+ productsCache.clear();
4125
+ productsInflight.clear();
4126
+ previousLocationIdRef.current = locationId;
4127
+ }
4128
+ }, [locationId]);
4129
+ const load = useCallback(
4130
+ async (force = false) => {
4131
+ if (!enabled) {
4132
+ setIsLoading(false);
4133
+ return;
4134
+ }
4135
+ const nextRequestId = ++requestIdRef.current;
4136
+ setError(null);
4137
+ if (!force) {
4138
+ const cacheEntry = productsCache.get(cacheKey);
4139
+ if (cacheEntry) {
4140
+ setProducts(cacheEntry.products);
4141
+ setIsLoading(false);
4142
+ return;
4143
+ }
4144
+ }
4145
+ setIsLoading(true);
4146
+ try {
4147
+ const existing = productsInflight.get(cacheKey);
4148
+ const promise = existing ?? (async () => {
4149
+ const result = await client.catalogue.getProducts(queryOptions);
4150
+ if (!result.ok) {
4151
+ throw result.error;
4152
+ }
4153
+ return result.value;
4154
+ })();
4155
+ if (!existing) {
4156
+ productsInflight.set(
4157
+ cacheKey,
4158
+ promise.finally(() => {
4159
+ productsInflight.delete(cacheKey);
4160
+ })
4161
+ );
4162
+ }
4163
+ const value = await promise;
4164
+ productsCache.set(cacheKey, { products: value });
4165
+ if (nextRequestId === requestIdRef.current) {
4166
+ setProducts(value);
4167
+ setError(null);
4168
+ }
4169
+ } catch (loadError) {
4170
+ if (nextRequestId === requestIdRef.current) {
4171
+ setError(loadError);
4172
+ }
4173
+ } finally {
4174
+ if (nextRequestId === requestIdRef.current) {
4175
+ setIsLoading(false);
4176
+ }
4177
+ }
4178
+ },
4179
+ [cacheKey, client, enabled, queryOptions]
4180
+ );
4181
+ useEffect(() => {
4182
+ void load(false);
4183
+ }, [load]);
4184
+ const refetch = useCallback(async () => {
4185
+ productsCache.delete(cacheKey);
4186
+ await load(true);
4187
+ }, [cacheKey, load]);
4188
+ return { products, isLoading, error, refetch };
4189
+ }
4190
+ var productCache = /* @__PURE__ */ new Map();
4191
+ var productInflight = /* @__PURE__ */ new Map();
4192
+ function isLikelySlug(value) {
4193
+ return /^[a-z0-9-]+$/.test(value);
4194
+ }
4195
+ function buildProductCacheKey(client, locationId, slugOrId) {
4196
+ return JSON.stringify({
4197
+ key: client.getPublicKey(),
4198
+ location_id: locationId || "__none__",
4199
+ slug_or_id: slugOrId
4200
+ });
4201
+ }
4202
+ function useProduct(slugOrId, options = {}) {
4203
+ const context = useOptionalCimplify();
4204
+ const client = options.client ?? context?.client;
4205
+ if (!client) {
4206
+ throw new Error("useProduct must be used within CimplifyProvider or passed { client }.");
4207
+ }
4208
+ const enabled = options.enabled ?? true;
4209
+ const locationId = client.getLocationId();
4210
+ const previousLocationIdRef = useRef(locationId);
4211
+ const requestIdRef = useRef(0);
4212
+ const normalizedSlugOrId = useMemo(() => (slugOrId || "").trim(), [slugOrId]);
4213
+ const cacheKey = useMemo(
4214
+ () => buildProductCacheKey(client, locationId, normalizedSlugOrId),
4215
+ [client, locationId, normalizedSlugOrId]
4216
+ );
4217
+ const cached = productCache.get(cacheKey);
4218
+ const [product, setProduct] = useState(cached?.product ?? null);
4219
+ const [isLoading, setIsLoading] = useState(
4220
+ enabled && normalizedSlugOrId.length > 0 && !cached
4221
+ );
4222
+ const [error, setError] = useState(null);
4223
+ useEffect(() => {
4224
+ if (previousLocationIdRef.current !== locationId) {
4225
+ productCache.clear();
4226
+ productInflight.clear();
4227
+ previousLocationIdRef.current = locationId;
4228
+ }
4229
+ }, [locationId]);
4230
+ const load = useCallback(
4231
+ async (force = false) => {
4232
+ if (!enabled || normalizedSlugOrId.length === 0) {
4233
+ setProduct(null);
4234
+ setIsLoading(false);
4235
+ return;
4236
+ }
4237
+ const nextRequestId = ++requestIdRef.current;
4238
+ setError(null);
4239
+ if (!force) {
4240
+ const cacheEntry = productCache.get(cacheKey);
4241
+ if (cacheEntry) {
4242
+ setProduct(cacheEntry.product);
4243
+ setIsLoading(false);
4244
+ return;
4245
+ }
4246
+ }
4247
+ setIsLoading(true);
4248
+ try {
4249
+ const existing = productInflight.get(cacheKey);
4250
+ const promise = existing ?? (async () => {
4251
+ const result = isLikelySlug(normalizedSlugOrId) ? await client.catalogue.getProductBySlug(normalizedSlugOrId) : await client.catalogue.getProduct(normalizedSlugOrId);
4252
+ if (!result.ok) {
4253
+ throw result.error;
4254
+ }
4255
+ return result.value;
4256
+ })();
4257
+ if (!existing) {
4258
+ productInflight.set(
4259
+ cacheKey,
4260
+ promise.finally(() => {
4261
+ productInflight.delete(cacheKey);
4262
+ })
4263
+ );
4264
+ }
4265
+ const value = await promise;
4266
+ productCache.set(cacheKey, { product: value });
4267
+ if (nextRequestId === requestIdRef.current) {
4268
+ setProduct(value);
4269
+ setError(null);
4270
+ }
4271
+ } catch (loadError) {
4272
+ if (nextRequestId === requestIdRef.current) {
4273
+ setError(loadError);
4274
+ }
4275
+ } finally {
4276
+ if (nextRequestId === requestIdRef.current) {
4277
+ setIsLoading(false);
4278
+ }
4279
+ }
4280
+ },
4281
+ [cacheKey, client, enabled, normalizedSlugOrId]
4282
+ );
4283
+ useEffect(() => {
4284
+ void load(false);
4285
+ }, [load]);
4286
+ const refetch = useCallback(async () => {
4287
+ productCache.delete(cacheKey);
4288
+ await load(true);
4289
+ }, [cacheKey, load]);
4290
+ return { product, isLoading, error, refetch };
4291
+ }
4292
+ var categoriesCache = /* @__PURE__ */ new Map();
4293
+ var categoriesInflight = /* @__PURE__ */ new Map();
4294
+ function buildCategoriesCacheKey(client) {
4295
+ return client.getPublicKey() || "__demo__";
4296
+ }
4297
+ function useCategories(options = {}) {
4298
+ const context = useOptionalCimplify();
4299
+ const client = options.client ?? context?.client;
4300
+ if (!client) {
4301
+ throw new Error("useCategories must be used within CimplifyProvider or passed { client }.");
4302
+ }
4303
+ const enabled = options.enabled ?? true;
4304
+ const cacheKey = useMemo(() => buildCategoriesCacheKey(client), [client]);
4305
+ const cached = categoriesCache.get(cacheKey);
4306
+ const [categories, setCategories] = useState(cached ?? []);
4307
+ const [isLoading, setIsLoading] = useState(enabled && !cached);
4308
+ const [error, setError] = useState(null);
4309
+ const load = useCallback(
4310
+ async (force = false) => {
4311
+ if (!enabled) {
4312
+ setIsLoading(false);
4313
+ return;
4314
+ }
4315
+ setError(null);
4316
+ if (!force) {
4317
+ const cachedCategories = categoriesCache.get(cacheKey);
4318
+ if (cachedCategories) {
4319
+ setCategories(cachedCategories);
4320
+ setIsLoading(false);
4321
+ return;
4322
+ }
4323
+ }
4324
+ setIsLoading(true);
4325
+ try {
4326
+ const existing = categoriesInflight.get(cacheKey);
4327
+ const promise = existing ?? (async () => {
4328
+ const result = await client.catalogue.getCategories();
4329
+ if (!result.ok) {
4330
+ throw result.error;
4331
+ }
4332
+ return result.value;
4333
+ })();
4334
+ if (!existing) {
4335
+ categoriesInflight.set(
4336
+ cacheKey,
4337
+ promise.finally(() => {
4338
+ categoriesInflight.delete(cacheKey);
4339
+ })
4340
+ );
4341
+ }
4342
+ const value = await promise;
4343
+ categoriesCache.set(cacheKey, value);
4344
+ setCategories(value);
4345
+ } catch (loadError) {
4346
+ setError(loadError);
4347
+ } finally {
4348
+ setIsLoading(false);
4349
+ }
4350
+ },
4351
+ [cacheKey, client, enabled]
4352
+ );
4353
+ useEffect(() => {
4354
+ void load(false);
4355
+ }, [load]);
4356
+ const refetch = useCallback(async () => {
4357
+ categoriesCache.delete(cacheKey);
4358
+ await load(true);
4359
+ }, [cacheKey, load]);
4360
+ return { categories, isLoading, error, refetch };
4361
+ }
4362
+ var CART_STORAGE_PREFIX = "cimplify:cart:v2";
4363
+ var DEFAULT_TAX_RATE = 0.08875;
4364
+ var cartStores = /* @__PURE__ */ new Map();
4365
+ function toNumber(value) {
4366
+ if (typeof value === "number" && Number.isFinite(value)) {
4367
+ return value;
4368
+ }
4369
+ if (typeof value === "string") {
4370
+ const parsed = Number.parseFloat(value);
4371
+ return Number.isFinite(parsed) ? parsed : 0;
4372
+ }
4373
+ return 0;
4374
+ }
4375
+ function roundMoney(value) {
4376
+ return Math.max(0, Number(value.toFixed(2)));
4377
+ }
4378
+ function clampQuantity(quantity) {
4379
+ if (!Number.isFinite(quantity)) {
4380
+ return 1;
4381
+ }
4382
+ return Math.max(1, Math.floor(quantity));
4383
+ }
4384
+ function normalizeOptionIds(value) {
4385
+ if (!value || value.length === 0) {
4386
+ return void 0;
4387
+ }
4388
+ const normalized = Array.from(
4389
+ new Set(
4390
+ value.map((entry) => entry.trim()).filter((entry) => entry.length > 0)
4391
+ )
4392
+ ).sort();
4393
+ return normalized.length > 0 ? normalized : void 0;
4394
+ }
4395
+ function buildLineKey(productId, options) {
4396
+ const parts = [productId];
4397
+ if (options.locationId) {
4398
+ parts.push(`l:${options.locationId}`);
4399
+ }
4400
+ if (options.variantId) {
4401
+ parts.push(`v:${options.variantId}`);
4402
+ }
4403
+ const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
4404
+ if (addOnOptionIds && addOnOptionIds.length > 0) {
4405
+ parts.push(`a:${addOnOptionIds.join(",")}`);
4406
+ }
4407
+ if (options.bundleSelections && options.bundleSelections.length > 0) {
4408
+ parts.push(`b:${JSON.stringify(options.bundleSelections)}`);
4409
+ }
4410
+ if (options.compositeSelections && options.compositeSelections.length > 0) {
4411
+ parts.push(`c:${JSON.stringify(options.compositeSelections)}`);
4412
+ }
4413
+ if (options.quoteId) {
4414
+ parts.push(`q:${options.quoteId}`);
4415
+ }
4416
+ return parts.join("|");
4417
+ }
4418
+ function calculateLineSubtotal(item) {
4419
+ let unitPrice = parsePrice(item.product.default_price);
4420
+ if (item.variant?.price_adjustment) {
4421
+ unitPrice += parsePrice(item.variant.price_adjustment);
4422
+ }
4423
+ for (const option of item.addOnOptions || []) {
4424
+ if (option.default_price) {
4425
+ unitPrice += parsePrice(option.default_price);
4426
+ }
4427
+ }
4428
+ return roundMoney(unitPrice * item.quantity);
4429
+ }
4430
+ function calculateSummary(items) {
4431
+ const subtotal = roundMoney(items.reduce((sum, item) => sum + calculateLineSubtotal(item), 0));
4432
+ const tax = roundMoney(subtotal * DEFAULT_TAX_RATE);
4433
+ return {
4434
+ subtotal,
4435
+ tax,
4436
+ total: roundMoney(subtotal + tax)
4437
+ };
4438
+ }
4439
+ function toProductFromServerItem(item, businessId) {
4440
+ return {
4441
+ id: item.item_id,
4442
+ business_id: businessId,
4443
+ category_id: item.category_id,
4444
+ name: item.name,
4445
+ slug: item.item_id,
4446
+ description: item.description,
4447
+ image_url: item.image_url,
4448
+ default_price: item.base_price,
4449
+ product_type: "product",
4450
+ inventory_type: "none",
4451
+ variant_strategy: "fetch_all",
4452
+ is_active: item.is_available,
4453
+ created_at: item.created_at,
4454
+ updated_at: item.updated_at,
4455
+ metadata: {
4456
+ from_sdk: true
4457
+ }
4458
+ };
4459
+ }
4460
+ function mapServerCart(serverCart) {
4461
+ const items = serverCart.items.map((item) => {
4462
+ const variant = item.variant_info || item.variant_name ? {
4463
+ id: item.variant_info?.id || item.variant_id || "",
4464
+ name: item.variant_info?.name || item.variant_name || "Variant",
4465
+ price_adjustment: item.variant_info?.price_adjustment
4466
+ } : void 0;
4467
+ const quoteId = typeof item.metadata?.quote_id === "string" ? item.metadata.quote_id : void 0;
4468
+ return {
4469
+ id: item.id,
4470
+ product: toProductFromServerItem(item, serverCart.business_id),
4471
+ quantity: clampQuantity(item.quantity),
4472
+ locationId: serverCart.location_id,
4473
+ variantId: item.variant_id,
4474
+ variant,
4475
+ quoteId,
4476
+ addOnOptionIds: item.add_on_option_ids || void 0,
4477
+ addOnOptions: (item.add_on_options || []).map((option) => ({
4478
+ id: option.id,
4479
+ name: option.name,
4480
+ default_price: option.price
4481
+ })),
4482
+ bundleSelections: item.bundle_selections || void 0,
4483
+ compositeSelections: item.composite_selections || void 0,
4484
+ specialInstructions: item.special_instructions
4485
+ };
4486
+ });
4487
+ return {
4488
+ items,
4489
+ subtotal: roundMoney(toNumber(serverCart.pricing.subtotal)),
4490
+ tax: roundMoney(toNumber(serverCart.pricing.tax_amount)),
4491
+ total: roundMoney(toNumber(serverCart.pricing.total_price)),
4492
+ currency: serverCart.pricing.currency || "USD"
4493
+ };
4494
+ }
4495
+ function buildStorageKey(storeKey) {
4496
+ return `${CART_STORAGE_PREFIX}:${storeKey}`;
4497
+ }
4498
+ function createEmptySnapshot(currency) {
4499
+ return {
4500
+ items: [],
4501
+ subtotal: 0,
4502
+ tax: 0,
4503
+ total: 0,
4504
+ currency,
4505
+ isLoading: true
4506
+ };
4507
+ }
4508
+ function createCartStore(params) {
4509
+ const { client, storeKey, locationId, isDemoMode, currency } = params;
4510
+ const storageKey = buildStorageKey(storeKey);
4511
+ const listeners = /* @__PURE__ */ new Set();
4512
+ let snapshot = createEmptySnapshot(currency);
4513
+ let initialized = false;
4514
+ let initializePromise = null;
4515
+ function emit() {
4516
+ listeners.forEach((listener) => {
4517
+ listener();
4518
+ });
4519
+ }
4520
+ function setSnapshot(nextSnapshot) {
4521
+ snapshot = nextSnapshot;
4522
+ persistSnapshot();
4523
+ emit();
4524
+ }
4525
+ function updateSnapshot(updater) {
4526
+ setSnapshot(updater(snapshot));
4527
+ }
4528
+ function persistSnapshot() {
4529
+ if (typeof window === "undefined" || !window.localStorage) {
4530
+ return;
4531
+ }
4532
+ try {
4533
+ window.localStorage.setItem(
4534
+ storageKey,
4535
+ JSON.stringify({
4536
+ items: snapshot.items,
4537
+ subtotal: snapshot.subtotal,
4538
+ tax: snapshot.tax,
4539
+ total: snapshot.total,
4540
+ currency: snapshot.currency
4541
+ })
4542
+ );
4543
+ } catch {
4544
+ }
4545
+ }
4546
+ function hydrateFromStorage() {
4547
+ if (typeof window === "undefined" || !window.localStorage) {
4548
+ return;
4549
+ }
4550
+ try {
4551
+ const raw = window.localStorage.getItem(storageKey);
4552
+ if (!raw) {
4553
+ return;
4554
+ }
4555
+ const parsed = JSON.parse(raw);
4556
+ if (!parsed || !Array.isArray(parsed.items)) {
4557
+ return;
4558
+ }
4559
+ snapshot = {
4560
+ items: parsed.items,
4561
+ subtotal: toNumber(parsed.subtotal),
4562
+ tax: toNumber(parsed.tax),
4563
+ total: toNumber(parsed.total),
4564
+ currency: typeof parsed.currency === "string" && parsed.currency ? parsed.currency : currency,
4565
+ isLoading: !isDemoMode
4566
+ };
4567
+ emit();
4568
+ } catch {
4569
+ }
4570
+ }
4571
+ async function sync() {
4572
+ if (isDemoMode) {
4573
+ updateSnapshot((current) => ({
4574
+ ...current,
4575
+ isLoading: false
4576
+ }));
4577
+ return;
4578
+ }
4579
+ updateSnapshot((current) => ({
4580
+ ...current,
4581
+ isLoading: true
4582
+ }));
4583
+ const result = await client.cart.get();
4584
+ if (!result.ok) {
4585
+ updateSnapshot((current) => ({
4586
+ ...current,
4587
+ isLoading: false
4588
+ }));
4589
+ throw result.error;
4590
+ }
4591
+ const next = mapServerCart(result.value);
4592
+ setSnapshot({
4593
+ ...next,
4594
+ isLoading: false
4595
+ });
4596
+ }
4597
+ async function maybeResolveQuoteId(product, quantity, options) {
4598
+ if (options.quoteId) {
4599
+ return options.quoteId;
4600
+ }
4601
+ const addOnOptionIds = normalizeOptionIds(options.addOnOptionIds);
4602
+ const requiresQuote = Boolean(
4603
+ options.variantId || addOnOptionIds && addOnOptionIds.length > 0 || options.bundleSelections && options.bundleSelections.length > 0 || options.compositeSelections && options.compositeSelections.length > 0
4604
+ );
4605
+ if (!requiresQuote || isDemoMode) {
4606
+ return void 0;
4607
+ }
4608
+ const quoteResult = await client.catalogue.fetchQuote({
4609
+ product_id: product.id,
4610
+ quantity,
4611
+ location_id: options.locationId || locationId || void 0,
4612
+ variant_id: options.variantId,
4613
+ add_on_option_ids: addOnOptionIds,
4614
+ bundle_selections: options.bundleSelections,
4615
+ composite_selections: options.compositeSelections
4616
+ });
4617
+ if (!quoteResult.ok) {
4618
+ throw quoteResult.error;
4619
+ }
4620
+ return quoteResult.value.quote_id;
4621
+ }
4622
+ async function addItem(product, quantity = 1, options = {}) {
4623
+ const resolvedQuantity = clampQuantity(quantity);
4624
+ const normalizedOptions = {
4625
+ ...options,
4626
+ locationId: options.locationId || locationId || void 0,
4627
+ addOnOptionIds: normalizeOptionIds(options.addOnOptionIds)
4628
+ };
4629
+ const maybeProductWithVariants = product;
4630
+ if (Array.isArray(maybeProductWithVariants.variants) && maybeProductWithVariants.variants.length > 0 && !normalizedOptions.variantId) {
4631
+ console.warn(
4632
+ "[Cimplify] addItem() called without variantId for a product that has variants. Default variant pricing will be used."
4633
+ );
4634
+ }
4635
+ const previousSnapshot = snapshot;
4636
+ const lineKey = buildLineKey(product.id, normalizedOptions);
4637
+ const optimisticItem = {
4638
+ id: `tmp_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`,
4639
+ product,
4640
+ quantity: resolvedQuantity,
4641
+ locationId: normalizedOptions.locationId,
4642
+ variantId: normalizedOptions.variantId,
4643
+ variant: normalizedOptions.variant,
4644
+ quoteId: normalizedOptions.quoteId,
4645
+ addOnOptionIds: normalizedOptions.addOnOptionIds,
4646
+ addOnOptions: normalizedOptions.addOnOptions,
4647
+ bundleSelections: normalizedOptions.bundleSelections,
4648
+ compositeSelections: normalizedOptions.compositeSelections,
4649
+ specialInstructions: normalizedOptions.specialInstructions
4650
+ };
4651
+ updateSnapshot((current) => {
4652
+ const existingIndex = current.items.findIndex((item) => {
4653
+ const itemKey = buildLineKey(item.product.id, {
4654
+ locationId: item.locationId,
4655
+ variantId: item.variantId,
4656
+ quoteId: item.quoteId,
4657
+ addOnOptionIds: item.addOnOptionIds,
4658
+ bundleSelections: item.bundleSelections,
4659
+ compositeSelections: item.compositeSelections
4660
+ });
4661
+ return itemKey === lineKey;
4662
+ });
4663
+ const nextItems = [...current.items];
4664
+ if (existingIndex >= 0) {
4665
+ const existing = nextItems[existingIndex];
4666
+ nextItems[existingIndex] = {
4667
+ ...existing,
4668
+ quantity: existing.quantity + resolvedQuantity
4669
+ };
4670
+ } else {
4671
+ nextItems.push(optimisticItem);
4672
+ }
4673
+ const summary = calculateSummary(nextItems);
4674
+ return {
4675
+ ...current,
4676
+ items: nextItems,
4677
+ subtotal: summary.subtotal,
4678
+ tax: summary.tax,
4679
+ total: summary.total
4680
+ };
4681
+ });
4682
+ if (isDemoMode) {
4683
+ return;
4684
+ }
4685
+ try {
4686
+ const quoteId = await maybeResolveQuoteId(product, resolvedQuantity, normalizedOptions);
4687
+ const result = await client.cart.addItem({
4688
+ item_id: product.id,
4689
+ quantity: resolvedQuantity,
4690
+ variant_id: normalizedOptions.variantId,
4691
+ quote_id: quoteId,
4692
+ add_on_options: normalizedOptions.addOnOptionIds,
4693
+ special_instructions: normalizedOptions.specialInstructions,
4694
+ bundle_selections: normalizedOptions.bundleSelections,
4695
+ composite_selections: normalizedOptions.compositeSelections
4696
+ });
4697
+ if (!result.ok) {
4698
+ throw result.error;
4699
+ }
4700
+ await sync();
4701
+ } catch (error) {
4702
+ snapshot = previousSnapshot;
4703
+ persistSnapshot();
4704
+ emit();
4705
+ throw error;
4706
+ }
4707
+ }
4708
+ async function removeItem(itemId) {
4709
+ const previousSnapshot = snapshot;
4710
+ updateSnapshot((current) => {
4711
+ const nextItems = current.items.filter((item) => item.id !== itemId);
4712
+ const summary = calculateSummary(nextItems);
4713
+ return {
4714
+ ...current,
4715
+ items: nextItems,
4716
+ subtotal: summary.subtotal,
4717
+ tax: summary.tax,
4718
+ total: summary.total
4719
+ };
4720
+ });
4721
+ if (isDemoMode) {
4722
+ return;
4723
+ }
4724
+ try {
4725
+ const result = await client.cart.removeItem(itemId);
4726
+ if (!result.ok) {
4727
+ throw result.error;
4728
+ }
4729
+ await sync();
4730
+ } catch (error) {
4731
+ snapshot = previousSnapshot;
4732
+ persistSnapshot();
4733
+ emit();
4734
+ throw error;
4735
+ }
4736
+ }
4737
+ async function updateQuantity(itemId, quantity) {
4738
+ if (quantity <= 0) {
4739
+ await removeItem(itemId);
4740
+ return;
4741
+ }
4742
+ const resolvedQuantity = clampQuantity(quantity);
4743
+ const previousSnapshot = snapshot;
4744
+ updateSnapshot((current) => {
4745
+ const nextItems = current.items.map(
4746
+ (item) => item.id === itemId ? {
4747
+ ...item,
4748
+ quantity: resolvedQuantity
4749
+ } : item
4750
+ );
4751
+ const summary = calculateSummary(nextItems);
4752
+ return {
4753
+ ...current,
4754
+ items: nextItems,
4755
+ subtotal: summary.subtotal,
4756
+ tax: summary.tax,
4757
+ total: summary.total
4758
+ };
4759
+ });
4760
+ if (isDemoMode) {
4761
+ return;
4762
+ }
4763
+ try {
4764
+ const result = await client.cart.updateQuantity(itemId, resolvedQuantity);
4765
+ if (!result.ok) {
4766
+ throw result.error;
4767
+ }
4768
+ await sync();
4769
+ } catch (error) {
4770
+ snapshot = previousSnapshot;
4771
+ persistSnapshot();
4772
+ emit();
4773
+ throw error;
4774
+ }
4775
+ }
4776
+ async function clearCart() {
4777
+ const previousSnapshot = snapshot;
4778
+ updateSnapshot((current) => ({
4779
+ ...current,
4780
+ items: [],
4781
+ subtotal: 0,
4782
+ tax: 0,
4783
+ total: 0
4784
+ }));
4785
+ if (isDemoMode) {
4786
+ return;
4787
+ }
4788
+ try {
4789
+ const result = await client.cart.clear();
4790
+ if (!result.ok) {
4791
+ throw result.error;
4792
+ }
4793
+ await sync();
4794
+ } catch (error) {
4795
+ snapshot = previousSnapshot;
4796
+ persistSnapshot();
4797
+ emit();
4798
+ throw error;
4799
+ }
4800
+ }
4801
+ async function initialize() {
4802
+ if (initialized) {
4803
+ if (initializePromise) {
4804
+ await initializePromise;
4805
+ }
4806
+ return;
4807
+ }
4808
+ initialized = true;
4809
+ hydrateFromStorage();
4810
+ if (isDemoMode) {
4811
+ updateSnapshot((current) => ({
4812
+ ...current,
4813
+ isLoading: false
4814
+ }));
4815
+ return;
4816
+ }
4817
+ initializePromise = sync().catch(() => {
4818
+ updateSnapshot((current) => ({
4819
+ ...current,
4820
+ isLoading: false
4821
+ }));
4822
+ });
4823
+ await initializePromise;
4824
+ initializePromise = null;
4825
+ }
4826
+ return {
4827
+ subscribe(listener) {
4828
+ listeners.add(listener);
4829
+ return () => {
4830
+ listeners.delete(listener);
4831
+ };
4832
+ },
4833
+ getSnapshot() {
4834
+ return snapshot;
4835
+ },
4836
+ initialize,
4837
+ addItem,
4838
+ removeItem,
4839
+ updateQuantity,
4840
+ clearCart,
4841
+ sync
4842
+ };
4843
+ }
4844
+ function getStoreKey(client, locationId, isDemoMode) {
4845
+ return [
4846
+ client.getPublicKey() || "__demo__",
4847
+ locationId || "__no_location__",
4848
+ isDemoMode ? "demo" : "live"
4849
+ ].join(":");
4850
+ }
4851
+ function getOrCreateStore(params) {
4852
+ const { client, locationId, isDemoMode, currency } = params;
4853
+ const storeKey = getStoreKey(client, locationId, isDemoMode);
4854
+ const existing = cartStores.get(storeKey);
4855
+ if (existing) {
4856
+ return existing;
4857
+ }
4858
+ const created = createCartStore({
4859
+ client,
4860
+ storeKey,
4861
+ locationId,
4862
+ isDemoMode,
4863
+ currency
4864
+ });
4865
+ cartStores.set(storeKey, created);
4866
+ return created;
4867
+ }
4868
+ function useCart(options = {}) {
4869
+ const context = useOptionalCimplify();
4870
+ const client = options.client ?? context?.client;
4871
+ if (!client) {
4872
+ throw new Error("useCart must be used within CimplifyProvider or passed { client }.");
4873
+ }
4874
+ const locationId = options.locationId ?? client.getLocationId();
4875
+ const isDemoMode = options.demoMode ?? context?.isDemoMode ?? client.getPublicKey().trim().length === 0;
4876
+ const currency = options.currency ?? context?.currency ?? "USD";
4877
+ const store = useMemo(
4878
+ () => getOrCreateStore({
4879
+ client,
4880
+ locationId,
4881
+ isDemoMode,
4882
+ currency
4883
+ }),
4884
+ [client, currency, isDemoMode, locationId]
4885
+ );
4886
+ const snapshot = useSyncExternalStore(
4887
+ store.subscribe,
4888
+ store.getSnapshot,
4889
+ store.getSnapshot
4890
+ );
4891
+ useEffect(() => {
4892
+ void store.initialize();
4893
+ }, [store]);
4894
+ const addItem = useCallback(
4895
+ async (product, quantity, addOptions) => {
4896
+ await store.addItem(product, quantity, addOptions);
4897
+ },
4898
+ [store]
4899
+ );
4900
+ const removeItem = useCallback(
4901
+ async (itemId) => {
4902
+ await store.removeItem(itemId);
4903
+ },
4904
+ [store]
4905
+ );
4906
+ const updateQuantity = useCallback(
4907
+ async (itemId, quantity) => {
4908
+ await store.updateQuantity(itemId, quantity);
4909
+ },
4910
+ [store]
4911
+ );
4912
+ const clearCart = useCallback(async () => {
4913
+ await store.clearCart();
4914
+ }, [store]);
4915
+ const sync = useCallback(async () => {
4916
+ try {
4917
+ await store.sync();
4918
+ } catch (syncError) {
4919
+ throw syncError;
4920
+ }
4921
+ }, [store]);
4922
+ const itemCount = useMemo(
4923
+ () => snapshot.items.reduce((sum, item) => sum + item.quantity, 0),
4924
+ [snapshot.items]
4925
+ );
4926
+ return {
4927
+ items: snapshot.items,
4928
+ itemCount,
4929
+ subtotal: snapshot.subtotal,
4930
+ tax: snapshot.tax,
4931
+ total: snapshot.total,
4932
+ currency: snapshot.currency,
4933
+ isEmpty: itemCount === 0,
4934
+ isLoading: snapshot.isLoading,
4935
+ addItem,
4936
+ removeItem,
4937
+ updateQuantity,
4938
+ clearCart,
4939
+ sync
4940
+ };
4941
+ }
4942
+ var orderCache = /* @__PURE__ */ new Map();
4943
+ var orderInflight = /* @__PURE__ */ new Map();
4944
+ function buildOrderCacheKey(client, orderId) {
4945
+ return `${client.getPublicKey() || "__demo__"}:${orderId}`;
4946
+ }
4947
+ function useOrder(orderId, options = {}) {
4948
+ const context = useOptionalCimplify();
4949
+ const client = options.client ?? context?.client;
4950
+ if (!client) {
4951
+ throw new Error("useOrder must be used within CimplifyProvider or passed { client }.");
4952
+ }
4953
+ const normalizedOrderId = useMemo(() => (orderId || "").trim(), [orderId]);
4954
+ const enabled = options.enabled ?? true;
4955
+ const poll = options.poll ?? false;
4956
+ const pollInterval = options.pollInterval ?? 5e3;
4957
+ const requestIdRef = useRef(0);
4958
+ const cacheKey = useMemo(
4959
+ () => buildOrderCacheKey(client, normalizedOrderId),
4960
+ [client, normalizedOrderId]
4961
+ );
4962
+ const cached = orderCache.get(cacheKey);
4963
+ const [order, setOrder] = useState(cached?.order ?? null);
4964
+ const [isLoading, setIsLoading] = useState(
4965
+ enabled && normalizedOrderId.length > 0 && !cached
4966
+ );
4967
+ const [error, setError] = useState(null);
4968
+ const load = useCallback(
4969
+ async (force = false) => {
4970
+ if (!enabled || normalizedOrderId.length === 0) {
4971
+ setOrder(null);
4972
+ setIsLoading(false);
4973
+ return;
4974
+ }
4975
+ const nextRequestId = ++requestIdRef.current;
4976
+ setError(null);
4977
+ if (!force) {
4978
+ const cacheEntry = orderCache.get(cacheKey);
4979
+ if (cacheEntry) {
4980
+ setOrder(cacheEntry.order);
4981
+ setIsLoading(false);
4982
+ return;
4983
+ }
4984
+ }
4985
+ setIsLoading(true);
4986
+ try {
4987
+ const existing = orderInflight.get(cacheKey);
4988
+ const promise = existing ?? (async () => {
4989
+ const result = await client.orders.get(normalizedOrderId);
4990
+ if (!result.ok) {
4991
+ throw result.error;
4992
+ }
4993
+ return result.value;
4994
+ })();
4995
+ if (!existing) {
4996
+ orderInflight.set(
4997
+ cacheKey,
4998
+ promise.finally(() => {
4999
+ orderInflight.delete(cacheKey);
5000
+ })
5001
+ );
5002
+ }
5003
+ const value = await promise;
5004
+ orderCache.set(cacheKey, { order: value });
5005
+ if (nextRequestId === requestIdRef.current) {
5006
+ setOrder(value);
5007
+ setError(null);
5008
+ }
5009
+ } catch (loadError) {
5010
+ if (nextRequestId === requestIdRef.current) {
5011
+ setError(loadError);
5012
+ }
5013
+ } finally {
5014
+ if (nextRequestId === requestIdRef.current) {
5015
+ setIsLoading(false);
5016
+ }
5017
+ }
5018
+ },
5019
+ [cacheKey, client, enabled, normalizedOrderId]
5020
+ );
5021
+ useEffect(() => {
5022
+ void load(false);
5023
+ }, [load]);
5024
+ useEffect(() => {
5025
+ if (!poll || !enabled || normalizedOrderId.length === 0) {
5026
+ return;
5027
+ }
5028
+ const timer = window.setInterval(() => {
5029
+ void load(true);
5030
+ }, Math.max(1e3, pollInterval));
5031
+ return () => {
5032
+ window.clearInterval(timer);
5033
+ };
5034
+ }, [enabled, load, normalizedOrderId.length, poll, pollInterval]);
5035
+ const refetch = useCallback(async () => {
5036
+ orderCache.delete(cacheKey);
5037
+ await load(true);
5038
+ }, [cacheKey, load]);
5039
+ return { order, isLoading, error, refetch };
5040
+ }
5041
+ var LOCATION_STORAGE_KEY2 = "cimplify_location_id";
5042
+ function readStoredLocationId() {
5043
+ if (typeof window === "undefined" || !window.localStorage) {
5044
+ return null;
5045
+ }
5046
+ const value = window.localStorage.getItem(LOCATION_STORAGE_KEY2);
5047
+ if (!value) {
5048
+ return null;
5049
+ }
5050
+ const normalized = value.trim();
5051
+ return normalized.length > 0 ? normalized : null;
5052
+ }
5053
+ function writeStoredLocationId(locationId) {
5054
+ if (typeof window === "undefined" || !window.localStorage) {
5055
+ return;
5056
+ }
5057
+ if (!locationId) {
5058
+ window.localStorage.removeItem(LOCATION_STORAGE_KEY2);
5059
+ return;
5060
+ }
5061
+ window.localStorage.setItem(LOCATION_STORAGE_KEY2, locationId);
5062
+ }
5063
+ function resolveLocation(locations, preferredId) {
5064
+ if (locations.length === 0) {
5065
+ return null;
5066
+ }
5067
+ if (preferredId) {
5068
+ const found = locations.find((location) => location.id === preferredId);
5069
+ if (found) {
5070
+ return found;
5071
+ }
5072
+ }
5073
+ return locations[0];
5074
+ }
5075
+ function useLocations(options = {}) {
5076
+ const context = useOptionalCimplify();
5077
+ if (context && (!options.client || context.client === options.client)) {
5078
+ return {
5079
+ locations: context.locations,
5080
+ currentLocation: context.currentLocation,
5081
+ setCurrentLocation: context.setCurrentLocation,
5082
+ isLoading: !context.isReady
5083
+ };
5084
+ }
5085
+ const client = options.client;
5086
+ const [locations, setLocations] = useState([]);
5087
+ const [currentLocation, setCurrentLocationState] = useState(null);
5088
+ const [isLoading, setIsLoading] = useState(true);
5089
+ const setCurrentLocation = useCallback(
5090
+ (location) => {
5091
+ setCurrentLocationState(location);
5092
+ if (client) {
5093
+ client.setLocationId(location.id);
5094
+ }
5095
+ writeStoredLocationId(location.id);
5096
+ },
5097
+ [client]
5098
+ );
5099
+ useEffect(() => {
5100
+ if (!client) {
5101
+ setLocations([]);
5102
+ setCurrentLocationState(null);
5103
+ setIsLoading(false);
5104
+ return;
5105
+ }
5106
+ const activeClient = client;
5107
+ let cancelled = false;
5108
+ async function loadLocations() {
5109
+ setIsLoading(true);
5110
+ const result = await activeClient.business.getLocations();
5111
+ if (cancelled) {
5112
+ return;
5113
+ }
5114
+ if (!result.ok) {
5115
+ setLocations([]);
5116
+ setCurrentLocationState(null);
5117
+ setIsLoading(false);
5118
+ return;
5119
+ }
5120
+ const nextLocations = result.value;
5121
+ const preferredId = activeClient.getLocationId() ?? readStoredLocationId();
5122
+ const resolved = resolveLocation(nextLocations, preferredId);
5123
+ setLocations(nextLocations);
5124
+ setCurrentLocationState(resolved);
5125
+ activeClient.setLocationId(resolved?.id || null);
5126
+ writeStoredLocationId(resolved?.id || null);
5127
+ setIsLoading(false);
5128
+ }
5129
+ void loadLocations();
5130
+ return () => {
5131
+ cancelled = true;
5132
+ };
5133
+ }, [client]);
5134
+ return {
5135
+ locations,
5136
+ currentLocation,
5137
+ setCurrentLocation,
5138
+ isLoading
5139
+ };
5140
+ }
5141
+ var collectionsCache = /* @__PURE__ */ new Map();
5142
+ var collectionsInflight = /* @__PURE__ */ new Map();
5143
+ function buildCollectionsCacheKey(client, locationId) {
5144
+ return JSON.stringify({
5145
+ key: client.getPublicKey(),
5146
+ location_id: locationId || "__none__"
5147
+ });
5148
+ }
5149
+ function useCollections(options = {}) {
5150
+ const context = useOptionalCimplify();
5151
+ const client = options.client ?? context?.client;
5152
+ if (!client) {
5153
+ throw new Error("useCollections must be used within CimplifyProvider or passed { client }.");
5154
+ }
5155
+ const enabled = options.enabled ?? true;
5156
+ const locationId = client.getLocationId();
5157
+ const previousLocationIdRef = useRef(locationId);
5158
+ const requestIdRef = useRef(0);
5159
+ const cacheKey = useMemo(
5160
+ () => buildCollectionsCacheKey(client, locationId),
5161
+ [client, locationId]
5162
+ );
5163
+ const cached = collectionsCache.get(cacheKey);
5164
+ const [collections, setCollections] = useState(cached?.collections ?? []);
5165
+ const [isLoading, setIsLoading] = useState(enabled && !cached);
5166
+ const [error, setError] = useState(null);
5167
+ useEffect(() => {
5168
+ if (previousLocationIdRef.current !== locationId) {
5169
+ collectionsCache.clear();
5170
+ collectionsInflight.clear();
5171
+ previousLocationIdRef.current = locationId;
5172
+ }
5173
+ }, [locationId]);
5174
+ const load = useCallback(
5175
+ async (force = false) => {
5176
+ if (!enabled) {
5177
+ setIsLoading(false);
5178
+ return;
5179
+ }
5180
+ const nextRequestId = ++requestIdRef.current;
5181
+ setError(null);
5182
+ if (!force) {
5183
+ const cacheEntry = collectionsCache.get(cacheKey);
5184
+ if (cacheEntry) {
5185
+ setCollections(cacheEntry.collections);
5186
+ setIsLoading(false);
5187
+ return;
5188
+ }
5189
+ }
5190
+ setIsLoading(true);
5191
+ try {
5192
+ const existing = collectionsInflight.get(cacheKey);
5193
+ const promise = existing ?? (async () => {
5194
+ const result = await client.catalogue.getCollections();
5195
+ if (!result.ok) {
5196
+ throw result.error;
5197
+ }
5198
+ return result.value;
5199
+ })();
5200
+ if (!existing) {
5201
+ collectionsInflight.set(cacheKey, promise);
5202
+ promise.finally(() => {
5203
+ collectionsInflight.delete(cacheKey);
5204
+ }).catch(() => void 0);
5205
+ }
5206
+ const value = await promise;
5207
+ collectionsCache.set(cacheKey, { collections: value });
5208
+ if (nextRequestId === requestIdRef.current) {
5209
+ setCollections(value);
5210
+ setError(null);
5211
+ }
5212
+ } catch (loadError) {
5213
+ if (nextRequestId === requestIdRef.current) {
5214
+ setError(loadError);
5215
+ }
5216
+ } finally {
5217
+ if (nextRequestId === requestIdRef.current) {
5218
+ setIsLoading(false);
5219
+ }
5220
+ }
5221
+ },
5222
+ [cacheKey, client, enabled]
5223
+ );
5224
+ useEffect(() => {
5225
+ void load(false);
5226
+ }, [load]);
5227
+ const refetch = useCallback(async () => {
5228
+ collectionsCache.delete(cacheKey);
5229
+ await load(true);
5230
+ }, [cacheKey, load]);
5231
+ return { collections, isLoading, error, refetch };
5232
+ }
5233
+ var collectionCache = /* @__PURE__ */ new Map();
5234
+ var collectionInflight = /* @__PURE__ */ new Map();
5235
+ function isLikelySlug2(value) {
5236
+ return /^[a-z0-9-]+$/.test(value);
5237
+ }
5238
+ function buildCollectionCacheKey(client, locationId, idOrSlug) {
5239
+ return JSON.stringify({
5240
+ key: client.getPublicKey(),
5241
+ location_id: locationId || "__none__",
5242
+ collection: idOrSlug
5243
+ });
5244
+ }
5245
+ function useCollection(idOrSlug, options = {}) {
5246
+ const context = useOptionalCimplify();
5247
+ const client = options.client ?? context?.client;
5248
+ if (!client) {
5249
+ throw new Error("useCollection must be used within CimplifyProvider or passed { client }.");
5250
+ }
5251
+ const enabled = options.enabled ?? true;
5252
+ const locationId = client.getLocationId();
5253
+ const previousLocationIdRef = useRef(locationId);
5254
+ const requestIdRef = useRef(0);
5255
+ const normalizedIdOrSlug = useMemo(() => (idOrSlug || "").trim(), [idOrSlug]);
5256
+ const cacheKey = useMemo(
5257
+ () => buildCollectionCacheKey(client, locationId, normalizedIdOrSlug),
5258
+ [client, locationId, normalizedIdOrSlug]
5259
+ );
5260
+ const cached = collectionCache.get(cacheKey);
5261
+ const [collection, setCollection] = useState(cached?.collection ?? null);
5262
+ const [products, setProducts] = useState(cached?.products ?? []);
5263
+ const [isLoading, setIsLoading] = useState(
5264
+ enabled && normalizedIdOrSlug.length > 0 && !cached
5265
+ );
5266
+ const [error, setError] = useState(null);
5267
+ useEffect(() => {
5268
+ if (previousLocationIdRef.current !== locationId) {
5269
+ collectionCache.clear();
5270
+ collectionInflight.clear();
5271
+ previousLocationIdRef.current = locationId;
5272
+ }
5273
+ }, [locationId]);
5274
+ const load = useCallback(
5275
+ async (force = false) => {
5276
+ if (!enabled || normalizedIdOrSlug.length === 0) {
5277
+ setCollection(null);
5278
+ setProducts([]);
5279
+ setIsLoading(false);
5280
+ return;
5281
+ }
5282
+ const nextRequestId = ++requestIdRef.current;
5283
+ setError(null);
5284
+ if (!force) {
5285
+ const cacheEntry = collectionCache.get(cacheKey);
5286
+ if (cacheEntry) {
5287
+ setCollection(cacheEntry.collection);
5288
+ setProducts(cacheEntry.products);
5289
+ setIsLoading(false);
5290
+ return;
5291
+ }
5292
+ }
5293
+ setIsLoading(true);
5294
+ try {
5295
+ const existing = collectionInflight.get(cacheKey);
5296
+ const promise = existing ?? (async () => {
5297
+ const collectionResult = isLikelySlug2(normalizedIdOrSlug) ? await client.catalogue.getCollectionBySlug(normalizedIdOrSlug) : await client.catalogue.getCollection(normalizedIdOrSlug);
5298
+ if (!collectionResult.ok) {
5299
+ throw collectionResult.error;
5300
+ }
5301
+ const productsResult = await client.catalogue.getCollectionProducts(
5302
+ collectionResult.value.id
5303
+ );
5304
+ if (!productsResult.ok) {
5305
+ throw productsResult.error;
5306
+ }
5307
+ return {
5308
+ collection: collectionResult.value,
5309
+ products: productsResult.value
5310
+ };
5311
+ })();
5312
+ if (!existing) {
5313
+ collectionInflight.set(cacheKey, promise);
5314
+ promise.finally(() => {
5315
+ collectionInflight.delete(cacheKey);
5316
+ }).catch(() => void 0);
5317
+ }
5318
+ const value = await promise;
5319
+ collectionCache.set(cacheKey, value);
5320
+ if (nextRequestId === requestIdRef.current) {
5321
+ setCollection(value.collection);
5322
+ setProducts(value.products);
5323
+ setError(null);
5324
+ }
5325
+ } catch (loadError) {
5326
+ if (nextRequestId === requestIdRef.current) {
5327
+ setError(loadError);
5328
+ }
5329
+ } finally {
5330
+ if (nextRequestId === requestIdRef.current) {
5331
+ setIsLoading(false);
5332
+ }
5333
+ }
5334
+ },
5335
+ [cacheKey, client, enabled, normalizedIdOrSlug]
5336
+ );
5337
+ useEffect(() => {
5338
+ void load(false);
5339
+ }, [load]);
5340
+ const refetch = useCallback(async () => {
5341
+ collectionCache.delete(cacheKey);
5342
+ await load(true);
5343
+ }, [cacheKey, load]);
5344
+ return { collection, products, isLoading, error, refetch };
5345
+ }
5346
+ var bundleCache = /* @__PURE__ */ new Map();
5347
+ var bundleInflight = /* @__PURE__ */ new Map();
5348
+ function isLikelySlug3(value) {
5349
+ return /^[a-z0-9-]+$/.test(value);
5350
+ }
5351
+ function buildBundleCacheKey(client, locationId, idOrSlug) {
5352
+ return JSON.stringify({
5353
+ key: client.getPublicKey(),
5354
+ location_id: locationId || "__none__",
5355
+ bundle: idOrSlug
5356
+ });
5357
+ }
5358
+ function useBundle(idOrSlug, options = {}) {
5359
+ const context = useOptionalCimplify();
5360
+ const client = options.client ?? context?.client;
5361
+ if (!client) {
5362
+ throw new Error("useBundle must be used within CimplifyProvider or passed { client }.");
5363
+ }
5364
+ const enabled = options.enabled ?? true;
5365
+ const locationId = client.getLocationId();
5366
+ const previousLocationIdRef = useRef(locationId);
5367
+ const requestIdRef = useRef(0);
5368
+ const normalizedIdOrSlug = useMemo(() => (idOrSlug || "").trim(), [idOrSlug]);
5369
+ const cacheKey = useMemo(
5370
+ () => buildBundleCacheKey(client, locationId, normalizedIdOrSlug),
5371
+ [client, locationId, normalizedIdOrSlug]
5372
+ );
5373
+ const cached = bundleCache.get(cacheKey);
5374
+ const [bundle, setBundle] = useState(cached?.bundle ?? null);
5375
+ const [isLoading, setIsLoading] = useState(
5376
+ enabled && normalizedIdOrSlug.length > 0 && !cached
5377
+ );
5378
+ const [error, setError] = useState(null);
5379
+ useEffect(() => {
5380
+ if (previousLocationIdRef.current !== locationId) {
5381
+ bundleCache.clear();
5382
+ bundleInflight.clear();
5383
+ previousLocationIdRef.current = locationId;
5384
+ }
5385
+ }, [locationId]);
5386
+ const load = useCallback(
5387
+ async (force = false) => {
5388
+ if (!enabled || normalizedIdOrSlug.length === 0) {
5389
+ setBundle(null);
5390
+ setIsLoading(false);
5391
+ return;
5392
+ }
5393
+ const nextRequestId = ++requestIdRef.current;
5394
+ setError(null);
5395
+ if (!force) {
5396
+ const cacheEntry = bundleCache.get(cacheKey);
5397
+ if (cacheEntry) {
5398
+ setBundle(cacheEntry.bundle);
5399
+ setIsLoading(false);
5400
+ return;
5401
+ }
5402
+ }
5403
+ setIsLoading(true);
5404
+ try {
5405
+ const existing = bundleInflight.get(cacheKey);
5406
+ const promise = existing ?? (async () => {
5407
+ const result = isLikelySlug3(normalizedIdOrSlug) ? await client.catalogue.getBundleBySlug(normalizedIdOrSlug) : await client.catalogue.getBundle(normalizedIdOrSlug);
5408
+ if (!result.ok) {
5409
+ throw result.error;
5410
+ }
5411
+ return result.value;
5412
+ })();
5413
+ if (!existing) {
5414
+ bundleInflight.set(cacheKey, promise);
5415
+ promise.finally(() => {
5416
+ bundleInflight.delete(cacheKey);
5417
+ }).catch(() => void 0);
5418
+ }
5419
+ const value = await promise;
5420
+ bundleCache.set(cacheKey, { bundle: value });
5421
+ if (nextRequestId === requestIdRef.current) {
5422
+ setBundle(value);
5423
+ setError(null);
5424
+ }
5425
+ } catch (loadError) {
5426
+ if (nextRequestId === requestIdRef.current) {
5427
+ setError(loadError);
5428
+ }
5429
+ } finally {
5430
+ if (nextRequestId === requestIdRef.current) {
5431
+ setIsLoading(false);
5432
+ }
5433
+ }
5434
+ },
5435
+ [cacheKey, client, enabled, normalizedIdOrSlug]
5436
+ );
5437
+ useEffect(() => {
5438
+ void load(false);
5439
+ }, [load]);
5440
+ const refetch = useCallback(async () => {
5441
+ bundleCache.delete(cacheKey);
5442
+ await load(true);
5443
+ }, [cacheKey, load]);
5444
+ return { bundle, isLoading, error, refetch };
5445
+ }
5446
+ var compositeCache = /* @__PURE__ */ new Map();
5447
+ var compositeInflight = /* @__PURE__ */ new Map();
5448
+ function shouldFetchByProductId(idOrProductId, byProductId) {
5449
+ if (typeof byProductId === "boolean") {
5450
+ return byProductId;
5451
+ }
5452
+ return idOrProductId.startsWith("prod_");
5453
+ }
5454
+ function buildCompositeCacheKey(client, locationId, idOrProductId, byProductId) {
5455
+ return JSON.stringify({
5456
+ key: client.getPublicKey(),
5457
+ location_id: locationId || "__none__",
5458
+ composite: idOrProductId,
5459
+ by_product_id: byProductId
5460
+ });
5461
+ }
5462
+ function useComposite(idOrProductId, options = {}) {
5463
+ const context = useOptionalCimplify();
5464
+ const client = options.client ?? context?.client;
5465
+ if (!client) {
5466
+ throw new Error("useComposite must be used within CimplifyProvider or passed { client }.");
5467
+ }
5468
+ const enabled = options.enabled ?? true;
5469
+ const locationId = client.getLocationId();
5470
+ const previousLocationIdRef = useRef(locationId);
5471
+ const requestIdRef = useRef(0);
5472
+ const priceRequestIdRef = useRef(0);
5473
+ const normalizedIdOrProductId = useMemo(
5474
+ () => (idOrProductId || "").trim(),
5475
+ [idOrProductId]
5476
+ );
5477
+ const byProductId = useMemo(
5478
+ () => shouldFetchByProductId(normalizedIdOrProductId, options.byProductId),
5479
+ [normalizedIdOrProductId, options.byProductId]
5480
+ );
5481
+ const cacheKey = useMemo(
5482
+ () => buildCompositeCacheKey(client, locationId, normalizedIdOrProductId, byProductId),
5483
+ [byProductId, client, locationId, normalizedIdOrProductId]
5484
+ );
5485
+ const cached = compositeCache.get(cacheKey);
5486
+ const [composite, setComposite] = useState(cached?.composite ?? null);
5487
+ const [isLoading, setIsLoading] = useState(
5488
+ enabled && normalizedIdOrProductId.length > 0 && !cached
5489
+ );
5490
+ const [error, setError] = useState(null);
5491
+ const [priceResult, setPriceResult] = useState(null);
5492
+ const [isPriceLoading, setIsPriceLoading] = useState(false);
5493
+ useEffect(() => {
5494
+ if (previousLocationIdRef.current !== locationId) {
5495
+ compositeCache.clear();
5496
+ compositeInflight.clear();
5497
+ previousLocationIdRef.current = locationId;
5498
+ }
5499
+ }, [locationId]);
5500
+ const load = useCallback(
5501
+ async (force = false) => {
5502
+ if (!enabled || normalizedIdOrProductId.length === 0) {
5503
+ setComposite(null);
5504
+ setPriceResult(null);
5505
+ setIsLoading(false);
5506
+ return;
5507
+ }
5508
+ const nextRequestId = ++requestIdRef.current;
5509
+ setError(null);
5510
+ if (!force) {
5511
+ const cacheEntry = compositeCache.get(cacheKey);
5512
+ if (cacheEntry) {
5513
+ setComposite(cacheEntry.composite);
5514
+ setIsLoading(false);
5515
+ return;
5516
+ }
5517
+ }
5518
+ setIsLoading(true);
5519
+ try {
5520
+ const existing = compositeInflight.get(cacheKey);
5521
+ const promise = existing ?? (async () => {
5522
+ const result = byProductId ? await client.catalogue.getCompositeByProductId(normalizedIdOrProductId) : await client.catalogue.getComposite(normalizedIdOrProductId);
5523
+ if (!result.ok) {
5524
+ throw result.error;
5525
+ }
5526
+ return result.value;
5527
+ })();
5528
+ if (!existing) {
5529
+ compositeInflight.set(cacheKey, promise);
5530
+ promise.finally(() => {
5531
+ compositeInflight.delete(cacheKey);
5532
+ }).catch(() => void 0);
5533
+ }
5534
+ const value = await promise;
5535
+ compositeCache.set(cacheKey, { composite: value });
5536
+ if (nextRequestId === requestIdRef.current) {
5537
+ setComposite(value);
5538
+ setPriceResult(null);
5539
+ setError(null);
5540
+ }
5541
+ } catch (loadError) {
5542
+ if (nextRequestId === requestIdRef.current) {
5543
+ setError(loadError);
5544
+ }
5545
+ } finally {
5546
+ if (nextRequestId === requestIdRef.current) {
5547
+ setIsLoading(false);
5548
+ }
5549
+ }
5550
+ },
5551
+ [byProductId, cacheKey, client, enabled, normalizedIdOrProductId]
5552
+ );
5553
+ useEffect(() => {
5554
+ void load(false);
5555
+ }, [load]);
5556
+ const calculatePrice = useCallback(
5557
+ async (selections, overrideLocationId) => {
5558
+ if (!composite) {
5559
+ return null;
5560
+ }
5561
+ const nextRequestId = ++priceRequestIdRef.current;
5562
+ setIsPriceLoading(true);
5563
+ try {
5564
+ const result = await client.catalogue.calculateCompositePrice(
5565
+ composite.id,
5566
+ selections,
5567
+ overrideLocationId
5568
+ );
5569
+ if (!result.ok) {
5570
+ throw result.error;
5571
+ }
5572
+ if (nextRequestId === priceRequestIdRef.current) {
5573
+ setPriceResult(result.value);
5574
+ setError(null);
5575
+ }
5576
+ return result.value;
5577
+ } catch (loadError) {
5578
+ if (nextRequestId === priceRequestIdRef.current) {
5579
+ setError(loadError);
5580
+ }
5581
+ return null;
5582
+ } finally {
5583
+ if (nextRequestId === priceRequestIdRef.current) {
5584
+ setIsPriceLoading(false);
5585
+ }
5586
+ }
5587
+ },
5588
+ [client, composite]
5589
+ );
5590
+ const refetch = useCallback(async () => {
5591
+ compositeCache.delete(cacheKey);
5592
+ await load(true);
5593
+ }, [cacheKey, load]);
5594
+ return { composite, isLoading, error, refetch, calculatePrice, priceResult, isPriceLoading };
5595
+ }
5596
+ function useSearch(options = {}) {
5597
+ const context = useOptionalCimplify();
5598
+ const client = options.client ?? context?.client;
5599
+ if (!client) {
5600
+ throw new Error("useSearch must be used within CimplifyProvider or passed { client }.");
5601
+ }
5602
+ const minLength = Math.max(0, options.minLength ?? 2);
5603
+ const debounceMs = Math.max(0, options.debounceMs ?? 300);
5604
+ const limit = Math.max(1, options.limit ?? 20);
5605
+ const [query, setQueryState] = useState("");
5606
+ const [results, setResults] = useState([]);
5607
+ const [isLoading, setIsLoading] = useState(false);
5608
+ const [error, setError] = useState(null);
5609
+ const requestIdRef = useRef(0);
5610
+ const timerRef = useRef(null);
5611
+ useEffect(() => {
5612
+ if (timerRef.current) {
5613
+ clearTimeout(timerRef.current);
5614
+ timerRef.current = null;
5615
+ }
5616
+ const trimmedQuery = query.trim();
5617
+ if (trimmedQuery.length < minLength) {
5618
+ setResults([]);
5619
+ setError(null);
5620
+ setIsLoading(false);
5621
+ return;
5622
+ }
5623
+ const nextRequestId = ++requestIdRef.current;
5624
+ setError(null);
5625
+ setIsLoading(true);
5626
+ timerRef.current = setTimeout(() => {
5627
+ void (async () => {
5628
+ try {
5629
+ const result = await client.catalogue.searchProducts(trimmedQuery, {
5630
+ limit,
5631
+ category: options.category
5632
+ });
5633
+ if (!result.ok) {
5634
+ throw result.error;
5635
+ }
5636
+ if (nextRequestId === requestIdRef.current) {
5637
+ setResults(result.value);
5638
+ setError(null);
5639
+ }
5640
+ } catch (loadError) {
5641
+ if (nextRequestId === requestIdRef.current) {
5642
+ setError(loadError);
5643
+ }
5644
+ } finally {
5645
+ if (nextRequestId === requestIdRef.current) {
5646
+ setIsLoading(false);
5647
+ }
5648
+ }
5649
+ })();
5650
+ }, debounceMs);
5651
+ return () => {
5652
+ if (timerRef.current) {
5653
+ clearTimeout(timerRef.current);
5654
+ timerRef.current = null;
5655
+ }
5656
+ };
5657
+ }, [client, debounceMs, limit, minLength, options.category, query]);
5658
+ const setQuery = useCallback((nextQuery) => {
5659
+ setQueryState(nextQuery);
5660
+ }, []);
5661
+ const clear = useCallback(() => {
5662
+ requestIdRef.current += 1;
5663
+ if (timerRef.current) {
5664
+ clearTimeout(timerRef.current);
5665
+ timerRef.current = null;
5666
+ }
5667
+ setQueryState("");
5668
+ setResults([]);
5669
+ setError(null);
5670
+ setIsLoading(false);
5671
+ }, []);
5672
+ return { results, isLoading, error, query, setQuery, clear };
5673
+ }
5674
+ var quoteCache = /* @__PURE__ */ new Map();
5675
+ var quoteInflight = /* @__PURE__ */ new Map();
5676
+ function buildQuoteCacheKey(client, locationId, inputSignature) {
5677
+ return JSON.stringify({
5678
+ key: client.getPublicKey(),
5679
+ location_id: locationId || "__none__",
5680
+ input: inputSignature
5681
+ });
5682
+ }
5683
+ function isQuoteExpired(quote) {
5684
+ if (!quote?.expires_at) {
5685
+ return false;
5686
+ }
5687
+ const expiresAt = Date.parse(quote.expires_at);
5688
+ if (!Number.isFinite(expiresAt)) {
5689
+ return false;
5690
+ }
5691
+ return expiresAt <= Date.now();
5692
+ }
5693
+ function normalizeInput(input, fallbackLocationId) {
5694
+ const productId = input.productId.trim();
5695
+ const variantId = input.variantId?.trim();
5696
+ const locationId = input.locationId?.trim() || fallbackLocationId || void 0;
5697
+ return {
5698
+ product_id: productId,
5699
+ variant_id: variantId && variantId.length > 0 ? variantId : void 0,
5700
+ location_id: locationId && locationId.length > 0 ? locationId : void 0,
5701
+ quantity: input.quantity,
5702
+ add_on_option_ids: input.addOnOptionIds,
5703
+ bundle_selections: input.bundleSelections,
5704
+ composite_selections: input.compositeSelections
5705
+ };
5706
+ }
5707
+ function useQuote(input, options = {}) {
5708
+ const context = useOptionalCimplify();
5709
+ const client = options.client ?? context?.client;
5710
+ if (!client) {
5711
+ throw new Error("useQuote must be used within CimplifyProvider or passed { client }.");
5712
+ }
5713
+ const enabled = options.enabled ?? true;
5714
+ const autoRefresh = options.autoRefresh ?? true;
5715
+ const refreshBeforeExpiryMs = Math.max(0, options.refreshBeforeExpiryMs ?? 3e4);
5716
+ const locationId = client.getLocationId();
5717
+ const requestIdRef = useRef(0);
5718
+ const refreshTimerRef = useRef(null);
5719
+ const expiryTimerRef = useRef(null);
5720
+ const inputSignature = useMemo(() => JSON.stringify(input ?? null), [input]);
5721
+ const normalizedInput = useMemo(() => {
5722
+ if (!input) {
5723
+ return null;
5724
+ }
5725
+ const normalized = normalizeInput(input, locationId);
5726
+ return normalized.product_id.length > 0 ? normalized : null;
5727
+ }, [inputSignature, locationId]);
5728
+ const cacheKey = useMemo(
5729
+ () => buildQuoteCacheKey(client, locationId, inputSignature),
5730
+ [client, inputSignature, locationId]
5731
+ );
5732
+ const cached = quoteCache.get(cacheKey);
5733
+ const [quote, setQuote] = useState(cached?.quote ?? null);
5734
+ const [isLoading, setIsLoading] = useState(enabled && normalizedInput !== null && !cached);
5735
+ const [error, setError] = useState(null);
5736
+ const [isExpired, setIsExpired] = useState(isQuoteExpired(cached?.quote ?? null));
5737
+ const [messages, setMessages] = useState(cached?.quote?.ui_messages ?? []);
5738
+ const load = useCallback(
5739
+ async (force = false) => {
5740
+ if (!enabled || !normalizedInput) {
5741
+ setQuote(null);
5742
+ setMessages([]);
5743
+ setIsExpired(false);
5744
+ setError(null);
5745
+ setIsLoading(false);
5746
+ return;
5747
+ }
5748
+ const nextRequestId = ++requestIdRef.current;
5749
+ setError(null);
5750
+ if (!force) {
5751
+ const cacheEntry = quoteCache.get(cacheKey);
5752
+ if (cacheEntry) {
5753
+ setQuote(cacheEntry.quote);
5754
+ setMessages(cacheEntry.quote?.ui_messages ?? []);
5755
+ setIsExpired(isQuoteExpired(cacheEntry.quote));
5756
+ setIsLoading(false);
5757
+ return;
5758
+ }
5759
+ }
5760
+ setIsLoading(true);
5761
+ try {
5762
+ const existing = quoteInflight.get(cacheKey);
5763
+ const promise = existing ?? (async () => {
5764
+ const result = await client.catalogue.fetchQuote(normalizedInput);
5765
+ if (!result.ok) {
5766
+ throw result.error;
5767
+ }
5768
+ return result.value;
5769
+ })();
5770
+ if (!existing) {
5771
+ quoteInflight.set(cacheKey, promise);
5772
+ promise.finally(() => {
5773
+ quoteInflight.delete(cacheKey);
5774
+ }).catch(() => void 0);
5775
+ }
5776
+ const value = await promise;
5777
+ quoteCache.set(cacheKey, { quote: value });
5778
+ if (nextRequestId === requestIdRef.current) {
5779
+ setQuote(value);
5780
+ setMessages(value.ui_messages ?? []);
5781
+ setIsExpired(isQuoteExpired(value));
5782
+ setError(null);
5783
+ }
5784
+ } catch (loadError) {
5785
+ if (nextRequestId === requestIdRef.current) {
5786
+ setError(loadError);
5787
+ }
5788
+ } finally {
5789
+ if (nextRequestId === requestIdRef.current) {
5790
+ setIsLoading(false);
5791
+ }
5792
+ }
5793
+ },
5794
+ [cacheKey, client, enabled, normalizedInput]
5795
+ );
5796
+ useEffect(() => {
5797
+ void load(false);
5798
+ }, [load]);
5799
+ const refresh = useCallback(async () => {
5800
+ if (!enabled || !normalizedInput) {
5801
+ return;
5802
+ }
5803
+ if (!quote?.quote_id) {
5804
+ await load(true);
5805
+ return;
5806
+ }
5807
+ const nextRequestId = ++requestIdRef.current;
5808
+ setError(null);
5809
+ setIsLoading(true);
5810
+ try {
5811
+ const result = await client.catalogue.refreshQuote({
5812
+ quote_id: quote.quote_id,
5813
+ ...normalizedInput
5814
+ });
5815
+ if (!result.ok) {
5816
+ throw result.error;
5817
+ }
5818
+ const refreshed = result.value.quote;
5819
+ quoteCache.set(cacheKey, { quote: refreshed });
5820
+ if (nextRequestId === requestIdRef.current) {
5821
+ setQuote(refreshed);
5822
+ setMessages(refreshed.ui_messages ?? []);
5823
+ setIsExpired(isQuoteExpired(refreshed));
5824
+ setError(null);
5825
+ }
5826
+ } catch (refreshError) {
5827
+ if (nextRequestId === requestIdRef.current) {
5828
+ setError(refreshError);
5829
+ }
5830
+ } finally {
5831
+ if (nextRequestId === requestIdRef.current) {
5832
+ setIsLoading(false);
5833
+ }
5834
+ }
5835
+ }, [cacheKey, client, enabled, load, normalizedInput, quote]);
5836
+ useEffect(() => {
5837
+ if (expiryTimerRef.current) {
5838
+ clearTimeout(expiryTimerRef.current);
5839
+ expiryTimerRef.current = null;
5840
+ }
5841
+ const expiresAt = quote?.expires_at ? Date.parse(quote.expires_at) : NaN;
5842
+ if (!Number.isFinite(expiresAt)) {
5843
+ setIsExpired(false);
5844
+ return;
5845
+ }
5846
+ const expired = expiresAt <= Date.now();
5847
+ setIsExpired(expired);
5848
+ if (!expired) {
5849
+ expiryTimerRef.current = setTimeout(() => {
5850
+ setIsExpired(true);
5851
+ }, Math.max(0, expiresAt - Date.now()));
5852
+ }
5853
+ return () => {
5854
+ if (expiryTimerRef.current) {
5855
+ clearTimeout(expiryTimerRef.current);
5856
+ expiryTimerRef.current = null;
5857
+ }
5858
+ };
5859
+ }, [quote?.expires_at, quote?.quote_id]);
5860
+ useEffect(() => {
5861
+ if (refreshTimerRef.current) {
5862
+ clearTimeout(refreshTimerRef.current);
5863
+ refreshTimerRef.current = null;
5864
+ }
5865
+ if (!autoRefresh || !enabled || !quote?.expires_at) {
5866
+ return;
5867
+ }
5868
+ const expiresAt = Date.parse(quote.expires_at);
5869
+ if (!Number.isFinite(expiresAt)) {
5870
+ return;
5871
+ }
5872
+ const delay = Math.max(0, expiresAt - Date.now() - refreshBeforeExpiryMs);
5873
+ refreshTimerRef.current = setTimeout(() => {
5874
+ void refresh();
5875
+ }, delay);
5876
+ return () => {
5877
+ if (refreshTimerRef.current) {
5878
+ clearTimeout(refreshTimerRef.current);
5879
+ refreshTimerRef.current = null;
5880
+ }
5881
+ };
5882
+ }, [autoRefresh, enabled, quote?.expires_at, quote?.quote_id, refresh, refreshBeforeExpiryMs]);
5883
+ return { quote, isLoading, error, refresh, isExpired, messages };
5884
+ }
3864
5885
  var ElementsContext = createContext({
3865
5886
  elements: null,
3866
5887
  isReady: false
@@ -4021,4 +6042,4 @@ function useCheckout() {
4021
6042
  return { submit, process, isLoading };
4022
6043
  }
4023
6044
 
4024
- export { Ad, AdProvider, AddressElement, AuthElement, CimplifyCheckout, CimplifyProvider, ElementsProvider, PaymentElement, useAds, useCheckout, useCimplify, useElements, useElementsReady };
6045
+ export { Ad, AdProvider, AddressElement, AuthElement, CimplifyCheckout, CimplifyProvider, ElementsProvider, PaymentElement, useAds, useBundle, useCart, useCategories, useCheckout, useCimplify, useCollection, useCollections, useComposite, useElements, useElementsReady, useLocations, useOptionalCimplify, useOrder, useProduct, useProducts, useQuote, useSearch };