@moonbase.sh/storefront-api 2.5.0 → 3.1.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 +21 -0
- package/dist/index.cjs +634 -18
- package/dist/index.d.cts +8055 -8021
- package/dist/index.d.ts +8055 -8021
- package/dist/index.js +627 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,10 +18,14 @@ import { z as z4 } from "zod";
|
|
|
18
18
|
var schemas_exports2 = {};
|
|
19
19
|
__export(schemas_exports2, {
|
|
20
20
|
offerConditionSchema: () => offerConditionSchema,
|
|
21
|
+
offerScopeSchema: () => offerScopeSchema,
|
|
21
22
|
storefrontBundleSchema: () => storefrontBundleSchema,
|
|
22
23
|
storefrontOfferSchema: () => storefrontOfferSchema,
|
|
23
24
|
storefrontOfferTargetSchema: () => storefrontOfferTargetSchema,
|
|
24
25
|
storefrontProductSchema: () => storefrontProductSchema,
|
|
26
|
+
storefrontPromotionCtaSchema: () => storefrontPromotionCtaSchema,
|
|
27
|
+
storefrontPromotionSchema: () => storefrontPromotionSchema,
|
|
28
|
+
storefrontPromotionTargetSchema: () => storefrontPromotionTargetSchema,
|
|
25
29
|
storefrontSchema: () => storefrontSchema,
|
|
26
30
|
taxEstimateSchema: () => taxEstimateSchema
|
|
27
31
|
});
|
|
@@ -61,6 +65,11 @@ function utmToObject(utm) {
|
|
|
61
65
|
|
|
62
66
|
// src/globalSchemas.ts
|
|
63
67
|
var priceCollectionSchema = z.record(z.number());
|
|
68
|
+
var discountPromotionFields = {
|
|
69
|
+
promotionId: z.string().optional(),
|
|
70
|
+
validFrom: z.string().optional(),
|
|
71
|
+
validUntil: z.string().optional()
|
|
72
|
+
};
|
|
64
73
|
var percentageOffDiscountSchema = z.object({
|
|
65
74
|
type: z.literal("PercentageOffDiscount"),
|
|
66
75
|
name: z.string(),
|
|
@@ -68,7 +77,8 @@ var percentageOffDiscountSchema = z.object({
|
|
|
68
77
|
percentage: z.number(),
|
|
69
78
|
total: priceCollectionSchema.optional(),
|
|
70
79
|
isExclusive: z.boolean(),
|
|
71
|
-
recurringPaymentUseCount: z.number().optional()
|
|
80
|
+
recurringPaymentUseCount: z.number().optional(),
|
|
81
|
+
...discountPromotionFields
|
|
72
82
|
});
|
|
73
83
|
var flatAmountOffDiscountSchema = z.object({
|
|
74
84
|
type: z.literal("FlatAmountOffDiscount"),
|
|
@@ -76,7 +86,8 @@ var flatAmountOffDiscountSchema = z.object({
|
|
|
76
86
|
description: z.string().optional(),
|
|
77
87
|
total: priceCollectionSchema.optional(),
|
|
78
88
|
isExclusive: z.boolean(),
|
|
79
|
-
recurringPaymentUseCount: z.number().optional()
|
|
89
|
+
recurringPaymentUseCount: z.number().optional(),
|
|
90
|
+
...discountPromotionFields
|
|
80
91
|
});
|
|
81
92
|
var moneySchema = z.object({
|
|
82
93
|
currency: z.string(),
|
|
@@ -110,6 +121,7 @@ var rawPropertyValueSchema = z.lazy(
|
|
|
110
121
|
z.string(),
|
|
111
122
|
z.number(),
|
|
112
123
|
z.boolean(),
|
|
124
|
+
z.null(),
|
|
113
125
|
z.record(rawPropertyValueSchema),
|
|
114
126
|
z.array(rawPropertyValueSchema)
|
|
115
127
|
])
|
|
@@ -196,6 +208,10 @@ var productSummarySchema = z2.object({
|
|
|
196
208
|
releaseDescription: z2.string().optional(),
|
|
197
209
|
downloadsNeedsUser: z2.boolean(),
|
|
198
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(),
|
|
199
215
|
downloads: z2.array(downloadSchema).optional()
|
|
200
216
|
});
|
|
201
217
|
|
|
@@ -236,7 +252,22 @@ var cartContainsItemsConditionSchema = z3.object({
|
|
|
236
252
|
minimumItems: z3.number(),
|
|
237
253
|
relevantItemVariations: z3.record(z3.string(), z3.string().array())
|
|
238
254
|
});
|
|
239
|
-
var
|
|
255
|
+
var cartTotalConditionSchema = z3.object({
|
|
256
|
+
type: z3.literal("CartTotal"),
|
|
257
|
+
// Both bounds are optional, but never both absent. They serialize as an explicit `null` when
|
|
258
|
+
// unset, hence `.nullish()` rather than `.optional()`. The API converts them into every enabled
|
|
259
|
+
// currency up front, precisely so we can tell the buyer how much further they have to go without
|
|
260
|
+
// doing any currency conversion ourselves.
|
|
261
|
+
minimumTotal: priceCollectionSchema.nullish(),
|
|
262
|
+
maximumTotal: priceCollectionSchema.nullish()
|
|
263
|
+
});
|
|
264
|
+
var knownOfferConditionSchema = z3.discriminatedUnion("type", [
|
|
265
|
+
cartContainsItemsConditionSchema,
|
|
266
|
+
cartTotalConditionSchema
|
|
267
|
+
]);
|
|
268
|
+
var unsupportedOfferConditionSchema = z3.object({ type: z3.string() }).passthrough().transform(({ type }) => ({ type: "Unsupported", unsupportedType: type }));
|
|
269
|
+
var offerConditionSchema = z3.union([knownOfferConditionSchema, unsupportedOfferConditionSchema]);
|
|
270
|
+
var offerScopeSchema = z3.enum(["Item", "Cart"]).catch("Item").optional().default("Item");
|
|
240
271
|
var offerTargetItemSchema = z3.union([storefrontProductSchema, storefrontBundleSchema]);
|
|
241
272
|
var storefrontOfferTargetSchema = z3.object({
|
|
242
273
|
// The product or bundle the offer applies to.
|
|
@@ -252,8 +283,10 @@ function legacyOfferTargets(target, targetVariations) {
|
|
|
252
283
|
}
|
|
253
284
|
var storefrontOfferSchema = z3.object({
|
|
254
285
|
id: z3.string(),
|
|
286
|
+
scope: offerScopeSchema,
|
|
255
287
|
// Each target pairs a product/bundle with the pricing variations the offer applies to (an empty
|
|
256
|
-
// list means all variations). This is the model to consume.
|
|
288
|
+
// list means all variations). This is the model to consume. A cart offer may have no targets at
|
|
289
|
+
// all, which means it discounts every line item.
|
|
257
290
|
targets: z3.array(storefrontOfferTargetSchema).optional(),
|
|
258
291
|
// Legacy fields, kept optional only so old cached storefront responses still parse. The API now
|
|
259
292
|
// exposes the unified `targets` model; we derive `targets` from these when a response predates it.
|
|
@@ -267,6 +300,57 @@ var storefrontOfferSchema = z3.object({
|
|
|
267
300
|
...rest,
|
|
268
301
|
targets: targets != null ? targets : legacyOfferTargets(target, targetVariations)
|
|
269
302
|
}));
|
|
303
|
+
var storefrontPromotionTargetSchema = z3.object({
|
|
304
|
+
// A prefixed reference to the item the sale covers, `Product/<id>` or `Bundle/<id>`. The item
|
|
305
|
+
// itself is already serialized in `products`/`bundles`; embedding it again would duplicate the
|
|
306
|
+
// bulk of the payload for a sale spanning dozens of products.
|
|
307
|
+
referenceId: z3.string(),
|
|
308
|
+
// The pricing variation IDs the sale is restricted to; an empty list means all variations.
|
|
309
|
+
variations: z3.string().array()
|
|
310
|
+
});
|
|
311
|
+
var storefrontPromotionCtaSchema = z3.object({
|
|
312
|
+
url: z3.string(),
|
|
313
|
+
label: z3.string()
|
|
314
|
+
});
|
|
315
|
+
var storefrontPromotionSchema = z3.object({
|
|
316
|
+
id: z3.string(),
|
|
317
|
+
name: z3.string(),
|
|
318
|
+
description: z3.string().nullish(),
|
|
319
|
+
imageUrl: z3.string().nullish(),
|
|
320
|
+
// Absent when the merchant has not set one, in which case the storefront decides whether the
|
|
321
|
+
// sale leads anywhere.
|
|
322
|
+
cta: storefrontPromotionCtaSchema.nullish(),
|
|
323
|
+
// How the merchant wants the sale surfaced, e.g. `['Banner', 'Popup']`; an empty list means no
|
|
324
|
+
// preference. Kept as free-form strings and narrowed with `isPromotionDisplay`, so a display
|
|
325
|
+
// mode added server-side later is ignored rather than failing the whole storefront.
|
|
326
|
+
display: z3.string().array(),
|
|
327
|
+
// The promotion's *intended* scope, not what is actually showing the sale price: each variation
|
|
328
|
+
// displays only its single best discount, so a targeted item may be showing a better one. The
|
|
329
|
+
// authoritative signal is `promotionId` on a variation's own discount.
|
|
330
|
+
targets: storefrontPromotionTargetSchema.array(),
|
|
331
|
+
// The promotion's own discount, for banner copy ("20% off"). Per-item prices are already
|
|
332
|
+
// discounted on each variation.
|
|
333
|
+
discount: discountSchema,
|
|
334
|
+
validFrom: z3.string().nullish(),
|
|
335
|
+
validUntil: z3.string().nullish(),
|
|
336
|
+
properties: z3.record(rawPropertyValueSchema).nullish()
|
|
337
|
+
});
|
|
338
|
+
var storefrontOffersSchema = z3.array(z3.unknown()).transform((offers) => offers.flatMap((offer) => {
|
|
339
|
+
const parsed = storefrontOfferSchema.safeParse(offer);
|
|
340
|
+
if (!parsed.success) {
|
|
341
|
+
console.warn("Ignoring storefront offer that could not be parsed:", parsed.error.issues);
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return [parsed.data];
|
|
345
|
+
}));
|
|
346
|
+
var storefrontPromotionsSchema = z3.array(z3.unknown()).transform((promotions) => promotions.flatMap((promotion) => {
|
|
347
|
+
const parsed = storefrontPromotionSchema.safeParse(promotion);
|
|
348
|
+
if (!parsed.success) {
|
|
349
|
+
console.warn("Ignoring storefront promotion that could not be parsed:", parsed.error.issues);
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
return [parsed.data];
|
|
353
|
+
}));
|
|
270
354
|
var taxEstimateSchema = z3.object({
|
|
271
355
|
rate: z3.number(),
|
|
272
356
|
mode: z3.enum(["Exclusive", "Inclusive"]),
|
|
@@ -280,7 +364,9 @@ var storefrontSchema = z3.object({
|
|
|
280
364
|
products: storefrontProductSchema.array(),
|
|
281
365
|
bundles: storefrontBundleSchema.array(),
|
|
282
366
|
// Offers need to be optional since we may still have old, cached representations in browsers
|
|
283
|
-
offers:
|
|
367
|
+
offers: storefrontOffersSchema.optional(),
|
|
368
|
+
// Promotions need to be optional since we may still have old, cached representations in browsers
|
|
369
|
+
promotions: storefrontPromotionsSchema.optional(),
|
|
284
370
|
// Depending on circumstances, we may have a tax estimate available
|
|
285
371
|
estimatedTax: taxEstimateSchema.optional()
|
|
286
372
|
});
|
|
@@ -1026,6 +1112,7 @@ import { z as z13 } from "zod";
|
|
|
1026
1112
|
var schemas_exports7 = {};
|
|
1027
1113
|
__export(schemas_exports7, {
|
|
1028
1114
|
completedOrderSchema: () => completedOrderSchema,
|
|
1115
|
+
lineItemTotalSchema: () => lineItemTotalSchema,
|
|
1029
1116
|
openBundleLineItem: () => openBundleLineItem,
|
|
1030
1117
|
openOrderLineItem: () => openOrderLineItem,
|
|
1031
1118
|
openOrderSchema: () => openOrderSchema,
|
|
@@ -1061,7 +1148,10 @@ var lineItemTotalSchema = z12.object({
|
|
|
1061
1148
|
original: moneySchema,
|
|
1062
1149
|
discount: moneySchema,
|
|
1063
1150
|
subtotal: moneySchema,
|
|
1064
|
-
due: moneySchema
|
|
1151
|
+
due: moneySchema,
|
|
1152
|
+
// This line's per-unit share of the order's cart offer, kept apart from the offer the buyer picked
|
|
1153
|
+
// for this item. Both are part of `discount`. Absent on orders priced before cart offers existed.
|
|
1154
|
+
cartOfferDiscount: moneySchema.optional()
|
|
1065
1155
|
});
|
|
1066
1156
|
var openProductLineItem = z12.object({
|
|
1067
1157
|
id: z12.string(),
|
|
@@ -1113,7 +1203,10 @@ var orderTotalSchema = z12.object({
|
|
|
1113
1203
|
discount: moneySchema,
|
|
1114
1204
|
subtotal: moneySchema,
|
|
1115
1205
|
taxes: moneySchema,
|
|
1116
|
-
due: moneySchema
|
|
1206
|
+
due: moneySchema,
|
|
1207
|
+
// How much of `discount` came from the cart offer, so it can be shown as one order-wide saving
|
|
1208
|
+
// rather than a share on every line. Absent when no cart offer applied.
|
|
1209
|
+
cartOfferDiscount: moneySchema.optional()
|
|
1117
1210
|
});
|
|
1118
1211
|
var openOrderSchema = z12.object({
|
|
1119
1212
|
id: z12.string(),
|
|
@@ -1121,6 +1214,12 @@ var openOrderSchema = z12.object({
|
|
|
1121
1214
|
total: orderTotalSchema.nullish(),
|
|
1122
1215
|
items: openOrderLineItem.array(),
|
|
1123
1216
|
couponsApplied: couponSchema.array(),
|
|
1217
|
+
// The cart offer on the order as a whole: the buyer's intent. It is part of the cart payload, so
|
|
1218
|
+
// a content push that leaves it out clears it. The API echoes it back even while the cart sits
|
|
1219
|
+
// below the offer's threshold.
|
|
1220
|
+
offerId: z12.string().optional(),
|
|
1221
|
+
// The cart offer as actually applied: the result. Absent while the offer's condition doesn't hold.
|
|
1222
|
+
appliedOffer: discountSchema.optional(),
|
|
1124
1223
|
checkoutUrl: z12.string().optional(),
|
|
1125
1224
|
hostedCheckoutUrl: z12.string().optional(),
|
|
1126
1225
|
embeddedCheckoutUrl: z12.string().optional()
|
|
@@ -1140,7 +1239,8 @@ var completedOrderSchema = z12.object({
|
|
|
1140
1239
|
customer: customerSnapshotSchema,
|
|
1141
1240
|
total: orderTotalSchema,
|
|
1142
1241
|
items: openOrderLineItem.array(),
|
|
1143
|
-
couponsApplied: couponSchema.array()
|
|
1242
|
+
couponsApplied: couponSchema.array(),
|
|
1243
|
+
appliedOffer: discountSchema.optional()
|
|
1144
1244
|
});
|
|
1145
1245
|
var orderSchema = z12.discriminatedUnion("status", [
|
|
1146
1246
|
openOrderSchema.extend({
|
|
@@ -1276,6 +1376,9 @@ var OrderEndpoints = class {
|
|
|
1276
1376
|
if (options.checkout.returnUrl) {
|
|
1277
1377
|
query.append("returnUrl", options.checkout.returnUrl);
|
|
1278
1378
|
}
|
|
1379
|
+
if (options.checkout.embedded !== void 0) {
|
|
1380
|
+
query.append("embedded", options.checkout.embedded ? "true" : "false");
|
|
1381
|
+
}
|
|
1279
1382
|
}
|
|
1280
1383
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}?${query.toString()}`, orderSchema, {
|
|
1281
1384
|
abort: options == null ? void 0 : options.abort
|
|
@@ -1290,6 +1393,8 @@ var OrderEndpoints = class {
|
|
|
1290
1393
|
query.checkout = "true";
|
|
1291
1394
|
if (checkout.returnUrl)
|
|
1292
1395
|
query.returnUrl = checkout.returnUrl;
|
|
1396
|
+
if (checkout.embedded !== void 0)
|
|
1397
|
+
query.embedded = checkout.embedded ? "true" : "false";
|
|
1293
1398
|
}
|
|
1294
1399
|
const response = await this.api.fetch(
|
|
1295
1400
|
`/api/customer/orders/${order.id}?${new URLSearchParams(query).toString()}`,
|
|
@@ -1298,6 +1403,7 @@ var OrderEndpoints = class {
|
|
|
1298
1403
|
method: "PATCH",
|
|
1299
1404
|
body: {
|
|
1300
1405
|
currency: order.currency || void 0,
|
|
1406
|
+
offerId: order.offerId,
|
|
1301
1407
|
items: order.items
|
|
1302
1408
|
}
|
|
1303
1409
|
}
|
|
@@ -1312,6 +1418,12 @@ var OrderEndpoints = class {
|
|
|
1312
1418
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}/billing-details`, openOrderSchema, { method: "DELETE" });
|
|
1313
1419
|
return response.data;
|
|
1314
1420
|
}
|
|
1421
|
+
// Takes the cart offer off the order without re-sending the cart. Offers picked for a single item
|
|
1422
|
+
// come off by pushing that item without its `offerId` instead.
|
|
1423
|
+
async removeOffer(orderId) {
|
|
1424
|
+
const response = await this.api.fetch(`/api/customer/orders/${orderId}/offer`, openOrderSchema, { method: "DELETE" });
|
|
1425
|
+
return response.data;
|
|
1426
|
+
}
|
|
1315
1427
|
async addCouponCode(orderId, code) {
|
|
1316
1428
|
const response = await this.api.fetch(`/api/customer/orders/${orderId}/coupon?code=${encodeURIComponent(code)}`, openOrderSchema, { method: "PATCH" });
|
|
1317
1429
|
return response.data;
|
|
@@ -1616,6 +1728,12 @@ __export(schemas_exports11, {
|
|
|
1616
1728
|
subscriptions: () => schemas_exports8
|
|
1617
1729
|
});
|
|
1618
1730
|
|
|
1731
|
+
// src/storefront/models.ts
|
|
1732
|
+
var promotionDisplays = ["Banner", "Popup"];
|
|
1733
|
+
function isPromotionDisplay(value) {
|
|
1734
|
+
return promotionDisplays.includes(value);
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1619
1737
|
// src/utils/discount.ts
|
|
1620
1738
|
var DiscountUtils = class {
|
|
1621
1739
|
static apply(discount, price) {
|
|
@@ -1642,25 +1760,68 @@ var MoneyCollectionUtils = class {
|
|
|
1642
1760
|
};
|
|
1643
1761
|
|
|
1644
1762
|
// src/utils/offer.ts
|
|
1763
|
+
function evaluated(unit, currency, current, target, eligible) {
|
|
1764
|
+
return {
|
|
1765
|
+
eligible,
|
|
1766
|
+
progress: {
|
|
1767
|
+
unit,
|
|
1768
|
+
currency,
|
|
1769
|
+
current,
|
|
1770
|
+
target,
|
|
1771
|
+
remaining: Math.max(0, target - current),
|
|
1772
|
+
fraction: target <= 0 ? 1 : Math.min(1, Math.max(0, current / target)),
|
|
1773
|
+
eligible
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1645
1777
|
var OfferUtils = class _OfferUtils {
|
|
1646
|
-
|
|
1647
|
-
|
|
1778
|
+
// Callers that only have an order keep working and simply have no cart total, so a monetary
|
|
1779
|
+
// condition fails closed for them. Pass the full context to have `CartTotal` actually evaluate.
|
|
1780
|
+
static eligible(offer, context) {
|
|
1781
|
+
return _OfferUtils.evaluate(offer, context).eligible;
|
|
1782
|
+
}
|
|
1783
|
+
// How close the cart is to satisfying the offer, or `null` when there is nothing to show progress
|
|
1784
|
+
// toward: an unsupported condition, an unknown cart total, or a condition with only an upper
|
|
1785
|
+
// bound (which the buyer starts out satisfying rather than working toward).
|
|
1786
|
+
static progress(offer, context) {
|
|
1787
|
+
return _OfferUtils.evaluate(offer, context).progress;
|
|
1788
|
+
}
|
|
1789
|
+
// The single evaluator behind both faces, so the "spend €12 more" nudge and the gate that unlocks
|
|
1790
|
+
// the discount can never disagree about the same cart.
|
|
1791
|
+
static evaluate(offer, context) {
|
|
1792
|
+
var _a, _b;
|
|
1793
|
+
const { order, cartTotal } = "items" in context ? { order: context, cartTotal: void 0 } : context;
|
|
1794
|
+
const condition = offer.condition;
|
|
1795
|
+
switch (condition.type) {
|
|
1648
1796
|
case "CartContainsItems": {
|
|
1649
1797
|
const relevantItems = order.items.filter(
|
|
1650
|
-
(i) => i.type === "Product" &&
|
|
1798
|
+
(i) => i.type === "Product" && condition.relevantItemVariations[`Product/${i.productId}`] && condition.relevantItemVariations[`Product/${i.productId}`].includes(i.variationId) || i.type === "Bundle" && condition.relevantItemVariations[`Bundle/${i.bundleId}`] && condition.relevantItemVariations[`Bundle/${i.bundleId}`].includes(i.variationId)
|
|
1651
1799
|
);
|
|
1652
1800
|
const relevantReplacedItems = order.items.flatMap((i) => {
|
|
1653
|
-
var
|
|
1654
|
-
return offer.id === i.offerId && i.type === "Bundle" && _OfferUtils.targetContainsBundle(offer, i.id) ? (
|
|
1801
|
+
var _a2;
|
|
1802
|
+
return offer.id === i.offerId && i.type === "Bundle" && _OfferUtils.targetContainsBundle(offer, i.id) ? (_a2 = i.replaced) != null ? _a2 : [] : [];
|
|
1655
1803
|
}).filter((ref) => {
|
|
1656
1804
|
const [productId, variationId] = ref.split("/");
|
|
1657
|
-
return
|
|
1805
|
+
return condition.relevantItemVariations[`Product/${productId}`] && condition.relevantItemVariations[`Product/${productId}`].includes(variationId);
|
|
1658
1806
|
});
|
|
1659
|
-
|
|
1807
|
+
const current = relevantItems.length + relevantReplacedItems.length;
|
|
1808
|
+
return evaluated("Items", void 0, current, condition.minimumItems, current >= condition.minimumItems);
|
|
1809
|
+
}
|
|
1810
|
+
case "CartTotal": {
|
|
1811
|
+
if (!cartTotal)
|
|
1812
|
+
return { eligible: false, progress: null };
|
|
1813
|
+
const minimum = (_a = condition.minimumTotal) == null ? void 0 : _a[cartTotal.currency];
|
|
1814
|
+
const maximum = (_b = condition.maximumTotal) == null ? void 0 : _b[cartTotal.currency];
|
|
1815
|
+
if (minimum === void 0 && maximum === void 0)
|
|
1816
|
+
return { eligible: false, progress: null };
|
|
1817
|
+
const eligible = (minimum === void 0 || cartTotal.amount >= minimum) && (maximum === void 0 || cartTotal.amount <= maximum);
|
|
1818
|
+
if (minimum === void 0)
|
|
1819
|
+
return { eligible, progress: null };
|
|
1820
|
+
return evaluated("Money", cartTotal.currency, cartTotal.amount, minimum, eligible);
|
|
1660
1821
|
}
|
|
1661
1822
|
}
|
|
1662
|
-
console.warn("Unsupported offer condition found:",
|
|
1663
|
-
return false;
|
|
1823
|
+
console.warn("Unsupported offer condition found:", condition);
|
|
1824
|
+
return { eligible: false, progress: null };
|
|
1664
1825
|
}
|
|
1665
1826
|
// The relevant targets of an offer with more than a single target needs to:
|
|
1666
1827
|
// a. Not be owned, or all targets need to be owned
|
|
@@ -1668,10 +1829,29 @@ var OfferUtils = class _OfferUtils {
|
|
|
1668
1829
|
static relevantTargets(offer, cartItems) {
|
|
1669
1830
|
if (offer.targets.length > 1) {
|
|
1670
1831
|
const allOwned = offer.targets.every((t) => t.item.owned);
|
|
1671
|
-
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !
|
|
1832
|
+
return offer.targets.filter((t) => (allOwned || !t.item.owned) && !_OfferUtils.targetInCart(t, cartItems));
|
|
1672
1833
|
}
|
|
1673
1834
|
return offer.targets;
|
|
1674
1835
|
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Whether every item an offer targets is already in the cart, so the offer has no upsell left to
|
|
1838
|
+
* show.
|
|
1839
|
+
*
|
|
1840
|
+
* This is the set of offers the cart hides: a card inviting the buyer to add what they have already
|
|
1841
|
+
* added says nothing, so it is dropped. Which makes it exactly the set whose discount nothing can
|
|
1842
|
+
* currently claim: an offer is only good for one line, and there is no card left to spend it from.
|
|
1843
|
+
*
|
|
1844
|
+
* An offer with no targets names nothing and so is never in this set.
|
|
1845
|
+
*/
|
|
1846
|
+
static allTargetsInCart(offer, cartItems) {
|
|
1847
|
+
return offer.targets.length > 0 && offer.targets.every((target) => _OfferUtils.targetInCart(target, cartItems));
|
|
1848
|
+
}
|
|
1849
|
+
// Matched on the item alone, not the variation: a target narrowed to one variation still counts as
|
|
1850
|
+
// in the cart when another variation of it is. That is the reading the cart's own upsell has always
|
|
1851
|
+
// taken, and the two have to agree or an offer could be hidden and auto-applied at once.
|
|
1852
|
+
static targetInCart(target, cartItems) {
|
|
1853
|
+
return cartItems.some((l) => l.type === target.item.type && target.item.id === (l.type === "Product" ? l.productId : l.bundleId));
|
|
1854
|
+
}
|
|
1675
1855
|
// The pricing variations a target restricts the offer to. An empty (or missing) variation list
|
|
1676
1856
|
// means the offer is not narrowed to specific variations, in which case we keep the established
|
|
1677
1857
|
// single default-variation upsell.
|
|
@@ -1701,6 +1881,428 @@ var OfferUtils = class _OfferUtils {
|
|
|
1701
1881
|
}
|
|
1702
1882
|
};
|
|
1703
1883
|
|
|
1884
|
+
// src/utils/cart.ts
|
|
1885
|
+
var _CartUtils = class _CartUtils {
|
|
1886
|
+
// The pricing variation a line item points at, falling back to the item's default variation when
|
|
1887
|
+
// the variation id doesn't resolve, which is what the cart has always displayed.
|
|
1888
|
+
static resolveVariation(catalog, item) {
|
|
1889
|
+
var _a, _b;
|
|
1890
|
+
const source = item.type === "Product" ? catalog.products.find((p) => p.id === item.productId) : catalog.bundles.find((b) => b.id === item.bundleId);
|
|
1891
|
+
return (_b = (_a = source == null ? void 0 : source.variations) == null ? void 0 : _a.find((v) => v.id === item.variationId)) != null ? _b : source == null ? void 0 : source.defaultVariation;
|
|
1892
|
+
}
|
|
1893
|
+
// A line's unit price after its product discount and any pricing tier, and *before* any offer.
|
|
1894
|
+
// This is the basis the API measures cart total conditions on, which is why it reads the catalog
|
|
1895
|
+
// rather than the line item's own `price`: `enrichLineItems` rewrites that to the offer-discounted
|
|
1896
|
+
// price, and reading it here would take the offer off twice.
|
|
1897
|
+
static unitPrice(catalog, item, currency) {
|
|
1898
|
+
var _a;
|
|
1899
|
+
return (_a = _CartUtils.unitPricing(catalog, item, currency)) == null ? void 0 : _a.price;
|
|
1900
|
+
}
|
|
1901
|
+
// A line's list and post-product-discount unit prices, from whichever pricing tier applies. Both
|
|
1902
|
+
// come from the same tier so they can be subtracted from one another: taking the list price off one
|
|
1903
|
+
// tier and the paid price off another would invent a discount neither of them offers.
|
|
1904
|
+
static unitPricing(catalog, item, currency) {
|
|
1905
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1906
|
+
const variation = _CartUtils.resolveVariation(catalog, item);
|
|
1907
|
+
const price = variation == null ? void 0 : variation.price[currency];
|
|
1908
|
+
if (price === void 0 || variation === void 0)
|
|
1909
|
+
return void 0;
|
|
1910
|
+
const eligiblePricingTiers = (_b = (_a = variation.pricingTiers) == null ? void 0 : _a.filter((tier) => item.quantity >= tier.minQuantity)) != null ? _b : [];
|
|
1911
|
+
const matchingPricingTier = eligiblePricingTiers.length > 0 ? eligiblePricingTiers[eligiblePricingTiers.length - 1] : null;
|
|
1912
|
+
const tierPrice = matchingPricingTier == null ? void 0 : matchingPricingTier.price[currency];
|
|
1913
|
+
if (tierPrice !== void 0 && tierPrice < price) {
|
|
1914
|
+
return {
|
|
1915
|
+
price: tierPrice,
|
|
1916
|
+
originalPrice: (_d = (_c = matchingPricingTier == null ? void 0 : matchingPricingTier.originalPrice) == null ? void 0 : _c[currency]) != null ? _d : tierPrice
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
return { price, originalPrice: (_f = (_e = variation.originalPrice) == null ? void 0 : _e[currency]) != null ? _f : price };
|
|
1920
|
+
}
|
|
1921
|
+
// The number the API measures cart conditions against: each line's price after its product
|
|
1922
|
+
// discount, times quantity, before offers and coupons. `undefined` when any line's price can't be
|
|
1923
|
+
// resolved in `currency`. One unreadable line makes the whole measurement a guess, and a guess
|
|
1924
|
+
// must not unlock an offer.
|
|
1925
|
+
static measure(catalog, items, currency) {
|
|
1926
|
+
if (!currency)
|
|
1927
|
+
return void 0;
|
|
1928
|
+
let amount = 0;
|
|
1929
|
+
for (const item of items) {
|
|
1930
|
+
const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
|
|
1931
|
+
if (unitPrice === void 0)
|
|
1932
|
+
return void 0;
|
|
1933
|
+
amount += unitPrice * item.quantity;
|
|
1934
|
+
}
|
|
1935
|
+
return { currency, amount };
|
|
1936
|
+
}
|
|
1937
|
+
static conditionContext(catalog, order, currency) {
|
|
1938
|
+
return { order, cartTotal: _CartUtils.measure(catalog, order.items, currency) };
|
|
1939
|
+
}
|
|
1940
|
+
// Which lines a cart offer discounts. No targets means the whole cart, a reading that only holds
|
|
1941
|
+
// for cart-scope offers, where an empty target list is the API's way of saying "every line".
|
|
1942
|
+
static cartOfferAppliesToLine(offer, item) {
|
|
1943
|
+
return offer.targets.length === 0 || _CartUtils.offerTargetsLine(offer, item);
|
|
1944
|
+
}
|
|
1945
|
+
// Whether an offer names this line among its targets. An empty variation list on a target leaves
|
|
1946
|
+
// the offer open to every variation of that item. An offer with no targets at all names nothing
|
|
1947
|
+
// here, which is why the "every line" reading stays in `cartOfferAppliesToLine`: it is only true
|
|
1948
|
+
// of a cart offer.
|
|
1949
|
+
static offerTargetsLine(offer, item) {
|
|
1950
|
+
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)));
|
|
1951
|
+
}
|
|
1952
|
+
// The cart offer actually discounting this cart, as opposed to the one attached to the order. An
|
|
1953
|
+
// offer whose targets name nothing in the cart discounts nothing however healthy the total looks,
|
|
1954
|
+
// so it has not really applied.
|
|
1955
|
+
static appliedCartOffer(catalog, order, context) {
|
|
1956
|
+
var _a;
|
|
1957
|
+
if (!order.offerId)
|
|
1958
|
+
return void 0;
|
|
1959
|
+
const offer = (_a = catalog.offers) == null ? void 0 : _a.find((o) => o.id === order.offerId);
|
|
1960
|
+
if (!offer || offer.scope !== "Cart" || !OfferUtils.eligible(offer, context))
|
|
1961
|
+
return void 0;
|
|
1962
|
+
return order.items.some((item) => _CartUtils.cartOfferAppliesToLine(offer, item)) ? offer : void 0;
|
|
1963
|
+
}
|
|
1964
|
+
// What the cart is worth to the buyer, broken down so the cart can show the saving on its own row.
|
|
1965
|
+
// Unlike `measure`, an unresolvable price counts as zero here: this number fills a label, so a
|
|
1966
|
+
// half-loaded catalog should still render a total rather than nothing.
|
|
1967
|
+
static total(catalog, order, currency) {
|
|
1968
|
+
var _a, _b;
|
|
1969
|
+
const context = _CartUtils.conditionContext(catalog, order, currency);
|
|
1970
|
+
const cartOffer = _CartUtils.appliedCartOffer(catalog, order, context);
|
|
1971
|
+
let original = 0;
|
|
1972
|
+
let subtotal = 0;
|
|
1973
|
+
let cartOfferDiscount = 0;
|
|
1974
|
+
for (const item of order.items) {
|
|
1975
|
+
const unitPrice = _CartUtils.unitPriceAfterItemOffer(catalog, item, currency, context);
|
|
1976
|
+
original += ((_b = (_a = _CartUtils.unitPricing(catalog, item, currency)) == null ? void 0 : _a.originalPrice) != null ? _b : unitPrice) * item.quantity;
|
|
1977
|
+
subtotal += unitPrice * item.quantity;
|
|
1978
|
+
if (cartOffer && _CartUtils.cartOfferAppliesToLine(cartOffer, item))
|
|
1979
|
+
cartOfferDiscount += DiscountUtils.apply(cartOffer.discount, { [currency]: unitPrice })[currency] * item.quantity;
|
|
1980
|
+
}
|
|
1981
|
+
const amount = subtotal - cartOfferDiscount;
|
|
1982
|
+
return {
|
|
1983
|
+
currency,
|
|
1984
|
+
original,
|
|
1985
|
+
subtotal,
|
|
1986
|
+
cartOffer: cartOffer ? { offer: cartOffer, discount: cartOfferDiscount } : null,
|
|
1987
|
+
discount: original - amount,
|
|
1988
|
+
amount
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
// The cart offer worth attaching to the order. Cart offers are always percentages, but 10% off
|
|
1992
|
+
// everything can still beat 50% off one cheap line, so they are ranked by what they would actually
|
|
1993
|
+
// take off this cart rather than by percentage.
|
|
1994
|
+
static bestCartOffer(catalog, order, currency) {
|
|
1995
|
+
var _a;
|
|
1996
|
+
const context = _CartUtils.conditionContext(catalog, order, currency);
|
|
1997
|
+
let best;
|
|
1998
|
+
for (const offer of (_a = catalog.offers) != null ? _a : []) {
|
|
1999
|
+
if (offer.scope !== "Cart" || !OfferUtils.eligible(offer, context))
|
|
2000
|
+
continue;
|
|
2001
|
+
const value = _CartUtils.cartOfferValue(catalog, order, offer, currency, context);
|
|
2002
|
+
if (value <= 0)
|
|
2003
|
+
continue;
|
|
2004
|
+
if (!best || value > best.value || value === best.value && offer.id === order.offerId)
|
|
2005
|
+
best = { offer, value };
|
|
2006
|
+
}
|
|
2007
|
+
return best == null ? void 0 : best.offer;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* What a cart offer would take off this cart, whether or not its condition is met yet.
|
|
2011
|
+
*
|
|
2012
|
+
* Computed on the same post-line-offer price `total` discounts, so "which offer do we attach" and
|
|
2013
|
+
* "what does the cart actually save" can never disagree. Ranking on the list price instead would
|
|
2014
|
+
* over-value an offer aimed at a line that already carries a steep offer of its own: 50% of a
|
|
2015
|
+
* line discounted to a tenth of its price is worth less than 10% of a full-price one.
|
|
2016
|
+
*/
|
|
2017
|
+
static cartOfferValue(catalog, order, offer, currency, context) {
|
|
2018
|
+
const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
|
|
2019
|
+
return order.items.reduce((aggregate, item) => {
|
|
2020
|
+
if (!_CartUtils.cartOfferAppliesToLine(offer, item))
|
|
2021
|
+
return aggregate;
|
|
2022
|
+
const unitPrice = _CartUtils.unitPriceAfterItemOffer(catalog, item, currency, conditionContext);
|
|
2023
|
+
return aggregate + DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency] * item.quantity;
|
|
2024
|
+
}, 0);
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* The item-scope offer worth attaching to a single line, or `undefined` when none is.
|
|
2028
|
+
*
|
|
2029
|
+
* Ranked by what it would actually take off this line rather than by percentage, the same way
|
|
2030
|
+
* `bestCartOffer` ranks a cart: a flat amount off can beat a bigger-looking percentage on a cheap
|
|
2031
|
+
* line, and the other way round on an expensive one.
|
|
2032
|
+
*
|
|
2033
|
+
* `unavailableOfferIds` are offers the rest of the cart has already claimed. An offer is only ever
|
|
2034
|
+
* good for one line, since the cart treats an offer held by any line as in use. So a caller walking
|
|
2035
|
+
* the cart must reserve each one as it goes, or two lines would both count on the same offer and
|
|
2036
|
+
* the API would honour it once. `assignItemOffers` does that bookkeeping for a whole cart.
|
|
2037
|
+
*/
|
|
2038
|
+
static bestItemOffer(catalog, item, currency, context, unavailableOfferIds) {
|
|
2039
|
+
let best;
|
|
2040
|
+
for (const candidate of _CartUtils.itemOfferCandidates(catalog, [item], currency, context, unavailableOfferIds)) {
|
|
2041
|
+
if (!best || candidate.value > best.value)
|
|
2042
|
+
best = candidate;
|
|
2043
|
+
}
|
|
2044
|
+
return best == null ? void 0 : best.offer;
|
|
2045
|
+
}
|
|
2046
|
+
/**
|
|
2047
|
+
* Which item-scope offer to attach to which line, for the lines not already carrying one.
|
|
2048
|
+
*
|
|
2049
|
+
* Chosen for what the pairings are worth together rather than by cart order. An offer is only good
|
|
2050
|
+
* for one line, so walking the cart and taking the best offer for each line in turn hands a
|
|
2051
|
+
* multi-target offer to whichever line the buyer happened to add first, which is arbitrary and can
|
|
2052
|
+
* be the cheapest one. A buyer claiming these from the upsell cards would have placed them where
|
|
2053
|
+
* they are worth most, so that is what this does.
|
|
2054
|
+
*
|
|
2055
|
+
* Lines already holding an offer are left alone and their offers reserved: an offer on a line is
|
|
2056
|
+
* intent, whether the buyer's from the upsell or the API's from a refreshed order, and this is not
|
|
2057
|
+
* the place to second-guess it.
|
|
2058
|
+
*/
|
|
2059
|
+
static assignItemOffers(catalog, order, currency, context) {
|
|
2060
|
+
const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
|
|
2061
|
+
const claimed = new Set(order.items.map((i) => i.offerId).filter((id) => !!id));
|
|
2062
|
+
return _CartUtils.maximalItemOfferAssignment(
|
|
2063
|
+
_CartUtils.itemOfferCandidates(catalog, order.items.filter((i) => !i.offerId), currency, conditionContext, claimed)
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
/**
|
|
2067
|
+
* The pairings worth the most together, given each offer can only be spent once.
|
|
2068
|
+
*
|
|
2069
|
+
* Taking the most valuable pairing first is not enough: with one offer worth 100 on line A and 99 on
|
|
2070
|
+
* line B, and a second offer worth 98 on line A alone, claiming the 100 strands the second offer and
|
|
2071
|
+
* saves 100, where putting the first offer on B and the second on A saves 197. The pairings have to
|
|
2072
|
+
* be chosen as a set.
|
|
2073
|
+
*
|
|
2074
|
+
* Walked line by line, memoised on which offers are still free. The candidates have already been
|
|
2075
|
+
* through targeting and eligibility, so in a real cart there are a handful; past `searchableOffers`
|
|
2076
|
+
* distinct offers it takes the most valuable pairing first instead, which is never worse than
|
|
2077
|
+
* applying nothing.
|
|
2078
|
+
*/
|
|
2079
|
+
static maximalItemOfferAssignment(candidates) {
|
|
2080
|
+
var _a;
|
|
2081
|
+
const optionsByItem = /* @__PURE__ */ new Map();
|
|
2082
|
+
const offerBits = /* @__PURE__ */ new Map();
|
|
2083
|
+
for (const { item, offer, value } of candidates) {
|
|
2084
|
+
let bit = offerBits.get(offer.id);
|
|
2085
|
+
if (bit === void 0) {
|
|
2086
|
+
bit = offerBits.size;
|
|
2087
|
+
offerBits.set(offer.id, bit);
|
|
2088
|
+
}
|
|
2089
|
+
const options = (_a = optionsByItem.get(item.id)) != null ? _a : [];
|
|
2090
|
+
options.push({ offerId: offer.id, bit, value });
|
|
2091
|
+
optionsByItem.set(item.id, options);
|
|
2092
|
+
}
|
|
2093
|
+
if (offerBits.size > _CartUtils.searchableOffers)
|
|
2094
|
+
return _CartUtils.mostValuableItemOfferPairings(candidates);
|
|
2095
|
+
const itemIds = [...optionsByItem.keys()];
|
|
2096
|
+
const memo = /* @__PURE__ */ new Map();
|
|
2097
|
+
const from = (index, spent) => {
|
|
2098
|
+
var _a2;
|
|
2099
|
+
if (index >= itemIds.length)
|
|
2100
|
+
return { value: 0, assignments: [] };
|
|
2101
|
+
const key = `${index}:${spent}`;
|
|
2102
|
+
const memoised = memo.get(key);
|
|
2103
|
+
if (memoised)
|
|
2104
|
+
return memoised;
|
|
2105
|
+
let best = from(index + 1, spent);
|
|
2106
|
+
const itemId = itemIds[index];
|
|
2107
|
+
for (const option of (_a2 = optionsByItem.get(itemId)) != null ? _a2 : []) {
|
|
2108
|
+
if (spent & 1 << option.bit)
|
|
2109
|
+
continue;
|
|
2110
|
+
const rest = from(index + 1, spent | 1 << option.bit);
|
|
2111
|
+
const value = option.value + rest.value;
|
|
2112
|
+
if (value > best.value)
|
|
2113
|
+
best = { value, assignments: [{ itemId, offerId: option.offerId }, ...rest.assignments] };
|
|
2114
|
+
}
|
|
2115
|
+
memo.set(key, best);
|
|
2116
|
+
return best;
|
|
2117
|
+
};
|
|
2118
|
+
return from(0, 0).assignments;
|
|
2119
|
+
}
|
|
2120
|
+
// The fallback for a candidate set too large to search: repeatedly take the most valuable pairing
|
|
2121
|
+
// still available. Can strand a better combination, which is the whole reason it is only a fallback.
|
|
2122
|
+
static mostValuableItemOfferPairings(candidates) {
|
|
2123
|
+
const assignments = [];
|
|
2124
|
+
const assignedItemIds = /* @__PURE__ */ new Set();
|
|
2125
|
+
const spentOfferIds = /* @__PURE__ */ new Set();
|
|
2126
|
+
for (const { item, offer } of [...candidates].sort((a, b) => b.value - a.value)) {
|
|
2127
|
+
if (assignedItemIds.has(item.id) || spentOfferIds.has(offer.id))
|
|
2128
|
+
continue;
|
|
2129
|
+
assignedItemIds.add(item.id);
|
|
2130
|
+
spentOfferIds.add(offer.id);
|
|
2131
|
+
assignments.push({ itemId: item.id, offerId: offer.id });
|
|
2132
|
+
}
|
|
2133
|
+
return assignments;
|
|
2134
|
+
}
|
|
2135
|
+
// Every line-and-offer pairing the cart could make, and what each would save. Enumerated in cart
|
|
2136
|
+
// order and then catalog order, which `sort` preserves for equal savings, so an unchanged cart
|
|
2137
|
+
// always resolves the same way.
|
|
2138
|
+
static itemOfferCandidates(catalog, items, currency, context, unavailableOfferIds) {
|
|
2139
|
+
var _a;
|
|
2140
|
+
const candidates = [];
|
|
2141
|
+
for (const item of items) {
|
|
2142
|
+
const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
|
|
2143
|
+
if (unitPrice === void 0)
|
|
2144
|
+
continue;
|
|
2145
|
+
for (const offer of (_a = catalog.offers) != null ? _a : []) {
|
|
2146
|
+
if (offer.scope === "Cart" || (unavailableOfferIds == null ? void 0 : unavailableOfferIds.has(offer.id)))
|
|
2147
|
+
continue;
|
|
2148
|
+
if (!_CartUtils.offerTargetsLine(offer, item))
|
|
2149
|
+
continue;
|
|
2150
|
+
if (!OfferUtils.allTargetsInCart(offer, context.order.items))
|
|
2151
|
+
continue;
|
|
2152
|
+
if (!OfferUtils.eligible(offer, context))
|
|
2153
|
+
continue;
|
|
2154
|
+
const value = _CartUtils.itemOfferUnitDiscount(offer, unitPrice, currency) * item.quantity;
|
|
2155
|
+
if (value <= 0)
|
|
2156
|
+
continue;
|
|
2157
|
+
candidates.push({ item, offer, value });
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
return candidates;
|
|
2161
|
+
}
|
|
2162
|
+
// What an item offer takes off one unit of a line. Measured off the post-product-discount price,
|
|
2163
|
+
// which is what an item offer comes off in the API's order: product discount -> line offer -> cart
|
|
2164
|
+
// offer -> coupons.
|
|
2165
|
+
static itemOfferUnitDiscount(offer, unitPrice, currency) {
|
|
2166
|
+
return DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency];
|
|
2167
|
+
}
|
|
2168
|
+
// A line's unit price once its own offer has come off, which is what a cart offer is applied to:
|
|
2169
|
+
// the API discounts in the order product discount -> line offer -> cart offer -> coupons. An
|
|
2170
|
+
// unresolvable price counts as zero, matching `total`'s "still render something" stance rather
|
|
2171
|
+
// than `measure`'s "refuse to guess".
|
|
2172
|
+
static unitPriceAfterItemOffer(catalog, item, currency, context) {
|
|
2173
|
+
var _a, _b;
|
|
2174
|
+
const unitPrice = (_a = _CartUtils.unitPrice(catalog, item, currency)) != null ? _a : 0;
|
|
2175
|
+
const itemOffer = item.offerId ? (_b = catalog.offers) == null ? void 0 : _b.find((o) => o.id === item.offerId) : void 0;
|
|
2176
|
+
if (!itemOffer || !OfferUtils.eligible(itemOffer, context))
|
|
2177
|
+
return unitPrice;
|
|
2178
|
+
return unitPrice - _CartUtils.itemOfferUnitDiscount(itemOffer, unitPrice, currency);
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
// How many distinct offers can compete for the same cart before the exact search is given up on.
|
|
2182
|
+
// 12 leaves 4096 states per line, which is nothing, and no real storefront has a dozen item offers
|
|
2183
|
+
// eligible against one cart at once.
|
|
2184
|
+
_CartUtils.searchableOffers = 12;
|
|
2185
|
+
var CartUtils = _CartUtils;
|
|
2186
|
+
|
|
2187
|
+
// src/utils/image.ts
|
|
2188
|
+
var imageWidths = [16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048];
|
|
2189
|
+
var maxImageDimension = imageWidths[imageWidths.length - 1];
|
|
2190
|
+
var transformPathSegment = "_i";
|
|
2191
|
+
var maxCoverAspectRatio = 4;
|
|
2192
|
+
function isAssetUrl(url) {
|
|
2193
|
+
let parsed;
|
|
2194
|
+
try {
|
|
2195
|
+
parsed = new URL(url);
|
|
2196
|
+
} catch (e) {
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
return parsed.hostname === "assets.moonbase.sh" || parsed.hostname.startsWith("assets.") && parsed.hostname.endsWith(".moonbase.sh");
|
|
2200
|
+
}
|
|
2201
|
+
function isRendition(url) {
|
|
2202
|
+
try {
|
|
2203
|
+
return new URL(url).pathname.startsWith(`/${transformPathSegment}/`);
|
|
2204
|
+
} catch (e) {
|
|
2205
|
+
return false;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
function isTransformableImageUrl(url) {
|
|
2209
|
+
return isAssetUrl(url) && !isRendition(url);
|
|
2210
|
+
}
|
|
2211
|
+
var warned = /* @__PURE__ */ new Set();
|
|
2212
|
+
function warnOnce(reason, url, message) {
|
|
2213
|
+
const key = `${reason}:${url}`;
|
|
2214
|
+
if (warned.has(key))
|
|
2215
|
+
return;
|
|
2216
|
+
warned.add(key);
|
|
2217
|
+
console.warn(message, url);
|
|
2218
|
+
}
|
|
2219
|
+
function snap(value) {
|
|
2220
|
+
var _a;
|
|
2221
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0)
|
|
2222
|
+
return void 0;
|
|
2223
|
+
return (_a = imageWidths.find((width) => value <= width)) != null ? _a : maxImageDimension;
|
|
2224
|
+
}
|
|
2225
|
+
var ImageUtils = class _ImageUtils {
|
|
2226
|
+
/**
|
|
2227
|
+
* Rewrites an asset URL to ask the CDN for a rendition at the given size.
|
|
2228
|
+
*
|
|
2229
|
+
* Returns the URL untouched — never a broken one — when there is nothing to do: a URL that
|
|
2230
|
+
* isn't a Moonbase asset, one that is already a rendition, an empty transform, or a `cover`
|
|
2231
|
+
* crop too extreme for the CDN to serve. Callers can apply it unconditionally.
|
|
2232
|
+
*/
|
|
2233
|
+
static transform(url, transform) {
|
|
2234
|
+
if (!url)
|
|
2235
|
+
return void 0;
|
|
2236
|
+
if (!isAssetUrl(url)) {
|
|
2237
|
+
warnOnce(
|
|
2238
|
+
"host",
|
|
2239
|
+
url,
|
|
2240
|
+
"Only images hosted by Moonbase can be resized on the fly. Serving this one at its original size; host it in Moonbase, or size it yourself before uploading:"
|
|
2241
|
+
);
|
|
2242
|
+
return url;
|
|
2243
|
+
}
|
|
2244
|
+
if (isRendition(url))
|
|
2245
|
+
return url;
|
|
2246
|
+
const query = _ImageUtils.toQuery(transform);
|
|
2247
|
+
if (query === null) {
|
|
2248
|
+
warnOnce(
|
|
2249
|
+
"aspect-ratio",
|
|
2250
|
+
url,
|
|
2251
|
+
`A 'cover' crop more lopsided than ${maxCoverAspectRatio}:1 is not served. Serving this image at its original size instead:`
|
|
2252
|
+
);
|
|
2253
|
+
return url;
|
|
2254
|
+
}
|
|
2255
|
+
if (query.length === 0)
|
|
2256
|
+
return url;
|
|
2257
|
+
const parsed = new URL(url);
|
|
2258
|
+
const separator = parsed.search ? "&" : "?";
|
|
2259
|
+
return `${parsed.origin}/${transformPathSegment}${parsed.pathname}${parsed.search}${separator}${query.map(([key, value]) => `${key}=${value}`).join("&")}`;
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* A `srcset` covering the given pixel densities, so a retina screen gets the sharper rendition
|
|
2263
|
+
* without every screen paying for it. Pair it with a `src` built from the same transform.
|
|
2264
|
+
*/
|
|
2265
|
+
static srcSet(url, transform, ...scales) {
|
|
2266
|
+
if (!url || scales.length === 0)
|
|
2267
|
+
return void 0;
|
|
2268
|
+
return scales.map((scale) => `${_ImageUtils.transform(url, _ImageUtils.atScale(transform, scale))} ${scale}x`).join(", ");
|
|
2269
|
+
}
|
|
2270
|
+
/** The same transform at a different pixel density. */
|
|
2271
|
+
static atScale(transform, scale) {
|
|
2272
|
+
return {
|
|
2273
|
+
...transform,
|
|
2274
|
+
width: transform.width === void 0 ? void 0 : transform.width * scale,
|
|
2275
|
+
height: transform.height === void 0 ? void 0 : transform.height * scale
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
/**
|
|
2279
|
+
* The query parameters for a transform, in the CDN's own order, with defaults left out so two
|
|
2280
|
+
* equivalent transforms always produce the same URL — and therefore the same cache entry.
|
|
2281
|
+
* `null` when the CDN would refuse the transform outright.
|
|
2282
|
+
*/
|
|
2283
|
+
static toQuery(transform) {
|
|
2284
|
+
var _a, _b;
|
|
2285
|
+
const fit = (_a = transform.fit) != null ? _a : "contain";
|
|
2286
|
+
const width = snap(transform.width);
|
|
2287
|
+
const height = fit === "cover" ? (_b = snap(transform.height)) != null ? _b : width : snap(transform.height);
|
|
2288
|
+
if (fit === "cover" && width && height) {
|
|
2289
|
+
const ratio = width > height ? width / height : height / width;
|
|
2290
|
+
if (ratio > maxCoverAspectRatio)
|
|
2291
|
+
return null;
|
|
2292
|
+
}
|
|
2293
|
+
const query = [];
|
|
2294
|
+
if (width)
|
|
2295
|
+
query.push(["w", String(width)]);
|
|
2296
|
+
if (height && (fit === "cover" || transform.height !== void 0))
|
|
2297
|
+
query.push(["h", String(height)]);
|
|
2298
|
+
if (fit !== "contain")
|
|
2299
|
+
query.push(["fit", fit]);
|
|
2300
|
+
if (transform.format)
|
|
2301
|
+
query.push(["f", transform.format]);
|
|
2302
|
+
return query;
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
|
|
1704
2306
|
// src/utils/utm.ts
|
|
1705
2307
|
function parseOffSiteReferrer(rawReferrer, currentOrigin) {
|
|
1706
2308
|
if (!rawReferrer)
|
|
@@ -1747,10 +2349,12 @@ export {
|
|
|
1747
2349
|
ActivationRequestStatus,
|
|
1748
2350
|
ActivationStatus,
|
|
1749
2351
|
Architecture,
|
|
2352
|
+
CartUtils,
|
|
1750
2353
|
ConnectableAccountProvider,
|
|
1751
2354
|
ConsoleLogger,
|
|
1752
2355
|
CycleLength,
|
|
1753
2356
|
DiscountUtils,
|
|
2357
|
+
ImageUtils,
|
|
1754
2358
|
InMemoryStore,
|
|
1755
2359
|
LicenseStatus,
|
|
1756
2360
|
LocalStorageStore,
|
|
@@ -1768,9 +2372,14 @@ export {
|
|
|
1768
2372
|
Platform,
|
|
1769
2373
|
SubscriptionStatus,
|
|
1770
2374
|
TokenStore,
|
|
2375
|
+
imageWidths,
|
|
2376
|
+
isPromotionDisplay,
|
|
2377
|
+
isTransformableImageUrl,
|
|
2378
|
+
maxImageDimension,
|
|
1771
2379
|
objectToQuery,
|
|
1772
2380
|
parseOffSiteReferrer,
|
|
1773
2381
|
problemDetailsSchema,
|
|
2382
|
+
promotionDisplays,
|
|
1774
2383
|
resolveUtm,
|
|
1775
2384
|
schemas_exports12 as schemas,
|
|
1776
2385
|
utmToObject
|