@shoppexio/storefront 1.0.103 → 1.0.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -48,6 +48,7 @@ __export(src_exports, {
48
48
  createTicket: () => createTicket,
49
49
  dashboard: () => dashboard,
50
50
  default: () => src_default,
51
+ discardTicketImage: () => discardTicketImage,
51
52
  emailPreferences: () => emailPreferences,
52
53
  enrollAsReseller: () => enrollAsReseller,
53
54
  ensureCartLineId: () => ensureCartLineId,
@@ -79,6 +80,7 @@ __export(src_exports, {
79
80
  normalizeStorefrontCustomFields: () => normalizeStorefrontCustomFields,
80
81
  notificationPreferences: () => notificationPreferences,
81
82
  order: () => order,
83
+ orderReview: () => orderReview,
82
84
  orders: () => orders,
83
85
  pauseSubscription: () => pauseSubscription,
84
86
  productMatchesSearchQuery: () => productMatchesSearchQuery,
@@ -115,11 +117,13 @@ __export(src_exports, {
115
117
  touchStorefrontPresence: () => touchStorefrontPresence,
116
118
  trackCartActivity: () => trackCartActivity,
117
119
  trackPageView: () => trackPageView,
120
+ trackProductView: () => trackProductView,
118
121
  undoSubscriptionCancel: () => undoSubscriptionCancel,
119
122
  updateAvatar: () => updateAvatar,
120
123
  updateEmailPreferences: () => updateEmailPreferences,
121
124
  updateNotificationPreferences: () => updateNotificationPreferences,
122
125
  updateProfile: () => updateProfile,
126
+ uploadTicketImage: () => uploadTicketImage,
123
127
  validateStorefrontCustomFieldValue: () => validateStorefrontCustomFieldValue,
124
128
  verifyOtp: () => verifyOtp,
125
129
  warranties: () => warranties
@@ -962,1101 +966,1141 @@ async function captureAffiliateFromUrl(param = "ref") {
962
966
  return holdsOtherCode ? held.code : code;
963
967
  }
964
968
 
965
- // ../sdk/src/utils/cart-line-id.ts
966
- function hashString(value) {
967
- let hash2 = 2166136261;
968
- for (let i = 0; i < value.length; i += 1) {
969
- hash2 ^= value.charCodeAt(i);
970
- hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
969
+ // ../contracts/src/storefront-consent-choice.ts
970
+ var COOKIE_CONSENT_VERSION = 1;
971
+ var COOKIE_CONSENT_COOKIE = "shoppex_consent";
972
+ var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
973
+ var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
974
+ function parseCookieConsentValue(value) {
975
+ const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
976
+ return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
977
+ }
978
+
979
+ // ../sdk/src/core/analytics-session.ts
980
+ var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
981
+ var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
982
+ var ANALYTICS_VISITS_MAX = 20;
983
+ var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
984
+ var UTM_STORAGE_PREFIX = "shoppex:utm:";
985
+ var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
986
+ var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
987
+ function pageLanding() {
988
+ let r = "";
989
+ try {
990
+ r = document.referrer ? new URL(document.referrer).hostname : "";
991
+ } catch {
971
992
  }
972
- return (hash2 >>> 0).toString(16);
993
+ const params = new URLSearchParams(window.location?.search ?? "");
994
+ const u = ["source", "medium", "campaign"].map((key) => params.get(`utm_${key}`)?.trim().toLowerCase() ?? "").join("|");
995
+ return { r: r === window.location?.hostname ? "" : r, u: u === "||" ? "" : u };
973
996
  }
974
- function normalizeAddons(addons) {
975
- if (!addons?.length) return [];
976
- return [...addons].map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 })).sort((a, b) => a.id.localeCompare(b.id));
997
+ function readAnalyticsConsent() {
998
+ if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
999
+ const privacy = navigator;
1000
+ if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
1001
+ const prefix = `${COOKIE_CONSENT_COOKIE}=`;
1002
+ let raw;
1003
+ try {
1004
+ raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1005
+ } catch {
1006
+ return "unknown";
1007
+ }
1008
+ const choice = parseCookieConsentValue(raw ?? null);
1009
+ if (!choice) return "unknown";
1010
+ return choice.analytics ? "granted" : "declined";
977
1011
  }
978
- function normalizeCustomFields(fields) {
979
- if (!fields) return {};
980
- const entries = Object.entries(fields).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b));
981
- return Object.fromEntries(entries);
1012
+ function readVisits(storeSlug, now) {
1013
+ const stored = getItem(analyticsVisitsKey(storeSlug));
1014
+ if (!Array.isArray(stored)) return [];
1015
+ return stored.filter((visit2) => typeof visit2?.id === "string" && typeof visit2.at === "number" && now - visit2.at < ANALYTICS_VISITS_WINDOW_MS);
982
1016
  }
983
- function computeCartLineId(input2) {
984
- const payload = {
985
- product_id: input2.product_id,
986
- variant_id: input2.variant_id,
987
- price_variant_id: input2.price_variant_id ?? null,
988
- addons: normalizeAddons(input2.addons),
989
- custom_fields: normalizeCustomFields(input2.custom_fields),
990
- unit_price: typeof input2.price_data?.unit_price === "number" && Number.isFinite(input2.price_data.unit_price) ? input2.price_data.unit_price : null,
991
- pay_what_you_want_price: typeof input2.pay_what_you_want_price === "number" && Number.isFinite(input2.pay_what_you_want_price) ? input2.pay_what_you_want_price : null
992
- };
993
- return hashString(JSON.stringify(payload));
1017
+ function forgetAnalyticsData(storeSlug) {
1018
+ removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
1019
+ removeItem(analyticsVisitsKey(storeSlug));
994
1020
  }
995
- function ensureCartLineId(item) {
996
- if (typeof item.line_id === "string" && item.line_id.trim()) {
997
- return item;
1021
+ function analyticsSessionId(storeSlug, options, now = Date.now()) {
1022
+ removeItem(`presence_connection_${storeSlug}`);
1023
+ if (readAnalyticsConsent() !== "granted") {
1024
+ forgetAnalyticsData(storeSlug);
1025
+ return null;
998
1026
  }
999
- return { ...item, line_id: computeCartLineId(item) };
1027
+ const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
1028
+ const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
1029
+ if (!options.touch) return live;
1030
+ const landing = pageLanding();
1031
+ const newLanding = landing.r && landing.r !== stored?.r || landing.u && landing.u !== stored?.u;
1032
+ if (live && !newLanding) {
1033
+ setItem(analyticsSessionKey(storeSlug), { ...stored, id: live, at: now }, "sessionStorage");
1034
+ return live;
1035
+ }
1036
+ if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
1037
+ if (landing.r) {
1038
+ try {
1039
+ globalThis.localStorage.removeItem(storefrontStorageKey(`${UTM_STORAGE_PREFIX}${storeSlug}`));
1040
+ } catch {
1041
+ }
1042
+ }
1043
+ const id = crypto.randomUUID();
1044
+ setItem(analyticsSessionKey(storeSlug), { id, at: now, ...landing }, "sessionStorage");
1045
+ setItem(
1046
+ analyticsVisitsKey(storeSlug),
1047
+ [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
1048
+ );
1049
+ return id;
1000
1050
  }
1001
-
1002
- // ../contracts/src/catalog-unit-price.ts
1003
- var CATALOG_UNIT_PRICE_DECIMAL_PLACES = 3;
1004
- var PAYABLE_AMOUNT_DECIMAL_PLACES = 2;
1005
- var CATALOG_UNIT_PRICE_FORMAT_OPTIONS = {
1006
- maximumFractionDigits: CATALOG_UNIT_PRICE_DECIMAL_PLACES
1007
- };
1008
- function roundPayableAmount(value) {
1009
- if (!Number.isFinite(value)) return value;
1010
- const sign = value < 0 ? -1 : 1;
1011
- const shifted = shiftDecimal(Math.abs(value), PAYABLE_AMOUNT_DECIMAL_PLACES);
1012
- const rounded = Math.round(shifted);
1013
- if (rounded === 0) return 0;
1014
- return sign * shiftDecimal(rounded, -PAYABLE_AMOUNT_DECIMAL_PLACES);
1051
+ function checkoutAttribution(storeSlug, now = Date.now()) {
1052
+ const consent = readAnalyticsConsent();
1053
+ if (consent === "declined") {
1054
+ forgetAnalyticsData(storeSlug);
1055
+ return { declined: true };
1056
+ }
1057
+ if (consent === "unknown") return void 0;
1058
+ const sessionIds = readVisits(storeSlug, now).map((visit2) => visit2.id);
1059
+ return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
1015
1060
  }
1016
- function shiftDecimal(value, places) {
1017
- const [coefficient, currentExponent = "0"] = String(value).split("e");
1018
- return Number(`${coefficient}e${Number(currentExponent) + places}`);
1061
+ var engagedMs = 0;
1062
+ var visibleSince = null;
1063
+ var clockInstalled = false;
1064
+ var pageViewed = false;
1065
+ var pageSession = null;
1066
+ function markPageViewed(sessionId) {
1067
+ pageViewed = true;
1068
+ pageSession = sessionId;
1019
1069
  }
1020
-
1021
- // ../sdk/src/modules/cart.ts
1022
- var STORAGE_KEYS = {
1023
- cart: "cart",
1024
- cartBackup: "cart_backup",
1025
- meta: "cart_meta",
1026
- metaBackup: "cart_backup_meta",
1027
- coupon: "cart_coupon",
1028
- couponBackup: "cart_coupon_backup"
1029
- };
1030
- function getStorageKey(type) {
1031
- return `${STORAGE_KEYS[type]}_${getConfig().storeSlug}`;
1070
+ function currentPageSession() {
1071
+ return pageSession;
1032
1072
  }
1033
- function cartStorageArea() {
1034
- return getConfig().preview ? "sessionStorage" : "localStorage";
1073
+ function isPageViewed() {
1074
+ return pageViewed;
1035
1075
  }
1036
- function readCartStorage(type) {
1037
- return getItem(getStorageKey(type), cartStorageArea());
1076
+ function settleClock(now) {
1077
+ if (visibleSince !== null) {
1078
+ engagedMs += now - visibleSince;
1079
+ visibleSince = document.visibilityState === "visible" ? now : null;
1080
+ }
1038
1081
  }
1039
- function writeCartStorage(type, value) {
1040
- setItem(getStorageKey(type), value, cartStorageArea());
1082
+ var consentListenerInstalled = false;
1083
+ function onAnalyticsConsent(onYes) {
1084
+ if (consentListenerInstalled || typeof window === "undefined") return;
1085
+ consentListenerInstalled = true;
1086
+ window.addEventListener("shoppex:consent", () => {
1087
+ if (readAnalyticsConsent() === "granted") onYes();
1088
+ });
1041
1089
  }
1042
- function removeCartStorage(type) {
1043
- removeItem(getStorageKey(type), cartStorageArea());
1090
+ function installEngagedClock(onHidden) {
1091
+ if (clockInstalled || typeof document === "undefined") return;
1092
+ clockInstalled = true;
1093
+ visibleSince = document.visibilityState === "visible" ? Date.now() : null;
1094
+ document.addEventListener("visibilitychange", () => {
1095
+ const now = Date.now();
1096
+ if (document.visibilityState === "visible") {
1097
+ visibleSince ?? (visibleSince = now);
1098
+ return;
1099
+ }
1100
+ settleClock(now);
1101
+ visibleSince = null;
1102
+ onHidden();
1103
+ });
1104
+ window.addEventListener("pagehide", () => {
1105
+ settleClock(Date.now());
1106
+ visibleSince = null;
1107
+ onHidden();
1108
+ });
1109
+ window.addEventListener("pageshow", (event) => {
1110
+ if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
1111
+ });
1044
1112
  }
1045
- function hashString2(value) {
1046
- let hash2 = 2166136261;
1047
- for (let i = 0; i < value.length; i += 1) {
1048
- hash2 ^= value.charCodeAt(i);
1049
- hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
1113
+ function takeEngagedMs(now = Date.now()) {
1114
+ settleClock(now);
1115
+ const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
1116
+ engagedMs = 0;
1117
+ return taken;
1118
+ }
1119
+
1120
+ // ../sdk/src/core/attribution.ts
1121
+ var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1122
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
1123
+ var MAX_VALUE_LENGTH = 255;
1124
+ function readFromUrl(search) {
1125
+ let params;
1126
+ try {
1127
+ params = new URLSearchParams(search);
1128
+ } catch {
1129
+ return {};
1050
1130
  }
1051
- return (hash2 >>> 0).toString(16);
1131
+ const utm = {};
1132
+ for (const key of UTM_KEYS) {
1133
+ const value = params.get(key)?.trim();
1134
+ if (value) {
1135
+ utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1136
+ }
1137
+ }
1138
+ return utm;
1052
1139
  }
1053
- function computeChecksum(cart) {
1054
- return hashString2(JSON.stringify(cart));
1140
+ function storageKey(storeSlug) {
1141
+ return `${UTM_STORAGE_PREFIX}${storeSlug}`;
1055
1142
  }
1056
- function normalizeQuantity(value) {
1057
- if (!Number.isFinite(value)) {
1058
- throw new CartError("quantity must be a finite number");
1143
+ function readStored(storeSlug) {
1144
+ try {
1145
+ const raw = window.localStorage.getItem(storefrontStorageKey(storageKey(storeSlug)));
1146
+ if (!raw) return {};
1147
+ const parsed = JSON.parse(raw);
1148
+ if (!parsed || typeof parsed.at !== "number" || typeof parsed.utm !== "object") return {};
1149
+ if (Date.now() - parsed.at > TTL_MS) {
1150
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1151
+ return {};
1152
+ }
1153
+ const utm = {};
1154
+ for (const key of UTM_KEYS) {
1155
+ const value = parsed.utm?.[key];
1156
+ if (typeof value === "string" && value.length > 0) {
1157
+ utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1158
+ }
1159
+ }
1160
+ return utm;
1161
+ } catch {
1162
+ return {};
1059
1163
  }
1060
- return Math.floor(value);
1061
1164
  }
1062
- function normalizeCouponCode(value) {
1063
- const normalized = value?.trim().toUpperCase();
1064
- return normalized ? normalized : null;
1165
+ function writeStored(storeSlug, utm) {
1166
+ try {
1167
+ const payload = { utm, at: Date.now() };
1168
+ window.localStorage.setItem(storefrontStorageKey(storageKey(storeSlug)), JSON.stringify(payload));
1169
+ } catch {
1170
+ }
1065
1171
  }
1066
- function getStoredCartCode() {
1067
- const raw = readCartStorage("coupon");
1068
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1069
- return null;
1172
+ function resolveUtmParameters(storeSlug) {
1173
+ if (typeof window === "undefined" || typeof window.location === "undefined") {
1174
+ return void 0;
1070
1175
  }
1071
- const record2 = raw;
1072
- const code = typeof record2.code === "string" ? normalizeCouponCode(record2.code) : null;
1073
- const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1074
- return code && source ? { code, source } : null;
1075
- }
1076
- function normalizeCartItems(value) {
1077
- if (!Array.isArray(value)) return [];
1078
- const normalized = [];
1079
- for (const entry of value) {
1080
- if (!entry || typeof entry !== "object") continue;
1081
- const record2 = entry;
1082
- const productId = typeof record2.product_id === "string" ? record2.product_id.trim() : "";
1083
- const variantId = typeof record2.variant_id === "string" ? record2.variant_id.trim() : "";
1084
- const quantity = Number(record2.quantity);
1085
- if (!productId || !variantId || !Number.isFinite(quantity) || quantity < 1) {
1086
- continue;
1087
- }
1088
- const item = {
1089
- line_id: "",
1090
- product_id: productId,
1091
- variant_id: variantId,
1092
- quantity: Math.floor(quantity)
1093
- };
1094
- if (typeof record2.price_variant_id === "string") {
1095
- item.price_variant_id = record2.price_variant_id;
1096
- }
1097
- if (record2.price_data && typeof record2.price_data === "object") {
1098
- const priceData = record2.price_data;
1099
- if (typeof priceData.unit_price === "number" && Number.isFinite(priceData.unit_price)) {
1100
- item.price_data = { unit_price: priceData.unit_price };
1101
- }
1102
- }
1103
- if (typeof record2.pay_what_you_want_price === "number" && Number.isFinite(record2.pay_what_you_want_price)) {
1104
- item.pay_what_you_want_price = record2.pay_what_you_want_price;
1105
- }
1106
- if (Array.isArray(record2.addons)) {
1107
- item.addons = record2.addons;
1108
- }
1109
- if (record2.custom_fields && typeof record2.custom_fields === "object" && !Array.isArray(record2.custom_fields)) {
1110
- item.custom_fields = record2.custom_fields;
1111
- }
1112
- if (typeof record2.title === "string") {
1113
- item.title = record2.title;
1114
- }
1115
- if (typeof record2.variant_title === "string") {
1116
- item.variant_title = record2.variant_title;
1117
- }
1118
- if (typeof record2.image_url === "string") {
1119
- item.image_url = record2.image_url;
1120
- }
1121
- if (Array.isArray(record2.addon_labels)) {
1122
- item.addon_labels = record2.addon_labels.filter((label) => typeof label === "string");
1123
- }
1124
- if (typeof record2.max_quantity === "number" && Number.isFinite(record2.max_quantity)) {
1125
- item.max_quantity = record2.max_quantity;
1126
- }
1127
- if (typeof record2.min_quantity === "number" && Number.isFinite(record2.min_quantity)) {
1128
- item.min_quantity = record2.min_quantity;
1176
+ const fromUrl = readFromUrl(window.location.search);
1177
+ if (readAnalyticsConsent() !== "granted") {
1178
+ try {
1179
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1180
+ } catch {
1129
1181
  }
1130
- clampQuantityToBounds(item);
1131
- const storedLineId = typeof record2.line_id === "string" ? record2.line_id.trim() : "";
1132
- item.line_id = storedLineId || computeCartLineId(item);
1133
- normalized.push(ensureCartLineId(item));
1182
+ return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1134
1183
  }
1135
- return normalized;
1184
+ if (Object.keys(fromUrl).length > 0) {
1185
+ writeStored(storeSlug, fromUrl);
1186
+ return fromUrl;
1187
+ }
1188
+ const stored = readStored(storeSlug);
1189
+ return Object.keys(stored).length > 0 ? stored : void 0;
1136
1190
  }
1137
- function getCartMetadata() {
1138
- return readCartStorage("meta");
1191
+
1192
+ // ../sdk/src/core/page-path.ts
1193
+ var MAX_PATH_LENGTH = 256;
1194
+ function resolveStorefrontPagePath() {
1195
+ if (typeof window === "undefined") return void 0;
1196
+ let pathname;
1197
+ try {
1198
+ const location = window.location;
1199
+ pathname = typeof location?.pathname === "string" && location.pathname ? location.pathname : new URL(String(location?.href ?? "")).pathname;
1200
+ } catch {
1201
+ return void 0;
1202
+ }
1203
+ if (!pathname || !pathname.startsWith("/")) return void 0;
1204
+ return pathname.slice(0, MAX_PATH_LENGTH);
1139
1205
  }
1140
- function getCartCoupon() {
1141
- return getStoredCartCode()?.code ?? null;
1206
+
1207
+ // ../sdk/src/core/page-tracker.ts
1208
+ var PLATFORM_PAGE_TRACKER_FLAG = "__shoppexPageTracker";
1209
+ function isPlatformPageTrackerActive() {
1210
+ if (typeof window === "undefined") return false;
1211
+ try {
1212
+ return Boolean(window[PLATFORM_PAGE_TRACKER_FLAG]);
1213
+ } catch {
1214
+ return false;
1215
+ }
1142
1216
  }
1143
- function getCartCouponSource() {
1144
- return getStoredCartCode()?.source ?? null;
1217
+
1218
+ // ../sdk/src/modules/analytics.ts
1219
+ var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
1220
+ var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
1221
+ var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
1222
+ var CART_CLAIM_TTL_MS = 1e4;
1223
+ function confirmedCartKey(storeSlug) {
1224
+ return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
1145
1225
  }
1146
- function setCartCoupon(coupon, source = "COUPON") {
1147
- const normalizedCoupon = normalizeCouponCode(coupon);
1148
- if (!normalizedCoupon) {
1149
- removeCartStorage("coupon");
1226
+ function inFlightCartKey(storeSlug) {
1227
+ return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
1228
+ }
1229
+ function cartSequenceKey(storeSlug) {
1230
+ return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
1231
+ }
1232
+ function cartSnapshot(cartValue, itemCount) {
1233
+ return `${cartValue ?? ""}|${itemCount ?? ""}`;
1234
+ }
1235
+ function isCartSequence(value) {
1236
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1237
+ }
1238
+ var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
1239
+ function readCartCounter(storeSlug) {
1240
+ const counter = getItem(cartSequenceKey(storeSlug));
1241
+ if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
1150
1242
  return null;
1151
1243
  }
1152
- writeCartStorage("coupon", { code: normalizedCoupon, source });
1153
- return normalizedCoupon;
1244
+ return counter;
1154
1245
  }
1155
- function clearCartCoupon() {
1156
- removeCartStorage("coupon");
1246
+ var lastCartOrder = null;
1247
+ function nextCartOrder(storeSlug) {
1248
+ const stored = readCartCounter(storeSlug);
1249
+ const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
1250
+ const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
1251
+ const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
1252
+ lastCartOrder = order2;
1253
+ setItem(cartSequenceKey(storeSlug), order2);
1254
+ return order2;
1157
1255
  }
1158
- function writeCart(cart) {
1159
- const normalizedCart = normalizeCartItems(cart);
1160
- writeCartStorage("cart", normalizedCart);
1161
- const now = Date.now();
1162
- const previous = getCartMetadata();
1163
- const nextMeta = {
1164
- created_at: previous?.created_at ?? now,
1165
- last_modified: now,
1166
- version: (previous?.version ?? 0) + 1,
1167
- checksum: computeChecksum(normalizedCart)
1168
- };
1169
- writeCartStorage("meta", nextMeta);
1170
- if (normalizedCart.length === 0) {
1171
- clearCartCoupon();
1172
- }
1256
+ function cartSequenceLocks() {
1257
+ if (typeof navigator === "undefined") return null;
1258
+ const locks = navigator.locks;
1259
+ return locks && typeof locks.request === "function" ? locks : null;
1173
1260
  }
1174
- function setCartWithMetadata(cart, metadata) {
1175
- const normalizedCart = normalizeCartItems(cart);
1176
- writeCartStorage("cart", normalizedCart);
1177
- const now = Date.now();
1178
- const base = metadata ?? getCartMetadata();
1179
- const nextMeta = {
1180
- created_at: base?.created_at ?? now,
1181
- last_modified: now,
1182
- version: base?.version ?? 1,
1183
- checksum: computeChecksum(normalizedCart)
1184
- };
1185
- writeCartStorage("meta", nextMeta);
1261
+ function reserveCartOrder(storeSlug) {
1262
+ const take = () => nextCartOrder(storeSlug);
1263
+ const locks = cartSequenceLocks();
1264
+ if (!locks) return Promise.resolve(take());
1265
+ return locks.request(cartSequenceKey(storeSlug), take).catch(take);
1186
1266
  }
1187
- function getCart() {
1188
- const raw = readCartStorage("cart");
1189
- return normalizeCartItems(raw);
1267
+ function readConfirmedCart(storeSlug) {
1268
+ const confirmed = getItem(confirmedCartKey(storeSlug));
1269
+ if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
1270
+ return confirmed;
1190
1271
  }
1191
- function resolveCartLineId(productId, variantId, cart = getCart()) {
1192
- const matches = cart.filter(
1193
- (item) => item.product_id === productId && item.variant_id === variantId
1194
- );
1195
- if (matches.length === 0) {
1196
- throw new CartError(`No cart line found for ${productId}/${variantId}`);
1272
+ function readInFlightCart(storeSlug) {
1273
+ const claim2 = getItem(inFlightCartKey(storeSlug));
1274
+ if (!claim2 || typeof claim2.snapshot !== "string" || !isCartSequence(claim2.seq) || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
1275
+ return null;
1197
1276
  }
1198
- return matches[0].line_id;
1277
+ return claim2;
1199
1278
  }
1200
- function getCartItemCount() {
1201
- const cart = getCart();
1202
- return cart.reduce((sum, item) => sum + item.quantity, 0);
1279
+ function isCartReportedOrInFlight(storeSlug, snapshot) {
1280
+ const confirmed = readConfirmedCart(storeSlug);
1281
+ const claim2 = readInFlightCart(storeSlug);
1282
+ const liveClaim = claim2 && Date.now() - claim2.at < CART_CLAIM_TTL_MS ? claim2 : null;
1283
+ const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
1284
+ return expected?.snapshot === snapshot;
1203
1285
  }
1204
- function addToCart(productId, variantId, quantity = 1, options) {
1205
- if (!productId || !variantId) {
1206
- throw new CartError("product_id and variant_id are required");
1207
- }
1208
- const normalizedQuantity = normalizeQuantity(quantity);
1209
- if (normalizedQuantity < 1) {
1210
- throw new CartError("quantity must be at least 1");
1211
- }
1212
- const cart = getCart();
1213
- const lineId = computeCartLineId({
1214
- product_id: productId,
1215
- variant_id: variantId,
1216
- addons: options?.addons,
1217
- custom_fields: options?.custom_fields,
1218
- price_variant_id: options?.price_variant_id,
1219
- price_data: options?.price_data,
1220
- pay_what_you_want_price: options?.pay_what_you_want_price
1221
- });
1222
- const existingIndex = cart.findIndex((item) => item.line_id === lineId);
1223
- const quantityBeforeAdd = existingIndex >= 0 ? cart[existingIndex].quantity : 0;
1224
- if (existingIndex >= 0) {
1225
- cart[existingIndex].quantity += normalizedQuantity;
1226
- if (options?.addons) {
1227
- cart[existingIndex].addons = options.addons;
1228
- }
1229
- if (options?.custom_fields) {
1230
- cart[existingIndex].custom_fields = options.custom_fields;
1231
- }
1232
- if (options?.price_variant_id) {
1233
- cart[existingIndex].price_variant_id = options.price_variant_id;
1234
- }
1235
- if (options?.price_data) {
1236
- cart[existingIndex].price_data = options.price_data;
1286
+ function createClaimToken() {
1287
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1288
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
1289
+ }
1290
+ function claimCartReport(storeSlug, seq, cartValue, itemCount) {
1291
+ const snapshot = cartSnapshot(cartValue, itemCount);
1292
+ const token = createClaimToken();
1293
+ setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
1294
+ return (delivered) => {
1295
+ if (delivered) {
1296
+ const confirmed = readConfirmedCart(storeSlug);
1297
+ if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
1237
1298
  }
1238
- if (options?.pay_what_you_want_price !== void 0) {
1239
- cart[existingIndex].pay_what_you_want_price = options.pay_what_you_want_price;
1240
- }
1241
- applyDisplaySnapshot(cart[existingIndex], options);
1242
- clampQuantityToBounds(cart[existingIndex]);
1243
- } else {
1244
- const pushed = {
1245
- line_id: lineId,
1246
- product_id: productId,
1247
- variant_id: variantId,
1248
- quantity: normalizedQuantity,
1249
- addons: options?.addons,
1250
- custom_fields: options?.custom_fields,
1251
- price_variant_id: options?.price_variant_id,
1252
- price_data: options?.price_data,
1253
- pay_what_you_want_price: options?.pay_what_you_want_price,
1254
- title: options?.title,
1255
- variant_title: options?.variant_title,
1256
- image_url: options?.image_url,
1257
- addon_labels: options?.addon_labels,
1258
- max_quantity: options?.max_quantity,
1259
- min_quantity: options?.min_quantity
1260
- };
1261
- clampQuantityToBounds(pushed);
1262
- cart.push(pushed);
1263
- }
1264
- const intendedQuantity = (existingIndex >= 0 ? cart[existingIndex] : cart[cart.length - 1]).quantity;
1265
- writeCart(cart);
1266
- const persistedLine = getCart().find((item) => item.line_id === lineId);
1267
- if (intendedQuantity > quantityBeforeAdd && persistedLine?.quantity === intendedQuantity) {
1268
- const cartAffiliateCode = getCartCouponSource() === "AFFILIATE" ? getCartCoupon() : null;
1269
- void trackAffiliateEvent("add_to_cart", cartAffiliateCode ? { code: cartAffiliateCode } : void 0);
1270
- }
1299
+ if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
1300
+ };
1271
1301
  }
1272
- function clampQuantityToBounds(item) {
1273
- const min = item.min_quantity;
1274
- if (typeof min === "number" && Number.isFinite(min) && item.quantity < min) {
1275
- item.quantity = Math.floor(min);
1276
- }
1277
- const max = item.max_quantity;
1278
- if (typeof max === "number" && Number.isFinite(max) && item.quantity > max) {
1279
- item.quantity = Math.floor(max);
1280
- }
1281
- if (item.quantity < 1) {
1282
- item.quantity = 1;
1302
+ var cartSendTail = Promise.resolve();
1303
+ var latestCartRequest = 0;
1304
+ function enqueueCartSend(send) {
1305
+ const request2 = ++latestCartRequest;
1306
+ const run = () => send(() => request2 === latestCartRequest);
1307
+ cartSendTail = cartSendTail.then(run, run);
1308
+ return cartSendTail;
1309
+ }
1310
+ async function postPing(body) {
1311
+ const config2 = getConfig();
1312
+ const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1313
+ let timer;
1314
+ const timedOut = new Promise((resolve) => {
1315
+ timer = setTimeout(() => {
1316
+ controller?.abort();
1317
+ resolve(false);
1318
+ }, CART_CLAIM_TTL_MS);
1319
+ });
1320
+ try {
1321
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", {
1322
+ storeSlug: config2.storeSlug
1323
+ });
1324
+ const delivered = fetch(`${config2.apiBaseUrl}${endpoint}`, {
1325
+ method: "POST",
1326
+ headers: { "Content-Type": "application/json" },
1327
+ body: JSON.stringify(body),
1328
+ signal: controller?.signal
1329
+ }).then((response) => response.ok);
1330
+ return await Promise.race([delivered, timedOut]);
1331
+ } catch {
1332
+ return false;
1333
+ } finally {
1334
+ clearTimeout(timer);
1283
1335
  }
1284
1336
  }
1285
- function applyDisplaySnapshot(item, options) {
1286
- if (options?.replace_display_snapshot) {
1287
- item.title = options.title;
1288
- item.variant_title = options.variant_title;
1289
- item.image_url = options.image_url;
1290
- item.addon_labels = options.addon_labels;
1291
- item.max_quantity = options.max_quantity;
1292
- item.min_quantity = options.min_quantity;
1337
+ async function trackPageView(cartValue, itemCount) {
1338
+ if (!isInitialized()) return;
1339
+ if (typeof document === "undefined") return;
1340
+ const config2 = getConfig();
1341
+ const carriesCart = cartValue !== void 0 || itemCount !== void 0;
1342
+ const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
1343
+ if (isPlatformPageTrackerActive()) {
1344
+ if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
1293
1345
  return;
1294
1346
  }
1295
- if (options?.title !== void 0) {
1296
- item.title = options.title;
1297
- }
1298
- if (options?.variant_title !== void 0) {
1299
- item.variant_title = options.variant_title;
1300
- }
1301
- if (options?.image_url !== void 0) {
1302
- item.image_url = options.image_url;
1303
- }
1304
- if (options?.addon_labels !== void 0) {
1305
- item.addon_labels = options.addon_labels;
1306
- }
1307
- if (options?.max_quantity !== void 0) {
1308
- item.max_quantity = options.max_quantity;
1309
- }
1310
- if (options?.min_quantity !== void 0) {
1311
- item.min_quantity = options.min_quantity;
1312
- }
1347
+ const sendView = async () => {
1348
+ const order2 = reservedOrder ? await reservedOrder : void 0;
1349
+ const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1350
+ const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
1351
+ const previousSession = currentPageSession();
1352
+ if (previousSession && previousSession !== sessionId) reportPageLeave();
1353
+ markPageViewed(sessionId);
1354
+ if (sessionId) installEngagedClock(reportPageLeave);
1355
+ onAnalyticsConsent(startSessionOnConsent);
1356
+ const delivered = await postPing({
1357
+ referer: document.referrer || void 0,
1358
+ cart_value: cartValue,
1359
+ item_count: itemCount,
1360
+ cart_seq: order2?.seq,
1361
+ cart_seq_scope: order2?.scope,
1362
+ session_id: sessionId ?? void 0,
1363
+ // Time on screen of the page before this one (client-side navigation).
1364
+ engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1365
+ path: resolveStorefrontPagePath(),
1366
+ utm: resolveUtmParameters(config2.storeSlug)
1367
+ });
1368
+ settle2?.(delivered);
1369
+ };
1370
+ await (reservedOrder ? enqueueCartSend(sendView) : sendView());
1313
1371
  }
1314
- function setCartItem(productId, variantId, quantity = 1, options) {
1315
- if (!productId || !variantId) {
1316
- throw new CartError("product_id and variant_id are required");
1317
- }
1318
- const normalizedQuantity = normalizeQuantity(quantity);
1319
- if (normalizedQuantity < 1) {
1320
- throw new CartError("quantity must be at least 1");
1321
- }
1322
- const cart = getCart();
1323
- const lineId = computeCartLineId({
1324
- product_id: productId,
1325
- variant_id: variantId,
1326
- addons: options?.addons,
1327
- custom_fields: options?.custom_fields,
1328
- price_variant_id: options?.price_variant_id,
1329
- price_data: options?.price_data,
1330
- pay_what_you_want_price: options?.pay_what_you_want_price
1372
+ async function trackCartActivity(cartValue, itemCount) {
1373
+ if (!isInitialized()) return;
1374
+ if (typeof document === "undefined") return;
1375
+ const snapshot = cartSnapshot(cartValue, itemCount);
1376
+ const reservedOrder = reserveCartOrder(getConfig().storeSlug);
1377
+ await enqueueCartSend(async (isLatest) => {
1378
+ const order2 = await reservedOrder;
1379
+ if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
1380
+ await sendCartActivity(order2, cartValue, itemCount);
1331
1381
  });
1332
- for (let index = cart.length - 1; index >= 0; index -= 1) {
1333
- if (cart[index].product_id === productId && cart[index].variant_id === variantId) {
1334
- cart.splice(index, 1);
1335
- }
1336
- }
1337
- const pushed = {
1338
- line_id: lineId,
1339
- product_id: productId,
1340
- variant_id: variantId,
1341
- quantity: normalizedQuantity,
1342
- addons: options?.addons,
1343
- custom_fields: options?.custom_fields,
1344
- price_variant_id: options?.price_variant_id,
1345
- price_data: options?.price_data,
1346
- pay_what_you_want_price: options?.pay_what_you_want_price,
1347
- title: options?.title,
1348
- variant_title: options?.variant_title,
1349
- image_url: options?.image_url,
1350
- addon_labels: options?.addon_labels,
1351
- max_quantity: options?.max_quantity,
1352
- min_quantity: options?.min_quantity
1353
- };
1354
- clampQuantityToBounds(pushed);
1355
- cart.push(pushed);
1356
- writeCart(cart);
1357
1382
  }
1358
- function updateCartItem(lineId, updates) {
1359
- const cart = getCart();
1360
- const normalizedLineId = lineId.trim();
1361
- if (!normalizedLineId) {
1362
- throw new CartError("line_id is required");
1363
- }
1364
- const index = cart.findIndex((item) => item.line_id === normalizedLineId);
1365
- if (index < 0) {
1366
- throw new CartError("Item not found in cart");
1367
- }
1368
- const before = JSON.stringify(cart);
1369
- if (updates.quantity !== void 0) {
1370
- const normalizedQuantity = normalizeQuantity(updates.quantity);
1371
- if (normalizedQuantity < 1) {
1372
- cart.splice(index, 1);
1373
- writeCart(cart);
1374
- return;
1375
- }
1376
- cart[index].quantity = normalizedQuantity;
1377
- }
1378
- if (updates.addons !== void 0) {
1379
- cart[index].addons = updates.addons;
1380
- }
1381
- if (updates.custom_fields !== void 0) {
1382
- cart[index].custom_fields = updates.custom_fields;
1383
- }
1384
- if (updates.price_variant_id !== void 0) {
1385
- cart[index].price_variant_id = updates.price_variant_id;
1386
- }
1387
- if (updates.price_data !== void 0) {
1388
- cart[index].price_data = updates.price_data;
1389
- }
1390
- if (updates.pay_what_you_want_price !== void 0) {
1391
- cart[index].pay_what_you_want_price = updates.pay_what_you_want_price;
1392
- }
1393
- if (updates.variant_title !== void 0) {
1394
- if (updates.variant_title === null) delete cart[index].variant_title;
1395
- else cart[index].variant_title = updates.variant_title;
1396
- }
1397
- if (updates.max_quantity !== void 0) {
1398
- if (updates.max_quantity === null) delete cart[index].max_quantity;
1399
- else cart[index].max_quantity = updates.max_quantity;
1400
- }
1401
- if (updates.min_quantity !== void 0) {
1402
- if (updates.min_quantity === null) delete cart[index].min_quantity;
1403
- else cart[index].min_quantity = updates.min_quantity;
1404
- }
1405
- clampQuantityToBounds(cart[index]);
1406
- const nextLineId = computeCartLineId(cart[index]);
1407
- const duplicateIndex = cart.findIndex((item, itemIndex) => itemIndex !== index && item.line_id === nextLineId);
1408
- if (duplicateIndex >= 0) {
1409
- cart[duplicateIndex].quantity += cart[index].quantity;
1410
- clampQuantityToBounds(cart[duplicateIndex]);
1411
- cart.splice(index, 1);
1412
- } else {
1413
- cart[index].line_id = nextLineId;
1414
- }
1415
- if (JSON.stringify(cart) === before) return;
1416
- writeCart(cart);
1383
+ async function sendCartActivity(order2, cartValue, itemCount) {
1384
+ const config2 = getConfig();
1385
+ const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
1386
+ const delivered = await postPing({
1387
+ event_type: "presence",
1388
+ cart_value: cartValue,
1389
+ item_count: itemCount,
1390
+ cart_seq: order2.seq,
1391
+ cart_seq_scope: order2.scope,
1392
+ // A cart marks the analytics session as one that did not bounce.
1393
+ session_id: analyticsSessionId(config2.storeSlug, { touch: false }) ?? void 0,
1394
+ path: resolveStorefrontPagePath()
1395
+ });
1396
+ settle2(delivered);
1417
1397
  }
1418
- function removeFromCart(lineId) {
1419
- const normalizedLineId = lineId.trim();
1420
- if (!normalizedLineId) {
1421
- throw new CartError("line_id is required");
1422
- }
1423
- const cart = getCart();
1424
- const filtered = cart.filter((item) => item.line_id !== normalizedLineId);
1425
- writeCart(filtered);
1398
+ async function trackProductView(productId) {
1399
+ if (!isInitialized() || typeof document === "undefined" || !productId) return;
1400
+ await sendVisitEvent({ event_type: "product_view", product_id: productId });
1426
1401
  }
1427
- function clearCart() {
1428
- removeCartStorage("cart");
1429
- removeCartStorage("meta");
1430
- clearCartCoupon();
1431
- clearLatestQuoteToken();
1402
+ async function trackCartAdd(productId, variantId, quantity) {
1403
+ if (!isInitialized() || typeof document === "undefined" || quantity < 1) return;
1404
+ await sendVisitEvent({ event_type: "cart_add", product_id: productId, variant_id: variantId, quantity });
1432
1405
  }
1433
- function createCartBackup() {
1434
- const cart = getCart();
1435
- writeCartStorage("cartBackup", cart);
1436
- const cartCode = getStoredCartCode();
1437
- if (cartCode) {
1438
- writeCartStorage("couponBackup", cartCode);
1439
- } else {
1440
- removeCartStorage("couponBackup");
1441
- }
1442
- const metadata = getCartMetadata();
1443
- if (metadata) {
1444
- writeCartStorage("metaBackup", metadata);
1445
- } else {
1446
- const now = Date.now();
1447
- writeCartStorage("metaBackup", {
1448
- created_at: now,
1449
- last_modified: now,
1450
- version: 1,
1451
- checksum: computeChecksum(cart)
1406
+ async function sendVisitEvent(body) {
1407
+ const config2 = getConfig();
1408
+ if (config2.preview) return;
1409
+ await postPing({
1410
+ ...body,
1411
+ referer: document.referrer || void 0,
1412
+ session_id: analyticsSessionId(config2.storeSlug, { touch: true }) ?? void 0,
1413
+ path: resolveStorefrontPagePath(),
1414
+ utm: resolveUtmParameters(config2.storeSlug)
1415
+ });
1416
+ }
1417
+ function startSessionOnConsent() {
1418
+ const config2 = getConfig();
1419
+ if (!isPageViewed() || analyticsSessionId(config2.storeSlug, { touch: false })) return;
1420
+ const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
1421
+ if (!sessionId) return;
1422
+ markPageViewed(sessionId);
1423
+ installEngagedClock(reportPageLeave);
1424
+ void postPing({
1425
+ event_type: "session_start",
1426
+ referer: document.referrer || void 0,
1427
+ session_id: sessionId,
1428
+ path: resolveStorefrontPagePath(),
1429
+ utm: resolveUtmParameters(config2.storeSlug)
1430
+ });
1431
+ }
1432
+ function reportPageLeave() {
1433
+ const config2 = getConfig();
1434
+ const sessionId = currentPageSession();
1435
+ const engagedMs2 = takeEngagedMs();
1436
+ if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
1437
+ try {
1438
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config2.storeSlug });
1439
+ void fetch(`${config2.apiBaseUrl}${endpoint}`, {
1440
+ method: "POST",
1441
+ headers: { "Content-Type": "application/json" },
1442
+ body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
1443
+ keepalive: true
1444
+ }).catch(() => {
1452
1445
  });
1446
+ } catch {
1453
1447
  }
1454
1448
  }
1455
- function restoreCartFromBackup() {
1456
- const backupRaw = readCartStorage("cartBackup");
1457
- const backup = normalizeCartItems(backupRaw);
1458
- const backupMeta = readCartStorage("metaBackup");
1459
- const backupCoupon = readCartStorage("couponBackup");
1460
- if (backup && backup.length > 0) {
1461
- setCartWithMetadata(backup, backupMeta);
1462
- if (backupCoupon && typeof backupCoupon === "object" && !Array.isArray(backupCoupon)) {
1463
- const record2 = backupCoupon;
1464
- const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1465
- if (typeof record2.code === "string" && source) {
1466
- setCartCoupon(record2.code, source);
1467
- } else {
1468
- clearCartCoupon();
1469
- }
1470
- } else {
1471
- clearCartCoupon();
1472
- }
1473
- return true;
1449
+
1450
+ // ../sdk/src/utils/cart-line-id.ts
1451
+ function hashString(value) {
1452
+ let hash2 = 2166136261;
1453
+ for (let i = 0; i < value.length; i += 1) {
1454
+ hash2 ^= value.charCodeAt(i);
1455
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
1474
1456
  }
1475
- return false;
1457
+ return (hash2 >>> 0).toString(16);
1476
1458
  }
1477
- function mergeBaskets(items) {
1478
- const cart = getCart();
1479
- for (const incoming of items) {
1480
- const productId = typeof incoming.product_id === "string" ? incoming.product_id.trim() : "";
1481
- const variantId = typeof incoming.variant_id === "string" ? incoming.variant_id.trim() : "";
1482
- if (!productId || !variantId) {
1483
- continue;
1484
- }
1485
- let normalizedQuantity;
1486
- try {
1487
- normalizedQuantity = normalizeQuantity(incoming.quantity);
1488
- } catch {
1489
- continue;
1490
- }
1491
- if (normalizedQuantity < 1) {
1492
- continue;
1493
- }
1494
- const mergedLine = ensureCartLineId({
1495
- ...incoming,
1496
- line_id: incoming.line_id ?? computeCartLineId(incoming),
1497
- product_id: productId,
1498
- variant_id: variantId,
1499
- quantity: normalizedQuantity
1500
- });
1501
- const index = cart.findIndex((item) => item.line_id === mergedLine.line_id);
1502
- if (index >= 0) {
1503
- cart[index].quantity = Math.max(cart[index].quantity, normalizedQuantity);
1504
- clampQuantityToBounds(cart[index]);
1505
- } else {
1506
- clampQuantityToBounds(mergedLine);
1507
- cart.push(mergedLine);
1508
- }
1459
+ function normalizeAddons(addons) {
1460
+ if (!addons?.length) return [];
1461
+ return [...addons].map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 })).sort((a, b) => a.id.localeCompare(b.id));
1462
+ }
1463
+ function normalizeCustomFields(fields) {
1464
+ if (!fields) return {};
1465
+ const entries = Object.entries(fields).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b));
1466
+ return Object.fromEntries(entries);
1467
+ }
1468
+ function computeCartLineId(input2) {
1469
+ const payload = {
1470
+ product_id: input2.product_id,
1471
+ variant_id: input2.variant_id,
1472
+ price_variant_id: input2.price_variant_id ?? null,
1473
+ addons: normalizeAddons(input2.addons),
1474
+ custom_fields: normalizeCustomFields(input2.custom_fields),
1475
+ unit_price: typeof input2.price_data?.unit_price === "number" && Number.isFinite(input2.price_data.unit_price) ? input2.price_data.unit_price : null,
1476
+ pay_what_you_want_price: typeof input2.pay_what_you_want_price === "number" && Number.isFinite(input2.pay_what_you_want_price) ? input2.pay_what_you_want_price : null
1477
+ };
1478
+ return hashString(JSON.stringify(payload));
1479
+ }
1480
+ function ensureCartLineId(item) {
1481
+ if (typeof item.line_id === "string" && item.line_id.trim()) {
1482
+ return item;
1509
1483
  }
1510
- writeCart(cart);
1511
- return cart;
1484
+ return { ...item, line_id: computeCartLineId(item) };
1485
+ }
1486
+
1487
+ // ../contracts/src/catalog-unit-price.ts
1488
+ var CATALOG_UNIT_PRICE_DECIMAL_PLACES = 3;
1489
+ var PAYABLE_AMOUNT_DECIMAL_PLACES = 2;
1490
+ var CATALOG_UNIT_PRICE_FORMAT_OPTIONS = {
1491
+ maximumFractionDigits: CATALOG_UNIT_PRICE_DECIMAL_PLACES
1492
+ };
1493
+ function roundPayableAmount(value) {
1494
+ if (!Number.isFinite(value)) return value;
1495
+ const sign = value < 0 ? -1 : 1;
1496
+ const shifted = shiftDecimal(Math.abs(value), PAYABLE_AMOUNT_DECIMAL_PLACES);
1497
+ const rounded = Math.round(shifted);
1498
+ if (rounded === 0) return 0;
1499
+ return sign * shiftDecimal(rounded, -PAYABLE_AMOUNT_DECIMAL_PLACES);
1500
+ }
1501
+ function shiftDecimal(value, places) {
1502
+ const [coefficient, currentExponent = "0"] = String(value).split("e");
1503
+ return Number(`${coefficient}e${Number(currentExponent) + places}`);
1504
+ }
1505
+
1506
+ // ../sdk/src/modules/cart.ts
1507
+ var STORAGE_KEYS = {
1508
+ cart: "cart",
1509
+ cartBackup: "cart_backup",
1510
+ meta: "cart_meta",
1511
+ metaBackup: "cart_backup_meta",
1512
+ coupon: "cart_coupon",
1513
+ couponBackup: "cart_coupon_backup"
1514
+ };
1515
+ function getStorageKey(type) {
1516
+ return `${STORAGE_KEYS[type]}_${getConfig().storeSlug}`;
1517
+ }
1518
+ function cartStorageArea() {
1519
+ return getConfig().preview ? "sessionStorage" : "localStorage";
1520
+ }
1521
+ function readCartStorage(type) {
1522
+ return getItem(getStorageKey(type), cartStorageArea());
1523
+ }
1524
+ function writeCartStorage(type, value) {
1525
+ setItem(getStorageKey(type), value, cartStorageArea());
1512
1526
  }
1513
- function moveBasketItem(fromProductId, fromVariantId, toProductId, toVariantId) {
1514
- const cart = getCart();
1515
- const fromIndex = cart.findIndex(
1516
- (item) => item.product_id === fromProductId && item.variant_id === fromVariantId
1517
- );
1518
- if (fromIndex < 0) {
1519
- throw new CartError("Item not found in cart");
1520
- }
1521
- const [fromItem] = cart.splice(fromIndex, 1);
1522
- const toLineId = computeCartLineId({
1523
- ...fromItem,
1524
- product_id: toProductId,
1525
- variant_id: toVariantId
1526
- });
1527
- const toIndex = cart.findIndex((item) => item.line_id === toLineId);
1528
- if (toIndex >= 0) {
1529
- cart[toIndex].quantity += fromItem.quantity;
1530
- clampQuantityToBounds(cart[toIndex]);
1531
- } else {
1532
- const moved = ensureCartLineId({
1533
- ...fromItem,
1534
- product_id: toProductId,
1535
- variant_id: toVariantId,
1536
- line_id: toLineId
1537
- });
1538
- clampQuantityToBounds(moved);
1539
- cart.push(moved);
1527
+ function removeCartStorage(type) {
1528
+ removeItem(getStorageKey(type), cartStorageArea());
1529
+ }
1530
+ function hashString2(value) {
1531
+ let hash2 = 2166136261;
1532
+ for (let i = 0; i < value.length; i += 1) {
1533
+ hash2 ^= value.charCodeAt(i);
1534
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
1540
1535
  }
1541
- writeCart(cart);
1536
+ return (hash2 >>> 0).toString(16);
1542
1537
  }
1543
- function validateCartIntegrity() {
1544
- const cart = getCart();
1545
- const metadata = getCartMetadata();
1546
- const checksum = computeChecksum(cart);
1547
- if (!metadata) {
1548
- setCartWithMetadata(cart, null);
1549
- return true;
1538
+ function computeChecksum(cart) {
1539
+ return hashString2(JSON.stringify(cart));
1540
+ }
1541
+ function normalizeQuantity(value) {
1542
+ if (!Number.isFinite(value)) {
1543
+ throw new CartError("quantity must be a finite number");
1550
1544
  }
1551
- return metadata.checksum === checksum;
1545
+ return Math.floor(value);
1552
1546
  }
1553
- function getCartStats() {
1554
- const cart = getCart();
1555
- const integrityValid = validateCartIntegrity();
1556
- const metadata = getCartMetadata();
1557
- const backup = readCartStorage("cartBackup") ?? [];
1558
- const hasCompletePriceSnapshots = cart.length > 0 && cart.every((item) => typeof item.price_data?.unit_price === "number");
1559
- const totalPrice = cart.reduce((sum, item) => {
1560
- const unitPrice = item.price_data?.unit_price ?? 0;
1561
- return sum + roundPayableAmount(unitPrice * item.quantity);
1562
- }, 0);
1563
- return {
1564
- item_count: cart.length,
1565
- total_quantity: cart.reduce((sum, item) => sum + item.quantity, 0),
1566
- last_modified: metadata?.last_modified ?? 0,
1567
- version: metadata?.version ?? 0,
1568
- has_backup: backup.length > 0,
1569
- integrity_valid: integrityValid,
1570
- total_price: roundPayableAmount(totalPrice),
1571
- total_price_is_estimate: cart.length > 0 && !hasCompletePriceSnapshots
1572
- };
1547
+ function normalizeCouponCode(value) {
1548
+ const normalized = value?.trim().toUpperCase();
1549
+ return normalized ? normalized : null;
1573
1550
  }
1574
- function getCartPayload(coupon) {
1575
- const config2 = getConfig();
1576
- const normalizedCoupon = normalizeCouponCode(coupon) ?? getCartCoupon() ?? void 0;
1577
- return {
1578
- store_slug: config2.storeSlug,
1579
- items: getCart(),
1580
- coupon: normalizedCoupon
1581
- };
1551
+ function getStoredCartCode() {
1552
+ const raw = readCartStorage("coupon");
1553
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1554
+ return null;
1555
+ }
1556
+ const record2 = raw;
1557
+ const code = typeof record2.code === "string" ? normalizeCouponCode(record2.code) : null;
1558
+ const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1559
+ return code && source ? { code, source } : null;
1582
1560
  }
1583
- function getAmbientAffiliateCodeForQuote() {
1584
- const storedCode = getCartCoupon();
1585
- const storedSource = getCartCouponSource();
1586
- if (storedSource === "AFFILIATE" && storedCode) {
1587
- return storedCode.trim().toLowerCase();
1561
+ function normalizeCartItems(value) {
1562
+ if (!Array.isArray(value)) return [];
1563
+ const normalized = [];
1564
+ for (const entry of value) {
1565
+ if (!entry || typeof entry !== "object") continue;
1566
+ const record2 = entry;
1567
+ const productId = typeof record2.product_id === "string" ? record2.product_id.trim() : "";
1568
+ const variantId = typeof record2.variant_id === "string" ? record2.variant_id.trim() : "";
1569
+ const quantity = Number(record2.quantity);
1570
+ if (!productId || !variantId || !Number.isFinite(quantity) || quantity < 1) {
1571
+ continue;
1572
+ }
1573
+ const item = {
1574
+ line_id: "",
1575
+ product_id: productId,
1576
+ variant_id: variantId,
1577
+ quantity: Math.floor(quantity)
1578
+ };
1579
+ if (typeof record2.price_variant_id === "string") {
1580
+ item.price_variant_id = record2.price_variant_id;
1581
+ }
1582
+ if (record2.price_data && typeof record2.price_data === "object") {
1583
+ const priceData = record2.price_data;
1584
+ if (typeof priceData.unit_price === "number" && Number.isFinite(priceData.unit_price)) {
1585
+ item.price_data = { unit_price: priceData.unit_price };
1586
+ }
1587
+ }
1588
+ if (typeof record2.pay_what_you_want_price === "number" && Number.isFinite(record2.pay_what_you_want_price)) {
1589
+ item.pay_what_you_want_price = record2.pay_what_you_want_price;
1590
+ }
1591
+ if (Array.isArray(record2.addons)) {
1592
+ item.addons = record2.addons;
1593
+ }
1594
+ if (record2.custom_fields && typeof record2.custom_fields === "object" && !Array.isArray(record2.custom_fields)) {
1595
+ item.custom_fields = record2.custom_fields;
1596
+ }
1597
+ if (typeof record2.title === "string") {
1598
+ item.title = record2.title;
1599
+ }
1600
+ if (typeof record2.variant_title === "string") {
1601
+ item.variant_title = record2.variant_title;
1602
+ }
1603
+ if (typeof record2.image_url === "string") {
1604
+ item.image_url = record2.image_url;
1605
+ }
1606
+ if (Array.isArray(record2.addon_labels)) {
1607
+ item.addon_labels = record2.addon_labels.filter((label) => typeof label === "string");
1608
+ }
1609
+ if (typeof record2.max_quantity === "number" && Number.isFinite(record2.max_quantity)) {
1610
+ item.max_quantity = record2.max_quantity;
1611
+ }
1612
+ if (typeof record2.min_quantity === "number" && Number.isFinite(record2.min_quantity)) {
1613
+ item.min_quantity = record2.min_quantity;
1614
+ }
1615
+ clampQuantityToBounds(item);
1616
+ const storedLineId = typeof record2.line_id === "string" ? record2.line_id.trim() : "";
1617
+ item.line_id = storedLineId || computeCartLineId(item);
1618
+ normalized.push(ensureCartLineId(item));
1588
1619
  }
1589
- return getAffiliateCode();
1620
+ return normalized;
1590
1621
  }
1591
- var latestQuoteToken = null;
1592
- var quoteSequence = 0;
1593
- function getLatestQuoteToken() {
1594
- return latestQuoteToken;
1622
+ function getCartMetadata() {
1623
+ return readCartStorage("meta");
1595
1624
  }
1596
- function clearLatestQuoteToken() {
1597
- latestQuoteToken = null;
1598
- quoteSequence += 1;
1625
+ function getCartCoupon() {
1626
+ return getStoredCartCode()?.code ?? null;
1599
1627
  }
1600
- async function quoteCart(coupon, currency) {
1601
- const sequence = quoteSequence += 1;
1602
- const payload = getCartPayload(coupon);
1603
- const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
1604
- const response = await post("/v1/storefront/cart/quote", {
1605
- shop_slug: payload.store_slug,
1606
- cart: payload.items,
1607
- coupon: payload.coupon,
1608
- affiliate_code: getAmbientAffiliateCodeForQuote() ?? void 0,
1609
- currency: normalizedCurrency ?? void 0
1610
- });
1611
- if (sequence !== quoteSequence) return response;
1612
- if (response.success) {
1613
- latestQuoteToken = typeof response.data?.quote_token === "string" ? response.data.quote_token : null;
1628
+ function getCartCouponSource() {
1629
+ return getStoredCartCode()?.source ?? null;
1630
+ }
1631
+ function setCartCoupon(coupon, source = "COUPON") {
1632
+ const normalizedCoupon = normalizeCouponCode(coupon);
1633
+ if (!normalizedCoupon) {
1634
+ removeCartStorage("coupon");
1635
+ return null;
1614
1636
  }
1615
- return response;
1637
+ writeCartStorage("coupon", { code: normalizedCoupon, source });
1638
+ return normalizedCoupon;
1616
1639
  }
1617
-
1618
- // ../contracts/src/storefront-consent-choice.ts
1619
- var COOKIE_CONSENT_VERSION = 1;
1620
- var COOKIE_CONSENT_COOKIE = "shoppex_consent";
1621
- var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
1622
- var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
1623
- function parseCookieConsentValue(value) {
1624
- const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
1625
- return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
1640
+ function clearCartCoupon() {
1641
+ removeCartStorage("coupon");
1626
1642
  }
1627
-
1628
- // ../sdk/src/core/analytics-session.ts
1629
- var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
1630
- var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
1631
- var ANALYTICS_VISITS_MAX = 20;
1632
- var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
1633
- var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
1634
- var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
1635
- function readAnalyticsConsent() {
1636
- if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
1637
- const privacy = navigator;
1638
- if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
1639
- const prefix = `${COOKIE_CONSENT_COOKIE}=`;
1640
- let raw;
1641
- try {
1642
- raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1643
- } catch {
1644
- return "unknown";
1643
+ function writeCart(cart) {
1644
+ const normalizedCart = normalizeCartItems(cart);
1645
+ writeCartStorage("cart", normalizedCart);
1646
+ const now = Date.now();
1647
+ const previous = getCartMetadata();
1648
+ const nextMeta = {
1649
+ created_at: previous?.created_at ?? now,
1650
+ last_modified: now,
1651
+ version: (previous?.version ?? 0) + 1,
1652
+ checksum: computeChecksum(normalizedCart)
1653
+ };
1654
+ writeCartStorage("meta", nextMeta);
1655
+ if (normalizedCart.length === 0) {
1656
+ clearCartCoupon();
1645
1657
  }
1646
- const choice = parseCookieConsentValue(raw ?? null);
1647
- if (!choice) return "unknown";
1648
- return choice.analytics ? "granted" : "declined";
1649
1658
  }
1650
- function readVisits(storeSlug, now) {
1651
- const stored = getItem(analyticsVisitsKey(storeSlug));
1652
- if (!Array.isArray(stored)) return [];
1653
- return stored.filter((visit2) => typeof visit2?.id === "string" && typeof visit2.at === "number" && now - visit2.at < ANALYTICS_VISITS_WINDOW_MS);
1659
+ function setCartWithMetadata(cart, metadata) {
1660
+ const normalizedCart = normalizeCartItems(cart);
1661
+ writeCartStorage("cart", normalizedCart);
1662
+ const now = Date.now();
1663
+ const base = metadata ?? getCartMetadata();
1664
+ const nextMeta = {
1665
+ created_at: base?.created_at ?? now,
1666
+ last_modified: now,
1667
+ version: base?.version ?? 1,
1668
+ checksum: computeChecksum(normalizedCart)
1669
+ };
1670
+ writeCartStorage("meta", nextMeta);
1654
1671
  }
1655
- function forgetAnalyticsData(storeSlug) {
1656
- removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
1657
- removeItem(analyticsVisitsKey(storeSlug));
1672
+ function getCart() {
1673
+ const raw = readCartStorage("cart");
1674
+ return normalizeCartItems(raw);
1658
1675
  }
1659
- function analyticsSessionId(storeSlug, options, now = Date.now()) {
1660
- removeItem(`presence_connection_${storeSlug}`);
1661
- if (readAnalyticsConsent() !== "granted") {
1662
- forgetAnalyticsData(storeSlug);
1663
- return null;
1664
- }
1665
- const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
1666
- const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
1667
- if (!options.touch) return live;
1668
- if (live) {
1669
- setItem(analyticsSessionKey(storeSlug), { id: live, at: now }, "sessionStorage");
1670
- return live;
1671
- }
1672
- if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
1673
- const id = crypto.randomUUID();
1674
- setItem(analyticsSessionKey(storeSlug), { id, at: now }, "sessionStorage");
1675
- setItem(
1676
- analyticsVisitsKey(storeSlug),
1677
- [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
1676
+ function resolveCartLineId(productId, variantId, cart = getCart()) {
1677
+ const matches = cart.filter(
1678
+ (item) => item.product_id === productId && item.variant_id === variantId
1678
1679
  );
1679
- return id;
1680
- }
1681
- function checkoutAttribution(storeSlug, now = Date.now()) {
1682
- const consent = readAnalyticsConsent();
1683
- if (consent === "declined") {
1684
- forgetAnalyticsData(storeSlug);
1685
- return { declined: true };
1680
+ if (matches.length === 0) {
1681
+ throw new CartError(`No cart line found for ${productId}/${variantId}`);
1686
1682
  }
1687
- if (consent === "unknown") return void 0;
1688
- const sessionIds = readVisits(storeSlug, now).map((visit2) => visit2.id);
1689
- return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
1683
+ return matches[0].line_id;
1690
1684
  }
1691
- var engagedMs = 0;
1692
- var visibleSince = null;
1693
- var clockInstalled = false;
1694
- var pageViewed = false;
1695
- var pageSession = null;
1696
- function markPageViewed(sessionId) {
1697
- pageViewed = true;
1698
- pageSession = sessionId;
1685
+ function getCartItemCount() {
1686
+ const cart = getCart();
1687
+ return cart.reduce((sum, item) => sum + item.quantity, 0);
1699
1688
  }
1700
- function currentPageSession() {
1701
- return pageSession;
1689
+ function addToCart(productId, variantId, quantity = 1, options) {
1690
+ if (!productId || !variantId) {
1691
+ throw new CartError("product_id and variant_id are required");
1692
+ }
1693
+ const normalizedQuantity = normalizeQuantity(quantity);
1694
+ if (normalizedQuantity < 1) {
1695
+ throw new CartError("quantity must be at least 1");
1696
+ }
1697
+ const cart = getCart();
1698
+ const lineId = computeCartLineId({
1699
+ product_id: productId,
1700
+ variant_id: variantId,
1701
+ addons: options?.addons,
1702
+ custom_fields: options?.custom_fields,
1703
+ price_variant_id: options?.price_variant_id,
1704
+ price_data: options?.price_data,
1705
+ pay_what_you_want_price: options?.pay_what_you_want_price
1706
+ });
1707
+ const existingIndex = cart.findIndex((item) => item.line_id === lineId);
1708
+ const quantityBeforeAdd = existingIndex >= 0 ? cart[existingIndex].quantity : 0;
1709
+ if (existingIndex >= 0) {
1710
+ cart[existingIndex].quantity += normalizedQuantity;
1711
+ if (options?.addons) {
1712
+ cart[existingIndex].addons = options.addons;
1713
+ }
1714
+ if (options?.custom_fields) {
1715
+ cart[existingIndex].custom_fields = options.custom_fields;
1716
+ }
1717
+ if (options?.price_variant_id) {
1718
+ cart[existingIndex].price_variant_id = options.price_variant_id;
1719
+ }
1720
+ if (options?.price_data) {
1721
+ cart[existingIndex].price_data = options.price_data;
1722
+ }
1723
+ if (options?.pay_what_you_want_price !== void 0) {
1724
+ cart[existingIndex].pay_what_you_want_price = options.pay_what_you_want_price;
1725
+ }
1726
+ applyDisplaySnapshot(cart[existingIndex], options);
1727
+ clampQuantityToBounds(cart[existingIndex]);
1728
+ } else {
1729
+ const pushed = {
1730
+ line_id: lineId,
1731
+ product_id: productId,
1732
+ variant_id: variantId,
1733
+ quantity: normalizedQuantity,
1734
+ addons: options?.addons,
1735
+ custom_fields: options?.custom_fields,
1736
+ price_variant_id: options?.price_variant_id,
1737
+ price_data: options?.price_data,
1738
+ pay_what_you_want_price: options?.pay_what_you_want_price,
1739
+ title: options?.title,
1740
+ variant_title: options?.variant_title,
1741
+ image_url: options?.image_url,
1742
+ addon_labels: options?.addon_labels,
1743
+ max_quantity: options?.max_quantity,
1744
+ min_quantity: options?.min_quantity
1745
+ };
1746
+ clampQuantityToBounds(pushed);
1747
+ cart.push(pushed);
1748
+ }
1749
+ const intendedQuantity = (existingIndex >= 0 ? cart[existingIndex] : cart[cart.length - 1]).quantity;
1750
+ writeCart(cart);
1751
+ const persistedLine = getCart().find((item) => item.line_id === lineId);
1752
+ if (intendedQuantity > quantityBeforeAdd && persistedLine?.quantity === intendedQuantity) {
1753
+ const cartAffiliateCode = getCartCouponSource() === "AFFILIATE" ? getCartCoupon() : null;
1754
+ void trackAffiliateEvent("add_to_cart", cartAffiliateCode ? { code: cartAffiliateCode } : void 0);
1755
+ void trackCartAdd(productId, variantId, intendedQuantity - quantityBeforeAdd);
1756
+ }
1702
1757
  }
1703
- function isPageViewed() {
1704
- return pageViewed;
1758
+ function clampQuantityToBounds(item) {
1759
+ const min = item.min_quantity;
1760
+ if (typeof min === "number" && Number.isFinite(min) && item.quantity < min) {
1761
+ item.quantity = Math.floor(min);
1762
+ }
1763
+ const max = item.max_quantity;
1764
+ if (typeof max === "number" && Number.isFinite(max) && item.quantity > max) {
1765
+ item.quantity = Math.floor(max);
1766
+ }
1767
+ if (item.quantity < 1) {
1768
+ item.quantity = 1;
1769
+ }
1705
1770
  }
1706
- function settleClock(now) {
1707
- if (visibleSince !== null) {
1708
- engagedMs += now - visibleSince;
1709
- visibleSince = document.visibilityState === "visible" ? now : null;
1771
+ function applyDisplaySnapshot(item, options) {
1772
+ if (options?.replace_display_snapshot) {
1773
+ item.title = options.title;
1774
+ item.variant_title = options.variant_title;
1775
+ item.image_url = options.image_url;
1776
+ item.addon_labels = options.addon_labels;
1777
+ item.max_quantity = options.max_quantity;
1778
+ item.min_quantity = options.min_quantity;
1779
+ return;
1780
+ }
1781
+ if (options?.title !== void 0) {
1782
+ item.title = options.title;
1783
+ }
1784
+ if (options?.variant_title !== void 0) {
1785
+ item.variant_title = options.variant_title;
1786
+ }
1787
+ if (options?.image_url !== void 0) {
1788
+ item.image_url = options.image_url;
1789
+ }
1790
+ if (options?.addon_labels !== void 0) {
1791
+ item.addon_labels = options.addon_labels;
1792
+ }
1793
+ if (options?.max_quantity !== void 0) {
1794
+ item.max_quantity = options.max_quantity;
1795
+ }
1796
+ if (options?.min_quantity !== void 0) {
1797
+ item.min_quantity = options.min_quantity;
1710
1798
  }
1711
1799
  }
1712
- var consentListenerInstalled = false;
1713
- function onAnalyticsConsent(onYes) {
1714
- if (consentListenerInstalled || typeof window === "undefined") return;
1715
- consentListenerInstalled = true;
1716
- window.addEventListener("shoppex:consent", () => {
1717
- if (readAnalyticsConsent() === "granted") onYes();
1800
+ function setCartItem(productId, variantId, quantity = 1, options) {
1801
+ if (!productId || !variantId) {
1802
+ throw new CartError("product_id and variant_id are required");
1803
+ }
1804
+ const normalizedQuantity = normalizeQuantity(quantity);
1805
+ if (normalizedQuantity < 1) {
1806
+ throw new CartError("quantity must be at least 1");
1807
+ }
1808
+ const cart = getCart();
1809
+ const lineId = computeCartLineId({
1810
+ product_id: productId,
1811
+ variant_id: variantId,
1812
+ addons: options?.addons,
1813
+ custom_fields: options?.custom_fields,
1814
+ price_variant_id: options?.price_variant_id,
1815
+ price_data: options?.price_data,
1816
+ pay_what_you_want_price: options?.pay_what_you_want_price
1718
1817
  });
1719
- }
1720
- function installEngagedClock(onHidden) {
1721
- if (clockInstalled || typeof document === "undefined") return;
1722
- clockInstalled = true;
1723
- visibleSince = document.visibilityState === "visible" ? Date.now() : null;
1724
- document.addEventListener("visibilitychange", () => {
1725
- const now = Date.now();
1726
- if (document.visibilityState === "visible") {
1727
- visibleSince ?? (visibleSince = now);
1728
- return;
1818
+ for (let index = cart.length - 1; index >= 0; index -= 1) {
1819
+ if (cart[index].product_id === productId && cart[index].variant_id === variantId) {
1820
+ cart.splice(index, 1);
1729
1821
  }
1730
- settleClock(now);
1731
- visibleSince = null;
1732
- onHidden();
1733
- });
1734
- window.addEventListener("pagehide", () => {
1735
- settleClock(Date.now());
1736
- visibleSince = null;
1737
- onHidden();
1738
- });
1739
- window.addEventListener("pageshow", (event) => {
1740
- if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
1741
- });
1742
- }
1743
- function takeEngagedMs(now = Date.now()) {
1744
- settleClock(now);
1745
- const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
1746
- engagedMs = 0;
1747
- return taken;
1822
+ }
1823
+ const pushed = {
1824
+ line_id: lineId,
1825
+ product_id: productId,
1826
+ variant_id: variantId,
1827
+ quantity: normalizedQuantity,
1828
+ addons: options?.addons,
1829
+ custom_fields: options?.custom_fields,
1830
+ price_variant_id: options?.price_variant_id,
1831
+ price_data: options?.price_data,
1832
+ pay_what_you_want_price: options?.pay_what_you_want_price,
1833
+ title: options?.title,
1834
+ variant_title: options?.variant_title,
1835
+ image_url: options?.image_url,
1836
+ addon_labels: options?.addon_labels,
1837
+ max_quantity: options?.max_quantity,
1838
+ min_quantity: options?.min_quantity
1839
+ };
1840
+ clampQuantityToBounds(pushed);
1841
+ cart.push(pushed);
1842
+ writeCart(cart);
1843
+ const persistedLine = getCart().find((item) => item.line_id === lineId);
1844
+ if (persistedLine) void trackCartAdd(productId, variantId, persistedLine.quantity);
1748
1845
  }
1749
-
1750
- // ../sdk/src/core/attribution.ts
1751
- var STORAGE_PREFIX2 = "shoppex:utm:";
1752
- var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1753
- var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
1754
- var MAX_VALUE_LENGTH = 255;
1755
- function readFromUrl(search) {
1756
- let params;
1757
- try {
1758
- params = new URLSearchParams(search);
1759
- } catch {
1760
- return {};
1846
+ function updateCartItem(lineId, updates) {
1847
+ const cart = getCart();
1848
+ const normalizedLineId = lineId.trim();
1849
+ if (!normalizedLineId) {
1850
+ throw new CartError("line_id is required");
1761
1851
  }
1762
- const utm = {};
1763
- for (const key of UTM_KEYS) {
1764
- const value = params.get(key)?.trim();
1765
- if (value) {
1766
- utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1767
- }
1852
+ const index = cart.findIndex((item) => item.line_id === normalizedLineId);
1853
+ if (index < 0) {
1854
+ throw new CartError("Item not found in cart");
1768
1855
  }
1769
- return utm;
1770
- }
1771
- function storageKey(storeSlug) {
1772
- return `${STORAGE_PREFIX2}${storeSlug}`;
1773
- }
1774
- function readStored(storeSlug) {
1775
- try {
1776
- const raw = window.localStorage.getItem(storefrontStorageKey(storageKey(storeSlug)));
1777
- if (!raw) return {};
1778
- const parsed = JSON.parse(raw);
1779
- if (!parsed || typeof parsed.at !== "number" || typeof parsed.utm !== "object") return {};
1780
- if (Date.now() - parsed.at > TTL_MS) {
1781
- window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1782
- return {};
1783
- }
1784
- const utm = {};
1785
- for (const key of UTM_KEYS) {
1786
- const value = parsed.utm?.[key];
1787
- if (typeof value === "string" && value.length > 0) {
1788
- utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1789
- }
1856
+ const before = JSON.stringify(cart);
1857
+ if (updates.quantity !== void 0) {
1858
+ const normalizedQuantity = normalizeQuantity(updates.quantity);
1859
+ if (normalizedQuantity < 1) {
1860
+ cart.splice(index, 1);
1861
+ writeCart(cart);
1862
+ return;
1790
1863
  }
1791
- return utm;
1792
- } catch {
1793
- return {};
1864
+ cart[index].quantity = normalizedQuantity;
1794
1865
  }
1795
- }
1796
- function writeStored(storeSlug, utm) {
1797
- try {
1798
- const payload = { utm, at: Date.now() };
1799
- window.localStorage.setItem(storefrontStorageKey(storageKey(storeSlug)), JSON.stringify(payload));
1800
- } catch {
1866
+ if (updates.addons !== void 0) {
1867
+ cart[index].addons = updates.addons;
1801
1868
  }
1802
- }
1803
- function resolveUtmParameters(storeSlug) {
1804
- if (typeof window === "undefined" || typeof window.location === "undefined") {
1805
- return void 0;
1869
+ if (updates.custom_fields !== void 0) {
1870
+ cart[index].custom_fields = updates.custom_fields;
1806
1871
  }
1807
- const fromUrl = readFromUrl(window.location.search);
1808
- if (readAnalyticsConsent() !== "granted") {
1809
- try {
1810
- window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1811
- } catch {
1812
- }
1813
- return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1872
+ if (updates.price_variant_id !== void 0) {
1873
+ cart[index].price_variant_id = updates.price_variant_id;
1814
1874
  }
1815
- if (Object.keys(fromUrl).length > 0) {
1816
- writeStored(storeSlug, fromUrl);
1817
- return fromUrl;
1875
+ if (updates.price_data !== void 0) {
1876
+ cart[index].price_data = updates.price_data;
1818
1877
  }
1819
- const stored = readStored(storeSlug);
1820
- return Object.keys(stored).length > 0 ? stored : void 0;
1821
- }
1822
-
1823
- // ../sdk/src/core/page-path.ts
1824
- var MAX_PATH_LENGTH = 256;
1825
- function resolveStorefrontPagePath() {
1826
- if (typeof window === "undefined") return void 0;
1827
- let pathname;
1828
- try {
1829
- const location = window.location;
1830
- pathname = typeof location?.pathname === "string" && location.pathname ? location.pathname : new URL(String(location?.href ?? "")).pathname;
1831
- } catch {
1832
- return void 0;
1878
+ if (updates.pay_what_you_want_price !== void 0) {
1879
+ cart[index].pay_what_you_want_price = updates.pay_what_you_want_price;
1833
1880
  }
1834
- if (!pathname || !pathname.startsWith("/")) return void 0;
1835
- return pathname.slice(0, MAX_PATH_LENGTH);
1836
- }
1837
-
1838
- // ../sdk/src/core/page-tracker.ts
1839
- var PLATFORM_PAGE_TRACKER_FLAG = "__shoppexPageTracker";
1840
- function isPlatformPageTrackerActive() {
1841
- if (typeof window === "undefined") return false;
1842
- try {
1843
- return Boolean(window[PLATFORM_PAGE_TRACKER_FLAG]);
1844
- } catch {
1845
- return false;
1881
+ if (updates.variant_title !== void 0) {
1882
+ if (updates.variant_title === null) delete cart[index].variant_title;
1883
+ else cart[index].variant_title = updates.variant_title;
1846
1884
  }
1847
- }
1848
-
1849
- // ../sdk/src/modules/analytics.ts
1850
- var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
1851
- var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
1852
- var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
1853
- var CART_CLAIM_TTL_MS = 1e4;
1854
- function confirmedCartKey(storeSlug) {
1855
- return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
1856
- }
1857
- function inFlightCartKey(storeSlug) {
1858
- return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
1859
- }
1860
- function cartSequenceKey(storeSlug) {
1861
- return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
1862
- }
1863
- function cartSnapshot(cartValue, itemCount) {
1864
- return `${cartValue ?? ""}|${itemCount ?? ""}`;
1865
- }
1866
- function isCartSequence(value) {
1867
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1868
- }
1869
- var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
1870
- function readCartCounter(storeSlug) {
1871
- const counter = getItem(cartSequenceKey(storeSlug));
1872
- if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
1873
- return null;
1885
+ if (updates.max_quantity !== void 0) {
1886
+ if (updates.max_quantity === null) delete cart[index].max_quantity;
1887
+ else cart[index].max_quantity = updates.max_quantity;
1874
1888
  }
1875
- return counter;
1889
+ if (updates.min_quantity !== void 0) {
1890
+ if (updates.min_quantity === null) delete cart[index].min_quantity;
1891
+ else cart[index].min_quantity = updates.min_quantity;
1892
+ }
1893
+ clampQuantityToBounds(cart[index]);
1894
+ const nextLineId = computeCartLineId(cart[index]);
1895
+ const duplicateIndex = cart.findIndex((item, itemIndex) => itemIndex !== index && item.line_id === nextLineId);
1896
+ if (duplicateIndex >= 0) {
1897
+ cart[duplicateIndex].quantity += cart[index].quantity;
1898
+ clampQuantityToBounds(cart[duplicateIndex]);
1899
+ cart.splice(index, 1);
1900
+ } else {
1901
+ cart[index].line_id = nextLineId;
1902
+ }
1903
+ if (JSON.stringify(cart) === before) return;
1904
+ writeCart(cart);
1876
1905
  }
1877
- var lastCartOrder = null;
1878
- function nextCartOrder(storeSlug) {
1879
- const stored = readCartCounter(storeSlug);
1880
- const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
1881
- const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
1882
- const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
1883
- lastCartOrder = order2;
1884
- setItem(cartSequenceKey(storeSlug), order2);
1885
- return order2;
1906
+ function removeFromCart(lineId) {
1907
+ const normalizedLineId = lineId.trim();
1908
+ if (!normalizedLineId) {
1909
+ throw new CartError("line_id is required");
1910
+ }
1911
+ const cart = getCart();
1912
+ const filtered = cart.filter((item) => item.line_id !== normalizedLineId);
1913
+ writeCart(filtered);
1886
1914
  }
1887
- function cartSequenceLocks() {
1888
- if (typeof navigator === "undefined") return null;
1889
- const locks = navigator.locks;
1890
- return locks && typeof locks.request === "function" ? locks : null;
1915
+ function clearCart() {
1916
+ removeCartStorage("cart");
1917
+ removeCartStorage("meta");
1918
+ clearCartCoupon();
1919
+ clearLatestQuoteToken();
1891
1920
  }
1892
- function reserveCartOrder(storeSlug) {
1893
- const take = () => nextCartOrder(storeSlug);
1894
- const locks = cartSequenceLocks();
1895
- if (!locks) return Promise.resolve(take());
1896
- return locks.request(cartSequenceKey(storeSlug), take).catch(take);
1921
+ function createCartBackup() {
1922
+ const cart = getCart();
1923
+ writeCartStorage("cartBackup", cart);
1924
+ const cartCode = getStoredCartCode();
1925
+ if (cartCode) {
1926
+ writeCartStorage("couponBackup", cartCode);
1927
+ } else {
1928
+ removeCartStorage("couponBackup");
1929
+ }
1930
+ const metadata = getCartMetadata();
1931
+ if (metadata) {
1932
+ writeCartStorage("metaBackup", metadata);
1933
+ } else {
1934
+ const now = Date.now();
1935
+ writeCartStorage("metaBackup", {
1936
+ created_at: now,
1937
+ last_modified: now,
1938
+ version: 1,
1939
+ checksum: computeChecksum(cart)
1940
+ });
1941
+ }
1897
1942
  }
1898
- function readConfirmedCart(storeSlug) {
1899
- const confirmed = getItem(confirmedCartKey(storeSlug));
1900
- if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
1901
- return confirmed;
1943
+ function restoreCartFromBackup() {
1944
+ const backupRaw = readCartStorage("cartBackup");
1945
+ const backup = normalizeCartItems(backupRaw);
1946
+ const backupMeta = readCartStorage("metaBackup");
1947
+ const backupCoupon = readCartStorage("couponBackup");
1948
+ if (backup && backup.length > 0) {
1949
+ setCartWithMetadata(backup, backupMeta);
1950
+ if (backupCoupon && typeof backupCoupon === "object" && !Array.isArray(backupCoupon)) {
1951
+ const record2 = backupCoupon;
1952
+ const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1953
+ if (typeof record2.code === "string" && source) {
1954
+ setCartCoupon(record2.code, source);
1955
+ } else {
1956
+ clearCartCoupon();
1957
+ }
1958
+ } else {
1959
+ clearCartCoupon();
1960
+ }
1961
+ return true;
1962
+ }
1963
+ return false;
1902
1964
  }
1903
- function readInFlightCart(storeSlug) {
1904
- const claim2 = getItem(inFlightCartKey(storeSlug));
1905
- if (!claim2 || typeof claim2.snapshot !== "string" || !isCartSequence(claim2.seq) || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
1906
- return null;
1965
+ function mergeBaskets(items) {
1966
+ const cart = getCart();
1967
+ for (const incoming of items) {
1968
+ const productId = typeof incoming.product_id === "string" ? incoming.product_id.trim() : "";
1969
+ const variantId = typeof incoming.variant_id === "string" ? incoming.variant_id.trim() : "";
1970
+ if (!productId || !variantId) {
1971
+ continue;
1972
+ }
1973
+ let normalizedQuantity;
1974
+ try {
1975
+ normalizedQuantity = normalizeQuantity(incoming.quantity);
1976
+ } catch {
1977
+ continue;
1978
+ }
1979
+ if (normalizedQuantity < 1) {
1980
+ continue;
1981
+ }
1982
+ const mergedLine = ensureCartLineId({
1983
+ ...incoming,
1984
+ line_id: incoming.line_id ?? computeCartLineId(incoming),
1985
+ product_id: productId,
1986
+ variant_id: variantId,
1987
+ quantity: normalizedQuantity
1988
+ });
1989
+ const index = cart.findIndex((item) => item.line_id === mergedLine.line_id);
1990
+ if (index >= 0) {
1991
+ cart[index].quantity = Math.max(cart[index].quantity, normalizedQuantity);
1992
+ clampQuantityToBounds(cart[index]);
1993
+ } else {
1994
+ clampQuantityToBounds(mergedLine);
1995
+ cart.push(mergedLine);
1996
+ }
1907
1997
  }
1908
- return claim2;
1998
+ writeCart(cart);
1999
+ return cart;
1909
2000
  }
1910
- function isCartReportedOrInFlight(storeSlug, snapshot) {
1911
- const confirmed = readConfirmedCart(storeSlug);
1912
- const claim2 = readInFlightCart(storeSlug);
1913
- const liveClaim = claim2 && Date.now() - claim2.at < CART_CLAIM_TTL_MS ? claim2 : null;
1914
- const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
1915
- return expected?.snapshot === snapshot;
2001
+ function moveBasketItem(fromProductId, fromVariantId, toProductId, toVariantId) {
2002
+ const cart = getCart();
2003
+ const fromIndex = cart.findIndex(
2004
+ (item) => item.product_id === fromProductId && item.variant_id === fromVariantId
2005
+ );
2006
+ if (fromIndex < 0) {
2007
+ throw new CartError("Item not found in cart");
2008
+ }
2009
+ const [fromItem] = cart.splice(fromIndex, 1);
2010
+ const toLineId = computeCartLineId({
2011
+ ...fromItem,
2012
+ product_id: toProductId,
2013
+ variant_id: toVariantId
2014
+ });
2015
+ const toIndex = cart.findIndex((item) => item.line_id === toLineId);
2016
+ if (toIndex >= 0) {
2017
+ cart[toIndex].quantity += fromItem.quantity;
2018
+ clampQuantityToBounds(cart[toIndex]);
2019
+ } else {
2020
+ const moved = ensureCartLineId({
2021
+ ...fromItem,
2022
+ product_id: toProductId,
2023
+ variant_id: toVariantId,
2024
+ line_id: toLineId
2025
+ });
2026
+ clampQuantityToBounds(moved);
2027
+ cart.push(moved);
2028
+ }
2029
+ writeCart(cart);
1916
2030
  }
1917
- function createClaimToken() {
1918
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1919
- return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
2031
+ function validateCartIntegrity() {
2032
+ const cart = getCart();
2033
+ const metadata = getCartMetadata();
2034
+ const checksum = computeChecksum(cart);
2035
+ if (!metadata) {
2036
+ setCartWithMetadata(cart, null);
2037
+ return true;
2038
+ }
2039
+ return metadata.checksum === checksum;
1920
2040
  }
1921
- function claimCartReport(storeSlug, seq, cartValue, itemCount) {
1922
- const snapshot = cartSnapshot(cartValue, itemCount);
1923
- const token = createClaimToken();
1924
- setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
1925
- return (delivered) => {
1926
- if (delivered) {
1927
- const confirmed = readConfirmedCart(storeSlug);
1928
- if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
1929
- }
1930
- if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
2041
+ function getCartStats() {
2042
+ const cart = getCart();
2043
+ const integrityValid = validateCartIntegrity();
2044
+ const metadata = getCartMetadata();
2045
+ const backup = readCartStorage("cartBackup") ?? [];
2046
+ const hasCompletePriceSnapshots = cart.length > 0 && cart.every((item) => typeof item.price_data?.unit_price === "number");
2047
+ const totalPrice = cart.reduce((sum, item) => {
2048
+ const unitPrice = item.price_data?.unit_price ?? 0;
2049
+ return sum + roundPayableAmount(unitPrice * item.quantity);
2050
+ }, 0);
2051
+ return {
2052
+ item_count: cart.length,
2053
+ total_quantity: cart.reduce((sum, item) => sum + item.quantity, 0),
2054
+ last_modified: metadata?.last_modified ?? 0,
2055
+ version: metadata?.version ?? 0,
2056
+ has_backup: backup.length > 0,
2057
+ integrity_valid: integrityValid,
2058
+ total_price: roundPayableAmount(totalPrice),
2059
+ total_price_is_estimate: cart.length > 0 && !hasCompletePriceSnapshots
1931
2060
  };
1932
2061
  }
1933
- var cartSendTail = Promise.resolve();
1934
- var latestCartRequest = 0;
1935
- function enqueueCartSend(send) {
1936
- const request2 = ++latestCartRequest;
1937
- const run = () => send(() => request2 === latestCartRequest);
1938
- cartSendTail = cartSendTail.then(run, run);
1939
- return cartSendTail;
1940
- }
1941
- async function postPing(body) {
2062
+ function getCartPayload(coupon) {
1942
2063
  const config2 = getConfig();
1943
- const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1944
- let timer;
1945
- const timedOut = new Promise((resolve) => {
1946
- timer = setTimeout(() => {
1947
- controller?.abort();
1948
- resolve(false);
1949
- }, CART_CLAIM_TTL_MS);
1950
- });
1951
- try {
1952
- const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", {
1953
- storeSlug: config2.storeSlug
1954
- });
1955
- const delivered = fetch(`${config2.apiBaseUrl}${endpoint}`, {
1956
- method: "POST",
1957
- headers: { "Content-Type": "application/json" },
1958
- body: JSON.stringify(body),
1959
- signal: controller?.signal
1960
- }).then((response) => response.ok);
1961
- return await Promise.race([delivered, timedOut]);
1962
- } catch {
1963
- return false;
1964
- } finally {
1965
- clearTimeout(timer);
1966
- }
2064
+ const normalizedCoupon = normalizeCouponCode(coupon) ?? getCartCoupon() ?? void 0;
2065
+ return {
2066
+ store_slug: config2.storeSlug,
2067
+ items: getCart(),
2068
+ coupon: normalizedCoupon
2069
+ };
1967
2070
  }
1968
- async function trackPageView(cartValue, itemCount) {
1969
- if (!isInitialized()) return;
1970
- if (typeof document === "undefined") return;
1971
- const config2 = getConfig();
1972
- const carriesCart = cartValue !== void 0 || itemCount !== void 0;
1973
- const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
1974
- if (isPlatformPageTrackerActive()) {
1975
- if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
1976
- return;
2071
+ function getAmbientAffiliateCodeForQuote() {
2072
+ const storedCode = getCartCoupon();
2073
+ const storedSource = getCartCouponSource();
2074
+ if (storedSource === "AFFILIATE" && storedCode) {
2075
+ return storedCode.trim().toLowerCase();
1977
2076
  }
1978
- const sendView = async () => {
1979
- const order2 = reservedOrder ? await reservedOrder : void 0;
1980
- const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1981
- const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
1982
- const previousSession = currentPageSession();
1983
- if (previousSession && previousSession !== sessionId) reportPageLeave();
1984
- markPageViewed(sessionId);
1985
- if (sessionId) installEngagedClock(reportPageLeave);
1986
- onAnalyticsConsent(startSessionOnConsent);
1987
- const delivered = await postPing({
1988
- referer: document.referrer || void 0,
1989
- cart_value: cartValue,
1990
- item_count: itemCount,
1991
- cart_seq: order2?.seq,
1992
- cart_seq_scope: order2?.scope,
1993
- session_id: sessionId ?? void 0,
1994
- // Time on screen of the page before this one (client-side navigation).
1995
- engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1996
- path: resolveStorefrontPagePath(),
1997
- utm: resolveUtmParameters(config2.storeSlug)
1998
- });
1999
- settle2?.(delivered);
2000
- };
2001
- await (reservedOrder ? enqueueCartSend(sendView) : sendView());
2077
+ return getAffiliateCode();
2002
2078
  }
2003
- async function trackCartActivity(cartValue, itemCount) {
2004
- if (!isInitialized()) return;
2005
- if (typeof document === "undefined") return;
2006
- const snapshot = cartSnapshot(cartValue, itemCount);
2007
- const reservedOrder = reserveCartOrder(getConfig().storeSlug);
2008
- await enqueueCartSend(async (isLatest) => {
2009
- const order2 = await reservedOrder;
2010
- if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
2011
- await sendCartActivity(order2, cartValue, itemCount);
2012
- });
2079
+ var latestQuoteToken = null;
2080
+ var quoteSequence = 0;
2081
+ function getLatestQuoteToken() {
2082
+ return latestQuoteToken;
2013
2083
  }
2014
- async function sendCartActivity(order2, cartValue, itemCount) {
2015
- const config2 = getConfig();
2016
- const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
2017
- const delivered = await postPing({
2018
- event_type: "presence",
2019
- cart_value: cartValue,
2020
- item_count: itemCount,
2021
- cart_seq: order2.seq,
2022
- cart_seq_scope: order2.scope,
2023
- // A cart marks the analytics session as one that did not bounce.
2024
- session_id: analyticsSessionId(config2.storeSlug, { touch: false }) ?? void 0,
2025
- path: resolveStorefrontPagePath()
2026
- });
2027
- settle2(delivered);
2084
+ function clearLatestQuoteToken() {
2085
+ latestQuoteToken = null;
2086
+ quoteSequence += 1;
2028
2087
  }
2029
- function startSessionOnConsent() {
2030
- const config2 = getConfig();
2031
- if (!isPageViewed() || analyticsSessionId(config2.storeSlug, { touch: false })) return;
2032
- const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
2033
- if (!sessionId) return;
2034
- markPageViewed(sessionId);
2035
- installEngagedClock(reportPageLeave);
2036
- void postPing({
2037
- event_type: "session_start",
2038
- referer: document.referrer || void 0,
2039
- session_id: sessionId,
2040
- path: resolveStorefrontPagePath(),
2041
- utm: resolveUtmParameters(config2.storeSlug)
2088
+ async function quoteCart(coupon, currency) {
2089
+ const sequence = quoteSequence += 1;
2090
+ const payload = getCartPayload(coupon);
2091
+ const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
2092
+ const response = await post("/v1/storefront/cart/quote", {
2093
+ shop_slug: payload.store_slug,
2094
+ cart: payload.items,
2095
+ coupon: payload.coupon,
2096
+ affiliate_code: getAmbientAffiliateCodeForQuote() ?? void 0,
2097
+ currency: normalizedCurrency ?? void 0
2042
2098
  });
2043
- }
2044
- function reportPageLeave() {
2045
- const config2 = getConfig();
2046
- const sessionId = currentPageSession();
2047
- const engagedMs2 = takeEngagedMs();
2048
- if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
2049
- try {
2050
- const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config2.storeSlug });
2051
- void fetch(`${config2.apiBaseUrl}${endpoint}`, {
2052
- method: "POST",
2053
- headers: { "Content-Type": "application/json" },
2054
- body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
2055
- keepalive: true
2056
- }).catch(() => {
2057
- });
2058
- } catch {
2099
+ if (sequence !== quoteSequence) return response;
2100
+ if (response.success) {
2101
+ latestQuoteToken = typeof response.data?.quote_token === "string" ? response.data.quote_token : null;
2059
2102
  }
2103
+ return response;
2060
2104
  }
2061
2105
 
2062
2106
  // ../sdk/src/storefront-storage-sdk.ts
@@ -2102,7 +2146,8 @@ var storefrontStorageSdk = {
2102
2146
  trackAffiliateEvent,
2103
2147
  // Analytics
2104
2148
  trackPageView,
2105
- trackCartActivity
2149
+ trackCartActivity,
2150
+ trackProductView
2106
2151
  };
2107
2152
 
2108
2153
  // ../../node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
@@ -2911,15 +2956,21 @@ async function getCategories() {
2911
2956
  }
2912
2957
 
2913
2958
  // ../contracts/src/redirect-link-template.ts
2914
- var REMAINING_PLACEHOLDER_PATTERNS = [
2915
- /\{\{[\s\S]*?\}\}/g,
2916
- /(?<!\{)\{[^{}]+\}(?!\})/g
2917
- ];
2959
+ function isInsideDoubleBrace(raw) {
2960
+ return raw.startsWith("{{");
2961
+ }
2962
+ function placeholderMatches(template, patterns) {
2963
+ return [
2964
+ ...template.matchAll(patterns.double),
2965
+ ...[...template.matchAll(patterns.single)].filter((match) => !isInsideDoubleBrace(match[0]))
2966
+ ];
2967
+ }
2968
+ var REMAINING_PLACEHOLDER_PATTERNS = {
2969
+ double: /\{\{[\s\S]*?\}\}/g,
2970
+ single: /\{?\{[^{}]+\}(?!\})/g
2971
+ };
2918
2972
  function findDanglingBraceFragments(template) {
2919
- let stripped = template;
2920
- for (const pattern of REMAINING_PLACEHOLDER_PATTERNS) {
2921
- stripped = stripped.replace(pattern, "");
2922
- }
2973
+ const stripped = template.replace(REMAINING_PLACEHOLDER_PATTERNS.double, "").replace(REMAINING_PLACEHOLDER_PATTERNS.single, (raw) => isInsideDoubleBrace(raw) ? raw : "");
2923
2974
  return /[{}]/.test(stripped) ? [...new Set(stripped.match(/[{}]+/g) ?? [])] : [];
2924
2975
  }
2925
2976
  function findRemainingProductRedirectPlaceholders(template) {
@@ -2927,10 +2978,8 @@ function findRemainingProductRedirectPlaceholders(template) {
2927
2978
  return [];
2928
2979
  }
2929
2980
  const remaining = /* @__PURE__ */ new Set();
2930
- for (const pattern of REMAINING_PLACEHOLDER_PATTERNS) {
2931
- for (const match of template.matchAll(pattern)) {
2932
- remaining.add(match[0]);
2933
- }
2981
+ for (const match of placeholderMatches(template, REMAINING_PLACEHOLDER_PATTERNS)) {
2982
+ remaining.add(match[0]);
2934
2983
  }
2935
2984
  for (const fragment of findDanglingBraceFragments(template)) {
2936
2985
  remaining.add(fragment);
@@ -24349,6 +24398,27 @@ var customerPortalAffiliateLedgerStatusSchema = external_exports.enum([
24349
24398
  "split",
24350
24399
  "voided"
24351
24400
  ]);
24401
+ var customerPortalAffiliateCommissionBreakdownSchema = external_exports.object({
24402
+ lines: external_exports.array(external_exports.object({
24403
+ product_id: external_exports.string().nullable(),
24404
+ title: external_exports.string(),
24405
+ base_usd: external_exports.string(),
24406
+ percent: external_exports.number(),
24407
+ source: external_exports.enum(["PRODUCT_DISABLED", "LINK", "AFFILIATE", "TIER", "PRODUCT", "SHOP_DEFAULT"]),
24408
+ amount_usd: external_exports.string()
24409
+ })),
24410
+ base_usd: external_exports.string(),
24411
+ gross_usd: external_exports.string(),
24412
+ cap_usd: nullableStringSchema,
24413
+ cap_applied: external_exports.boolean(),
24414
+ minimum_order_usd: nullableStringSchema,
24415
+ minimum_met: external_exports.boolean(),
24416
+ shareback_usd: external_exports.string(),
24417
+ amount_usd: external_exports.string(),
24418
+ tier_name: nullableStringSchema,
24419
+ attribution_source: external_exports.enum(["CODE", "BINDING"]),
24420
+ bound_at: nullableStringSchema
24421
+ });
24352
24422
  var customerPortalAffiliateSummarySchema = external_exports.object({
24353
24423
  customer: external_exports.object({
24354
24424
  id: external_exports.string(),
@@ -24368,6 +24438,9 @@ var customerPortalAffiliateSummarySchema = external_exports.object({
24368
24438
  address: external_exports.string()
24369
24439
  }).nullable(),
24370
24440
  balance_currency: external_exports.string(),
24441
+ // The shop's IANA zone; `bound_at` is a day on its calendar.
24442
+ // Optional until the backend that sends it is deployed.
24443
+ time_zone: external_exports.string().optional(),
24371
24444
  balances: external_exports.object({
24372
24445
  pending: external_exports.string(),
24373
24446
  available: external_exports.string(),
@@ -24391,6 +24464,7 @@ var customerPortalAffiliateSummarySchema = external_exports.object({
24391
24464
  store_credit_amount: external_exports.string(),
24392
24465
  base_amount: external_exports.string(),
24393
24466
  discount_amount: external_exports.string(),
24467
+ breakdown: customerPortalAffiliateCommissionBreakdownSchema.nullable(),
24394
24468
  status: external_exports.string(),
24395
24469
  created_at: nullableStringSchema,
24396
24470
  paid_at: nullableStringSchema,
@@ -24457,7 +24531,10 @@ var customerPortalSubscriptionSummarySchema = external_exports.object({
24457
24531
  can_cancel: external_exports.boolean(),
24458
24532
  can_undo_cancel: external_exports.boolean(),
24459
24533
  can_manage_payment_method: external_exports.boolean()
24460
- })
24534
+ }),
24535
+ // The open renewal's hosted checkout ("Pay now"). Optional until the backend
24536
+ // that sends it is deployed: a storefront script can reach buyers first.
24537
+ renewal_checkout_url: nullableStringSchema.optional()
24461
24538
  });
24462
24539
  var customerLoyaltySchema = external_exports.object({
24463
24540
  enabled: external_exports.boolean(),
@@ -24566,7 +24643,17 @@ var customerPortalDashboardSchema = external_exports.object({
24566
24643
  label: external_exports.string(),
24567
24644
  value: external_exports.string(),
24568
24645
  href: nullableStringSchema,
24569
- created_at: external_exports.string()
24646
+ created_at: external_exports.string(),
24647
+ // Files only: downloads left, end date and why a file closed (BuyerFileAccess).
24648
+ // Optional until the backend that sends it is deployed.
24649
+ access: external_exports.object({
24650
+ state: external_exports.enum(["open", "limit_reached", "expired", "revoked", "refunded"]),
24651
+ downloads_left: external_exports.number().nullable(),
24652
+ download_limit: external_exports.number().nullable(),
24653
+ downloads_used: external_exports.number(),
24654
+ ends_at: nullableStringSchema,
24655
+ time_zone: external_exports.string()
24656
+ }).nullable().optional()
24570
24657
  })),
24571
24658
  tickets: external_exports.array(external_exports.object({
24572
24659
  uniqid: external_exports.string(),
@@ -24576,6 +24663,22 @@ var customerPortalDashboardSchema = external_exports.object({
24576
24663
  created_at: nullableStringSchema
24577
24664
  }))
24578
24665
  });
24666
+ var customerPortalOrderReviewSchema = external_exports.object({
24667
+ can_review: external_exports.boolean(),
24668
+ already_submitted: external_exports.boolean(),
24669
+ feedback_uniqid: nullableStringSchema,
24670
+ product_title: nullableStringSchema,
24671
+ score: external_exports.union([external_exports.literal(1), external_exports.literal(3), external_exports.literal(5)]).nullable(),
24672
+ is_automated: external_exports.boolean(),
24673
+ message: nullableStringSchema,
24674
+ submitted_at: nullableStringSchema,
24675
+ reviewer_name: nullableStringSchema,
24676
+ anonymous: external_exports.boolean(),
24677
+ // The hosted review on the checkout host, null unless `can_review`. A storefront
24678
+ // account links here and never posts (#1152). Optional until the backend that
24679
+ // sends it is deployed.
24680
+ review_url: nullableStringSchema.optional()
24681
+ });
24579
24682
  var customerPortalNotificationPreferencesSchema = external_exports.object({
24580
24683
  order_created_emails: external_exports.boolean(),
24581
24684
  order_completed_emails: external_exports.boolean(),
@@ -24722,6 +24825,9 @@ function order(id) {
24722
24825
  schema: customerPortalInvoiceDetailSchema
24723
24826
  });
24724
24827
  }
24828
+ function orderReview(uniqid) {
24829
+ return requestCustomer(`/invoices/${encodeURIComponent(uniqid)}/review`, { schema: customerPortalOrderReviewSchema });
24830
+ }
24725
24831
  async function invoicePdf(id) {
24726
24832
  try {
24727
24833
  const response = await fetch(`${CUSTOMER_API_PREFIX}/invoices/${encodeURIComponent(id)}/pdf`, {
@@ -24854,19 +24960,29 @@ function createTicket(payload) {
24854
24960
  body: {
24855
24961
  ...payload.title !== void 0 ? { title: payload.title } : {},
24856
24962
  message: payload.message,
24857
- ...payload.invoice_id !== void 0 ? { invoice_id: payload.invoice_id } : {}
24963
+ ...payload.invoice_id !== void 0 ? { invoice_id: payload.invoice_id } : {},
24964
+ ...payload.attachment_ids?.length ? { attachment_ids: payload.attachment_ids } : {}
24858
24965
  }
24859
24966
  });
24860
24967
  }
24861
24968
  function ticket(uniqid) {
24862
24969
  return requestCustomer(`/tickets/${encodeURIComponent(uniqid)}`);
24863
24970
  }
24864
- function replyToTicket(uniqid, message) {
24971
+ function replyToTicket(uniqid, message, attachmentIds = []) {
24865
24972
  return requestCustomer(`/tickets/${encodeURIComponent(uniqid)}/reply`, {
24866
24973
  method: "POST",
24867
- body: { message }
24974
+ body: attachmentIds.length > 0 ? { message, attachment_ids: attachmentIds } : { message }
24868
24975
  });
24869
24976
  }
24977
+ function uploadTicketImage(file2, ticket2) {
24978
+ const form = new FormData();
24979
+ form.append("file", file2);
24980
+ if (ticket2) form.append("ticket", ticket2);
24981
+ return requestCustomer("/ticket-attachments", { method: "POST", formData: form });
24982
+ }
24983
+ function discardTicketImage(id) {
24984
+ return requestCustomer(`/ticket-attachments/${encodeURIComponent(id)}`, { method: "DELETE" });
24985
+ }
24870
24986
  function updateProfile(patch) {
24871
24987
  return requestCustomer("/profile", {
24872
24988
  method: "POST",
@@ -24977,6 +25093,7 @@ var shoppex = {
24977
25093
  redeemLoyaltyPoints,
24978
25094
  warranties,
24979
25095
  claimWarranty,
25096
+ orderReview,
24980
25097
  resetLicenseHwid,
24981
25098
  subscriptionBillingHistory,
24982
25099
  cancelSubscription,
@@ -24992,6 +25109,8 @@ var shoppex = {
24992
25109
  createTicket,
24993
25110
  ticket,
24994
25111
  replyToTicket,
25112
+ uploadTicketImage,
25113
+ discardTicketImage,
24995
25114
  updateProfile,
24996
25115
  updateAvatar,
24997
25116
  removeAvatar,
@@ -25049,6 +25168,7 @@ if (typeof window !== "undefined") {
25049
25168
  createAffiliateLink,
25050
25169
  createTicket,
25051
25170
  dashboard,
25171
+ discardTicketImage,
25052
25172
  emailPreferences,
25053
25173
  enrollAsReseller,
25054
25174
  ensureCartLineId,
@@ -25080,6 +25200,7 @@ if (typeof window !== "undefined") {
25080
25200
  normalizeStorefrontCustomFields,
25081
25201
  notificationPreferences,
25082
25202
  order,
25203
+ orderReview,
25083
25204
  orders,
25084
25205
  pauseSubscription,
25085
25206
  productMatchesSearchQuery,
@@ -25116,11 +25237,13 @@ if (typeof window !== "undefined") {
25116
25237
  touchStorefrontPresence,
25117
25238
  trackCartActivity,
25118
25239
  trackPageView,
25240
+ trackProductView,
25119
25241
  undoSubscriptionCancel,
25120
25242
  updateAvatar,
25121
25243
  updateEmailPreferences,
25122
25244
  updateNotificationPreferences,
25123
25245
  updateProfile,
25246
+ uploadTicketImage,
25124
25247
  validateStorefrontCustomFieldValue,
25125
25248
  verifyOtp,
25126
25249
  warranties