@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/README.md +11 -1
- package/dist/index.cjs +307 -39
- package/dist/index.d.cts +139 -7
- package/dist/index.d.ts +139 -7
- package/dist/index.js +306 -39
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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) && !
|
|
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
|
|
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
|
-
|
|
1916
|
-
|
|
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 -
|
|
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.
|
|
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",
|