@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.
- package/CHANGELOG.md +120 -0
- package/dist/account.css +4 -8
- package/dist/account.js +2387 -942
- 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 +243 -29
- package/dist/index.js +221 -77
- package/dist/index.js.map +1 -1
- package/eject/account/account-availability.ts +4 -1
- package/eject/account/account-downloads.ts +89 -1
- package/eject/account/account-referrals.ts +249 -7
- package/eject/account/account-reseller.ts +6 -140
- package/eject/account/dashboard.tsx +1 -0
- package/eject/account/downloads.tsx +73 -4
- package/eject/account/hosted-page.ts +45 -0
- package/eject/account/messages/ar.json +94 -31
- package/eject/account/messages/de.json +94 -31
- package/eject/account/messages/en.json +94 -31
- package/eject/account/messages/es.json +94 -31
- package/eject/account/messages/fi.json +94 -31
- package/eject/account/messages/fr.json +94 -31
- package/eject/account/messages/hi.json +94 -31
- package/eject/account/messages/it.json +94 -31
- package/eject/account/messages/lt.json +94 -31
- package/eject/account/messages/nl.json +94 -31
- package/eject/account/messages/pl.json +94 -31
- package/eject/account/messages/pt.json +94 -31
- package/eject/account/messages/ru.json +94 -31
- package/eject/account/messages/tr.json +94 -31
- package/eject/account/messages/uk.json +94 -31
- package/eject/account/messages/zh.json +94 -31
- package/eject/account/orders.tsx +141 -0
- package/eject/account/overview.tsx +6 -0
- package/eject/account/referrals.tsx +444 -156
- package/eject/account/reseller.tsx +59 -306
- package/eject/account/rewards.tsx +43 -6
- package/eject/account/settings.tsx +98 -2
- package/eject/account/shared.tsx +42 -3
- package/eject/account/subscriptions.tsx +75 -16
- package/package.json +7 -4
- 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" };
|
|
@@ -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 ?
|
|
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:
|
|
772
|
-
|
|
773
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
1116
|
+
const images = [];
|
|
974
1117
|
for (const entry of value) {
|
|
975
|
-
const
|
|
976
|
-
|
|
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
|
|
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
|
|
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
|
|
1241
|
+
return toCents2(value) / 100;
|
|
1107
1242
|
}
|
|
1108
|
-
function calculateDisplayLineTotal(baseUnitPrice, quantity, discountPercent, rules) {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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 (
|
|
1119
|
-
|
|
1120
|
-
|
|
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
|
-
|
|
1664
|
+
return new Intl.NumberFormat("en-US", { style: "currency", currency: code }).format(amount);
|
|
1526
1665
|
} 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));
|
|
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
|
|
1898
|
+
const addonPrices = addons.map((addon) => addon.price);
|
|
1769
1899
|
if (payWhatYouWantPrice !== void 0) {
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
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 =
|
|
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:
|
|
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,
|