@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.js CHANGED
@@ -713,6 +713,15 @@ function CimplifyCheckout({
713
713
  }
714
714
 
715
715
  // src/types/common.ts
716
+ function money(value) {
717
+ return value;
718
+ }
719
+ function moneyFromNumber(value) {
720
+ return value.toFixed(2);
721
+ }
722
+ function currencyCode(value) {
723
+ return value;
724
+ }
716
725
  var ErrorCode = {
717
726
  // General
718
727
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
@@ -824,6 +833,11 @@ function err(error) {
824
833
  return { ok: false, error };
825
834
  }
826
835
 
836
+ // src/query/builder.ts
837
+ function escapeQueryValue(value) {
838
+ return value.replace(/'/g, "\\'");
839
+ }
840
+
827
841
  // src/catalogue.ts
828
842
  function toCimplifyError(error) {
829
843
  if (error instanceof CimplifyError) return enrichError(error);
@@ -904,7 +918,7 @@ var CatalogueQueries = class {
904
918
  let query = "products";
905
919
  const filters = [];
906
920
  if (options?.category) {
907
- filters.push(`@.category_id=='${options.category}'`);
921
+ filters.push(`@.category_id=='${escapeQueryValue(options.category)}'`);
908
922
  }
909
923
  if (options?.featured !== void 0) {
910
924
  filters.push(`@.featured==${options.featured}`);
@@ -913,7 +927,7 @@ var CatalogueQueries = class {
913
927
  filters.push(`@.in_stock==${options.in_stock}`);
914
928
  }
915
929
  if (options?.search) {
916
- filters.push(`@.name contains '${options.search}'`);
930
+ filters.push(`@.name contains '${escapeQueryValue(options.search)}'`);
917
931
  }
918
932
  if (options?.min_price !== void 0) {
919
933
  filters.push(`@.price>=${options.min_price}`);
@@ -944,7 +958,9 @@ var CatalogueQueries = class {
944
958
  }
945
959
  async getProductBySlug(slug) {
946
960
  const filteredResult = await safe(
947
- this.client.query(`products[?(@.slug=='${slug}')]`)
961
+ this.client.query(
962
+ `products[?(@.slug=='${escapeQueryValue(slug)}')]`
963
+ )
948
964
  );
949
965
  if (!filteredResult.ok) return filteredResult;
950
966
  const exactMatch = findProductBySlug(filteredResult.value, slug);
@@ -996,7 +1012,7 @@ var CatalogueQueries = class {
996
1012
  }
997
1013
  async getCategoryBySlug(slug) {
998
1014
  const result = await safe(
999
- this.client.query(`categories[?(@.slug=='${slug}')]`)
1015
+ this.client.query(`categories[?(@.slug=='${escapeQueryValue(slug)}')]`)
1000
1016
  );
1001
1017
  if (!result.ok) return result;
1002
1018
  if (!result.value.length) {
@@ -1005,7 +1021,11 @@ var CatalogueQueries = class {
1005
1021
  return ok(result.value[0]);
1006
1022
  }
1007
1023
  async getCategoryProducts(categoryId) {
1008
- return safe(this.client.query(`products[?(@.category_id=='${categoryId}')]`));
1024
+ return safe(
1025
+ this.client.query(
1026
+ `products[?(@.category_id=='${escapeQueryValue(categoryId)}')]`
1027
+ )
1028
+ );
1009
1029
  }
1010
1030
  async getCollections() {
1011
1031
  return safe(this.client.query("collections"));
@@ -1015,7 +1035,9 @@ var CatalogueQueries = class {
1015
1035
  }
1016
1036
  async getCollectionBySlug(slug) {
1017
1037
  const result = await safe(
1018
- this.client.query(`collections[?(@.slug=='${slug}')]`)
1038
+ this.client.query(
1039
+ `collections[?(@.slug=='${escapeQueryValue(slug)}')]`
1040
+ )
1019
1041
  );
1020
1042
  if (!result.ok) return result;
1021
1043
  if (!result.value.length) {
@@ -1028,7 +1050,9 @@ var CatalogueQueries = class {
1028
1050
  }
1029
1051
  async searchCollections(query, limit = 20) {
1030
1052
  return safe(
1031
- this.client.query(`collections[?(@.name contains '${query}')]#limit(${limit})`)
1053
+ this.client.query(
1054
+ `collections[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
1055
+ )
1032
1056
  );
1033
1057
  }
1034
1058
  async getBundles() {
@@ -1039,7 +1063,9 @@ var CatalogueQueries = class {
1039
1063
  }
1040
1064
  async getBundleBySlug(slug) {
1041
1065
  const result = await safe(
1042
- this.client.query(`bundles[?(@.slug=='${slug}')]`)
1066
+ this.client.query(
1067
+ `bundles[?(@.slug=='${escapeQueryValue(slug)}')]`
1068
+ )
1043
1069
  );
1044
1070
  if (!result.ok) return result;
1045
1071
  if (!result.value.length) {
@@ -1049,7 +1075,9 @@ var CatalogueQueries = class {
1049
1075
  }
1050
1076
  async searchBundles(query, limit = 20) {
1051
1077
  return safe(
1052
- this.client.query(`bundles[?(@.name contains '${query}')]#limit(${limit})`)
1078
+ this.client.query(
1079
+ `bundles[?(@.name contains '${escapeQueryValue(query)}')]#limit(${limit})`
1080
+ )
1053
1081
  );
1054
1082
  }
1055
1083
  async getComposites(options) {
@@ -1089,9 +1117,9 @@ var CatalogueQueries = class {
1089
1117
  }
1090
1118
  async search(query, options) {
1091
1119
  const limit = options?.limit ?? 20;
1092
- let searchQuery = `products[?(@.name contains '${query}')]`;
1120
+ let searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}')]`;
1093
1121
  if (options?.category) {
1094
- searchQuery = `products[?(@.name contains '${query}' && @.category_id=='${options.category}')]`;
1122
+ searchQuery = `products[?(@.name contains '${escapeQueryValue(query)}' && @.category_id=='${escapeQueryValue(options.category)}')]`;
1095
1123
  }
1096
1124
  searchQuery += `#limit(${limit})`;
1097
1125
  return safe(this.client.query(searchQuery));
@@ -1108,7 +1136,7 @@ var CatalogueQueries = class {
1108
1136
  async getMenu(options) {
1109
1137
  let query = "menu";
1110
1138
  if (options?.category) {
1111
- query = `menu[?(@.category=='${options.category}')]`;
1139
+ query = `menu[?(@.category=='${escapeQueryValue(options.category)}')]`;
1112
1140
  }
1113
1141
  if (options?.limit) {
1114
1142
  query += `#limit(${options.limit})`;
@@ -1402,6 +1430,8 @@ function normalizeStatusResponse(response) {
1402
1430
  }
1403
1431
  const res = response;
1404
1432
  const normalizedStatus = normalizePaymentStatusValue(res.status ?? void 0);
1433
+ const normalizedAmount = typeof res.amount === "string" ? money(res.amount) : typeof res.amount === "number" && Number.isFinite(res.amount) ? moneyFromNumber(res.amount) : void 0;
1434
+ const normalizedCurrency = typeof res.currency === "string" && res.currency.trim().length > 0 ? currencyCode(res.currency) : void 0;
1405
1435
  const paidValue = res.paid === true;
1406
1436
  const derivedPaid = paidValue || [
1407
1437
  "success",
@@ -1414,8 +1444,8 @@ function normalizeStatusResponse(response) {
1414
1444
  return {
1415
1445
  status: normalizedStatus,
1416
1446
  paid: derivedPaid,
1417
- amount: res.amount,
1418
- currency: res.currency,
1447
+ amount: normalizedAmount,
1448
+ currency: normalizedCurrency,
1419
1449
  reference: res.reference,
1420
1450
  message: res.message || ""
1421
1451
  };
@@ -2107,14 +2137,17 @@ var CheckoutService = class {
2107
2137
  pay_currency: data.pay_currency,
2108
2138
  fx_quote_id: data.fx_quote_id
2109
2139
  };
2110
- const baseCurrency = (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase();
2140
+ const baseCurrency = currencyCode(
2141
+ (cart.pricing.currency || checkoutData.pay_currency || "GHS").toUpperCase()
2142
+ );
2111
2143
  const payCurrency = data.pay_currency?.trim().toUpperCase();
2144
+ const payCurrencyCode = payCurrency ? currencyCode(payCurrency) : void 0;
2112
2145
  const cartTotalAmount = Number.parseFloat(cart.pricing.total_price || "0");
2113
- if (payCurrency && payCurrency !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
2146
+ if (payCurrencyCode && payCurrencyCode !== baseCurrency && !checkoutData.fx_quote_id && Number.isFinite(cartTotalAmount) && cartTotalAmount > 0) {
2114
2147
  const fxQuoteResult = await this.client.fx.lockQuote({
2115
2148
  from: baseCurrency,
2116
- to: payCurrency,
2117
- amount: cartTotalAmount
2149
+ to: payCurrencyCode,
2150
+ amount: cart.pricing.total_price
2118
2151
  });
2119
2152
  if (!fxQuoteResult.ok) {
2120
2153
  return ok(
@@ -2125,7 +2158,7 @@ var CheckoutService = class {
2125
2158
  )
2126
2159
  );
2127
2160
  }
2128
- checkoutData.pay_currency = payCurrency;
2161
+ checkoutData.pay_currency = payCurrencyCode;
2129
2162
  checkoutData.fx_quote_id = fxQuoteResult.value.id;
2130
2163
  }
2131
2164
  data.on_status_change?.("processing", {});
@@ -3576,7 +3609,7 @@ var CimplifyClient = class {
3576
3609
  signal: controller.signal
3577
3610
  });
3578
3611
  clearTimeout(timeoutId);
3579
- if (response.ok || response.status >= 400 && response.status < 500) {
3612
+ if (response.ok) {
3580
3613
  this.hooks.onRequestSuccess?.({
3581
3614
  ...context,
3582
3615
  status: response.status,
@@ -3584,6 +3617,21 @@ var CimplifyClient = class {
3584
3617
  });
3585
3618
  return response;
3586
3619
  }
3620
+ if (response.status >= 400 && response.status < 500) {
3621
+ this.hooks.onRequestError?.({
3622
+ ...context,
3623
+ error: new CimplifyError(
3624
+ `HTTP_${response.status}`,
3625
+ `Request failed with status ${response.status}`,
3626
+ false
3627
+ ),
3628
+ status: response.status,
3629
+ durationMs: Date.now() - startTime,
3630
+ retryCount,
3631
+ retryable: false
3632
+ });
3633
+ return response;
3634
+ }
3587
3635
  if (response.status >= 500 && attempt < this.maxRetries) {
3588
3636
  retryCount++;
3589
3637
  const delay = this.retryDelay * Math.pow(2, attempt);
@@ -3596,10 +3644,17 @@ var CimplifyClient = class {
3596
3644
  await sleep(delay);
3597
3645
  continue;
3598
3646
  }
3599
- this.hooks.onRequestSuccess?.({
3647
+ this.hooks.onRequestError?.({
3600
3648
  ...context,
3649
+ error: new CimplifyError(
3650
+ "SERVER_ERROR",
3651
+ `Server error ${response.status} after ${retryCount} retries`,
3652
+ false
3653
+ ),
3601
3654
  status: response.status,
3602
- durationMs: Date.now() - startTime
3655
+ durationMs: Date.now() - startTime,
3656
+ retryCount,
3657
+ retryable: false
3603
3658
  });
3604
3659
  return response;
3605
3660
  } catch (error) {
@@ -5085,6 +5140,750 @@ function useLocations(options = {}) {
5085
5140
  isLoading
5086
5141
  };
5087
5142
  }
5143
+ var collectionsCache = /* @__PURE__ */ new Map();
5144
+ var collectionsInflight = /* @__PURE__ */ new Map();
5145
+ function buildCollectionsCacheKey(client, locationId) {
5146
+ return JSON.stringify({
5147
+ key: client.getPublicKey(),
5148
+ location_id: locationId || "__none__"
5149
+ });
5150
+ }
5151
+ function useCollections(options = {}) {
5152
+ const context = useOptionalCimplify();
5153
+ const client = options.client ?? context?.client;
5154
+ if (!client) {
5155
+ throw new Error("useCollections must be used within CimplifyProvider or passed { client }.");
5156
+ }
5157
+ const enabled = options.enabled ?? true;
5158
+ const locationId = client.getLocationId();
5159
+ const previousLocationIdRef = react.useRef(locationId);
5160
+ const requestIdRef = react.useRef(0);
5161
+ const cacheKey = react.useMemo(
5162
+ () => buildCollectionsCacheKey(client, locationId),
5163
+ [client, locationId]
5164
+ );
5165
+ const cached = collectionsCache.get(cacheKey);
5166
+ const [collections, setCollections] = react.useState(cached?.collections ?? []);
5167
+ const [isLoading, setIsLoading] = react.useState(enabled && !cached);
5168
+ const [error, setError] = react.useState(null);
5169
+ react.useEffect(() => {
5170
+ if (previousLocationIdRef.current !== locationId) {
5171
+ collectionsCache.clear();
5172
+ collectionsInflight.clear();
5173
+ previousLocationIdRef.current = locationId;
5174
+ }
5175
+ }, [locationId]);
5176
+ const load = react.useCallback(
5177
+ async (force = false) => {
5178
+ if (!enabled) {
5179
+ setIsLoading(false);
5180
+ return;
5181
+ }
5182
+ const nextRequestId = ++requestIdRef.current;
5183
+ setError(null);
5184
+ if (!force) {
5185
+ const cacheEntry = collectionsCache.get(cacheKey);
5186
+ if (cacheEntry) {
5187
+ setCollections(cacheEntry.collections);
5188
+ setIsLoading(false);
5189
+ return;
5190
+ }
5191
+ }
5192
+ setIsLoading(true);
5193
+ try {
5194
+ const existing = collectionsInflight.get(cacheKey);
5195
+ const promise = existing ?? (async () => {
5196
+ const result = await client.catalogue.getCollections();
5197
+ if (!result.ok) {
5198
+ throw result.error;
5199
+ }
5200
+ return result.value;
5201
+ })();
5202
+ if (!existing) {
5203
+ collectionsInflight.set(cacheKey, promise);
5204
+ promise.finally(() => {
5205
+ collectionsInflight.delete(cacheKey);
5206
+ }).catch(() => void 0);
5207
+ }
5208
+ const value = await promise;
5209
+ collectionsCache.set(cacheKey, { collections: value });
5210
+ if (nextRequestId === requestIdRef.current) {
5211
+ setCollections(value);
5212
+ setError(null);
5213
+ }
5214
+ } catch (loadError) {
5215
+ if (nextRequestId === requestIdRef.current) {
5216
+ setError(loadError);
5217
+ }
5218
+ } finally {
5219
+ if (nextRequestId === requestIdRef.current) {
5220
+ setIsLoading(false);
5221
+ }
5222
+ }
5223
+ },
5224
+ [cacheKey, client, enabled]
5225
+ );
5226
+ react.useEffect(() => {
5227
+ void load(false);
5228
+ }, [load]);
5229
+ const refetch = react.useCallback(async () => {
5230
+ collectionsCache.delete(cacheKey);
5231
+ await load(true);
5232
+ }, [cacheKey, load]);
5233
+ return { collections, isLoading, error, refetch };
5234
+ }
5235
+ var collectionCache = /* @__PURE__ */ new Map();
5236
+ var collectionInflight = /* @__PURE__ */ new Map();
5237
+ function isLikelySlug2(value) {
5238
+ return /^[a-z0-9-]+$/.test(value);
5239
+ }
5240
+ function buildCollectionCacheKey(client, locationId, idOrSlug) {
5241
+ return JSON.stringify({
5242
+ key: client.getPublicKey(),
5243
+ location_id: locationId || "__none__",
5244
+ collection: idOrSlug
5245
+ });
5246
+ }
5247
+ function useCollection(idOrSlug, options = {}) {
5248
+ const context = useOptionalCimplify();
5249
+ const client = options.client ?? context?.client;
5250
+ if (!client) {
5251
+ throw new Error("useCollection must be used within CimplifyProvider or passed { client }.");
5252
+ }
5253
+ const enabled = options.enabled ?? true;
5254
+ const locationId = client.getLocationId();
5255
+ const previousLocationIdRef = react.useRef(locationId);
5256
+ const requestIdRef = react.useRef(0);
5257
+ const normalizedIdOrSlug = react.useMemo(() => (idOrSlug || "").trim(), [idOrSlug]);
5258
+ const cacheKey = react.useMemo(
5259
+ () => buildCollectionCacheKey(client, locationId, normalizedIdOrSlug),
5260
+ [client, locationId, normalizedIdOrSlug]
5261
+ );
5262
+ const cached = collectionCache.get(cacheKey);
5263
+ const [collection, setCollection] = react.useState(cached?.collection ?? null);
5264
+ const [products, setProducts] = react.useState(cached?.products ?? []);
5265
+ const [isLoading, setIsLoading] = react.useState(
5266
+ enabled && normalizedIdOrSlug.length > 0 && !cached
5267
+ );
5268
+ const [error, setError] = react.useState(null);
5269
+ react.useEffect(() => {
5270
+ if (previousLocationIdRef.current !== locationId) {
5271
+ collectionCache.clear();
5272
+ collectionInflight.clear();
5273
+ previousLocationIdRef.current = locationId;
5274
+ }
5275
+ }, [locationId]);
5276
+ const load = react.useCallback(
5277
+ async (force = false) => {
5278
+ if (!enabled || normalizedIdOrSlug.length === 0) {
5279
+ setCollection(null);
5280
+ setProducts([]);
5281
+ setIsLoading(false);
5282
+ return;
5283
+ }
5284
+ const nextRequestId = ++requestIdRef.current;
5285
+ setError(null);
5286
+ if (!force) {
5287
+ const cacheEntry = collectionCache.get(cacheKey);
5288
+ if (cacheEntry) {
5289
+ setCollection(cacheEntry.collection);
5290
+ setProducts(cacheEntry.products);
5291
+ setIsLoading(false);
5292
+ return;
5293
+ }
5294
+ }
5295
+ setIsLoading(true);
5296
+ try {
5297
+ const existing = collectionInflight.get(cacheKey);
5298
+ const promise = existing ?? (async () => {
5299
+ const collectionResult = isLikelySlug2(normalizedIdOrSlug) ? await client.catalogue.getCollectionBySlug(normalizedIdOrSlug) : await client.catalogue.getCollection(normalizedIdOrSlug);
5300
+ if (!collectionResult.ok) {
5301
+ throw collectionResult.error;
5302
+ }
5303
+ const productsResult = await client.catalogue.getCollectionProducts(
5304
+ collectionResult.value.id
5305
+ );
5306
+ if (!productsResult.ok) {
5307
+ throw productsResult.error;
5308
+ }
5309
+ return {
5310
+ collection: collectionResult.value,
5311
+ products: productsResult.value
5312
+ };
5313
+ })();
5314
+ if (!existing) {
5315
+ collectionInflight.set(cacheKey, promise);
5316
+ promise.finally(() => {
5317
+ collectionInflight.delete(cacheKey);
5318
+ }).catch(() => void 0);
5319
+ }
5320
+ const value = await promise;
5321
+ collectionCache.set(cacheKey, value);
5322
+ if (nextRequestId === requestIdRef.current) {
5323
+ setCollection(value.collection);
5324
+ setProducts(value.products);
5325
+ setError(null);
5326
+ }
5327
+ } catch (loadError) {
5328
+ if (nextRequestId === requestIdRef.current) {
5329
+ setError(loadError);
5330
+ }
5331
+ } finally {
5332
+ if (nextRequestId === requestIdRef.current) {
5333
+ setIsLoading(false);
5334
+ }
5335
+ }
5336
+ },
5337
+ [cacheKey, client, enabled, normalizedIdOrSlug]
5338
+ );
5339
+ react.useEffect(() => {
5340
+ void load(false);
5341
+ }, [load]);
5342
+ const refetch = react.useCallback(async () => {
5343
+ collectionCache.delete(cacheKey);
5344
+ await load(true);
5345
+ }, [cacheKey, load]);
5346
+ return { collection, products, isLoading, error, refetch };
5347
+ }
5348
+ var bundleCache = /* @__PURE__ */ new Map();
5349
+ var bundleInflight = /* @__PURE__ */ new Map();
5350
+ function isLikelySlug3(value) {
5351
+ return /^[a-z0-9-]+$/.test(value);
5352
+ }
5353
+ function buildBundleCacheKey(client, locationId, idOrSlug) {
5354
+ return JSON.stringify({
5355
+ key: client.getPublicKey(),
5356
+ location_id: locationId || "__none__",
5357
+ bundle: idOrSlug
5358
+ });
5359
+ }
5360
+ function useBundle(idOrSlug, options = {}) {
5361
+ const context = useOptionalCimplify();
5362
+ const client = options.client ?? context?.client;
5363
+ if (!client) {
5364
+ throw new Error("useBundle must be used within CimplifyProvider or passed { client }.");
5365
+ }
5366
+ const enabled = options.enabled ?? true;
5367
+ const locationId = client.getLocationId();
5368
+ const previousLocationIdRef = react.useRef(locationId);
5369
+ const requestIdRef = react.useRef(0);
5370
+ const normalizedIdOrSlug = react.useMemo(() => (idOrSlug || "").trim(), [idOrSlug]);
5371
+ const cacheKey = react.useMemo(
5372
+ () => buildBundleCacheKey(client, locationId, normalizedIdOrSlug),
5373
+ [client, locationId, normalizedIdOrSlug]
5374
+ );
5375
+ const cached = bundleCache.get(cacheKey);
5376
+ const [bundle, setBundle] = react.useState(cached?.bundle ?? null);
5377
+ const [isLoading, setIsLoading] = react.useState(
5378
+ enabled && normalizedIdOrSlug.length > 0 && !cached
5379
+ );
5380
+ const [error, setError] = react.useState(null);
5381
+ react.useEffect(() => {
5382
+ if (previousLocationIdRef.current !== locationId) {
5383
+ bundleCache.clear();
5384
+ bundleInflight.clear();
5385
+ previousLocationIdRef.current = locationId;
5386
+ }
5387
+ }, [locationId]);
5388
+ const load = react.useCallback(
5389
+ async (force = false) => {
5390
+ if (!enabled || normalizedIdOrSlug.length === 0) {
5391
+ setBundle(null);
5392
+ setIsLoading(false);
5393
+ return;
5394
+ }
5395
+ const nextRequestId = ++requestIdRef.current;
5396
+ setError(null);
5397
+ if (!force) {
5398
+ const cacheEntry = bundleCache.get(cacheKey);
5399
+ if (cacheEntry) {
5400
+ setBundle(cacheEntry.bundle);
5401
+ setIsLoading(false);
5402
+ return;
5403
+ }
5404
+ }
5405
+ setIsLoading(true);
5406
+ try {
5407
+ const existing = bundleInflight.get(cacheKey);
5408
+ const promise = existing ?? (async () => {
5409
+ const result = isLikelySlug3(normalizedIdOrSlug) ? await client.catalogue.getBundleBySlug(normalizedIdOrSlug) : await client.catalogue.getBundle(normalizedIdOrSlug);
5410
+ if (!result.ok) {
5411
+ throw result.error;
5412
+ }
5413
+ return result.value;
5414
+ })();
5415
+ if (!existing) {
5416
+ bundleInflight.set(cacheKey, promise);
5417
+ promise.finally(() => {
5418
+ bundleInflight.delete(cacheKey);
5419
+ }).catch(() => void 0);
5420
+ }
5421
+ const value = await promise;
5422
+ bundleCache.set(cacheKey, { bundle: value });
5423
+ if (nextRequestId === requestIdRef.current) {
5424
+ setBundle(value);
5425
+ setError(null);
5426
+ }
5427
+ } catch (loadError) {
5428
+ if (nextRequestId === requestIdRef.current) {
5429
+ setError(loadError);
5430
+ }
5431
+ } finally {
5432
+ if (nextRequestId === requestIdRef.current) {
5433
+ setIsLoading(false);
5434
+ }
5435
+ }
5436
+ },
5437
+ [cacheKey, client, enabled, normalizedIdOrSlug]
5438
+ );
5439
+ react.useEffect(() => {
5440
+ void load(false);
5441
+ }, [load]);
5442
+ const refetch = react.useCallback(async () => {
5443
+ bundleCache.delete(cacheKey);
5444
+ await load(true);
5445
+ }, [cacheKey, load]);
5446
+ return { bundle, isLoading, error, refetch };
5447
+ }
5448
+ var compositeCache = /* @__PURE__ */ new Map();
5449
+ var compositeInflight = /* @__PURE__ */ new Map();
5450
+ function shouldFetchByProductId(idOrProductId, byProductId) {
5451
+ if (typeof byProductId === "boolean") {
5452
+ return byProductId;
5453
+ }
5454
+ return idOrProductId.startsWith("prod_");
5455
+ }
5456
+ function buildCompositeCacheKey(client, locationId, idOrProductId, byProductId) {
5457
+ return JSON.stringify({
5458
+ key: client.getPublicKey(),
5459
+ location_id: locationId || "__none__",
5460
+ composite: idOrProductId,
5461
+ by_product_id: byProductId
5462
+ });
5463
+ }
5464
+ function useComposite(idOrProductId, options = {}) {
5465
+ const context = useOptionalCimplify();
5466
+ const client = options.client ?? context?.client;
5467
+ if (!client) {
5468
+ throw new Error("useComposite must be used within CimplifyProvider or passed { client }.");
5469
+ }
5470
+ const enabled = options.enabled ?? true;
5471
+ const locationId = client.getLocationId();
5472
+ const previousLocationIdRef = react.useRef(locationId);
5473
+ const requestIdRef = react.useRef(0);
5474
+ const priceRequestIdRef = react.useRef(0);
5475
+ const normalizedIdOrProductId = react.useMemo(
5476
+ () => (idOrProductId || "").trim(),
5477
+ [idOrProductId]
5478
+ );
5479
+ const byProductId = react.useMemo(
5480
+ () => shouldFetchByProductId(normalizedIdOrProductId, options.byProductId),
5481
+ [normalizedIdOrProductId, options.byProductId]
5482
+ );
5483
+ const cacheKey = react.useMemo(
5484
+ () => buildCompositeCacheKey(client, locationId, normalizedIdOrProductId, byProductId),
5485
+ [byProductId, client, locationId, normalizedIdOrProductId]
5486
+ );
5487
+ const cached = compositeCache.get(cacheKey);
5488
+ const [composite, setComposite] = react.useState(cached?.composite ?? null);
5489
+ const [isLoading, setIsLoading] = react.useState(
5490
+ enabled && normalizedIdOrProductId.length > 0 && !cached
5491
+ );
5492
+ const [error, setError] = react.useState(null);
5493
+ const [priceResult, setPriceResult] = react.useState(null);
5494
+ const [isPriceLoading, setIsPriceLoading] = react.useState(false);
5495
+ react.useEffect(() => {
5496
+ if (previousLocationIdRef.current !== locationId) {
5497
+ compositeCache.clear();
5498
+ compositeInflight.clear();
5499
+ previousLocationIdRef.current = locationId;
5500
+ }
5501
+ }, [locationId]);
5502
+ const load = react.useCallback(
5503
+ async (force = false) => {
5504
+ if (!enabled || normalizedIdOrProductId.length === 0) {
5505
+ setComposite(null);
5506
+ setPriceResult(null);
5507
+ setIsLoading(false);
5508
+ return;
5509
+ }
5510
+ const nextRequestId = ++requestIdRef.current;
5511
+ setError(null);
5512
+ if (!force) {
5513
+ const cacheEntry = compositeCache.get(cacheKey);
5514
+ if (cacheEntry) {
5515
+ setComposite(cacheEntry.composite);
5516
+ setIsLoading(false);
5517
+ return;
5518
+ }
5519
+ }
5520
+ setIsLoading(true);
5521
+ try {
5522
+ const existing = compositeInflight.get(cacheKey);
5523
+ const promise = existing ?? (async () => {
5524
+ const result = byProductId ? await client.catalogue.getCompositeByProductId(normalizedIdOrProductId) : await client.catalogue.getComposite(normalizedIdOrProductId);
5525
+ if (!result.ok) {
5526
+ throw result.error;
5527
+ }
5528
+ return result.value;
5529
+ })();
5530
+ if (!existing) {
5531
+ compositeInflight.set(cacheKey, promise);
5532
+ promise.finally(() => {
5533
+ compositeInflight.delete(cacheKey);
5534
+ }).catch(() => void 0);
5535
+ }
5536
+ const value = await promise;
5537
+ compositeCache.set(cacheKey, { composite: value });
5538
+ if (nextRequestId === requestIdRef.current) {
5539
+ setComposite(value);
5540
+ setPriceResult(null);
5541
+ setError(null);
5542
+ }
5543
+ } catch (loadError) {
5544
+ if (nextRequestId === requestIdRef.current) {
5545
+ setError(loadError);
5546
+ }
5547
+ } finally {
5548
+ if (nextRequestId === requestIdRef.current) {
5549
+ setIsLoading(false);
5550
+ }
5551
+ }
5552
+ },
5553
+ [byProductId, cacheKey, client, enabled, normalizedIdOrProductId]
5554
+ );
5555
+ react.useEffect(() => {
5556
+ void load(false);
5557
+ }, [load]);
5558
+ const calculatePrice = react.useCallback(
5559
+ async (selections, overrideLocationId) => {
5560
+ if (!composite) {
5561
+ return null;
5562
+ }
5563
+ const nextRequestId = ++priceRequestIdRef.current;
5564
+ setIsPriceLoading(true);
5565
+ try {
5566
+ const result = await client.catalogue.calculateCompositePrice(
5567
+ composite.id,
5568
+ selections,
5569
+ overrideLocationId
5570
+ );
5571
+ if (!result.ok) {
5572
+ throw result.error;
5573
+ }
5574
+ if (nextRequestId === priceRequestIdRef.current) {
5575
+ setPriceResult(result.value);
5576
+ setError(null);
5577
+ }
5578
+ return result.value;
5579
+ } catch (loadError) {
5580
+ if (nextRequestId === priceRequestIdRef.current) {
5581
+ setError(loadError);
5582
+ }
5583
+ return null;
5584
+ } finally {
5585
+ if (nextRequestId === priceRequestIdRef.current) {
5586
+ setIsPriceLoading(false);
5587
+ }
5588
+ }
5589
+ },
5590
+ [client, composite]
5591
+ );
5592
+ const refetch = react.useCallback(async () => {
5593
+ compositeCache.delete(cacheKey);
5594
+ await load(true);
5595
+ }, [cacheKey, load]);
5596
+ return { composite, isLoading, error, refetch, calculatePrice, priceResult, isPriceLoading };
5597
+ }
5598
+ function useSearch(options = {}) {
5599
+ const context = useOptionalCimplify();
5600
+ const client = options.client ?? context?.client;
5601
+ if (!client) {
5602
+ throw new Error("useSearch must be used within CimplifyProvider or passed { client }.");
5603
+ }
5604
+ const minLength = Math.max(0, options.minLength ?? 2);
5605
+ const debounceMs = Math.max(0, options.debounceMs ?? 300);
5606
+ const limit = Math.max(1, options.limit ?? 20);
5607
+ const [query, setQueryState] = react.useState("");
5608
+ const [results, setResults] = react.useState([]);
5609
+ const [isLoading, setIsLoading] = react.useState(false);
5610
+ const [error, setError] = react.useState(null);
5611
+ const requestIdRef = react.useRef(0);
5612
+ const timerRef = react.useRef(null);
5613
+ react.useEffect(() => {
5614
+ if (timerRef.current) {
5615
+ clearTimeout(timerRef.current);
5616
+ timerRef.current = null;
5617
+ }
5618
+ const trimmedQuery = query.trim();
5619
+ if (trimmedQuery.length < minLength) {
5620
+ setResults([]);
5621
+ setError(null);
5622
+ setIsLoading(false);
5623
+ return;
5624
+ }
5625
+ const nextRequestId = ++requestIdRef.current;
5626
+ setError(null);
5627
+ setIsLoading(true);
5628
+ timerRef.current = setTimeout(() => {
5629
+ void (async () => {
5630
+ try {
5631
+ const result = await client.catalogue.searchProducts(trimmedQuery, {
5632
+ limit,
5633
+ category: options.category
5634
+ });
5635
+ if (!result.ok) {
5636
+ throw result.error;
5637
+ }
5638
+ if (nextRequestId === requestIdRef.current) {
5639
+ setResults(result.value);
5640
+ setError(null);
5641
+ }
5642
+ } catch (loadError) {
5643
+ if (nextRequestId === requestIdRef.current) {
5644
+ setError(loadError);
5645
+ }
5646
+ } finally {
5647
+ if (nextRequestId === requestIdRef.current) {
5648
+ setIsLoading(false);
5649
+ }
5650
+ }
5651
+ })();
5652
+ }, debounceMs);
5653
+ return () => {
5654
+ if (timerRef.current) {
5655
+ clearTimeout(timerRef.current);
5656
+ timerRef.current = null;
5657
+ }
5658
+ };
5659
+ }, [client, debounceMs, limit, minLength, options.category, query]);
5660
+ const setQuery = react.useCallback((nextQuery) => {
5661
+ setQueryState(nextQuery);
5662
+ }, []);
5663
+ const clear = react.useCallback(() => {
5664
+ requestIdRef.current += 1;
5665
+ if (timerRef.current) {
5666
+ clearTimeout(timerRef.current);
5667
+ timerRef.current = null;
5668
+ }
5669
+ setQueryState("");
5670
+ setResults([]);
5671
+ setError(null);
5672
+ setIsLoading(false);
5673
+ }, []);
5674
+ return { results, isLoading, error, query, setQuery, clear };
5675
+ }
5676
+ var quoteCache = /* @__PURE__ */ new Map();
5677
+ var quoteInflight = /* @__PURE__ */ new Map();
5678
+ function buildQuoteCacheKey(client, locationId, inputSignature) {
5679
+ return JSON.stringify({
5680
+ key: client.getPublicKey(),
5681
+ location_id: locationId || "__none__",
5682
+ input: inputSignature
5683
+ });
5684
+ }
5685
+ function isQuoteExpired(quote) {
5686
+ if (!quote?.expires_at) {
5687
+ return false;
5688
+ }
5689
+ const expiresAt = Date.parse(quote.expires_at);
5690
+ if (!Number.isFinite(expiresAt)) {
5691
+ return false;
5692
+ }
5693
+ return expiresAt <= Date.now();
5694
+ }
5695
+ function normalizeInput(input, fallbackLocationId) {
5696
+ const productId = input.productId.trim();
5697
+ const variantId = input.variantId?.trim();
5698
+ const locationId = input.locationId?.trim() || fallbackLocationId || void 0;
5699
+ return {
5700
+ product_id: productId,
5701
+ variant_id: variantId && variantId.length > 0 ? variantId : void 0,
5702
+ location_id: locationId && locationId.length > 0 ? locationId : void 0,
5703
+ quantity: input.quantity,
5704
+ add_on_option_ids: input.addOnOptionIds,
5705
+ bundle_selections: input.bundleSelections,
5706
+ composite_selections: input.compositeSelections
5707
+ };
5708
+ }
5709
+ function useQuote(input, options = {}) {
5710
+ const context = useOptionalCimplify();
5711
+ const client = options.client ?? context?.client;
5712
+ if (!client) {
5713
+ throw new Error("useQuote must be used within CimplifyProvider or passed { client }.");
5714
+ }
5715
+ const enabled = options.enabled ?? true;
5716
+ const autoRefresh = options.autoRefresh ?? true;
5717
+ const refreshBeforeExpiryMs = Math.max(0, options.refreshBeforeExpiryMs ?? 3e4);
5718
+ const locationId = client.getLocationId();
5719
+ const requestIdRef = react.useRef(0);
5720
+ const refreshTimerRef = react.useRef(null);
5721
+ const expiryTimerRef = react.useRef(null);
5722
+ const inputSignature = react.useMemo(() => JSON.stringify(input ?? null), [input]);
5723
+ const normalizedInput = react.useMemo(() => {
5724
+ if (!input) {
5725
+ return null;
5726
+ }
5727
+ const normalized = normalizeInput(input, locationId);
5728
+ return normalized.product_id.length > 0 ? normalized : null;
5729
+ }, [inputSignature, locationId]);
5730
+ const cacheKey = react.useMemo(
5731
+ () => buildQuoteCacheKey(client, locationId, inputSignature),
5732
+ [client, inputSignature, locationId]
5733
+ );
5734
+ const cached = quoteCache.get(cacheKey);
5735
+ const [quote, setQuote] = react.useState(cached?.quote ?? null);
5736
+ const [isLoading, setIsLoading] = react.useState(enabled && normalizedInput !== null && !cached);
5737
+ const [error, setError] = react.useState(null);
5738
+ const [isExpired, setIsExpired] = react.useState(isQuoteExpired(cached?.quote ?? null));
5739
+ const [messages, setMessages] = react.useState(cached?.quote?.ui_messages ?? []);
5740
+ const load = react.useCallback(
5741
+ async (force = false) => {
5742
+ if (!enabled || !normalizedInput) {
5743
+ setQuote(null);
5744
+ setMessages([]);
5745
+ setIsExpired(false);
5746
+ setError(null);
5747
+ setIsLoading(false);
5748
+ return;
5749
+ }
5750
+ const nextRequestId = ++requestIdRef.current;
5751
+ setError(null);
5752
+ if (!force) {
5753
+ const cacheEntry = quoteCache.get(cacheKey);
5754
+ if (cacheEntry) {
5755
+ setQuote(cacheEntry.quote);
5756
+ setMessages(cacheEntry.quote?.ui_messages ?? []);
5757
+ setIsExpired(isQuoteExpired(cacheEntry.quote));
5758
+ setIsLoading(false);
5759
+ return;
5760
+ }
5761
+ }
5762
+ setIsLoading(true);
5763
+ try {
5764
+ const existing = quoteInflight.get(cacheKey);
5765
+ const promise = existing ?? (async () => {
5766
+ const result = await client.catalogue.fetchQuote(normalizedInput);
5767
+ if (!result.ok) {
5768
+ throw result.error;
5769
+ }
5770
+ return result.value;
5771
+ })();
5772
+ if (!existing) {
5773
+ quoteInflight.set(cacheKey, promise);
5774
+ promise.finally(() => {
5775
+ quoteInflight.delete(cacheKey);
5776
+ }).catch(() => void 0);
5777
+ }
5778
+ const value = await promise;
5779
+ quoteCache.set(cacheKey, { quote: value });
5780
+ if (nextRequestId === requestIdRef.current) {
5781
+ setQuote(value);
5782
+ setMessages(value.ui_messages ?? []);
5783
+ setIsExpired(isQuoteExpired(value));
5784
+ setError(null);
5785
+ }
5786
+ } catch (loadError) {
5787
+ if (nextRequestId === requestIdRef.current) {
5788
+ setError(loadError);
5789
+ }
5790
+ } finally {
5791
+ if (nextRequestId === requestIdRef.current) {
5792
+ setIsLoading(false);
5793
+ }
5794
+ }
5795
+ },
5796
+ [cacheKey, client, enabled, normalizedInput]
5797
+ );
5798
+ react.useEffect(() => {
5799
+ void load(false);
5800
+ }, [load]);
5801
+ const refresh = react.useCallback(async () => {
5802
+ if (!enabled || !normalizedInput) {
5803
+ return;
5804
+ }
5805
+ if (!quote?.quote_id) {
5806
+ await load(true);
5807
+ return;
5808
+ }
5809
+ const nextRequestId = ++requestIdRef.current;
5810
+ setError(null);
5811
+ setIsLoading(true);
5812
+ try {
5813
+ const result = await client.catalogue.refreshQuote({
5814
+ quote_id: quote.quote_id,
5815
+ ...normalizedInput
5816
+ });
5817
+ if (!result.ok) {
5818
+ throw result.error;
5819
+ }
5820
+ const refreshed = result.value.quote;
5821
+ quoteCache.set(cacheKey, { quote: refreshed });
5822
+ if (nextRequestId === requestIdRef.current) {
5823
+ setQuote(refreshed);
5824
+ setMessages(refreshed.ui_messages ?? []);
5825
+ setIsExpired(isQuoteExpired(refreshed));
5826
+ setError(null);
5827
+ }
5828
+ } catch (refreshError) {
5829
+ if (nextRequestId === requestIdRef.current) {
5830
+ setError(refreshError);
5831
+ }
5832
+ } finally {
5833
+ if (nextRequestId === requestIdRef.current) {
5834
+ setIsLoading(false);
5835
+ }
5836
+ }
5837
+ }, [cacheKey, client, enabled, load, normalizedInput, quote]);
5838
+ react.useEffect(() => {
5839
+ if (expiryTimerRef.current) {
5840
+ clearTimeout(expiryTimerRef.current);
5841
+ expiryTimerRef.current = null;
5842
+ }
5843
+ const expiresAt = quote?.expires_at ? Date.parse(quote.expires_at) : NaN;
5844
+ if (!Number.isFinite(expiresAt)) {
5845
+ setIsExpired(false);
5846
+ return;
5847
+ }
5848
+ const expired = expiresAt <= Date.now();
5849
+ setIsExpired(expired);
5850
+ if (!expired) {
5851
+ expiryTimerRef.current = setTimeout(() => {
5852
+ setIsExpired(true);
5853
+ }, Math.max(0, expiresAt - Date.now()));
5854
+ }
5855
+ return () => {
5856
+ if (expiryTimerRef.current) {
5857
+ clearTimeout(expiryTimerRef.current);
5858
+ expiryTimerRef.current = null;
5859
+ }
5860
+ };
5861
+ }, [quote?.expires_at, quote?.quote_id]);
5862
+ react.useEffect(() => {
5863
+ if (refreshTimerRef.current) {
5864
+ clearTimeout(refreshTimerRef.current);
5865
+ refreshTimerRef.current = null;
5866
+ }
5867
+ if (!autoRefresh || !enabled || !quote?.expires_at) {
5868
+ return;
5869
+ }
5870
+ const expiresAt = Date.parse(quote.expires_at);
5871
+ if (!Number.isFinite(expiresAt)) {
5872
+ return;
5873
+ }
5874
+ const delay = Math.max(0, expiresAt - Date.now() - refreshBeforeExpiryMs);
5875
+ refreshTimerRef.current = setTimeout(() => {
5876
+ void refresh();
5877
+ }, delay);
5878
+ return () => {
5879
+ if (refreshTimerRef.current) {
5880
+ clearTimeout(refreshTimerRef.current);
5881
+ refreshTimerRef.current = null;
5882
+ }
5883
+ };
5884
+ }, [autoRefresh, enabled, quote?.expires_at, quote?.quote_id, refresh, refreshBeforeExpiryMs]);
5885
+ return { quote, isLoading, error, refresh, isExpired, messages };
5886
+ }
5088
5887
  var ElementsContext = react.createContext({
5089
5888
  elements: null,
5090
5889
  isReady: false
@@ -5254,10 +6053,14 @@ exports.CimplifyProvider = CimplifyProvider;
5254
6053
  exports.ElementsProvider = ElementsProvider;
5255
6054
  exports.PaymentElement = PaymentElement;
5256
6055
  exports.useAds = useAds;
6056
+ exports.useBundle = useBundle;
5257
6057
  exports.useCart = useCart;
5258
6058
  exports.useCategories = useCategories;
5259
6059
  exports.useCheckout = useCheckout;
5260
6060
  exports.useCimplify = useCimplify;
6061
+ exports.useCollection = useCollection;
6062
+ exports.useCollections = useCollections;
6063
+ exports.useComposite = useComposite;
5261
6064
  exports.useElements = useElements;
5262
6065
  exports.useElementsReady = useElementsReady;
5263
6066
  exports.useLocations = useLocations;
@@ -5265,3 +6068,5 @@ exports.useOptionalCimplify = useOptionalCimplify;
5265
6068
  exports.useOrder = useOrder;
5266
6069
  exports.useProduct = useProduct;
5267
6070
  exports.useProducts = useProducts;
6071
+ exports.useQuote = useQuote;
6072
+ exports.useSearch = useSearch;