@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/CHANGELOG.md +97 -0
- package/dist/account.js +1161 -195
- package/dist/account.js.map +1 -1
- package/dist/{chunk-ZEFZPUAW.js → chunk-NBOIS53R.js} +2 -1
- package/dist/chunk-NBOIS53R.js.map +1 -0
- package/dist/index.d.ts +151 -21
- package/dist/index.js +206 -61
- package/dist/index.js.map +1 -1
- package/eject/account/account-availability.ts +4 -1
- package/eject/account/account-referrals.ts +179 -6
- package/eject/account/dashboard.tsx +1 -0
- package/eject/account/messages/ar.json +40 -3
- package/eject/account/messages/de.json +40 -3
- package/eject/account/messages/en.json +40 -3
- package/eject/account/messages/es.json +40 -3
- package/eject/account/messages/fi.json +40 -3
- package/eject/account/messages/fr.json +40 -3
- package/eject/account/messages/hi.json +40 -3
- package/eject/account/messages/it.json +40 -3
- package/eject/account/messages/lt.json +40 -3
- package/eject/account/messages/nl.json +40 -3
- package/eject/account/messages/pl.json +40 -3
- package/eject/account/messages/pt.json +40 -3
- package/eject/account/messages/ru.json +40 -3
- package/eject/account/messages/tr.json +40 -3
- package/eject/account/messages/uk.json +40 -3
- package/eject/account/messages/zh.json +40 -3
- package/eject/account/orders.tsx +53 -0
- package/eject/account/referrals.tsx +399 -153
- package/eject/account/rewards.tsx +38 -6
- package/eject/account/settings.tsx +98 -2
- package/eject/account/shared.tsx +3 -2
- package/package.json +4 -1
- 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-
|
|
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 ?
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
1114
|
+
const images = [];
|
|
974
1115
|
for (const entry of value) {
|
|
975
|
-
const
|
|
976
|
-
|
|
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
|
|
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
|
|
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
|
|
1239
|
+
return toCents2(value) / 100;
|
|
1107
1240
|
}
|
|
1108
|
-
function calculateDisplayLineTotal(baseUnitPrice, quantity, discountPercent, rules) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1667
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency: code }).format(amount);
|
|
1526
1668
|
} catch {
|
|
1527
|
-
|
|
1528
|
-
|
|
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
|
|
1901
|
+
const addonPrices = addons.map((addon) => addon.price);
|
|
1769
1902
|
if (payWhatYouWantPrice !== void 0) {
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
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 =
|
|
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:
|
|
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,
|