@shoppexio/storefront-react 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  resolveAccountView,
24
24
  sdkFavoritesTransport,
25
25
  useFavorites
26
- } from "./chunk-ZEFZPUAW.js";
26
+ } from "./chunk-NBOIS53R.js";
27
27
 
28
28
  // src/account-route-messages.ts
29
29
  var ACCOUNT_ROUTE_MESSAGE_KEYS = [
@@ -71,6 +71,125 @@ import {
71
71
  useState
72
72
  } from "react";
73
73
 
74
+ // ../contracts/src/catalog-line-price.ts
75
+ function ruleTypeName(entry) {
76
+ if (typeof entry.type === "string") return entry.type;
77
+ if (typeof entry.discount_type === "string") return entry.discount_type;
78
+ if (typeof entry.discountType === "string") return entry.discountType;
79
+ return "";
80
+ }
81
+ function parseVolumeDiscountRules(raw) {
82
+ let parsed = raw;
83
+ if (typeof raw === "string") {
84
+ try {
85
+ parsed = JSON.parse(raw);
86
+ } catch {
87
+ return [];
88
+ }
89
+ }
90
+ const container = parsed && typeof parsed === "object" ? parsed : null;
91
+ const source = Array.isArray(parsed) ? parsed : Array.isArray(container?.volume_discounts) ? container.volume_discounts : Array.isArray(container?.volumeDiscounts) ? container.volumeDiscounts : [];
92
+ const rules = [];
93
+ for (const candidate of source) {
94
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
95
+ const entry = candidate;
96
+ const quantity = Number(entry.quantity ?? entry.min_quantity ?? entry.minQuantity);
97
+ const value = Number(entry.value ?? entry.discount ?? entry.amount);
98
+ const typeNormalized = ruleTypeName(entry).trim().toUpperCase();
99
+ const type = typeNormalized.includes("PERCENT") ? "PERCENTAGE" : typeNormalized.includes("FIX") || typeNormalized.includes("AMOUNT") ? "FIXED" : null;
100
+ if (!Number.isFinite(quantity) || quantity < 1) continue;
101
+ if (!Number.isFinite(value) || value <= 0) continue;
102
+ if (!type) continue;
103
+ rules.push({ quantity: Math.floor(quantity), type, value });
104
+ }
105
+ return rules.sort((a, b) => a.quantity - b.quantity);
106
+ }
107
+ function resolveVolumeDiscountRule(rules, quantity) {
108
+ if (!Number.isFinite(quantity) || quantity < 1) return null;
109
+ let resolved = null;
110
+ for (const rule of rules) {
111
+ if (quantity >= rule.quantity) resolved = rule;
112
+ }
113
+ return resolved;
114
+ }
115
+ var TEN = BigInt(10);
116
+ var SIGNIFICANT_DIGITS = 20;
117
+ function pow10(n) {
118
+ return TEN ** BigInt(n);
119
+ }
120
+ function divRoundHalfUp(value, divisor) {
121
+ const negative = value < BigInt(0);
122
+ const abs = negative ? -value : value;
123
+ let quotient = abs / divisor;
124
+ if (abs % divisor * BigInt(2) >= divisor) quotient += BigInt(1);
125
+ return negative ? -quotient : quotient;
126
+ }
127
+ function rounded(d) {
128
+ const digits = (d.c < BigInt(0) ? -d.c : d.c).toString().length;
129
+ if (digits <= SIGNIFICANT_DIGITS) return d;
130
+ const drop = digits - SIGNIFICANT_DIGITS;
131
+ return { c: divRoundHalfUp(d.c, pow10(drop)), e: d.e + drop };
132
+ }
133
+ function dec(value) {
134
+ const match = /^(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?$/.exec(String(value));
135
+ if (!match) throw new RangeError(`Not a finite amount: ${value}`);
136
+ const fraction = match[3] ?? "";
137
+ return { c: BigInt(`${match[1]}${match[2]}${fraction}`), e: Number(match[4] ?? 0) - fraction.length };
138
+ }
139
+ function plus(a, b) {
140
+ const e = Math.min(a.e, b.e);
141
+ return rounded({ c: a.c * pow10(a.e - e) + b.c * pow10(b.e - e), e });
142
+ }
143
+ function minus(a, b) {
144
+ return plus(a, { c: -b.c, e: b.e });
145
+ }
146
+ function times(a, b) {
147
+ return rounded({ c: a.c * b.c, e: a.e + b.e });
148
+ }
149
+ function percentOf(amount, percent) {
150
+ const product = times(amount, dec(percent));
151
+ return rounded({ c: product.c, e: product.e - 2 });
152
+ }
153
+ function greaterThan(a, b) {
154
+ return minus(a, b).c > BigInt(0);
155
+ }
156
+ function toCents(d) {
157
+ return d.e >= -2 ? d.c * pow10(d.e + 2) : divRoundHalfUp(d.c, pow10(-2 - d.e));
158
+ }
159
+ function centsToAmount(cents) {
160
+ const negative = cents < BigInt(0);
161
+ const digits = (negative ? -cents : cents).toString().padStart(3, "0");
162
+ return Number(`${negative ? "-" : ""}${digits.slice(0, -2)}.${digits.slice(-2)}`);
163
+ }
164
+ function clampPercent(value) {
165
+ return Math.min(Math.max(value, 0), 100);
166
+ }
167
+ function calculateCatalogLinePrice(input) {
168
+ const unitPrice = Number.isFinite(input.unitPrice) ? Math.max(input.unitPrice, 0) : 0;
169
+ const quantity = Number.isFinite(input.quantity) ? Math.max(1, Math.floor(input.quantity ?? 1)) : 1;
170
+ const addonsTotal = (input.addonPrices ?? []).reduce(
171
+ (sum, price) => plus(sum, dec(Number.isFinite(price) ? Math.max(price, 0) : 0)),
172
+ dec(0)
173
+ );
174
+ const subtotalCents = toCents(times(plus(dec(unitPrice), addonsTotal), dec(quantity)));
175
+ const subtotal = { c: subtotalCents, e: -2 };
176
+ const rawPercent = Number(input.productDiscountPercent ?? 0);
177
+ const productDiscount = Number.isFinite(rawPercent) && rawPercent > 0 ? percentOf(subtotal, clampPercent(rawPercent)) : dec(0);
178
+ const afterProduct = minus(subtotal, productDiscount);
179
+ const rule = resolveVolumeDiscountRule(parseVolumeDiscountRules(input.volumeDiscounts), quantity);
180
+ const volumeRaw = !rule ? dec(0) : rule.type === "PERCENTAGE" ? percentOf(afterProduct, clampPercent(rule.value)) : dec(rule.value);
181
+ const volumeDiscount = greaterThan(volumeRaw, afterProduct) ? afterProduct : volumeRaw;
182
+ const totalCents = toCents(minus(afterProduct, volumeDiscount));
183
+ const afterProductCents = toCents(afterProduct);
184
+ return {
185
+ subtotal: centsToAmount(subtotalCents),
186
+ productDiscount: centsToAmount(subtotalCents - afterProductCents),
187
+ subtotalAfterProductDiscount: centsToAmount(afterProductCents),
188
+ volumeDiscount: centsToAmount(afterProductCents - totalCents),
189
+ total: centsToAmount(totalCents)
190
+ };
191
+ }
192
+
74
193
  // src/parse.ts
75
194
  function isRecord(value) {
76
195
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -434,6 +553,10 @@ function parseDeliverable(value) {
434
553
  const normalized = typeof value === "string" ? value.trim().toUpperCase() : "";
435
554
  return DELIVERABLES_BY_TYPE[normalized] ?? null;
436
555
  }
556
+ var DELIVERY_TYPES = /* @__PURE__ */ new Set(["key", "download", "service", "shipped"]);
557
+ function parseDeliveryType(value) {
558
+ return typeof value === "string" && DELIVERY_TYPES.has(value) ? value : null;
559
+ }
437
560
  function resolveYouReceive(product) {
438
561
  if (product.deliverable === "serials") return { kind: "serials", unitLabel: product.unitLabelSingular };
439
562
  if (product.deliverable === "file") return { kind: "download" };
@@ -520,7 +643,8 @@ function getProductGroups() {
520
643
  categoryId: optionalString(group.category_id),
521
644
  productIds,
522
645
  productsCount: normalizeOptionalNonNegativeNumber(group.products_count) ?? productIds.length,
523
- sortPriority: normalizeOptionalNonNegativeNumber(group.sort_priority) ?? 0
646
+ sortPriority: normalizeOptionalNonNegativeNumber(group.sort_priority) ?? 0,
647
+ ...parseGroupFeedback(group.feedback, `${context}.feedback`)
524
648
  };
525
649
  });
526
650
  }
@@ -585,6 +709,7 @@ function getStorefrontShop(productCount) {
585
709
  const termsHtml = store.tos_enabled === true ? optionalString(store.terms_of_service) : null;
586
710
  return {
587
711
  name: requireString(store, "name", "store"),
712
+ description: optionalString(store.description),
588
713
  logoUrl: firstOptionalString(store, ["logo", "avatar"]),
589
714
  avatarUrl: firstOptionalString(store, ["avatar", "logo"]),
590
715
  socialLinks: SOCIAL_NETWORKS.flatMap((network) => {
@@ -745,7 +870,8 @@ function parseProduct(value, index) {
745
870
  const id = requireString(product, "uniqid", context);
746
871
  const productId = optionalString(product.id) ?? id;
747
872
  const discountPercent = product.pay_what_you_want === true ? 0 : normalizeDiscountPercent(product.price_discount);
748
- const applyDiscount = (amount) => discountPercent > 0 ? roundCurrency(amount * (1 - discountPercent / 100)) : amount;
873
+ const applyDiscount = (amount) => discountPercent > 0 ? calculateDisplayLineTotal(amount, 1, discountPercent, []) : amount;
874
+ const images = parseProductImages(product.images);
749
875
  const rawVariants = parsePriceVariants(product.price_variants, context);
750
876
  const priceVariants = rawVariants.map((variant) => ({
751
877
  ...variant,
@@ -768,7 +894,8 @@ function parseProduct(value, index) {
768
894
  description: optionalString(product.description),
769
895
  slug: optionalString(product.slug),
770
896
  imageUrl: resolveProductImage(product),
771
- imageUrls: parseProductImageUrls(product.images),
897
+ imageUrls: images.map((image) => image.url),
898
+ images,
772
899
  stock: applyOrderable(
773
900
  normalizeStock(product.stock, `${context}.stock`),
774
901
  product.orderable,
@@ -778,7 +905,7 @@ function parseProduct(value, index) {
778
905
  normalizeOptionalNonNegativeNumber(product.quantity_warning)
779
906
  ),
780
907
  hideStockCounter: typeof product.hide_stock_counter === "boolean" ? product.hide_stock_counter : null,
781
- volumeDiscounts: parseVolumeDiscounts(product.volume_discounts),
908
+ volumeDiscounts: parseVolumeDiscountRules(product.volume_discounts),
782
909
  payWhatYouWant: product.pay_what_you_want === true,
783
910
  discountPercent,
784
911
  baseUnitPrice: undiscountedPrice,
@@ -796,6 +923,8 @@ function parseProduct(value, index) {
796
923
  automaticDeals: parseAutomaticDeals(product.automatic_deals),
797
924
  videoEmbedSrc: resolveProductVideoEmbedSrc(product.video_link),
798
925
  addons: parseAddons(product.addons),
926
+ addonsRequired: optionalBoolean(product.addons_required, false, `${context}.addons_required`),
927
+ upsellProductIds: parseUpsellProductIds(product.upsell_product_ids, context),
799
928
  customFields: parseCustomFields(product.custom_fields, context),
800
929
  // Strict on purpose: the serializer always emits `on_hold`, so an absent
801
930
  // flag is the only tolerated shape (false) and a present non-boolean is a
@@ -808,8 +937,12 @@ function parseProduct(value, index) {
808
937
  ratingTally: rating?.tally ?? null,
809
938
  shortDescription: optionalString(product.short_description),
810
939
  deliverable: parseDeliverable(product.type),
940
+ deliveryType: parseDeliveryType(product.delivery_type),
811
941
  unitLabelSingular: optionalString(product.label_singular),
812
942
  unitLabelPlural: optionalString(product.label_plural),
943
+ deliveryFormat: optionalString(product.delivery_format),
944
+ region: optionalString(product.region),
945
+ restockedAt: optionalString(product.restocked_at),
813
946
  serviceStatus: parseServiceStatus(product.service_status, context),
814
947
  highlights: parseHighlights(product.product_highlights),
815
948
  metadata: parseMetadata(product.metadata),
@@ -962,20 +1095,34 @@ function parseAddons(value) {
962
1095
  addons.push({
963
1096
  id,
964
1097
  title,
1098
+ description: optionalString(entry.description),
965
1099
  price: Number.isFinite(price) && price > 0 ? price : 0,
966
1100
  required: entry.required === true
967
1101
  });
968
1102
  }
969
1103
  return addons;
970
1104
  }
971
- function parseProductImageUrls(value) {
1105
+ function parseUpsellProductIds(value, context) {
1106
+ if (value === void 0) return [];
1107
+ if (!Array.isArray(value) || !value.every((id) => typeof id === "string")) {
1108
+ throw new Error(`Invalid Shoppex ${context}.upsell_product_ids: expected an array of product uniqids.`);
1109
+ }
1110
+ return value;
1111
+ }
1112
+ function parseProductImages(value) {
972
1113
  if (!Array.isArray(value)) return [];
973
- const urls = [];
1114
+ const images = [];
974
1115
  for (const entry of value) {
975
- const url = typeof entry === "string" ? optionalString(entry) : isRecord(entry) ? optionalString(entry.url) : null;
976
- if (url && !urls.includes(url)) urls.push(url);
1116
+ const record = isRecord(entry) ? entry : null;
1117
+ const url = record ? optionalString(record.url) : typeof entry === "string" ? optionalString(entry) : null;
1118
+ if (!url || images.some((image) => image.url === url)) continue;
1119
+ images.push({
1120
+ url,
1121
+ kind: record && (record.kind === "art" || record.kind === "logo") ? record.kind : null,
1122
+ inkColor: record && typeof record.ink_color === "string" && /^#[0-9a-f]{6}$/.test(record.ink_color) ? record.ink_color : null
1123
+ });
977
1124
  }
978
- return urls;
1125
+ return images;
979
1126
  }
980
1127
  function parseMetadata(value) {
981
1128
  if (!isRecord(value)) return {};
@@ -1012,6 +1159,7 @@ function parseCustomFields(value, context) {
1012
1159
  const rawType = typeof entry.type === "string" ? entry.type.trim().toLowerCase() : "";
1013
1160
  const type = rawType || "text";
1014
1161
  if (type === "hidden") continue;
1162
+ if ([entry.collect_after_payment, entry.collectAfterPayment].some((flag) => flag === true || flag === 1 || flag === "1" || flag === "true")) continue;
1015
1163
  const defaultValue = firstOptionalString(entry, ["default_value", "default", "value"]);
1016
1164
  fields.push({
1017
1165
  name,
@@ -1023,7 +1171,8 @@ function parseCustomFields(value, context) {
1023
1171
  required: entry.required === true || entry.required === 1 || entry.required === "1" || entry.required === "true",
1024
1172
  defaultValue: defaultValue ?? "",
1025
1173
  placeholder: typeof entry.placeholder === "string" ? entry.placeholder : "",
1026
- regex: optionalString(entry.regex)
1174
+ regex: optionalString(entry.regex),
1175
+ ...type === "select" ? { options: Array.isArray(entry.options) ? entry.options.filter((option) => typeof option === "string") : [] } : {}
1027
1176
  });
1028
1177
  }
1029
1178
  return fields;
@@ -1083,41 +1232,32 @@ function parseFaqs(value) {
1083
1232
  }
1084
1233
  return faqs;
1085
1234
  }
1086
- function parseVolumeDiscounts(value) {
1087
- const source = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.volume_discounts) ? value.volume_discounts : [];
1088
- const rules = [];
1089
- for (const entry of source) {
1090
- if (!isRecord(entry)) continue;
1091
- const quantity = Number(entry.quantity ?? entry.min_quantity);
1092
- const amount = Number(entry.value ?? entry.discount ?? entry.amount);
1093
- const typeRaw = String(entry.type ?? entry.discount_type ?? "").toUpperCase();
1094
- const type = typeRaw.includes("PERCENT") ? "PERCENTAGE" : typeRaw.includes("FIX") || typeRaw.includes("AMOUNT") ? "FIXED" : null;
1095
- if (!Number.isFinite(quantity) || quantity < 1 || !Number.isFinite(amount) || amount <= 0 || !type) {
1096
- continue;
1097
- }
1098
- rules.push({ quantity: Math.floor(quantity), type, value: amount });
1099
- }
1100
- return rules.sort((a, b) => a.quantity - b.quantity);
1101
- }
1102
- function toCents(value) {
1235
+ function toCents2(value) {
1103
1236
  return Math.round(Number((value * 100).toPrecision(15)));
1104
1237
  }
1105
1238
  function roundCurrency(value) {
1106
- return toCents(value) / 100;
1239
+ return toCents2(value) / 100;
1107
1240
  }
1108
- function calculateDisplayLineTotal(baseUnitPrice, quantity, discountPercent, rules) {
1109
- const subtotalCents = toCents(baseUnitPrice) * quantity;
1110
- const afterPercentCents = discountPercent > 0 ? subtotalCents * (1 - discountPercent / 100) : subtotalCents;
1111
- return applyVolumeDiscountCents(afterPercentCents, quantity, rules) / 100;
1241
+ function calculateDisplayLineTotal(baseUnitPrice, quantity, discountPercent, rules, addonPrices = []) {
1242
+ return calculateCatalogLinePrice({
1243
+ unitPrice: baseUnitPrice,
1244
+ quantity,
1245
+ addonPrices,
1246
+ productDiscountPercent: discountPercent,
1247
+ volumeDiscounts: rules
1248
+ }).total;
1112
1249
  }
1113
- function applyVolumeDiscountCents(subtotalCents, quantity, rules) {
1114
- let matched = null;
1115
- for (const rule of rules) {
1116
- if (quantity >= rule.quantity) matched = rule;
1250
+ function parseGroupFeedback(value, context) {
1251
+ const feedback = requireUnknownRecord(value, context);
1252
+ const { total, average } = feedback;
1253
+ if (typeof total !== "number" || !Number.isInteger(total) || total < 0) {
1254
+ throw new Error(`Invalid Shoppex initial data: ${context}.total must be a non-negative integer.`);
1255
+ }
1256
+ if (total === 0 && average === null) return { ratingCount: 0, rating: null };
1257
+ if (total === 0 || typeof average !== "number" || average < 1 || average > 5) {
1258
+ throw new Error(`Invalid Shoppex initial data: ${context}.average must be null without reviews and 1-5 with them.`);
1117
1259
  }
1118
- if (!matched) return Math.max(0, Math.round(subtotalCents));
1119
- const discountCents = matched.type === "PERCENTAGE" ? subtotalCents * Math.min(Math.max(matched.value, 0), 100) / 100 : toCents(matched.value);
1120
- return Math.max(0, Math.round(subtotalCents - Math.min(discountCents, subtotalCents)));
1260
+ return { ratingCount: total, rating: average };
1121
1261
  }
1122
1262
  function deriveProductRating(value) {
1123
1263
  if (!isRecord(value)) return null;
@@ -1265,6 +1405,9 @@ function validateCheckoutFieldValue(field, value) {
1265
1405
  }
1266
1406
  const normalized = value.trim();
1267
1407
  if (field.required && !normalized) return `${field.name} is required.`;
1408
+ if (field.type === "select") {
1409
+ return normalized && !(field.options ?? []).includes(normalized) ? `${field.name} must be one of the listed options.` : null;
1410
+ }
1268
1411
  if (field.regex && normalized) {
1269
1412
  let pattern;
1270
1413
  try {
@@ -1520,21 +1663,11 @@ function isBlankMerchantHtml(html) {
1520
1663
  // src/catalog.ts
1521
1664
  function formatMoney(amount, currency) {
1522
1665
  const code = currency && currency !== "AUTO" ? currency : "USD";
1523
- let format;
1524
1666
  try {
1525
- format = new Intl.NumberFormat("en-US", { style: "currency", currency: code });
1667
+ return new Intl.NumberFormat("en-US", { style: "currency", currency: code }).format(amount);
1526
1668
  } catch {
1527
- const fixed = amount.toFixed(2);
1528
- return `${code} ${fixed.endsWith(".00") ? fixed.slice(0, -3) : fixed}`;
1529
- }
1530
- const decimals = format.resolvedOptions().maximumFractionDigits ?? 2;
1531
- if (decimals === 0 || !Number.isInteger(Number(amount.toFixed(decimals)))) return format.format(amount);
1532
- return new Intl.NumberFormat("en-US", {
1533
- style: "currency",
1534
- currency: code,
1535
- minimumFractionDigits: 0,
1536
- maximumFractionDigits: 0
1537
- }).format(Math.round(amount));
1669
+ return `${code} ${amount.toFixed(2)}`;
1670
+ }
1538
1671
  }
1539
1672
  function formatCompactCount(value) {
1540
1673
  if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
@@ -1765,10 +1898,10 @@ function buildProductDetail(product, settings, cards) {
1765
1898
  };
1766
1899
  }
1767
1900
  function calculateLinePricing(product, variant, addons, quantity, payWhatYouWantPrice) {
1768
- const addonsTotal = addons.reduce((sum, addon) => sum + addon.price, 0);
1901
+ const addonPrices = addons.map((addon) => addon.price);
1769
1902
  if (payWhatYouWantPrice !== void 0) {
1770
- const effectiveUnitPrice = payWhatYouWantPrice + addonsTotal;
1771
- const total2 = roundCurrency(effectiveUnitPrice * quantity);
1903
+ const total2 = calculateDisplayLineTotal(payWhatYouWantPrice, quantity, 0, [], addonPrices);
1904
+ const effectiveUnitPrice = payWhatYouWantPrice + addonPrices.reduce((sum, price) => sum + price, 0);
1772
1905
  return {
1773
1906
  total: total2,
1774
1907
  originalSubtotal: total2,
@@ -1777,14 +1910,15 @@ function calculateLinePricing(product, variant, addons, quantity, payWhatYouWant
1777
1910
  hasDiscount: false
1778
1911
  };
1779
1912
  }
1780
- const unitPrice = (variant?.baseUnitPrice ?? product.baseUnitPrice) + addonsTotal;
1913
+ const unitPrice = variant?.baseUnitPrice ?? product.baseUnitPrice;
1781
1914
  const total = calculateDisplayLineTotal(
1782
1915
  unitPrice,
1783
1916
  quantity,
1784
1917
  product.discountPercent,
1785
- product.volumeDiscounts
1918
+ product.volumeDiscounts,
1919
+ addonPrices
1786
1920
  );
1787
- const originalSubtotal = calculateDisplayLineTotal(unitPrice, quantity, 0, []);
1921
+ const originalSubtotal = calculateDisplayLineTotal(unitPrice, quantity, 0, [], addonPrices);
1788
1922
  return {
1789
1923
  total,
1790
1924
  originalSubtotal,
@@ -2037,6 +2171,7 @@ function CartProvider({ children }) {
2037
2171
  unsellableLines.current = [];
2038
2172
  }
2039
2173
  }, [rememberCartMaximum, rememberQuotedAffiliate, settleQuotedCart]);
2174
+ const requote = useCallback(() => refreshNow(), [refreshNow]);
2040
2175
  const scheduleRefresh = useCallback(() => new Promise((resolve) => {
2041
2176
  refreshResolvers.current.push(resolve);
2042
2177
  if (refreshTimer.current !== null) {
@@ -2097,6 +2232,9 @@ function CartProvider({ children }) {
2097
2232
  }
2098
2233
  const { min: minimum, max: maximum } = resolveQuantityBounds(product, variant);
2099
2234
  const addons = withRequiredAddons(product, options?.addons ?? []);
2235
+ if (addonChoiceMissing(product, addons)) {
2236
+ throw new Error(`Choose an add-on for ${product.name} to continue.`);
2237
+ }
2100
2238
  const payWhatYouWantPrice = options?.payWhatYouWantPrice ?? (product.payWhatYouWant ? resolvePayWhatYouWantMinimum(product, variant) : void 0);
2101
2239
  const addonsUnitPrice = addons.reduce((sum, addon) => sum + addon.price, 0);
2102
2240
  const unitPriceSnapshot = payWhatYouWantPrice !== void 0 ? payWhatYouWantPrice + addonsUnitPrice : calculateLinePricing(product, variant, addons, quantity).effectiveUnitPrice;
@@ -2132,7 +2270,7 @@ function CartProvider({ children }) {
2132
2270
  }, [mutate]);
2133
2271
  const addBundleMember = useCallback(async (item) => {
2134
2272
  const member = resolveBundleMember(item);
2135
- if (member === null || member.product.onHold || isSelectionSoldOut(member.product, member.variant)) {
2273
+ if (member === null || member.product.onHold || isSelectionSoldOut(member.product, member.variant) || addonChoiceMissing(member.product, withRequiredAddons(member.product, []))) {
2136
2274
  throw new Error(`${item.title} is not available and was not added to your cart.`);
2137
2275
  }
2138
2276
  const { bounds } = member;
@@ -2375,6 +2513,7 @@ function CartProvider({ children }) {
2375
2513
  count,
2376
2514
  quote,
2377
2515
  quoteError,
2516
+ requote,
2378
2517
  mutationError,
2379
2518
  couponRejected,
2380
2519
  quotedAmbientAffiliate,
@@ -2421,6 +2560,7 @@ function CartProvider({ children }) {
2421
2560
  quotedAmbientAffiliate,
2422
2561
  remove,
2423
2562
  removeCoupon,
2563
+ requote,
2424
2564
  selfReferralBlocked,
2425
2565
  update
2426
2566
  ]);
@@ -2435,6 +2575,10 @@ function withRequiredAddons(product, chosen) {
2435
2575
  const ids = new Set(chosen.map((addon) => addon.id));
2436
2576
  return [...chosen, ...product.addons.filter((addon) => addon.required && !ids.has(addon.id))];
2437
2577
  }
2578
+ function addonChoiceMissing(product, chosen) {
2579
+ if (!product.addonsRequired || product.addons.length === 0) return false;
2580
+ return !chosen.some((addon) => product.addons.some((offered) => offered.id === addon.id));
2581
+ }
2438
2582
  function startCartMutation(mutation) {
2439
2583
  void mutation.catch(() => void 0);
2440
2584
  }
@@ -2469,7 +2613,7 @@ function buildCartView(lines) {
2469
2613
  subtitle: lineSubtitle(line),
2470
2614
  imageUrl: line.image_url?.trim() || null,
2471
2615
  quantity,
2472
- lineTotal: roundCurrency((unitPrice ?? 0) * quantity),
2616
+ lineTotal: calculateDisplayLineTotal(unitPrice ?? 0, quantity, 0, []),
2473
2617
  canDecrease: quantity > minimumQuantity,
2474
2618
  canIncrease: quantity < maximumQuantity,
2475
2619
  minimumQuantity
@@ -2945,6 +3089,7 @@ export {
2945
3089
  accountRouteMessage,
2946
3090
  accountTabHref,
2947
3091
  accountTabLabelKey,
3092
+ addonChoiceMissing,
2948
3093
  averageRating,
2949
3094
  badgesForCard,
2950
3095
  badgesForProductPage,