@blotoutio/providers-blotout-popup-sdk 1.70.2

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.
@@ -0,0 +1,228 @@
1
+ const BLOTOUT_POPUP_KEY_PREFIX = 'blotoutPopup:';
2
+ const clearKeysWithPrefix = (storage) => {
3
+ if (!storage) {
4
+ return;
5
+ }
6
+ const toRemove = [];
7
+ for (let i = 0; i < storage.length; i++) {
8
+ const key = storage.key(i);
9
+ if (key && key.startsWith(BLOTOUT_POPUP_KEY_PREFIX)) {
10
+ toRemove.push(key);
11
+ }
12
+ }
13
+ for (const key of toRemove) {
14
+ storage.removeItem(key);
15
+ }
16
+ };
17
+ const clearBlotoutPopupState = () => {
18
+ if (typeof window === 'undefined') {
19
+ return;
20
+ }
21
+ try {
22
+ clearKeysWithPrefix(window.sessionStorage);
23
+ }
24
+ catch {
25
+ // ignore — sandboxed / private-mode access throws
26
+ }
27
+ try {
28
+ clearKeysWithPrefix(window.localStorage);
29
+ }
30
+ catch {
31
+ // ignore
32
+ }
33
+ };
34
+
35
+ var _a$1;
36
+ const registryKey = Symbol.for('blotout-popup');
37
+ if (typeof window !== 'undefined') {
38
+ (_a$1 = window[registryKey]) !== null && _a$1 !== void 0 ? _a$1 : (window[registryKey] = {});
39
+ window[registryKey].clearState = clearBlotoutPopupState;
40
+ }
41
+
42
+ const canLog = () => {
43
+ try {
44
+ return localStorage.getItem('edgeTagDebug') === '1';
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ };
50
+ const prefix = `[EdgeTag]`;
51
+ const logger = {
52
+ log: (...args) => {
53
+ if (canLog()) {
54
+ console.log(prefix, ...args);
55
+ }
56
+ },
57
+ error: (...args) => {
58
+ if (canLog()) {
59
+ console.error(prefix, ...args);
60
+ }
61
+ },
62
+ info: (...args) => {
63
+ if (canLog()) {
64
+ console.info(prefix, ...args);
65
+ }
66
+ },
67
+ trace: (...args) => {
68
+ if (canLog()) {
69
+ console.trace(prefix, ...args);
70
+ }
71
+ },
72
+ };
73
+
74
+ /**
75
+ * Themes that announce cart changes do it with one of these. There is no
76
+ * standard event, and nothing in this repo proves any given theme emits one, so
77
+ * these are a latency optimisation on top of the poll below — never the only
78
+ * way a mutation is noticed.
79
+ */
80
+ const THEME_CART_EVENTS = [
81
+ 'cart:refresh',
82
+ 'cart:change',
83
+ 'cart:updated',
84
+ 'cart-updated',
85
+ 'shopify:cart:updated',
86
+ 'ajaxify:updated',
87
+ ];
88
+ // Long enough to be invisible in request logs, short enough that "added to
89
+ // cart" still feels immediate. Only runs while the tab is visible.
90
+ const POLL_MS = 5000;
91
+ // Shopify prices are integer cents.
92
+ const CENTS_PER_UNIT = 100;
93
+ // A stalled request would otherwise hold the tracker's single in-flight slot
94
+ // forever, and every later read — every theme event, every poll — would be
95
+ // coalesced into a re-run that never comes.
96
+ const READ_TIMEOUT_MS = 3000;
97
+ /**
98
+ * `window.Shopify.routes.root` is a page global, so any other script on the
99
+ * storefront can set it. Resolving it against the page's own origin and
100
+ * refusing anything that leaves that origin is what stops a marketing app or a
101
+ * compromised third-party tag pointing the cart read at a host of its choosing
102
+ * — which would both beacon every shopper to it and let it dictate
103
+ * `cart.totalValue`, forcing a discount popup on every visitor.
104
+ */
105
+ const cartUrl = () => {
106
+ var _a, _b, _c;
107
+ const root = (_c = (_b = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.routes) === null || _b === void 0 ? void 0 : _b.root) !== null && _c !== void 0 ? _c : '/';
108
+ try {
109
+ const url = new URL(`${root}cart.js`, window.location.origin);
110
+ return url.origin === window.location.origin ? url.href : undefined;
111
+ }
112
+ catch {
113
+ return undefined;
114
+ }
115
+ };
116
+ // AbortSignal.timeout is Safari 16 / Chrome 103; older browsers get no timeout
117
+ // rather than a broken read.
118
+ const readSignal = () => typeof AbortSignal !== 'undefined' && 'timeout' in AbortSignal
119
+ ? AbortSignal.timeout(READ_TIMEOUT_MS)
120
+ : undefined;
121
+ const toSnapshot = (raw) => {
122
+ if (typeof raw !== 'object' || raw === null) {
123
+ return undefined;
124
+ }
125
+ const cart = raw;
126
+ if (typeof cart['item_count'] !== 'number') {
127
+ return undefined;
128
+ }
129
+ const totalCents = typeof cart['total_price'] === 'number' ? cart['total_price'] : 0;
130
+ return {
131
+ itemCount: cart['item_count'],
132
+ totalValue: totalCents / CENTS_PER_UNIT,
133
+ };
134
+ };
135
+ const shopifyCartAdapter = {
136
+ id: 'shopify',
137
+ // `window.Shopify` is set by the platform, not the theme, so it is the one
138
+ // reliable marker. A non-Shopify host stays inert rather than polling a URL
139
+ // that will never answer.
140
+ detect: () => { var _a; return typeof ((_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.shop) === 'string'; },
141
+ readCart: async () => {
142
+ const url = cartUrl();
143
+ if (!url) {
144
+ logger.info('BlotoutPopup SDK: cart route is not same-origin; ignored');
145
+ return undefined;
146
+ }
147
+ try {
148
+ const response = await fetch(url, {
149
+ method: 'GET',
150
+ credentials: 'same-origin',
151
+ headers: { Accept: 'application/json' },
152
+ signal: readSignal(),
153
+ });
154
+ if (!response.ok) {
155
+ return undefined;
156
+ }
157
+ const snapshot = toSnapshot(await response.json());
158
+ if (!snapshot) {
159
+ logger.info('BlotoutPopup SDK: unexpected cart.js shape; cart ignored');
160
+ }
161
+ return snapshot;
162
+ }
163
+ catch {
164
+ // A blocked or offline request is not an empty cart.
165
+ return undefined;
166
+ }
167
+ },
168
+ watch: (onMutation) => {
169
+ // No monkey-patching of window.fetch or XMLHttpRequest. A thrown wrapper
170
+ // would take the storefront's own cart down with it, and a poll catches
171
+ // every mutation regardless of how the theme performs it.
172
+ const notify = () => {
173
+ try {
174
+ onMutation();
175
+ }
176
+ catch {
177
+ // Never let our own listener escape into the theme's event dispatch.
178
+ }
179
+ };
180
+ let timer;
181
+ const startPolling = () => {
182
+ if (timer === undefined) {
183
+ timer = setInterval(notify, POLL_MS);
184
+ }
185
+ };
186
+ const stopPolling = () => {
187
+ if (timer !== undefined) {
188
+ clearInterval(timer);
189
+ timer = undefined;
190
+ }
191
+ };
192
+ // A background tab cannot be shopping, and a returning shopper may have
193
+ // changed the cart in another tab, so re-read once on the way back in.
194
+ const onVisibility = () => {
195
+ if (document.visibilityState === 'visible') {
196
+ notify();
197
+ startPolling();
198
+ }
199
+ else {
200
+ stopPolling();
201
+ }
202
+ };
203
+ for (const name of THEME_CART_EVENTS) {
204
+ document.addEventListener(name, notify);
205
+ }
206
+ document.addEventListener('visibilitychange', onVisibility);
207
+ // Fires on a back-button restore, where the cart may have moved on.
208
+ window.addEventListener('pageshow', notify);
209
+ if (document.visibilityState === 'visible') {
210
+ startPolling();
211
+ }
212
+ return () => {
213
+ stopPolling();
214
+ for (const name of THEME_CART_EVENTS) {
215
+ document.removeEventListener(name, notify);
216
+ }
217
+ document.removeEventListener('visibilitychange', onVisibility);
218
+ window.removeEventListener('pageshow', notify);
219
+ };
220
+ },
221
+ };
222
+
223
+ var _a, _b;
224
+ var _c;
225
+ if (typeof window !== 'undefined') {
226
+ (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
227
+ (_b = (_c = window[registryKey]).cartAdapter) !== null && _b !== void 0 ? _b : (_c.cartAdapter = shopifyCartAdapter);
228
+ }