@shoppexio/storefront 1.0.102 → 1.0.104

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,
@@ -115,11 +116,13 @@ __export(src_exports, {
115
116
  touchStorefrontPresence: () => touchStorefrontPresence,
116
117
  trackCartActivity: () => trackCartActivity,
117
118
  trackPageView: () => trackPageView,
119
+ trackProductView: () => trackProductView,
118
120
  undoSubscriptionCancel: () => undoSubscriptionCancel,
119
121
  updateAvatar: () => updateAvatar,
120
122
  updateEmailPreferences: () => updateEmailPreferences,
121
123
  updateNotificationPreferences: () => updateNotificationPreferences,
122
124
  updateProfile: () => updateProfile,
125
+ uploadTicketImage: () => uploadTicketImage,
123
126
  validateStorefrontCustomFieldValue: () => validateStorefrontCustomFieldValue,
124
127
  verifyOtp: () => verifyOtp,
125
128
  warranties: () => warranties
@@ -399,27 +402,9 @@ function buildEndpoint(template, params) {
399
402
  }
400
403
 
401
404
  // ../sdk/src/core/telemetry.ts
402
- var CONNECTION_ID_STORAGE_PREFIX = "presence_connection_";
403
405
  var CLIENT_ERROR_DEDUPE_WINDOW_MS = 15e3;
404
406
  var CLIENT_ERROR_TEXT_MAX_LENGTH = 500;
405
407
  var CLIENT_ERROR_URL_MAX_LENGTH = 2e3;
406
- function createPresenceConnectionId() {
407
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
408
- return crypto.randomUUID();
409
- }
410
- return `spx_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
411
- }
412
- function getStorefrontConnectionId(storeSlug) {
413
- if (typeof window === "undefined") return null;
414
- const storageKey2 = `${CONNECTION_ID_STORAGE_PREFIX}${storeSlug}`;
415
- const existing = getItem(storageKey2);
416
- if (existing && existing.trim()) {
417
- return existing;
418
- }
419
- const nextId = createPresenceConnectionId();
420
- setItem(storageKey2, nextId);
421
- return nextId;
422
- }
423
408
  function getRecentClientErrorKey(payload) {
424
409
  return [
425
410
  payload.method,
@@ -449,7 +434,6 @@ async function reportStorefrontClientError(payload) {
449
434
  const body = JSON.stringify({
450
435
  event_type: "client_error",
451
436
  referer: document.referrer || void 0,
452
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
453
437
  client_error: {
454
438
  source: payload.source ?? "sdk",
455
439
  phase: payload.phase ?? "request",
@@ -981,266 +965,747 @@ async function captureAffiliateFromUrl(param = "ref") {
981
965
  return holdsOtherCode ? held.code : code;
982
966
  }
983
967
 
984
- // ../sdk/src/utils/cart-line-id.ts
985
- function hashString(value) {
986
- let hash2 = 2166136261;
987
- for (let i = 0; i < value.length; i += 1) {
988
- hash2 ^= value.charCodeAt(i);
989
- hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
968
+ // ../contracts/src/storefront-consent-choice.ts
969
+ var COOKIE_CONSENT_VERSION = 1;
970
+ var COOKIE_CONSENT_COOKIE = "shoppex_consent";
971
+ var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
972
+ var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
973
+ function parseCookieConsentValue(value) {
974
+ const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
975
+ return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
976
+ }
977
+
978
+ // ../sdk/src/core/analytics-session.ts
979
+ var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
980
+ var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
981
+ var ANALYTICS_VISITS_MAX = 20;
982
+ var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
983
+ var UTM_STORAGE_PREFIX = "shoppex:utm:";
984
+ var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
985
+ var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
986
+ function pageLanding() {
987
+ let r = "";
988
+ try {
989
+ r = document.referrer ? new URL(document.referrer).hostname : "";
990
+ } catch {
990
991
  }
991
- return (hash2 >>> 0).toString(16);
992
+ const params = new URLSearchParams(window.location?.search ?? "");
993
+ const u = ["source", "medium", "campaign"].map((key) => params.get(`utm_${key}`)?.trim().toLowerCase() ?? "").join("|");
994
+ return { r: r === window.location?.hostname ? "" : r, u: u === "||" ? "" : u };
992
995
  }
993
- function normalizeAddons(addons) {
994
- if (!addons?.length) return [];
995
- return [...addons].map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 })).sort((a, b) => a.id.localeCompare(b.id));
996
+ function readAnalyticsConsent() {
997
+ if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
998
+ const privacy = navigator;
999
+ if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
1000
+ const prefix = `${COOKIE_CONSENT_COOKIE}=`;
1001
+ let raw;
1002
+ try {
1003
+ raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1004
+ } catch {
1005
+ return "unknown";
1006
+ }
1007
+ const choice = parseCookieConsentValue(raw ?? null);
1008
+ if (!choice) return "unknown";
1009
+ return choice.analytics ? "granted" : "declined";
996
1010
  }
997
- function normalizeCustomFields(fields) {
998
- if (!fields) return {};
999
- const entries = Object.entries(fields).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b));
1000
- return Object.fromEntries(entries);
1011
+ function readVisits(storeSlug, now) {
1012
+ const stored = getItem(analyticsVisitsKey(storeSlug));
1013
+ if (!Array.isArray(stored)) return [];
1014
+ return stored.filter((visit2) => typeof visit2?.id === "string" && typeof visit2.at === "number" && now - visit2.at < ANALYTICS_VISITS_WINDOW_MS);
1001
1015
  }
1002
- function computeCartLineId(input2) {
1003
- const payload = {
1004
- product_id: input2.product_id,
1005
- variant_id: input2.variant_id,
1006
- price_variant_id: input2.price_variant_id ?? null,
1007
- addons: normalizeAddons(input2.addons),
1008
- custom_fields: normalizeCustomFields(input2.custom_fields),
1009
- unit_price: typeof input2.price_data?.unit_price === "number" && Number.isFinite(input2.price_data.unit_price) ? input2.price_data.unit_price : null,
1010
- 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
1011
- };
1012
- return hashString(JSON.stringify(payload));
1016
+ function forgetAnalyticsData(storeSlug) {
1017
+ removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
1018
+ removeItem(analyticsVisitsKey(storeSlug));
1013
1019
  }
1014
- function ensureCartLineId(item) {
1015
- if (typeof item.line_id === "string" && item.line_id.trim()) {
1016
- return item;
1020
+ function analyticsSessionId(storeSlug, options, now = Date.now()) {
1021
+ removeItem(`presence_connection_${storeSlug}`);
1022
+ if (readAnalyticsConsent() !== "granted") {
1023
+ forgetAnalyticsData(storeSlug);
1024
+ return null;
1017
1025
  }
1018
- return { ...item, line_id: computeCartLineId(item) };
1026
+ const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
1027
+ const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
1028
+ if (!options.touch) return live;
1029
+ const landing = pageLanding();
1030
+ const newLanding = landing.r && landing.r !== stored?.r || landing.u && landing.u !== stored?.u;
1031
+ if (live && !newLanding) {
1032
+ setItem(analyticsSessionKey(storeSlug), { ...stored, id: live, at: now }, "sessionStorage");
1033
+ return live;
1034
+ }
1035
+ if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
1036
+ if (landing.r) {
1037
+ try {
1038
+ globalThis.localStorage.removeItem(storefrontStorageKey(`${UTM_STORAGE_PREFIX}${storeSlug}`));
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ const id = crypto.randomUUID();
1043
+ setItem(analyticsSessionKey(storeSlug), { id, at: now, ...landing }, "sessionStorage");
1044
+ setItem(
1045
+ analyticsVisitsKey(storeSlug),
1046
+ [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
1047
+ );
1048
+ return id;
1049
+ }
1050
+ function checkoutAttribution(storeSlug, now = Date.now()) {
1051
+ const consent = readAnalyticsConsent();
1052
+ if (consent === "declined") {
1053
+ forgetAnalyticsData(storeSlug);
1054
+ return { declined: true };
1055
+ }
1056
+ if (consent === "unknown") return void 0;
1057
+ const sessionIds = readVisits(storeSlug, now).map((visit2) => visit2.id);
1058
+ return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
1059
+ }
1060
+ var engagedMs = 0;
1061
+ var visibleSince = null;
1062
+ var clockInstalled = false;
1063
+ var pageViewed = false;
1064
+ var pageSession = null;
1065
+ function markPageViewed(sessionId) {
1066
+ pageViewed = true;
1067
+ pageSession = sessionId;
1068
+ }
1069
+ function currentPageSession() {
1070
+ return pageSession;
1071
+ }
1072
+ function isPageViewed() {
1073
+ return pageViewed;
1074
+ }
1075
+ function settleClock(now) {
1076
+ if (visibleSince !== null) {
1077
+ engagedMs += now - visibleSince;
1078
+ visibleSince = document.visibilityState === "visible" ? now : null;
1079
+ }
1080
+ }
1081
+ var consentListenerInstalled = false;
1082
+ function onAnalyticsConsent(onYes) {
1083
+ if (consentListenerInstalled || typeof window === "undefined") return;
1084
+ consentListenerInstalled = true;
1085
+ window.addEventListener("shoppex:consent", () => {
1086
+ if (readAnalyticsConsent() === "granted") onYes();
1087
+ });
1019
1088
  }
1020
-
1021
- // ../contracts/src/catalog-unit-price.ts
1022
- var CATALOG_UNIT_PRICE_DECIMAL_PLACES = 3;
1023
- var PAYABLE_AMOUNT_DECIMAL_PLACES = 2;
1024
- var CATALOG_UNIT_PRICE_FORMAT_OPTIONS = {
1025
- maximumFractionDigits: CATALOG_UNIT_PRICE_DECIMAL_PLACES
1026
- };
1027
- function roundPayableAmount(value) {
1028
- if (!Number.isFinite(value)) return value;
1029
- const sign = value < 0 ? -1 : 1;
1030
- const shifted = shiftDecimal(Math.abs(value), PAYABLE_AMOUNT_DECIMAL_PLACES);
1031
- const rounded = Math.round(shifted);
1032
- if (rounded === 0) return 0;
1033
- return sign * shiftDecimal(rounded, -PAYABLE_AMOUNT_DECIMAL_PLACES);
1089
+ function installEngagedClock(onHidden) {
1090
+ if (clockInstalled || typeof document === "undefined") return;
1091
+ clockInstalled = true;
1092
+ visibleSince = document.visibilityState === "visible" ? Date.now() : null;
1093
+ document.addEventListener("visibilitychange", () => {
1094
+ const now = Date.now();
1095
+ if (document.visibilityState === "visible") {
1096
+ visibleSince ?? (visibleSince = now);
1097
+ return;
1098
+ }
1099
+ settleClock(now);
1100
+ visibleSince = null;
1101
+ onHidden();
1102
+ });
1103
+ window.addEventListener("pagehide", () => {
1104
+ settleClock(Date.now());
1105
+ visibleSince = null;
1106
+ onHidden();
1107
+ });
1108
+ window.addEventListener("pageshow", (event) => {
1109
+ if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
1110
+ });
1034
1111
  }
1035
- function shiftDecimal(value, places) {
1036
- const [coefficient, currentExponent = "0"] = String(value).split("e");
1037
- return Number(`${coefficient}e${Number(currentExponent) + places}`);
1112
+ function takeEngagedMs(now = Date.now()) {
1113
+ settleClock(now);
1114
+ const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
1115
+ engagedMs = 0;
1116
+ return taken;
1038
1117
  }
1039
1118
 
1040
- // ../sdk/src/modules/cart.ts
1041
- var STORAGE_KEYS = {
1042
- cart: "cart",
1043
- cartBackup: "cart_backup",
1044
- meta: "cart_meta",
1045
- metaBackup: "cart_backup_meta",
1046
- coupon: "cart_coupon",
1047
- couponBackup: "cart_coupon_backup"
1048
- };
1049
- function getStorageKey(type) {
1050
- return `${STORAGE_KEYS[type]}_${getConfig().storeSlug}`;
1051
- }
1052
- function cartStorageArea() {
1053
- return getConfig().preview ? "sessionStorage" : "localStorage";
1119
+ // ../sdk/src/core/attribution.ts
1120
+ var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1121
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
1122
+ var MAX_VALUE_LENGTH = 255;
1123
+ function readFromUrl(search) {
1124
+ let params;
1125
+ try {
1126
+ params = new URLSearchParams(search);
1127
+ } catch {
1128
+ return {};
1129
+ }
1130
+ const utm = {};
1131
+ for (const key of UTM_KEYS) {
1132
+ const value = params.get(key)?.trim();
1133
+ if (value) {
1134
+ utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1135
+ }
1136
+ }
1137
+ return utm;
1054
1138
  }
1055
- function readCartStorage(type) {
1056
- return getItem(getStorageKey(type), cartStorageArea());
1139
+ function storageKey(storeSlug) {
1140
+ return `${UTM_STORAGE_PREFIX}${storeSlug}`;
1057
1141
  }
1058
- function writeCartStorage(type, value) {
1059
- setItem(getStorageKey(type), value, cartStorageArea());
1142
+ function readStored(storeSlug) {
1143
+ try {
1144
+ const raw = window.localStorage.getItem(storefrontStorageKey(storageKey(storeSlug)));
1145
+ if (!raw) return {};
1146
+ const parsed = JSON.parse(raw);
1147
+ if (!parsed || typeof parsed.at !== "number" || typeof parsed.utm !== "object") return {};
1148
+ if (Date.now() - parsed.at > TTL_MS) {
1149
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1150
+ return {};
1151
+ }
1152
+ const utm = {};
1153
+ for (const key of UTM_KEYS) {
1154
+ const value = parsed.utm?.[key];
1155
+ if (typeof value === "string" && value.length > 0) {
1156
+ utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1157
+ }
1158
+ }
1159
+ return utm;
1160
+ } catch {
1161
+ return {};
1162
+ }
1060
1163
  }
1061
- function removeCartStorage(type) {
1062
- removeItem(getStorageKey(type), cartStorageArea());
1164
+ function writeStored(storeSlug, utm) {
1165
+ try {
1166
+ const payload = { utm, at: Date.now() };
1167
+ window.localStorage.setItem(storefrontStorageKey(storageKey(storeSlug)), JSON.stringify(payload));
1168
+ } catch {
1169
+ }
1063
1170
  }
1064
- function hashString2(value) {
1065
- let hash2 = 2166136261;
1066
- for (let i = 0; i < value.length; i += 1) {
1067
- hash2 ^= value.charCodeAt(i);
1068
- hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
1171
+ function resolveUtmParameters(storeSlug) {
1172
+ if (typeof window === "undefined" || typeof window.location === "undefined") {
1173
+ return void 0;
1069
1174
  }
1070
- return (hash2 >>> 0).toString(16);
1175
+ const fromUrl = readFromUrl(window.location.search);
1176
+ if (readAnalyticsConsent() !== "granted") {
1177
+ try {
1178
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1179
+ } catch {
1180
+ }
1181
+ return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1182
+ }
1183
+ if (Object.keys(fromUrl).length > 0) {
1184
+ writeStored(storeSlug, fromUrl);
1185
+ return fromUrl;
1186
+ }
1187
+ const stored = readStored(storeSlug);
1188
+ return Object.keys(stored).length > 0 ? stored : void 0;
1071
1189
  }
1072
- function computeChecksum(cart) {
1073
- return hashString2(JSON.stringify(cart));
1190
+
1191
+ // ../sdk/src/core/page-path.ts
1192
+ var MAX_PATH_LENGTH = 256;
1193
+ function resolveStorefrontPagePath() {
1194
+ if (typeof window === "undefined") return void 0;
1195
+ let pathname;
1196
+ try {
1197
+ const location = window.location;
1198
+ pathname = typeof location?.pathname === "string" && location.pathname ? location.pathname : new URL(String(location?.href ?? "")).pathname;
1199
+ } catch {
1200
+ return void 0;
1201
+ }
1202
+ if (!pathname || !pathname.startsWith("/")) return void 0;
1203
+ return pathname.slice(0, MAX_PATH_LENGTH);
1074
1204
  }
1075
- function normalizeQuantity(value) {
1076
- if (!Number.isFinite(value)) {
1077
- throw new CartError("quantity must be a finite number");
1205
+
1206
+ // ../sdk/src/core/page-tracker.ts
1207
+ var PLATFORM_PAGE_TRACKER_FLAG = "__shoppexPageTracker";
1208
+ function isPlatformPageTrackerActive() {
1209
+ if (typeof window === "undefined") return false;
1210
+ try {
1211
+ return Boolean(window[PLATFORM_PAGE_TRACKER_FLAG]);
1212
+ } catch {
1213
+ return false;
1078
1214
  }
1079
- return Math.floor(value);
1080
1215
  }
1081
- function normalizeCouponCode(value) {
1082
- const normalized = value?.trim().toUpperCase();
1083
- return normalized ? normalized : null;
1216
+
1217
+ // ../sdk/src/modules/analytics.ts
1218
+ var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
1219
+ var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
1220
+ var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
1221
+ var CART_CLAIM_TTL_MS = 1e4;
1222
+ function confirmedCartKey(storeSlug) {
1223
+ return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
1084
1224
  }
1085
- function getStoredCartCode() {
1086
- const raw = readCartStorage("coupon");
1087
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1225
+ function inFlightCartKey(storeSlug) {
1226
+ return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
1227
+ }
1228
+ function cartSequenceKey(storeSlug) {
1229
+ return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
1230
+ }
1231
+ function cartSnapshot(cartValue, itemCount) {
1232
+ return `${cartValue ?? ""}|${itemCount ?? ""}`;
1233
+ }
1234
+ function isCartSequence(value) {
1235
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1236
+ }
1237
+ var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
1238
+ function readCartCounter(storeSlug) {
1239
+ const counter = getItem(cartSequenceKey(storeSlug));
1240
+ if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
1088
1241
  return null;
1089
1242
  }
1090
- const record2 = raw;
1091
- const code = typeof record2.code === "string" ? normalizeCouponCode(record2.code) : null;
1092
- const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1093
- return code && source ? { code, source } : null;
1243
+ return counter;
1094
1244
  }
1095
- function normalizeCartItems(value) {
1096
- if (!Array.isArray(value)) return [];
1097
- const normalized = [];
1098
- for (const entry of value) {
1099
- if (!entry || typeof entry !== "object") continue;
1100
- const record2 = entry;
1101
- const productId = typeof record2.product_id === "string" ? record2.product_id.trim() : "";
1102
- const variantId = typeof record2.variant_id === "string" ? record2.variant_id.trim() : "";
1103
- const quantity = Number(record2.quantity);
1104
- if (!productId || !variantId || !Number.isFinite(quantity) || quantity < 1) {
1105
- continue;
1106
- }
1107
- const item = {
1108
- line_id: "",
1109
- product_id: productId,
1110
- variant_id: variantId,
1111
- quantity: Math.floor(quantity)
1112
- };
1113
- if (typeof record2.price_variant_id === "string") {
1114
- item.price_variant_id = record2.price_variant_id;
1115
- }
1116
- if (record2.price_data && typeof record2.price_data === "object") {
1117
- const priceData = record2.price_data;
1118
- if (typeof priceData.unit_price === "number" && Number.isFinite(priceData.unit_price)) {
1119
- item.price_data = { unit_price: priceData.unit_price };
1120
- }
1121
- }
1122
- if (typeof record2.pay_what_you_want_price === "number" && Number.isFinite(record2.pay_what_you_want_price)) {
1123
- item.pay_what_you_want_price = record2.pay_what_you_want_price;
1124
- }
1125
- if (Array.isArray(record2.addons)) {
1126
- item.addons = record2.addons;
1127
- }
1128
- if (record2.custom_fields && typeof record2.custom_fields === "object" && !Array.isArray(record2.custom_fields)) {
1129
- item.custom_fields = record2.custom_fields;
1130
- }
1131
- if (typeof record2.title === "string") {
1132
- item.title = record2.title;
1133
- }
1134
- if (typeof record2.variant_title === "string") {
1135
- item.variant_title = record2.variant_title;
1136
- }
1137
- if (typeof record2.image_url === "string") {
1138
- item.image_url = record2.image_url;
1139
- }
1140
- if (Array.isArray(record2.addon_labels)) {
1141
- item.addon_labels = record2.addon_labels.filter((label) => typeof label === "string");
1142
- }
1143
- if (typeof record2.max_quantity === "number" && Number.isFinite(record2.max_quantity)) {
1144
- item.max_quantity = record2.max_quantity;
1145
- }
1146
- if (typeof record2.min_quantity === "number" && Number.isFinite(record2.min_quantity)) {
1147
- item.min_quantity = record2.min_quantity;
1148
- }
1149
- clampQuantityToBounds(item);
1150
- const storedLineId = typeof record2.line_id === "string" ? record2.line_id.trim() : "";
1151
- item.line_id = storedLineId || computeCartLineId(item);
1152
- normalized.push(ensureCartLineId(item));
1153
- }
1154
- return normalized;
1245
+ var lastCartOrder = null;
1246
+ function nextCartOrder(storeSlug) {
1247
+ const stored = readCartCounter(storeSlug);
1248
+ const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
1249
+ const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
1250
+ const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
1251
+ lastCartOrder = order2;
1252
+ setItem(cartSequenceKey(storeSlug), order2);
1253
+ return order2;
1155
1254
  }
1156
- function getCartMetadata() {
1157
- return readCartStorage("meta");
1255
+ function cartSequenceLocks() {
1256
+ if (typeof navigator === "undefined") return null;
1257
+ const locks = navigator.locks;
1258
+ return locks && typeof locks.request === "function" ? locks : null;
1158
1259
  }
1159
- function getCartCoupon() {
1160
- return getStoredCartCode()?.code ?? null;
1260
+ function reserveCartOrder(storeSlug) {
1261
+ const take = () => nextCartOrder(storeSlug);
1262
+ const locks = cartSequenceLocks();
1263
+ if (!locks) return Promise.resolve(take());
1264
+ return locks.request(cartSequenceKey(storeSlug), take).catch(take);
1161
1265
  }
1162
- function getCartCouponSource() {
1163
- return getStoredCartCode()?.source ?? null;
1266
+ function readConfirmedCart(storeSlug) {
1267
+ const confirmed = getItem(confirmedCartKey(storeSlug));
1268
+ if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
1269
+ return confirmed;
1164
1270
  }
1165
- function setCartCoupon(coupon, source = "COUPON") {
1166
- const normalizedCoupon = normalizeCouponCode(coupon);
1167
- if (!normalizedCoupon) {
1168
- removeCartStorage("coupon");
1271
+ function readInFlightCart(storeSlug) {
1272
+ const claim2 = getItem(inFlightCartKey(storeSlug));
1273
+ if (!claim2 || typeof claim2.snapshot !== "string" || !isCartSequence(claim2.seq) || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
1169
1274
  return null;
1170
1275
  }
1171
- writeCartStorage("coupon", { code: normalizedCoupon, source });
1172
- return normalizedCoupon;
1276
+ return claim2;
1173
1277
  }
1174
- function clearCartCoupon() {
1175
- removeCartStorage("coupon");
1278
+ function isCartReportedOrInFlight(storeSlug, snapshot) {
1279
+ const confirmed = readConfirmedCart(storeSlug);
1280
+ const claim2 = readInFlightCart(storeSlug);
1281
+ const liveClaim = claim2 && Date.now() - claim2.at < CART_CLAIM_TTL_MS ? claim2 : null;
1282
+ const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
1283
+ return expected?.snapshot === snapshot;
1176
1284
  }
1177
- function writeCart(cart) {
1178
- const normalizedCart = normalizeCartItems(cart);
1179
- writeCartStorage("cart", normalizedCart);
1180
- const now = Date.now();
1181
- const previous = getCartMetadata();
1182
- const nextMeta = {
1183
- created_at: previous?.created_at ?? now,
1184
- last_modified: now,
1185
- version: (previous?.version ?? 0) + 1,
1186
- checksum: computeChecksum(normalizedCart)
1285
+ function createClaimToken() {
1286
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1287
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
1288
+ }
1289
+ function claimCartReport(storeSlug, seq, cartValue, itemCount) {
1290
+ const snapshot = cartSnapshot(cartValue, itemCount);
1291
+ const token = createClaimToken();
1292
+ setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
1293
+ return (delivered) => {
1294
+ if (delivered) {
1295
+ const confirmed = readConfirmedCart(storeSlug);
1296
+ if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
1297
+ }
1298
+ if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
1187
1299
  };
1188
- writeCartStorage("meta", nextMeta);
1189
- if (normalizedCart.length === 0) {
1190
- clearCartCoupon();
1300
+ }
1301
+ var cartSendTail = Promise.resolve();
1302
+ var latestCartRequest = 0;
1303
+ function enqueueCartSend(send) {
1304
+ const request2 = ++latestCartRequest;
1305
+ const run = () => send(() => request2 === latestCartRequest);
1306
+ cartSendTail = cartSendTail.then(run, run);
1307
+ return cartSendTail;
1308
+ }
1309
+ async function postPing(body) {
1310
+ const config2 = getConfig();
1311
+ const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1312
+ let timer;
1313
+ const timedOut = new Promise((resolve) => {
1314
+ timer = setTimeout(() => {
1315
+ controller?.abort();
1316
+ resolve(false);
1317
+ }, CART_CLAIM_TTL_MS);
1318
+ });
1319
+ try {
1320
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", {
1321
+ storeSlug: config2.storeSlug
1322
+ });
1323
+ const delivered = fetch(`${config2.apiBaseUrl}${endpoint}`, {
1324
+ method: "POST",
1325
+ headers: { "Content-Type": "application/json" },
1326
+ body: JSON.stringify(body),
1327
+ signal: controller?.signal
1328
+ }).then((response) => response.ok);
1329
+ return await Promise.race([delivered, timedOut]);
1330
+ } catch {
1331
+ return false;
1332
+ } finally {
1333
+ clearTimeout(timer);
1191
1334
  }
1192
1335
  }
1193
- function setCartWithMetadata(cart, metadata) {
1194
- const normalizedCart = normalizeCartItems(cart);
1195
- writeCartStorage("cart", normalizedCart);
1196
- const now = Date.now();
1197
- const base = metadata ?? getCartMetadata();
1198
- const nextMeta = {
1199
- created_at: base?.created_at ?? now,
1200
- last_modified: now,
1201
- version: base?.version ?? 1,
1202
- checksum: computeChecksum(normalizedCart)
1336
+ async function trackPageView(cartValue, itemCount) {
1337
+ if (!isInitialized()) return;
1338
+ if (typeof document === "undefined") return;
1339
+ const config2 = getConfig();
1340
+ const carriesCart = cartValue !== void 0 || itemCount !== void 0;
1341
+ const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
1342
+ if (isPlatformPageTrackerActive()) {
1343
+ if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
1344
+ return;
1345
+ }
1346
+ const sendView = async () => {
1347
+ const order2 = reservedOrder ? await reservedOrder : void 0;
1348
+ const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1349
+ const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
1350
+ const previousSession = currentPageSession();
1351
+ if (previousSession && previousSession !== sessionId) reportPageLeave();
1352
+ markPageViewed(sessionId);
1353
+ if (sessionId) installEngagedClock(reportPageLeave);
1354
+ onAnalyticsConsent(startSessionOnConsent);
1355
+ const delivered = await postPing({
1356
+ referer: document.referrer || void 0,
1357
+ cart_value: cartValue,
1358
+ item_count: itemCount,
1359
+ cart_seq: order2?.seq,
1360
+ cart_seq_scope: order2?.scope,
1361
+ session_id: sessionId ?? void 0,
1362
+ // Time on screen of the page before this one (client-side navigation).
1363
+ engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1364
+ path: resolveStorefrontPagePath(),
1365
+ utm: resolveUtmParameters(config2.storeSlug)
1366
+ });
1367
+ settle2?.(delivered);
1203
1368
  };
1204
- writeCartStorage("meta", nextMeta);
1369
+ await (reservedOrder ? enqueueCartSend(sendView) : sendView());
1205
1370
  }
1206
- function getCart() {
1207
- const raw = readCartStorage("cart");
1208
- return normalizeCartItems(raw);
1371
+ async function trackCartActivity(cartValue, itemCount) {
1372
+ if (!isInitialized()) return;
1373
+ if (typeof document === "undefined") return;
1374
+ const snapshot = cartSnapshot(cartValue, itemCount);
1375
+ const reservedOrder = reserveCartOrder(getConfig().storeSlug);
1376
+ await enqueueCartSend(async (isLatest) => {
1377
+ const order2 = await reservedOrder;
1378
+ if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
1379
+ await sendCartActivity(order2, cartValue, itemCount);
1380
+ });
1209
1381
  }
1210
- function resolveCartLineId(productId, variantId, cart = getCart()) {
1211
- const matches = cart.filter(
1212
- (item) => item.product_id === productId && item.variant_id === variantId
1213
- );
1214
- if (matches.length === 0) {
1215
- throw new CartError(`No cart line found for ${productId}/${variantId}`);
1216
- }
1217
- return matches[0].line_id;
1382
+ async function sendCartActivity(order2, cartValue, itemCount) {
1383
+ const config2 = getConfig();
1384
+ const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
1385
+ const delivered = await postPing({
1386
+ event_type: "presence",
1387
+ cart_value: cartValue,
1388
+ item_count: itemCount,
1389
+ cart_seq: order2.seq,
1390
+ cart_seq_scope: order2.scope,
1391
+ // A cart marks the analytics session as one that did not bounce.
1392
+ session_id: analyticsSessionId(config2.storeSlug, { touch: false }) ?? void 0,
1393
+ path: resolveStorefrontPagePath()
1394
+ });
1395
+ settle2(delivered);
1218
1396
  }
1219
- function getCartItemCount() {
1220
- const cart = getCart();
1221
- return cart.reduce((sum, item) => sum + item.quantity, 0);
1397
+ async function trackProductView(productId) {
1398
+ if (!isInitialized() || typeof document === "undefined" || !productId) return;
1399
+ await sendVisitEvent({ event_type: "product_view", product_id: productId });
1222
1400
  }
1223
- function addToCart(productId, variantId, quantity = 1, options) {
1224
- if (!productId || !variantId) {
1225
- throw new CartError("product_id and variant_id are required");
1401
+ async function trackCartAdd(productId, variantId, quantity) {
1402
+ if (!isInitialized() || typeof document === "undefined" || quantity < 1) return;
1403
+ await sendVisitEvent({ event_type: "cart_add", product_id: productId, variant_id: variantId, quantity });
1404
+ }
1405
+ async function sendVisitEvent(body) {
1406
+ const config2 = getConfig();
1407
+ if (config2.preview) return;
1408
+ await postPing({
1409
+ ...body,
1410
+ referer: document.referrer || void 0,
1411
+ session_id: analyticsSessionId(config2.storeSlug, { touch: true }) ?? void 0,
1412
+ path: resolveStorefrontPagePath(),
1413
+ utm: resolveUtmParameters(config2.storeSlug)
1414
+ });
1415
+ }
1416
+ function startSessionOnConsent() {
1417
+ const config2 = getConfig();
1418
+ if (!isPageViewed() || analyticsSessionId(config2.storeSlug, { touch: false })) return;
1419
+ const sessionId = analyticsSessionId(config2.storeSlug, { touch: true });
1420
+ if (!sessionId) return;
1421
+ markPageViewed(sessionId);
1422
+ installEngagedClock(reportPageLeave);
1423
+ void postPing({
1424
+ event_type: "session_start",
1425
+ referer: document.referrer || void 0,
1426
+ session_id: sessionId,
1427
+ path: resolveStorefrontPagePath(),
1428
+ utm: resolveUtmParameters(config2.storeSlug)
1429
+ });
1430
+ }
1431
+ function reportPageLeave() {
1432
+ const config2 = getConfig();
1433
+ const sessionId = currentPageSession();
1434
+ const engagedMs2 = takeEngagedMs();
1435
+ if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
1436
+ try {
1437
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config2.storeSlug });
1438
+ void fetch(`${config2.apiBaseUrl}${endpoint}`, {
1439
+ method: "POST",
1440
+ headers: { "Content-Type": "application/json" },
1441
+ body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
1442
+ keepalive: true
1443
+ }).catch(() => {
1444
+ });
1445
+ } catch {
1226
1446
  }
1227
- const normalizedQuantity = normalizeQuantity(quantity);
1228
- if (normalizedQuantity < 1) {
1229
- throw new CartError("quantity must be at least 1");
1447
+ }
1448
+
1449
+ // ../sdk/src/utils/cart-line-id.ts
1450
+ function hashString(value) {
1451
+ let hash2 = 2166136261;
1452
+ for (let i = 0; i < value.length; i += 1) {
1453
+ hash2 ^= value.charCodeAt(i);
1454
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
1230
1455
  }
1231
- const cart = getCart();
1232
- const lineId = computeCartLineId({
1233
- product_id: productId,
1234
- variant_id: variantId,
1235
- addons: options?.addons,
1236
- custom_fields: options?.custom_fields,
1237
- price_variant_id: options?.price_variant_id,
1238
- price_data: options?.price_data,
1239
- pay_what_you_want_price: options?.pay_what_you_want_price
1240
- });
1241
- const existingIndex = cart.findIndex((item) => item.line_id === lineId);
1242
- const quantityBeforeAdd = existingIndex >= 0 ? cart[existingIndex].quantity : 0;
1243
- if (existingIndex >= 0) {
1456
+ return (hash2 >>> 0).toString(16);
1457
+ }
1458
+ function normalizeAddons(addons) {
1459
+ if (!addons?.length) return [];
1460
+ return [...addons].map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 })).sort((a, b) => a.id.localeCompare(b.id));
1461
+ }
1462
+ function normalizeCustomFields(fields) {
1463
+ if (!fields) return {};
1464
+ const entries = Object.entries(fields).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b));
1465
+ return Object.fromEntries(entries);
1466
+ }
1467
+ function computeCartLineId(input2) {
1468
+ const payload = {
1469
+ product_id: input2.product_id,
1470
+ variant_id: input2.variant_id,
1471
+ price_variant_id: input2.price_variant_id ?? null,
1472
+ addons: normalizeAddons(input2.addons),
1473
+ custom_fields: normalizeCustomFields(input2.custom_fields),
1474
+ unit_price: typeof input2.price_data?.unit_price === "number" && Number.isFinite(input2.price_data.unit_price) ? input2.price_data.unit_price : null,
1475
+ 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
1476
+ };
1477
+ return hashString(JSON.stringify(payload));
1478
+ }
1479
+ function ensureCartLineId(item) {
1480
+ if (typeof item.line_id === "string" && item.line_id.trim()) {
1481
+ return item;
1482
+ }
1483
+ return { ...item, line_id: computeCartLineId(item) };
1484
+ }
1485
+
1486
+ // ../contracts/src/catalog-unit-price.ts
1487
+ var CATALOG_UNIT_PRICE_DECIMAL_PLACES = 3;
1488
+ var PAYABLE_AMOUNT_DECIMAL_PLACES = 2;
1489
+ var CATALOG_UNIT_PRICE_FORMAT_OPTIONS = {
1490
+ maximumFractionDigits: CATALOG_UNIT_PRICE_DECIMAL_PLACES
1491
+ };
1492
+ function roundPayableAmount(value) {
1493
+ if (!Number.isFinite(value)) return value;
1494
+ const sign = value < 0 ? -1 : 1;
1495
+ const shifted = shiftDecimal(Math.abs(value), PAYABLE_AMOUNT_DECIMAL_PLACES);
1496
+ const rounded = Math.round(shifted);
1497
+ if (rounded === 0) return 0;
1498
+ return sign * shiftDecimal(rounded, -PAYABLE_AMOUNT_DECIMAL_PLACES);
1499
+ }
1500
+ function shiftDecimal(value, places) {
1501
+ const [coefficient, currentExponent = "0"] = String(value).split("e");
1502
+ return Number(`${coefficient}e${Number(currentExponent) + places}`);
1503
+ }
1504
+
1505
+ // ../sdk/src/modules/cart.ts
1506
+ var STORAGE_KEYS = {
1507
+ cart: "cart",
1508
+ cartBackup: "cart_backup",
1509
+ meta: "cart_meta",
1510
+ metaBackup: "cart_backup_meta",
1511
+ coupon: "cart_coupon",
1512
+ couponBackup: "cart_coupon_backup"
1513
+ };
1514
+ function getStorageKey(type) {
1515
+ return `${STORAGE_KEYS[type]}_${getConfig().storeSlug}`;
1516
+ }
1517
+ function cartStorageArea() {
1518
+ return getConfig().preview ? "sessionStorage" : "localStorage";
1519
+ }
1520
+ function readCartStorage(type) {
1521
+ return getItem(getStorageKey(type), cartStorageArea());
1522
+ }
1523
+ function writeCartStorage(type, value) {
1524
+ setItem(getStorageKey(type), value, cartStorageArea());
1525
+ }
1526
+ function removeCartStorage(type) {
1527
+ removeItem(getStorageKey(type), cartStorageArea());
1528
+ }
1529
+ function hashString2(value) {
1530
+ let hash2 = 2166136261;
1531
+ for (let i = 0; i < value.length; i += 1) {
1532
+ hash2 ^= value.charCodeAt(i);
1533
+ hash2 += (hash2 << 1) + (hash2 << 4) + (hash2 << 7) + (hash2 << 8) + (hash2 << 24);
1534
+ }
1535
+ return (hash2 >>> 0).toString(16);
1536
+ }
1537
+ function computeChecksum(cart) {
1538
+ return hashString2(JSON.stringify(cart));
1539
+ }
1540
+ function normalizeQuantity(value) {
1541
+ if (!Number.isFinite(value)) {
1542
+ throw new CartError("quantity must be a finite number");
1543
+ }
1544
+ return Math.floor(value);
1545
+ }
1546
+ function normalizeCouponCode(value) {
1547
+ const normalized = value?.trim().toUpperCase();
1548
+ return normalized ? normalized : null;
1549
+ }
1550
+ function getStoredCartCode() {
1551
+ const raw = readCartStorage("coupon");
1552
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1553
+ return null;
1554
+ }
1555
+ const record2 = raw;
1556
+ const code = typeof record2.code === "string" ? normalizeCouponCode(record2.code) : null;
1557
+ const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1558
+ return code && source ? { code, source } : null;
1559
+ }
1560
+ function normalizeCartItems(value) {
1561
+ if (!Array.isArray(value)) return [];
1562
+ const normalized = [];
1563
+ for (const entry of value) {
1564
+ if (!entry || typeof entry !== "object") continue;
1565
+ const record2 = entry;
1566
+ const productId = typeof record2.product_id === "string" ? record2.product_id.trim() : "";
1567
+ const variantId = typeof record2.variant_id === "string" ? record2.variant_id.trim() : "";
1568
+ const quantity = Number(record2.quantity);
1569
+ if (!productId || !variantId || !Number.isFinite(quantity) || quantity < 1) {
1570
+ continue;
1571
+ }
1572
+ const item = {
1573
+ line_id: "",
1574
+ product_id: productId,
1575
+ variant_id: variantId,
1576
+ quantity: Math.floor(quantity)
1577
+ };
1578
+ if (typeof record2.price_variant_id === "string") {
1579
+ item.price_variant_id = record2.price_variant_id;
1580
+ }
1581
+ if (record2.price_data && typeof record2.price_data === "object") {
1582
+ const priceData = record2.price_data;
1583
+ if (typeof priceData.unit_price === "number" && Number.isFinite(priceData.unit_price)) {
1584
+ item.price_data = { unit_price: priceData.unit_price };
1585
+ }
1586
+ }
1587
+ if (typeof record2.pay_what_you_want_price === "number" && Number.isFinite(record2.pay_what_you_want_price)) {
1588
+ item.pay_what_you_want_price = record2.pay_what_you_want_price;
1589
+ }
1590
+ if (Array.isArray(record2.addons)) {
1591
+ item.addons = record2.addons;
1592
+ }
1593
+ if (record2.custom_fields && typeof record2.custom_fields === "object" && !Array.isArray(record2.custom_fields)) {
1594
+ item.custom_fields = record2.custom_fields;
1595
+ }
1596
+ if (typeof record2.title === "string") {
1597
+ item.title = record2.title;
1598
+ }
1599
+ if (typeof record2.variant_title === "string") {
1600
+ item.variant_title = record2.variant_title;
1601
+ }
1602
+ if (typeof record2.image_url === "string") {
1603
+ item.image_url = record2.image_url;
1604
+ }
1605
+ if (Array.isArray(record2.addon_labels)) {
1606
+ item.addon_labels = record2.addon_labels.filter((label) => typeof label === "string");
1607
+ }
1608
+ if (typeof record2.max_quantity === "number" && Number.isFinite(record2.max_quantity)) {
1609
+ item.max_quantity = record2.max_quantity;
1610
+ }
1611
+ if (typeof record2.min_quantity === "number" && Number.isFinite(record2.min_quantity)) {
1612
+ item.min_quantity = record2.min_quantity;
1613
+ }
1614
+ clampQuantityToBounds(item);
1615
+ const storedLineId = typeof record2.line_id === "string" ? record2.line_id.trim() : "";
1616
+ item.line_id = storedLineId || computeCartLineId(item);
1617
+ normalized.push(ensureCartLineId(item));
1618
+ }
1619
+ return normalized;
1620
+ }
1621
+ function getCartMetadata() {
1622
+ return readCartStorage("meta");
1623
+ }
1624
+ function getCartCoupon() {
1625
+ return getStoredCartCode()?.code ?? null;
1626
+ }
1627
+ function getCartCouponSource() {
1628
+ return getStoredCartCode()?.source ?? null;
1629
+ }
1630
+ function setCartCoupon(coupon, source = "COUPON") {
1631
+ const normalizedCoupon = normalizeCouponCode(coupon);
1632
+ if (!normalizedCoupon) {
1633
+ removeCartStorage("coupon");
1634
+ return null;
1635
+ }
1636
+ writeCartStorage("coupon", { code: normalizedCoupon, source });
1637
+ return normalizedCoupon;
1638
+ }
1639
+ function clearCartCoupon() {
1640
+ removeCartStorage("coupon");
1641
+ }
1642
+ function writeCart(cart) {
1643
+ const normalizedCart = normalizeCartItems(cart);
1644
+ writeCartStorage("cart", normalizedCart);
1645
+ const now = Date.now();
1646
+ const previous = getCartMetadata();
1647
+ const nextMeta = {
1648
+ created_at: previous?.created_at ?? now,
1649
+ last_modified: now,
1650
+ version: (previous?.version ?? 0) + 1,
1651
+ checksum: computeChecksum(normalizedCart)
1652
+ };
1653
+ writeCartStorage("meta", nextMeta);
1654
+ if (normalizedCart.length === 0) {
1655
+ clearCartCoupon();
1656
+ }
1657
+ }
1658
+ function setCartWithMetadata(cart, metadata) {
1659
+ const normalizedCart = normalizeCartItems(cart);
1660
+ writeCartStorage("cart", normalizedCart);
1661
+ const now = Date.now();
1662
+ const base = metadata ?? getCartMetadata();
1663
+ const nextMeta = {
1664
+ created_at: base?.created_at ?? now,
1665
+ last_modified: now,
1666
+ version: base?.version ?? 1,
1667
+ checksum: computeChecksum(normalizedCart)
1668
+ };
1669
+ writeCartStorage("meta", nextMeta);
1670
+ }
1671
+ function getCart() {
1672
+ const raw = readCartStorage("cart");
1673
+ return normalizeCartItems(raw);
1674
+ }
1675
+ function resolveCartLineId(productId, variantId, cart = getCart()) {
1676
+ const matches = cart.filter(
1677
+ (item) => item.product_id === productId && item.variant_id === variantId
1678
+ );
1679
+ if (matches.length === 0) {
1680
+ throw new CartError(`No cart line found for ${productId}/${variantId}`);
1681
+ }
1682
+ return matches[0].line_id;
1683
+ }
1684
+ function getCartItemCount() {
1685
+ const cart = getCart();
1686
+ return cart.reduce((sum, item) => sum + item.quantity, 0);
1687
+ }
1688
+ function addToCart(productId, variantId, quantity = 1, options) {
1689
+ if (!productId || !variantId) {
1690
+ throw new CartError("product_id and variant_id are required");
1691
+ }
1692
+ const normalizedQuantity = normalizeQuantity(quantity);
1693
+ if (normalizedQuantity < 1) {
1694
+ throw new CartError("quantity must be at least 1");
1695
+ }
1696
+ const cart = getCart();
1697
+ const lineId = computeCartLineId({
1698
+ product_id: productId,
1699
+ variant_id: variantId,
1700
+ addons: options?.addons,
1701
+ custom_fields: options?.custom_fields,
1702
+ price_variant_id: options?.price_variant_id,
1703
+ price_data: options?.price_data,
1704
+ pay_what_you_want_price: options?.pay_what_you_want_price
1705
+ });
1706
+ const existingIndex = cart.findIndex((item) => item.line_id === lineId);
1707
+ const quantityBeforeAdd = existingIndex >= 0 ? cart[existingIndex].quantity : 0;
1708
+ if (existingIndex >= 0) {
1244
1709
  cart[existingIndex].quantity += normalizedQuantity;
1245
1710
  if (options?.addons) {
1246
1711
  cart[existingIndex].addons = options.addons;
@@ -1286,6 +1751,7 @@ function addToCart(productId, variantId, quantity = 1, options) {
1286
1751
  if (intendedQuantity > quantityBeforeAdd && persistedLine?.quantity === intendedQuantity) {
1287
1752
  const cartAffiliateCode = getCartCouponSource() === "AFFILIATE" ? getCartCoupon() : null;
1288
1753
  void trackAffiliateEvent("add_to_cart", cartAffiliateCode ? { code: cartAffiliateCode } : void 0);
1754
+ void trackCartAdd(productId, variantId, intendedQuantity - quantityBeforeAdd);
1289
1755
  }
1290
1756
  }
1291
1757
  function clampQuantityToBounds(item) {
@@ -1373,6 +1839,8 @@ function setCartItem(productId, variantId, quantity = 1, options) {
1373
1839
  clampQuantityToBounds(pushed);
1374
1840
  cart.push(pushed);
1375
1841
  writeCart(cart);
1842
+ const persistedLine = getCart().find((item) => item.line_id === lineId);
1843
+ if (persistedLine) void trackCartAdd(productId, variantId, persistedLine.quantity);
1376
1844
  }
1377
1845
  function updateCartItem(lineId, updates) {
1378
1846
  const cart = getCart();
@@ -1466,436 +1934,172 @@ function createCartBackup() {
1466
1934
  writeCartStorage("metaBackup", {
1467
1935
  created_at: now,
1468
1936
  last_modified: now,
1469
- version: 1,
1470
- checksum: computeChecksum(cart)
1471
- });
1472
- }
1473
- }
1474
- function restoreCartFromBackup() {
1475
- const backupRaw = readCartStorage("cartBackup");
1476
- const backup = normalizeCartItems(backupRaw);
1477
- const backupMeta = readCartStorage("metaBackup");
1478
- const backupCoupon = readCartStorage("couponBackup");
1479
- if (backup && backup.length > 0) {
1480
- setCartWithMetadata(backup, backupMeta);
1481
- if (backupCoupon && typeof backupCoupon === "object" && !Array.isArray(backupCoupon)) {
1482
- const record2 = backupCoupon;
1483
- const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1484
- if (typeof record2.code === "string" && source) {
1485
- setCartCoupon(record2.code, source);
1486
- } else {
1487
- clearCartCoupon();
1488
- }
1489
- } else {
1490
- clearCartCoupon();
1491
- }
1492
- return true;
1493
- }
1494
- return false;
1495
- }
1496
- function mergeBaskets(items) {
1497
- const cart = getCart();
1498
- for (const incoming of items) {
1499
- const productId = typeof incoming.product_id === "string" ? incoming.product_id.trim() : "";
1500
- const variantId = typeof incoming.variant_id === "string" ? incoming.variant_id.trim() : "";
1501
- if (!productId || !variantId) {
1502
- continue;
1503
- }
1504
- let normalizedQuantity;
1505
- try {
1506
- normalizedQuantity = normalizeQuantity(incoming.quantity);
1507
- } catch {
1508
- continue;
1509
- }
1510
- if (normalizedQuantity < 1) {
1511
- continue;
1512
- }
1513
- const mergedLine = ensureCartLineId({
1514
- ...incoming,
1515
- line_id: incoming.line_id ?? computeCartLineId(incoming),
1516
- product_id: productId,
1517
- variant_id: variantId,
1518
- quantity: normalizedQuantity
1519
- });
1520
- const index = cart.findIndex((item) => item.line_id === mergedLine.line_id);
1521
- if (index >= 0) {
1522
- cart[index].quantity = Math.max(cart[index].quantity, normalizedQuantity);
1523
- clampQuantityToBounds(cart[index]);
1524
- } else {
1525
- clampQuantityToBounds(mergedLine);
1526
- cart.push(mergedLine);
1527
- }
1528
- }
1529
- writeCart(cart);
1530
- return cart;
1531
- }
1532
- function moveBasketItem(fromProductId, fromVariantId, toProductId, toVariantId) {
1533
- const cart = getCart();
1534
- const fromIndex = cart.findIndex(
1535
- (item) => item.product_id === fromProductId && item.variant_id === fromVariantId
1536
- );
1537
- if (fromIndex < 0) {
1538
- throw new CartError("Item not found in cart");
1539
- }
1540
- const [fromItem] = cart.splice(fromIndex, 1);
1541
- const toLineId = computeCartLineId({
1542
- ...fromItem,
1543
- product_id: toProductId,
1544
- variant_id: toVariantId
1545
- });
1546
- const toIndex = cart.findIndex((item) => item.line_id === toLineId);
1547
- if (toIndex >= 0) {
1548
- cart[toIndex].quantity += fromItem.quantity;
1549
- clampQuantityToBounds(cart[toIndex]);
1550
- } else {
1551
- const moved = ensureCartLineId({
1552
- ...fromItem,
1553
- product_id: toProductId,
1554
- variant_id: toVariantId,
1555
- line_id: toLineId
1556
- });
1557
- clampQuantityToBounds(moved);
1558
- cart.push(moved);
1559
- }
1560
- writeCart(cart);
1561
- }
1562
- function validateCartIntegrity() {
1563
- const cart = getCart();
1564
- const metadata = getCartMetadata();
1565
- const checksum = computeChecksum(cart);
1566
- if (!metadata) {
1567
- setCartWithMetadata(cart, null);
1568
- return true;
1569
- }
1570
- return metadata.checksum === checksum;
1571
- }
1572
- function getCartStats() {
1573
- const cart = getCart();
1574
- const integrityValid = validateCartIntegrity();
1575
- const metadata = getCartMetadata();
1576
- const backup = readCartStorage("cartBackup") ?? [];
1577
- const hasCompletePriceSnapshots = cart.length > 0 && cart.every((item) => typeof item.price_data?.unit_price === "number");
1578
- const totalPrice = cart.reduce((sum, item) => {
1579
- const unitPrice = item.price_data?.unit_price ?? 0;
1580
- return sum + roundPayableAmount(unitPrice * item.quantity);
1581
- }, 0);
1582
- return {
1583
- item_count: cart.length,
1584
- total_quantity: cart.reduce((sum, item) => sum + item.quantity, 0),
1585
- last_modified: metadata?.last_modified ?? 0,
1586
- version: metadata?.version ?? 0,
1587
- has_backup: backup.length > 0,
1588
- integrity_valid: integrityValid,
1589
- total_price: roundPayableAmount(totalPrice),
1590
- total_price_is_estimate: cart.length > 0 && !hasCompletePriceSnapshots
1591
- };
1592
- }
1593
- function getCartPayload(coupon) {
1594
- const config2 = getConfig();
1595
- const normalizedCoupon = normalizeCouponCode(coupon) ?? getCartCoupon() ?? void 0;
1596
- return {
1597
- store_slug: config2.storeSlug,
1598
- items: getCart(),
1599
- coupon: normalizedCoupon
1600
- };
1601
- }
1602
- function getAmbientAffiliateCodeForQuote() {
1603
- const storedCode = getCartCoupon();
1604
- const storedSource = getCartCouponSource();
1605
- if (storedSource === "AFFILIATE" && storedCode) {
1606
- return storedCode.trim().toLowerCase();
1607
- }
1608
- return getAffiliateCode();
1609
- }
1610
- var latestQuoteToken = null;
1611
- var quoteSequence = 0;
1612
- function getLatestQuoteToken() {
1613
- return latestQuoteToken;
1614
- }
1615
- function clearLatestQuoteToken() {
1616
- latestQuoteToken = null;
1617
- quoteSequence += 1;
1618
- }
1619
- async function quoteCart(coupon, currency) {
1620
- const sequence = quoteSequence += 1;
1621
- const payload = getCartPayload(coupon);
1622
- const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
1623
- const response = await post("/v1/storefront/cart/quote", {
1624
- shop_slug: payload.store_slug,
1625
- cart: payload.items,
1626
- coupon: payload.coupon,
1627
- affiliate_code: getAmbientAffiliateCodeForQuote() ?? void 0,
1628
- currency: normalizedCurrency ?? void 0
1629
- });
1630
- if (sequence !== quoteSequence) return response;
1631
- if (response.success) {
1632
- latestQuoteToken = typeof response.data?.quote_token === "string" ? response.data.quote_token : null;
1633
- }
1634
- return response;
1635
- }
1636
-
1637
- // ../sdk/src/core/attribution.ts
1638
- var STORAGE_PREFIX2 = "shoppex:utm:";
1639
- var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1640
- var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
1641
- var MAX_VALUE_LENGTH = 255;
1642
- function readFromUrl(search) {
1643
- let params;
1644
- try {
1645
- params = new URLSearchParams(search);
1646
- } catch {
1647
- return {};
1648
- }
1649
- const utm = {};
1650
- for (const key of UTM_KEYS) {
1651
- const value = params.get(key)?.trim();
1652
- if (value) {
1653
- utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1654
- }
1655
- }
1656
- return utm;
1657
- }
1658
- function storageKey(storeSlug) {
1659
- return `${STORAGE_PREFIX2}${storeSlug}`;
1660
- }
1661
- function readStored(storeSlug) {
1662
- try {
1663
- const raw = window.localStorage.getItem(storefrontStorageKey(storageKey(storeSlug)));
1664
- if (!raw) return {};
1665
- const parsed = JSON.parse(raw);
1666
- if (!parsed || typeof parsed.at !== "number" || typeof parsed.utm !== "object") return {};
1667
- if (Date.now() - parsed.at > TTL_MS) {
1668
- window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1669
- return {};
1670
- }
1671
- const utm = {};
1672
- for (const key of UTM_KEYS) {
1673
- const value = parsed.utm?.[key];
1674
- if (typeof value === "string" && value.length > 0) {
1675
- utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1676
- }
1677
- }
1678
- return utm;
1679
- } catch {
1680
- return {};
1681
- }
1682
- }
1683
- function writeStored(storeSlug, utm) {
1684
- try {
1685
- const payload = { utm, at: Date.now() };
1686
- window.localStorage.setItem(storefrontStorageKey(storageKey(storeSlug)), JSON.stringify(payload));
1687
- } catch {
1688
- }
1689
- }
1690
- function resolveUtmParameters(storeSlug) {
1691
- if (typeof window === "undefined" || typeof window.location === "undefined") {
1692
- return void 0;
1693
- }
1694
- const fromUrl = readFromUrl(window.location.search);
1695
- if (Object.keys(fromUrl).length > 0) {
1696
- writeStored(storeSlug, fromUrl);
1697
- return fromUrl;
1698
- }
1699
- const stored = readStored(storeSlug);
1700
- return Object.keys(stored).length > 0 ? stored : void 0;
1701
- }
1702
-
1703
- // ../sdk/src/core/page-path.ts
1704
- var MAX_PATH_LENGTH = 256;
1705
- function resolveStorefrontPagePath() {
1706
- if (typeof window === "undefined") return void 0;
1707
- let pathname;
1708
- try {
1709
- const location = window.location;
1710
- pathname = typeof location?.pathname === "string" && location.pathname ? location.pathname : new URL(String(location?.href ?? "")).pathname;
1711
- } catch {
1712
- return void 0;
1713
- }
1714
- if (!pathname || !pathname.startsWith("/")) return void 0;
1715
- return pathname.slice(0, MAX_PATH_LENGTH);
1716
- }
1717
-
1718
- // ../sdk/src/core/page-tracker.ts
1719
- var PLATFORM_PAGE_TRACKER_FLAG = "__shoppexPageTracker";
1720
- function isPlatformPageTrackerActive() {
1721
- if (typeof window === "undefined") return false;
1722
- try {
1723
- return Boolean(window[PLATFORM_PAGE_TRACKER_FLAG]);
1724
- } catch {
1725
- return false;
1726
- }
1727
- }
1728
-
1729
- // ../sdk/src/modules/analytics.ts
1730
- var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
1731
- var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
1732
- var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
1733
- var CART_CLAIM_TTL_MS = 1e4;
1734
- function confirmedCartKey(storeSlug) {
1735
- return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
1736
- }
1737
- function inFlightCartKey(storeSlug) {
1738
- return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
1739
- }
1740
- function cartSequenceKey(storeSlug) {
1741
- return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
1742
- }
1743
- function cartSnapshot(cartValue, itemCount) {
1744
- return `${cartValue ?? ""}|${itemCount ?? ""}`;
1745
- }
1746
- function isCartSequence(value) {
1747
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1748
- }
1749
- var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
1750
- function readCartCounter(storeSlug) {
1751
- const counter = getItem(cartSequenceKey(storeSlug));
1752
- if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
1753
- return null;
1754
- }
1755
- return counter;
1756
- }
1757
- var lastCartOrder = null;
1758
- function nextCartOrder(storeSlug) {
1759
- const stored = readCartCounter(storeSlug);
1760
- const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
1761
- const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
1762
- const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
1763
- lastCartOrder = order2;
1764
- setItem(cartSequenceKey(storeSlug), order2);
1765
- return order2;
1766
- }
1767
- function cartSequenceLocks() {
1768
- if (typeof navigator === "undefined") return null;
1769
- const locks = navigator.locks;
1770
- return locks && typeof locks.request === "function" ? locks : null;
1771
- }
1772
- function reserveCartOrder(storeSlug) {
1773
- const take = () => nextCartOrder(storeSlug);
1774
- const locks = cartSequenceLocks();
1775
- if (!locks) return Promise.resolve(take());
1776
- return locks.request(cartSequenceKey(storeSlug), take).catch(take);
1777
- }
1778
- function readConfirmedCart(storeSlug) {
1779
- const confirmed = getItem(confirmedCartKey(storeSlug));
1780
- if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
1781
- return confirmed;
1782
- }
1783
- function readInFlightCart(storeSlug) {
1784
- const claim2 = getItem(inFlightCartKey(storeSlug));
1785
- if (!claim2 || typeof claim2.snapshot !== "string" || !isCartSequence(claim2.seq) || typeof claim2.token !== "string" || typeof claim2.at !== "number") {
1786
- return null;
1787
- }
1788
- return claim2;
1789
- }
1790
- function isCartReportedOrInFlight(storeSlug, snapshot) {
1791
- const confirmed = readConfirmedCart(storeSlug);
1792
- const claim2 = readInFlightCart(storeSlug);
1793
- const liveClaim = claim2 && Date.now() - claim2.at < CART_CLAIM_TTL_MS ? claim2 : null;
1794
- const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
1795
- return expected?.snapshot === snapshot;
1796
- }
1797
- function createClaimToken() {
1798
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1799
- return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
1937
+ version: 1,
1938
+ checksum: computeChecksum(cart)
1939
+ });
1940
+ }
1800
1941
  }
1801
- function claimCartReport(storeSlug, seq, cartValue, itemCount) {
1802
- const snapshot = cartSnapshot(cartValue, itemCount);
1803
- const token = createClaimToken();
1804
- setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
1805
- return (delivered) => {
1806
- if (delivered) {
1807
- const confirmed = readConfirmedCart(storeSlug);
1808
- if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
1942
+ function restoreCartFromBackup() {
1943
+ const backupRaw = readCartStorage("cartBackup");
1944
+ const backup = normalizeCartItems(backupRaw);
1945
+ const backupMeta = readCartStorage("metaBackup");
1946
+ const backupCoupon = readCartStorage("couponBackup");
1947
+ if (backup && backup.length > 0) {
1948
+ setCartWithMetadata(backup, backupMeta);
1949
+ if (backupCoupon && typeof backupCoupon === "object" && !Array.isArray(backupCoupon)) {
1950
+ const record2 = backupCoupon;
1951
+ const source = record2.source === "COUPON" || record2.source === "AFFILIATE" ? record2.source : null;
1952
+ if (typeof record2.code === "string" && source) {
1953
+ setCartCoupon(record2.code, source);
1954
+ } else {
1955
+ clearCartCoupon();
1956
+ }
1957
+ } else {
1958
+ clearCartCoupon();
1809
1959
  }
1810
- if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
1811
- };
1960
+ return true;
1961
+ }
1962
+ return false;
1812
1963
  }
1813
- var cartSendTail = Promise.resolve();
1814
- var latestCartRequest = 0;
1815
- function enqueueCartSend(send) {
1816
- const request2 = ++latestCartRequest;
1817
- const run = () => send(() => request2 === latestCartRequest);
1818
- cartSendTail = cartSendTail.then(run, run);
1819
- return cartSendTail;
1964
+ function mergeBaskets(items) {
1965
+ const cart = getCart();
1966
+ for (const incoming of items) {
1967
+ const productId = typeof incoming.product_id === "string" ? incoming.product_id.trim() : "";
1968
+ const variantId = typeof incoming.variant_id === "string" ? incoming.variant_id.trim() : "";
1969
+ if (!productId || !variantId) {
1970
+ continue;
1971
+ }
1972
+ let normalizedQuantity;
1973
+ try {
1974
+ normalizedQuantity = normalizeQuantity(incoming.quantity);
1975
+ } catch {
1976
+ continue;
1977
+ }
1978
+ if (normalizedQuantity < 1) {
1979
+ continue;
1980
+ }
1981
+ const mergedLine = ensureCartLineId({
1982
+ ...incoming,
1983
+ line_id: incoming.line_id ?? computeCartLineId(incoming),
1984
+ product_id: productId,
1985
+ variant_id: variantId,
1986
+ quantity: normalizedQuantity
1987
+ });
1988
+ const index = cart.findIndex((item) => item.line_id === mergedLine.line_id);
1989
+ if (index >= 0) {
1990
+ cart[index].quantity = Math.max(cart[index].quantity, normalizedQuantity);
1991
+ clampQuantityToBounds(cart[index]);
1992
+ } else {
1993
+ clampQuantityToBounds(mergedLine);
1994
+ cart.push(mergedLine);
1995
+ }
1996
+ }
1997
+ writeCart(cart);
1998
+ return cart;
1820
1999
  }
1821
- async function postPing(body) {
1822
- const config2 = getConfig();
1823
- const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1824
- let timer;
1825
- const timedOut = new Promise((resolve) => {
1826
- timer = setTimeout(() => {
1827
- controller?.abort();
1828
- resolve(false);
1829
- }, CART_CLAIM_TTL_MS);
2000
+ function moveBasketItem(fromProductId, fromVariantId, toProductId, toVariantId) {
2001
+ const cart = getCart();
2002
+ const fromIndex = cart.findIndex(
2003
+ (item) => item.product_id === fromProductId && item.variant_id === fromVariantId
2004
+ );
2005
+ if (fromIndex < 0) {
2006
+ throw new CartError("Item not found in cart");
2007
+ }
2008
+ const [fromItem] = cart.splice(fromIndex, 1);
2009
+ const toLineId = computeCartLineId({
2010
+ ...fromItem,
2011
+ product_id: toProductId,
2012
+ variant_id: toVariantId
1830
2013
  });
1831
- try {
1832
- const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", {
1833
- storeSlug: config2.storeSlug
2014
+ const toIndex = cart.findIndex((item) => item.line_id === toLineId);
2015
+ if (toIndex >= 0) {
2016
+ cart[toIndex].quantity += fromItem.quantity;
2017
+ clampQuantityToBounds(cart[toIndex]);
2018
+ } else {
2019
+ const moved = ensureCartLineId({
2020
+ ...fromItem,
2021
+ product_id: toProductId,
2022
+ variant_id: toVariantId,
2023
+ line_id: toLineId
1834
2024
  });
1835
- const delivered = fetch(`${config2.apiBaseUrl}${endpoint}`, {
1836
- method: "POST",
1837
- headers: { "Content-Type": "application/json" },
1838
- body: JSON.stringify(body),
1839
- signal: controller?.signal
1840
- }).then((response) => response.ok);
1841
- return await Promise.race([delivered, timedOut]);
1842
- } catch {
1843
- return false;
1844
- } finally {
1845
- clearTimeout(timer);
2025
+ clampQuantityToBounds(moved);
2026
+ cart.push(moved);
1846
2027
  }
2028
+ writeCart(cart);
1847
2029
  }
1848
- async function trackPageView(cartValue, itemCount) {
1849
- if (!isInitialized()) return;
1850
- if (typeof document === "undefined") return;
1851
- const config2 = getConfig();
1852
- const carriesCart = cartValue !== void 0 || itemCount !== void 0;
1853
- const reservedOrder = carriesCart ? reserveCartOrder(config2.storeSlug) : void 0;
1854
- if (isPlatformPageTrackerActive()) {
1855
- if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
1856
- return;
2030
+ function validateCartIntegrity() {
2031
+ const cart = getCart();
2032
+ const metadata = getCartMetadata();
2033
+ const checksum = computeChecksum(cart);
2034
+ if (!metadata) {
2035
+ setCartWithMetadata(cart, null);
2036
+ return true;
1857
2037
  }
1858
- const sendView = async () => {
1859
- const order2 = reservedOrder ? await reservedOrder : void 0;
1860
- const settle2 = order2 ? claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1861
- const delivered = await postPing({
1862
- referer: document.referrer || void 0,
1863
- cart_value: cartValue,
1864
- item_count: itemCount,
1865
- cart_seq: order2?.seq,
1866
- cart_seq_scope: order2?.scope,
1867
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
1868
- path: resolveStorefrontPagePath(),
1869
- utm: resolveUtmParameters(config2.storeSlug)
1870
- });
1871
- settle2?.(delivered);
1872
- };
1873
- await (reservedOrder ? enqueueCartSend(sendView) : sendView());
2038
+ return metadata.checksum === checksum;
1874
2039
  }
1875
- async function trackCartActivity(cartValue, itemCount) {
1876
- if (!isInitialized()) return;
1877
- if (typeof document === "undefined") return;
1878
- const snapshot = cartSnapshot(cartValue, itemCount);
1879
- const reservedOrder = reserveCartOrder(getConfig().storeSlug);
1880
- await enqueueCartSend(async (isLatest) => {
1881
- const order2 = await reservedOrder;
1882
- if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
1883
- await sendCartActivity(order2, cartValue, itemCount);
1884
- });
2040
+ function getCartStats() {
2041
+ const cart = getCart();
2042
+ const integrityValid = validateCartIntegrity();
2043
+ const metadata = getCartMetadata();
2044
+ const backup = readCartStorage("cartBackup") ?? [];
2045
+ const hasCompletePriceSnapshots = cart.length > 0 && cart.every((item) => typeof item.price_data?.unit_price === "number");
2046
+ const totalPrice = cart.reduce((sum, item) => {
2047
+ const unitPrice = item.price_data?.unit_price ?? 0;
2048
+ return sum + roundPayableAmount(unitPrice * item.quantity);
2049
+ }, 0);
2050
+ return {
2051
+ item_count: cart.length,
2052
+ total_quantity: cart.reduce((sum, item) => sum + item.quantity, 0),
2053
+ last_modified: metadata?.last_modified ?? 0,
2054
+ version: metadata?.version ?? 0,
2055
+ has_backup: backup.length > 0,
2056
+ integrity_valid: integrityValid,
2057
+ total_price: roundPayableAmount(totalPrice),
2058
+ total_price_is_estimate: cart.length > 0 && !hasCompletePriceSnapshots
2059
+ };
1885
2060
  }
1886
- async function sendCartActivity(order2, cartValue, itemCount) {
2061
+ function getCartPayload(coupon) {
1887
2062
  const config2 = getConfig();
1888
- const settle2 = claimCartReport(config2.storeSlug, order2.seq, cartValue, itemCount);
1889
- const delivered = await postPing({
1890
- event_type: "presence",
1891
- cart_value: cartValue,
1892
- item_count: itemCount,
1893
- cart_seq: order2.seq,
1894
- cart_seq_scope: order2.scope,
1895
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
1896
- path: resolveStorefrontPagePath()
2063
+ const normalizedCoupon = normalizeCouponCode(coupon) ?? getCartCoupon() ?? void 0;
2064
+ return {
2065
+ store_slug: config2.storeSlug,
2066
+ items: getCart(),
2067
+ coupon: normalizedCoupon
2068
+ };
2069
+ }
2070
+ function getAmbientAffiliateCodeForQuote() {
2071
+ const storedCode = getCartCoupon();
2072
+ const storedSource = getCartCouponSource();
2073
+ if (storedSource === "AFFILIATE" && storedCode) {
2074
+ return storedCode.trim().toLowerCase();
2075
+ }
2076
+ return getAffiliateCode();
2077
+ }
2078
+ var latestQuoteToken = null;
2079
+ var quoteSequence = 0;
2080
+ function getLatestQuoteToken() {
2081
+ return latestQuoteToken;
2082
+ }
2083
+ function clearLatestQuoteToken() {
2084
+ latestQuoteToken = null;
2085
+ quoteSequence += 1;
2086
+ }
2087
+ async function quoteCart(coupon, currency) {
2088
+ const sequence = quoteSequence += 1;
2089
+ const payload = getCartPayload(coupon);
2090
+ const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
2091
+ const response = await post("/v1/storefront/cart/quote", {
2092
+ shop_slug: payload.store_slug,
2093
+ cart: payload.items,
2094
+ coupon: payload.coupon,
2095
+ affiliate_code: getAmbientAffiliateCodeForQuote() ?? void 0,
2096
+ currency: normalizedCurrency ?? void 0
1897
2097
  });
1898
- settle2(delivered);
2098
+ if (sequence !== quoteSequence) return response;
2099
+ if (response.success) {
2100
+ latestQuoteToken = typeof response.data?.quote_token === "string" ? response.data.quote_token : null;
2101
+ }
2102
+ return response;
1899
2103
  }
1900
2104
 
1901
2105
  // ../sdk/src/storefront-storage-sdk.ts
@@ -1941,7 +2145,8 @@ var storefrontStorageSdk = {
1941
2145
  trackAffiliateEvent,
1942
2146
  // Analytics
1943
2147
  trackPageView,
1944
- trackCartActivity
2148
+ trackCartActivity,
2149
+ trackProductView
1945
2150
  };
1946
2151
 
1947
2152
  // ../../node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
@@ -2789,16 +2994,6 @@ function isSafeHttpsRedirectTemplateUrl(url2) {
2789
2994
  }
2790
2995
  }
2791
2996
 
2792
- // ../contracts/src/storefront-consent-choice.ts
2793
- var COOKIE_CONSENT_VERSION = 1;
2794
- var COOKIE_CONSENT_COOKIE = "shoppex_consent";
2795
- var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
2796
- var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
2797
- function parseCookieConsentValue(value) {
2798
- const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
2799
- return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
2800
- }
2801
-
2802
2997
  // ../sdk/src/modules/checkout.ts
2803
2998
  var CheckoutCreateError = class _CheckoutCreateError extends Error {
2804
2999
  constructor(message, options = {}) {
@@ -3097,7 +3292,10 @@ async function performCheckout(resolvedOptions) {
3097
3292
  // automatically so the server can refuse an invoice priced above it.
3098
3293
  // Undefined when nothing has been quoted this session — the endpoint
3099
3294
  // treats that exactly as an older SDK.
3100
- quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0
3295
+ quote_token: resolvedOptions.quoteToken !== void 0 ? resolvedOptions.quoteToken ?? void 0 : getLatestQuoteToken() ?? void 0,
3296
+ // Where the buyer came from: their analytics sessions after a yes on the
3297
+ // cookie banner, `declined` after a no. The server looks up the sources.
3298
+ attribution: checkoutAttribution(config2.storeSlug)
3101
3299
  };
3102
3300
  const createAttempt = acquireCheckoutCreateIdempotency(checkoutRequestTarget, createIntent);
3103
3301
  const response = await post(
@@ -3946,7 +4144,6 @@ async function touchStorefrontPresence() {
3946
4144
  {
3947
4145
  event_type: "presence",
3948
4146
  referer: document.referrer || void 0,
3949
- connection_id: getStorefrontConnectionId(config2.storeSlug) ?? void 0,
3950
4147
  path: resolveStorefrontPagePath(),
3951
4148
  utm: resolveUtmParameters(config2.storeSlug)
3952
4149
  },
@@ -24196,6 +24393,27 @@ var customerPortalAffiliateLedgerStatusSchema = external_exports.enum([
24196
24393
  "split",
24197
24394
  "voided"
24198
24395
  ]);
24396
+ var customerPortalAffiliateCommissionBreakdownSchema = external_exports.object({
24397
+ lines: external_exports.array(external_exports.object({
24398
+ product_id: external_exports.string().nullable(),
24399
+ title: external_exports.string(),
24400
+ base_usd: external_exports.string(),
24401
+ percent: external_exports.number(),
24402
+ source: external_exports.enum(["PRODUCT_DISABLED", "LINK", "AFFILIATE", "TIER", "PRODUCT", "SHOP_DEFAULT"]),
24403
+ amount_usd: external_exports.string()
24404
+ })),
24405
+ base_usd: external_exports.string(),
24406
+ gross_usd: external_exports.string(),
24407
+ cap_usd: nullableStringSchema,
24408
+ cap_applied: external_exports.boolean(),
24409
+ minimum_order_usd: nullableStringSchema,
24410
+ minimum_met: external_exports.boolean(),
24411
+ shareback_usd: external_exports.string(),
24412
+ amount_usd: external_exports.string(),
24413
+ tier_name: nullableStringSchema,
24414
+ attribution_source: external_exports.enum(["CODE", "BINDING"]),
24415
+ bound_at: nullableStringSchema
24416
+ });
24199
24417
  var customerPortalAffiliateSummarySchema = external_exports.object({
24200
24418
  customer: external_exports.object({
24201
24419
  id: external_exports.string(),
@@ -24238,6 +24456,7 @@ var customerPortalAffiliateSummarySchema = external_exports.object({
24238
24456
  store_credit_amount: external_exports.string(),
24239
24457
  base_amount: external_exports.string(),
24240
24458
  discount_amount: external_exports.string(),
24459
+ breakdown: customerPortalAffiliateCommissionBreakdownSchema.nullable(),
24241
24460
  status: external_exports.string(),
24242
24461
  created_at: nullableStringSchema,
24243
24462
  paid_at: nullableStringSchema,
@@ -24701,19 +24920,29 @@ function createTicket(payload) {
24701
24920
  body: {
24702
24921
  ...payload.title !== void 0 ? { title: payload.title } : {},
24703
24922
  message: payload.message,
24704
- ...payload.invoice_id !== void 0 ? { invoice_id: payload.invoice_id } : {}
24923
+ ...payload.invoice_id !== void 0 ? { invoice_id: payload.invoice_id } : {},
24924
+ ...payload.attachment_ids?.length ? { attachment_ids: payload.attachment_ids } : {}
24705
24925
  }
24706
24926
  });
24707
24927
  }
24708
24928
  function ticket(uniqid) {
24709
24929
  return requestCustomer(`/tickets/${encodeURIComponent(uniqid)}`);
24710
24930
  }
24711
- function replyToTicket(uniqid, message) {
24931
+ function replyToTicket(uniqid, message, attachmentIds = []) {
24712
24932
  return requestCustomer(`/tickets/${encodeURIComponent(uniqid)}/reply`, {
24713
24933
  method: "POST",
24714
- body: { message }
24934
+ body: attachmentIds.length > 0 ? { message, attachment_ids: attachmentIds } : { message }
24715
24935
  });
24716
24936
  }
24937
+ function uploadTicketImage(file2, ticket2) {
24938
+ const form = new FormData();
24939
+ form.append("file", file2);
24940
+ if (ticket2) form.append("ticket", ticket2);
24941
+ return requestCustomer("/ticket-attachments", { method: "POST", formData: form });
24942
+ }
24943
+ function discardTicketImage(id) {
24944
+ return requestCustomer(`/ticket-attachments/${encodeURIComponent(id)}`, { method: "DELETE" });
24945
+ }
24717
24946
  function updateProfile(patch) {
24718
24947
  return requestCustomer("/profile", {
24719
24948
  method: "POST",
@@ -24839,6 +25068,8 @@ var shoppex = {
24839
25068
  createTicket,
24840
25069
  ticket,
24841
25070
  replyToTicket,
25071
+ uploadTicketImage,
25072
+ discardTicketImage,
24842
25073
  updateProfile,
24843
25074
  updateAvatar,
24844
25075
  removeAvatar,
@@ -24896,6 +25127,7 @@ if (typeof window !== "undefined") {
24896
25127
  createAffiliateLink,
24897
25128
  createTicket,
24898
25129
  dashboard,
25130
+ discardTicketImage,
24899
25131
  emailPreferences,
24900
25132
  enrollAsReseller,
24901
25133
  ensureCartLineId,
@@ -24963,11 +25195,13 @@ if (typeof window !== "undefined") {
24963
25195
  touchStorefrontPresence,
24964
25196
  trackCartActivity,
24965
25197
  trackPageView,
25198
+ trackProductView,
24966
25199
  undoSubscriptionCancel,
24967
25200
  updateAvatar,
24968
25201
  updateEmailPreferences,
24969
25202
  updateNotificationPreferences,
24970
25203
  updateProfile,
25204
+ uploadTicketImage,
24971
25205
  validateStorefrontCustomFieldValue,
24972
25206
  verifyOtp,
24973
25207
  warranties