@shoppexio/storefront 1.0.103 → 1.0.104

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