@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.js CHANGED
@@ -5,11 +5,12 @@ import {
5
5
  customerPortalDashboardSchema,
6
6
  customerPortalInvoiceDetailSchema,
7
7
  customerPortalNotificationPreferencesSchema,
8
+ customerPortalOrderReviewSchema,
8
9
  customerPortalPaginatedInvoicesSchema,
9
10
  customerWarrantyClaimSchema,
10
11
  customerWarrantyListSchema,
11
12
  storefrontStorageKey
12
- } from "./chunk-ESI3HWQF.js";
13
+ } from "./chunk-NTLZJQ6I.js";
13
14
 
14
15
  // ../sdk/src/core/errors.ts
15
16
  var CURRENCY_UNAVAILABLE_ERROR_CODE = "errors.storefront.currency_unavailable";
@@ -836,1101 +837,1141 @@ async function captureAffiliateFromUrl(param = "ref") {
836
837
  return holdsOtherCode ? held.code : code;
837
838
  }
838
839
 
839
- // ../sdk/src/utils/cart-line-id.ts
840
- function hashString(value) {
841
- let hash = 2166136261;
842
- for (let i = 0; i < value.length; i += 1) {
843
- hash ^= value.charCodeAt(i);
844
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
840
+ // ../contracts/src/storefront-consent-choice.ts
841
+ var COOKIE_CONSENT_VERSION = 1;
842
+ var COOKIE_CONSENT_COOKIE = "shoppex_consent";
843
+ var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
844
+ var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
845
+ function parseCookieConsentValue(value) {
846
+ const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
847
+ return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
848
+ }
849
+
850
+ // ../sdk/src/core/analytics-session.ts
851
+ var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
852
+ var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
853
+ var ANALYTICS_VISITS_MAX = 20;
854
+ var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
855
+ var UTM_STORAGE_PREFIX = "shoppex:utm:";
856
+ var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
857
+ var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
858
+ function pageLanding() {
859
+ let r = "";
860
+ try {
861
+ r = document.referrer ? new URL(document.referrer).hostname : "";
862
+ } catch {
845
863
  }
846
- return (hash >>> 0).toString(16);
864
+ const params = new URLSearchParams(window.location?.search ?? "");
865
+ const u = ["source", "medium", "campaign"].map((key) => params.get(`utm_${key}`)?.trim().toLowerCase() ?? "").join("|");
866
+ return { r: r === window.location?.hostname ? "" : r, u: u === "||" ? "" : u };
847
867
  }
848
- function normalizeAddons(addons) {
849
- if (!addons?.length) return [];
850
- return [...addons].map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 })).sort((a, b) => a.id.localeCompare(b.id));
868
+ function readAnalyticsConsent() {
869
+ if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
870
+ const privacy = navigator;
871
+ if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
872
+ const prefix = `${COOKIE_CONSENT_COOKIE}=`;
873
+ let raw;
874
+ try {
875
+ raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
876
+ } catch {
877
+ return "unknown";
878
+ }
879
+ const choice = parseCookieConsentValue(raw ?? null);
880
+ if (!choice) return "unknown";
881
+ return choice.analytics ? "granted" : "declined";
851
882
  }
852
- function normalizeCustomFields(fields) {
853
- if (!fields) return {};
854
- const entries = Object.entries(fields).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b));
855
- return Object.fromEntries(entries);
883
+ function readVisits(storeSlug, now) {
884
+ const stored = getItem(analyticsVisitsKey(storeSlug));
885
+ if (!Array.isArray(stored)) return [];
886
+ return stored.filter((visit) => typeof visit?.id === "string" && typeof visit.at === "number" && now - visit.at < ANALYTICS_VISITS_WINDOW_MS);
856
887
  }
