@shoppexio/storefront-react 0.2.1 → 0.4.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/account.css +4 -8
  3. package/dist/account.js +2387 -942
  4. package/dist/account.js.map +1 -1
  5. package/dist/{chunk-ZEFZPUAW.js → chunk-NBOIS53R.js} +2 -1
  6. package/dist/chunk-NBOIS53R.js.map +1 -0
  7. package/dist/index.d.ts +243 -29
  8. package/dist/index.js +221 -77
  9. package/dist/index.js.map +1 -1
  10. package/eject/account/account-availability.ts +4 -1
  11. package/eject/account/account-downloads.ts +89 -1
  12. package/eject/account/account-referrals.ts +249 -7
  13. package/eject/account/account-reseller.ts +6 -140
  14. package/eject/account/dashboard.tsx +1 -0
  15. package/eject/account/downloads.tsx +73 -4
  16. package/eject/account/hosted-page.ts +45 -0
  17. package/eject/account/messages/ar.json +94 -31
  18. package/eject/account/messages/de.json +94 -31
  19. package/eject/account/messages/en.json +94 -31
  20. package/eject/account/messages/es.json +94 -31
  21. package/eject/account/messages/fi.json +94 -31
  22. package/eject/account/messages/fr.json +94 -31
  23. package/eject/account/messages/hi.json +94 -31
  24. package/eject/account/messages/it.json +94 -31
  25. package/eject/account/messages/lt.json +94 -31
  26. package/eject/account/messages/nl.json +94 -31
  27. package/eject/account/messages/pl.json +94 -31
  28. package/eject/account/messages/pt.json +94 -31
  29. package/eject/account/messages/ru.json +94 -31
  30. package/eject/account/messages/tr.json +94 -31
  31. package/eject/account/messages/uk.json +94 -31
  32. package/eject/account/messages/zh.json +94 -31
  33. package/eject/account/orders.tsx +141 -0
  34. package/eject/account/overview.tsx +6 -0
  35. package/eject/account/referrals.tsx +444 -156
  36. package/eject/account/reseller.tsx +59 -306
  37. package/eject/account/rewards.tsx +43 -6
  38. package/eject/account/settings.tsx +98 -2
  39. package/eject/account/shared.tsx +42 -3
  40. package/eject/account/subscriptions.tsx +75 -16
  41. package/package.json +7 -4
  42. package/dist/chunk-ZEFZPUAW.js.map +0 -1
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" };
@@ -456,6 +579,10 @@ function formatSubscriptionTrial(subscription) {
456
579
  const days = subscription.trialDays;
457
580
  return days > 0 ? `${days}-day free trial` : "";
458
581
  }
582
+ function subscriptionForVariant(subscription, variant) {
583
+ const own = variant?.billingInterval;
584
+ return own ? { ...subscription, intervalUnit: own.unit, intervalCount: own.count } : subscription;
585
+ }
459
586
  function parseSubscription(product, productType) {
460
587
  if (productType !== "SUBSCRIPTION") return null;
461
588
  const rawUnit = typeof product.recurring_interval === "string" ? product.recurring_interval.toUpperCase() : "";
@@ -467,6 +594,11 @@ function parseSubscription(product, productType) {
467
594
  trialDays: normalizePositiveDays(product.trial_period) ?? 0
468
595
  };
469
596
  }
597
+ function parseVariantBillingInterval(variant) {
598
+ const unit = SUBSCRIPTION_INTERVAL_UNITS.find((entry) => entry === variant.billing_interval);
599
+ const count = normalizePositiveDays(variant.billing_interval_count);
600
+ return unit && count ? { unit, count } : null;
601
+ }
470
602
  var PAYMENT_METHOD_KINDS = ["card", "wallet", "crypto"];
