@moonbase.sh/storefront-api 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -208,6 +208,10 @@ var productSummarySchema = z2.object({
208
208
  releaseDescription: z2.string().optional(),
209
209
  downloadsNeedsUser: z2.boolean(),
210
210
  downloadsNeedsOwnership: z2.boolean(),
211
+ downloadsNeedsGroupMembership: z2.boolean(),
212
+ // Whether this caller clears every requirement above, including the group membership the client
213
+ // can't see for itself.
214
+ downloadsAllowed: z2.boolean(),
211
215
  downloads: z2.array(downloadSchema).optional()
212
216
  });
213
217
 
@@ -695,15 +699,15 @@ var IdentityEndpoints = class {
695
699
  throw new NotFoundError("User not found");
696
700
  if (response.status >= 400)
697
701
  await handleResponseProblem(response, this.logger);
702
+ let identity;
698
703
  try {
699
- const data = await response.json();
700
- const user = identitySchema.parse(data);
701
- this.tokenStore.setUser(user);
702
- return userSchema.parse(user);
704
+ identity = identitySchema.parse(await response.json());
703
705
  } catch (err) {
704
706
  this.logger.warn("Could not sign in user", { email, response, err });
705
707
  throw new MoonbaseError("Bad response", "Could not sign in user", response.status);
706
708
  }
709
+ this.tokenStore.setUser(identity);
710
+ return userSchema.parse(identity);
707
711
  }
708
712
  async signUp(name, email, password, address, acceptedPrivacyPolicy, acceptedTermsAndConditions, communicationOptIn) {
709
713
  const path = `/api/customer/identity/sign-up?scheme=JWT&communicationOptIn=${communicationOptIn ? "true" : "false"}`;
@@ -730,14 +734,15 @@ var IdentityEndpoints = class {
730
734
  const parsed = pendingActivationSchema.parse(data);
731
735
  return { status: "activation-pending", email: parsed.email };
732
736
  }
737
+ let identity;
733
738
  try {
734
- const identity = identitySchema.parse(data);
735
- this.tokenStore.setUser(identity);
736
- return { status: "signed-in", user: userSchema.parse(identity) };
739
+ identity = identitySchema.parse(data);
737
740
  } catch (err) {
738
741
  this.logger.warn("Could not sign up user", { email, response, err });
739
742
  throw new MoonbaseError("Bad response", "Could not sign up user", response.status);
740
743
  }
744
+ this.tokenStore.setUser(identity);
745
+ return { status: "signed-in", user: userSchema.parse(identity) };
741
746
  }
742
747
  async signOut() {
743
748
  this.tokenStore.clear();
@@ -1017,6 +1022,15 @@ var MoonbaseApi = class {
1017
1022
  status: response.status,
1018
1023
  duration: finishedAt.getTime() - startedAt.getTime()
1019
1024
  });