857
- function computeCartLineId(input) {
858
- const payload = {
859
- product_id: input.product_id,
860
- variant_id: input.variant_id,
861
- price_variant_id: input.price_variant_id ?? null,
862
- addons: normalizeAddons(input.addons),
863
- custom_fields: normalizeCustomFields(input.custom_fields),
864
- unit_price: typeof input.price_data?.unit_price === "number" && Number.isFinite(input.price_data.unit_price) ? input.price_data.unit_price : null,
865
- pay_what_you_want_price: typeof input.pay_what_you_want_price === "number" && Number.isFinite(input.pay_what_you_want_price) ? input.pay_what_you_want_price : null
866
- };
867
- return hashString(JSON.stringify(payload));
888
+ function forgetAnalyticsData(storeSlug) {
889
+ removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
890
+ removeItem(analyticsVisitsKey(storeSlug));
868
891
  }
869
- function ensureCartLineId(item) {
870
- if (typeof item.line_id === "string" && item.line_id.trim()) {
871
- return item;
892
+ function analyticsSessionId(storeSlug, options, now = Date.now()) {
893
+ removeItem(`presence_connection_${storeSlug}`);
894
+ if (readAnalyticsConsent() !== "granted") {
895
+ forgetAnalyticsData(storeSlug);
896
+ return null;
872
897
  }
873
- return { ...item, line_id: computeCartLineId(item) };
898
+ const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
899
+ const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
900
+ if (!options.touch) return live;
901
+ const landing = pageLanding();
902
+ const newLanding = landing.r && landing.r !== stored?.r || landing.u && landing.u !== stored?.u;
903
+ if (live && !newLanding) {
904
+ setItem(analyticsSessionKey(storeSlug), { ...stored, id: live, at: now }, "sessionStorage");
905
+ return live;
906
+ }
907
+ if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
908
+ if (landing.r) {
909
+ try {
910
+ globalThis.localStorage.removeItem(storefrontStorageKey(`${UTM_STORAGE_PREFIX}${storeSlug}`));
911
+ } catch {
912
+ }
913
+ }
914
+ const id = crypto.randomUUID();
915
+ setItem(analyticsSessionKey(storeSlug), { id, at: now, ...landing }, "sessionStorage");
916
+ setItem(
917
+ analyticsVisitsKey(storeSlug),
918
+ [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
919
+ );
920
+ return id;
874
921
  }
875
-
876
- // ../contracts/src/catalog-unit-price.ts
877
- var CATALOG_UNIT_PRICE_DECIMAL_PLACES = 3;
878
- var PAYABLE_AMOUNT_DECIMAL_PLACES = 2;
879
- var CATALOG_UNIT_PRICE_FORMAT_OPTIONS = {
880
- maximumFractionDigits: CATALOG_UNIT_PRICE_DECIMAL_PLACES
881
- };
882
- function roundPayableAmount(value) {
883
- if (!Number.isFinite(value)) return value;
884
- const sign = value < 0 ? -1 : 1;
885
- const shifted = shiftDecimal(Math.abs(value), PAYABLE_AMOUNT_DECIMAL_PLACES);
886
- const rounded = Math.round(shifted);
887
- if (rounded === 0) return 0;
888
- return sign * shiftDecimal(rounded, -PAYABLE_AMOUNT_DECIMAL_PLACES);
922
+ function checkoutAttribution(storeSlug, now = Date.now()) {
923
+ const consent = readAnalyticsConsent();
924
+ if (consent === "declined") {
925
+ forgetAnalyticsData(storeSlug);
926
+ return { declined: true };
927
+ }
928
+ if (consent === "unknown") return void 0;
929
+ const sessionIds = readVisits(storeSlug, now).map((visit) => visit.id);
930
+ return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
889
931
  }
890
- function shiftDecimal(value, places) {
891
- const [coefficient, currentExponent = "0"] = String(value).split("e");
892
- return Number(`${coefficient}e${Number(currentExponent) + places}`);
932
+ var engagedMs = 0;
933
+ var visibleSince = null;
934
+ var clockInstalled = false;
935
+ var pageViewed = false;
936
+ var pageSession = null;
937
+ function markPageViewed(sessionId) {
938
+ pageViewed = true;
939
+ pageSession = sessionId;
893
940
  }
894
-
895
- // ../sdk/src/modules/cart.ts
896
- var STORAGE_KEYS = {
897
- cart: "cart",
898
- cartBackup: "cart_backup",
899
- meta: "cart_meta",
900
- metaBackup: "cart_backup_meta",
901
- coupon: "cart_coupon",
902
- couponBackup: "cart_coupon_backup"
903
- };
904
- function getStorageKey(type) {
905
- return `${STORAGE_KEYS[type]}_${getConfig().storeSlug}`;
941
+ function currentPageSession() {
942
+ return pageSession;
906
943
  }
907
- function cartStorageArea() {
908
- return getConfig().preview ? "sessionStorage" : "localStorage";
944
+ function isPageViewed() {
945
+ return pageViewed;
909
946
  }
910
- function readCartStorage(type) {
911
- return getItem(getStorageKey(type), cartStorageArea());
947
+ function settleClock(now) {
948
+ if (visibleSince !== null) {
949
+ engagedMs += now - visibleSince;
950
+ visibleSince = document.visibilityState === "visible" ? now : null;
951
+ }
912
952
  }
913
- function writeCartStorage(type, value) {
914
- setItem(getStorageKey(type), value, cartStorageArea());
953
+ var consentListenerInstalled = false;
954
+ function onAnalyticsConsent(onYes) {
955
+ if (consentListenerInstalled || typeof window === "undefined") return;
956
+ consentListenerInstalled = true;
957
+ window.addEventListener("shoppex:consent", () => {
958
+ if (readAnalyticsConsent() === "granted") onYes();
959
+ });
915
960
  }
916
- function removeCartStorage(type) {
917
- removeItem(getStorageKey(type), cartStorageArea());
961
+ function installEngagedClock(onHidden) {
962
+ if (clockInstalled || typeof document === "undefined") return;
963
+ clockInstalled = true;
964
+ visibleSince = document.visibilityState === "visible" ? Date.now() : null;
965
+ document.addEventListener("visibilitychange", () => {
966
+ const now = Date.now();
967
+ if (document.visibilityState === "visible") {
968
+ visibleSince ?? (visibleSince = now);
969
+ return;
970
+ }
971
+ settleClock(now);
972
+ visibleSince = null;
973
+ onHidden();
974
+ });
975
+ window.addEventListener("pagehide", () => {
976
+ settleClock(Date.now());
977
+ visibleSince = null;
978
+ onHidden();
979
+ });
980
+ window.addEventListener("pageshow", (event) => {
981
+ if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
982
+ });
918
983
  }
919
- function hashString2(value) {
920
- let hash = 2166136261;
921
- for (let i = 0; i < value.length; i += 1) {
922
- hash ^= value.charCodeAt(i);
923
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
984
+ function takeEngagedMs(now = Date.now()) {
985
+ settleClock(now);
986
+ const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
987
+ engagedMs = 0;
988
+ return taken;
989
+ }
990
+
991
+ // ../sdk/src/core/attribution.ts
992
+ var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
993
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
994
+ var MAX_VALUE_LENGTH = 255;
995
+ function readFromUrl(search) {
996
+ let params;
997
+ try {
998
+ params = new URLSearchParams(search);
999
+ } catch {
1000
+ return {};
924
1001
  }
925
- return (hash >>> 0).toString(16);
1002
+ const utm = {};
1003
+ for (const key of UTM_KEYS) {
1004
+ const value = params.get(key)?.trim();
1005
+ if (value) {
1006
+ utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1007
+ }
1008
+ }
1009
+ return utm;
926
1010
  }
927
- function computeChecksum(cart) {
928
- return hashString2(JSON.stringify(cart));
1011
+ function storageKey(storeSlug) {
1012
+ return `${UTM_STORAGE_PREFIX}${storeSlug}`;
929
1013
  }
930
- function normalizeQuantity(value) {
931
- if (!Number.isFinite(value)) {
932
- throw new CartError("quantity must be a finite number");
1014
+ function readStored(storeSlug) {
1015
+ try {
1016
+ const raw = window.localStorage.getItem(storefrontStorageKey(storageKey(storeSlug)));
1017
+ if (!raw) return {};
1018
+ const parsed = JSON.parse(raw);
1019
+ if (!parsed || typeof parsed.at !== "number" || typeof parsed.utm !== "object") return {};
1020
+ if (Date.now() - parsed.at > TTL_MS) {
1021
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1022
+ return {};
1023
+ }
1024
+ const utm = {};
1025
+ for (const key of UTM_KEYS) {
1026
+ const value = parsed.utm?.[key];
1027
+ if (typeof value === "string" && value.length > 0) {
1028
+ utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1029
+ }
1030
+ }
1031
+ return utm;
1032
+ } catch {
1033
+ return {};
933
1034
  }
934
- return Math.floor(value);
935
1035
  }
936
- function normalizeCouponCode(value) {
937
- const normalized = value?.trim().toUpperCase();
938
- return normalized ? normalized : null;
1036
+ function writeStored(storeSlug, utm) {
1037
+ try {
1038
+ const payload = { utm, at: Date.now() };
1039
+ window.localStorage.setItem(storefrontStorageKey(storageKey(storeSlug)), JSON.stringify(payload));
1040
+ } catch {
1041
+ }
939
1042
  }
940
- function getStoredCartCode() {
941
- const raw = readCartStorage("coupon");
942
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
943
- return null;
1043
+ function resolveUtmParameters(storeSlug) {
1044
+ if (typeof window === "undefined" || typeof window.location === "undefined") {
1045
+ return void 0;
944
1046
  }
945
- const record = raw;
946
- const code = typeof record.code === "string" ? normalizeCouponCode(record.code) : null;
947
- const source = record.source === "COUPON" || record.source === "AFFILIATE" ? record.source : null;
948
- return code && source ? { code, source } : null;
949
- }
950
- function normalizeCartItems(value) {
951
- if (!Array.isArray(value)) return [];
952
- const normalized = [];
953
- for (const entry of value) {
954
- if (!entry || typeof entry !== "object") continue;
955
- const record = entry;
956
- const productId = typeof record.product_id === "string" ? record.product_id.trim() : "";
957
- const variantId = typeof record.variant_id === "string" ? record.variant_id.trim() : "";
958
- const quantity = Number(record.quantity);
959
- if (!productId || !variantId || !Number.isFinite(quantity) || quantity < 1) {
960
- continue;
961
- }
962
- const item = {
963
- line_id: "",
964
- product_id: productId,
965
- variant_id: variantId,
966
- quantity: Math.floor(quantity)
967
- };
968
- if (typeof record.price_variant_id === "string") {
969
- item.price_variant_id = record.price_variant_id;
970
- }
971
- if (record.price_data && typeof record.price_data === "object") {
972
- const priceData = record.price_data;
973
- if (typeof priceData.unit_price === "number" && Number.isFinite(priceData.unit_price)) {
974
- item.price_data = { unit_price: priceData.unit_price };
975
- }
976
- }
977
- if (typeof record.pay_what_you_want_price === "number" && Number.isFinite(record.pay_what_you_want_price)) {
978
- item.pay_what_you_want_price = record.pay_what_you_want_price;
979
- }
980
- if (Array.isArray(record.addons)) {
981
- item.addons = record.addons;
982
- }
983
- if (record.custom_fields && typeof record.custom_fields === "object" && !Array.isArray(record.custom_fields)) {
984
- item.custom_fields = record.custom_fields;
985
- }
986
- if (typeof record.title === "string") {
987
- item.title = record.title;
988
- }
989
- if (typeof record.variant_title === "string") {
990
- item.variant_title = record.variant_title;
991
- }
992
- if (typeof record.image_url === "string") {
993
- item.image_url = record.image_url;
994
- }
995
- if (Array.isArray(record.addon_labels)) {
996
- item.addon_labels = record.addon_labels.filter((label) => typeof label === "string");
997
- }
998
- if (typeof record.max_quantity === "number" && Number.isFinite(record.max_quantity)) {
999
- item.max_quantity = record.max_quantity;
1000
- }
1001
- if (typeof record.min_quantity === "number" && Number.isFinite(record.min_quantity)) {
1002
- item.min_quantity = record.min_quantity;
1047
+ const fromUrl = readFromUrl(window.location.search);
1048
+ if (readAnalyticsConsent() !== "granted") {
1049
+ try {
1050
+ window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1051
+ } catch {
1003
1052
  }
1004
- clampQuantityToBounds(item);
1005
- const storedLineId = typeof record.line_id === "string" ? record.line_id.trim() : "";
1006
- item.line_id = storedLineId || computeCartLineId(item);
1007
- normalized.push(ensureCartLineId(item));
1053
+ return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1008
1054
  }
1009
- return normalized;
1055
+ if (Object.keys(fromUrl).length > 0) {
1056
+ writeStored(storeSlug, fromUrl);
1057
+ return fromUrl;
1058
+ }
1059
+ const stored = readStored(storeSlug);
1060
+ return Object.keys(stored).length > 0 ? stored : void 0;
1010
1061
  }
1011
- function getCartMetadata() {
1012
- return readCartStorage("meta");
1062
+
1063
+ // ../sdk/src/core/page-path.ts
1064
+ var MAX_PATH_LENGTH = 256;
1065
+ function resolveStorefrontPagePath() {
1066
+ if (typeof window === "undefined") return void 0;
1067
+ let pathname;
1068
+ try {
1069
+ const location = window.location;
1070
+ pathname = typeof location?.pathname === "string" && location.pathname ? location.pathname : new URL(String(location?.href ?? "")).pathname;
1071
+ } catch {
1072
+ return void 0;
1073
+ }
1074
+ if (!pathname || !pathname.startsWith("/")) return void 0;
1075
+ return pathname.slice(0, MAX_PATH_LENGTH);
1013
1076
  }
1014
- function getCartCoupon() {
1015
- return getStoredCartCode()?.code ?? null;
1077
+
1078
+ // ../sdk/src/core/page-tracker.ts
1079
+ var PLATFORM_PAGE_TRACKER_FLAG = "__shoppexPageTracker";
1080
+ function isPlatformPageTrackerActive() {
1081
+ if (typeof window === "undefined") return false;
1082
+ try {
1083
+ return Boolean(window[PLATFORM_PAGE_TRACKER_FLAG]);
1084
+ } catch {
1085
+ return false;
1086
+ }
1016
1087
  }
1017
- function getCartCouponSource() {
1018
- return getStoredCartCode()?.source ?? null;
1088
+
1089
+ // ../sdk/src/modules/analytics.ts
1090
+ var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
1091
+ var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
1092
+ var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
1093
+ var CART_CLAIM_TTL_MS = 1e4;
1094
+ function confirmedCartKey(storeSlug) {
1095
+ return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
1019
1096
  }
1020
- function setCartCoupon(coupon, source = "COUPON") {
1021
- const normalizedCoupon = normalizeCouponCode(coupon);
1022
- if (!normalizedCoupon) {
1023
- removeCartStorage("coupon");
1097
+ function inFlightCartKey(storeSlug) {
1098
+ return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
1099
+ }
1100
+ function cartSequenceKey(storeSlug) {
1101
+ return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
1102
+ }
1103
+ function cartSnapshot(cartValue, itemCount) {
1104
+ return `${cartValue ?? ""}|${itemCount ?? ""}`;
1105
+ }
1106
+ function isCartSequence(value) {
1107
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1108
+ }
1109
+ var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
1110
+ function readCartCounter(storeSlug) {
1111
+ const counter = getItem(cartSequenceKey(storeSlug));
1112
+ if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
1024
1113
  return null;
1025
1114
  }
1026
- writeCartStorage("coupon", { code: normalizedCoupon, source });
1027
- return normalizedCoupon;
1115
+ return counter;
1028
1116
  }
1029
- function clearCartCoupon() {
1030
- removeCartStorage("coupon");
1117
+ var lastCartOrder = null;
1118
+ function nextCartOrder(storeSlug) {
1119
+ const stored = readCartCounter(storeSlug);
1120
+ const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
1121
+ const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
1122
+ const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
1123
+ lastCartOrder = order2;
1124
+ setItem(cartSequenceKey(storeSlug), order2);
1125
+ return order2;
1031
1126
  }
1032
- function writeCart(cart) {
1033
- const normalizedCart = normalizeCartItems(cart);
1034
- writeCartStorage("cart", normalizedCart);
1035
- const now = Date.now();
1036
- const previous = getCartMetadata();
1037
- const nextMeta = {
1038
- created_at: previous?.created_at ?? now,
1039
- last_modified: now,
1040
- version: (previous?.version ?? 0) + 1,
1041
- checksum: computeChecksum(normalizedCart)
1042
- };
1043
- writeCartStorage("meta", nextMeta);
1044
- if (normalizedCart.length === 0) {
1045
- clearCartCoupon();
1046
- }
1127
+ function cartSequenceLocks() {
1128
+ if (typeof navigator === "undefined") return null;
1129
+ const locks = navigator.locks;
1130
+ return locks && typeof locks.request === "function" ? locks : null;
1047
1131
  }
1048
- function setCartWithMetadata(cart, metadata) {
1049
- const normalizedCart = normalizeCartItems(cart);
1050
- writeCartStorage("cart", normalizedCart);
1051
- const now = Date.now();
1052
- const base = metadata ?? getCartMetadata();
1053
- const nextMeta = {
1054
- created_at: base?.created_at ?? now,
1055
- last_modified: now,
1056
- version: base?.version ?? 1,
1057
- checksum: computeChecksum(normalizedCart)
1058
- };
1059
- writeCartStorage("meta", nextMeta);
1132
+ function reserveCartOrder(storeSlug) {
1133
+ const take = () => nextCartOrder(storeSlug);
1134
+ const locks = cartSequenceLocks();
1135
+ if (!locks) return Promise.resolve(take());
1136
+ return locks.request(cartSequenceKey(storeSlug), take).catch(take);
1060
1137
  }
1061
- function getCart() {
1062
- const raw = readCartStorage("cart");
1063
- return normalizeCartItems(raw);
1138
+ function readConfirmedCart(storeSlug) {
1139
+ const confirmed = getItem(confirmedCartKey(storeSlug));
1140
+ if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
1141
+ return confirmed;
1064
1142
  }
1065
- function resolveCartLineId(productId, variantId, cart = getCart()) {
1066
- const matches = cart.filter(
1067
- (item) => item.product_id === productId && item.variant_id === variantId
1068
- );
1069
- if (matches.length === 0) {
1070
- throw new CartError(`No cart line found for ${productId}/${variantId}`);
1143
+ function readInFlightCart(storeSlug) {
1144
+ const claim = getItem(inFlightCartKey(storeSlug));
1145
+ if (!claim || typeof claim.snapshot !== "string" || !isCartSequence(claim.seq) || typeof claim.token !== "string" || typeof claim.at !== "number") {
1146
+ return null;
1071
1147
  }
1072
- return matches[0].line_id;
1148
+ return claim;
1073
1149
  }
1074
- function getCartItemCount() {
1075
- const cart = getCart();
1076
- return cart.reduce((sum, item) => sum + item.quantity, 0);
1150
+ function isCartReportedOrInFlight(storeSlug, snapshot) {
1151
+ const confirmed = readConfirmedCart(storeSlug);
1152
+ const claim = readInFlightCart(storeSlug);
1153
+ const liveClaim = claim && Date.now() - claim.at < CART_CLAIM_TTL_MS ? claim : null;
1154
+ const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
1155
+ return expected?.snapshot === snapshot;
1077
1156
  }
1078
- function addToCart(productId, variantId, quantity = 1, options) {
1079
- if (!productId || !variantId) {
1080
- throw new CartError("product_id and variant_id are required");
1081
- }
1082
- const normalizedQuantity = normalizeQuantity(quantity);
1083
- if (normalizedQuantity < 1) {
1084
- throw new CartError("quantity must be at least 1");
1085
- }
1086
- const cart = getCart();
1087
- const lineId = computeCartLineId({
1088
- product_id: productId,
1089
- variant_id: variantId,
1090
- addons: options?.addons,
1091
- custom_fields: options?.custom_fields,
1092
- price_variant_id: options?.price_variant_id,
1093
- price_data: options?.price_data,
1094
- pay_what_you_want_price: options?.pay_what_you_want_price
1095
- });
1096
- const existingIndex = cart.findIndex((item) => item.line_id === lineId);
1097
- const quantityBeforeAdd = existingIndex >= 0 ? cart[existingIndex].quantity : 0;
1098
- if (existingIndex >= 0) {
1099
- cart[existingIndex].quantity += normalizedQuantity;
1100
- if (options?.addons) {
1101
- cart[existingIndex].addons = options.addons;
1102
- }
1103
- if (options?.custom_fields) {
1104
- cart[existingIndex].custom_fields = options.custom_fields;
1105
- }
1106
- if (options?.price_variant_id) {
1107
- cart[existingIndex].price_variant_id = options.price_variant_id;
1108
- }
1109
- if (options?.price_data) {
1110
- cart[existingIndex].price_data = options.price_data;
1157
+ function createClaimToken() {
1158
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1159
+ return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
1160
+ }
1161
+ function claimCartReport(storeSlug, seq, cartValue, itemCount) {
1162
+ const snapshot = cartSnapshot(cartValue, itemCount);
1163
+ const token = createClaimToken();
1164
+ setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
1165
+ return (delivered) => {
1166
+ if (delivered) {
1167
+ const confirmed = readConfirmedCart(storeSlug);
1168
+ if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
1111
1169
  }
1112
- if (options?.pay_what_you_want_price !== void 0) {
1113
- cart[existingIndex].pay_what_you_want_price = options.pay_what_you_want_price;
1114
- }
1115
- applyDisplaySnapshot(cart[existingIndex], options);
1116
- clampQuantityToBounds(cart[existingIndex]);
1117
- } else {
1118
- const pushed = {
1119
- line_id: lineId,
1120
- product_id: productId,
1121
- variant_id: variantId,
1122
- quantity: normalizedQuantity,
1123
- addons: options?.addons,
1124
- custom_fields: options?.custom_fields,
1125
- price_variant_id: options?.price_variant_id,
1126
- price_data: options?.price_data,
1127
- pay_what_you_want_price: options?.pay_what_you_want_price,
1128
- title: options?.title,
1129
- variant_title: options?.variant_title,
1130
- image_url: options?.image_url,
1131
- addon_labels: options?.addon_labels,
1132
- max_quantity: options?.max_quantity,
1133
- min_quantity: options?.min_quantity
1134
- };
1135
- clampQuantityToBounds(pushed);
1136
- cart.push(pushed);
1137
- }
1138
- const intendedQuantity = (existingIndex >= 0 ? cart[existingIndex] : cart[cart.length - 1]).quantity;
1139
- writeCart(cart);
1140
- const persistedLine = getCart().find((item) => item.line_id === lineId);
1141
- if (intendedQuantity > quantityBeforeAdd && persistedLine?.quantity === intendedQuantity) {
1142
- const cartAffiliateCode = getCartCouponSource() === "AFFILIATE" ? getCartCoupon() : null;
1143
- void trackAffiliateEvent("add_to_cart", cartAffiliateCode ? { code: cartAffiliateCode } : void 0);
1144
- }
1145
- }
1146
- function clampQuantityToBounds(item) {
1147
- const min = item.min_quantity;
1148
- if (typeof min === "number" && Number.isFinite(min) && item.quantity < min) {
1149
- item.quantity = Math.floor(min);
1150
- }
1151
- const max = item.max_quantity;
1152
- if (typeof max === "number" && Number.isFinite(max) && item.quantity > max) {
1153
- item.quantity = Math.floor(max);
1154
- }
1155
- if (item.quantity < 1) {
1156
- item.quantity = 1;
1157
- }
1170
+ if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
1171
+ };
1158
1172
  }
1159
- function applyDisplaySnapshot(item, options) {
1160
- if (options?.replace_display_snapshot) {
1161
- item.title = options.title;
1162
- item.variant_title = options.variant_title;
1163
- item.image_url = options.image_url;
1164
- item.addon_labels = options.addon_labels;
1165
- item.max_quantity = options.max_quantity;
1166
- item.min_quantity = options.min_quantity;
1167
- return;
1168
- }
1169
- if (options?.title !== void 0) {
1170
- item.title = options.title;
1171
- }
1172
- if (options?.variant_title !== void 0) {
1173
- item.variant_title = options.variant_title;
1174
- }
1175
- if (options?.image_url !== void 0) {
1176
- item.image_url = options.image_url;
1177
- }
1178
- if (options?.addon_labels !== void 0) {
1179
- item.addon_labels = options.addon_labels;
1180
- }
1181
- if (options?.max_quantity !== void 0) {
1182
- item.max_quantity = options.max_quantity;
1183
- }
1184
- if (options?.min_quantity !== void 0) {
1185
- item.min_quantity = options.min_quantity;
1186
- }
1173
+ var cartSendTail = Promise.resolve();
1174
+ var latestCartRequest = 0;
1175
+ function enqueueCartSend(send) {
1176
+ const request2 = ++latestCartRequest;
1177
+ const run = () => send(() => request2 === latestCartRequest);
1178
+ cartSendTail = cartSendTail.then(run, run);
1179
+ return cartSendTail;
1187
1180
  }
1188
- function setCartItem(productId, variantId, quantity = 1, options) {
1189
- if (!productId || !variantId) {
1190
- throw new CartError("product_id and variant_id are required");
1191
- }
1192
- const normalizedQuantity = normalizeQuantity(quantity);
1193
- if (normalizedQuantity < 1) {
1194
- throw new CartError("quantity must be at least 1");
1195
- }
1196
- const cart = getCart();
1197
- const lineId = computeCartLineId({
1198
- product_id: productId,
1199
- variant_id: variantId,
1200
- addons: options?.addons,
1201
- custom_fields: options?.custom_fields,
1202
- price_variant_id: options?.price_variant_id,
1203
- price_data: options?.price_data,
1204
- pay_what_you_want_price: options?.pay_what_you_want_price
1181
+ async function postPing(body) {
1182
+ const config = getConfig();
1183
+ const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1184
+ let timer;
1185
+ const timedOut = new Promise((resolve) => {
1186
+ timer = setTimeout(() => {
1187
+ controller?.abort();
1188
+ resolve(false);
1189
+ }, CART_CLAIM_TTL_MS);
1205
1190
  });
1206
- for (let index = cart.length - 1; index >= 0; index -= 1) {
1207
- if (cart[index].product_id === productId && cart[index].variant_id === variantId) {
1208
- cart.splice(index, 1);
1209
- }
1210
- }
1211
- const pushed = {
1212
- line_id: lineId,
1213
- product_id: productId,
1214
- variant_id: variantId,
1215
- quantity: normalizedQuantity,
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
- title: options?.title,
1222
- variant_title: options?.variant_title,
1223
- image_url: options?.image_url,
1224
- addon_labels: options?.addon_labels,
1225
- max_quantity: options?.max_quantity,
1226
- min_quantity: options?.min_quantity
1227
- };
1228
- clampQuantityToBounds(pushed);
1229
- cart.push(pushed);
1230
- writeCart(cart);
1231
- }
1232
- function updateCartItem(lineId, updates) {
1233
- const cart = getCart();
1234
- const normalizedLineId = lineId.trim();
1235
- if (!normalizedLineId) {
1236
- throw new CartError("line_id is required");
1237
- }
1238
- const index = cart.findIndex((item) => item.line_id === normalizedLineId);
1239
- if (index < 0) {
1240
- throw new CartError("Item not found in cart");
1241
- }
1242
- const before = JSON.stringify(cart);
1243
- if (updates.quantity !== void 0) {
1244
- const normalizedQuantity = normalizeQuantity(updates.quantity);
1245
- if (normalizedQuantity < 1) {
1246
- cart.splice(index, 1);
1247
- writeCart(cart);
1248
- return;
1249
- }
1250
- cart[index].quantity = normalizedQuantity;
1251
- }
1252
- if (updates.addons !== void 0) {
1253
- cart[index].addons = updates.addons;
1254
- }
1255
- if (updates.custom_fields !== void 0) {
1256
- cart[index].custom_fields = updates.custom_fields;
1257
- }
1258
- if (updates.price_variant_id !== void 0) {
1259
- cart[index].price_variant_id = updates.price_variant_id;
1260
- }
1261
- if (updates.price_data !== void 0) {
1262
- cart[index].price_data = updates.price_data;
1263
- }
1264
- if (updates.pay_what_you_want_price !== void 0) {
1265
- cart[index].pay_what_you_want_price = updates.pay_what_you_want_price;
1266
- }
1267
- if (updates.variant_title !== void 0) {
1268
- if (updates.variant_title === null) delete cart[index].variant_title;
1269
- else cart[index].variant_title = updates.variant_title;
1270
- }
1271
- if (updates.max_quantity !== void 0) {
1272
- if (updates.max_quantity === null) delete cart[index].max_quantity;
1273
- else cart[index].max_quantity = updates.max_quantity;
1274
- }
1275
- if (updates.min_quantity !== void 0) {
1276
- if (updates.min_quantity === null) delete cart[index].min_quantity;
1277
- else cart[index].min_quantity = updates.min_quantity;
1278
- }
1279
- clampQuantityToBounds(cart[index]);
1280
- const nextLineId = computeCartLineId(cart[index]);
1281
- const duplicateIndex = cart.findIndex((item, itemIndex) => itemIndex !== index && item.line_id === nextLineId);
1282
- if (duplicateIndex >= 0) {
1283
- cart[duplicateIndex].quantity += cart[index].quantity;
1284
- clampQuantityToBounds(cart[duplicateIndex]);
1285
- cart.splice(index, 1);
1286
- } else {
1287
- cart[index].line_id = nextLineId;
1191
+ try {
1192
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", {
1193
+ storeSlug: config.storeSlug
1194
+ });
1195
+ const delivered = fetch(`${config.apiBaseUrl}${endpoint}`, {
1196
+ method: "POST",
1197
+ headers: { "Content-Type": "application/json" },
1198
+ body: JSON.stringify(body),
1199
+ signal: controller?.signal
1200
+ }).then((response) => response.ok);
1201
+ return await Promise.race([delivered, timedOut]);
1202
+ } catch {
1203
+ return false;
1204
+ } finally {
1205
+ clearTimeout(timer);
1288
1206
  }
1289
- if (JSON.stringify(cart) === before) return;
1290
- writeCart(cart);
1291
1207
  }
1292
- function removeFromCart(lineId) {
1293
- const normalizedLineId = lineId.trim();
1294
- if (!normalizedLineId) {
1295
- throw new CartError("line_id is required");
1208
+ async function trackPageView(cartValue, itemCount) {
1209
+ if (!isInitialized()) return;
1210
+ if (typeof document === "undefined") return;
1211
+ const config = getConfig();
1212
+ const carriesCart = cartValue !== void 0 || itemCount !== void 0;
1213
+ const reservedOrder = carriesCart ? reserveCartOrder(config.storeSlug) : void 0;
1214
+ if (isPlatformPageTrackerActive()) {
1215
+ if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
1216
+ return;
1296
1217
  }
1297
- const cart = getCart();
1298
- const filtered = cart.filter((item) => item.line_id !== normalizedLineId);
1299
- writeCart(filtered);
1218
+ const sendView = async () => {
1219
+ const order2 = reservedOrder ? await reservedOrder : void 0;
1220
+ const settle = order2 ? claimCartReport(config.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1221
+ const sessionId = analyticsSessionId(config.storeSlug, { touch: true });
1222
+ const previousSession = currentPageSession();
1223
+ if (previousSession && previousSession !== sessionId) reportPageLeave();
1224
+ markPageViewed(sessionId);
1225
+ if (sessionId) installEngagedClock(reportPageLeave);
1226
+ onAnalyticsConsent(startSessionOnConsent);
1227
+ const delivered = await postPing({
1228
+ referer: document.referrer || void 0,
1229
+ cart_value: cartValue,
1230
+ item_count: itemCount,
1231
+ cart_seq: order2?.seq,
1232
+ cart_seq_scope: order2?.scope,
1233
+ session_id: sessionId ?? void 0,
1234
+ // Time on screen of the page before this one (client-side navigation).
1235
+ engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1236
+ path: resolveStorefrontPagePath(),
1237
+ utm: resolveUtmParameters(config.storeSlug)
1238
+ });
1239
+ settle?.(delivered);
1240
+ };
1241
+ await (reservedOrder ? enqueueCartSend(sendView) : sendView());
1300
1242
  }
1301
- function clearCart() {
1302
- removeCartStorage("cart");
1303
- removeCartStorage("meta");
1304
- clearCartCoupon();
1305
- clearLatestQuoteToken();
1243
+ async function trackCartActivity(cartValue, itemCount) {
1244
+ if (!isInitialized()) return;
1245
+ if (typeof document === "undefined") return;
1246
+ const snapshot = cartSnapshot(cartValue, itemCount);
1247
+ const reservedOrder = reserveCartOrder(getConfig().storeSlug);
1248
+ await enqueueCartSend(async (isLatest) => {
1249
+ const order2 = await reservedOrder;
1250
+ if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
1251
+ await sendCartActivity(order2, cartValue, itemCount);
1252
+ });
1306
1253
  }
1307
- function createCartBackup() {
1308
- const cart = getCart();
1309
- writeCartStorage("cartBackup", cart);
1310
- const cartCode = getStoredCartCode();
1311
- if (cartCode) {
1312
- writeCartStorage("couponBackup", cartCode);
1313
- } else {
1314
- removeCartStorage("couponBackup");
1315
- }
1316
- const metadata = getCartMetadata();
1317
- if (metadata) {
1318
- writeCartStorage("metaBackup", metadata);
1319
- } else {
1320
- const now = Date.now();
1321
- writeCartStorage("metaBackup", {
1322
- created_at: now,
1323
- last_modified: now,
1324
- version: 1,
1325
- checksum: computeChecksum(cart)
1326
- });
1327
- }
1254
+ async function sendCartActivity(order2, cartValue, itemCount) {
1255
+ const config = getConfig();
1256
+ const settle = claimCartReport(config.storeSlug, order2.seq, cartValue, itemCount);
1257
+ const delivered = await postPing({
1258
+ event_type: "presence",
1259
+ cart_value: cartValue,
1260
+ item_count: itemCount,
1261
+ cart_seq: order2.seq,
1262
+ cart_seq_scope: order2.scope,
1263
+ // A cart marks the analytics session as one that did not bounce.
1264
+ session_id: analyticsSessionId(config.storeSlug, { touch: false }) ?? void 0,
1265
+ path: resolveStorefrontPagePath()
1266
+ });
1267
+ settle(delivered);
1328
1268
  }
1329
- function restoreCartFromBackup() {
1330
- const backupRaw = readCartStorage("cartBackup");
1331
- const backup = normalizeCartItems(backupRaw);
1332
- const backupMeta = readCartStorage("metaBackup");
1333
- const backupCoupon = readCartStorage("couponBackup");
1334
- if (backup && backup.length > 0) {
1335
- setCartWithMetadata(backup, backupMeta);
1336
- if (backupCoupon && typeof backupCoupon === "object" && !Array.isArray(backupCoupon)) {
1337
- const record = backupCoupon;
1338
- const source = record.source === "COUPON" || record.source === "AFFILIATE" ? record.source : null;
1339
- if (typeof record.code === "string" && source) {
1340
- setCartCoupon(record.code, source);
1341
- } else {
1342
- clearCartCoupon();
1343
- }
1344
- } else {
1345
- clearCartCoupon();
1346
- }
1347
- return true;
1348
- }
1349
- return false;
1269
+ async function trackProductView(productId) {
1270
+ if (!isInitialized() || typeof document === "undefined" || !productId) return;
1271
+ await sendVisitEvent({ event_type: "product_view", product_id: productId });
1350
1272
  }
1351
- function mergeBaskets(items) {
1352
- const cart = getCart();
1353
- for (const incoming of items) {
1354
- const productId = typeof incoming.product_id === "string" ? incoming.product_id.trim() : "";
1355
- const variantId = typeof incoming.variant_id === "string" ? incoming.variant_id.trim() : "";
1356
- if (!productId || !variantId) {
1357
- continue;
1358
- }
1359
- let normalizedQuantity;
1360
- try {
1361
- normalizedQuantity = normalizeQuantity(incoming.quantity);
1362
- } catch {
1363
- continue;
1364
- }
1365
- if (normalizedQuantity < 1) {
1366
- continue;
1367
- }
1368
- const mergedLine = ensureCartLineId({
1369
- ...incoming,
1370
- line_id: incoming.line_id ?? computeCartLineId(incoming),
1371
- product_id: productId,
1372
- variant_id: variantId,
1373
- quantity: normalizedQuantity
1374
- });
1375
- const index = cart.findIndex((item) => item.line_id === mergedLine.line_id);
1376
- if (index >= 0) {
1377
- cart[index].quantity = Math.max(cart[index].quantity, normalizedQuantity);
1378
- clampQuantityToBounds(cart[index]);
1379
- } else {
1380
- clampQuantityToBounds(mergedLine);
1381
- cart.push(mergedLine);
1382
- }
1383
- }
1384
- writeCart(cart);
1385
- return cart;
1273
+ async function trackCartAdd(productId, variantId, quantity) {
1274
+ if (!isInitialized() || typeof document === "undefined" || quantity < 1) return;
1275
+ await sendVisitEvent({ event_type: "cart_add", product_id: productId, variant_id: variantId, quantity });
1386
1276
  }
1387
- function moveBasketItem(fromProductId, fromVariantId, toProductId, toVariantId) {
1388
- const cart = getCart();
1389
- const fromIndex = cart.findIndex(
1390
- (item) => item.product_id === fromProductId && item.variant_id === fromVariantId
1391
- );
1392
- if (fromIndex < 0) {
1393
- throw new CartError("Item not found in cart");
1394
- }
1395
- const [fromItem] = cart.splice(fromIndex, 1);
1396
- const toLineId = computeCartLineId({
1397
- ...fromItem,
1398
- product_id: toProductId,
1399
- variant_id: toVariantId
1277
+ async function sendVisitEvent(body) {
1278
+ const config = getConfig();
1279
+ if (config.preview) return;
1280
+ await postPing({
1281
+ ...body,
1282
+ referer: document.referrer || void 0,
1283
+ session_id: analyticsSessionId(config.storeSlug, { touch: true }) ?? void 0,
1284
+ path: resolveStorefrontPagePath(),
1285
+ utm: resolveUtmParameters(config.storeSlug)
1400
1286
  });
1401
- const toIndex = cart.findIndex((item) => item.line_id === toLineId);
1402
- if (toIndex >= 0) {
1403
- cart[toIndex].quantity += fromItem.quantity;
1404
- clampQuantityToBounds(cart[toIndex]);
1405
- } else {
1406
- const moved = ensureCartLineId({
1407
- ...fromItem,
1408
- product_id: toProductId,
1409
- variant_id: toVariantId,
1410
- line_id: toLineId
1411
- });
1412
- clampQuantityToBounds(moved);
1413
- cart.push(moved);
1414
- }
1415
- writeCart(cart);
1416
- }
1417
- function validateCartIntegrity() {
1418
- const cart = getCart();
1419
- const metadata = getCartMetadata();
1420
- const checksum = computeChecksum(cart);
1421
- if (!metadata) {
1422
- setCartWithMetadata(cart, null);
1423
- return true;
1424
- }
1425
- return metadata.checksum === checksum;
1426
1287
  }
1427
- function getCartStats() {
1428
- const cart = getCart();
1429
- const integrityValid = validateCartIntegrity();
1430
- const metadata = getCartMetadata();
1431
- const backup = readCartStorage("cartBackup") ?? [];
1432
- const hasCompletePriceSnapshots = cart.length > 0 && cart.every((item) => typeof item.price_data?.unit_price === "number");
1433
- const totalPrice = cart.reduce((sum, item) => {
1434
- const unitPrice = item.price_data?.unit_price ?? 0;
1435
- return sum + roundPayableAmount(unitPrice * item.quantity);
1436
- }, 0);
1437
- return {
1438
- item_count: cart.length,
1439
- total_quantity: cart.reduce((sum, item) => sum + item.quantity, 0),
1440
- last_modified: metadata?.last_modified ?? 0,
1441
- version: metadata?.version ?? 0,
1442
- has_backup: backup.length > 0,
1443
- integrity_valid: integrityValid,
1444
- total_price: roundPayableAmount(totalPrice),
1445
- total_price_is_estimate: cart.length > 0 && !hasCompletePriceSnapshots
1446
- };
1288
+ function startSessionOnConsent() {
1289
+ const config = getConfig();
1290
+ if (!isPageViewed() || analyticsSessionId(config.storeSlug, { touch: false })) return;
1291
+ const sessionId = analyticsSessionId(config.storeSlug, { touch: true });
1292
+ if (!sessionId) return;
1293
+ markPageViewed(sessionId);
1294
+ installEngagedClock(reportPageLeave);
1295
+ void postPing({
1296
+ event_type: "session_start",
1297
+ referer: document.referrer || void 0,
1298
+ session_id: sessionId,
1299
+ path: resolveStorefrontPagePath(),
1300
+ utm: resolveUtmParameters(config.storeSlug)
1301
+ });
1447
1302
  }
1448
- function getCartPayload(coupon) {
1303
+ function reportPageLeave() {
1449
1304
  const config = getConfig();
1450
- const normalizedCoupon = normalizeCouponCode(coupon) ?? getCartCoupon() ?? void 0;
1451
- return {
1452
- store_slug: config.storeSlug,
1453
- items: getCart(),
1454
- coupon: normalizedCoupon
1455
- };
1305
+ const sessionId = currentPageSession();
1306
+ const engagedMs2 = takeEngagedMs();
1307
+ if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
1308
+ try {
1309
+ const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config.storeSlug });
1310
+ void fetch(`${config.apiBaseUrl}${endpoint}`, {
1311
+ method: "POST",
1312
+ headers: { "Content-Type": "application/json" },
1313
+ body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
1314
+ keepalive: true
1315
+ }).catch(() => {
1316
+ });
1317
+ } catch {
1318
+ }
1456
1319
  }
1457
- function getAmbientAffiliateCodeForQuote() {
1458
- const storedCode = getCartCoupon();
1459
- const storedSource = getCartCouponSource();
1460
- if (storedSource === "AFFILIATE" && storedCode) {
1461
- return storedCode.trim().toLowerCase();
1320
+
1321
+ // ../sdk/src/utils/cart-line-id.ts
1322
+ function hashString(value) {
1323
+ let hash = 2166136261;
1324
+ for (let i = 0; i < value.length; i += 1) {
1325
+ hash ^= value.charCodeAt(i);
1326
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
1462
1327
  }
1463
- return getAffiliateCode();
1328
+ return (hash >>> 0).toString(16);
1464
1329
  }
1465
- var latestQuoteToken = null;
1466
- var quoteSequence = 0;
1467
- function getLatestQuoteToken() {
1468
- return latestQuoteToken;
1330
+ function normalizeAddons(addons) {
1331
+ if (!addons?.length) return [];
1332
+ return [...addons].map((addon) => ({ id: addon.id, quantity: addon.quantity ?? 1 })).sort((a, b) => a.id.localeCompare(b.id));
1469
1333
  }
1470
- function clearLatestQuoteToken() {
1471
- latestQuoteToken = null;
1472
- quoteSequence += 1;
1334
+ function normalizeCustomFields(fields) {
1335
+ if (!fields) return {};
1336
+ const entries = Object.entries(fields).filter(([, value]) => typeof value === "string").sort(([a], [b]) => a.localeCompare(b));
1337
+ return Object.fromEntries(entries);
1473
1338
  }
1474
- async function quoteCart(coupon, currency) {
1475
- const sequence = quoteSequence += 1;
1476
- const payload = getCartPayload(coupon);
1477
- const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
1478
- const response = await post("/v1/storefront/cart/quote", {
1479
- shop_slug: payload.store_slug,
1480
- cart: payload.items,
1481
- coupon: payload.coupon,
1482
- affiliate_code: getAmbientAffiliateCodeForQuote() ?? void 0,
1483
- currency: normalizedCurrency ?? void 0
1484
- });
1485
- if (sequence !== quoteSequence) return response;
1486
- if (response.success) {
1487
- latestQuoteToken = typeof response.data?.quote_token === "string" ? response.data.quote_token : null;
1339
+ function computeCartLineId(input) {
1340
+ const payload = {
1341
+ product_id: input.product_id,
1342
+ variant_id: input.variant_id,
1343
+ price_variant_id: input.price_variant_id ?? null,
1344
+ addons: normalizeAddons(input.addons),
1345
+ custom_fields: normalizeCustomFields(input.custom_fields),
1346
+ unit_price: typeof input.price_data?.unit_price === "number" && Number.isFinite(input.price_data.unit_price) ? input.price_data.unit_price : null,
1347
+ pay_what_you_want_price: typeof input.pay_what_you_want_price === "number" && Number.isFinite(input.pay_what_you_want_price) ? input.pay_what_you_want_price : null
1348
+ };
1349
+ return hashString(JSON.stringify(payload));
1350
+ }
1351
+ function ensureCartLineId(item) {
1352
+ if (typeof item.line_id === "string" && item.line_id.trim()) {
1353
+ return item;
1488
1354
  }
1489
- return response;
1355
+ return { ...item, line_id: computeCartLineId(item) };
1490
1356
  }
1491
1357
 
1492
- // ../contracts/src/storefront-consent-choice.ts
1493
- var COOKIE_CONSENT_VERSION = 1;
1494
- var COOKIE_CONSENT_COOKIE = "shoppex_consent";
1495
- var COOKIE_CONSENT_CHECKOUT_HASH_KEY = "shoppex_consent";
1496
- var COOKIE_CONSENT_VALUE = new RegExp(`^v${COOKIE_CONSENT_VERSION}\\.f([01])\\.a([01])\\.m([01])$`);
1497
- function parseCookieConsentValue(value) {
1498
- const match = value ? COOKIE_CONSENT_VALUE.exec(value) : null;
1499
- return match ? { functional: match[1] === "1", analytics: match[2] === "1", marketing: match[3] === "1" } : null;
1358
+ // ../contracts/src/catalog-unit-price.ts
1359
+ var CATALOG_UNIT_PRICE_DECIMAL_PLACES = 3;
1360
+ var PAYABLE_AMOUNT_DECIMAL_PLACES = 2;
1361
+ var CATALOG_UNIT_PRICE_FORMAT_OPTIONS = {
1362
+ maximumFractionDigits: CATALOG_UNIT_PRICE_DECIMAL_PLACES
1363
+ };
1364
+ function roundPayableAmount(value) {
1365
+ if (!Number.isFinite(value)) return value;
1366
+ const sign = value < 0 ? -1 : 1;
1367
+ const shifted = shiftDecimal(Math.abs(value), PAYABLE_AMOUNT_DECIMAL_PLACES);
1368
+ const rounded = Math.round(shifted);
1369
+ if (rounded === 0) return 0;
1370
+ return sign * shiftDecimal(rounded, -PAYABLE_AMOUNT_DECIMAL_PLACES);
1371
+ }
1372
+ function shiftDecimal(value, places) {
1373
+ const [coefficient, currentExponent = "0"] = String(value).split("e");
1374
+ return Number(`${coefficient}e${Number(currentExponent) + places}`);
1500
1375
  }
1501
1376
 
1502
- // ../sdk/src/core/analytics-session.ts
1503
- var ANALYTICS_SESSION_IDLE_MS = 30 * 60 * 1e3;
1504
- var ANALYTICS_VISITS_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
1505
- var ANALYTICS_VISITS_MAX = 20;
1506
- var MAX_ENGAGED_MS_PER_PAGE = 30 * 60 * 1e3;
1507
- var analyticsSessionKey = (storeSlug) => `analytics_session_${storeSlug}`;
1508
- var analyticsVisitsKey = (storeSlug) => `analytics_visits_${storeSlug}`;
1509
- function readAnalyticsConsent() {
1510
- if (typeof document === "undefined" || typeof navigator === "undefined") return "unknown";
1511
- const privacy = navigator;
1512
- if (privacy.globalPrivacyControl === true || privacy.doNotTrack === "1") return "declined";
1513
- const prefix = `${COOKIE_CONSENT_COOKIE}=`;
1514
- let raw;
1515
- try {
1516
- raw = document.cookie.split(/;\s*/).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1517
- } catch {
1518
- return "unknown";
1377
+ // ../sdk/src/modules/cart.ts
1378
+ var STORAGE_KEYS = {
1379
+ cart: "cart",
1380
+ cartBackup: "cart_backup",
1381
+ meta: "cart_meta",
1382
+ metaBackup: "cart_backup_meta",
1383
+ coupon: "cart_coupon",
1384
+ couponBackup: "cart_coupon_backup"
1385
+ };
1386
+ function getStorageKey(type) {
1387
+ return `${STORAGE_KEYS[type]}_${getConfig().storeSlug}`;
1388
+ }
1389
+ function cartStorageArea() {
1390
+ return getConfig().preview ? "sessionStorage" : "localStorage";
1391
+ }
1392
+ function readCartStorage(type) {
1393
+ return getItem(getStorageKey(type), cartStorageArea());
1394
+ }
1395
+ function writeCartStorage(type, value) {
1396
+ setItem(getStorageKey(type), value, cartStorageArea());
1397
+ }
1398
+ function removeCartStorage(type) {
1399
+ removeItem(getStorageKey(type), cartStorageArea());
1400
+ }
1401
+ function hashString2(value) {
1402
+ let hash = 2166136261;
1403
+ for (let i = 0; i < value.length; i += 1) {
1404
+ hash ^= value.charCodeAt(i);
1405
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
1519
1406
  }
1520
- const choice = parseCookieConsentValue(raw ?? null);
1521
- if (!choice) return "unknown";
1522
- return choice.analytics ? "granted" : "declined";
1407
+ return (hash >>> 0).toString(16);
1523
1408
  }
1524
- function readVisits(storeSlug, now) {
1525
- const stored = getItem(analyticsVisitsKey(storeSlug));
1526
- if (!Array.isArray(stored)) return [];
1527
- return stored.filter((visit) => typeof visit?.id === "string" && typeof visit.at === "number" && now - visit.at < ANALYTICS_VISITS_WINDOW_MS);
1409
+ function computeChecksum(cart) {
1410
+ return hashString2(JSON.stringify(cart));
1528
1411
  }
1529
- function forgetAnalyticsData(storeSlug) {
1530
- removeItem(analyticsSessionKey(storeSlug), "sessionStorage");
1531
- removeItem(analyticsVisitsKey(storeSlug));
1412
+ function normalizeQuantity(value) {
1413
+ if (!Number.isFinite(value)) {
1414
+ throw new CartError("quantity must be a finite number");
1415
+ }
1416
+ return Math.floor(value);
1532
1417
  }
1533
- function analyticsSessionId(storeSlug, options, now = Date.now()) {
1534
- removeItem(`presence_connection_${storeSlug}`);
1535
- if (readAnalyticsConsent() !== "granted") {
1536
- forgetAnalyticsData(storeSlug);
1418
+ function normalizeCouponCode(value) {
1419
+ const normalized = value?.trim().toUpperCase();
1420
+ return normalized ? normalized : null;
1421
+ }
1422
+ function getStoredCartCode() {
1423
+ const raw = readCartStorage("coupon");
1424
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1537
1425
  return null;
1538
1426
  }
1539
- const stored = getItem(analyticsSessionKey(storeSlug), "sessionStorage");
1540
- const live = stored && typeof stored.id === "string" && typeof stored.at === "number" && now - stored.at < ANALYTICS_SESSION_IDLE_MS ? stored.id : null;
1541
- if (!options.touch) return live;
1542
- if (live) {
1543
- setItem(analyticsSessionKey(storeSlug), { id: live, at: now }, "sessionStorage");
1544
- return live;
1545
- }
1546
- if (typeof crypto === "undefined" || typeof crypto.randomUUID !== "function") return null;
1547
- const id = crypto.randomUUID();
1548
- setItem(analyticsSessionKey(storeSlug), { id, at: now }, "sessionStorage");
1549
- setItem(
1550
- analyticsVisitsKey(storeSlug),
1551
- [{ id, at: now }, ...readVisits(storeSlug, now)].slice(0, ANALYTICS_VISITS_MAX)
1552
- );
1553
- return id;
1427
+ const record = raw;
1428
+ const code = typeof record.code === "string" ? normalizeCouponCode(record.code) : null;
1429
+ const source = record.source === "COUPON" || record.source === "AFFILIATE" ? record.source : null;
1430
+ return code && source ? { code, source } : null;
1554
1431
  }
1555
- function checkoutAttribution(storeSlug, now = Date.now()) {
1556
- const consent = readAnalyticsConsent();
1557
- if (consent === "declined") {
1558
- forgetAnalyticsData(storeSlug);
1559
- return { declined: true };
1432
+ function normalizeCartItems(value) {
1433
+ if (!Array.isArray(value)) return [];
1434
+ const normalized = [];
1435
+ for (const entry of value) {
1436
+ if (!entry || typeof entry !== "object") continue;
1437
+ const record = entry;
1438
+ const productId = typeof record.product_id === "string" ? record.product_id.trim() : "";
1439
+ const variantId = typeof record.variant_id === "string" ? record.variant_id.trim() : "";
1440
+ const quantity = Number(record.quantity);
1441
+ if (!productId || !variantId || !Number.isFinite(quantity) || quantity < 1) {
1442
+ continue;
1443
+ }
1444
+ const item = {
1445
+ line_id: "",
1446
+ product_id: productId,
1447
+ variant_id: variantId,
1448
+ quantity: Math.floor(quantity)
1449
+ };
1450
+ if (typeof record.price_variant_id === "string") {
1451
+ item.price_variant_id = record.price_variant_id;
1452
+ }
1453
+ if (record.price_data && typeof record.price_data === "object") {
1454
+ const priceData = record.price_data;
1455
+ if (typeof priceData.unit_price === "number" && Number.isFinite(priceData.unit_price)) {
1456
+ item.price_data = { unit_price: priceData.unit_price };
1457
+ }
1458
+ }
1459
+ if (typeof record.pay_what_you_want_price === "number" && Number.isFinite(record.pay_what_you_want_price)) {
1460
+ item.pay_what_you_want_price = record.pay_what_you_want_price;
1461
+ }
1462
+ if (Array.isArray(record.addons)) {
1463
+ item.addons = record.addons;
1464
+ }
1465
+ if (record.custom_fields && typeof record.custom_fields === "object" && !Array.isArray(record.custom_fields)) {
1466
+ item.custom_fields = record.custom_fields;
1467
+ }
1468
+ if (typeof record.title === "string") {
1469
+ item.title = record.title;
1470
+ }
1471
+ if (typeof record.variant_title === "string") {
1472
+ item.variant_title = record.variant_title;
1473
+ }
1474
+ if (typeof record.image_url === "string") {
1475
+ item.image_url = record.image_url;
1476
+ }
1477
+ if (Array.isArray(record.addon_labels)) {
1478
+ item.addon_labels = record.addon_labels.filter((label) => typeof label === "string");
1479
+ }
1480
+ if (typeof record.max_quantity === "number" && Number.isFinite(record.max_quantity)) {
1481
+ item.max_quantity = record.max_quantity;
1482
+ }
1483
+ if (typeof record.min_quantity === "number" && Number.isFinite(record.min_quantity)) {
1484
+ item.min_quantity = record.min_quantity;
1485
+ }
1486
+ clampQuantityToBounds(item);
1487
+ const storedLineId = typeof record.line_id === "string" ? record.line_id.trim() : "";
1488
+ item.line_id = storedLineId || computeCartLineId(item);
1489
+ normalized.push(ensureCartLineId(item));
1560
1490
  }
1561
- if (consent === "unknown") return void 0;
1562
- const sessionIds = readVisits(storeSlug, now).map((visit) => visit.id);
1563
- return sessionIds.length > 0 ? { session_ids: sessionIds } : void 0;
1491
+ return normalized;
1492
+ }
1493
+ function getCartMetadata() {
1494
+ return readCartStorage("meta");
1495
+ }
1496
+ function getCartCoupon() {
1497
+ return getStoredCartCode()?.code ?? null;
1564
1498
  }
1565
- var engagedMs = 0;
1566
- var visibleSince = null;
1567
- var clockInstalled = false;
1568
- var pageViewed = false;
1569
- var pageSession = null;
1570
- function markPageViewed(sessionId) {
1571
- pageViewed = true;
1572
- pageSession = sessionId;
1499
+ function getCartCouponSource() {
1500
+ return getStoredCartCode()?.source ?? null;
1573
1501
  }
1574
- function currentPageSession() {
1575
- return pageSession;
1502
+ function setCartCoupon(coupon, source = "COUPON") {
1503
+ const normalizedCoupon = normalizeCouponCode(coupon);
1504
+ if (!normalizedCoupon) {
1505
+ removeCartStorage("coupon");
1506
+ return null;
1507
+ }
1508
+ writeCartStorage("coupon", { code: normalizedCoupon, source });
1509
+ return normalizedCoupon;
1576
1510
  }
1577
- function isPageViewed() {
1578
- return pageViewed;
1511
+ function clearCartCoupon() {
1512
+ removeCartStorage("coupon");
1579
1513
  }
1580
- function settleClock(now) {
1581
- if (visibleSince !== null) {
1582
- engagedMs += now - visibleSince;
1583
- visibleSince = document.visibilityState === "visible" ? now : null;
1514
+ function writeCart(cart) {
1515
+ const normalizedCart = normalizeCartItems(cart);
1516
+ writeCartStorage("cart", normalizedCart);
1517
+ const now = Date.now();
1518
+ const previous = getCartMetadata();
1519
+ const nextMeta = {
1520
+ created_at: previous?.created_at ?? now,
1521
+ last_modified: now,
1522
+ version: (previous?.version ?? 0) + 1,
1523
+ checksum: computeChecksum(normalizedCart)
1524
+ };
1525
+ writeCartStorage("meta", nextMeta);
1526
+ if (normalizedCart.length === 0) {
1527
+ clearCartCoupon();
1584
1528
  }
1585
1529
  }
1586
- var consentListenerInstalled = false;
1587
- function onAnalyticsConsent(onYes) {
1588
- if (consentListenerInstalled || typeof window === "undefined") return;
1589
- consentListenerInstalled = true;
1590
- window.addEventListener("shoppex:consent", () => {
1591
- if (readAnalyticsConsent() === "granted") onYes();
1592
- });
1530
+ function setCartWithMetadata(cart, metadata) {
1531
+ const normalizedCart = normalizeCartItems(cart);
1532
+ writeCartStorage("cart", normalizedCart);
1533
+ const now = Date.now();
1534
+ const base = metadata ?? getCartMetadata();
1535
+ const nextMeta = {
1536
+ created_at: base?.created_at ?? now,
1537
+ last_modified: now,
1538
+ version: base?.version ?? 1,
1539
+ checksum: computeChecksum(normalizedCart)
1540
+ };
1541
+ writeCartStorage("meta", nextMeta);
1593
1542
  }
1594
- function installEngagedClock(onHidden) {
1595
- if (clockInstalled || typeof document === "undefined") return;
1596
- clockInstalled = true;
1597
- visibleSince = document.visibilityState === "visible" ? Date.now() : null;
1598
- document.addEventListener("visibilitychange", () => {
1599
- const now = Date.now();
1600
- if (document.visibilityState === "visible") {
1601
- visibleSince ?? (visibleSince = now);
1602
- return;
1603
- }
1604
- settleClock(now);
1605
- visibleSince = null;
1606
- onHidden();
1607
- });
1608
- window.addEventListener("pagehide", () => {
1609
- settleClock(Date.now());
1610
- visibleSince = null;
1611
- onHidden();
1612
- });
1613
- window.addEventListener("pageshow", (event) => {
1614
- if (event.persisted && document.visibilityState === "visible") visibleSince = Date.now();
1615
- });
1543
+ function getCart() {
1544
+ const raw = readCartStorage("cart");
1545
+ return normalizeCartItems(raw);
1616
1546
  }
1617
- function takeEngagedMs(now = Date.now()) {
1618
- settleClock(now);
1619
- const taken = Math.min(Math.max(0, Math.round(engagedMs)), MAX_ENGAGED_MS_PER_PAGE);
1620
- engagedMs = 0;
1621
- return taken;
1547
+ function resolveCartLineId(productId, variantId, cart = getCart()) {
1548
+ const matches = cart.filter(
1549
+ (item) => item.product_id === productId && item.variant_id === variantId
1550
+ );
1551
+ if (matches.length === 0) {
1552
+ throw new CartError(`No cart line found for ${productId}/${variantId}`);
1553
+ }
1554
+ return matches[0].line_id;
1622
1555
  }
1623
-
1624
- // ../sdk/src/core/attribution.ts
1625
- var STORAGE_PREFIX2 = "shoppex:utm:";
1626
- var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
1627
- var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
1628
- var MAX_VALUE_LENGTH = 255;
1629
- function readFromUrl(search) {
1630
- let params;
1631
- try {
1632
- params = new URLSearchParams(search);
1633
- } catch {
1634
- return {};
1556
+ function getCartItemCount() {
1557
+ const cart = getCart();
1558
+ return cart.reduce((sum, item) => sum + item.quantity, 0);
1559
+ }
1560
+ function addToCart(productId, variantId, quantity = 1, options) {
1561
+ if (!productId || !variantId) {
1562
+ throw new CartError("product_id and variant_id are required");
1635
1563
  }
1636
- const utm = {};
1637
- for (const key of UTM_KEYS) {
1638
- const value = params.get(key)?.trim();
1639
- if (value) {
1640
- utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1564
+ const normalizedQuantity = normalizeQuantity(quantity);
1565
+ if (normalizedQuantity < 1) {
1566
+ throw new CartError("quantity must be at least 1");
1567
+ }
1568
+ const cart = getCart();
1569
+ const lineId = computeCartLineId({
1570
+ product_id: productId,
1571
+ variant_id: variantId,
1572
+ addons: options?.addons,
1573
+ custom_fields: options?.custom_fields,
1574
+ price_variant_id: options?.price_variant_id,
1575
+ price_data: options?.price_data,
1576
+ pay_what_you_want_price: options?.pay_what_you_want_price
1577
+ });
1578
+ const existingIndex = cart.findIndex((item) => item.line_id === lineId);
1579
+ const quantityBeforeAdd = existingIndex >= 0 ? cart[existingIndex].quantity : 0;
1580
+ if (existingIndex >= 0) {
1581
+ cart[existingIndex].quantity += normalizedQuantity;
1582
+ if (options?.addons) {
1583
+ cart[existingIndex].addons = options.addons;
1584
+ }
1585
+ if (options?.custom_fields) {
1586
+ cart[existingIndex].custom_fields = options.custom_fields;
1587
+ }
1588
+ if (options?.price_variant_id) {
1589
+ cart[existingIndex].price_variant_id = options.price_variant_id;
1590
+ }
1591
+ if (options?.price_data) {
1592
+ cart[existingIndex].price_data = options.price_data;
1641
1593
  }
1594
+ if (options?.pay_what_you_want_price !== void 0) {
1595
+ cart[existingIndex].pay_what_you_want_price = options.pay_what_you_want_price;
1596
+ }
1597
+ applyDisplaySnapshot(cart[existingIndex], options);
1598
+ clampQuantityToBounds(cart[existingIndex]);
1599
+ } else {
1600
+ const pushed = {
1601
+ line_id: lineId,
1602
+ product_id: productId,
1603
+ variant_id: variantId,
1604
+ quantity: normalizedQuantity,
1605
+ addons: options?.addons,
1606
+ custom_fields: options?.custom_fields,
1607
+ price_variant_id: options?.price_variant_id,
1608
+ price_data: options?.price_data,
1609
+ pay_what_you_want_price: options?.pay_what_you_want_price,
1610
+ title: options?.title,
1611
+ variant_title: options?.variant_title,
1612
+ image_url: options?.image_url,
1613
+ addon_labels: options?.addon_labels,
1614
+ max_quantity: options?.max_quantity,
1615
+ min_quantity: options?.min_quantity
1616
+ };
1617
+ clampQuantityToBounds(pushed);
1618
+ cart.push(pushed);
1619
+ }
1620
+ const intendedQuantity = (existingIndex >= 0 ? cart[existingIndex] : cart[cart.length - 1]).quantity;
1621
+ writeCart(cart);
1622
+ const persistedLine = getCart().find((item) => item.line_id === lineId);
1623
+ if (intendedQuantity > quantityBeforeAdd && persistedLine?.quantity === intendedQuantity) {
1624
+ const cartAffiliateCode = getCartCouponSource() === "AFFILIATE" ? getCartCoupon() : null;
1625
+ void trackAffiliateEvent("add_to_cart", cartAffiliateCode ? { code: cartAffiliateCode } : void 0);
1626
+ void trackCartAdd(productId, variantId, intendedQuantity - quantityBeforeAdd);
1642
1627
  }
1643
- return utm;
1644
1628
  }
1645
- function storageKey(storeSlug) {
1646
- return `${STORAGE_PREFIX2}${storeSlug}`;
1629
+ function clampQuantityToBounds(item) {
1630
+ const min = item.min_quantity;
1631
+ if (typeof min === "number" && Number.isFinite(min) && item.quantity < min) {
1632
+ item.quantity = Math.floor(min);
1633
+ }
1634
+ const max = item.max_quantity;
1635
+ if (typeof max === "number" && Number.isFinite(max) && item.quantity > max) {
1636
+ item.quantity = Math.floor(max);
1637
+ }
1638
+ if (item.quantity < 1) {
1639
+ item.quantity = 1;
1640
+ }
1647
1641
  }
1648
- function readStored(storeSlug) {
1649
- try {
1650
- const raw = window.localStorage.getItem(storefrontStorageKey(storageKey(storeSlug)));
1651
- if (!raw) return {};
1652
- const parsed = JSON.parse(raw);
1653
- if (!parsed || typeof parsed.at !== "number" || typeof parsed.utm !== "object") return {};
1654
- if (Date.now() - parsed.at > TTL_MS) {
1655
- window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1656
- return {};
1657
- }
1658
- const utm = {};
1659
- for (const key of UTM_KEYS) {
1660
- const value = parsed.utm?.[key];
1661
- if (typeof value === "string" && value.length > 0) {
1662
- utm[key] = value.slice(0, MAX_VALUE_LENGTH);
1663
- }
1642
+ function applyDisplaySnapshot(item, options) {
1643
+ if (options?.replace_display_snapshot) {
1644
+ item.title = options.title;
1645
+ item.variant_title = options.variant_title;
1646
+ item.image_url = options.image_url;
1647
+ item.addon_labels = options.addon_labels;
1648
+ item.max_quantity = options.max_quantity;
1649
+ item.min_quantity = options.min_quantity;
1650
+ return;
1651
+ }
1652
+ if (options?.title !== void 0) {
1653
+ item.title = options.title;
1654
+ }
1655
+ if (options?.variant_title !== void 0) {
1656
+ item.variant_title = options.variant_title;
1657
+ }
1658
+ if (options?.image_url !== void 0) {
1659
+ item.image_url = options.image_url;
1660
+ }
1661
+ if (options?.addon_labels !== void 0) {
1662
+ item.addon_labels = options.addon_labels;
1663
+ }
1664
+ if (options?.max_quantity !== void 0) {
1665
+ item.max_quantity = options.max_quantity;
1666
+ }
1667
+ if (options?.min_quantity !== void 0) {
1668
+ item.min_quantity = options.min_quantity;
1669
+ }
1670
+ }
1671
+ function setCartItem(productId, variantId, quantity = 1, options) {
1672
+ if (!productId || !variantId) {
1673
+ throw new CartError("product_id and variant_id are required");
1674
+ }
1675
+ const normalizedQuantity = normalizeQuantity(quantity);
1676
+ if (normalizedQuantity < 1) {
1677
+ throw new CartError("quantity must be at least 1");
1678
+ }
1679
+ const cart = getCart();
1680
+ const lineId = computeCartLineId({
1681
+ product_id: productId,
1682
+ variant_id: variantId,
1683
+ addons: options?.addons,
1684
+ custom_fields: options?.custom_fields,
1685
+ price_variant_id: options?.price_variant_id,
1686
+ price_data: options?.price_data,
1687
+ pay_what_you_want_price: options?.pay_what_you_want_price
1688
+ });
1689
+ for (let index = cart.length - 1; index >= 0; index -= 1) {
1690
+ if (cart[index].product_id === productId && cart[index].variant_id === variantId) {
1691
+ cart.splice(index, 1);
1664
1692
  }
1665
- return utm;
1666
- } catch {
1667
- return {};
1668
1693
  }
1694
+ const pushed = {
1695
+ line_id: lineId,
1696
+ product_id: productId,
1697
+ variant_id: variantId,
1698
+ quantity: normalizedQuantity,
1699
+ addons: options?.addons,
1700
+ custom_fields: options?.custom_fields,
1701
+ price_variant_id: options?.price_variant_id,
1702
+ price_data: options?.price_data,
1703
+ pay_what_you_want_price: options?.pay_what_you_want_price,
1704
+ title: options?.title,
1705
+ variant_title: options?.variant_title,
1706
+ image_url: options?.image_url,
1707
+ addon_labels: options?.addon_labels,
1708
+ max_quantity: options?.max_quantity,
1709
+ min_quantity: options?.min_quantity
1710
+ };
1711
+ clampQuantityToBounds(pushed);
1712
+ cart.push(pushed);
1713
+ writeCart(cart);
1714
+ const persistedLine = getCart().find((item) => item.line_id === lineId);
1715
+ if (persistedLine) void trackCartAdd(productId, variantId, persistedLine.quantity);
1669
1716
  }
1670
- function writeStored(storeSlug, utm) {
1671
- try {
1672
- const payload = { utm, at: Date.now() };
1673
- window.localStorage.setItem(storefrontStorageKey(storageKey(storeSlug)), JSON.stringify(payload));
1674
- } catch {
1717
+ function updateCartItem(lineId, updates) {
1718
+ const cart = getCart();
1719
+ const normalizedLineId = lineId.trim();
1720
+ if (!normalizedLineId) {
1721
+ throw new CartError("line_id is required");
1675
1722
  }
1676
- }
1677
- function resolveUtmParameters(storeSlug) {
1678
- if (typeof window === "undefined" || typeof window.location === "undefined") {
1679
- return void 0;
1723
+ const index = cart.findIndex((item) => item.line_id === normalizedLineId);
1724
+ if (index < 0) {
1725
+ throw new CartError("Item not found in cart");
1680
1726
  }
1681
- const fromUrl = readFromUrl(window.location.search);
1682
- if (readAnalyticsConsent() !== "granted") {
1683
- try {
1684
- window.localStorage.removeItem(storefrontStorageKey(storageKey(storeSlug)));
1685
- } catch {
1727
+ const before = JSON.stringify(cart);
1728
+ if (updates.quantity !== void 0) {
1729
+ const normalizedQuantity = normalizeQuantity(updates.quantity);
1730
+ if (normalizedQuantity < 1) {
1731
+ cart.splice(index, 1);
1732
+ writeCart(cart);
1733
+ return;
1686
1734
  }
1687
- return Object.keys(fromUrl).length > 0 ? fromUrl : void 0;
1735
+ cart[index].quantity = normalizedQuantity;
1688
1736
  }
1689
- if (Object.keys(fromUrl).length > 0) {
1690
- writeStored(storeSlug, fromUrl);
1691
- return fromUrl;
1737
+ if (updates.addons !== void 0) {
1738
+ cart[index].addons = updates.addons;
1692
1739
  }
1693
- const stored = readStored(storeSlug);
1694
- return Object.keys(stored).length > 0 ? stored : void 0;
1695
- }
1696
-
1697
- // ../sdk/src/core/page-path.ts
1698
- var MAX_PATH_LENGTH = 256;
1699
- function resolveStorefrontPagePath() {
1700
- if (typeof window === "undefined") return void 0;
1701
- let pathname;
1702
- try {
1703
- const location = window.location;
1704
- pathname = typeof location?.pathname === "string" && location.pathname ? location.pathname : new URL(String(location?.href ?? "")).pathname;
1705
- } catch {
1706
- return void 0;
1740
+ if (updates.custom_fields !== void 0) {
1741
+ cart[index].custom_fields = updates.custom_fields;
1707
1742
  }
1708
- if (!pathname || !pathname.startsWith("/")) return void 0;
1709
- return pathname.slice(0, MAX_PATH_LENGTH);
1710
- }
1711
-
1712
- // ../sdk/src/core/page-tracker.ts
1713
- var PLATFORM_PAGE_TRACKER_FLAG = "__shoppexPageTracker";
1714
- function isPlatformPageTrackerActive() {
1715
- if (typeof window === "undefined") return false;
1716
- try {
1717
- return Boolean(window[PLATFORM_PAGE_TRACKER_FLAG]);
1718
- } catch {
1719
- return false;
1743
+ if (updates.price_variant_id !== void 0) {
1744
+ cart[index].price_variant_id = updates.price_variant_id;
1720
1745
  }
1721
- }
1722
-
1723
- // ../sdk/src/modules/analytics.ts
1724
- var CONFIRMED_CART_STORAGE_PREFIX = "reported_cart_";
1725
- var IN_FLIGHT_CART_STORAGE_PREFIX = "reporting_cart_";
1726
- var CART_SEQUENCE_STORAGE_PREFIX = "presence_cart_seq_";
1727
- var CART_CLAIM_TTL_MS = 1e4;
1728
- function confirmedCartKey(storeSlug) {
1729
- return `${CONFIRMED_CART_STORAGE_PREFIX}${storeSlug}`;
1730
- }
1731
- function inFlightCartKey(storeSlug) {
1732
- return `${IN_FLIGHT_CART_STORAGE_PREFIX}${storeSlug}`;
1733
- }
1734
- function cartSequenceKey(storeSlug) {
1735
- return `${CART_SEQUENCE_STORAGE_PREFIX}${storeSlug}`;
1736
- }
1737
- function cartSnapshot(cartValue, itemCount) {
1738
- return `${cartValue ?? ""}|${itemCount ?? ""}`;
1739
- }
1740
- function isCartSequence(value) {
1741
- return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
1742
- }
1743
- var CART_SEQUENCE_SCOPE_MAX_LENGTH = 64;
1744
- function readCartCounter(storeSlug) {
1745
- const counter = getItem(cartSequenceKey(storeSlug));
1746
- if (!counter || typeof counter.scope !== "string" || counter.scope.length === 0 || counter.scope.length > CART_SEQUENCE_SCOPE_MAX_LENGTH || !isCartSequence(counter.seq)) {
1747
- return null;
1746
+ if (updates.price_data !== void 0) {
1747
+ cart[index].price_data = updates.price_data;
1748
1748
  }
1749
- return counter;
1749
+ if (updates.pay_what_you_want_price !== void 0) {
1750
+ cart[index].pay_what_you_want_price = updates.pay_what_you_want_price;
1751
+ }
1752
+ if (updates.variant_title !== void 0) {
1753
+ if (updates.variant_title === null) delete cart[index].variant_title;
1754
+ else cart[index].variant_title = updates.variant_title;
1755
+ }
1756
+ if (updates.max_quantity !== void 0) {
1757
+ if (updates.max_quantity === null) delete cart[index].max_quantity;
1758
+ else cart[index].max_quantity = updates.max_quantity;
1759
+ }
1760
+ if (updates.min_quantity !== void 0) {
1761
+ if (updates.min_quantity === null) delete cart[index].min_quantity;
1762
+ else cart[index].min_quantity = updates.min_quantity;
1763
+ }
1764
+ clampQuantityToBounds(cart[index]);
1765
+ const nextLineId = computeCartLineId(cart[index]);
1766
+ const duplicateIndex = cart.findIndex((item, itemIndex) => itemIndex !== index && item.line_id === nextLineId);
1767
+ if (duplicateIndex >= 0) {
1768
+ cart[duplicateIndex].quantity += cart[index].quantity;
1769
+ clampQuantityToBounds(cart[duplicateIndex]);
1770
+ cart.splice(index, 1);
1771
+ } else {
1772
+ cart[index].line_id = nextLineId;
1773
+ }
1774
+ if (JSON.stringify(cart) === before) return;
1775
+ writeCart(cart);
1750
1776
  }
1751
- var lastCartOrder = null;
1752
- function nextCartOrder(storeSlug) {
1753
- const stored = readCartCounter(storeSlug);
1754
- const scope = stored?.scope ?? lastCartOrder?.scope ?? createClaimToken();
1755
- const floor = Math.max(stored?.seq ?? 0, lastCartOrder?.scope === scope ? lastCartOrder.seq : 0);
1756
- const order2 = { scope, seq: Math.max(Date.now(), floor + 1) };
1757
- lastCartOrder = order2;
1758
- setItem(cartSequenceKey(storeSlug), order2);
1759
- return order2;
1777
+ function removeFromCart(lineId) {
1778
+ const normalizedLineId = lineId.trim();
1779
+ if (!normalizedLineId) {
1780
+ throw new CartError("line_id is required");
1781
+ }
1782
+ const cart = getCart();
1783
+ const filtered = cart.filter((item) => item.line_id !== normalizedLineId);
1784
+ writeCart(filtered);
1760
1785
  }
1761
- function cartSequenceLocks() {
1762
- if (typeof navigator === "undefined") return null;
1763
- const locks = navigator.locks;
1764
- return locks && typeof locks.request === "function" ? locks : null;
1786
+ function clearCart() {
1787
+ removeCartStorage("cart");
1788
+ removeCartStorage("meta");
1789
+ clearCartCoupon();
1790
+ clearLatestQuoteToken();
1765
1791
  }
1766
- function reserveCartOrder(storeSlug) {
1767
- const take = () => nextCartOrder(storeSlug);
1768
- const locks = cartSequenceLocks();
1769
- if (!locks) return Promise.resolve(take());
1770
- return locks.request(cartSequenceKey(storeSlug), take).catch(take);
1792
+ function createCartBackup() {
1793
+ const cart = getCart();
1794
+ writeCartStorage("cartBackup", cart);
1795
+ const cartCode = getStoredCartCode();
1796
+ if (cartCode) {
1797
+ writeCartStorage("couponBackup", cartCode);
1798
+ } else {
1799
+ removeCartStorage("couponBackup");
1800
+ }
1801
+ const metadata = getCartMetadata();
1802
+ if (metadata) {
1803
+ writeCartStorage("metaBackup", metadata);
1804
+ } else {
1805
+ const now = Date.now();
1806
+ writeCartStorage("metaBackup", {
1807
+ created_at: now,
1808
+ last_modified: now,
1809
+ version: 1,
1810
+ checksum: computeChecksum(cart)
1811
+ });
1812
+ }
1771
1813
  }
1772
- function readConfirmedCart(storeSlug) {
1773
- const confirmed = getItem(confirmedCartKey(storeSlug));
1774
- if (!confirmed || typeof confirmed.snapshot !== "string" || !isCartSequence(confirmed.seq)) return null;
1775
- return confirmed;
1814
+ function restoreCartFromBackup() {
1815
+ const backupRaw = readCartStorage("cartBackup");
1816
+ const backup = normalizeCartItems(backupRaw);
1817
+ const backupMeta = readCartStorage("metaBackup");
1818
+ const backupCoupon = readCartStorage("couponBackup");
1819
+ if (backup && backup.length > 0) {
1820
+ setCartWithMetadata(backup, backupMeta);
1821
+ if (backupCoupon && typeof backupCoupon === "object" && !Array.isArray(backupCoupon)) {
1822
+ const record = backupCoupon;
1823
+ const source = record.source === "COUPON" || record.source === "AFFILIATE" ? record.source : null;
1824
+ if (typeof record.code === "string" && source) {
1825
+ setCartCoupon(record.code, source);
1826
+ } else {
1827
+ clearCartCoupon();
1828
+ }
1829
+ } else {
1830
+ clearCartCoupon();
1831
+ }
1832
+ return true;
1833
+ }
1834
+ return false;
1776
1835
  }
1777
- function readInFlightCart(storeSlug) {
1778
- const claim = getItem(inFlightCartKey(storeSlug));
1779
- if (!claim || typeof claim.snapshot !== "string" || !isCartSequence(claim.seq) || typeof claim.token !== "string" || typeof claim.at !== "number") {
1780
- return null;
1836
+ function mergeBaskets(items) {
1837
+ const cart = getCart();
1838
+ for (const incoming of items) {
1839
+ const productId = typeof incoming.product_id === "string" ? incoming.product_id.trim() : "";
1840
+ const variantId = typeof incoming.variant_id === "string" ? incoming.variant_id.trim() : "";
1841
+ if (!productId || !variantId) {
1842
+ continue;
1843
+ }
1844
+ let normalizedQuantity;
1845
+ try {
1846
+ normalizedQuantity = normalizeQuantity(incoming.quantity);
1847
+ } catch {
1848
+ continue;
1849
+ }
1850
+ if (normalizedQuantity < 1) {
1851
+ continue;
1852
+ }
1853
+ const mergedLine = ensureCartLineId({
1854
+ ...incoming,
1855
+ line_id: incoming.line_id ?? computeCartLineId(incoming),
1856
+ product_id: productId,
1857
+ variant_id: variantId,
1858
+ quantity: normalizedQuantity
1859
+ });
1860
+ const index = cart.findIndex((item) => item.line_id === mergedLine.line_id);
1861
+ if (index >= 0) {
1862
+ cart[index].quantity = Math.max(cart[index].quantity, normalizedQuantity);
1863
+ clampQuantityToBounds(cart[index]);
1864
+ } else {
1865
+ clampQuantityToBounds(mergedLine);
1866
+ cart.push(mergedLine);
1867
+ }
1781
1868
  }
1782
- return claim;
1869
+ writeCart(cart);
1870
+ return cart;
1783
1871
  }
1784
- function isCartReportedOrInFlight(storeSlug, snapshot) {
1785
- const confirmed = readConfirmedCart(storeSlug);
1786
- const claim = readInFlightCart(storeSlug);
1787
- const liveClaim = claim && Date.now() - claim.at < CART_CLAIM_TTL_MS ? claim : null;
1788
- const expected = liveClaim && (!confirmed || liveClaim.seq > confirmed.seq) ? liveClaim : confirmed;
1789
- return expected?.snapshot === snapshot;
1872
+ function moveBasketItem(fromProductId, fromVariantId, toProductId, toVariantId) {
1873
+ const cart = getCart();
1874
+ const fromIndex = cart.findIndex(
1875
+ (item) => item.product_id === fromProductId && item.variant_id === fromVariantId
1876
+ );
1877
+ if (fromIndex < 0) {
1878
+ throw new CartError("Item not found in cart");
1879
+ }
1880
+ const [fromItem] = cart.splice(fromIndex, 1);
1881
+ const toLineId = computeCartLineId({
1882
+ ...fromItem,
1883
+ product_id: toProductId,
1884
+ variant_id: toVariantId
1885
+ });
1886
+ const toIndex = cart.findIndex((item) => item.line_id === toLineId);
1887
+ if (toIndex >= 0) {
1888
+ cart[toIndex].quantity += fromItem.quantity;
1889
+ clampQuantityToBounds(cart[toIndex]);
1890
+ } else {
1891
+ const moved = ensureCartLineId({
1892
+ ...fromItem,
1893
+ product_id: toProductId,
1894
+ variant_id: toVariantId,
1895
+ line_id: toLineId
1896
+ });
1897
+ clampQuantityToBounds(moved);
1898
+ cart.push(moved);
1899
+ }
1900
+ writeCart(cart);
1790
1901
  }
1791
- function createClaimToken() {
1792
- if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
1793
- return `${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`;
1902
+ function validateCartIntegrity() {
1903
+ const cart = getCart();
1904
+ const metadata = getCartMetadata();
1905
+ const checksum = computeChecksum(cart);
1906
+ if (!metadata) {
1907
+ setCartWithMetadata(cart, null);
1908
+ return true;
1909
+ }
1910
+ return metadata.checksum === checksum;
1794
1911
  }
1795
- function claimCartReport(storeSlug, seq, cartValue, itemCount) {
1796
- const snapshot = cartSnapshot(cartValue, itemCount);
1797
- const token = createClaimToken();
1798
- setItem(inFlightCartKey(storeSlug), { snapshot, seq, token, at: Date.now() });
1799
- return (delivered) => {
1800
- if (delivered) {
1801
- const confirmed = readConfirmedCart(storeSlug);
1802
- if (!confirmed || seq > confirmed.seq) setItem(confirmedCartKey(storeSlug), { snapshot, seq });
1803
- }
1804
- if (readInFlightCart(storeSlug)?.token === token) removeItem(inFlightCartKey(storeSlug));
1912
+ function getCartStats() {
1913
+ const cart = getCart();
1914
+ const integrityValid = validateCartIntegrity();
1915
+ const metadata = getCartMetadata();
1916
+ const backup = readCartStorage("cartBackup") ?? [];
1917
+ const hasCompletePriceSnapshots = cart.length > 0 && cart.every((item) => typeof item.price_data?.unit_price === "number");
1918
+ const totalPrice = cart.reduce((sum, item) => {
1919
+ const unitPrice = item.price_data?.unit_price ?? 0;
1920
+ return sum + roundPayableAmount(unitPrice * item.quantity);
1921
+ }, 0);
1922
+ return {
1923
+ item_count: cart.length,
1924
+ total_quantity: cart.reduce((sum, item) => sum + item.quantity, 0),
1925
+ last_modified: metadata?.last_modified ?? 0,
1926
+ version: metadata?.version ?? 0,
1927
+ has_backup: backup.length > 0,
1928
+ integrity_valid: integrityValid,
1929
+ total_price: roundPayableAmount(totalPrice),
1930
+ total_price_is_estimate: cart.length > 0 && !hasCompletePriceSnapshots
1805
1931
  };
1806
1932
  }
1807
- var cartSendTail = Promise.resolve();
1808
- var latestCartRequest = 0;
1809
- function enqueueCartSend(send) {
1810
- const request2 = ++latestCartRequest;
1811
- const run = () => send(() => request2 === latestCartRequest);
1812
- cartSendTail = cartSendTail.then(run, run);
1813
- return cartSendTail;
1814
- }
1815
- async function postPing(body) {
1933
+ function getCartPayload(coupon) {
1816
1934
  const config = getConfig();
1817
- const controller = typeof AbortController === "undefined" ? void 0 : new AbortController();
1818
- let timer;
1819
- const timedOut = new Promise((resolve) => {
1820
- timer = setTimeout(() => {
1821
- controller?.abort();
1822
- resolve(false);
1823
- }, CART_CLAIM_TTL_MS);
1824
- });
1825
- try {
1826
- const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", {
1827
- storeSlug: config.storeSlug
1828
- });
1829
- const delivered = fetch(`${config.apiBaseUrl}${endpoint}`, {
1830
- method: "POST",
1831
- headers: { "Content-Type": "application/json" },
1832
- body: JSON.stringify(body),
1833
- signal: controller?.signal
1834
- }).then((response) => response.ok);
1835
- return await Promise.race([delivered, timedOut]);
1836
- } catch {
1837
- return false;
1838
- } finally {
1839
- clearTimeout(timer);
1840
- }
1935
+ const normalizedCoupon = normalizeCouponCode(coupon) ?? getCartCoupon() ?? void 0;
1936
+ return {
1937
+ store_slug: config.storeSlug,
1938
+ items: getCart(),
1939
+ coupon: normalizedCoupon
1940
+ };
1841
1941
  }
1842
- async function trackPageView(cartValue, itemCount) {
1843
- if (!isInitialized()) return;
1844
- if (typeof document === "undefined") return;
1845
- const config = getConfig();
1846
- const carriesCart = cartValue !== void 0 || itemCount !== void 0;
1847
- const reservedOrder = carriesCart ? reserveCartOrder(config.storeSlug) : void 0;
1848
- if (isPlatformPageTrackerActive()) {
1849
- if (reservedOrder) await enqueueCartSend(async () => sendCartActivity(await reservedOrder, cartValue, itemCount));
1850
- return;
1942
+ function getAmbientAffiliateCodeForQuote() {
1943
+ const storedCode = getCartCoupon();
1944
+ const storedSource = getCartCouponSource();
1945
+ if (storedSource === "AFFILIATE" && storedCode) {
1946
+ return storedCode.trim().toLowerCase();
1851
1947
  }
1852
- const sendView = async () => {
1853
- const order2 = reservedOrder ? await reservedOrder : void 0;
1854
- const settle = order2 ? claimCartReport(config.storeSlug, order2.seq, cartValue, itemCount) : void 0;
1855
- const sessionId = analyticsSessionId(config.storeSlug, { touch: true });
1856
- const previousSession = currentPageSession();
1857
- if (previousSession && previousSession !== sessionId) reportPageLeave();
1858
- markPageViewed(sessionId);
1859
- if (sessionId) installEngagedClock(reportPageLeave);
1860
- onAnalyticsConsent(startSessionOnConsent);
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
- session_id: sessionId ?? void 0,
1868
- // Time on screen of the page before this one (client-side navigation).
1869
- engaged_ms: sessionId ? takeEngagedMs() || void 0 : void 0,
1870
- path: resolveStorefrontPagePath(),
1871
- utm: resolveUtmParameters(config.storeSlug)
1872
- });
1873
- settle?.(delivered);
1874
- };
1875
- await (reservedOrder ? enqueueCartSend(sendView) : sendView());
1948
+ return getAffiliateCode();
1876
1949
  }
1877
- async function trackCartActivity(cartValue, itemCount) {
1878
- if (!isInitialized()) return;
1879
- if (typeof document === "undefined") return;
1880
- const snapshot = cartSnapshot(cartValue, itemCount);
1881
- const reservedOrder = reserveCartOrder(getConfig().storeSlug);
1882
- await enqueueCartSend(async (isLatest) => {
1883
- const order2 = await reservedOrder;
1884
- if (!isLatest() || isCartReportedOrInFlight(getConfig().storeSlug, snapshot)) return;
1885
- await sendCartActivity(order2, cartValue, itemCount);
1886
- });
1950
+ var latestQuoteToken = null;
1951
+ var quoteSequence = 0;
1952
+ function getLatestQuoteToken() {
1953
+ return latestQuoteToken;
1887
1954
  }
1888
- async function sendCartActivity(order2, cartValue, itemCount) {
1889
- const config = getConfig();
1890
- const settle = claimCartReport(config.storeSlug, order2.seq, cartValue, itemCount);
1891
- const delivered = await postPing({
1892
- event_type: "presence",
1893
- cart_value: cartValue,
1894
- item_count: itemCount,
1895
- cart_seq: order2.seq,
1896
- cart_seq_scope: order2.scope,
1897
- // A cart marks the analytics session as one that did not bounce.
1898
- session_id: analyticsSessionId(config.storeSlug, { touch: false }) ?? void 0,
1899
- path: resolveStorefrontPagePath()
1900
- });
1901
- settle(delivered);
1955
+ function clearLatestQuoteToken() {
1956
+ latestQuoteToken = null;
1957
+ quoteSequence += 1;
1902
1958
  }
1903
- function startSessionOnConsent() {
1904
- const config = getConfig();
1905
- if (!isPageViewed() || analyticsSessionId(config.storeSlug, { touch: false })) return;
1906
- const sessionId = analyticsSessionId(config.storeSlug, { touch: true });
1907
- if (!sessionId) return;
1908
- markPageViewed(sessionId);
1909
- installEngagedClock(reportPageLeave);
1910
- void postPing({
1911
- event_type: "session_start",
1912
- referer: document.referrer || void 0,
1913
- session_id: sessionId,
1914
- path: resolveStorefrontPagePath(),
1915
- utm: resolveUtmParameters(config.storeSlug)
1959
+ async function quoteCart(coupon, currency) {
1960
+ const sequence = quoteSequence += 1;
1961
+ const payload = getCartPayload(coupon);
1962
+ const normalizedCurrency = normalizeRequestedCurrency(currency) ?? resolveBuyerCurrency(getConfig().currency);
1963
+ const response = await post("/v1/storefront/cart/quote", {
1964
+ shop_slug: payload.store_slug,
1965
+ cart: payload.items,
1966
+ coupon: payload.coupon,
1967
+ affiliate_code: getAmbientAffiliateCodeForQuote() ?? void 0,
1968
+ currency: normalizedCurrency ?? void 0
1916
1969
  });
1917
- }
1918
- function reportPageLeave() {
1919
- const config = getConfig();
1920
- const sessionId = currentPageSession();
1921
- const engagedMs2 = takeEngagedMs();
1922
- if (!sessionId || engagedMs2 === 0 || readAnalyticsConsent() !== "granted") return;
1923
- try {
1924
- const endpoint = buildEndpoint("/v1/storefront/shops/:storeSlug/ping", { storeSlug: config.storeSlug });
1925
- void fetch(`${config.apiBaseUrl}${endpoint}`, {
1926
- method: "POST",
1927
- headers: { "Content-Type": "application/json" },
1928
- body: JSON.stringify({ event_type: "page_leave", session_id: sessionId, engaged_ms: engagedMs2 }),
1929
- keepalive: true
1930
- }).catch(() => {
1931
- });
1932
- } catch {
1970
+ if (sequence !== quoteSequence) return response;
1971
+ if (response.success) {
1972
+ latestQuoteToken = typeof response.data?.quote_token === "string" ? response.data.quote_token : null;
1933
1973
  }
1974
+ return response;
1934
1975
  }
1935
1976
 
1936
1977
  // ../sdk/src/storefront-storage-sdk.ts
@@ -1976,7 +2017,8 @@ var storefrontStorageSdk = {
1976
2017
  trackAffiliateEvent,
1977
2018
  // Analytics
1978
2019
  trackPageView,
1979
- trackCartActivity
2020
+ trackCartActivity,
2021
+ trackProductView
1980
2022
  };
1981
2023
 
1982
2024
  // ../../node_modules/.bun/openapi-fetch@0.17.0/node_modules/openapi-fetch/dist/index.mjs
@@ -2785,15 +2827,21 @@ async function getCategories() {
2785
2827
  }
2786
2828
 
2787
2829
  // ../contracts/src/redirect-link-template.ts
2788
- var REMAINING_PLACEHOLDER_PATTERNS = [
2789
- /\{\{[\s\S]*?\}\}/g,
2790
- /(?<!\{)\{[^{}]+\}(?!\})/g
2791
- ];
2830
+ function isInsideDoubleBrace(raw) {
2831
+ return raw.startsWith("{{");
2832
+ }
2833
+ function placeholderMatches(template, patterns) {
2834
+ return [
2835
+ ...template.matchAll(patterns.double),
2836
+ ...[...template.matchAll(patterns.single)].filter((match) => !isInsideDoubleBrace(match[0]))
2837
+ ];
2838
+ }
2839
+ var REMAINING_PLACEHOLDER_PATTERNS = {
2840
+ double: /\{\{[\s\S]*?\}\}/g,
2841
+ single: /\{?\{[^{}]+\}(?!\})/g
2842
+ };
2792
2843
  function findDanglingBraceFragments(template) {
2793
- let stripped = template;
2794
- for (const pattern of REMAINING_PLACEHOLDER_PATTERNS) {
2795
- stripped = stripped.replace(pattern, "");
2796
- }
2844
+ const stripped = template.replace(REMAINING_PLACEHOLDER_PATTERNS.double, "").replace(REMAINING_PLACEHOLDER_PATTERNS.single, (raw) => isInsideDoubleBrace(raw) ? raw : "");
2797
2845
  return /[{}]/.test(stripped) ? [...new Set(stripped.match(/[{}]+/g) ?? [])] : [];
2798
2846
  }
2799
2847
  function findRemainingProductRedirectPlaceholders(template) {
@@ -2801,10 +2849,8 @@ function findRemainingProductRedirectPlaceholders(template) {
2801
2849
  return [];
2802
2850
  }
2803
2851
  const remaining = /* @__PURE__ */ new Set();
2804
- for (const pattern of REMAINING_PLACEHOLDER_PATTERNS) {
2805
- for (const match of template.matchAll(pattern)) {
2806
- remaining.add(match[0]);
2807
- }
2852
+ for (const match of placeholderMatches(template, REMAINING_PLACEHOLDER_PATTERNS)) {
2853
+ remaining.add(match[0]);
2808
2854
  }
2809
2855
  for (const fragment of findDanglingBraceFragments(template)) {
2810
2856
  remaining.add(fragment);
@@ -4442,6 +4488,9 @@ function order(id) {
4442
4488
  schema: customerPortalInvoiceDetailSchema
4443
4489
  });
4444
4490
  }
4491
+ function orderReview(uniqid) {
4492
+ return requestCustomer(`/invoices/${encodeURIComponent(uniqid)}/review`, { schema: customerPortalOrderReviewSchema });
4493
+ }
4445
4494
  async function invoicePdf(id) {
4446
4495
  try {
4447
4496
  const response = await fetch(`${CUSTOMER_API_PREFIX}/invoices/${encodeURIComponent(id)}/pdf`, {
@@ -4574,19 +4623,29 @@ function createTicket(payload) {
4574
4623
  body: {
4575
4624
  ...payload.title !== void 0 ? { title: payload.title } : {},
4576
4625
  message: payload.message,
4577
- ...payload.invoice_id !== void 0 ? { invoice_id: payload.invoice_id } : {}
4626
+ ...payload.invoice_id !== void 0 ? { invoice_id: payload.invoice_id } : {},
4627
+ ...payload.attachment_ids?.length ? { attachment_ids: payload.attachment_ids } : {}
4578
4628
  }
4579
4629
  });
4580
4630
  }
4581
4631
  function ticket(uniqid) {
4582
4632
  return requestCustomer(`/tickets/${encodeURIComponent(uniqid)}`);
4583
4633
  }
4584
- function replyToTicket(uniqid, message) {
4634
+ function replyToTicket(uniqid, message, attachmentIds = []) {
4585
4635
  return requestCustomer(`/tickets/${encodeURIComponent(uniqid)}/reply`, {
4586
4636
  method: "POST",
4587
- body: { message }
4637
+ body: attachmentIds.length > 0 ? { message, attachment_ids: attachmentIds } : { message }
4588
4638
  });
4589
4639
  }
4640
+ function uploadTicketImage(file, ticket2) {
4641
+ const form = new FormData();
4642
+ form.append("file", file);
4643
+ if (ticket2) form.append("ticket", ticket2);
4644
+ return requestCustomer("/ticket-attachments", { method: "POST", formData: form });
4645
+ }
4646
+ function discardTicketImage(id) {
4647
+ return requestCustomer(`/ticket-attachments/${encodeURIComponent(id)}`, { method: "DELETE" });
4648
+ }
4590
4649
  function updateProfile(patch) {
4591
4650
  return requestCustomer("/profile", {
4592
4651
  method: "POST",
@@ -4697,6 +4756,7 @@ var shoppex = {
4697
4756
  redeemLoyaltyPoints,
4698
4757
  warranties,
4699
4758
  claimWarranty,
4759
+ orderReview,
4700
4760
  resetLicenseHwid,
4701
4761
  subscriptionBillingHistory,
4702
4762
  cancelSubscription,
@@ -4712,6 +4772,8 @@ var shoppex = {
4712
4772
  createTicket,
4713
4773
  ticket,
4714
4774
  replyToTicket,
4775
+ uploadTicketImage,
4776
+ discardTicketImage,
4715
4777
  updateProfile,
4716
4778
  updateAvatar,
4717
4779
  removeAvatar,
@@ -4769,6 +4831,7 @@ export {
4769
4831
  createTicket,
4770
4832
  dashboard,
4771
4833
  src_default as default,
4834
+ discardTicketImage,
4772
4835
  emailPreferences,
4773
4836
  enrollAsReseller,
4774
4837
  ensureCartLineId,
@@ -4800,6 +4863,7 @@ export {
4800
4863
  normalizeStorefrontCustomFields,
4801
4864
  notificationPreferences,
4802
4865
  order,
4866
+ orderReview,
4803
4867
  orders,
4804
4868
  pauseSubscription,
4805
4869
  productMatchesSearchQuery,
@@ -4836,11 +4900,13 @@ export {
4836
4900
  touchStorefrontPresence,
4837
4901
  trackCartActivity,
4838
4902
  trackPageView,
4903
+ trackProductView,
4839
4904
  undoSubscriptionCancel,
4840
4905
  updateAvatar,
4841
4906
  updateEmailPreferences,
4842
4907
  updateNotificationPreferences,
4843
4908
  updateProfile,
4909
+ uploadTicketImage,
4844
4910
  validateStorefrontCustomFieldValue,
4845
4911
  verifyOtp,
4846
4912
  warranties