@vio-live/web-sdk 0.23.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,261 @@ var __decorateClass = (decorators, target, key, kind) => {
11
11
  return result;
12
12
  };
13
13
 
14
+ // src/core/slots.ts
15
+ var SLOT_ID_MAX_LENGTH = 100;
16
+ var SLOT_DECLARATION_TTL_MS = 24 * 60 * 60 * 1e3;
17
+ var SLOT_DECLARATION_FAILURE_TTL_MS = 60 * 60 * 1e3;
18
+ var MAX_LOCATIONS_PER_DECLARATION = 100;
19
+ var MAX_DECLARATIONS_PER_LOAD = 3;
20
+ var SLOT_KIND_MAX_LENGTH = 32;
21
+ function componentKindOf(templateId, slot) {
22
+ const hint = typeof templateId === "string" ? templateId.trim().toLowerCase() : "";
23
+ if (hint) return hint.replace(/^(product|vev)-/, "").slice(0, SLOT_KIND_MAX_LENGTH) || void 0;
24
+ const id = typeof slot === "string" ? slot.toLowerCase() : "";
25
+ const m = id.match(/(?:^|-)(card|carousel|grid|detail|banner|list|hero|stepper)(?:-|$)/);
26
+ return m?.[1];
27
+ }
28
+ function normalizeSlotKind(value) {
29
+ if (typeof value !== "string") return void 0;
30
+ const kind = value.trim().toLowerCase().slice(0, SLOT_KIND_MAX_LENGTH).trim();
31
+ return kind || void 0;
32
+ }
33
+ function resolveSlotCampaign(boot, slot, now = Date.now(), platform, pageCampaignId, sponsorId) {
34
+ if (!boot) return null;
35
+ const page = activeCampaignEntry(boot, pageCampaignId);
36
+ const assignment = findSlotAssignmentDetail(boot, slot, now, platform);
37
+ const sponsor = toNumber(sponsorId);
38
+ if (assignment && assignmentApplies(assignment, page, sponsor)) return assignment.campaign;
39
+ return activeCampaignEntry(boot, pageCampaignId, sponsor) ?? (sponsor !== void 0 ? sponsorCampaignEntry(boot, sponsor) : null) ?? defaultCampaignEntry(boot);
40
+ }
41
+ function sponsorCampaignEntry(boot, sponsorId) {
42
+ const id = toNumber(sponsorId);
43
+ if (!boot || id === void 0) return null;
44
+ try {
45
+ const matches = activeCampaigns(boot).filter((c) => campaignSponsorIds(c).includes(id));
46
+ if (matches.length === 0) return null;
47
+ const defaultId = toNumber(boot.campaign?.id);
48
+ return matches.find((c) => toNumber(c.id) === defaultId) ?? matches[0] ?? null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ function assignmentApplies(assignment, page, sponsorId) {
54
+ if (assignment.slot.source !== "page") return true;
55
+ if (isStalePageAssignment(assignment, page)) return false;
56
+ return sponsorId === void 0 || campaignSponsorIds(assignment.campaign).includes(sponsorId);
57
+ }
58
+ function isStalePageAssignment(assignment, page) {
59
+ return !!page && assignment.slot.source === "page" && toNumber(page.id) !== toNumber(assignment.campaign.id);
60
+ }
61
+ function toCampaignId(value) {
62
+ let n;
63
+ if (typeof value === "number") n = value;
64
+ else if (typeof value === "string" && /^\s*\d+\s*$/.test(value)) n = Number(value);
65
+ else return void 0;
66
+ return Number.isSafeInteger(n) && n > 0 ? n : void 0;
67
+ }
68
+ function campaignCandidates(hint) {
69
+ const raw = Array.isArray(hint) ? hint : [hint];
70
+ const out = [];
71
+ for (const v of raw) {
72
+ const id = toCampaignId(v);
73
+ if (id !== void 0 && !out.includes(id)) out.push(id);
74
+ }
75
+ return out;
76
+ }
77
+ function activeCampaignEntry(boot, hint, sponsorId) {
78
+ if (!boot) return null;
79
+ try {
80
+ const active = activeCampaigns(boot);
81
+ for (const id of campaignCandidates(hint)) {
82
+ const entry = active.find((c) => toNumber(c.id) === id);
83
+ if (!entry) continue;
84
+ if (sponsorId !== void 0 && !campaignSponsorIds(entry).includes(sponsorId)) continue;
85
+ return entry;
86
+ }
87
+ } catch {
88
+ }
89
+ return null;
90
+ }
91
+ function campaignSponsorIds(campaign) {
92
+ const ids = [];
93
+ const primary = toNumber(campaign.primarySponsor?.id);
94
+ if (primary !== void 0) ids.push(primary);
95
+ const secondaries = Array.isArray(campaign.secondarySponsors) ? campaign.secondarySponsors : [];
96
+ for (const s of secondaries) {
97
+ const id = toNumber(s?.id);
98
+ if (id !== void 0) ids.push(id);
99
+ }
100
+ return ids;
101
+ }
102
+ function findSlotAssignmentDetail(boot, slot, now = Date.now(), platform) {
103
+ const name = typeof slot === "string" ? slot.trim() : "";
104
+ if (!boot || !name) return null;
105
+ try {
106
+ const matches = [];
107
+ for (const campaign of activeCampaigns(boot)) {
108
+ const match = slotsOf(campaign).find((s) => assignmentMatches(s, name, now, platform));
109
+ if (match) matches.push({ campaign, slot: match });
110
+ }
111
+ if (matches.length === 0) return null;
112
+ const defaultId = toNumber(boot.campaign?.id);
113
+ return matches.find((m) => toNumber(m.campaign.id) === defaultId) ?? matches[0] ?? null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+ function defaultCampaignEntry(boot) {
119
+ if (!boot?.campaign || typeof boot.campaign !== "object") return null;
120
+ const defaultId = toNumber(boot.campaign.id);
121
+ if (defaultId === void 0) return null;
122
+ try {
123
+ const listed = wellFormedCampaigns(boot).find((c) => toNumber(c.id) === defaultId);
124
+ if (listed) return listed;
125
+ } catch {
126
+ }
127
+ return {
128
+ ...boot.campaign,
129
+ isDefault: true,
130
+ primarySponsor: boot.primarySponsor ?? null,
131
+ secondarySponsors: Array.isArray(boot.secondarySponsors) ? boot.secondarySponsors : [],
132
+ slots: []
133
+ };
134
+ }
135
+ function wellFormedCampaigns(boot) {
136
+ const list = Array.isArray(boot.campaigns) ? boot.campaigns : [];
137
+ return list.filter((c) => !!c && typeof c === "object");
138
+ }
139
+ function activeCampaigns(boot) {
140
+ return wellFormedCampaigns(boot).filter((c) => c.status === "active");
141
+ }
142
+ function slotsOf(campaign) {
143
+ const slots = Array.isArray(campaign.slots) ? campaign.slots : [];
144
+ return slots.filter((s) => !!s && typeof s === "object");
145
+ }
146
+ function assignmentMatches(s, name, now, platform) {
147
+ if (typeof s.name !== "string" || s.name.trim() !== name) return false;
148
+ if (platform && s.platform != null && s.platform !== platform) return false;
149
+ const start = parseTime(s.startDate);
150
+ if (start !== void 0 && start > now) return false;
151
+ const end = parseTime(s.endDate);
152
+ if (end !== void 0 && now >= end) return false;
153
+ return true;
154
+ }
155
+ function parseTime(value) {
156
+ if (value == null || value === "") return void 0;
157
+ const t = Date.parse(String(value));
158
+ return Number.isFinite(t) ? t : void 0;
159
+ }
160
+ function toNumber(value) {
161
+ if (value === void 0 || value === null || value === "") return void 0;
162
+ const n = Number(value);
163
+ return Number.isFinite(n) ? n : void 0;
164
+ }
165
+ function normalizeSlotId(value) {
166
+ if (typeof value !== "string") return "";
167
+ return value.trim().slice(0, SLOT_ID_MAX_LENGTH).trim();
168
+ }
169
+ function normalizeSlotLocations(slots) {
170
+ const out = [];
171
+ const seen = /* @__PURE__ */ new Set();
172
+ if (!Array.isArray(slots)) return out;
173
+ for (const raw of slots) {
174
+ const id = normalizeSlotId(typeof raw === "string" ? raw : raw?.id);
175
+ if (!id || seen.has(id)) continue;
176
+ seen.add(id);
177
+ const displayName = raw && typeof raw === "object" && typeof raw.displayName === "string" ? raw.displayName.trim() : "";
178
+ const campaignId = raw && typeof raw === "object" ? toCampaignId(raw.campaignId) : void 0;
179
+ const kind = raw && typeof raw === "object" ? normalizeSlotKind(raw.kind) : void 0;
180
+ const location2 = { id };
181
+ if (displayName) location2.displayName = displayName;
182
+ if (campaignId !== void 0) location2.campaignId = campaignId;
183
+ if (kind) location2.kind = kind;
184
+ out.push(location2);
185
+ }
186
+ return out;
187
+ }
188
+ function autoSlotId(kind, ordinal, scope) {
189
+ const tail = `${kind}-${ordinal + 1}`;
190
+ let prefix = normalizeScope(scope);
191
+ const room = SLOT_ID_MAX_LENGTH - tail.length - 1;
192
+ if (prefix.length > room) prefix = prefix.slice(0, Math.max(0, room)).replace(/-+$/, "");
193
+ return `${prefix ? `${prefix}-` : ""}${tail}`;
194
+ }
195
+ function autoSlotDisplayName(kind, ordinal, scopeName) {
196
+ const label = `${kind.charAt(0).toUpperCase()}${kind.slice(1)} ${ordinal + 1}`;
197
+ const name = typeof scopeName === "string" ? scopeName.trim() : "";
198
+ return name ? `${label} \xB7 ${name}` : label;
199
+ }
200
+ function normalizeScope(scope) {
201
+ if (typeof scope !== "string") return "";
202
+ return scope.trim().replace(/\s+/g, "-");
203
+ }
204
+ var SLOT_DECLARATION_MEMORY_CAP = 500;
205
+ function slotDeclarationKey(location2, pageCampaignId) {
206
+ const campaignId = toCampaignId(location2.campaignId) ?? toCampaignId(pageCampaignId);
207
+ const kind = normalizeSlotKind(location2.kind);
208
+ return location2.id + (location2.displayName ? `${location2.displayName}` : "") + (campaignId !== void 0 ? `${campaignId}` : "") + (kind !== void 0 ? `${kind}` : "");
209
+ }
210
+ function pendingSlotKeys(memory, keys, now = Date.now(), ttl = SLOT_DECLARATION_TTL_MS) {
211
+ return keys.filter((key) => {
212
+ const at = memory?.[key];
213
+ if (typeof at !== "number" || !Number.isFinite(at)) return true;
214
+ return now - at >= ttl || now < at;
215
+ });
216
+ }
217
+ function slotKeyId(key) {
218
+ const cut4 = key.search(/[\u001f\u001e\u001d]/);
219
+ return cut4 >= 0 ? key.slice(0, cut4) : key;
220
+ }
221
+ function rememberSlotKeys(memory, keys, at, cap = SLOT_DECLARATION_MEMORY_CAP) {
222
+ const next = { ...memory ?? {} };
223
+ const ids = new Set(keys.map(slotKeyId));
224
+ for (const existing of Object.keys(next)) {
225
+ if (ids.has(slotKeyId(existing)) && !keys.includes(existing)) delete next[existing];
226
+ }
227
+ for (const key of keys) next[key] = at;
228
+ const entries = Object.entries(next);
229
+ if (entries.length <= cap) return next;
230
+ entries.sort((a, b) => b[1] - a[1]);
231
+ return Object.fromEntries(entries.slice(0, cap));
232
+ }
233
+ function slotDeclarationStorageKey(platform, apiKey) {
234
+ return `vio.slots.${platform}.${fnv1a(apiKey)}`;
235
+ }
236
+ function fnv1a(input) {
237
+ let hash = 2166136261;
238
+ for (let i = 0; i < input.length; i++) {
239
+ hash ^= input.charCodeAt(i);
240
+ hash = Math.imul(hash, 16777619) >>> 0;
241
+ }
242
+ return hash.toString(16).padStart(8, "0");
243
+ }
244
+ function readSlotDeclaration(storageKey) {
245
+ try {
246
+ if (typeof localStorage === "undefined") return {};
247
+ const raw = localStorage.getItem(storageKey);
248
+ if (!raw) return {};
249
+ const parsed = JSON.parse(raw);
250
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
251
+ if (Array.isArray(parsed.keys)) return {};
252
+ const out = {};
253
+ for (const [key, at] of Object.entries(parsed)) {
254
+ if (typeof at === "number" && Number.isFinite(at)) out[key] = at;
255
+ }
256
+ return out;
257
+ } catch {
258
+ return {};
259
+ }
260
+ }
261
+ function writeSlotDeclaration(storageKey, memory) {
262
+ try {
263
+ if (typeof localStorage === "undefined") return;
264
+ localStorage.setItem(storageKey, JSON.stringify(memory));
265
+ } catch {
266
+ }
267
+ }
268
+
14
269
  // src/core/configuration.ts
15
270
  var DEFAULT_URLS = {
16
271
  // The Vio Backend development environment was removed on 2026-09-16; its REST
@@ -49,6 +304,10 @@ var ConfigurationSingleton = class {
49
304
  platform: input.platform ?? "web",
50
305
  contentId: input.contentId,
51
306
  contentTitle: input.contentTitle,
307
+ campaignId: toCampaignId(input.campaignId),
308
+ debug: input.debug === true,
309
+ // Only the two words the backend knows; anything else is "not said".
310
+ privacyMode: input.privacyMode === "minimal" || input.privacyMode === "standard" ? input.privacyMode : void 0,
52
311
  apiBase: input.apiBase ?? defaults.apiBase,
53
312
  graphQLBase: input.graphQLBase ?? defaults.graphQLBase,
54
313
  eventsBase: input.eventsBase ?? defaults.eventsBase,
@@ -138,6 +397,14 @@ var VioApi = class {
138
397
  async declareManifest(body) {
139
398
  return this.post("/v2/mobile/components/manifest", body);
140
399
  }
400
+ /**
401
+ * The surface of the key (`GET /v2/mobile/surface`): its id, name, status
402
+ * and `privacyMode` — what decides whether the order attribution may carry
403
+ * the entry context (ADR-0035). A 404 means a service that predates it.
404
+ */
405
+ async surface() {
406
+ return this.get("/v2/mobile/surface");
407
+ }
141
408
  /**
142
409
  * Get placement components for a campaign. Used by the carousel auto-fetch
143
410
  * mode (`<vio-product-carousel campaign-id="36" location="news-favorites">`).
@@ -457,6 +724,214 @@ function createCommerceClient(options) {
457
724
  return new CommerceClient(options);
458
725
  }
459
726
 
727
+ // src/core/content.ts
728
+ var CONTENT_ID_QUERY = "vio_content";
729
+ var CONTENT_TITLE_QUERY = "vio_content_title";
730
+ var CONTENT_ID_MAX_LENGTH = 128;
731
+ var CONTENT_TITLE_MAX_LENGTH = 255;
732
+ var CONTENT_URL_MAX_LENGTH = 2048;
733
+ function originAndPath(raw, max = CONTENT_URL_MAX_LENGTH) {
734
+ const value = text(raw);
735
+ if (!value) return void 0;
736
+ try {
737
+ const url = new URL(value);
738
+ return cutTo(`${url.origin}${url.pathname}`, max);
739
+ } catch {
740
+ const end = value.search(/[?#]/);
741
+ return cutTo(end >= 0 ? value.slice(0, end) : value, max);
742
+ }
743
+ }
744
+ function cutTo(value, max) {
745
+ const out = value.length > max ? value.slice(0, max) : value;
746
+ return out || void 0;
747
+ }
748
+ function capContentUrl(value) {
749
+ const url = text(value);
750
+ if (!url) return void 0;
751
+ if (url.length <= CONTENT_URL_MAX_LENGTH) return url;
752
+ const cut4 = url.search(/[?#]/);
753
+ const withoutQuery = cut4 >= 0 ? url.slice(0, cut4) : url;
754
+ return withoutQuery.length <= CONTENT_URL_MAX_LENGTH ? withoutQuery : withoutQuery.slice(0, CONTENT_URL_MAX_LENGTH);
755
+ }
756
+ function resolveContent(init, doc = typeof document !== "undefined" ? document : null, loc = typeof location !== "undefined" ? location : null) {
757
+ const out = {};
758
+ try {
759
+ const url = originAndPath(loc?.href);
760
+ if (url !== void 0) out.url = url;
761
+ } catch {
762
+ }
763
+ try {
764
+ const query = parseQuery(loc?.search);
765
+ const id = fits(text(init?.contentId)) ?? fits(text(query.get(CONTENT_ID_QUERY))) ?? fits(text(meta(doc, "name", "vio:content-id"))) ?? fits(stripQueryAndHash(text(attr(doc, 'link[rel="canonical"]', "href")))) ?? fits(stripQueryAndHash(text(meta(doc, "property", "og:url")))) ?? fits(text(loc?.pathname));
766
+ const title = text(init?.contentTitle) ?? text(query.get(CONTENT_TITLE_QUERY)) ?? text(meta(doc, "name", "vio:content-title")) ?? text(meta(doc, "property", "og:title")) ?? text(doc?.title);
767
+ if (id !== void 0) out.id = id;
768
+ if (title !== void 0) out.title = title.slice(0, CONTENT_TITLE_MAX_LENGTH);
769
+ return out;
770
+ } catch {
771
+ return out;
772
+ }
773
+ }
774
+ function fits(value) {
775
+ return value !== void 0 && value.length <= CONTENT_ID_MAX_LENGTH ? value : void 0;
776
+ }
777
+ function text(value) {
778
+ if (value === void 0 || value === null) return void 0;
779
+ const s = String(value).trim();
780
+ return s ? s : void 0;
781
+ }
782
+ function parseQuery(search) {
783
+ try {
784
+ return new URLSearchParams(typeof search === "string" ? search : "");
785
+ } catch {
786
+ return new URLSearchParams();
787
+ }
788
+ }
789
+ function meta(doc, key, value) {
790
+ return attr(doc, `meta[${key}="${value}"]`, "content");
791
+ }
792
+ function attr(doc, selector, name) {
793
+ try {
794
+ if (!doc || typeof doc.querySelector !== "function") return void 0;
795
+ const el = doc.querySelector(selector);
796
+ const v = el?.getAttribute?.(name);
797
+ return typeof v === "string" ? v : void 0;
798
+ } catch {
799
+ return void 0;
800
+ }
801
+ }
802
+ function stripQueryAndHash(url) {
803
+ if (!url) return void 0;
804
+ const cut4 = url.search(/[?#]/);
805
+ const clean = cut4 >= 0 ? url.slice(0, cut4) : url;
806
+ return clean.trim() || void 0;
807
+ }
808
+
809
+ // src/core/cart/origin-wire.ts
810
+ var GRAPHQL_INT_MAX = 2147483647;
811
+ var LINE_ORIGIN_CAPS = Object.freeze({
812
+ locationId: SLOT_ID_MAX_LENGTH,
813
+ componentKind: SLOT_KIND_MAX_LENGTH,
814
+ contentUrl: 2048,
815
+ contentTitle: CONTENT_TITLE_MAX_LENGTH
816
+ });
817
+ function lineOriginWire(origin, options = {}) {
818
+ if (!origin || typeof origin !== "object") return void 0;
819
+ const page = options.page ?? {};
820
+ const standard = options.privacyMode === "standard";
821
+ const out = {};
822
+ const campaignId = Number(origin.campaignId);
823
+ if (Number.isInteger(campaignId) && campaignId > 0 && campaignId <= GRAPHQL_INT_MAX) out.campaign_id = campaignId;
824
+ const locationId = cut(origin.slot, LINE_ORIGIN_CAPS.locationId);
825
+ if (locationId && standard) out.location_id = locationId;
826
+ const kind = cut(origin.componentKind, LINE_ORIGIN_CAPS.componentKind) ?? componentKindOf(void 0, origin.slot);
827
+ if (kind) out.component_kind = kind;
828
+ if (!out.campaign_id && !out.location_id && !out.component_kind) return void 0;
829
+ if (!standard) return out;
830
+ const url = originAndPath(origin.contentUrl, LINE_ORIGIN_CAPS.contentUrl) ?? originAndPath(page.contentUrl, LINE_ORIGIN_CAPS.contentUrl);
831
+ if (url) out.content_url = url;
832
+ const title = cut(origin.contentTitle, LINE_ORIGIN_CAPS.contentTitle) ?? cut(page.contentTitle, LINE_ORIGIN_CAPS.contentTitle);
833
+ if (title) out.content_title = title;
834
+ return out;
835
+ }
836
+ function currentOriginPage() {
837
+ const out = {};
838
+ try {
839
+ const facade = globalThis.__VIO_FACADE__;
840
+ const content = typeof facade?.content === "function" ? facade.content() : resolveContent();
841
+ const url = originAndPath(content?.url, LINE_ORIGIN_CAPS.contentUrl);
842
+ if (url) out.contentUrl = url;
843
+ const title = cut(content?.title, LINE_ORIGIN_CAPS.contentTitle);
844
+ if (title) out.contentTitle = title;
845
+ } catch {
846
+ }
847
+ return out;
848
+ }
849
+ function cut(value, max) {
850
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
851
+ const text3 = String(value).trim();
852
+ if (!text3) return void 0;
853
+ return text3.length > max ? text3.slice(0, max).trim() || void 0 : text3;
854
+ }
855
+ async function currentPrivacyMode() {
856
+ try {
857
+ const facade = globalThis.__VIO_FACADE__;
858
+ if (typeof facade?.privacyMode !== "function") return "unknown";
859
+ const mode = await facade.privacyMode();
860
+ return mode === "standard" || mode === "minimal" ? mode : "unknown";
861
+ } catch {
862
+ return "unknown";
863
+ }
864
+ }
865
+
866
+ // src/core/buyer-ref.ts
867
+ var BUYER_REF_MAX_LENGTH = 128;
868
+ var BUYER_REF_PATTERN = /^[A-Za-z0-9._~-]+$/;
869
+ function normalizeBuyerRef(value) {
870
+ if (typeof value !== "string") return void 0;
871
+ const ref = value.trim();
872
+ if (!ref || ref.length > BUYER_REF_MAX_LENGTH || !BUYER_REF_PATTERN.test(ref)) return void 0;
873
+ return ref;
874
+ }
875
+
876
+ // src/core/api/gateway-features.ts
877
+ var FEATURE_TOKENS = {
878
+ buyer_ref: ["buyer_ref"],
879
+ line_origin: ["origin", "CartLineOriginInput"],
880
+ checkout_attribution: ["attribution", "CheckoutAttributionInput", "CheckoutAttributionEntryInput"]
881
+ };
882
+ var UNKNOWN_PATTERN = /cannot query field|unknown field|unknown argument|unknown type|is not defined by type|not defined by type|field "[^"]+" is not defined/i;
883
+ var refused = /* @__PURE__ */ new Set();
884
+ var warned = /* @__PURE__ */ new Set();
885
+ function gatewayHas(feature) {
886
+ return !refused.has(feature);
887
+ }
888
+ function resetGatewayFeatures(feature) {
889
+ if (feature) {
890
+ refused.delete(feature);
891
+ warned.delete(feature);
892
+ return;
893
+ }
894
+ refused.clear();
895
+ warned.clear();
896
+ }
897
+ function messagesOf(err) {
898
+ if (!err || typeof err !== "object") return [];
899
+ const { message, graphql } = err;
900
+ return [message, ...(graphql?.errors ?? []).map((e) => e?.message)].filter(
901
+ (m) => typeof m === "string"
902
+ );
903
+ }
904
+ function ranSomething(err) {
905
+ if (!err || typeof err !== "object") return false;
906
+ const graphql = err.graphql;
907
+ if (graphql?.hasData === true) return true;
908
+ return (graphql?.errors ?? []).some((e) => Array.isArray(e?.path) && e.path.length > 0);
909
+ }
910
+ function isUnknownTokenError(err, token) {
911
+ if (ranSomething(err)) return false;
912
+ return messagesOf(err).some((m) => m.includes(`"${token}"`) && UNKNOWN_PATTERN.test(m));
913
+ }
914
+ function isUnknownFeatureError(err, feature) {
915
+ return FEATURE_TOKENS[feature].some((token) => isUnknownTokenError(err, token));
916
+ }
917
+ async function withGatewayFeature(feature, request) {
918
+ if (!gatewayHas(feature)) return request(false);
919
+ try {
920
+ return await request(true);
921
+ } catch (err) {
922
+ if (!isUnknownFeatureError(err, feature)) throw err;
923
+ refused.add(feature);
924
+ if (!warned.has(feature) && typeof console !== "undefined") {
925
+ warned.add(feature);
926
+ console.warn(
927
+ `[Vio] the commerce gateway does not know "${FEATURE_TOKENS[feature][0]}" (${feature}) yet \u2014 sent without it on this page load:`,
928
+ messagesOf(err)[0] ?? ""
929
+ );
930
+ }
931
+ return request(false);
932
+ }
933
+ }
934
+
460
935
  // src/core/api/cart-queries.ts
461
936
  var CREATE_CART_MUTATION = `
462
937
  mutation CreateCart($customerSessionId: String!, $currency: String!, $shippingCountry: String) {
@@ -674,10 +1149,10 @@ mutation DeleteItem($cartId: String!, $cartItemId: String!) {
674
1149
  }
675
1150
  }
676
1151
  `;
677
- var UPDATE_ITEM_MUTATION = `
678
- mutation UpdateItem($cartId: String!, $cartItemId: String!, $shippingId: String, $qty: Int) {
1152
+ var updateItemMutation = (withExtra) => `
1153
+ mutation UpdateItem($cartId: String!, $cartItemId: String!, $shippingId: String, $qty: Int${withExtra ? ", $origin: CartLineOriginInput" : ""}) {
679
1154
  Cart {
680
- UpdateItem(cart_id: $cartId, cart_item_id: $cartItemId, shipping_id: $shippingId, qty: $qty) {
1155
+ UpdateItem(cart_id: $cartId, cart_item_id: $cartItemId, shipping_id: $shippingId, qty: $qty${withExtra ? ", origin: $origin" : ""}) {
681
1156
  cart_id
682
1157
  customer_session_id
683
1158
  shipping_country
@@ -746,6 +1221,8 @@ mutation UpdateItem($cartId: String!, $cartItemId: String!, $shippingId: String,
746
1221
  }
747
1222
  }
748
1223
  `;
1224
+ var UPDATE_ITEM_MUTATION = updateItemMutation(false);
1225
+ var UPDATE_ITEM_WITH_ORIGIN_MUTATION = updateItemMutation(true);
749
1226
  var GET_CART_QUERY = `
750
1227
  query GetCart($cartId: String!) {
751
1228
  Cart {
@@ -959,10 +1436,10 @@ mutation UpdateShippingsBySupplier($cartId: String!, $data: [DataUpdateShippings
959
1436
  }
960
1437
  }
961
1438
  `;
962
- var CREATE_CHECKOUT_MUTATION = `
963
- mutation CreateCheckout($cartId: String!) {
1439
+ var createCheckoutMutation = (withExtra) => `
1440
+ mutation CreateCheckout($cartId: String!${withExtra ? ", $attribution: CheckoutAttributionInput" : ""}) {
964
1441
  Checkout {
965
- CreateCheckout(cart_id: $cartId) {
1442
+ CreateCheckout(cart_id: $cartId${withExtra ? ", attribution: $attribution" : ""}) {
966
1443
  buyer_accepts_purchase_conditions
967
1444
  buyer_accepts_terms_conditions
968
1445
  created_at
@@ -1091,10 +1568,12 @@ mutation CreateCheckout($cartId: String!) {
1091
1568
  }
1092
1569
  }
1093
1570
  `;
1094
- var UPDATE_CHECKOUT_MUTATION = `
1095
- mutation UpdateCheckout($checkoutId: String!, $status: String, $successUrl: String, $email: String, $cancelUrl: String, $paymentMethod: String, $shippingAddress: AddressArgs, $billingAddress: AddressArgs, $buyerAcceptsPurchaseConditions: Boolean, $buyerAcceptsTermsConditions: Boolean) {
1571
+ var CREATE_CHECKOUT_MUTATION = createCheckoutMutation(false);
1572
+ var CREATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION = createCheckoutMutation(true);
1573
+ var updateCheckoutMutation = (withExtra) => `
1574
+ mutation UpdateCheckout($checkoutId: String!, $status: String, $successUrl: String, $email: String, $cancelUrl: String, $paymentMethod: String, $shippingAddress: AddressArgs, $billingAddress: AddressArgs, $buyerAcceptsPurchaseConditions: Boolean, $buyerAcceptsTermsConditions: Boolean${withExtra ? ", $attribution: CheckoutAttributionInput" : ""}) {
1096
1575
  Checkout {
1097
- UpdateCheckout(checkout_id: $checkoutId, status: $status, success_url: $successUrl, email: $email, cancel_url: $cancelUrl, payment_method: $paymentMethod, shipping_address: $shippingAddress, billing_address: $billingAddress, buyer_accepts_purchase_conditions: $buyerAcceptsPurchaseConditions, buyer_accepts_terms_conditions: $buyerAcceptsTermsConditions) {
1576
+ UpdateCheckout(checkout_id: $checkoutId, status: $status, success_url: $successUrl, email: $email, cancel_url: $cancelUrl, payment_method: $paymentMethod, shipping_address: $shippingAddress, billing_address: $billingAddress, buyer_accepts_purchase_conditions: $buyerAcceptsPurchaseConditions, buyer_accepts_terms_conditions: $buyerAcceptsTermsConditions${withExtra ? ", attribution: $attribution" : ""}) {
1098
1577
  buyer_accepts_purchase_conditions
1099
1578
  buyer_accepts_terms_conditions
1100
1579
  created_at
@@ -1223,6 +1702,8 @@ mutation UpdateCheckout($checkoutId: String!, $status: String, $successUrl: Stri
1223
1702
  }
1224
1703
  }
1225
1704
  `;
1705
+ var UPDATE_CHECKOUT_MUTATION = updateCheckoutMutation(false);
1706
+ var UPDATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION = updateCheckoutMutation(true);
1226
1707
  var GET_CHECKOUT_QUERY = `
1227
1708
  query GetCheckout($checkoutId: String!) {
1228
1709
  Checkout {
@@ -1445,7 +1926,9 @@ async function executeCartGraphQL(query, variables, options = {}) {
1445
1926
  const json = await Promise.race([res.json().catch(() => ({})), timedOut]);
1446
1927
  if (!res.ok || json.errors) {
1447
1928
  const errMsg = json.errors?.[0]?.message || `GraphQL request failed (HTTP ${res.status})`;
1448
- throw new Error(errMsg);
1929
+ throw Object.assign(new Error(errMsg), {
1930
+ graphql: { hasData: json.data !== void 0 && json.data !== null, errors: json.errors ?? [] }
1931
+ });
1449
1932
  }
1450
1933
  return json;
1451
1934
  } finally {
@@ -1465,19 +1948,37 @@ async function createCart(customerSessionId, currency, shippingCountry, options)
1465
1948
  return json?.data?.Cart?.CreateCart;
1466
1949
  }
1467
1950
  async function addItem(cartId, lineItems, options) {
1468
- const json = await executeCartGraphQL(ADD_ITEM_MUTATION, { cartId, lineItems }, options);
1951
+ const attributed = lineItems.some((li) => !!li?.origin);
1952
+ const json = attributed ? await withGatewayFeature(
1953
+ "line_origin",
1954
+ (supported) => executeCartGraphQL(
1955
+ ADD_ITEM_MUTATION,
1956
+ { cartId, lineItems: supported ? lineItems : lineItems.map(withoutOrigin) },
1957
+ options
1958
+ )
1959
+ ) : await executeCartGraphQL(ADD_ITEM_MUTATION, { cartId, lineItems }, options);
1469
1960
  return json?.data?.Cart?.AddItem;
1470
1961
  }
1962
+ function withoutOrigin(lineItem) {
1963
+ if (!lineItem || typeof lineItem !== "object") return lineItem;
1964
+ const { origin: _origin, ...rest } = lineItem;
1965
+ return rest;
1966
+ }
1471
1967
  async function deleteItem(cartId, cartItemId, options) {
1472
1968
  const json = await executeCartGraphQL(DELETE_ITEM_MUTATION, { cartId, cartItemId }, options);
1473
1969
  return json?.data?.Cart?.DeleteItem;
1474
1970
  }
1475
- async function updateItem(cartId, cartItemId, shippingId, qty, options) {
1476
- const json = await executeCartGraphQL(
1477
- UPDATE_ITEM_MUTATION,
1478
- { cartId, cartItemId, shippingId: shippingId || null, qty: qty != null ? qty : null },
1479
- options
1480
- );
1971
+ async function updateItem(cartId, cartItemId, shippingId, qty, options, extra = {}) {
1972
+ const variables = { cartId, cartItemId, shippingId: shippingId || null, qty: qty != null ? qty : null };
1973
+ const origin = extra.origin;
1974
+ const json = origin ? await withGatewayFeature(
1975
+ "line_origin",
1976
+ (supported) => executeCartGraphQL(
1977
+ supported ? UPDATE_ITEM_WITH_ORIGIN_MUTATION : UPDATE_ITEM_MUTATION,
1978
+ supported ? { ...variables, origin } : variables,
1979
+ options
1980
+ )
1981
+ ) : await executeCartGraphQL(UPDATE_ITEM_MUTATION, variables, options);
1481
1982
  return json?.data?.Cart?.UpdateItem;
1482
1983
  }
1483
1984
  async function getCart(cartId, options) {
@@ -1496,32 +1997,68 @@ async function updateShippingsBySupplier(cartId, data, options) {
1496
1997
  );
1497
1998
  return json?.data?.Cart?.UpdateShippingsBySupplier;
1498
1999
  }
1499
- async function createCheckout(cartId, options) {
1500
- const json = await executeCartGraphQL(CREATE_CHECKOUT_MUTATION, { cartId }, options);
2000
+ async function createCheckout(cartId, options, extra = {}) {
2001
+ const attribution = extra.attribution;
2002
+ const json = attribution ? await withGatewayFeature(
2003
+ "checkout_attribution",
2004
+ (supported) => executeCartGraphQL(
2005
+ supported ? CREATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION : CREATE_CHECKOUT_MUTATION,
2006
+ supported ? { cartId, attribution } : { cartId },
2007
+ options
2008
+ )
2009
+ ) : await executeCartGraphQL(CREATE_CHECKOUT_MUTATION, { cartId }, options);
1501
2010
  return json?.data?.Checkout?.CreateCheckout;
1502
2011
  }
1503
2012
  async function updateCheckout(variables, options) {
1504
- const json = await executeCartGraphQL(UPDATE_CHECKOUT_MUTATION, variables, options);
2013
+ const { attribution, ...legacy } = variables;
2014
+ const json = attribution ? await withGatewayFeature(
2015
+ "checkout_attribution",
2016
+ (supported) => executeCartGraphQL(
2017
+ supported ? UPDATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION : UPDATE_CHECKOUT_MUTATION,
2018
+ supported ? variables : legacy,
2019
+ options
2020
+ )
2021
+ ) : await executeCartGraphQL(UPDATE_CHECKOUT_MUTATION, legacy, options);
1505
2022
  return json?.data?.Checkout?.UpdateCheckout;
1506
2023
  }
1507
2024
  async function getCheckout(checkoutId, options) {
1508
2025
  const json = await executeCartGraphQL(GET_CHECKOUT_QUERY, { checkoutId }, options);
1509
2026
  return json?.data?.Checkout?.GetCheckout;
1510
2027
  }
1511
- var GET_CHECKOUT_ORDER_NUMBER_QUERY = `
2028
+ var BUYER_REF_FIELD = "buyer_ref";
2029
+ function gatewayHasBuyerRef() {
2030
+ return gatewayHas("buyer_ref");
2031
+ }
2032
+ function withBuyerRef(request) {
2033
+ return withGatewayFeature("buyer_ref", request);
2034
+ }
2035
+ var orderNumberQuery = (withRef) => `
1512
2036
  query GetCheckoutOrderNumber($checkoutId: String!) {
1513
2037
  Checkout {
1514
2038
  GetCheckout(checkout_id: $checkoutId) {
1515
2039
  id
1516
- order_id
2040
+ order_id${withRef ? `
2041
+ ${BUYER_REF_FIELD}` : ""}
1517
2042
  }
1518
2043
  }
1519
2044
  }
1520
2045
  `;
1521
- async function getCheckoutOrderNumber(checkoutId, options) {
1522
- const json = await executeCartGraphQL(GET_CHECKOUT_ORDER_NUMBER_QUERY, { checkoutId }, options);
1523
- const n = Number(json?.data?.Checkout?.GetCheckout?.order_id);
1524
- return Number.isInteger(n) && n > 0 ? n : null;
2046
+ var GET_CHECKOUT_ORDER_NUMBER_QUERY = orderNumberQuery(true);
2047
+ var GET_CHECKOUT_ORDER_NUMBER_QUERY_LEGACY = orderNumberQuery(false);
2048
+ async function getCheckoutReference(checkoutId, options) {
2049
+ const json = await withBuyerRef(
2050
+ (withRef) => executeCartGraphQL(
2051
+ withRef ? GET_CHECKOUT_ORDER_NUMBER_QUERY : GET_CHECKOUT_ORDER_NUMBER_QUERY_LEGACY,
2052
+ { checkoutId },
2053
+ options
2054
+ )
2055
+ );
2056
+ const checkout = json?.data?.Checkout?.GetCheckout;
2057
+ const n = Number(checkout?.order_id);
2058
+ return {
2059
+ orderNumber: Number.isInteger(n) && n > 0 ? n : null,
2060
+ buyerRef: normalizeBuyerRef(checkout?.[BUYER_REF_FIELD]) ?? null
2061
+ };
1525
2062
  }
1526
2063
  async function getAvailablePaymentMethods(options) {
1527
2064
  const json = await executeCartGraphQL(GET_AVAILABLE_PAYMENT_METHODS_QUERY, {}, options);
@@ -1555,18 +2092,27 @@ async function createPaymentApplePay(variables, options) {
1555
2092
  const json = await executeCartGraphQL(CREATE_PAYMENT_APPLE_PAY_MUTATION, variables, options);
1556
2093
  return json?.data?.Payment?.CreatePaymentApplePay;
1557
2094
  }
1558
- var CONFIRM_PAYMENT_APPLE_PAY_MUTATION = `
2095
+ var confirmApplePayMutation = (withRef) => `
1559
2096
  mutation ConfirmPaymentApplePay($checkoutId: String!, $applePayToken: String!, $email: String, $shippingAddress: ApplePayAddressInput) {
1560
2097
  Payment {
1561
2098
  ConfirmPaymentApplePay(checkout_id: $checkoutId, apple_pay_token: $applePayToken, email: $email, shipping_address: $shippingAddress) {
1562
2099
  order_id
1563
- status
2100
+ status${withRef ? `
2101
+ ${BUYER_REF_FIELD}` : ""}
1564
2102
  }
1565
2103
  }
1566
2104
  }
1567
2105
  `;
2106
+ var CONFIRM_PAYMENT_APPLE_PAY_MUTATION = confirmApplePayMutation(true);
2107
+ var CONFIRM_PAYMENT_APPLE_PAY_MUTATION_LEGACY = confirmApplePayMutation(false);
1568
2108
  async function confirmPaymentApplePay(variables, options) {
1569
- const json = await executeCartGraphQL(CONFIRM_PAYMENT_APPLE_PAY_MUTATION, variables, options);
2109
+ const json = await withBuyerRef(
2110
+ (withRef) => executeCartGraphQL(
2111
+ withRef ? CONFIRM_PAYMENT_APPLE_PAY_MUTATION : CONFIRM_PAYMENT_APPLE_PAY_MUTATION_LEGACY,
2112
+ variables,
2113
+ options
2114
+ )
2115
+ );
1570
2116
  return json?.data?.Payment?.ConfirmPaymentApplePay;
1571
2117
  }
1572
2118
 
@@ -1604,6 +2150,33 @@ async function getVippsStatus(variables, options) {
1604
2150
 
1605
2151
  // src/core/cart/cart-manager.ts
1606
2152
  var STORAGE_KEY = "vio.cart.v1";
2153
+ function finalTarget(sync) {
2154
+ return sync.steps.reduce((quantity, step) => quantity + step.delta, sync.sent);
2155
+ }
2156
+ function queueStep(sync, target, origin) {
2157
+ const delta = target - finalTarget(sync);
2158
+ if (delta === 0) return;
2159
+ if (delta > 0) {
2160
+ const last = sync.steps[sync.steps.length - 1];
2161
+ if (last && last.delta > 0 && sameOrigin(last.origin ?? {}, origin ?? {})) last.delta += delta;
2162
+ else sync.steps.push({ delta, origin });
2163
+ return;
2164
+ }
2165
+ let left = -delta;
2166
+ while (left > 0 && sync.steps.length > 0) {
2167
+ const last = sync.steps[sync.steps.length - 1];
2168
+ if (last.delta <= 0) break;
2169
+ const take = Math.min(last.delta, left);
2170
+ last.delta -= take;
2171
+ left -= take;
2172
+ if (last.delta === 0) sync.steps.pop();
2173
+ }
2174
+ if (left > 0) {
2175
+ const last = sync.steps[sync.steps.length - 1];
2176
+ if (last && last.delta < 0) last.delta -= left;
2177
+ else sync.steps.push({ delta: -left });
2178
+ }
2179
+ }
1607
2180
  function lineId() {
1608
2181
  return "li_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
1609
2182
  }
@@ -1695,7 +2268,7 @@ var CartManager = class extends EventTarget {
1695
2268
  * undefined when no quantity sync is active for it. */
1696
2269
  intendedQuantity(sponsorId, cartItemId) {
1697
2270
  const sync = this.qtySyncs.get(this.qtySyncKey(sponsorId, cartItemId));
1698
- return sync ? sync.pending ?? sync.sent : void 0;
2271
+ return sync ? finalTarget(sync) : void 0;
1699
2272
  }
1700
2273
  /** Apply a backend response only if no newer response was applied already.
1701
2274
  * Returns false when it was discarded as stale. */
@@ -1858,7 +2431,8 @@ var CartManager = class extends EventTarget {
1858
2431
  currency: li.price?.currency_code || backendCart.currency || matchedLocal?.currency || "NOK",
1859
2432
  imageUrl,
1860
2433
  quantity: intended ?? li.quantity,
1861
- // The server knows nothing about origins: keep the local ones. They
2434
+ // The server keeps its own copy of the origins (per request); the
2435
+ // cart read model does not answer them, so keep the local ones. They
1862
2436
  // are squared with its quantity (a stock clamp) only when no OTHER
1863
2437
  // mutation is in flight for this sponsor — an AddItem answering
1864
2438
  // while another is pending reports a quantity that is already old
@@ -1973,11 +2547,16 @@ var CartManager = class extends EventTarget {
1973
2547
  const cartId = await this.ensureCartId(opts.sponsorId, opts.currency || void 0);
1974
2548
  if (cartId) {
1975
2549
  const gqlOpts = await getCartGraphQLOptions(opts.sponsorId);
2550
+ const origin = opts.origin ? lineOriginWire(normalizeOrigin(opts.origin), {
2551
+ page: currentOriginPage(),
2552
+ privacyMode: await currentPrivacyMode()
2553
+ }) : void 0;
1976
2554
  const lineItemsInput = [
1977
2555
  {
1978
2556
  product_id: Number(opts.productId || 0),
1979
2557
  quantity: qty,
1980
- variant_id: opts.variantId != null ? Number(opts.variantId) : null
2558
+ variant_id: opts.variantId != null ? Number(opts.variantId) : null,
2559
+ ...origin ? { origin } : {}
1981
2560
  }
1982
2561
  ];
1983
2562
  const resCart = await addItem(cartId, lineItemsInput, gqlOpts);
@@ -2030,8 +2609,10 @@ var CartManager = class extends EventTarget {
2030
2609
  if (target === item.quantity) return;
2031
2610
  const targetItemId = item.cartItemId;
2032
2611
  const removedOrigin = latestOrigin(item);
2033
- if (target > item.quantity) addOriginQuantity(item, opts.origin, target - item.quantity);
2612
+ const increase = target > item.quantity;
2613
+ if (increase) addOriginQuantity(item, opts.origin, target - item.quantity);
2034
2614
  else removeOriginQuantity(item, item.quantity - target);
2615
+ const addedOrigin = increase ? latestOrigin(item) : void 0;
2035
2616
  if (target <= 0) {
2036
2617
  cart.items = cart.items.filter((i) => i !== item);
2037
2618
  if (typeof document !== "undefined") {
@@ -2066,37 +2647,47 @@ var CartManager = class extends EventTarget {
2066
2647
  this.pendingQtyIntents.set(this.intentKey(item.productId, item.variantId), target);
2067
2648
  return;
2068
2649
  }
2069
- this.syncQuantity(sponsorId, cart.cartId, targetItemId, target);
2650
+ this.syncQuantity(sponsorId, cart.cartId, targetItemId, target, addedOrigin);
2070
2651
  }
2071
2652
  /** Send a line's target quantity to the backend — ONE request in flight per
2072
- * line. A target that arrives while one is in flight replaces any earlier
2073
- * waiting one and goes out when the current request settles, so the server
2074
- * ends on the last intent and never sees two updates racing each other.
2075
- * While the sync is active the line keeps its optimistic quantity through
2076
- * any backend response (see `updateFromBackendCart`); a rejection drops
2077
- * what was queued and reconciles the cart from the server. */
2078
- syncQuantity(sponsorId, backendCartId, cartItemId, target) {
2653
+ * line. Targets that arrive while one is in flight queue behind it as
2654
+ * steps (`queueStep`): reduced like the line's origins, sent in order once
2655
+ * the current request settles — one request per queued increase from a
2656
+ * different origin, one for a net decrease — so the server ends on the
2657
+ * last intent, credits every increase to its own origin, and never sees two
2658
+ * updates racing each other. While the sync is active the line keeps its
2659
+ * optimistic quantity through any backend response (see
2660
+ * `updateFromBackendCart`); a rejection drops what was queued and
2661
+ * reconciles the cart from the server. */
2662
+ syncQuantity(sponsorId, backendCartId, cartItemId, target, origin) {
2079
2663
  const key = this.qtySyncKey(sponsorId, cartItemId);
2080
2664
  const active = this.qtySyncs.get(key);
2081
2665
  if (active) {
2082
- active.pending = target;
2666
+ queueStep(active, target, origin);
2083
2667
  return;
2084
2668
  }
2085
- const sync = { sent: target };
2669
+ const sync = { sent: target, steps: [] };
2086
2670
  this.qtySyncs.set(key, sync);
2087
2671
  let settling;
2088
2672
  const run = (async () => {
2089
2673
  let next = target;
2674
+ let nextOrigin = origin;
2090
2675
  let removalRetried = false;
2091
2676
  while (next !== void 0) {
2092
- sync.sent = next;
2093
- sync.pending = void 0;
2094
2677
  const seq = this.nextSeq(sponsorId);
2095
2678
  try {
2096
2679
  const gqlOpts = await getCartGraphQLOptions(sponsorId);
2097
- const resCart = next <= 0 ? await deleteItem(backendCartId, cartItemId, gqlOpts) : await updateItem(backendCartId, cartItemId, null, next, gqlOpts);
2098
- next = sync.pending;
2099
- if (next === sync.sent) next = void 0;
2680
+ const originWire = next > 0 && nextOrigin ? lineOriginWire(nextOrigin, { page: currentOriginPage(), privacyMode: await currentPrivacyMode() }) : void 0;
2681
+ const resCart = next <= 0 ? await deleteItem(backendCartId, cartItemId, gqlOpts) : originWire ? await updateItem(backendCartId, cartItemId, null, next, gqlOpts, { origin: originWire }) : await updateItem(backendCartId, cartItemId, null, next, gqlOpts);
2682
+ const step = sync.steps.shift();
2683
+ if (step) {
2684
+ next = sync.sent + step.delta;
2685
+ nextOrigin = step.delta > 0 ? step.origin : void 0;
2686
+ sync.sent = next;
2687
+ } else {
2688
+ next = void 0;
2689
+ nextOrigin = void 0;
2690
+ }
2100
2691
  if (next === void 0) this.qtySyncs.delete(key);
2101
2692
  if (resCart && Array.isArray(resCart.line_items)) {
2102
2693
  const applied = this.applyBackendCart(sponsorId, resCart, seq, settling);
@@ -2105,12 +2696,13 @@ var CartManager = class extends EventTarget {
2105
2696
  }
2106
2697
  }
2107
2698
  } catch (err) {
2108
- const retryRemoval = !removalRetried && sync.pending !== void 0 && sync.pending <= 0;
2699
+ const retryRemoval = !removalRetried && sync.steps.length > 0 && finalTarget(sync) <= 0;
2700
+ sync.steps = [];
2109
2701
  next = void 0;
2702
+ nextOrigin = void 0;
2110
2703
  if (retryRemoval) {
2111
2704
  removalRetried = true;
2112
2705
  sync.sent = 0;
2113
- sync.pending = void 0;
2114
2706
  next = 0;
2115
2707
  } else {
2116
2708
  this.qtySyncs.delete(key);
@@ -2247,10 +2839,17 @@ function normalizeOrigin(origin) {
2247
2839
  if (origin.contentId !== void 0 && origin.contentId !== null && String(origin.contentId).trim()) {
2248
2840
  out.contentId = String(origin.contentId).trim();
2249
2841
  }
2842
+ const kind = componentKindOf(origin.componentKind ?? origin.componentTemplateId);
2843
+ if (kind) out.componentKind = kind;
2844
+ const contentUrl = originAndPath(origin.contentUrl);
2845
+ if (contentUrl) out.contentUrl = contentUrl;
2846
+ if (typeof origin.contentTitle === "string" && origin.contentTitle.trim()) {
2847
+ out.contentTitle = origin.contentTitle.trim().slice(0, CONTENT_TITLE_MAX_LENGTH);
2848
+ }
2250
2849
  return out;
2251
2850
  }
2252
2851
  function sameOrigin(a, b) {
2253
- return a.campaignId === b.campaignId && a.slot === b.slot && a.contentId === b.contentId;
2852
+ return a.campaignId === b.campaignId && a.slot === b.slot && a.contentId === b.contentId && a.componentKind === b.componentKind;
2254
2853
  }
2255
2854
  function latestOrigin(line) {
2256
2855
  const origins = Array.isArray(line.origins) ? line.origins : [];
@@ -2514,7 +3113,7 @@ async function prepareApplePay(config) {
2514
3113
  }
2515
3114
 
2516
3115
  // src/core/version.ts
2517
- var SDK_VERSION = "0.23.1";
3116
+ var SDK_VERSION = "0.25.0";
2518
3117
 
2519
3118
  // src/core/checkout/payments/stripe-embedded.ts
2520
3119
  var CREATE_PAYMENT_INTENT_STRIPE_MUTATION = `
@@ -3822,13 +4421,14 @@ mutation CreatePaymentKlarnaNative($checkoutId: String!, $countryCode: String, $
3822
4421
  }
3823
4422
  }
3824
4423
  `;
3825
- var CONFIRM_PAYMENT_KLARNA_NATIVE_MUTATION = `
4424
+ var confirmPaymentKlarnaNativeMutation = (withRef) => `
3826
4425
  mutation ConfirmPaymentKlarnaNative($checkoutId: String!, $authorizationToken: String!, $autoCapture: Boolean, $billingAddress: KlarnaNativeAddressInput, $shippingAddress: KlarnaNativeAddressInput, $customer: KlarnaNativeCustomerInput) {
3827
4426
  Payment {
3828
4427
  ConfirmPaymentKlarnaNative(checkout_id: $checkoutId, authorization_token: $authorizationToken, auto_capture: $autoCapture, billing_address: $billingAddress, shipping_address: $shippingAddress, customer: $customer) {
3829
4428
  order_id
3830
4429
  checkout_id
3831
- fraud_status
4430
+ fraud_status${withRef ? `
4431
+ ${BUYER_REF_FIELD}` : ""}
3832
4432
  order {
3833
4433
  order_id
3834
4434
  status
@@ -4010,9 +4610,10 @@ async function executeKlarnaGraphQL(query, variables, sponsorId) {
4010
4610
  return executeCartGraphQL(query, variables, await getCartGraphQLOptions(sponsorId));
4011
4611
  }
4012
4612
  var CheckoutManager = class _CheckoutManager extends EventTarget {
4013
- constructor(cartManager) {
4613
+ constructor(cartManager, deps = {}) {
4014
4614
  super();
4015
4615
  this.cartManager = cartManager;
4616
+ this.deps = deps;
4016
4617
  this.state = null;
4017
4618
  /** Counts `open()` calls — see CheckoutState.session. */
4018
4619
  this.sessionSeq = 0;
@@ -4052,6 +4653,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4052
4653
  this.paymentMethodsCache = /* @__PURE__ */ new Map();
4053
4654
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
4054
4655
  this.paymentMethodsPromises = /* @__PURE__ */ new Map();
4656
+ /** In-flight `getOrderReference` per checkout id (see there). */
4657
+ this.referenceInFlight = /* @__PURE__ */ new Map();
4055
4658
  this.kustomController = null;
4056
4659
  preloadStripeJs();
4057
4660
  }
@@ -4079,7 +4682,20 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4079
4682
  }
4080
4683
  // MARK: - Backend checkout (Vio Commerce GraphQL)
4081
4684
  /* eslint-disable @typescript-eslint/no-explicit-any */
4082
- /** Create the backend checkout for the sponsor's cart (auto-accepts terms). */
4685
+ /**
4686
+ * The attribution of the order this sponsor's cart is about to become
4687
+ * (ADR-0035 phase B), through the facade; `undefined` when there is none
4688
+ * to give or it fails — attribution never costs a checkout.
4689
+ */
4690
+ async attributionFor(sponsorId) {
4691
+ if (!sponsorId || typeof this.deps.orderAttribution !== "function") return void 0;
4692
+ try {
4693
+ return await this.deps.orderAttribution(sponsorId) ?? void 0;
4694
+ } catch {
4695
+ return void 0;
4696
+ }
4697
+ }
4698
+ /** Create the backend checkout for the sponsor's cart (auto-accepts terms); born attributed when the facade knows the attribution. */
4083
4699
  async createCheckout(sponsorId) {
4084
4700
  await this.cartManager.waitForCartMutations(sponsorId);
4085
4701
  const cart = this.cartManager.getCart(sponsorId);
@@ -4091,7 +4707,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4091
4707
  }
4092
4708
  }
4093
4709
  const opts = await getCartGraphQLOptions(sponsorId);
4094
- const checkout = await createCheckout(cartId, opts);
4710
+ const attribution = await this.attributionFor(sponsorId);
4711
+ const checkout = attribution ? await createCheckout(cartId, opts, { attribution }) : await createCheckout(cartId, opts);
4095
4712
  if (checkout?.id) {
4096
4713
  this.state = {
4097
4714
  ...this.state ?? { sponsorId, subtotal: 0, currency: getGlobalCurrency() },
@@ -4122,12 +4739,15 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4122
4739
  }
4123
4740
  return checkout;
4124
4741
  }
4125
- /** Update the backend checkout (terms are re-asserted unless overridden). */
4742
+ /** Update the backend checkout (terms are re-asserted unless overridden; the attribution rides along unless the caller gives its own). */
4126
4743
  async updateCheckout(variables, sponsorId) {
4127
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
4744
+ const spId = sponsorId ?? this.state?.sponsorId;
4745
+ const opts = await getCartGraphQLOptions(spId);
4746
+ const attribution = "attribution" in variables ? void 0 : await this.attributionFor(spId);
4128
4747
  const fullVars = {
4129
4748
  buyerAcceptsPurchaseConditions: true,
4130
4749
  buyerAcceptsTermsConditions: true,
4750
+ ...attribution ? { attribution } : {},
4131
4751
  ...variables
4132
4752
  };
4133
4753
  const updated = await updateCheckout(fullVars, opts);
@@ -4147,13 +4767,30 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4147
4767
  * receipt, and a receipt does not fail for it.
4148
4768
  */
4149
4769
  async getOrderNumber(checkoutId, sponsorId) {
4150
- if (!checkoutId) return null;
4151
- try {
4152
- const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
4153
- return await getCheckoutOrderNumber(checkoutId, opts);
4154
- } catch {
4155
- return null;
4156
- }
4770
+ return (await this.getOrderReference(checkoutId, sponsorId)).orderNumber;
4771
+ }
4772
+ /**
4773
+ * What a paid checkout says of itself: the order number (as above) and the
4774
+ * buyer reference — shopcart's keyed hash of the buyer's email, what the
4775
+ * `vio:payment-success` detail carries as `buyerRef` and analytics sends as
4776
+ * `buyer_id`. Never throws; `null` for whatever the API does not say.
4777
+ */
4778
+ async getOrderReference(checkoutId, sponsorId) {
4779
+ if (!checkoutId) return { orderNumber: null, buyerRef: null };
4780
+ const inFlight = this.referenceInFlight.get(checkoutId);
4781
+ if (inFlight) return inFlight;
4782
+ const request = (async () => {
4783
+ try {
4784
+ const opts = await getCartGraphQLOptions(sponsorId ?? this.state?.sponsorId);
4785
+ return await getCheckoutReference(checkoutId, opts);
4786
+ } catch {
4787
+ return { orderNumber: null, buyerRef: null };
4788
+ } finally {
4789
+ this.referenceInFlight.delete(checkoutId);
4790
+ }
4791
+ })();
4792
+ this.referenceInFlight.set(checkoutId, request);
4793
+ return request;
4157
4794
  }
4158
4795
  /**
4159
4796
  * Payment methods enabled for the sponsor (backend-configured). Memoised per
@@ -4284,9 +4921,10 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4284
4921
  throw new Error(`[CheckoutManager] Failed to obtain cart_id for sponsor ${spId}`);
4285
4922
  }
4286
4923
  const opts = await getCartGraphQLOptions(spId);
4924
+ const attribution = await this.attributionFor(spId);
4287
4925
  let checkoutId = this.state?.checkoutId;
4288
4926
  if (!checkoutId) {
4289
- const checkoutRes = await createCheckout(cartId, opts);
4927
+ const checkoutRes = attribution ? await createCheckout(cartId, opts, { attribution }) : await createCheckout(cartId, opts);
4290
4928
  checkoutId = checkoutRes?.id;
4291
4929
  }
4292
4930
  if (!checkoutId) {
@@ -4305,6 +4943,7 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
4305
4943
  ...hasEmail ? { email: emailVal } : {},
4306
4944
  buyerAcceptsPurchaseConditions: true,
4307
4945
  buyerAcceptsTermsConditions: true,
4946
+ ...attribution ? { attribution } : {},
4308
4947
  ...extraUpdateVars
4309
4948
  };
4310
4949
  if (addressObj) {
@@ -5300,7 +5939,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5300
5939
  `[VioCheckout] Apple Pay confirmation failed (status: ${confirmRes?.status ?? "none"})`
5301
5940
  );
5302
5941
  }
5303
- return { orderId: confirmRes?.order_id, status: confirmRes?.status };
5942
+ const buyerRef = normalizeBuyerRef(confirmRes?.[BUYER_REF_FIELD]);
5943
+ return { orderId: confirmRes?.order_id, status: confirmRes?.status, ...buyerRef ? { buyerRef } : {} };
5304
5944
  };
5305
5945
  }
5306
5946
  static {
@@ -5803,16 +6443,19 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5803
6443
  shippingAddress: buildKlarnaAddress(address),
5804
6444
  customer: buildKlarnaCustomer(address)
5805
6445
  };
5806
- const json = await executeKlarnaGraphQL(
5807
- CONFIRM_PAYMENT_KLARNA_NATIVE_MUTATION,
5808
- variables,
5809
- ctx?.sponsorId ?? this.state?.sponsorId
6446
+ const json = await withBuyerRef(
6447
+ (withRef) => executeKlarnaGraphQL(
6448
+ confirmPaymentKlarnaNativeMutation(withRef),
6449
+ variables,
6450
+ ctx?.sponsorId ?? this.state?.sponsorId
6451
+ )
5810
6452
  );
5811
6453
  const confirmRes = json?.data?.Payment?.ConfirmPaymentKlarnaNative;
5812
6454
  if (!confirmRes) {
5813
6455
  throw new Error("Klarna order confirmation failed: missing response data");
5814
6456
  }
5815
6457
  const orderData = confirmRes.order || confirmRes;
6458
+ const buyerRef = normalizeBuyerRef(confirmRes[BUYER_REF_FIELD]);
5816
6459
  if (typeof sessionStorage !== "undefined") {
5817
6460
  sessionStorage.setItem("vio.klarna.confirmed.v1", authorizationToken);
5818
6461
  sessionStorage.removeItem(KLARNA_PENDING_KEY);
@@ -5830,7 +6473,8 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5830
6473
  result: {
5831
6474
  ...result,
5832
6475
  order: orderData,
5833
- chargedTotal: typeof orderData?.order_amount === "number" ? orderData.order_amount / 100 : ctx.orderAmount / 100
6476
+ chargedTotal: typeof orderData?.order_amount === "number" ? orderData.order_amount / 100 : ctx.orderAmount / 100,
6477
+ ...buyerRef ? { buyerRef } : {}
5834
6478
  }
5835
6479
  }
5836
6480
  })
@@ -5883,220 +6527,177 @@ var CheckoutManager = class _CheckoutManager extends EventTarget {
5883
6527
  }
5884
6528
  };
5885
6529
  function toSmallestUnit(amount, currency) {
5886
- const zeroDecimal = /* @__PURE__ */ new Set(["JPY", "KRW", "VND"]);
5887
- if (zeroDecimal.has(currency.toUpperCase())) return Math.round(amount);
5888
- return Math.round(amount * 100);
5889
- }
5890
-
5891
- // src/core/content.ts
5892
- var CONTENT_ID_QUERY = "vio_content";
5893
- var CONTENT_TITLE_QUERY = "vio_content_title";
5894
- var CONTENT_ID_MAX_LENGTH = 128;
5895
- var CONTENT_TITLE_MAX_LENGTH = 255;
5896
- function resolveContent(init, doc = typeof document !== "undefined" ? document : null, loc = typeof location !== "undefined" ? location : null) {
5897
- try {
5898
- const query = parseQuery(loc?.search);
5899
- const id = fits(text(init?.contentId)) ?? fits(text(query.get(CONTENT_ID_QUERY))) ?? fits(text(meta(doc, "name", "vio:content-id"))) ?? fits(stripQueryAndHash(text(attr(doc, 'link[rel="canonical"]', "href")))) ?? fits(stripQueryAndHash(text(meta(doc, "property", "og:url")))) ?? fits(text(loc?.pathname));
5900
- const title = text(init?.contentTitle) ?? text(query.get(CONTENT_TITLE_QUERY)) ?? text(meta(doc, "name", "vio:content-title")) ?? text(meta(doc, "property", "og:title")) ?? text(doc?.title);
5901
- const out = {};
5902
- if (id !== void 0) out.id = id;
5903
- if (title !== void 0) out.title = title.slice(0, CONTENT_TITLE_MAX_LENGTH);
5904
- return out;
5905
- } catch {
5906
- return {};
5907
- }
5908
- }
5909
- function fits(value) {
5910
- return value !== void 0 && value.length <= CONTENT_ID_MAX_LENGTH ? value : void 0;
5911
- }
5912
- function text(value) {
5913
- if (value === void 0 || value === null) return void 0;
5914
- const s = String(value).trim();
5915
- return s ? s : void 0;
5916
- }
5917
- function parseQuery(search) {
5918
- try {
5919
- return new URLSearchParams(typeof search === "string" ? search : "");
5920
- } catch {
5921
- return new URLSearchParams();
5922
- }
5923
- }
5924
- function meta(doc, key, value) {
5925
- return attr(doc, `meta[${key}="${value}"]`, "content");
5926
- }
5927
- function attr(doc, selector, name) {
5928
- try {
5929
- if (!doc || typeof doc.querySelector !== "function") return void 0;
5930
- const el = doc.querySelector(selector);
5931
- const v = el?.getAttribute?.(name);
5932
- return typeof v === "string" ? v : void 0;
5933
- } catch {
5934
- return void 0;
5935
- }
5936
- }
5937
- function stripQueryAndHash(url) {
5938
- if (!url) return void 0;
5939
- const cut = url.search(/[?#]/);
5940
- const clean = cut >= 0 ? url.slice(0, cut) : url;
5941
- return clean.trim() || void 0;
5942
- }
5943
-
5944
- // src/core/slots.ts
5945
- var SLOT_ID_MAX_LENGTH = 100;
5946
- var SLOT_DECLARATION_TTL_MS = 24 * 60 * 60 * 1e3;
5947
- var SLOT_DECLARATION_FAILURE_TTL_MS = 60 * 60 * 1e3;
5948
- var MAX_LOCATIONS_PER_DECLARATION = 100;
5949
- var MAX_DECLARATIONS_PER_LOAD = 3;
5950
- function resolveSlotCampaign(boot, slot, now = Date.now(), platform) {
5951
- if (!boot) return null;
5952
- return findSlotAssignment(boot, slot, now, platform) ?? defaultCampaignEntry(boot);
5953
- }
5954
- function findSlotAssignment(boot, slot, now = Date.now(), platform) {
5955
- const name = typeof slot === "string" ? slot.trim() : "";
5956
- if (!boot || !name) return null;
5957
- try {
5958
- const matches = activeCampaigns(boot).filter(
5959
- (c) => slotsOf(c).some((s) => assignmentMatches(s, name, now, platform))
5960
- );
5961
- if (matches.length === 0) return null;
5962
- const defaultId = toNumber(boot.campaign?.id);
5963
- return matches.find((c) => toNumber(c.id) === defaultId) ?? matches[0] ?? null;
5964
- } catch {
5965
- return null;
5966
- }
5967
- }
5968
- function defaultCampaignEntry(boot) {
5969
- if (!boot?.campaign || typeof boot.campaign !== "object") return null;
5970
- const defaultId = toNumber(boot.campaign.id);
5971
- if (defaultId === void 0) return null;
5972
- try {
5973
- const listed = wellFormedCampaigns(boot).find((c) => toNumber(c.id) === defaultId);
5974
- if (listed) return listed;
5975
- } catch {
5976
- }
6530
+ const zeroDecimal = /* @__PURE__ */ new Set(["JPY", "KRW", "VND"]);
6531
+ if (zeroDecimal.has(currency.toUpperCase())) return Math.round(amount);
6532
+ return Math.round(amount * 100);
6533
+ }
6534
+
6535
+ // src/core/analytics/client-context.ts
6536
+ var UTM_KEYS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
6537
+ var CLIENT_LANGUAGE_MAX_LENGTH = 16;
6538
+ var CLIENT_REFERRER_MAX_LENGTH = 2048;
6539
+ var CLIENT_UTM_MAX_LENGTH = 128;
6540
+ var CLIENT_TZ_MAX_LENGTH = 64;
6541
+ var CLIENT_SCREEN_MAX = 65535;
6542
+ var CLIENT_LANDING_URL_MAX_LENGTH = 2048;
6543
+ var LANDING_KEY = "vio.landing.v1";
6544
+ function defaultClientEnvironment() {
5977
6545
  return {
5978
- ...boot.campaign,
5979
- isDefault: true,
5980
- primarySponsor: boot.primarySponsor ?? null,
5981
- secondarySponsors: Array.isArray(boot.secondarySponsors) ? boot.secondarySponsors : [],
5982
- slots: []
6546
+ navigator: typeof navigator !== "undefined" ? navigator : null,
6547
+ document: typeof document !== "undefined" ? document : null,
6548
+ location: typeof location !== "undefined" ? location : null,
6549
+ screen: typeof screen !== "undefined" ? screen : null,
6550
+ storage: sessionStorageOrNull(),
6551
+ timeZone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
5983
6552
  };
5984
6553
  }
5985
- function wellFormedCampaigns(boot) {
5986
- const list = Array.isArray(boot.campaigns) ? boot.campaigns : [];
5987
- return list.filter((c) => !!c && typeof c === "object");
6554
+ function resolveLanding(env = defaultClientEnvironment(), persist = true) {
6555
+ try {
6556
+ const utm = utmOf(env.location?.search);
6557
+ const raw = text2(env.document?.referrer);
6558
+ const internal = isSameHost(raw, env.location?.host);
6559
+ const stored = readLanding(env.storage);
6560
+ if ((utm === void 0 || internal) && stored) return stored;
6561
+ const landing = {};
6562
+ const referrer = internal ? void 0 : landingReferrer(raw);
6563
+ if (referrer) landing.referrer = referrer;
6564
+ if (utm) landing.utm = utm;
6565
+ const url = landingUrl(env.location?.href);
6566
+ if (url) landing.url = url;
6567
+ if (persist) writeLanding(env.storage, landing);
6568
+ return landing;
6569
+ } catch {
6570
+ return {};
6571
+ }
5988
6572
  }
5989
- function activeCampaigns(boot) {
5990
- return wellFormedCampaigns(boot).filter((c) => c.status === "active");
6573
+ function landingReferrer(raw) {
6574
+ return originAndPath(raw, CLIENT_REFERRER_MAX_LENGTH);
5991
6575
  }
5992
- function slotsOf(campaign) {
5993
- const slots = Array.isArray(campaign.slots) ? campaign.slots : [];
5994
- return slots.filter((s) => !!s && typeof s === "object");
6576
+ function landingUrl(raw) {
6577
+ return originAndPath(raw, CLIENT_LANDING_URL_MAX_LENGTH);
5995
6578
  }
5996
- function assignmentMatches(s, name, now, platform) {
5997
- if (typeof s.name !== "string" || s.name.trim() !== name) return false;
5998
- if (platform && s.platform != null && s.platform !== platform) return false;
5999
- const start = parseTime(s.startDate);
6000
- if (start !== void 0 && start > now) return false;
6001
- const end = parseTime(s.endDate);
6002
- if (end !== void 0 && now >= end) return false;
6003
- return true;
6579
+ function storedLanding(storage2 = defaultClientEnvironment().storage) {
6580
+ return readLanding(storage2);
6004
6581
  }
6005
- function parseTime(value) {
6006
- if (value == null || value === "") return void 0;
6007
- const t = Date.parse(String(value));
6008
- return Number.isFinite(t) ? t : void 0;
6582
+ function isSameHost(referrer, host) {
6583
+ const value = text2(referrer);
6584
+ const own = text2(host);
6585
+ if (!value || !own) return false;
6586
+ try {
6587
+ return new URL(value).host.toLowerCase() === own.toLowerCase();
6588
+ } catch {
6589
+ return false;
6590
+ }
6009
6591
  }
6010
- function toNumber(value) {
6011
- if (value === void 0 || value === null || value === "") return void 0;
6012
- const n = Number(value);
6013
- return Number.isFinite(n) ? n : void 0;
6592
+ function persistLanding(landing, storage2 = defaultClientEnvironment().storage) {
6593
+ writeLanding(storage2, landing);
6014
6594
  }
6015
- function normalizeSlotId(value) {
6016
- if (typeof value !== "string") return "";
6017
- return value.trim().slice(0, SLOT_ID_MAX_LENGTH).trim();
6595
+ function removeLanding(storage2 = defaultClientEnvironment().storage) {
6596
+ try {
6597
+ storage2?.removeItem?.(LANDING_KEY);
6598
+ } catch {
6599
+ }
6018
6600
  }
6019
- function normalizeSlotLocations(slots) {
6020
- const out = [];
6021
- const seen = /* @__PURE__ */ new Set();
6022
- if (!Array.isArray(slots)) return out;
6023
- for (const raw of slots) {
6024
- const id = normalizeSlotId(typeof raw === "string" ? raw : raw?.id);
6025
- if (!id || seen.has(id)) continue;
6026
- seen.add(id);
6027
- const displayName = raw && typeof raw === "object" && typeof raw.displayName === "string" ? raw.displayName.trim() : "";
6028
- out.push(displayName ? { id, displayName } : { id });
6601
+ function resolveClientContext(env = defaultClientEnvironment(), landing = resolveLanding(env)) {
6602
+ const out = {};
6603
+ try {
6604
+ const language = cut2(text2(env.navigator?.language), CLIENT_LANGUAGE_MAX_LENGTH);
6605
+ if (language) out.language = language;
6606
+ const referrer = cut2(landing.referrer, CLIENT_REFERRER_MAX_LENGTH);
6607
+ if (referrer) out.referrer = referrer;
6608
+ for (const key of UTM_KEYS) {
6609
+ const value = cut2(landing.utm?.[key], CLIENT_UTM_MAX_LENGTH);
6610
+ if (value) out[key] = value;
6611
+ }
6612
+ const w = dimension(env.screen?.width);
6613
+ const h = dimension(env.screen?.height);
6614
+ if (w !== void 0) out.screen_w = w;
6615
+ if (h !== void 0) out.screen_h = h;
6616
+ const tz = cut2(text2(safeCall(env.timeZone)), CLIENT_TZ_MAX_LENGTH);
6617
+ if (tz) out.tz = tz;
6618
+ } catch {
6029
6619
  }
6030
6620
  return out;
6031
6621
  }
6032
- function autoSlotId(kind, ordinal, scope) {
6033
- const tail = `${kind}-${ordinal + 1}`;
6034
- let prefix = normalizeScope(scope);
6035
- const room = SLOT_ID_MAX_LENGTH - tail.length - 1;
6036
- if (prefix.length > room) prefix = prefix.slice(0, Math.max(0, room)).replace(/-+$/, "");
6037
- return `${prefix ? `${prefix}-` : ""}${tail}`;
6038
- }
6039
- function autoSlotDisplayName(kind, ordinal, scopeName) {
6040
- const label = `${kind.charAt(0).toUpperCase()}${kind.slice(1)} ${ordinal + 1}`;
6041
- const name = typeof scopeName === "string" ? scopeName.trim() : "";
6042
- return name ? `${label} \xB7 ${name}` : label;
6043
- }
6044
- function normalizeScope(scope) {
6045
- if (typeof scope !== "string") return "";
6046
- return scope.trim().replace(/\s+/g, "-");
6047
- }
6048
- var SLOT_DECLARATION_MEMORY_CAP = 500;
6049
- function slotDeclarationKey(location2) {
6050
- return location2.displayName ? `${location2.id}${location2.displayName}` : location2.id;
6051
- }
6052
- function pendingSlotKeys(memory, keys, now = Date.now(), ttl = SLOT_DECLARATION_TTL_MS) {
6053
- return keys.filter((key) => {
6054
- const at = memory?.[key];
6055
- if (typeof at !== "number" || !Number.isFinite(at)) return true;
6056
- return now - at >= ttl || now < at;
6057
- });
6058
- }
6059
- function rememberSlotKeys(memory, keys, at, cap = SLOT_DECLARATION_MEMORY_CAP) {
6060
- const next = { ...memory ?? {} };
6061
- for (const key of keys) next[key] = at;
6062
- const entries = Object.entries(next);
6063
- if (entries.length <= cap) return next;
6064
- entries.sort((a, b) => b[1] - a[1]);
6065
- return Object.fromEntries(entries.slice(0, cap));
6066
- }
6067
- function slotDeclarationStorageKey(platform, apiKey) {
6068
- return `vio.slots.${platform}.${fnv1a(apiKey)}`;
6069
- }
6070
- function fnv1a(input) {
6071
- let hash = 2166136261;
6072
- for (let i = 0; i < input.length; i++) {
6073
- hash ^= input.charCodeAt(i);
6074
- hash = Math.imul(hash, 16777619) >>> 0;
6622
+ function utmOf(search) {
6623
+ let params;
6624
+ try {
6625
+ params = new URLSearchParams(typeof search === "string" ? search : "");
6626
+ } catch {
6627
+ return void 0;
6075
6628
  }
6076
- return hash.toString(16).padStart(8, "0");
6629
+ const out = {};
6630
+ let any = false;
6631
+ for (const key of UTM_KEYS) {
6632
+ const value = cut2(text2(params.get(key)), CLIENT_UTM_MAX_LENGTH);
6633
+ if (value) {
6634
+ out[key] = value;
6635
+ any = true;
6636
+ }
6637
+ }
6638
+ return any ? out : void 0;
6077
6639
  }
6078
- function readSlotDeclaration(storageKey) {
6640
+ function readLanding(storage2) {
6079
6641
  try {
6080
- if (typeof localStorage === "undefined") return {};
6081
- const raw = localStorage.getItem(storageKey);
6082
- if (!raw) return {};
6642
+ const raw = storage2?.getItem(LANDING_KEY);
6643
+ if (!raw) return null;
6083
6644
  const parsed = JSON.parse(raw);
6084
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
6085
- if (Array.isArray(parsed.keys)) return {};
6086
- const out = {};
6087
- for (const [key, at] of Object.entries(parsed)) {
6088
- if (typeof at === "number" && Number.isFinite(at)) out[key] = at;
6645
+ if (!parsed || typeof parsed !== "object") return null;
6646
+ const p = parsed;
6647
+ const landing = {};
6648
+ const referrer = cut2(text2(p.referrer), CLIENT_REFERRER_MAX_LENGTH);
6649
+ if (referrer) landing.referrer = referrer;
6650
+ const url = cut2(text2(p.url), CLIENT_LANDING_URL_MAX_LENGTH);
6651
+ if (url) landing.url = url;
6652
+ if (p.utm && typeof p.utm === "object") {
6653
+ const utm = {};
6654
+ let any = false;
6655
+ for (const key of UTM_KEYS) {
6656
+ const value = cut2(text2(p.utm[key]), CLIENT_UTM_MAX_LENGTH);
6657
+ if (value) {
6658
+ utm[key] = value;
6659
+ any = true;
6660
+ }
6661
+ }
6662
+ if (any) landing.utm = utm;
6089
6663
  }
6090
- return out;
6664
+ return landing;
6091
6665
  } catch {
6092
- return {};
6666
+ return null;
6093
6667
  }
6094
6668
  }
6095
- function writeSlotDeclaration(storageKey, memory) {
6669
+ function writeLanding(storage2, landing) {
6096
6670
  try {
6097
- if (typeof localStorage === "undefined") return;
6098
- localStorage.setItem(storageKey, JSON.stringify(memory));
6671
+ storage2?.setItem(LANDING_KEY, JSON.stringify(landing));
6672
+ } catch {
6673
+ }
6674
+ }
6675
+ function sessionStorageOrNull() {
6676
+ try {
6677
+ return typeof sessionStorage !== "undefined" && sessionStorage ? sessionStorage : null;
6678
+ } catch {
6679
+ return null;
6680
+ }
6681
+ }
6682
+ function text2(value) {
6683
+ if (value === void 0 || value === null) return void 0;
6684
+ const s = String(value).trim();
6685
+ return s ? s : void 0;
6686
+ }
6687
+ function cut2(value, max) {
6688
+ if (value === void 0) return void 0;
6689
+ return value.length > max ? value.slice(0, max) : value;
6690
+ }
6691
+ function dimension(value) {
6692
+ const n = typeof value === "number" ? value : Number(value);
6693
+ if (!Number.isFinite(n) || n < 0) return void 0;
6694
+ return Math.min(Math.floor(n), CLIENT_SCREEN_MAX);
6695
+ }
6696
+ function safeCall(fn) {
6697
+ try {
6698
+ return fn ? fn() : void 0;
6099
6699
  } catch {
6700
+ return void 0;
6100
6701
  }
6101
6702
  }
6102
6703
 
@@ -6198,8 +6799,16 @@ var AnalyticsManager = class extends EventTarget {
6198
6799
  /** Last NON-EMPTY cart snapshot — `purchase` uses it because several flows
6199
6800
  * (Apple Pay) clear the cart BEFORE `vio:payment-success` fires. */
6200
6801
  this.lastCartSnapshot = null;
6802
+ /** The `client` block (language, landing referrer / utm, screen, tz): read once per page load, reused on every event. */
6803
+ this.client = null;
6804
+ /** The landing it was read from — persisted later when consent arrives after the first event. */
6805
+ this.landing = null;
6201
6806
  this.teardown = [];
6202
6807
  }
6808
+ /** The landing this manager resolved on this page load (the first event's), or null before it; read only — for the order's entry context when nothing is stored yet. */
6809
+ get currentLanding() {
6810
+ return this.landing;
6811
+ }
6203
6812
  // MARK: - Public API
6204
6813
  /**
6205
6814
  * Install listeners + begin flushing. Explicit on purpose (no module
@@ -6217,10 +6826,12 @@ var AnalyticsManager = class extends EventTarget {
6217
6826
  if (granted) {
6218
6827
  if (this.anonIdCache) safeStorageSet(ANON_KEY, this.anonIdCache);
6219
6828
  if (this.sessionId) this.persistSession();
6829
+ if (this.landing) persistLanding(this.landing);
6220
6830
  return;
6221
6831
  }
6222
6832
  safeStorageRemove(ANON_KEY);
6223
6833
  safeStorageRemove(SESSION_KEY);
6834
+ removeLanding();
6224
6835
  }
6225
6836
  /** ¿Podemos escribir en el dispositivo? */
6226
6837
  get canPersist() {
@@ -6381,7 +6992,7 @@ var AnalyticsManager = class extends EventTarget {
6381
6992
  // one is usually the tracking tail, not the page. The other host-given
6382
6993
  // fields (broadcastId, componentTemplateId, errorCode, variant, …) are
6383
6994
  // the host's responsibility: the SDK does not touch them.
6384
- content_url: capUrl(ctx.contentUrl ?? (typeof location !== "undefined" ? location.href : void 0)),
6995
+ content_url: capContentUrl(ctx.contentUrl ?? (typeof location !== "undefined" ? location.href : void 0)),
6385
6996
  // The host's own content id — its join key back onto its reporting.
6386
6997
  // Explicit on the event when it fits, else what the SDK resolved for
6387
6998
  // the page (already capped per source), else nothing.
@@ -6409,8 +7020,13 @@ var AnalyticsManager = class extends EventTarget {
6409
7020
  value: com.value,
6410
7021
  currency: com.currency,
6411
7022
  order_id: com.orderId,
6412
- payment_method: com.paymentMethod
7023
+ payment_method: com.paymentMethod,
7024
+ // Only a reference survives — never an email, whatever a host passes —
7025
+ // and only with consent to persist: a visit measured with ephemeral
7026
+ // ids does not carry a stable identifier of a person either.
7027
+ buyer_id: this.canPersist ? normalizeBuyerRef(com.buyerId) : void 0
6413
7028
  }) : void 0;
7029
+ const client = this.clientBlock();
6414
7030
  return {
6415
7031
  event_id: uuid(),
6416
7032
  name,
@@ -6423,9 +7039,22 @@ var AnalyticsManager = class extends EventTarget {
6423
7039
  ...this.externalUserId ? { external_user_id: this.externalUserId } : {},
6424
7040
  ...Object.keys(context).length ? { context } : {},
6425
7041
  ...commerce ? { commerce } : {},
7042
+ ...client ? { client } : {},
6426
7043
  ...opts.props ? { props: opts.props } : {}
6427
7044
  };
6428
7045
  }
7046
+ /**
7047
+ * The `client` block: read once — the landing is remembered for the session
7048
+ * only when we may persist — then the same (frozen) object on every event.
7049
+ * `null` when there is nothing to say (no browser).
7050
+ */
7051
+ clientBlock() {
7052
+ if (!this.client) {
7053
+ this.landing = resolveLanding(void 0, this.canPersist);
7054
+ this.client = Object.freeze(resolveClientContext(void 0, this.landing));
7055
+ }
7056
+ return Object.keys(this.client).length ? this.client : null;
7057
+ }
6429
7058
  /** Guarda la sesión solo si podemos escribir en el dispositivo. */
6430
7059
  persistSession(now = Date.now()) {
6431
7060
  if (!this.canPersist || !this.sessionId) return;
@@ -6539,7 +7168,7 @@ var AnalyticsManager = class extends EventTarget {
6539
7168
  const d = ev.detail ?? {};
6540
7169
  const line = this.findLine(d.productId, d.variantId);
6541
7170
  this.track("add_to_cart", {
6542
- context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, campaignId: campaignOf(d), slot: slotOf(d), ...contentOf(d) },
7171
+ context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, campaignId: campaignOf(d), slot: slotOf(d), componentTemplateId: templateOf(d), ...contentOf(d) },
6543
7172
  commerce: {
6544
7173
  items: [
6545
7174
  {
@@ -6589,7 +7218,7 @@ var AnalyticsManager = class extends EventTarget {
6589
7218
  if (d.productId === void 0) return;
6590
7219
  const price = Number(d.price);
6591
7220
  this.track("select_item", {
6592
- context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, campaignId: campaignOf(d), slot: slotOf(d), ...contentOf(d) },
7221
+ context: { sponsorId: d.sponsorId !== void 0 ? Number(d.sponsorId) : void 0, campaignId: campaignOf(d), slot: slotOf(d), componentTemplateId: templateOf(d), ...contentOf(d) },
6593
7222
  commerce: {
6594
7223
  items: [
6595
7224
  {
@@ -6608,13 +7237,15 @@ var AnalyticsManager = class extends EventTarget {
6608
7237
  });
6609
7238
  on("vio:checkout-open", (ev) => {
6610
7239
  const d = ev.detail ?? {};
6611
- const base = { sponsorId: d.sponsorId, campaignId: campaignOf(d), slot: slotOf(d), ...contentOf(d) };
7240
+ const base = { sponsorId: d.sponsorId, campaignId: campaignOf(d), slot: slotOf(d), componentTemplateId: templateOf(d), ...contentOf(d) };
7241
+ const buyerId = buyerRefOf(d);
7242
+ const buyer = buyerId !== void 0 ? { buyerId } : {};
6612
7243
  const { slices, explicit } = this.cartSlices(d.sponsorId);
6613
7244
  if (slices.length > 1) {
6614
7245
  for (const slice of slices) {
6615
7246
  this.track("begin_checkout", {
6616
7247
  context: this.sliceContext(slice, base),
6617
- commerce: sliceCommerce(slice),
7248
+ commerce: { ...sliceCommerce(slice), ...buyer },
6618
7249
  props: { slice: slice.slice, slices: slice.slices }
6619
7250
  });
6620
7251
  }
@@ -6623,7 +7254,7 @@ var AnalyticsManager = class extends EventTarget {
6623
7254
  const snap = this.cartCommerce(d.sponsorId);
6624
7255
  this.track("begin_checkout", {
6625
7256
  context: this.singleSliceContext(slices[0], explicit, base),
6626
- ...snap ? { commerce: snap } : {}
7257
+ ...snap || buyerId !== void 0 ? { commerce: { ...snap ?? {}, ...buyer } } : {}
6627
7258
  });
6628
7259
  });
6629
7260
  on("vio:payment-success", (ev) => {
@@ -6817,27 +7448,27 @@ function vioCart() {
6817
7448
  return null;
6818
7449
  }
6819
7450
  }
6820
- function resolveCampaignId(ctx, boot) {
7451
+ function resolveCampaignId(ctx, boot, opts = {}) {
6821
7452
  if (ctx.campaignId !== void 0) return ctx.campaignId;
6822
7453
  const defaultId = boot?.campaign?.id;
7454
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
7455
+ const sponsorId = toNumber(ctx.sponsorId);
7456
+ const candidates = [
7457
+ ...Array.isArray(opts.pageCampaignId) ? opts.pageCampaignId : [opts.pageCampaignId],
7458
+ config?.campaignId
7459
+ ];
7460
+ const page = activeCampaignEntry(boot, candidates, sponsorId);
6823
7461
  if (ctx.slot) {
6824
- const platform = Configuration.isInitialized ? Configuration.get().platform : void 0;
6825
- const assigned = toNumber(findSlotAssignment(boot, ctx.slot, Date.now(), platform)?.id);
6826
- if (assigned !== void 0) return assigned;
7462
+ const assignment = findSlotAssignmentDetail(boot, ctx.slot, Date.now(), config?.platform);
7463
+ if (assignment && assignmentApplies(assignment, activeCampaignEntry(boot, candidates), sponsorId)) {
7464
+ const assigned = toNumber(assignment.campaign.id);
7465
+ if (assigned !== void 0) return assigned;
7466
+ }
6827
7467
  }
6828
- const sponsorId = toNumber(ctx.sponsorId);
7468
+ const pageId = toNumber(page?.id);
7469
+ if (pageId !== void 0) return pageId;
6829
7470
  if (sponsorId === void 0) return defaultId;
6830
- try {
6831
- const campaigns = Array.isArray(boot?.campaigns) ? boot.campaigns : [];
6832
- const matches = campaigns.filter(
6833
- (c) => !!c && typeof c === "object" && c.status === "active" && campaignSponsorIds(c).includes(sponsorId)
6834
- );
6835
- const chosen = matches.find((c) => c.id === defaultId) ?? matches[0];
6836
- const chosenId = toNumber(chosen?.id);
6837
- if (chosenId !== void 0) return chosenId;
6838
- } catch {
6839
- }
6840
- return defaultId;
7471
+ return toNumber(sponsorCampaignEntry(boot, sponsorId)?.id) ?? defaultId;
6841
7472
  }
6842
7473
  function campaignOf(detail) {
6843
7474
  return toNumber(detail.campaignId);
@@ -6851,20 +7482,16 @@ function capTitle(value) {
6851
7482
  if (!value) return void 0;
6852
7483
  return value.length > CONTENT_TITLE_MAX_LENGTH ? value.slice(0, CONTENT_TITLE_MAX_LENGTH) : value;
6853
7484
  }
6854
- var CONTENT_URL_MAX_LENGTH = 2048;
6855
- function capUrl(value) {
6856
- if (!value) return void 0;
6857
- if (value.length <= CONTENT_URL_MAX_LENGTH) return value;
6858
- const cut = value.search(/[?#]/);
6859
- const withoutQuery = cut >= 0 ? value.slice(0, cut) : value;
6860
- return withoutQuery.length <= CONTENT_URL_MAX_LENGTH ? withoutQuery : withoutQuery.slice(0, CONTENT_URL_MAX_LENGTH);
6861
- }
6862
7485
  function sliceCommerce(slice) {
6863
7486
  return { items: slice.items, value: slice.value, currency: slice.currency };
6864
7487
  }
6865
7488
  function slotOf(detail) {
6866
7489
  return typeof detail.slot === "string" && detail.slot.trim() ? detail.slot.trim() : void 0;
6867
7490
  }
7491
+ function templateOf(detail) {
7492
+ const value = typeof detail.componentTemplateId === "string" ? detail.componentTemplateId.trim() : "";
7493
+ return fitsId(value || void 0);
7494
+ }
6868
7495
  function contentOf(detail) {
6869
7496
  const out = {};
6870
7497
  if (typeof detail.contentId === "number" && Number.isFinite(detail.contentId)) out.contentId = detail.contentId;
@@ -6872,21 +7499,33 @@ function contentOf(detail) {
6872
7499
  if (typeof detail.contentTitle === "string" && detail.contentTitle.trim()) out.contentTitle = detail.contentTitle.trim();
6873
7500
  return out;
6874
7501
  }
6875
- function campaignSponsorIds(campaign) {
6876
- const ids = [];
6877
- const primary = toNumber(campaign.primarySponsor?.id);
6878
- if (primary !== void 0) ids.push(primary);
6879
- const secondaries = Array.isArray(campaign.secondarySponsors) ? campaign.secondarySponsors : [];
6880
- for (const s of secondaries) {
6881
- const id = toNumber(s?.id);
6882
- if (id !== void 0) ids.push(id);
7502
+ function buyerRefOf(d) {
7503
+ if (!d) return void 0;
7504
+ const r = d.result ?? {};
7505
+ const candidates = [
7506
+ d.buyerRef,
7507
+ d.buyer_ref,
7508
+ r.buyerRef,
7509
+ r.buyer_ref,
7510
+ r.confirmation?.buyerRef,
7511
+ r.confirmation?.buyer_ref,
7512
+ r.checkout?.buyerRef,
7513
+ r.checkout?.buyer_ref
7514
+ ];
7515
+ for (const candidate of candidates) {
7516
+ const ref = normalizeBuyerRef(candidate);
7517
+ if (ref) return ref;
6883
7518
  }
6884
- return ids;
7519
+ return void 0;
6885
7520
  }
6886
7521
  function purchaseFromPaymentSuccess(d) {
6887
7522
  const r = d.result ?? {};
6888
7523
  const orderId = firstId(d.orderId, r.confirmation?.orderId, d.checkoutId);
6889
- return orderId !== void 0 ? { orderId } : {};
7524
+ const buyerId = buyerRefOf(d);
7525
+ return {
7526
+ ...orderId !== void 0 ? { orderId } : {},
7527
+ ...buyerId !== void 0 ? { buyerId } : {}
7528
+ };
6890
7529
  }
6891
7530
  function firstId(...candidates) {
6892
7531
  for (const c of candidates) {
@@ -6931,8 +7570,87 @@ function pushGa4(event) {
6931
7570
  });
6932
7571
  }
6933
7572
 
7573
+ // src/core/checkout/attribution.ts
7574
+ var CHECKOUT_ATTRIBUTION_CAPS = Object.freeze({
7575
+ surfaceKey: 255,
7576
+ contentUrl: 2048,
7577
+ contentTitle: CONTENT_TITLE_MAX_LENGTH,
7578
+ referrer: CLIENT_REFERRER_MAX_LENGTH,
7579
+ utm: CLIENT_UTM_MAX_LENGTH,
7580
+ landingUrl: CLIENT_LANDING_URL_MAX_LENGTH
7581
+ });
7582
+ function checkoutAttributionOf(input) {
7583
+ const out = {};
7584
+ const surfaceKey = cut3(input.surfaceKey, CHECKOUT_ATTRIBUTION_CAPS.surfaceKey);
7585
+ if (surfaceKey) out.surface_key = surfaceKey;
7586
+ const campaignId = positiveInt(input.pageCampaignId) ?? dominantCampaignOf(input.slices);
7587
+ if (campaignId !== void 0) out.campaign_id = campaignId;
7588
+ if (input.privacyMode === "standard") {
7589
+ const url = originAndPath(input.content?.url, CHECKOUT_ATTRIBUTION_CAPS.contentUrl);
7590
+ if (url) out.content_url = url;
7591
+ const title = cut3(input.content?.title, CHECKOUT_ATTRIBUTION_CAPS.contentTitle);
7592
+ if (title) out.content_title = title;
7593
+ const entry = entryContextOf(input.landing);
7594
+ if (entry) out.entry = entry;
7595
+ }
7596
+ return Object.keys(out).length ? out : void 0;
7597
+ }
7598
+ function entryContextOf(landing) {
7599
+ if (!landing || typeof landing !== "object") return void 0;
7600
+ const out = {};
7601
+ const referrer = cut3(landing.referrer, CHECKOUT_ATTRIBUTION_CAPS.referrer);
7602
+ if (referrer) out.referrer = referrer;
7603
+ const source = cut3(landing.utm?.utm_source, CHECKOUT_ATTRIBUTION_CAPS.utm);
7604
+ if (source) out.utm_source = source;
7605
+ const medium = cut3(landing.utm?.utm_medium, CHECKOUT_ATTRIBUTION_CAPS.utm);
7606
+ if (medium) out.utm_medium = medium;
7607
+ const campaign = cut3(landing.utm?.utm_campaign, CHECKOUT_ATTRIBUTION_CAPS.utm);
7608
+ if (campaign) out.utm_campaign = campaign;
7609
+ const landingUrl2 = cut3(landing.url, CHECKOUT_ATTRIBUTION_CAPS.landingUrl);
7610
+ if (landingUrl2) out.landing_url = landingUrl2;
7611
+ return Object.keys(out).length ? out : void 0;
7612
+ }
7613
+ function dominantCampaignOf(slices) {
7614
+ if (!Array.isArray(slices)) return void 0;
7615
+ let best;
7616
+ for (const slice of slices) {
7617
+ const campaignId = positiveInt(slice?.campaignId);
7618
+ if (campaignId === void 0) continue;
7619
+ const items = Array.isArray(slice.items) ? slice.items : [];
7620
+ const units = items.reduce((sum, item) => sum + (Number(item?.quantity) > 0 ? Number(item.quantity) : 0), 0);
7621
+ const value = Number(slice.value) || 0;
7622
+ if (!best || units > best.units || units === best.units && value > best.value) {
7623
+ best = { campaignId, units, value };
7624
+ }
7625
+ }
7626
+ return best?.campaignId;
7627
+ }
7628
+ function positiveInt(value) {
7629
+ if (value === void 0 || value === null || value === "") return void 0;
7630
+ const n = Number(value);
7631
+ return Number.isInteger(n) && n > 0 && n <= GRAPHQL_INT_MAX ? n : void 0;
7632
+ }
7633
+ function cut3(value, max) {
7634
+ if (typeof value !== "string" && typeof value !== "number") return void 0;
7635
+ const text3 = String(value).trim();
7636
+ if (!text3) return void 0;
7637
+ return text3.length > max ? text3.slice(0, max).trim() || void 0 : text3;
7638
+ }
7639
+
6934
7640
  // src/core/client.ts
6935
7641
  var SLOT_DECLARATION_DEBOUNCE_MS = 1e3;
7642
+ var PRIVACY_MODE_TIMEOUT_MS = 1500;
7643
+ var PRIVACY_UNKNOWN_TTL_MS = 6e4;
7644
+ function privacyModeOf(value) {
7645
+ return value === "minimal" || value === "standard" ? value : void 0;
7646
+ }
7647
+ function privacyModeOfSurface(res) {
7648
+ const surface = res?.surface;
7649
+ if (!surface || typeof surface !== "object") return "unknown";
7650
+ const raw = surface.privacyMode;
7651
+ if (raw === void 0 || raw === null) return "standard";
7652
+ return privacyModeOf(raw) ?? "unknown";
7653
+ }
6936
7654
  var VioFacade = class {
6937
7655
  constructor() {
6938
7656
  this.apiInstance = null;
@@ -6946,8 +7664,11 @@ var VioFacade = class {
6946
7664
  * (another key or environment) is still in flight after a re-init; its answer
6947
7665
  * belongs to that configuration and must not land in this one. */
6948
7666
  this.generation = 0;
6949
- /** Slot ids present on the page (`Vio.registerSlot`): how many elements hold each + its display name. */
7667
+ /** Slot ids present on the page (`Vio.registerSlot`): how many elements hold each, its display name, its own campaign and its kind. */
6950
7668
  this.slotRegistry = /* @__PURE__ */ new Map();
7669
+ /** Slots already warned about (`dashboard`: the dashboard's assignment stands; `replaced`: this page moved it). */
7670
+ this.warnedDashboardSlots = /* @__PURE__ */ new Set();
7671
+ this.warnedReplacedSlots = /* @__PURE__ */ new Set();
6951
7672
  /** Pending debounced declaration of the registry. */
6952
7673
  this.slotDeclareTimer = null;
6953
7674
  /** A name appeared (or the key changed) since the registry was last declared. */
@@ -6962,6 +7683,10 @@ var VioFacade = class {
6962
7683
  this.cartManager = null;
6963
7684
  this.checkoutManager = null;
6964
7685
  this.analyticsManager = null;
7686
+ /** What the surface answered of its privacy mode (`GET /v2/mobile/surface`) and when; `unknown` expires after a minute. */
7687
+ this.surfaceMode = null;
7688
+ /** The request in flight for it, if any. */
7689
+ this.surfaceRequest = null;
6965
7690
  }
6966
7691
  init(config) {
6967
7692
  Configuration.init(config);
@@ -6971,6 +7696,9 @@ var VioFacade = class {
6971
7696
  this.cachedBootstrap = null;
6972
7697
  this.bootstrapFailedAt = 0;
6973
7698
  this.generation++;
7699
+ this.surfaceMode = null;
7700
+ this.surfaceRequest = null;
7701
+ this.preloadPrivacyMode();
6974
7702
  this.checkoutManager?.clearPaymentMethodsCache();
6975
7703
  if (this.slotDeclareTimer) {
6976
7704
  clearTimeout(this.slotDeclareTimer);
@@ -6980,6 +7708,8 @@ var VioFacade = class {
6980
7708
  this.declarationsThisLoad = 0;
6981
7709
  this.warnedLocationCap = false;
6982
7710
  this.warnedDeclarationCap = false;
7711
+ this.warnedDashboardSlots.clear();
7712
+ this.warnedReplacedSlots.clear();
6983
7713
  }
6984
7714
  /**
6985
7715
  * The content (article, page) this SDK instance is rendered in — what every
@@ -7010,9 +7740,13 @@ var VioFacade = class {
7010
7740
  if (!this.cartManager) this.cartManager = new CartManager();
7011
7741
  return this.cartManager;
7012
7742
  }
7013
- /** Checkout flow manager. Subscribes to cart state via the CartManager. */
7743
+ /** Checkout flow manager. Subscribes to cart state via the CartManager; asks here for the order's attribution. */
7014
7744
  get checkout() {
7015
- if (!this.checkoutManager) this.checkoutManager = new CheckoutManager(this.cart);
7745
+ if (!this.checkoutManager) {
7746
+ this.checkoutManager = new CheckoutManager(this.cart, {
7747
+ orderAttribution: (sponsorId) => this.orderAttribution(sponsorId)
7748
+ });
7749
+ }
7016
7750
  return this.checkoutManager;
7017
7751
  }
7018
7752
  /**
@@ -7054,6 +7788,7 @@ var VioFacade = class {
7054
7788
  this.cachedBootstrap = res;
7055
7789
  this.bootstrapFailedAt = 0;
7056
7790
  if (this.slotsDirty) this.scheduleSlotDeclaration();
7791
+ this.preloadPrivacyMode();
7057
7792
  return res;
7058
7793
  },
7059
7794
  (err) => {
@@ -7117,9 +7852,14 @@ var VioFacade = class {
7117
7852
  const cfg = Configuration.get();
7118
7853
  storageKey = slotDeclarationStorageKey(cfg.platform, cfg.apiKey);
7119
7854
  now = Date.now();
7855
+ const pageCampaignId = cfg.campaignId;
7856
+ for (const l of all) {
7857
+ if (l.campaignId !== void 0 && l.campaignId === pageCampaignId) delete l.campaignId;
7858
+ }
7120
7859
  const memory = readSlotDeclaration(storageKey);
7121
- const pending2 = new Set(pendingSlotKeys(memory, all.map(slotDeclarationKey), now));
7122
- let locations = opts.force ? all : all.filter((l) => pending2.has(slotDeclarationKey(l)));
7860
+ const keyOf = (l) => slotDeclarationKey(l, pageCampaignId);
7861
+ const pending2 = new Set(pendingSlotKeys(memory, all.map(keyOf), now));
7862
+ let locations = opts.force ? all : all.filter((l) => pending2.has(keyOf(l)));
7123
7863
  if (locations.length === 0) return null;
7124
7864
  if (locations.length > MAX_LOCATIONS_PER_DECLARATION) {
7125
7865
  if (!this.warnedLocationCap) {
@@ -7130,7 +7870,7 @@ var VioFacade = class {
7130
7870
  }
7131
7871
  locations = locations.slice(0, MAX_LOCATIONS_PER_DECLARATION);
7132
7872
  }
7133
- keys = locations.map(slotDeclarationKey);
7873
+ keys = locations.map(keyOf);
7134
7874
  if (!opts.force && this.declarationsThisLoad >= MAX_DECLARATIONS_PER_LOAD) {
7135
7875
  if (!this.warnedDeclarationCap) {
7136
7876
  this.warnedDeclarationCap = true;
@@ -7144,10 +7884,16 @@ var VioFacade = class {
7144
7884
  const result = await this.api.declareManifest({
7145
7885
  platform: cfg.platform,
7146
7886
  mode: "merge",
7147
- locations
7887
+ locations,
7888
+ ...pageCampaignId !== void 0 ? { campaignId: pageCampaignId } : {}
7148
7889
  });
7890
+ this.reportAssignments(result, locations, pageCampaignId, cfg.debug);
7149
7891
  if (result?.mode === "merge") {
7150
- writeSlotDeclaration(storageKey, rememberSlotKeys(memory, keys, now));
7892
+ const unhandled = this.campaignNotTakenUp(result, locations, pageCampaignId);
7893
+ const settled = keys.filter((k) => !unhandled.has(k));
7894
+ let next = rememberSlotKeys(memory, settled, now);
7895
+ if (unhandled.size > 0) next = rememberSlotKeys(next, [...unhandled], failedAt(now));
7896
+ writeSlotDeclaration(storageKey, next);
7151
7897
  } else {
7152
7898
  writeSlotDeclaration(storageKey, rememberSlotKeys(memory, keys, failedAt(now)));
7153
7899
  warn(
@@ -7166,21 +7912,34 @@ var VioFacade = class {
7166
7912
  /**
7167
7913
  * Register a slot present on the page (the `<vio-*>` elements call it with
7168
7914
  * their effective id — the host's `vio-slot` or the auto-generated one —
7169
- * and the optional display name). The registry is declared ONCE, debounced,
7170
- * after the first successful bootstrap — and again whenever a new id or a
7171
- * new display name appears. A `displayName` given later for an id already
7172
- * registered replaces the one kept; `undefined` leaves it alone. The
7173
- * returned function unregisters; removal never re-declares.
7915
+ * the optional display name, and the element's own campaign when it has
7916
+ * one: `vio-campaign` or an ancestor's `data-vio-campaign`). The registry is
7917
+ * declared ONCE, debounced, after the first successful bootstrap — and again
7918
+ * whenever a new id, display name, campaign or kind appears. A `displayName` /
7919
+ * `campaignId` given later for an id already registered replaces the one
7920
+ * kept; `undefined` leaves it alone. The third argument is the campaign id
7921
+ * (as before) or `{ campaignId?, kind? }`, and a fourth `{ kind? }` may
7922
+ * follow a positional campaign — the elements call
7923
+ * `registerSlot(id, name, campaignId, { kind })` so a facade from an older
7924
+ * bundle, which reads only three arguments, still gets the campaign. `kind`
7925
+ * is what kind of element holds the slot (`card` | `carousel` | `detail`);
7926
+ * the FIRST kind an id gets stays (the cards a carousel renders share its
7927
+ * id: the slot is the carousel's). The returned function unregisters;
7928
+ * removal never re-declares.
7174
7929
  */
7175
- registerSlot(name, displayName) {
7930
+ registerSlot(name, displayName, campaignIdOrOptions, options) {
7176
7931
  const id = normalizeSlotId(name);
7177
7932
  if (!id) return () => {
7178
7933
  };
7179
7934
  const label = typeof displayName === "string" && displayName.trim() ? displayName.trim() : void 0;
7935
+ const positional = campaignIdOrOptions !== null && typeof campaignIdOrOptions === "object" ? campaignIdOrOptions : { campaignId: campaignIdOrOptions };
7936
+ const merged = { ...positional, ...options ?? {} };
7937
+ const campaign = toCampaignId(merged.campaignId);
7938
+ const kind = normalizeSlotKind(merged.kind);
7180
7939
  const entry = this.slotRegistry.get(id);
7181
7940
  let changed = false;
7182
7941
  if (!entry) {
7183
- this.slotRegistry.set(id, { count: 1, displayName: label });
7942
+ this.slotRegistry.set(id, { count: 1, displayName: label, campaignId: campaign, kind });
7184
7943
  changed = true;
7185
7944
  } else {
7186
7945
  entry.count++;
@@ -7188,6 +7947,14 @@ var VioFacade = class {
7188
7947
  entry.displayName = label;
7189
7948
  changed = true;
7190
7949
  }
7950
+ if (campaign !== void 0 && campaign !== entry.campaignId) {
7951
+ entry.campaignId = campaign;
7952
+ changed = true;
7953
+ }
7954
+ if (kind !== void 0 && entry.kind === void 0) {
7955
+ entry.kind = kind;
7956
+ changed = true;
7957
+ }
7191
7958
  }
7192
7959
  if (changed) {
7193
7960
  this.slotsDirty = true;
@@ -7208,9 +7975,30 @@ var VioFacade = class {
7208
7975
  * open campaign for the init platform, else the surface's default. With no
7209
7976
  * slot, the default. `null` before the bootstrap. See `resolveSlotCampaign`.
7210
7977
  */
7211
- campaignForSlot(slot) {
7212
- const platform = Configuration.isInitialized ? Configuration.get().platform : void 0;
7213
- return resolveSlotCampaign(this.cachedBootstrap, slot ?? null, Date.now(), platform);
7978
+ campaignForSlot(slot, hint) {
7979
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
7980
+ return resolveSlotCampaign(
7981
+ this.cachedBootstrap,
7982
+ slot ?? null,
7983
+ Date.now(),
7984
+ config?.platform,
7985
+ this.pageCampaignCandidates(hint?.campaignId),
7986
+ toNumber(hint?.sponsorId)
7987
+ );
7988
+ }
7989
+ /** The page-campaign candidates in precedence order: the element's (own, ancestor), then `Vio.init`'s. */
7990
+ pageCampaignCandidates(hint) {
7991
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
7992
+ return campaignCandidates([...Array.isArray(hint) ? hint : [hint], config?.campaignId]);
7993
+ }
7994
+ /**
7995
+ * The campaign the PAGE belongs to (`Vio.init({ campaignId })`, ADR-0034
7996
+ * amendment 3), as the last bootstrap lists it — only when it is there and
7997
+ * active; `null` otherwise (no page campaign, paused, unknown, no bootstrap).
7998
+ */
7999
+ pageCampaign() {
8000
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
8001
+ return activeCampaignEntry(this.cachedBootstrap, config?.campaignId);
7214
8002
  }
7215
8003
  /**
7216
8004
  * THE attribution helper — the SDK elements, Vev and Replit all use this
@@ -7222,8 +8010,12 @@ var VioFacade = class {
7222
8010
  *
7223
8011
  * `hint.sponsorId` — when the caller already knows the sponsor (a card with
7224
8012
  * `sponsor-id`) it is kept as `sponsorId`, and the campaign follows the same
7225
- * cascade analytics uses (slot assignment → the sponsor's active campaign →
7226
- * default), so what the cart remembers matches what the events say.
8013
+ * cascade analytics uses (slot assignment → page campaign → the sponsor's
8014
+ * active campaign → default), so what the cart remembers matches what the
8015
+ * events say — the page campaign applies there only when that sponsor takes
8016
+ * part in it. `hint.campaignId` — the element's own page campaign
8017
+ * candidates (`vio-campaign`, an ancestor's `data-vio-campaign`), ahead of
8018
+ * `Vio.init`'s; an unusable one falls through to the next; `0` = unset.
7227
8019
  */
7228
8020
  attribution(slot, hint) {
7229
8021
  const out = {};
@@ -7232,13 +8024,16 @@ var VioFacade = class {
7232
8024
  if (name) out.slot = name;
7233
8025
  const boot = this.cachedBootstrap;
7234
8026
  const hinted = toNumber(hint?.sponsorId);
8027
+ const pageCampaignId = this.pageCampaignCandidates(hint?.campaignId);
7235
8028
  if (boot) {
7236
8029
  if (hinted !== void 0) {
7237
- const campaignId = resolveCampaignId({ slot: name || void 0, sponsorId: hinted }, boot);
8030
+ const campaignId = resolveCampaignId({ slot: name || void 0, sponsorId: hinted }, boot, {
8031
+ pageCampaignId
8032
+ });
7238
8033
  if (campaignId !== void 0) out.campaignId = campaignId;
7239
8034
  out.sponsorId = hinted;
7240
8035
  } else {
7241
- const campaign = this.campaignForSlot(name || null);
8036
+ const campaign = this.campaignForSlot(name || null, { campaignId: pageCampaignId });
7242
8037
  const campaignId = toNumber(campaign?.id);
7243
8038
  if (campaignId !== void 0) out.campaignId = campaignId;
7244
8039
  const sponsorId = toNumber(campaign?.primarySponsor?.id);
@@ -7250,10 +8045,155 @@ var VioFacade = class {
7250
8045
  const content = this.content();
7251
8046
  if (content.id) out.contentId = content.id;
7252
8047
  if (content.title) out.contentTitle = content.title;
8048
+ if (content.url) out.contentUrl = content.url;
7253
8049
  } catch {
7254
8050
  }
7255
8051
  return out;
7256
8052
  }
8053
+ /**
8054
+ * What the surface answered and is still believed: `standard` / `minimal`
8055
+ * for the configuration; `unknown` for a minute after a timeout or a
8056
+ * failure (then it is asked again). `null` = nothing believed.
8057
+ */
8058
+ believedSurfaceMode() {
8059
+ const answer = this.surfaceMode;
8060
+ if (!answer) return null;
8061
+ if (answer.mode !== "unknown") return answer.mode;
8062
+ return Date.now() - answer.at < PRIVACY_UNKNOWN_TTL_MS ? "unknown" : null;
8063
+ }
8064
+ /**
8065
+ * Ask the surface its privacy mode (`GET /v2/mobile/surface`), once —
8066
+ * started at `init()` and again by `bootstrap()`, never by the payment
8067
+ * path, which only reads what is known (`privacyModeNow`). Resolves what
8068
+ * was learnt within 1.5 s: the mode; `standard` for a service that predates
8069
+ * the endpoint (404) or a surface that names none; `unknown` for a word the
8070
+ * SDK does not know, a timeout or any other failure — believed for a
8071
+ * minute, then asked again. An answer that arrives after the cap still
8072
+ * lands for the next step. Never throws.
8073
+ */
8074
+ preloadPrivacyMode() {
8075
+ try {
8076
+ if (this.surfaceRequest) return this.surfaceRequest;
8077
+ const believed = this.believedSurfaceMode();
8078
+ if (believed) return Promise.resolve(believed);
8079
+ if (!Configuration.isInitialized) return Promise.resolve("unknown");
8080
+ const now = Date.now();
8081
+ if (Configuration.get().privacyMode === "minimal") {
8082
+ this.surfaceMode = { mode: "minimal", at: now };
8083
+ return Promise.resolve("minimal");
8084
+ }
8085
+ const generation = this.generation;
8086
+ const learnt = (mode) => {
8087
+ if (generation !== this.generation) return;
8088
+ if (mode === "unknown" && this.surfaceMode && this.surfaceMode.mode !== "unknown") return;
8089
+ this.surfaceMode = { mode, at: Date.now() };
8090
+ };
8091
+ const asked = this.api.surface().then(
8092
+ (res) => privacyModeOfSurface(res),
8093
+ (err) => err instanceof VioApiError && err.status === 404 ? "standard" : "unknown"
8094
+ );
8095
+ let timer;
8096
+ const capped = new Promise((resolve) => {
8097
+ timer = setTimeout(() => resolve("unknown"), PRIVACY_MODE_TIMEOUT_MS);
8098
+ });
8099
+ const request = Promise.race([asked, capped]).then((mode) => {
8100
+ clearTimeout(timer);
8101
+ learnt(mode);
8102
+ if (this.surfaceRequest === request) this.surfaceRequest = null;
8103
+ return mode;
8104
+ });
8105
+ void asked.then((mode) => {
8106
+ clearTimeout(timer);
8107
+ learnt(mode);
8108
+ });
8109
+ this.surfaceRequest = request;
8110
+ return request;
8111
+ } catch {
8112
+ return Promise.resolve("unknown");
8113
+ }
8114
+ }
8115
+ /**
8116
+ * How much of the visit the order attribution may carry (ADR-0035): the
8117
+ * surface's privacy mode, as known RIGHT NOW, without waiting —
8118
+ * `Vio.init({ privacyMode: 'minimal' })` when the host says so, else what
8119
+ * the bootstrap says of the surface, else what the surface answered to the
8120
+ * ask `init()` started (`preloadPrivacyMode`), else the host's `standard`
8121
+ * once the surface could not tell. `unknown` while nothing is known: then
8122
+ * the page, the slot and the entry stay off what goes to Commerce, because
8123
+ * what can reveal which screen someone was on, and why, is only sent when
8124
+ * the surface is known to allow it. The payment path reads this one.
8125
+ */
8126
+ privacyModeNow() {
8127
+ try {
8128
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
8129
+ if (config?.privacyMode === "minimal") return "minimal";
8130
+ const fromBootstrap = privacyModeOf(this.cachedBootstrap?.surface?.privacyMode);
8131
+ if (fromBootstrap) return fromBootstrap;
8132
+ if (!config) return "unknown";
8133
+ const believed = this.believedSurfaceMode();
8134
+ if (believed === "standard" || believed === "minimal") return believed;
8135
+ if (!believed) this.preloadPrivacyMode();
8136
+ return believed === "unknown" ? config.privacyMode ?? "unknown" : "unknown";
8137
+ } catch {
8138
+ return "unknown";
8139
+ }
8140
+ }
8141
+ /**
8142
+ * `privacyModeNow`, but waiting for the ask in flight when there is one
8143
+ * (at most its 1.5 s cap) — for the cart's lines, which are not the
8144
+ * payment path. Never throws.
8145
+ */
8146
+ async privacyMode() {
8147
+ try {
8148
+ const now = this.privacyModeNow();
8149
+ if (now !== "unknown") return now;
8150
+ if (!this.surfaceRequest) return "unknown";
8151
+ const asked = await this.surfaceRequest;
8152
+ if (asked !== "unknown") return asked;
8153
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
8154
+ return config?.privacyMode ?? "unknown";
8155
+ } catch {
8156
+ return "unknown";
8157
+ }
8158
+ }
8159
+ /**
8160
+ * The attribution of the order a sponsor's cart is about to become
8161
+ * (ADR-0035 phase B) — what `CreateCheckout` / `UpdateCheckout` send as
8162
+ * `attribution` and shopcart writes on the order: the surface (its SDK
8163
+ * key), the page's campaign (`pageCampaign()`; without one, the campaign
8164
+ * that explains most units of the cart under the same rule `purchase` is
8165
+ * split by — `cartSlices`) and, on a surface KNOWN to be `standard`
8166
+ * (`privacyModeNow`), the page (`content()`'s url as origin + path, and
8167
+ * title) and where the session came in: the landing the session REMEMBERS
8168
+ * (`storedLanding`, else the one analytics resolved on this page) — never
8169
+ * the page the shopper pays on taken for a landing. Synchronous: nothing
8170
+ * on the way to a payment waits for it. snake_case, capped to Commerce's
8171
+ * limits. `undefined` when there is nothing to say; never throws.
8172
+ */
8173
+ orderAttribution(sponsorId) {
8174
+ try {
8175
+ const config = Configuration.isInitialized ? Configuration.get() : void 0;
8176
+ const content = this.content();
8177
+ return checkoutAttributionOf({
8178
+ surfaceKey: config?.apiKey,
8179
+ pageCampaignId: toNumber(this.pageCampaign()?.id),
8180
+ slices: this.cartSlices(sponsorId),
8181
+ content: { url: content.url, title: content.title },
8182
+ landing: this.rememberedLanding(),
8183
+ privacyMode: this.privacyModeNow()
8184
+ });
8185
+ } catch {
8186
+ return void 0;
8187
+ }
8188
+ }
8189
+ /** The landing the session remembers: what the first page stored, else what analytics resolved on this page load (consent pending); null otherwise. */
8190
+ rememberedLanding() {
8191
+ try {
8192
+ return storedLanding() ?? this.analyticsManager?.currentLanding ?? null;
8193
+ } catch {
8194
+ return null;
8195
+ }
8196
+ }
7257
8197
  /**
7258
8198
  * The cart split per campaign — the SAME rule `begin_checkout` / `purchase`
7259
8199
  * use, for hosts with their own analytics sink (Vev). Every unit of every
@@ -7283,15 +8223,63 @@ var VioFacade = class {
7283
8223
  return [];
7284
8224
  }
7285
8225
  }
8226
+ /**
8227
+ * What the micro did with the page's campaign per slot (`assignment`):
8228
+ * `console.debug` when `Vio.init({ debug: true })`; a warning once per
8229
+ * slot when the dashboard's own assignment stands (only for a slot this
8230
+ * page actually sent a campaign for) and once per slot when this page moved
8231
+ * it from an earlier page assignment (`replaced`).
8232
+ */
8233
+ reportAssignments(result, sent, pageCampaignId, debug) {
8234
+ const answered = Array.isArray(result?.locations) ? result.locations : [];
8235
+ const campaignFor = (id) => sent.find((l) => l.id === id)?.campaignId ?? pageCampaignId;
8236
+ for (const l of answered) {
8237
+ if (!l || typeof l !== "object" || !l.assignment) continue;
8238
+ if (debug && typeof console !== "undefined") console.debug("[Vio] slot", l.locationId, "assignment:", l.assignment);
8239
+ const sentCampaign = campaignFor(l.locationId);
8240
+ if (sentCampaign === void 0) continue;
8241
+ if (l.assignment === "dashboard" && !this.warnedDashboardSlots.has(l.locationId)) {
8242
+ this.warnedDashboardSlots.add(l.locationId);
8243
+ warn(`slot ${l.locationId} is assigned from the dashboard; the page's campaign is ignored there`);
8244
+ }
8245
+ if (l.assignment === "replaced" && !this.warnedReplacedSlots.has(l.locationId)) {
8246
+ this.warnedReplacedSlots.add(l.locationId);
8247
+ warn(`slot ${l.locationId} moved to campaign ${sentCampaign} by this page (it replaced an earlier page assignment)`);
8248
+ }
8249
+ }
8250
+ }
8251
+ /**
8252
+ * The memory keys of the sent locations whose campaign the micro did not
8253
+ * take up: every location when a campaign was sent and no answer carries
8254
+ * `assignment` (an older micro), or the ones answered `none` for a campaign
8255
+ * the bootstrap lists as active. They are remembered with the 1 h TTL.
8256
+ */
8257
+ campaignNotTakenUp(result, sent, pageCampaignId) {
8258
+ const out = /* @__PURE__ */ new Set();
8259
+ const withCampaign = sent.filter((l) => (l.campaignId ?? pageCampaignId) !== void 0);
8260
+ if (withCampaign.length === 0) return out;
8261
+ const answered = Array.isArray(result?.locations) ? result.locations : [];
8262
+ const anyAssignment = answered.some((l) => l && typeof l === "object" && !!l.assignment);
8263
+ for (const l of withCampaign) {
8264
+ const answer = answered.find((a) => a && a.locationId === l.id);
8265
+ const campaignId = l.campaignId ?? pageCampaignId;
8266
+ const notTaken = !anyAssignment || answer?.assignment === "none" && activeCampaignEntry(this.cachedBootstrap, campaignId) !== null;
8267
+ if (notTaken) out.add(slotDeclarationKey(l, pageCampaignId));
8268
+ }
8269
+ return out;
8270
+ }
7286
8271
  scheduleSlotDeclaration() {
7287
8272
  if (this.slotDeclareTimer) clearTimeout(this.slotDeclareTimer);
7288
8273
  this.slotDeclareTimer = setTimeout(() => {
7289
8274
  this.slotDeclareTimer = null;
7290
8275
  this.slotsDirty = false;
7291
8276
  void this.declareSlots(
7292
- [...this.slotRegistry.entries()].map(
7293
- ([id, { displayName }]) => displayName ? { id, displayName } : id
7294
- )
8277
+ [...this.slotRegistry.entries()].map(([id, { displayName, campaignId, kind }]) => ({
8278
+ id,
8279
+ ...displayName ? { displayName } : {},
8280
+ ...campaignId !== void 0 ? { campaignId } : {},
8281
+ ...kind ? { kind } : {}
8282
+ }))
7295
8283
  );
7296
8284
  }, SLOT_DECLARATION_DEBOUNCE_MS);
7297
8285
  }
@@ -7370,6 +8358,6 @@ function vippsButtonAttributes(verb = "buy", opts = {}) {
7370
8358
  };
7371
8359
  }
7372
8360
 
7373
- export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, KUSTOM_RETURN_QUERY_KEYS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, addItem, adyenReturnOutcome, adyenReturnParams, autoSlotDisplayName, autoSlotId, cartSlicesFrom, checkApplePayAvailability, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, ensureVippsButton, executeCartGraphQL, formatPrice, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, kustomReturnOutcome, lineOrigins, nexiReturnPaymentId, normalizeOrigin, normalizeScope, normalizeSlotId, primaryImageUrl, purchaseFromPaymentSuccess, readNexiPending, resolveCampaignId, resolveContent, resolveSlotCampaign, sameOrigin, showApplePaySheet, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes };
7374
- //# sourceMappingURL=chunk-BRJMT7P4.js.map
7375
- //# sourceMappingURL=chunk-BRJMT7P4.js.map
8361
+ export { ADD_ITEM_MUTATION, ADYEN_RETURN_METHOD, ADYEN_RETURN_QUERY_KEYS, AdyenOriginError, AnalyticsManager, BUYER_REF_MAX_LENGTH, CHECKOUT_ATTRIBUTION_CAPS, CLIENT_LANDING_URL_MAX_LENGTH, CONTENT_URL_MAX_LENGTH, CREATE_CART_MUTATION, CREATE_CHECKOUT_MUTATION, CREATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION, CREATE_PAYMENT_STRIPE_MUTATION, CREATE_PAYMENT_VIPPS_MUTATION, CartManager, CheckoutManager, CommerceClient, Configuration, DELETE_ITEM_MUTATION, GET_AVAILABLE_PAYMENT_METHODS_QUERY, GET_CART_QUERY, GET_CHECKOUT_QUERY, GET_LINE_ITEMS_BY_SUPPLIER_QUERY, GET_VIPPS_STATUS_QUERY, GRAPHQL_INT_MAX, KUSTOM_RETURN_QUERY_KEYS, LINE_ORIGIN_CAPS, SDK_VERSION, UPDATE_CHECKOUT_MUTATION, UPDATE_CHECKOUT_WITH_ATTRIBUTION_MUTATION, UPDATE_ITEM_MUTATION, UPDATE_ITEM_WITH_ORIGIN_MUTATION, UPDATE_SHIPPINGS_BY_SUPPLIER_MUTATION, VIPPS_BUTTON_SCRIPT, VIPPS_BUTTON_TAG, Vio, VioApi, VioApiError, __decorateClass, activeCampaignEntry, addItem, adyenReturnOutcome, adyenReturnParams, autoSlotDisplayName, autoSlotId, buyerRefOf, campaignCandidates, capContentUrl, cartSlicesFrom, checkApplePayAvailability, checkoutAttributionOf, componentKindOf, createCart, createCheckout, createCommerceClient, createPaymentStripe, createPaymentVipps, createVioApi, deleteItem, displayPrice, dominantCampaignOf, ensureVippsButton, entryContextOf, executeCartGraphQL, formatPrice, gatewayHas, gatewayHasBuyerRef, getAvailablePaymentMethods, getCart, getCartGraphQLOptions, getCheckout, getCheckoutReference, getCustomerSessionId, getGlobalCurrency, getLineItemsBySupplier, getVippsStatus, isUnknownFeatureError, isUnknownTokenError, kustomReturnOutcome, landingUrl, lineOriginWire, lineOrigins, nexiReturnPaymentId, normalizeBuyerRef, normalizeOrigin, normalizeScope, normalizeSlotId, normalizeSlotKind, originAndPath, primaryImageUrl, purchaseFromPaymentSuccess, readNexiPending, resetGatewayFeatures, resolveCampaignId, resolveClientContext, resolveContent, resolveLanding, resolveSlotCampaign, sameOrigin, showApplePaySheet, sponsorCampaignEntry, storedLanding, toCampaignId, updateCheckout, updateItem, updateShippingsBySupplier, vippsButtonAttributes, withGatewayFeature };
8362
+ //# sourceMappingURL=chunk-5KWICUME.js.map
8363
+ //# sourceMappingURL=chunk-5KWICUME.js.map