@moonbase.sh/storefront-api 2.4.0 → 3.0.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.cjs CHANGED
@@ -25,10 +25,12 @@ __export(index_exports, {
25
25
  ActivationRequestStatus: () => ActivationRequestStatus,
26
26
  ActivationStatus: () => ActivationStatus,
27
27
  Architecture: () => Architecture,
28
+ CartUtils: () => CartUtils,
28
29
  ConnectableAccountProvider: () => ConnectableAccountProvider,
29
30
  ConsoleLogger: () => ConsoleLogger,
30
31
  CycleLength: () => CycleLength,
31
32
  DiscountUtils: () => DiscountUtils,
33
+ ImageUtils: () => ImageUtils,
32
34
  InMemoryStore: () => InMemoryStore,
33
35
  LicenseStatus: () => LicenseStatus,
34
36
  LocalStorageStore: () => LocalStorageStore,
@@ -46,9 +48,14 @@ __export(index_exports, {
46
48
  Platform: () => Platform,
47
49
  SubscriptionStatus: () => SubscriptionStatus,
48
50
  TokenStore: () => TokenStore,
51
+ imageWidths: () => imageWidths,
52
+ isPromotionDisplay: () => isPromotionDisplay,
53
+ isTransformableImageUrl: () => isTransformableImageUrl,
54
+ maxImageDimension: () => maxImageDimension,
49
55
  objectToQuery: () => objectToQuery,
50
56
  parseOffSiteReferrer: () => parseOffSiteReferrer,
51
57
  problemDetailsSchema: () => problemDetailsSchema,
58
+ promotionDisplays: () => promotionDisplays,
52
59
  resolveUtm: () => resolveUtm,
53
60
  schemas: () => schemas_exports12,
54
61
  utmToObject: () => utmToObject
@@ -69,10 +76,14 @@ var import_zod4 = require("zod");
69
76
  var schemas_exports2 = {};
70
77
  __export(schemas_exports2, {
71
78
  offerConditionSchema: () => offerConditionSchema,
79
+ offerScopeSchema: () => offerScopeSchema,
72
80
  storefrontBundleSchema: () => storefrontBundleSchema,
73
81
  storefrontOfferSchema: () => storefrontOfferSchema,
74
82
  storefrontOfferTargetSchema: () => storefrontOfferTargetSchema,
75
83
  storefrontProductSchema: () => storefrontProductSchema,
84
+ storefrontPromotionCtaSchema: () => storefrontPromotionCtaSchema,
85
+ storefrontPromotionSchema: () => storefrontPromotionSchema,
86
+ storefrontPromotionTargetSchema: () => storefrontPromotionTargetSchema,
76
87
  storefrontSchema: () => storefrontSchema,
77
88
  taxEstimateSchema: () => taxEstimateSchema
78
89
  });
@@ -112,6 +123,11 @@ function utmToObject(utm) {
112
123
 
113
124
  // src/globalSchemas.ts
114
125
  var priceCollectionSchema = import_zod.z.record(import_zod.z.number());
126
+ var discountPromotionFields = {
127
+ promotionId: import_zod.z.string().optional(),
128
+ validFrom: import_zod.z.string().optional(),
129
+ validUntil: import_zod.z.string().optional()
130
+ };
115
131
  var percentageOffDiscountSchema = import_zod.z.object({
116
132
  type: import_zod.z.literal("PercentageOffDiscount"),
117
133
  name: import_zod.z.string(),
@@ -119,7 +135,8 @@ var percentageOffDiscountSchema = import_zod.z.object({
119
135
  percentage: import_zod.z.number(),
120
136
  total: priceCollectionSchema.optional(),
121
137
  isExclusive: import_zod.z.boolean(),
122
- recurringPaymentUseCount: import_zod.z.number().optional()
138
+ recurringPaymentUseCount: import_zod.z.number().optional(),
139
+ ...discountPromotionFields
123
140
  });
124
141
  var flatAmountOffDiscountSchema = import_zod.z.object({
125
142
  type: import_zod.z.literal("FlatAmountOffDiscount"),
@@ -127,7 +144,8 @@ var flatAmountOffDiscountSchema = import_zod.z.object({
127
144
  description: import_zod.z.string().optional(),
128
145
  total: priceCollectionSchema.optional(),
129
146
  isExclusive: import_zod.z.boolean(),
130
- recurringPaymentUseCount: import_zod.z.number().optional()
147
+ recurringPaymentUseCount: import_zod.z.number().optional(),
148
+ ...discountPromotionFields
131
149
  });
132
150
  var moneySchema = import_zod.z.object({
133
151
  currency: import_zod.z.string(),
@@ -161,6 +179,7 @@ var rawPropertyValueSchema = import_zod.z.lazy(
161
179
  import_zod.z.string(),
162
180
  import_zod.z.number(),
163
181
  import_zod.z.boolean(),
182
+ import_zod.z.null(),
164
183
  import_zod.z.record(rawPropertyValueSchema),
165
184
  import_zod.z.array(rawPropertyValueSchema)
166
185
  ])
@@ -231,7 +250,7 @@ var downloadSchema = import_zod2.z.object({
231
250
  var productSummarySchema = import_zod2.z.object({
232
251
  id: import_zod2.z.string(),
233
252
  name: import_zod2.z.string(),
234
- tagline: import_zod2.z.string(),
253
+ tagline: import_zod2.z.string().nullish(),
235
254
  description: import_zod2.z.string().nullish(),
236
255
  website: import_zod2.z.string().optional(),
237
256
  iconUrl: import_zod2.z.string().optional(),
@@ -254,8 +273,8 @@ var productSummarySchema = import_zod2.z.object({
254
273
  var storefrontProductSchema = import_zod3.z.object({
255
274
  id: import_zod3.z.string(),
256
275
  name: import_zod3.z.string(),
257
- tagline: import_zod3.z.string(),
258
- description: import_zod3.z.string(),
276
+ tagline: import_zod3.z.string().nullish(),
277
+ description: import_zod3.z.string().nullish(),
259
278
  iconUrl: import_zod3.z.string().nullable(),
260
279
  website: import_zod3.z.string().nullish(),
261
280
  owned: import_zod3.z.boolean(),
@@ -269,8 +288,8 @@ var storefrontProductSchema = import_zod3.z.object({
269
288
  var storefrontBundleSchema = import_zod3.z.object({
270
289
  id: import_zod3.z.string(),
271
290
  name: import_zod3.z.string(),
272
- tagline: import_zod3.z.string(),
273
- description: import_zod3.z.string(),
291
+ tagline: import_zod3.z.string().nullish(),
292
+ description: import_zod3.z.string().nullish(),
274
293
  iconUrl: import_zod3.z.string().nullable(),
275
294
  owned: import_zod3.z.boolean(),
276
295
  partial: import_zod3.z.boolean(),
@@ -287,7 +306,22 @@ var cartContainsItemsConditionSchema = import_zod3.z.object({
287
306
  minimumItems: import_zod3.z.number(),
288
307
  relevantItemVariations: import_zod3.z.record(import_zod3.z.string(), import_zod3.z.string().array())
289
308
  });
290
- var offerConditionSchema = import_zod3.z.discriminatedUnion("type", [cartContainsItemsConditionSchema]);
309
+ var cartTotalConditionSchema = import_zod3.z.object({
310
+ type: import_zod3.z.literal("CartTotal"),
311
+ // Both bounds are optional, but never both absent. They serialize as an explicit `null` when
312
+ // unset, hence `.nullish()` rather than `.optional()`. The API converts them into every enabled
313
+ // currency up front, precisely so we can tell the buyer how much further they have to go without
314
+ // doing any currency conversion ourselves.
315
+ minimumTotal: priceCollectionSchema.nullish(),
316
+ maximumTotal: priceCollectionSchema.nullish()
317
+ });
318
+ var knownOfferConditionSchema = import_zod3.z.discriminatedUnion("type", [
319
+ cartContainsItemsConditionSchema,
320
+ cartTotalConditionSchema
321
+ ]);
322
+ var unsupportedOfferConditionSchema = import_zod3.z.object({ type: import_zod3.z.string() }).passthrough().transform(({ type }) => ({ type: "Unsupported", unsupportedType: type }));
323
+ var offerConditionSchema = import_zod3.z.union([knownOfferConditionSchema, unsupportedOfferConditionSchema]);
324
+ var offerScopeSchema = import_zod3.z.enum(["Item", "Cart"]).catch("Item").optional().default("Item");
291
325
  var offerTargetItemSchema = import_zod3.z.union([storefrontProductSchema, storefrontBundleSchema]);
292
326
  var storefrontOfferTargetSchema = import_zod3.z.object({
293
327
  // The product or bundle the offer applies to.
@@ -303,8 +337,10 @@ function legacyOfferTargets(target, targetVariations) {
303
337
  }
304
338
  var storefrontOfferSchema = import_zod3.z.object({
305
339
  id: import_zod3.z.string(),
340
+ scope: offerScopeSchema,
306
341
  // Each target pairs a product/bundle with the pricing variations the offer applies to (an empty
307
- // list means all variations). This is the model to consume.
342
+ // list means all variations). This is the model to consume. A cart offer may have no targets at
343
+ // all, which means it discounts every line item.
308
344
  targets: import_zod3.z.array(storefrontOfferTargetSchema).optional(),
309
345
  // Legacy fields, kept optional only so old cached storefront responses still parse. The API now
310
346
  // exposes the unified `targets` model; we derive `targets` from these when a response predates it.
@@ -318,6 +354,57 @@ var storefrontOfferSchema = import_zod3.z.object({
318
354
  ...rest,
319
355
  targets: targets != null ? targets : legacyOfferTargets(target, targetVariations)
320
356
  }));
357
+ var storefrontPromotionTargetSchema = import_zod3.z.object({
358
+ // A prefixed reference to the item the sale covers, `Product/<id>` or `Bundle/<id>`. The item
359
+ // itself is already serialized in `products`/`bundles`; embedding it again would duplicate the
360
+ // bulk of the payload for a sale spanning dozens of products.
361
+ referenceId: import_zod3.z.string(),
362
+ // The pricing variation IDs the sale is restricted to; an empty list means all variations.
363
+ variations: import_zod3.z.string().array()
364
+ });
365
+ var storefrontPromotionCtaSchema = import_zod3.z.object({
366
+ url: import_zod3.z.string(),
367
+ label: import_zod3.z.string()
368
+ });
369
+ var storefrontPromotionSchema = import_zod3.z.object({
370
+ id: import_zod3.z.string(),
371
+ name: import_zod3.z.string(),
372
+ description: import_zod3.z.string().nullish(),
373
+ imageUrl: import_zod3.z.string().nullish(),
374
+ // Absent when the merchant has not set one, in which case the storefront decides whether the
375
+ // sale leads anywhere.
376
+ cta: storefrontPromotionCtaSchema.nullish(),
377
+ // How the merchant wants the sale surfaced, e.g. `['Banner', 'Popup']`; an empty list means no
378
+ // preference. Kept as free-form strings and narrowed with `isPromotionDisplay`, so a display
379
+ // mode added server-side later is ignored rather than failing the whole storefront.
380
+ display: import_zod3.z.string().array(),
381
+ // The promotion's *intended* scope, not what is actually showing the sale price: each variation
382
+ // displays only its single best discount, so a targeted item may be showing a better one. The
383
+ // authoritative signal is `promotionId` on a variation's own discount.
384
+ targets: storefrontPromotionTargetSchema.array(),
385
+ // The promotion's own discount, for banner copy ("20% off"). Per-item prices are already
386
+ // discounted on each variation.
387
+ discount: discountSchema,
388
+ validFrom: import_zod3.z.string().nullish(),
389
+ validUntil: import_zod3.z.string().nullish(),
390
+ properties: import_zod3.z.record(rawPropertyValueSchema).nullish()
391
+ });
392
+ var storefrontOffersSchema = import_zod3.z.array(import_zod3.z.unknown()).transform((offers) => offers.flatMap((offer) => {
393
+ const parsed = storefrontOfferSchema.safeParse(offer);
394
+ if (!parsed.success) {
395
+ console.warn("Ignoring storefront offer that could not be parsed:", parsed.error.issues);
396
+ return [];
397
+ }
398
+ return [parsed.data];
399
+ }));
400
+ var storefrontPromotionsSchema = import_zod3.z.array(import_zod3.z.unknown()).transform((promotions) => promotions.flatMap((promotion) => {
401
+ const parsed = storefrontPromotionSchema.safeParse(promotion);
402
+ if (!parsed.success) {
403
+ console.warn("Ignoring storefront promotion that could not be parsed:", parsed.error.issues);
404
+ return [];
405
+ }
406
+ return [parsed.data];
407
+ }));
321
408
  var taxEstimateSchema = import_zod3.z.object({
322
409
  rate: import_zod3.z.number(),
323
410
  mode: import_zod3.z.enum(["Exclusive", "Inclusive"]),
@@ -331,7 +418,9 @@ var storefrontSchema = import_zod3.z.object({
331
418
  products: storefrontProductSchema.array(),
332
419
  bundles: storefrontBundleSchema.array(),
333
420
  // Offers need to be optional since we may still have old, cached representations in browsers
334
- offers: storefrontOfferSchema.array().optional(),
421
+ offers: storefrontOffersSchema.optional(),
422
+ // Promotions need to be optional since we may still have old, cached representations in browsers
423
+ promotions: storefrontPromotionsSchema.optional(),
335
424
  // Depending on circumstances, we may have a tax estimate available
336
425
  estimatedTax: taxEstimateSchema.optional()
337
426
  });
@@ -1077,6 +1166,7 @@ var import_zod13 = require("zod");
1077
1166
  var schemas_exports7 = {};
1078
1167
  __export(schemas_exports7, {
1079
1168
  completedOrderSchema: () => completedOrderSchema,
1169
+ lineItemTotalSchema: () => lineItemTotalSchema,
1080
1170
  openBundleLineItem: () => openBundleLineItem,
1081
1171
  openOrderLineItem: () => openOrderLineItem,
1082
1172
  openOrderSchema: () => openOrderSchema,
@@ -1105,14 +1195,17 @@ var couponSchema = import_zod12.z.object({
1105
1195
  var lineItemProductSchema = import_zod12.z.object({
1106
1196
  id: import_zod12.z.string(),
1107
1197
  name: import_zod12.z.string(),
1108
- tagline: import_zod12.z.string(),
1198
+ tagline: import_zod12.z.string().nullish(),
1109
1199
  iconUrl: import_zod12.z.string().nullable()
1110
1200
  });
1111
1201
  var lineItemTotalSchema = import_zod12.z.object({
1112
1202
  original: moneySchema,
1113
1203
  discount: moneySchema,
1114
1204
  subtotal: moneySchema,
1115
- due: moneySchema
1205
+ due: moneySchema,
1206
+ // This line's per-unit share of the order's cart offer, kept apart from the offer the buyer picked
1207
+ // for this item. Both are part of `discount`. Absent on orders priced before cart offers existed.
1208
+ cartOfferDiscount: moneySchema.optional()
1116
1209
  });
1117
1210
  var openProductLineItem = import_zod12.z.object({
1118
1211
  id: import_zod12.z.string(),
@@ -1132,7 +1225,7 @@ var openProductLineItem = import_zod12.z.object({
1132
1225
  var lineItemBundleSchema = import_zod12.z.object({
1133
1226
  id: import_zod12.z.string(),
1134
1227
  name: import_zod12.z.string(),
1135
- tagline: import_zod12.z.string(),
1228
+ tagline: import_zod12.z.string().nullish(),
1136
1229
  iconUrl: import_zod12.z.string().nullable(),
1137
1230
  products: lineItemProductSchema.and(import_zod12.z.object({
1138
1231
  included: import_zod12.z.boolean().optional()
@@ -1164,7 +1257,10 @@ var orderTotalSchema = import_zod12.z.object({
1164
1257
  discount: moneySchema,
1165
1258
  subtotal: moneySchema,
1166
1259
  taxes: moneySchema,
1167
- due: moneySchema
1260
+ due: moneySchema,
1261
+ // How much of `discount` came from the cart offer, so it can be shown as one order-wide saving
1262
+ // rather than a share on every line. Absent when no cart offer applied.
1263
+ cartOfferDiscount: moneySchema.optional()
1168
1264
  });
1169
1265
  var openOrderSchema = import_zod12.z.object({
1170
1266
  id: import_zod12.z.string(),
@@ -1172,6 +1268,12 @@ var openOrderSchema = import_zod12.z.object({
1172
1268
  total: orderTotalSchema.nullish(),
1173
1269
  items: openOrderLineItem.array(),
1174
1270
  couponsApplied: couponSchema.array(),
1271
+ // The cart offer on the order as a whole: the buyer's intent. It is part of the cart payload, so
1272
+ // a content push that leaves it out clears it. The API echoes it back even while the cart sits
1273
+ // below the offer's threshold.
1274
+ offerId: import_zod12.z.string().optional(),
1275
+ // The cart offer as actually applied: the result. Absent while the offer's condition doesn't hold.
1276
+ appliedOffer: discountSchema.optional(),
1175
1277
  checkoutUrl: import_zod12.z.string().optional(),
1176
1278
  hostedCheckoutUrl: import_zod12.z.string().optional(),
1177
1279
  embeddedCheckoutUrl: import_zod12.z.string().optional()
@@ -1191,7 +1293,8 @@ var completedOrderSchema = import_zod12.z.object({
1191
1293
  customer: customerSnapshotSchema,
1192
1294
  total: orderTotalSchema,
1193
1295
  items: openOrderLineItem.array(),
1194
- couponsApplied: couponSchema.array()
1296
+ couponsApplied: couponSchema.array(),
1297
+ appliedOffer: discountSchema.optional()
1195
1298
  });
1196
1299
  var orderSchema = import_zod12.z.discriminatedUnion("status", [
1197
1300
  openOrderSchema.extend({
@@ -1349,6 +1452,7 @@ var OrderEndpoints = class {
1349
1452
  method: "PATCH",
1350
1453
  body: {
1351
1454
  currency: order.currency || void 0,
1455
+ offerId: order.offerId,
1352
1456
  items: order.items
1353
1457
  }
1354
1458
  }
@@ -1363,6 +1467,12 @@ var OrderEndpoints = class {
1363
1467
  const response = await this.api.fetch(`/api/customer/orders/${orderId}/billing-details`, openOrderSchema, { method: "DELETE" });
1364
1468
  return response.data;
1365
1469
  }
1470
+ // Takes the cart offer off the order without re-sending the cart. Offers picked for a single item
1471
+ // come off by pushing that item without its `offerId` instead.
1472
+ async removeOffer(orderId) {
1473
+ const response = await this.api.fetch(`/api/customer/orders/${orderId}/offer`, openOrderSchema, { method: "DELETE" });
1474
+ return response.data;
1475
+ }
1366
1476
  async addCouponCode(orderId, code) {
1367
1477
  const response = await this.api.fetch(`/api/customer/orders/${orderId}/coupon?code=${encodeURIComponent(code)}`, openOrderSchema, { method: "PATCH" });
1368
1478
  return response.data;
@@ -1667,6 +1777,12 @@ __export(schemas_exports11, {
1667
1777
  subscriptions: () => schemas_exports8
1668
1778
  });
1669
1779
 
1780
+ // src/storefront/models.ts
1781
+ var promotionDisplays = ["Banner", "Popup"];
1782
+ function isPromotionDisplay(value) {
1783
+ return promotionDisplays.includes(value);
1784
+ }
1785
+
1670
1786
  // src/utils/discount.ts
1671
1787
  var DiscountUtils = class {
1672
1788
  static apply(discount, price) {
@@ -1693,25 +1809,68 @@ var MoneyCollectionUtils = class {
1693
1809
  };
1694
1810
 
1695
1811
  // src/utils/offer.ts
1812
+ function evaluated(unit, currency, current, target, eligible) {
1813
+ return {
1814
+ eligible,
1815
+ progress: {
1816
+ unit,
1817
+ currency,
1818
+ current,
1819
+ target,
1820
+ remaining: Math.max(0, target - current),
1821
+ fraction: target <= 0 ? 1 : Math.min(1, Math.max(0, current / target)),
1822
+ eligible
1823
+ }
1824
+ };
1825
+ }
1696
1826
  var OfferUtils = class _OfferUtils {
1697
- static eligible(offer, order) {
1698
- switch (offer.condition.type) {
1827
+ // Callers that only have an order keep working and simply have no cart total, so a monetary
1828
+ // condition fails closed for them. Pass the full context to have `CartTotal` actually evaluate.
1829
+ static eligible(offer, context) {
1830
+ return _OfferUtils.evaluate(offer, context).eligible;
1831
+ }
1832
+ // How close the cart is to satisfying the offer, or `null` when there is nothing to show progress
1833
+ // toward: an unsupported condition, an unknown cart total, or a condition with only an upper
1834
+ // bound (which the buyer starts out satisfying rather than working toward).
1835
+ static progress(offer, context) {
1836
+ return _OfferUtils.evaluate(offer, context).progress;
1837
+ }
1838
+ // The single evaluator behind both faces, so the "spend €12 more" nudge and the gate that unlocks
1839
+ // the discount can never disagree about the same cart.
1840
+ static evaluate(offer, context) {
1841
+ var _a, _b;
1842
+ const { order, cartTotal } = "items" in context ? { order: context, cartTotal: void 0 } : context;
1843
+ const condition = offer.condition;
1844
+ switch (condition.type) {
1699
1845
  case "CartContainsItems": {
1700
1846
  const relevantItems = order.items.filter(
1701
- (i) => i.type === "Product" && offer.condition.relevantItemVariations[`Product/${i.productId}`] && offer.condition.relevantItemVariations[`Product/${i.productId}`].includes(i.variationId) || i.type === "Bundle" && offer.condition.relevantItemVariations[`Bundle/${i.bundleId}`] && offer.condition.relevantItemVariations[`Bundle/${i.bundleId}`].includes(i.variationId)
1847
+ (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)
1702
1848
  );
1703
1849
  const relevantReplacedItems = order.items.flatMap((i) => {
1704
- var _a;
1705
- return offer.id === i.offerId && i.type === "Bundle" && _OfferUtils.targetContainsBundle(offer, i.id) ? (_a = i.replaced) != null ? _a : [] : [];
1850
+ var _a2;
1851
+ return offer.id === i.offerId && i.type === "Bundle" && _OfferUtils.targetContainsBundle(offer, i.id) ? (_a2 = i.replaced) != null ? _a2 : [] : [];
1706
1852
  }).filter((ref) => {
1707
1853
  const [productId, variationId] = ref.split("/");
1708
- return offer.condition.relevantItemVariations[`Product/${productId}`] && offer.condition.relevantItemVariations[`Product/${productId}`].includes(variationId);
1854
+ return condition.relevantItemVariations[`Product/${productId}`] && condition.relevantItemVariations[`Product/${productId}`].includes(variationId);
1709
1855
  });
1710
- return relevantItems.length + relevantReplacedItems.length >= offer.condition.minimumItems;
1856
+ const current = relevantItems.length + relevantReplacedItems.length;
1857
+ return evaluated("Items", void 0, current, condition.minimumItems, current >= condition.minimumItems);
1858
+ }
1859
+ case "CartTotal": {
1860
+ if (!cartTotal)
1861
+ return { eligible: false, progress: null };
1862
+ const minimum = (_a = condition.minimumTotal) == null ? void 0 : _a[cartTotal.currency];
1863
+ const maximum = (_b = condition.maximumTotal) == null ? void 0 : _b[cartTotal.currency];
1864
+ if (minimum === void 0 && maximum === void 0)
1865
+ return { eligible: false, progress: null };
1866
+ const eligible = (minimum === void 0 || cartTotal.amount >= minimum) && (maximum === void 0 || cartTotal.amount <= maximum);
1867
+ if (minimum === void 0)
1868
+ return { eligible, progress: null };
1869
+ return evaluated("Money", cartTotal.currency, cartTotal.amount, minimum, eligible);
1711
1870
  }
1712
1871
  }
1713
- console.warn("Unsupported offer condition found:", offer.condition);
1714
- return false;
1872
+ console.warn("Unsupported offer condition found:", condition);
1873
+ return { eligible: false, progress: null };
1715
1874
  }
1716
1875
  // The relevant targets of an offer with more than a single target needs to:
1717
1876
  // a. Not be owned, or all targets need to be owned
@@ -1752,6 +1911,276 @@ var OfferUtils = class _OfferUtils {
1752
1911
  }
1753
1912
  };
1754
1913
 
1914
+ // src/utils/cart.ts
1915
+ var CartUtils = class _CartUtils {
1916
+ // The pricing variation a line item points at, falling back to the item's default variation when
1917
+ // the variation id doesn't resolve, which is what the cart has always displayed.
1918
+ static resolveVariation(catalog, item) {
1919
+ var _a, _b;
1920
+ const source = item.type === "Product" ? catalog.products.find((p) => p.id === item.productId) : catalog.bundles.find((b) => b.id === item.bundleId);
1921
+ 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;
1922
+ }
1923
+ // A line's unit price after its product discount and any pricing tier, and *before* any offer.
1924
+ // This is the basis the API measures cart total conditions on, which is why it reads the catalog
1925
+ // rather than the line item's own `price`: `enrichLineItems` rewrites that to the offer-discounted
1926
+ // price, and reading it here would take the offer off twice.
1927
+ static unitPrice(catalog, item, currency) {
1928
+ var _a;
1929
+ return (_a = _CartUtils.unitPricing(catalog, item, currency)) == null ? void 0 : _a.price;
1930
+ }
1931
+ // A line's list and post-product-discount unit prices, from whichever pricing tier applies. Both
1932
+ // come from the same tier so they can be subtracted from one another: taking the list price off one
1933
+ // tier and the paid price off another would invent a discount neither of them offers.
1934
+ static unitPricing(catalog, item, currency) {
1935
+ var _a, _b, _c, _d, _e, _f;
1936
+ const variation = _CartUtils.resolveVariation(catalog, item);
1937
+ const price = variation == null ? void 0 : variation.price[currency];
1938
+ if (price === void 0 || variation === void 0)
1939
+ return void 0;
1940
+ const eligiblePricingTiers = (_b = (_a = variation.pricingTiers) == null ? void 0 : _a.filter((tier) => item.quantity >= tier.minQuantity)) != null ? _b : [];
1941
+ const matchingPricingTier = eligiblePricingTiers.length > 0 ? eligiblePricingTiers[eligiblePricingTiers.length - 1] : null;
1942
+ const tierPrice = matchingPricingTier == null ? void 0 : matchingPricingTier.price[currency];
1943
+ if (tierPrice !== void 0 && tierPrice < price) {
1944
+ return {
1945
+ price: tierPrice,
1946
+ originalPrice: (_d = (_c = matchingPricingTier == null ? void 0 : matchingPricingTier.originalPrice) == null ? void 0 : _c[currency]) != null ? _d : tierPrice
1947
+ };
1948
+ }
1949
+ return { price, originalPrice: (_f = (_e = variation.originalPrice) == null ? void 0 : _e[currency]) != null ? _f : price };
1950
+ }
1951
+ // The number the API measures cart conditions against: each line's price after its product
1952
+ // discount, times quantity, before offers and coupons. `undefined` when any line's price can't be
1953
+ // resolved in `currency`. One unreadable line makes the whole measurement a guess, and a guess
1954
+ // must not unlock an offer.
1955
+ static measure(catalog, items, currency) {
1956
+ if (!currency)
1957
+ return void 0;
1958
+ let amount = 0;
1959
+ for (const item of items) {
1960
+ const unitPrice = _CartUtils.unitPrice(catalog, item, currency);
1961
+ if (unitPrice === void 0)
1962
+ return void 0;
1963
+ amount += unitPrice * item.quantity;
1964
+ }
1965
+ return { currency, amount };
1966
+ }
1967
+ static conditionContext(catalog, order, currency) {
1968
+ return { order, cartTotal: _CartUtils.measure(catalog, order.items, currency) };
1969
+ }
1970
+ // Which lines a cart offer discounts. No targets means the whole cart, a reading that only holds
1971
+ // for cart-scope offers, where an empty target list is the API's way of saying "every line".
1972
+ static cartOfferAppliesToLine(offer, item) {
1973
+ if (offer.targets.length === 0)
1974
+ return true;
1975
+ 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)));
1976
+ }
1977
+ // The cart offer actually discounting this cart, as opposed to the one attached to the order. An
1978
+ // offer whose targets name nothing in the cart discounts nothing however healthy the total looks,
1979
+ // so it has not really applied.
1980
+ static appliedCartOffer(catalog, order, context) {
1981
+ var _a;
1982
+ if (!order.offerId)
1983
+ return void 0;
1984
+ const offer = (_a = catalog.offers) == null ? void 0 : _a.find((o) => o.id === order.offerId);
1985
+ if (!offer || offer.scope !== "Cart" || !OfferUtils.eligible(offer, context))
1986
+ return void 0;
1987
+ return order.items.some((item) => _CartUtils.cartOfferAppliesToLine(offer, item)) ? offer : void 0;
1988
+ }
1989
+ // What the cart is worth to the buyer, broken down so the cart can show the saving on its own row.
1990
+ // Unlike `measure`, an unresolvable price counts as zero here: this number fills a label, so a
1991
+ // half-loaded catalog should still render a total rather than nothing.
1992
+ static total(catalog, order, currency) {
1993
+ var _a, _b;
1994
+ const context = _CartUtils.conditionContext(catalog, order, currency);
1995
+ const cartOffer = _CartUtils.appliedCartOffer(catalog, order, context);
1996
+ let original = 0;
1997
+ let subtotal = 0;
1998
+ let cartOfferDiscount = 0;
1999
+ for (const item of order.items) {
2000
+ const unitPrice = _CartUtils.unitPriceAfterItemOffer(catalog, item, currency, context);
2001
+ original += ((_b = (_a = _CartUtils.unitPricing(catalog, item, currency)) == null ? void 0 : _a.originalPrice) != null ? _b : unitPrice) * item.quantity;
2002
+ subtotal += unitPrice * item.quantity;
2003
+ if (cartOffer && _CartUtils.cartOfferAppliesToLine(cartOffer, item))
2004
+ cartOfferDiscount += DiscountUtils.apply(cartOffer.discount, { [currency]: unitPrice })[currency] * item.quantity;
2005
+ }
2006
+ const amount = subtotal - cartOfferDiscount;
2007
+ return {
2008
+ currency,
2009
+ original,
2010
+ subtotal,
2011
+ cartOffer: cartOffer ? { offer: cartOffer, discount: cartOfferDiscount } : null,
2012
+ discount: original - amount,
2013
+ amount
2014
+ };
2015
+ }
2016
+ // The cart offer worth attaching to the order. Cart offers are always percentages, but 10% off
2017
+ // everything can still beat 50% off one cheap line, so they are ranked by what they would actually
2018
+ // take off this cart rather than by percentage.
2019
+ static bestCartOffer(catalog, order, currency) {
2020
+ var _a;
2021
+ const context = _CartUtils.conditionContext(catalog, order, currency);
2022
+ let best;
2023
+ for (const offer of (_a = catalog.offers) != null ? _a : []) {
2024
+ if (offer.scope !== "Cart" || !OfferUtils.eligible(offer, context))
2025
+ continue;
2026
+ const value = _CartUtils.cartOfferValue(catalog, order, offer, currency, context);
2027
+ if (value <= 0)
2028
+ continue;
2029
+ if (!best || value > best.value || value === best.value && offer.id === order.offerId)
2030
+ best = { offer, value };
2031
+ }
2032
+ return best == null ? void 0 : best.offer;
2033
+ }
2034
+ /**
2035
+ * What a cart offer would take off this cart, whether or not its condition is met yet.
2036
+ *
2037
+ * Computed on the same post-line-offer price `total` discounts, so "which offer do we attach" and
2038
+ * "what does the cart actually save" can never disagree. Ranking on the list price instead would
2039
+ * over-value an offer aimed at a line that already carries a steep offer of its own: 50% of a
2040
+ * line discounted to a tenth of its price is worth less than 10% of a full-price one.
2041
+ */
2042
+ static cartOfferValue(catalog, order, offer, currency, context) {
2043
+ const conditionContext = context != null ? context : _CartUtils.conditionContext(catalog, order, currency);
2044
+ return order.items.reduce((aggregate, item) => {
2045
+ if (!_CartUtils.cartOfferAppliesToLine(offer, item))
2046
+ return aggregate;
2047
+ const unitPrice = _CartUtils.unitPriceAfterItemOffer(catalog, item, currency, conditionContext);
2048
+ return aggregate + DiscountUtils.apply(offer.discount, { [currency]: unitPrice })[currency] * item.quantity;
2049
+ }, 0);
2050
+ }
2051
+ // A line's unit price once its own offer has come off, which is what a cart offer is applied to:
2052
+ // the API discounts in the order product discount -> line offer -> cart offer -> coupons. An
2053
+ // unresolvable price counts as zero, matching `total`'s "still render something" stance rather
2054
+ // than `measure`'s "refuse to guess".
2055
+ static unitPriceAfterItemOffer(catalog, item, currency, context) {
2056
+ var _a, _b;
2057
+ const unitPrice = (_a = _CartUtils.unitPrice(catalog, item, currency)) != null ? _a : 0;
2058
+ const itemOffer = item.offerId ? (_b = catalog.offers) == null ? void 0 : _b.find((o) => o.id === item.offerId) : void 0;
2059
+ if (!itemOffer || !OfferUtils.eligible(itemOffer, context))
2060
+ return unitPrice;
2061
+ return unitPrice - DiscountUtils.apply(itemOffer.discount, { [currency]: unitPrice })[currency];
2062
+ }
2063
+ };
2064
+
2065
+ // src/utils/image.ts
2066
+ var imageWidths = [16, 32, 48, 64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048];
2067
+ var maxImageDimension = imageWidths[imageWidths.length - 1];
2068
+ var transformPathSegment = "_i";
2069
+ var maxCoverAspectRatio = 4;
2070
+ function isAssetUrl(url) {
2071
+ let parsed;
2072
+ try {
2073
+ parsed = new URL(url);
2074
+ } catch (e) {
2075
+ return false;
2076
+ }
2077
+ return parsed.hostname === "assets.moonbase.sh" || parsed.hostname.startsWith("assets.") && parsed.hostname.endsWith(".moonbase.sh");
2078
+ }
2079
+ function isRendition(url) {
2080
+ try {
2081
+ return new URL(url).pathname.startsWith(`/${transformPathSegment}/`);
2082
+ } catch (e) {
2083
+ return false;
2084
+ }
2085
+ }
2086
+ function isTransformableImageUrl(url) {
2087
+ return isAssetUrl(url) && !isRendition(url);
2088
+ }
2089
+ var warned = /* @__PURE__ */ new Set();
2090
+ function warnOnce(reason, url, message) {
2091
+ const key = `${reason}:${url}`;
2092
+ if (warned.has(key))
2093
+ return;
2094
+ warned.add(key);
2095
+ console.warn(message, url);
2096
+ }
2097
+ function snap(value) {
2098
+ var _a;
2099
+ if (value === void 0 || !Number.isFinite(value) || value <= 0)
2100
+ return void 0;
2101
+ return (_a = imageWidths.find((width) => value <= width)) != null ? _a : maxImageDimension;
2102
+ }
2103
+ var ImageUtils = class _ImageUtils {
2104
+ /**
2105
+ * Rewrites an asset URL to ask the CDN for a rendition at the given size.
2106
+ *
2107
+ * Returns the URL untouched — never a broken one — when there is nothing to do: a URL that
2108
+ * isn't a Moonbase asset, one that is already a rendition, an empty transform, or a `cover`
2109
+ * crop too extreme for the CDN to serve. Callers can apply it unconditionally.
2110
+ */
2111
+ static transform(url, transform) {
2112
+ if (!url)
2113
+ return void 0;
2114
+ if (!isAssetUrl(url)) {
2115
+ warnOnce(
2116
+ "host",
2117
+ url,
2118
+ "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:"
2119
+ );
2120
+ return url;
2121
+ }
2122
+ if (isRendition(url))
2123
+ return url;
2124
+ const query = _ImageUtils.toQuery(transform);
2125
+ if (query === null) {
2126
+ warnOnce(
2127
+ "aspect-ratio",
2128
+ url,
2129
+ `A 'cover' crop more lopsided than ${maxCoverAspectRatio}:1 is not served. Serving this image at its original size instead:`
2130
+ );
2131
+ return url;
2132
+ }
2133
+ if (query.length === 0)
2134
+ return url;
2135
+ const parsed = new URL(url);
2136
+ const separator = parsed.search ? "&" : "?";
2137
+ return `${parsed.origin}/${transformPathSegment}${parsed.pathname}${parsed.search}${separator}${query.map(([key, value]) => `${key}=${value}`).join("&")}`;
2138
+ }
2139
+ /**
2140
+ * A `srcset` covering the given pixel densities, so a retina screen gets the sharper rendition
2141
+ * without every screen paying for it. Pair it with a `src` built from the same transform.
2142
+ */
2143
+ static srcSet(url, transform, ...scales) {
2144
+ if (!url || scales.length === 0)
2145
+ return void 0;
2146
+ return scales.map((scale) => `${_ImageUtils.transform(url, _ImageUtils.atScale(transform, scale))} ${scale}x`).join(", ");
2147
+ }
2148
+ /** The same transform at a different pixel density. */
2149
+ static atScale(transform, scale) {
2150
+ return {
2151
+ ...transform,
2152
+ width: transform.width === void 0 ? void 0 : transform.width * scale,
2153
+ height: transform.height === void 0 ? void 0 : transform.height * scale
2154
+ };
2155
+ }
2156
+ /**
2157
+ * The query parameters for a transform, in the CDN's own order, with defaults left out so two
2158
+ * equivalent transforms always produce the same URL — and therefore the same cache entry.
2159
+ * `null` when the CDN would refuse the transform outright.
2160
+ */
2161
+ static toQuery(transform) {
2162
+ var _a, _b;
2163
+ const fit = (_a = transform.fit) != null ? _a : "contain";
2164
+ const width = snap(transform.width);
2165
+ const height = fit === "cover" ? (_b = snap(transform.height)) != null ? _b : width : snap(transform.height);
2166
+ if (fit === "cover" && width && height) {
2167
+ const ratio = width > height ? width / height : height / width;
2168
+ if (ratio > maxCoverAspectRatio)
2169
+ return null;
2170
+ }
2171
+ const query = [];
2172
+ if (width)
2173
+ query.push(["w", String(width)]);
2174
+ if (height && (fit === "cover" || transform.height !== void 0))
2175
+ query.push(["h", String(height)]);
2176
+ if (fit !== "contain")
2177
+ query.push(["fit", fit]);
2178
+ if (transform.format)
2179
+ query.push(["f", transform.format]);
2180
+ return query;
2181
+ }
2182
+ };
2183
+
1755
2184
  // src/utils/utm.ts
1756
2185
  function parseOffSiteReferrer(rawReferrer, currentOrigin) {
1757
2186
  if (!rawReferrer)
@@ -1799,10 +2228,12 @@ var MoonbaseClient = class {
1799
2228
  ActivationRequestStatus,
1800
2229
  ActivationStatus,
1801
2230
  Architecture,
2231
+ CartUtils,
1802
2232
  ConnectableAccountProvider,
1803
2233
  ConsoleLogger,
1804
2234
  CycleLength,
1805
2235
  DiscountUtils,
2236
+ ImageUtils,
1806
2237
  InMemoryStore,
1807
2238
  LicenseStatus,
1808
2239
  LocalStorageStore,
@@ -1820,9 +2251,14 @@ var MoonbaseClient = class {
1820
2251
  Platform,
1821
2252
  SubscriptionStatus,
1822
2253
  TokenStore,
2254
+ imageWidths,
2255
+ isPromotionDisplay,
2256
+ isTransformableImageUrl,
2257
+ maxImageDimension,
1823
2258
  objectToQuery,
1824
2259
  parseOffSiteReferrer,
1825
2260
  problemDetailsSchema,
2261
+ promotionDisplays,
1826
2262
  resolveUtm,
1827
2263
  schemas,
1828
2264
  utmToObject