1025
+ if (response.type === "opaqueredirect" || response.status === 0) {
1026
+ const err = new MoonbaseError(
1027
+ "Unexpected redirect",
1028
+ `The Moonbase API redirected ${path}, which the SDK is not configured to follow`,
1029
+ response.status
1030
+ );
1031
+ this.reportRequestProblem(path, request, response, err);
1032
+ throw err;
1033
+ }
1020
1034
  if (response.status >= 400) {
1021
1035
  try {
1022
1036
  await handleResponseProblem(response, this.logger);
@@ -1372,6 +1386,9 @@ var OrderEndpoints = class {
1372
1386
  if (options.checkout.returnUrl) {
1373
1387
  query.append("returnUrl", options.checkout.returnUrl);
1374
1388
  }
1389
+ if (options.checkout.embedded !== void 0) {
1390
+ query.append("embedded", options.checkout.embedded ? "true" : "false");
1391
+ }
1375
1392
  }
1376
1393
  const response = await this.api.fetch(`/api/customer/orders/${orderId}?${query.toString()}`, orderSchema, {
1377
1394
  abort: options == null ? void 0 : options.abort
@@ -1386,6 +1403,8 @@ var OrderEndpoints = class {
1386
1403
  query.checkout = "true";
1387
1404
  if (checkout.returnUrl)
1388
1405
  query.returnUrl = checkout.returnUrl;
1406
+ if (checkout.embedded !== void 0)
1407
+ query.embedded = checkout.embedded ? "true" : "false";
1389
1408
  }
1390
1409
  const response = await this.api.fetch(
1391
1410
  `/api/customer/orders/${order.id}?${new URLSearchParams(query).toString()}`,
@@ -1476,28 +1495,81 @@ var ConsoleLogger = class {
1476
1495
  };
1477
1496
 
1478
1497
  // src/utils/store.ts
1498
+ function isLocalStorageAvailable() {
1499
+ try {
1500
+ return typeof window !== "undefined" && !!window.localStorage;
1501
+ } catch (e) {
1502
+ return false;
1503
+ }
1504
+ }
1479
1505
  var LocalStorageStore = class {
1506
+ constructor(onDegraded) {
1507
+ this.onDegraded = onDegraded;
1508
+ // Values whose write to localStorage failed. Read back in preference to
1509
+ // localStorage so the rest of the session sees what it just wrote.
1510
+ this.fallback = /* @__PURE__ */ new Map();
1511
+ this.degraded = false;
1512
+ }
1513
+ get isDegraded() {
1514
+ return this.degraded;
1515
+ }
1480
1516
  get(key) {
1481
- const item = localStorage.getItem(key);
1482
- if (item) {
1483
- return JSON.parse(item);
1517
+ if (this.fallback.has(key))
1518
+ return this.fallback.get(key);
1519
+ try {
1520
+ const item = localStorage.getItem(key);
1521
+ if (item) {
1522
+ return JSON.parse(item);
1523
+ }
1524
+ return null;
1525
+ } catch (err) {
1526
+ this.reportDegraded(err);
1527
+ return null;
1484
1528
  }
1485
- return null;
1486
1529
  }
1487
1530
  set(key, item) {
1488
- localStorage.setItem(key, JSON.stringify(item));
1531
+ try {
1532
+ localStorage.setItem(key, JSON.stringify(item));
1533
+ this.fallback.delete(key);
1534
+ } catch (err) {
1535
+ this.reportDegraded(err);
1536
+ this.fallback.set(key, item);
1537
+ }
1489
1538
  }
1490
1539
  remove(key) {
1491
- localStorage.removeItem(key);
1540
+ try {
1541
+ localStorage.removeItem(key);
1542
+ this.fallback.delete(key);
1543
+ return;
1544
+ } catch (err) {
1545
+ this.reportDegraded(err);
1546
+ }
1547
+ this.fallback.set(key, null);
1548
+ try {
1549
+ localStorage.setItem(key, "");
1550
+ } catch (e) {
1551
+ }
1492
1552
  }
1493
1553
  listen(key, callback) {
1494
- if (typeof window !== "undefined") {
1495
- window.addEventListener("storage", (event) => {
1496
- if (event.key === key) {
1497
- callback(event.newValue ? JSON.parse(event.newValue) : null);
1498
- }
1499
- });
1500
- }
1554
+ if (typeof window === "undefined")
1555
+ return;
1556
+ window.addEventListener("storage", (event) => {
1557
+ if (event.key !== key)
1558
+ return;
1559
+ this.fallback.delete(key);
1560
+ try {
1561
+ callback(event.newValue ? JSON.parse(event.newValue) : null);
1562
+ } catch (err) {
1563
+ console.warn(`Moonbase could not parse the stored value for ${key}`, err);
1564
+ }
1565
+ });
1566
+ }
1567
+ reportDegraded(err) {
1568
+ var _a;
1569
+ if (this.degraded)
1570
+ return;
1571
+ this.degraded = true;
1572
+ (_a = this.onDegraded) == null ? void 0 : _a.call(this, err);
1501
1573
  }
1502
1574
  };
1503
1575
  var InMemoryStore = class {
@@ -1535,8 +1607,11 @@ var _TokenStore = class _TokenStore {
1535
1607
  this.tokens = null;
1536
1608
  this.refreshTimeoutId = null;
1537
1609
  this.refreshPromise = null;
1538
- var _a;
1539
- this.store = (_a = configuration.store) != null ? _a : typeof window !== "undefined" && localStorage ? new LocalStorageStore() : new InMemoryStore();
1610
+ var _a, _b;
1611
+ this.logger = (_a = configuration.logger) != null ? _a : new ConsoleLogger();
1612
+ this.store = (_b = configuration.store) != null ? _b : isLocalStorageAvailable() ? new LocalStorageStore((err) => this.warnAboutStorage(err)) : new InMemoryStore();
1613
+ if (typeof window !== "undefined" && !configuration.store && !(this.store instanceof LocalStorageStore))
1614
+ this.warnAboutStorage();
1540
1615
  const storedToken = this.store.get(_TokenStore.storageKey);
1541
1616
  if (storedToken) {
1542
1617
  this.tokens = {
@@ -1551,18 +1626,17 @@ var _TokenStore = class _TokenStore {
1551
1626
  return this.tokens;
1552
1627
  return null;
1553
1628
  }
1629
+ get isPersistent() {
1630
+ return this.store instanceof LocalStorageStore && !this.store.isDegraded;
1631
+ }
1554
1632
  async getAccessToken() {
1555
1633
  var _a, _b, _c;
1556
- if (this.isExpired) {
1557
- if (this.refreshPromise) {
1558
- const tokens2 = await this.refreshPromise;
1559
- return (_a = tokens2 == null ? void 0 : tokens2.accessToken) != null ? _a : null;
1560
- }
1561
- this.refreshPromise = this.refreshTokens();
1562
- const tokens = await this.refreshPromise;
1563
- return (_b = tokens == null ? void 0 : tokens.accessToken) != null ? _b : null;
1634
+ if (!this.isExpired) {
1635
+ return ((_a = this.tokens) == null ? void 0 : _a.accessToken) || null;
1564
1636
  }
1565
- return ((_c = this.tokens) == null ? void 0 : _c.accessToken) || null;
1637
+ const pending = (_b = this.refreshPromise) != null ? _b : this.startRefresh();
1638
+ const tokens = await pending;
1639
+ return (_c = tokens == null ? void 0 : tokens.accessToken) != null ? _c : null;
1566
1640
  }
1567
1641
  setUser(user) {
1568
1642
  const identity = user;
@@ -1587,7 +1661,7 @@ var _TokenStore = class _TokenStore {
1587
1661
  if (this.refreshTimeoutId != null)
1588
1662
  window.clearTimeout(this.refreshTimeoutId);
1589
1663
  this.refreshTimeoutId = window.setTimeout(() => {
1590
- this.refreshPromise = this.refreshTokens();
1664
+ this.startRefresh().catch((err) => this.logger.warn("Could not refresh access token", err));
1591
1665
  }, 10 * 60 * 1e3);
1592
1666
  }
1593
1667
  return this.tokens;
@@ -1602,10 +1676,25 @@ var _TokenStore = class _TokenStore {
1602
1676
  get isExpired() {
1603
1677
  return this.tokens != null && this.tokens.expiresAt < /* @__PURE__ */ new Date();
1604
1678
  }
1679
+ /**
1680
+ * Start a refresh and hold it so concurrent callers join it rather than firing
1681
+ * their own. The slot is cleared as soon as it settles: a retained promise
1682
+ * would hand every later expiry the same stale access token, and a rejected one
1683
+ * that stayed put would re-throw on every subsequent request (including the
1684
+ * anonymous ones) for the rest of the session.
1685
+ */
1686
+ startRefresh() {
1687
+ const pending = this.refreshTokens().finally(() => {
1688
+ if (this.refreshPromise === pending)
1689
+ this.refreshPromise = null;
1690
+ });
1691
+ this.refreshPromise = pending;
1692
+ return pending;
1693
+ }
1605
1694
  async refreshTokens() {
1606
1695
  if (!this.tokens)
1607
1696
  throw new Error("No tokens found to refresh");
1608
- const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${this.tokens.refreshToken}`, {
1697
+ const response = await fetch(`${this.configuration.endpoint}/api/customer/identity/refresh?token=${encodeURIComponent(this.tokens.refreshToken)}`, {
1609
1698
  method: "POST",
1610
1699
  headers: {
1611
1700
  "Accept": "application/json",
@@ -1627,12 +1716,18 @@ var _TokenStore = class _TokenStore {
1627
1716
  }
1628
1717
  handleStorageUpdate(updatedTokens) {
1629
1718
  this.tokens = updatedTokens;
1630
- if (this.refreshTimeoutId != null)
1719
+ if (this.refreshTimeoutId != null && typeof window !== "undefined")
1631
1720
  window.clearTimeout(this.refreshTimeoutId);
1632
1721
  if (this.tokens) {
1633
1722
  this.tokens.expiresAt = new Date(this.tokens.expiresAt);
1634
1723
  }
1635
1724
  }
1725
+ warnAboutStorage(err) {
1726
+ this.logger.warn(
1727
+ "Moonbase could not persist credentials to localStorage: this browser is blocking or has exhausted site storage. Sign-in will work for this page load but will not survive a reload.",
1728
+ err
1729
+ );
1730
+ }
1636
1731
  };
1637
1732
  _TokenStore.storageKey = "moonbase_auth";
1638
1733
  var TokenStore = _TokenStore;
@@ -1820,10 +1915,29 @@ var OfferUtils = class _OfferUtils {
1820
1915
  static relevantTargets(offer, cartItems) {
1821
1916
  if (offer.targets.length > 1) {
1822
1917
  const allOwned = offer.targets.every((t) => t.item.owned);
1823
- return offer.targets.filter((t) => (allOwned || !t.item.owned) && !cartItems.some((l) => l.type === t.item.type && t.item.id === (l.type === "Product" ? l.productId : l.bundleId)));
1918
+ return offer.targets.filter((t) => (allOwned || !t.item.owned) && !_OfferUtils.targetInCart(t, cartItems));
1824
1919
  }
1825
1920
  return offer.targets;
1826
1921
  }
1922
+ /**
1923
+ * Whether every item an offer targets is already in the cart, so the offer has no upsell left to
1924
+ * show.
1925
+ *
1926
+ * This is the set of offers the cart hides: a card inviting the buyer to add what they have already
1927
+ * added says nothing, so it is dropped. Which makes it exactly the set whose discount nothing can
1928
+ * currently claim: an offer is only good for one line, and there is no card left to spend it from.
1929
+ *
1930
+ * An offer with no targets names nothing and so is never in this set.
1931
+ */
1932
+ static allTargetsInCart(offer, cartItems) {
1933
+ return offer.targets.length > 0 && offer.targets.every((target) => _OfferUtils.targetInCart(target, cartItems));
1934
+ }
1935
+ // Matched on the item alone, not the variation: a target narrowed to one variation still counts as
1936
+ // in the cart when another variation of it is. That is the reading the cart's own upsell has always
1937
+ // taken, and the two have to agree or an offer could be hidden and auto-applied at once.
1938
+ static targetInCart(target, cartItems) {
1939
+ return cartItems.some((l) => l.type === target.item.type && target.item.id === (l.type === "Product" ? l.productId : l.bundleId));
1940
+ }
1827
1941
  // The pricing variations a target restricts the offer to. An empty (or missing) variation list
1828
1942
  // means the offer is not narrowed to specific variations, in which case we keep the established
1829
1943
  // single default-variation upsell.
@@ -1854,7 +1968,7 @@ var OfferUtils = class _OfferUtils {
1854
1968
  };
1855
1969
 
1856
1970
  // src/utils/cart.ts
1857
- var CartUtils = class _CartUtils {
1971
+ var _CartUtils = class _CartUtils {
1858
1972
  // The pricing variation a line item points at, falling back to the item's default variation when
1859
1973
  // the variation id doesn't resolve, which is what the cart has always displayed.
1860
1974
  static resolveVariation(catalog, item) {
@@ -1912,8 +2026,13 @@ var CartUtils = class _CartUtils {
1912
2026
  // Which lines a cart offer discounts. No targets means the whole cart, a reading that only holds
1913
2027
  // for cart-scope offers, where an empty target list is the API's way of saying "every line".
1914
2028
  static cartOfferAppliesToLine(offer, item) {
1915
- if (offer.targets.length === 0)
1916
- return true;
2029
+ return offer.targets.length === 0 || _CartUtils.offerTargetsLine(offer, item);
2030
+ }
2031
+ // Whether an offer names this line among its targets. An empty variation list on a target leaves
2032
+ // the offer open to every variation of that item. An offer with no targets at all names nothing
2033
+ // here, which is why the "every line" reading stays in `cartOfferAppliesToLine`: it is only true
2034
+ // of a cart offer.
2035
+ static offerTargetsLine(offer, item) {
1917
2036
  return offer.targets.some(({ item: target, variations }) => target.type === item.type && target.id === (item.type === "Product" ? item.productId : item.bundleId) && (variations.length === 0 || variations.includes(item.variationId)));
1918
2037
  }
1919
2038
  // The cart offer actually discounting this cart, as opposed to the one attached to the order. An
@@ -1990,6 +2109,148 @@ var CartUtils = class _CartUtils {
1990
2109
  return aggregate + DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency] * item.quantity;
1991
2110
  }, 0);
1992
2111
  }
2112
+ /**
2113
+ * The item-scope offer worth attaching to a single line, or `undefined` when none is.
2114
+ *
2115
+ * Ranked by what it would actually take off this line rather than by percentage, the same way
2116
+ * `bestCartOffer` ranks a cart: a flat amount off can beat a bigger-looking percentage on a cheap
2117
+ * line, and the other way round on an expensive one.
2118
+ *
2119
+ * `unavailableOfferIds` are offers the rest of the cart has already claimed. An offer is only ever
2120
+ * good for one line, since the cart treats an offer held by any line as in use. So a caller walking
2121
+ * the cart must reserve each one as it goes, or two lines would both count on the same offer and
2122
+ * the API would honour it once. `assignItemOffers` does that bookkeeping for a whole cart.
2123
+ */
2124
+ static bestItemOffer(catalog, item, currency, context, unavailableOfferIds) {
2125
+ let best;
2126
+ for (const candidate of _CartUtils.itemOfferCandidates(catalog, [item], currency, context, unavailableOfferIds)) {
2127
+ if (!best || candidate.value > best.value)
2128
+ best = candidate;
2129
+ }
2130
+ return best == null ? void 0 : best.offer;
2131
+ }
2132
+ /**
2133
+ * Which item-scope offer to attach to which line, for the lines not already carrying one.
2134
+ *
2135
+ * Chosen for what the pairings are worth together rather than by cart order. An offer is only good
2136
+ * for one line, so walking the cart and taking the best offer for each line in turn hands a
2137
+ * multi-target offer to whichever line the buyer happened to add first, which is arbitrary and can
2138
+ * be the cheapest one. A buyer claiming these from the upsell cards would have placed them where
2139
+ * they are worth most, so that is what this does.
2140
+ *
2141
+ * Lines already holding an offer are left alone and their offers reserved: an offer on a line is
2142
+ * intent, whether the buyer's from the upsell or the API's from a refreshed order, and this is not
2143
+ * the place to second-guess it.
2144
+ */
2145
+ static assignItemOffers(catalog, order, currency, context) {
2146
+ const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
2147
+ const claimed = new Set(order.items.map((i) => i.offerId).filter((id) => !!id));
2148
+ return _CartUtils.maximalItemOfferAssignment(
2149
+ _CartUtils.itemOfferCandidates(catalog, order.items.filter((i) => !i.offerId), currency, conditionContext, claimed)
2150
+ );
2151
+ }
2152
+ /**
2153
+ * The pairings worth the most together, given each offer can only be spent once.
2154
+ *
2155
+ * Taking the most valuable pairing first is not enough: with one offer worth 100 on line A and 99 on
2156
+ * line B, and a second offer worth 98 on line A alone, claiming the 100 strands the second offer and
2157
+ * saves 100, where putting the first offer on B and the second on A saves 197. The pairings have to
2158
+ * be chosen as a set.
2159
+ *
2160
+ * Walked line by line, memoised on which offers are still free. The candidates have already been
2161
+ * through targeting and eligibility, so in a real cart there are a handful; past `searchableOffers`
2162
+ * distinct offers it takes the most valuable pairing first instead, which is never worse than
2163
+ * applying nothing.
2164
+ */
2165
+ static maximalItemOfferAssignment(candidates) {
2166
+ var _a;
2167
+ const optionsByItem = /* @__PURE__ */ new Map();
2168
+ const offerBits = /* @__PURE__ */ new Map();
2169
+ for (const { item, offer, value } of candidates) {
2170
+ let bit = offerBits.get(offer.id);
2171
+ if (bit === void 0) {
2172
+ bit = offerBits.size;
2173
+ offerBits.set(offer.id, bit);
2174
+ }
2175
+ const options = (_a = optionsByItem.get(item.id)) != null ? _a : [];
2176
+ options.push({ offerId: offer.id, bit, value });
2177
+ optionsByItem.set(item.id, options);
2178
+ }
2179
+ if (offerBits.size > _CartUtils.searchableOffers)
2180
+ return _CartUtils.mostValuableItemOfferPairings(candidates);
2181
+ const itemIds = [...optionsByItem.keys()];
2182
+ const memo = /* @__PURE__ */ new Map();
2183
+ const from = (index, spent) => {
2184
+ var _a2;
2185
+ if (index >= itemIds.length)
2186
+ return { value: 0, assignments: [] };
2187
+ const key = `${index}:${spent}`;
2188
+ const memoised = memo.get(key);
2189
+ if (memoised)
2190
+ return memoised;
2191
+ let best = from(index + 1, spent);
2192
+ const itemId = itemIds[index];
2193
+ for (const option of (_a2 = optionsByItem.get(itemId)) != null ? _a2 : []) {
2194
+ if (spent & 1 << option.bit)
2195
+ continue;
2196
+ const rest = from(index + 1, spent | 1 << option.bit);
2197
+ const value = option.value + rest.value;
2198
+ if (value > best.value)
2199
+ best = { value, assignments: [{ itemId, offerId: option.offerId }, ...rest.assignments] };
2200
+ }
2201
+ memo.set(key, best);
2202
+ return best;
2203
+ };
2204
+ return from(0, 0).assignments;
2205
+ }
2206
+ // The fallback for a candidate set too large to search: repeatedly take the most valuable pairing
2207
+ // still available. Can strand a better combination, which is the whole reason it is only a fallback.
2208
+ static mostValuableItemOfferPairings(candidates) {
2209
+ const assignments = [];
2210
+ const assignedItemIds = /* @__PURE__ */ new Set();
2211
+ const spentOfferIds = /* @__PURE__ */ new Set();
2212
+ for (const { item, offer } of [...candidates].sort((a, b) => b.value - a.value)) {
2213
+ if (assignedItemIds.has(item.id) || spentOfferIds.has(offer.id))
2214
+ continue;
2215
+ assignedItemIds.add(item.id);
2216
+ spentOfferIds.add(offer.id);
2217
+ assignments.push({ itemId: item.id, offerId: offer.id });
2218
+ }
2219
+ return assignments;
2220
+ }
2221
+ // Every line-and-offer pairing the cart could make, and what each would save. Enumerated in cart
2222
+ // order and then catalog order, which `sort` preserves for equal savings, so an unchanged cart
2223
+ // always resolves the same way.
2224
+ static itemOfferCandidates(catalog, items, currency, context, unavailableOfferIds) {
2225
+ var _a;
2226
+ const candidates = [];
2227
+ for (const item of items) {
2228
+ const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
2229
+ if (unitPrice === void 0)
2230
+ continue;
2231
+ for (const offer of (_a = catalog.offers) != null ? _a : []) {
2232
+ if (offer.scope === "Cart" || (unavailableOfferIds == null ? void 0 : unavailableOfferIds.has(offer.id)))
2233
+ continue;
2234
+ if (!_CartUtils.offerTargetsLine(offer, item))
2235
+ continue;
2236
+ if (!OfferUtils.allTargetsInCart(offer, context.order.items))
2237
+ continue;
2238
+ if (!OfferUtils.eligible(offer, context))
2239
+ continue;
2240
+ const value = _CartUtils.itemOfferUnitDiscount(offer, unitPrice, currency) * item.quantity;
2241
+ if (value <= 0)
2242
+ continue;
2243
+ candidates.push({ item, offer, value });
2244
+ }
2245
+ }
2246
+ return candidates;
2247
+ }
2248
+ // What an item offer takes off one unit of a line. Measured off the post-product-discount price,
2249
+ // which is what an item offer comes off in the API's order: product discount -> line offer -> cart
2250
+ // offer -> coupons.
2251
+ static itemOfferUnitDiscount(offer, unitPrice, currency) {
2252
+ return DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency];
2253
+ }
1993
2254
  // A line's unit price once its own offer has come off, which is what a cart offer is applied to:
1994
2255
  // the API discounts in the order product discount -> line offer -> cart offer -> coupons. An
1995
2256
  // unresolvable price counts as zero, matching `total`'s "still render something" stance rather
@@ -2000,9 +2261,14 @@ var CartUtils = class _CartUtils {
2000
2261
  const itemOffer = item.offerId ? (_b = catalog.offers) == null ? void 0 : _b.find((o) => o.id === item.offerId) : void 0;
2001
2262
  if (!itemOffer || !OfferUtils.eligible(itemOffer, context))
2002
2263
  return unitPrice;
2003
- return unitPrice - DiscountUtils.apply(itemOffer.discount, { [currency]: unitPrice })[currency];
2264
+ return unitPrice - _CartUtils.itemOfferUnitDiscount(itemOffer, unitPrice, currency);
2004
2265
  }
2005
2266
  };
2267
+ // How many distinct offers can compete for the same cart before the exact search is given up on.
2268
+ // 12 leaves 4096 states per line, which is nothing, and no real storefront has a dozen item offers
2269
+ // eligible against one cart at once.
2270
+ _CartUtils.searchableOffers = 12;
2271
+ var CartUtils = _CartUtils;
2006
2272
 
2007
2273
  // src/utils/image.ts
2008
2274
  var imageWidths = [16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048];
@@ -2193,6 +2459,7 @@ export {
2193
2459
  SubscriptionStatus,
2194
2460
  TokenStore,
2195
2461
  imageWidths,
2462
+ isLocalStorageAvailable,
2196
2463
  isPromotionDisplay,
2197
2464
  isTransformableImageUrl,
2198
2465
  maxImageDimension,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moonbase.sh/storefront-api",
3
3
  "type": "module",
4
- "version": "3.0.0",
4
+ "version": "3.2.0",
5
5
  "description": "Package to let you build storefronts with Moonbase.sh as payment and delivery provider",
6
6
  "author": "Tobias Lønnerød Madsen <m@dsen.tv>",
7
7
  "license": "MIT",