471
603
  function parsePaymentMethods(value) {
472
604
  if (!Array.isArray(value)) {
@@ -520,7 +652,8 @@ function getProductGroups() {
520
652
  categoryId: optionalString(group.category_id),
521
653
  productIds,
522
654
  productsCount: normalizeOptionalNonNegativeNumber(group.products_count) ?? productIds.length,
523
- sortPriority: normalizeOptionalNonNegativeNumber(group.sort_priority) ?? 0
655
+ sortPriority: normalizeOptionalNonNegativeNumber(group.sort_priority) ?? 0,
656
+ ...parseGroupFeedback(group.feedback, `${context}.feedback`)
524
657
  };
525
658
  });
526
659
  }
@@ -585,6 +718,7 @@ function getStorefrontShop(productCount) {
585
718
  const termsHtml = store.tos_enabled === true ? optionalString(store.terms_of_service) : null;
586
719
  return {
587
720
  name: requireString(store, "name", "store"),
721
+ description: optionalString(store.description),
588
722
  logoUrl: firstOptionalString(store, ["logo", "avatar"]),
589
723
  avatarUrl: firstOptionalString(store, ["avatar", "logo"]),
590
724
  socialLinks: SOCIAL_NETWORKS.flatMap((network) => {
@@ -745,7 +879,8 @@ function parseProduct(value, index) {
745
879
  const id = requireString(product, "uniqid", context);
746
880
  const productId = optionalString(product.id) ?? id;
747
881
  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;
882
+ const applyDiscount = (amount) => discountPercent > 0 ? calculateDisplayLineTotal(amount, 1, discountPercent, []) : amount;
883
+ const images = parseProductImages(product.images);
749
884
  const rawVariants = parsePriceVariants(product.price_variants, context);
750
885
  const priceVariants = rawVariants.map((variant) => ({
751
886
  ...variant,
@@ -768,17 +903,14 @@ function parseProduct(value, index) {
768
903
  description: optionalString(product.description),
769
904
  slug: optionalString(product.slug),
770
905
  imageUrl: resolveProductImage(product),
771
- imageUrls: parseProductImageUrls(product.images),
772
- stock: applyOrderable(
773
- normalizeStock(product.stock, `${context}.stock`),
774
- product.orderable,
775
- product.supplier_backed
776
- ),
906
+ imageUrls: images.map((image) => image.url),
907
+ images,
908
+ stock: normalizeStock(product.stock, `${context}.stock`),
777
909
  lowStockThreshold: normalizeLowStockThreshold(
778
910
  normalizeOptionalNonNegativeNumber(product.quantity_warning)
779
911
  ),
780
912
  hideStockCounter: typeof product.hide_stock_counter === "boolean" ? product.hide_stock_counter : null,
781
- volumeDiscounts: parseVolumeDiscounts(product.volume_discounts),
913
+ volumeDiscounts: parseVolumeDiscountRules(product.volume_discounts),
782
914
  payWhatYouWant: product.pay_what_you_want === true,
783
915
  discountPercent,
784
916
  baseUnitPrice: undiscountedPrice,
@@ -796,6 +928,8 @@ function parseProduct(value, index) {
796
928
  automaticDeals: parseAutomaticDeals(product.automatic_deals),
797
929
  videoEmbedSrc: resolveProductVideoEmbedSrc(product.video_link),
798
930
  addons: parseAddons(product.addons),
931
+ addonsRequired: optionalBoolean(product.addons_required, false, `${context}.addons_required`),
932
+ upsellProductIds: parseUpsellProductIds(product.upsell_product_ids, context),
799
933
  customFields: parseCustomFields(product.custom_fields, context),
800
934
  // Strict on purpose: the serializer always emits `on_hold`, so an absent
801
935
  // flag is the only tolerated shape (false) and a present non-boolean is a
@@ -808,8 +942,12 @@ function parseProduct(value, index) {
808
942
  ratingTally: rating?.tally ?? null,
809
943
  shortDescription: optionalString(product.short_description),
810
944
  deliverable: parseDeliverable(product.type),
945
+ deliveryType: parseDeliveryType(product.delivery_type),
811
946
  unitLabelSingular: optionalString(product.label_singular),
812
947
  unitLabelPlural: optionalString(product.label_plural),
948
+ deliveryFormat: optionalString(product.delivery_format),
949
+ region: optionalString(product.region),
950
+ restockedAt: optionalString(product.restocked_at),
813
951
  serviceStatus: parseServiceStatus(product.service_status, context),
814
952
  highlights: parseHighlights(product.product_highlights),
815
953
  metadata: parseMetadata(product.metadata),
@@ -886,13 +1024,10 @@ function parsePriceVariants(value, productContext) {
886
1024
  label: requireStringFromKeys(variant, ["label", "title"], context),
887
1025
  description: optionalString(variant.description),
888
1026
  license: parseVariantLicense(variant),
1027
+ billingInterval: parseVariantBillingInterval(variant),
889
1028
  price: unitPrice,
890
1029
  baseUnitPrice: unitPrice,
891
- stock: applyOrderable(
892
- normalizeStock(variant.stock, `${context}.stock`),
893
- variant.orderable,
894
- variant.supplier_backed
895
- ),
1030
+ stock: normalizeStock(variant.stock, `${context}.stock`),
896
1031
  quantityMin,
897
1032
  quantityMax: normalizeMaximum(variant.quantity_max, quantityMin ?? 1, `${context}.quantity_max`),
898
1033
  imageUrl: optionalString(variant.image_url),
@@ -962,20 +1097,34 @@ function parseAddons(value) {
962
1097
  addons.push({
963
1098
  id,
964
1099
  title,
1100
+ description: optionalString(entry.description),
965
1101
  price: Number.isFinite(price) && price > 0 ? price : 0,
966
1102
  required: entry.required === true
967
1103
  });
968
1104
  }
969
1105
  return addons;
970
1106
  }
971
- function parseProductImageUrls(value) {
1107
+ function parseUpsellProductIds(value, context) {
1108
+ if (value === void 0) return [];
1109
+ if (!Array.isArray(value) || !value.every((id) => typeof id === "string")) {
1110
+ throw new Error(`Invalid Shoppex ${context}.upsell_product_ids: expected an array of product uniqids.`);
1111
+ }
1112
+ return value;
1113
+ }
1114
+ function parseProductImages(value) {
972
1115
  if (!Array.isArray(value)) return [];
973
- const urls = [];
1116
+ const images = [];
974
1117
  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);
1118
+ const record = isRecord(entry) ? entry : null;
1119
+ const url = record ? optionalString(record.url) : typeof entry === "string" ? optionalString(entry) : null;
1120
+ if (!url || images.some((image) => image.url === url)) continue;
1121
+ images.push({
1122
+ url,
1123
+ kind: record && (record.kind === "art" || record.kind === "logo") ? record.kind : null,
1124
+ inkColor: record && typeof record.ink_color === "string" && /^#[0-9a-f]{6}$/.test(record.ink_color) ? record.ink_color : null
1125
+ });
977
1126
  }
978
- return urls;
1127
+ return images;
979
1128
  }
980
1129
  function parseMetadata(value) {
981
1130
  if (!isRecord(value)) return {};
@@ -1012,6 +1161,7 @@ function parseCustomFields(value, context) {
1012
1161
  const rawType = typeof entry.type === "string" ? entry.type.trim().toLowerCase() : "";
1013
1162
  const type = rawType || "text";
1014
1163
  if (type === "hidden") continue;
1164
+ if ([entry.collect_after_payment, entry.collectAfterPayment].some((flag) => flag === true || flag === 1 || flag === "1" || flag === "true")) continue;
1015
1165
  const defaultValue = firstOptionalString(entry, ["default_value", "default", "value"]);
1016
1166
  fields.push({
1017
1167
  name,
@@ -1023,7 +1173,8 @@ function parseCustomFields(value, context) {
1023
1173
  required: entry.required === true || entry.required === 1 || entry.required === "1" || entry.required === "true",
1024
1174
  defaultValue: defaultValue ?? "",
1025
1175
  placeholder: typeof entry.placeholder === "string" ? entry.placeholder : "",
1026
- regex: optionalString(entry.regex)
1176
+ regex: optionalString(entry.regex),
1177
+ ...type === "select" ? { options: Array.isArray(entry.options) ? entry.options.filter((option) => typeof option === "string") : [] } : {}
1027
1178
  });
1028
1179
  }
1029
1180
  return fields;
@@ -1083,41 +1234,32 @@ function parseFaqs(value) {
1083
1234
  }
1084
1235
  return faqs;
1085
1236
  }
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) {
1237
+ function toCents2(value) {
1103
1238
  return Math.round(Number((value * 100).toPrecision(15)));
1104
1239
  }
1105
1240
  function roundCurrency(value) {
1106
- return toCents(value) / 100;
1241
+ return toCents2(value) / 100;
1107
1242
  }
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;
1243
+ function calculateDisplayLineTotal(baseUnitPrice, quantity, discountPercent, rules, addonPrices = []) {
1244
+ return calculateCatalogLinePrice({
1245
+ unitPrice: baseUnitPrice,
1246
+ quantity,
1247
+ addonPrices,
1248
+ productDiscountPercent: discountPercent,
1249
+ volumeDiscounts: rules
1250
+ }).total;
1112
1251
  }
1113
- function applyVolumeDiscountCents(subtotalCents, quantity, rules) {
1114
- let matched = null;
1115
- for (const rule of rules) {
1116
- if (quantity >= rule.quantity) matched = rule;
1252
+ function parseGroupFeedback(value, context) {
1253
+ const feedback = requireUnknownRecord(value, context);
1254
+ const { total, average } = feedback;
1255
+ if (typeof total !== "number" || !Number.isInteger(total) || total < 0) {
1256
+ throw new Error(`Invalid Shoppex initial data: ${context}.total must be a non-negative integer.`);
1117
1257
  }
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)));
1258
+ if (total === 0 && average === null) return { ratingCount: 0, rating: null };
1259
+ if (total === 0 || typeof average !== "number" || average < 1 || average > 5) {
1260
+ throw new Error(`Invalid Shoppex initial data: ${context}.average must be null without reviews and 1-5 with them.`);
1261
+ }
1262
+ return { ratingCount: total, rating: average };
1121
1263
  }
1122
1264
  function deriveProductRating(value) {
1123
1265
  if (!isRecord(value)) return null;
@@ -1154,11 +1296,6 @@ function normalizeStock(value, context) {
1154
1296
  const stock = parseFiniteNumber(value, context);
1155
1297
  return stock < 0 ? null : Math.floor(stock);
1156
1298
  }
1157
- function applyOrderable(stock, orderable, supplierBacked) {
1158
- if (orderable !== true) return stock;
1159
- if (supplierBacked === true) return null;
1160
- return stock === 0 ? null : stock;
1161
- }
1162
1299
  function normalizeOptionalNonNegativeNumber(value) {
1163
1300
  if (value === null || value === void 0 || value === "") return null;
1164
1301
  const parsed = Number(value);
@@ -1265,6 +1402,9 @@ function validateCheckoutFieldValue(field, value) {
1265
1402
  }
1266
1403
  const normalized = value.trim();
1267
1404
  if (field.required && !normalized) return `${field.name} is required.`;
1405
+ if (field.type === "select") {
1406
+ return normalized && !(field.options ?? []).includes(normalized) ? `${field.name} must be one of the listed options.` : null;
1407
+ }
1268
1408
  if (field.regex && normalized) {
1269
1409
  let pattern;
1270
1410
  try {
@@ -1520,21 +1660,11 @@ function isBlankMerchantHtml(html) {
1520
1660
  // src/catalog.ts
1521
1661
  function formatMoney(amount, currency) {
1522
1662
  const code = currency && currency !== "AUTO" ? currency : "USD";
1523
- let format;
1524
1663
  try {
1525
- format = new Intl.NumberFormat("en-US", { style: "currency", currency: code });
1664
+ return new Intl.NumberFormat("en-US", { style: "currency", currency: code }).format(amount);
1526
1665
  } 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));
1666
+ return `${code} ${amount.toFixed(2)}`;
1667
+ }
1538
1668
  }
1539
1669
  function formatCompactCount(value) {
1540
1670
  if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
@@ -1765,10 +1895,10 @@ function buildProductDetail(product, settings, cards) {
1765
1895
  };
1766
1896
  }
1767
1897
  function calculateLinePricing(product, variant, addons, quantity, payWhatYouWantPrice) {
1768
- const addonsTotal = addons.reduce((sum, addon) => sum + addon.price, 0);
1898
+ const addonPrices = addons.map((addon) => addon.price);
1769
1899
  if (payWhatYouWantPrice !== void 0) {
1770
- const effectiveUnitPrice = payWhatYouWantPrice + addonsTotal;
1771
- const total2 = roundCurrency(effectiveUnitPrice * quantity);
1900
+ const total2 = calculateDisplayLineTotal(payWhatYouWantPrice, quantity, 0, [], addonPrices);
1901
+ const effectiveUnitPrice = payWhatYouWantPrice + addonPrices.reduce((sum, price) => sum + price, 0);
1772
1902
  return {
1773
1903
  total: total2,
1774
1904
  originalSubtotal: total2,
@@ -1777,14 +1907,15 @@ function calculateLinePricing(product, variant, addons, quantity, payWhatYouWant
1777
1907
  hasDiscount: false
1778
1908
  };
1779
1909
  }
1780
- const unitPrice = (variant?.baseUnitPrice ?? product.baseUnitPrice) + addonsTotal;
1910
+ const unitPrice = variant?.baseUnitPrice ?? product.baseUnitPrice;
1781
1911
  const total = calculateDisplayLineTotal(
1782
1912
  unitPrice,
1783
1913
  quantity,
1784
1914
  product.discountPercent,
1785
- product.volumeDiscounts
1915
+ product.volumeDiscounts,
1916
+ addonPrices
1786
1917
  );
1787
- const originalSubtotal = calculateDisplayLineTotal(unitPrice, quantity, 0, []);
1918
+ const originalSubtotal = calculateDisplayLineTotal(unitPrice, quantity, 0, [], addonPrices);
1788
1919
  return {
1789
1920
  total,
1790
1921
  originalSubtotal,
@@ -2037,6 +2168,7 @@ function CartProvider({ children }) {
2037
2168
  unsellableLines.current = [];
2038
2169
  }
2039
2170
  }, [rememberCartMaximum, rememberQuotedAffiliate, settleQuotedCart]);
2171
+ const requote = useCallback(() => refreshNow(), [refreshNow]);
2040
2172
  const scheduleRefresh = useCallback(() => new Promise((resolve) => {
2041
2173
  refreshResolvers.current.push(resolve);
2042
2174
  if (refreshTimer.current !== null) {
@@ -2097,6 +2229,9 @@ function CartProvider({ children }) {
2097
2229
  }
2098
2230
  const { min: minimum, max: maximum } = resolveQuantityBounds(product, variant);
2099
2231
  const addons = withRequiredAddons(product, options?.addons ?? []);
2232
+ if (addonChoiceMissing(product, addons)) {
2233
+ throw new Error(`Choose an add-on for ${product.name} to continue.`);
2234
+ }
2100
2235
  const payWhatYouWantPrice = options?.payWhatYouWantPrice ?? (product.payWhatYouWant ? resolvePayWhatYouWantMinimum(product, variant) : void 0);
2101
2236
  const addonsUnitPrice = addons.reduce((sum, addon) => sum + addon.price, 0);
2102
2237
  const unitPriceSnapshot = payWhatYouWantPrice !== void 0 ? payWhatYouWantPrice + addonsUnitPrice : calculateLinePricing(product, variant, addons, quantity).effectiveUnitPrice;
@@ -2132,7 +2267,7 @@ function CartProvider({ children }) {
2132
2267
  }, [mutate]);
2133
2268
  const addBundleMember = useCallback(async (item) => {
2134
2269
  const member = resolveBundleMember(item);
2135
- if (member === null || member.product.onHold || isSelectionSoldOut(member.product, member.variant)) {
2270
+ if (member === null || member.product.onHold || isSelectionSoldOut(member.product, member.variant) || addonChoiceMissing(member.product, withRequiredAddons(member.product, []))) {
2136
2271
  throw new Error(`${item.title} is not available and was not added to your cart.`);
2137
2272
  }
2138
2273
  const { bounds } = member;
@@ -2375,6 +2510,7 @@ function CartProvider({ children }) {
2375
2510
  count,
2376
2511
  quote,
2377
2512
  quoteError,
2513
+ requote,
2378
2514
  mutationError,
2379
2515
  couponRejected,
2380
2516
  quotedAmbientAffiliate,
@@ -2421,6 +2557,7 @@ function CartProvider({ children }) {
2421
2557
  quotedAmbientAffiliate,
2422
2558
  remove,
2423
2559
  removeCoupon,
2560
+ requote,
2424
2561
  selfReferralBlocked,
2425
2562
  update
2426
2563
  ]);
@@ -2435,6 +2572,10 @@ function withRequiredAddons(product, chosen) {
2435
2572
  const ids = new Set(chosen.map((addon) => addon.id));
2436
2573
  return [...chosen, ...product.addons.filter((addon) => addon.required && !ids.has(addon.id))];
2437
2574
  }
2575
+ function addonChoiceMissing(product, chosen) {
2576
+ if (!product.addonsRequired || product.addons.length === 0) return false;
2577
+ return !chosen.some((addon) => product.addons.some((offered) => offered.id === addon.id));
2578
+ }
2438
2579
  function startCartMutation(mutation) {
2439
2580
  void mutation.catch(() => void 0);
2440
2581
  }
@@ -2469,7 +2610,7 @@ function buildCartView(lines) {
2469
2610
  subtitle: lineSubtitle(line),
2470
2611
  imageUrl: line.image_url?.trim() || null,
2471
2612
  quantity,
2472
- lineTotal: roundCurrency((unitPrice ?? 0) * quantity),
2613
+ lineTotal: calculateDisplayLineTotal(unitPrice ?? 0, quantity, 0, []),
2473
2614
  canDecrease: quantity > minimumQuantity,
2474
2615
  canIncrease: quantity < maximumQuantity,
2475
2616
  minimumQuantity
@@ -2677,7 +2818,8 @@ function toShopReviews(rows, labels) {
2677
2818
  timestamp,
2678
2819
  productTitle: row.product_title?.trim() || null,
2679
2820
  verified: row.verified === true,
2680
- automated: row.is_automated === true
2821
+ automated: row.is_automated === true,
2822
+ reply: row.reply?.trim() || null
2681
2823
  };
2682
2824
  }).sort((a, b) => b.timestamp - a.timestamp);
2683
2825
  }
@@ -2945,6 +3087,7 @@ export {
2945
3087
  accountRouteMessage,
2946
3088
  accountTabHref,
2947
3089
  accountTabLabelKey,
3090
+ addonChoiceMissing,
2948
3091
  averageRating,
2949
3092
  badgesForCard,
2950
3093
  badgesForProductPage,
@@ -3045,6 +3188,7 @@ export {
3045
3188
  startCartMutation,
3046
3189
  stripMerchantHtmlImages,
3047
3190
  submitContactTicket,
3191
+ subscriptionForVariant,
3048
3192
  toShopReviews,
3049
3193
  toStarCount,
3050
3194
  useCart,