@cimplify/sdk 0.6.7 → 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
@@ -711,6 +711,15 @@ function CimplifyCheckout({
711
711
  }
712
712
 
713
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
+ }
714
723
  var ErrorCode = {
715
724
  // General
716
725
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
@@ -822,6 +831,11 @@ function err(error) {
822
831
  return { ok: false, error };
823
832
  }
824
833
 
834
+ // src/query/builder.ts
835
+ function escapeQueryValue(value) {
836
+ return value.replace(/'/g, "\\'");
837
+ }
838
+
825
839
  // src/catalogue.ts
826
840
  function toCimplifyError(error) {
827
841
  if (error instanceof CimplifyError) return enrichError(error);
@@ -902,7 +916,7 @@ var CatalogueQueries = class {
902
916
  let query = "products";
903
917
  const filters = [];
904
918
  if (options?.category) {
905
- filters.push(`@.category_id=='${options.category}'`);
919
+ filters.push(`@.category_id=='${escapeQueryValue(options.category)}'`);
906
920
  }
907
921
  if (options?.featured !== void 0) {
908
922
  filters.push(`@.featured==${options.featured}`);
@@ -911,7 +925,7 @@ var CatalogueQueries = class {
911
925
  filters.push(`@.in_stock==${options.in_stock}`);
912
926
  }
913
927
  if (options?.search) {
914
- filters.push(`@.name contains '${options.search}'`);
928
+ filters.push(`@.name contains '${escapeQueryValue(options.search)}'`);
915
929
  }
916
930
  if (options?.min_price !== void 0) {
917
931
  filters.push(`@.price>=${options.min_price}`);
@@ -942,7 +956,9 @@ var CatalogueQueries = class {
942
956
  }
943
957
  async getProductBySlug(slug) {
944
958
  const filteredResult = await safe(
945
- this.client.query(`products[?(@.slug=='${slug}')]`)
959
+ this.client.query(
960
+ `products[?(@.slug=='${escapeQueryValue(slug)}')]`
961
+ )
946
962
  );
947
963
  if (!filteredResult.ok) return filteredResult;
948
964
  const exactMatch = findProductBySlug(filteredResult.value, slug);
@@ -994,7 +1010,7 @@ var CatalogueQueries = class {
994
1010
  }
995
1011
  async getCategoryBySlug(slug) {
996
1012
  const result = await safe(
997
- this.client.query(`categories[?(@.slug=='${slug}')]`)
1013
+ this.client.query(`categories[?(@.slug=='${escapeQueryValue(slug)}')]`)
998
1014
  );
999
1015
  if (!result.ok) return result;
1000
1016
  if (!result.value.length) {
@@ -1003,7 +1019,11 @@ var CatalogueQueries = class {
1003
1019
  return ok(result.value[0]);
1004
1020
  }
1005
1021
  async getCategoryProducts(categoryId) {
1006
- return safe(this.client.query(`products[?(@.category_id=='${categoryId}')]`));
1022
+ return safe(
1023
+ this.client.query(
1024
+ `products[?(@.category_id=='${escapeQueryValue(categoryId)}')]`
1025
+ )
1026
+ );
1007
1027
  }
1008
1028
  async getCollections() {
1009
1029
  return safe(this.client.query("collections"));
@@ -1013,7 +1033,9 @@ var CatalogueQueries = class {
1013
1033
  }
1014
1034
  async getCollectionBySlug(slug) {
1015
1035
  const result = await safe(
1016
- this.client.query(`collections[?(@.slug=='${slug}')]`)
1036
+ this.client.query(
1037
+ `collections[?(@.slug=='${escapeQueryValue(slug)}')]`
1038
+ )
1017
1039
  );
1018
1040
  if (!result.ok) return result;
1019
1041
  if (!result.value.length) {
@@ -1026,7 +1048,9 @@ var CatalogueQueries = class {
1026
1048
  }
1027
1049
  async searchCollections(query, limit = 20) {
1028
1050
  return safe(
1029
- this.client.query(`collections[?(@.name contains '${query}')]#limit(${limit})`)
1051
+ this.client.query(
1052
+ `collections[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
1053
+ )
1030
1054
  );
1031
1055
  }
1032
1056
  async getBundles() {
@@ -1037,7 +1061,9 @@ var CatalogueQueries = class {
1037
1061
  }
1038
1062
  async getBundleBySlug(slug) {
1039
1063
  const result = await safe(
1040
- this.client.query(`bundles[?(@.slug=='${slug}')]`)
1064
+ this.client.query(
1065
+ `bundles[?(@.slug=='${escapeQueryValue(slug)}')]`
1066
+ )
1041
1067
  );
1042
1068
  if (!result.ok) return result;
1043
1069
  if (!result.value.length) {
@@ -1047,7 +1073,9 @@ var CatalogueQueries = class {
1047
1073
  }
1048
1074
  async searchBundles(query, limit = 20) {
1049
1075
  return safe(
1050
- this.client.query(`bundles[?(@.name contains '${query}')]#limit(${limit})`)
1076
+ this.client.query(
1077
+ `bundles[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
1078
+ )
1051
1079
  );
1052
1080
  }
1053
1081
  async getComposites(options) {
@@ -1087,9 +1115,9 @@ var CatalogueQueries = class {
1087
1115
  }
1088
1116
  async search(query, options) {
1089
1117
  const limit = options?.limit ?? 20;
1090
- let searchQuery = `products[?(@.name contains '${query}')]`;
1118
+ let searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}')]`;
1091
1119
  if (options?.category) {
1092
- searchQuery = `products[?(@.name contains '${query}' && @.category_id=='${options.category}')]`;
1120
+ searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}' && @.category_id=='${escapeQueryValue(options.category)}')]`;
1093
1121
  }
1094
1122
  searchQuery += `#limit(${limit})`;
1095
1123
  return safe(this.client.query(searchQuery));
@@ -1106,7 +1134,7 @@ var CatalogueQueries = class {
1106
1134
  async getMenu(options) {
1107
1135
  let query = "menu";
1108
1136
  if (options?.category) {
1109
- query = `menu[?(@.category=='${options.category}')]`;
1137
+ query = `menu[?(@.category=='${escapeQueryValue(options.category)}')]`;
1110
1138
  }
1111
1139
  if (options?.limit) {
1112
1140
  query += `#limit(${options.limit})`;
@@ -1400,6 +1428,8 @@ function normalizeStatusResponse(response) {
1400
1428
  }
1401
1429
  const res = response;
1402
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;
1403
1433
  const paidValue = res.paid === true;
1404
1434
  const derivedPaid = paidValue || [
1405
1435
  "success",
@@ -1412,8 +1442,8 @@ function normalizeStatusResponse(response) {
1412
1442
  return {
1413
1443
  status: normalizedStatus,
1414
1444
  paid: derivedPaid,
1415
- amount: res.amount,
1416
- currency: res.currency,
1445
+ amount: normalizedAmount,
1446
+ currency: normalizedCurrency,
1417
1447
  reference: res.reference,
1418
1448
  message: res.message || ""
1419
1449
  };
@@ -2105,14 +2135,17 @@ var CheckoutService = class {
2105
2135
  pay_currency: data.pay_currency,
2106
2136
  fx_quote_id: data.fx_quote_id
2107
2137
  };
2108
- const baseCurrency = (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase();
2138
+ const baseCurrency = currencyCode(
2139
+ (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase()
2140
+ );
2109
2141
  const payCurrency = data.pay_currency?.trim().toUpperCase();
2142
+ const payCurrencyCode = payCurrency ? currencyCode(payCurrency) : void 0;
2110
2143
  const cartTotalAmount = Number.parseFloat(cart.pricing.total_price || "0");
2111
- 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) {
2112
2145
  const fxQuoteResult = await this.client.fx.lockQuote({
2113
2146
  from: baseCurrency,
2114
- to: payCurrency,
2115
- amount: cartTotalAmount
2147
+ to: payCurrencyCode,
2148
+ amount: cart.pricing.total_price
2116
2149
  });
2117
2150
  if (!fxQuoteResult.ok) {
2118
2151
  return ok(
@@ -2123,7 +2156,7 @@ var CheckoutService = class {
2123
2156
  )
2124
2157
  );
2125
2158
  }
2126
- checkoutData.pay_currency = payCurrency;
2159
+ checkoutData.pay_currency = payCurrencyCode;
2127
2160
  checkoutData.fx_quote_id = fxQuoteResult.value.id;
2128
2161
  }
2129
2162
  data.on_status_change?.("processing", {});
@@ -3574,7 +3607,7 @@ var CimplifyClient = class {
3574
3607
  signal: controller.signal
3575
3608
  });
3576
3609
  clearTimeout(timeoutId);
3577
- if (response.ok || response.status >= 400 && response.status < 500) {
3610
+ if (response.ok) {
3578
3611
  this.hooks.onRequestSuccess?.({
3579
3612
  ...context,
3580
3613
  status: response.status,
@@ -3582,6 +3615,21 @@ var CimplifyClient = class {
3582
3615
  });
3583
3616
  return response;
3584
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
+ }
3585
3633
  if (response.status >= 500 && attempt < this.maxRetries) {
3586
3634
  retryCount++;
3587
3635
  const delay = this.retryDelay * Math.pow(2, attempt);
@@ -3594,10 +3642,17 @@ var CimplifyClient = class {
3594
3642
  await sleep(delay);
3595
3643
  continue;
3596
3644
  }
3597
- this.hooks.onRequestSuccess?.({
3645
+ this.hooks.onRequestError?.({
3598
3646
  ...context,
3647
+ error: new CimplifyError(
3648
+ "SERVER_ERROR",
3649
+ `Server error ${response.status} after ${retryCount} retries`,
3650
+ false
3651
+ ),
3599
3652
  status: response.status,
3600
- durationMs: Date.now() - startTime
3653
+ durationMs: Date.now() - startTime,
3654
+ retryCount,
3655
+ retryable: false
3601
3656
  });
3602
3657
  return response;
3603
3658
  } catch (error) {
@@ -5083,6 +5138,750 @@ function useLocations(options = {}) {
5083
5138
  isLoading
5084
5139
  };
5085
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
+ }
5086
5885
  var ElementsContext = createContext({
5087
5886
  elements: null,
5088
5887
  isReady: false
@@ -5243,4 +6042,4 @@ function useCheckout() {
5243
6042
  return { submit, process, isLoading };
5244
6043
  }
5245
6044
 
5246
- export { Ad, AdProvider, AddressElement, AuthElement, CimplifyCheckout, CimplifyProvider, ElementsProvider, PaymentElement, useAds, useCart, useCategories, useCheckout, useCimplify, useElements, useElementsReady, useLocations, useOptionalCimplify, useOrder, useProduct, useProducts };
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 };