@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.
- package/README.md +38 -0
- package/index.cjs.js +9664 -0
- package/index.d.ts +19 -0
- package/index.js +9667 -0
- package/index.mjs +9662 -0
- package/package.json +41 -0
- package/stores/shopify/index.cjs.js +230 -0
- package/stores/shopify/index.js +233 -0
- package/stores/shopify/index.mjs +228 -0
|
@@ -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
|
+
}
|