@blotoutio/providers-consentiq-sdk 1.64.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.
package/README.md ADDED
@@ -0,0 +1,10 @@
1
+ # providers-consentiq-sdk
2
+
3
+ Browser SDK for the ConsentIQ provider. Collects the visitor's consent decisions
4
+ from the host store's privacy API (and any external CMP) and forwards them to the
5
+ Blotout `consentIQ/save-data` endpoint.
6
+
7
+ Delivered through the EdgeTag tag's `/load` + `/init` path — the core bundle
8
+ registers the provider, and a per-store adapter (`stores/shopify`) is appended by
9
+ `/load` only for the matching platform. This replaces the standalone ConsentIQ
10
+ Shopify app embed.
package/index.cjs.js ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const packageName = 'consentIQ';
4
+
5
+ const canLog = () => {
6
+ try {
7
+ return localStorage.getItem('edgeTagDebug') === '1';
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ };
13
+ const prefix = `[EdgeTag]`;
14
+ const logger = {
15
+ log: (...args) => {
16
+ if (canLog()) {
17
+ console.log(prefix, ...args);
18
+ }
19
+ },
20
+ error: (...args) => {
21
+ if (canLog()) {
22
+ console.error(prefix, ...args);
23
+ }
24
+ },
25
+ info: (...args) => {
26
+ if (canLog()) {
27
+ console.info(prefix, ...args);
28
+ }
29
+ },
30
+ trace: (...args) => {
31
+ if (canLog()) {
32
+ console.trace(prefix, ...args);
33
+ }
34
+ },
35
+ };
36
+
37
+ /**
38
+ * Global registry key shared across the separately-loaded consentIQ bundles
39
+ * (core `index` and the per-store adapter). `Symbol.for` resolves to the same
40
+ * symbol in every bundle, so the store adapter can register its factory here
41
+ * and the core `init` can read it back.
42
+ */
43
+ const registryKey = Symbol.for('consentiq');
44
+
45
+ /**
46
+ * consentIQ SDK entry point. Runs in the top-level browser window only.
47
+ *
48
+ * Resolves the store adapter registered by the per-store bundle (e.g.
49
+ * stores/shopify) and hands off to its `collectAnalytics`, which forwards the
50
+ * visitor's consent decisions to the Blotout `save-data` endpoint. When no
51
+ * adapter is present (platform without a consentIQ store bundle) this is a
52
+ * no-op.
53
+ */
54
+ const init = (params) => {
55
+ var _a, _b, _c, _d, _e;
56
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
57
+ // Not a browser (e.g. server-side SDK) — nothing to collect.
58
+ return;
59
+ }
60
+ if (window.top !== window) {
61
+ // Only collect from the top-level window, never nested iframes.
62
+ return;
63
+ }
64
+ try {
65
+ const registry = ((_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {}));
66
+ const storeAPI = ((_b = registry.storeAPI) !== null && _b !== void 0 ? _b : (registry.storeAPI = (_c = registry.storeAPIFactory) === null || _c === void 0 ? void 0 : _c.call(registry)));
67
+ if (!storeAPI) {
68
+ // No store adapter loaded for this platform — nothing to collect.
69
+ return;
70
+ }
71
+ storeAPI.collectAnalytics({
72
+ baseUrl: params.baseUrl,
73
+ isNewSession: (_e = (_d = params.session) === null || _d === void 0 ? void 0 : _d.isNewSession) !== null && _e !== void 0 ? _e : false,
74
+ });
75
+ }
76
+ catch (error) {
77
+ logger.error('Failed to initialize consentIQ:', error);
78
+ }
79
+ };
80
+
81
+ // eslint-disable-next-line @nx/enforce-module-boundaries
82
+ const data = {
83
+ name: packageName,
84
+ init,
85
+ };
86
+ try {
87
+ if (typeof window !== 'undefined') {
88
+ window.edgetagProviders = window.edgetagProviders || [];
89
+ window.edgetagProviders.push(data);
90
+ }
91
+ }
92
+ catch (error) {
93
+ logger.error('Failed to register consentIQ provider', error);
94
+ }
95
+
96
+ module.exports = data;
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '@blotoutio/providers-consentiq-sdk'
package/index.js ADDED
@@ -0,0 +1,99 @@
1
+ var ProvidersConsentiqSdk = (function () {
2
+ 'use strict';
3
+
4
+ const packageName = 'consentIQ';
5
+
6
+ const canLog = () => {
7
+ try {
8
+ return localStorage.getItem('edgeTagDebug') === '1';
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ };
14
+ const prefix = `[EdgeTag]`;
15
+ const logger = {
16
+ log: (...args) => {
17
+ if (canLog()) {
18
+ console.log(prefix, ...args);
19
+ }
20
+ },
21
+ error: (...args) => {
22
+ if (canLog()) {
23
+ console.error(prefix, ...args);
24
+ }
25
+ },
26
+ info: (...args) => {
27
+ if (canLog()) {
28
+ console.info(prefix, ...args);
29
+ }
30
+ },
31
+ trace: (...args) => {
32
+ if (canLog()) {
33
+ console.trace(prefix, ...args);
34
+ }
35
+ },
36
+ };
37
+
38
+ /**
39
+ * Global registry key shared across the separately-loaded consentIQ bundles
40
+ * (core `index` and the per-store adapter). `Symbol.for` resolves to the same
41
+ * symbol in every bundle, so the store adapter can register its factory here
42
+ * and the core `init` can read it back.
43
+ */
44
+ const registryKey = Symbol.for('consentiq');
45
+
46
+ /**
47
+ * consentIQ SDK entry point. Runs in the top-level browser window only.
48
+ *
49
+ * Resolves the store adapter registered by the per-store bundle (e.g.
50
+ * stores/shopify) and hands off to its `collectAnalytics`, which forwards the
51
+ * visitor's consent decisions to the Blotout `save-data` endpoint. When no
52
+ * adapter is present (platform without a consentIQ store bundle) this is a
53
+ * no-op.
54
+ */
55
+ const init = (params) => {
56
+ var _a, _b, _c, _d, _e;
57
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
58
+ // Not a browser (e.g. server-side SDK) — nothing to collect.
59
+ return;
60
+ }
61
+ if (window.top !== window) {
62
+ // Only collect from the top-level window, never nested iframes.
63
+ return;
64
+ }
65
+ try {
66
+ const registry = ((_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {}));
67
+ const storeAPI = ((_b = registry.storeAPI) !== null && _b !== void 0 ? _b : (registry.storeAPI = (_c = registry.storeAPIFactory) === null || _c === void 0 ? void 0 : _c.call(registry)));
68
+ if (!storeAPI) {
69
+ // No store adapter loaded for this platform — nothing to collect.
70
+ return;
71
+ }
72
+ storeAPI.collectAnalytics({
73
+ baseUrl: params.baseUrl,
74
+ isNewSession: (_e = (_d = params.session) === null || _d === void 0 ? void 0 : _d.isNewSession) !== null && _e !== void 0 ? _e : false,
75
+ });
76
+ }
77
+ catch (error) {
78
+ logger.error('Failed to initialize consentIQ:', error);
79
+ }
80
+ };
81
+
82
+ // eslint-disable-next-line @nx/enforce-module-boundaries
83
+ const data = {
84
+ name: packageName,
85
+ init,
86
+ };
87
+ try {
88
+ if (typeof window !== 'undefined') {
89
+ window.edgetagProviders = window.edgetagProviders || [];
90
+ window.edgetagProviders.push(data);
91
+ }
92
+ }
93
+ catch (error) {
94
+ logger.error('Failed to register consentIQ provider', error);
95
+ }
96
+
97
+ return data;
98
+
99
+ })();
package/index.mjs ADDED
@@ -0,0 +1,94 @@
1
+ const packageName = 'consentIQ';
2
+
3
+ const canLog = () => {
4
+ try {
5
+ return localStorage.getItem('edgeTagDebug') === '1';
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ };
11
+ const prefix = `[EdgeTag]`;
12
+ const logger = {
13
+ log: (...args) => {
14
+ if (canLog()) {
15
+ console.log(prefix, ...args);
16
+ }
17
+ },
18
+ error: (...args) => {
19
+ if (canLog()) {
20
+ console.error(prefix, ...args);
21
+ }
22
+ },
23
+ info: (...args) => {
24
+ if (canLog()) {
25
+ console.info(prefix, ...args);
26
+ }
27
+ },
28
+ trace: (...args) => {
29
+ if (canLog()) {
30
+ console.trace(prefix, ...args);
31
+ }
32
+ },
33
+ };
34
+
35
+ /**
36
+ * Global registry key shared across the separately-loaded consentIQ bundles
37
+ * (core `index` and the per-store adapter). `Symbol.for` resolves to the same
38
+ * symbol in every bundle, so the store adapter can register its factory here
39
+ * and the core `init` can read it back.
40
+ */
41
+ const registryKey = Symbol.for('consentiq');
42
+
43
+ /**
44
+ * consentIQ SDK entry point. Runs in the top-level browser window only.
45
+ *
46
+ * Resolves the store adapter registered by the per-store bundle (e.g.
47
+ * stores/shopify) and hands off to its `collectAnalytics`, which forwards the
48
+ * visitor's consent decisions to the Blotout `save-data` endpoint. When no
49
+ * adapter is present (platform without a consentIQ store bundle) this is a
50
+ * no-op.
51
+ */
52
+ const init = (params) => {
53
+ var _a, _b, _c, _d, _e;
54
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
55
+ // Not a browser (e.g. server-side SDK) — nothing to collect.
56
+ return;
57
+ }
58
+ if (window.top !== window) {
59
+ // Only collect from the top-level window, never nested iframes.
60
+ return;
61
+ }
62
+ try {
63
+ const registry = ((_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {}));
64
+ const storeAPI = ((_b = registry.storeAPI) !== null && _b !== void 0 ? _b : (registry.storeAPI = (_c = registry.storeAPIFactory) === null || _c === void 0 ? void 0 : _c.call(registry)));
65
+ if (!storeAPI) {
66
+ // No store adapter loaded for this platform — nothing to collect.
67
+ return;
68
+ }
69
+ storeAPI.collectAnalytics({
70
+ baseUrl: params.baseUrl,
71
+ isNewSession: (_e = (_d = params.session) === null || _d === void 0 ? void 0 : _d.isNewSession) !== null && _e !== void 0 ? _e : false,
72
+ });
73
+ }
74
+ catch (error) {
75
+ logger.error('Failed to initialize consentIQ:', error);
76
+ }
77
+ };
78
+
79
+ // eslint-disable-next-line @nx/enforce-module-boundaries
80
+ const data = {
81
+ name: packageName,
82
+ init,
83
+ };
84
+ try {
85
+ if (typeof window !== 'undefined') {
86
+ window.edgetagProviders = window.edgetagProviders || [];
87
+ window.edgetagProviders.push(data);
88
+ }
89
+ }
90
+ catch (error) {
91
+ logger.error('Failed to register consentIQ provider', error);
92
+ }
93
+
94
+ export { data as default };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@blotoutio/providers-consentiq-sdk",
3
+ "version": "1.64.0",
4
+ "description": "ConsentIQ Browser SDK for EdgeTag",
5
+ "author": "Blotout",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/blotoutio/edgetag-sdk",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "type": "module",
12
+ "main": "./index.cjs.js",
13
+ "module": "./index.mjs",
14
+ "types": "./index.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "require": "./index.cjs.js",
18
+ "import": "./index.mjs",
19
+ "default": "./index.js"
20
+ },
21
+ "./stores/shopify/index.js": {
22
+ "require": "./stores/shopify/index.cjs.js",
23
+ "import": "./stores/shopify/index.mjs",
24
+ "default": "./stores/shopify/index.js"
25
+ }
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/blotoutio/edgetag-sdk.git"
30
+ },
31
+ "files": [
32
+ "package.json",
33
+ "README.md",
34
+ "index.js",
35
+ "index.cjs.js",
36
+ "index.mjs",
37
+ "index.d.ts",
38
+ "stores/shopify/index.js",
39
+ "stores/shopify/index.cjs.js",
40
+ "stores/shopify/index.mjs"
41
+ ]
42
+ }
@@ -0,0 +1,262 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Global registry key shared across the separately-loaded consentIQ bundles
5
+ * (core `index` and the per-store adapter). `Symbol.for` resolves to the same
6
+ * symbol in every bundle, so the store adapter can register its factory here
7
+ * and the core `init` can read it back.
8
+ */
9
+ const registryKey = Symbol.for('consentiq');
10
+
11
+ const canLog = () => {
12
+ try {
13
+ return localStorage.getItem('edgeTagDebug') === '1';
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ };
19
+ const prefix = `[EdgeTag]`;
20
+ const logger = {
21
+ log: (...args) => {
22
+ if (canLog()) {
23
+ console.log(prefix, ...args);
24
+ }
25
+ },
26
+ error: (...args) => {
27
+ if (canLog()) {
28
+ console.error(prefix, ...args);
29
+ }
30
+ },
31
+ info: (...args) => {
32
+ if (canLog()) {
33
+ console.info(prefix, ...args);
34
+ }
35
+ },
36
+ trace: (...args) => {
37
+ if (canLog()) {
38
+ console.trace(prefix, ...args);
39
+ }
40
+ },
41
+ };
42
+
43
+ const getOnetrustConsent = () => {
44
+ try {
45
+ const name = 'OptanonConsent';
46
+ const cookie = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
47
+ if (cookie && cookie.length > 2) {
48
+ const match = decodeURIComponent(cookie[2]).match(new RegExp('&groups=([^&]+)'));
49
+ if (match && match.length > 1) {
50
+ const data = match[1].split(',');
51
+ if (!data) {
52
+ return null;
53
+ }
54
+ return {
55
+ analyticsSetting: data.some((g) => g === 'C0002:1' || g === '2:1')
56
+ ? 'allowed'
57
+ : 'disallowed',
58
+ marketingSetting: data.some((g) => g === 'C0004:1' || g === '4:1' || g === 'C0005:1' || g === '5:1')
59
+ ? 'allowed'
60
+ : 'disallowed',
61
+ preferencesSetting: data.some((g) => g === 'C0003:1' || g === '3:1')
62
+ ? 'allowed'
63
+ : 'disallowed',
64
+ };
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ };
73
+ const getOsanoConsent = () => {
74
+ try {
75
+ const osano = window.Osano;
76
+ if (!(osano === null || osano === void 0 ? void 0 : osano.cm)) {
77
+ return null;
78
+ }
79
+ return {
80
+ analyticsSetting: osano.cm.analytics ? 'allowed' : 'disallowed',
81
+ marketingSetting: osano.cm.marketing ? 'allowed' : 'disallowed',
82
+ preferencesSetting: osano.cm.personalization ? 'allowed' : 'disallowed',
83
+ saleOfDataSetting: osano.cm.optOut ? 'disallowed' : 'allowed',
84
+ };
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ };
90
+ const getTermlyConsent = () => {
91
+ var _a, _b;
92
+ try {
93
+ const state = (_b = (_a = window.Termly) === null || _a === void 0 ? void 0 : _a.getConsentState) === null || _b === void 0 ? void 0 : _b.call(_a);
94
+ if (!state) {
95
+ return null;
96
+ }
97
+ return {
98
+ analyticsSetting: state.analytics ? 'allowed' : 'disallowed',
99
+ marketingSetting: state.advertising || state.social_networking ? 'allowed' : 'disallowed',
100
+ preferencesSetting: state.performance ? 'allowed' : 'disallowed',
101
+ };
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ };
107
+ /**
108
+ * Reads the current decision from a supported external CMP (OneTrust, Osano, or
109
+ * Termly), if one is present. Applies regardless of host platform and takes
110
+ * precedence over the platform-derived `*Setting` fields in {@link sendConsentData}.
111
+ */
112
+ const getConsentSettings = () => {
113
+ const oneTrustConsent = getOnetrustConsent();
114
+ if (oneTrustConsent) {
115
+ return oneTrustConsent;
116
+ }
117
+ const osanoConsent = getOsanoConsent();
118
+ if (osanoConsent) {
119
+ return osanoConsent;
120
+ }
121
+ const termlyConsent = getTermlyConsent();
122
+ if (termlyConsent) {
123
+ return termlyConsent;
124
+ }
125
+ return {};
126
+ };
127
+ /**
128
+ * POSTs a consent snapshot to the Blotout backend `consentIQ/save-data`
129
+ * endpoint. The platform collector builds `body`; an external CMP still wins via
130
+ * {@link getConsentSettings}. Errors are logged and swallowed so consent capture
131
+ * never breaks the host page.
132
+ */
133
+ const sendConsentData = async (baseUrl, body) => {
134
+ const finalBody = {
135
+ ...body,
136
+ analyticsSetting: 'unknown',
137
+ marketingSetting: 'unknown',
138
+ preferencesSetting: 'unknown',
139
+ saleOfDataSetting: 'unknown',
140
+ // An external CMP (OneTrust/Osano/Termly) wins over the 'unknown' defaults.
141
+ ...getConsentSettings(),
142
+ };
143
+ try {
144
+ const response = await fetch(`${baseUrl}/providers/consentIQ/save-data`, {
145
+ method: 'POST',
146
+ body: JSON.stringify(finalBody),
147
+ headers: { 'Content-Type': 'application/json' },
148
+ credentials: 'include',
149
+ });
150
+ if (!response.ok) {
151
+ throw new Error(`save-data returned ${response.status} ${response.statusText}`);
152
+ }
153
+ }
154
+ catch (e) {
155
+ logger.error('[consentIQ] Failed to send consent data', e);
156
+ }
157
+ };
158
+
159
+ const processConsent = (consent) => {
160
+ if (consent == null) {
161
+ return 'unknown';
162
+ }
163
+ return consent ? '1' : '0';
164
+ };
165
+ // Shopify's boolean helpers (marketingAllowed(), etc.) always return a boolean
166
+ // and coerce an undecided visitor to a region default, so they cannot express
167
+ // "no decision". currentVisitorConsent() reports the truthful per-category state
168
+ // as 'yes' | 'no' | '' (empty = no decision); we read that and map empty to
169
+ // 'unknown'.
170
+ const hasConsentDecision = () => {
171
+ var _a, _b, _c;
172
+ const consent = (_c = (_b = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.customerPrivacy) === null || _b === void 0 ? void 0 : _b.currentVisitorConsent) === null || _c === void 0 ? void 0 : _c.call(_b);
173
+ return (!!consent && Object.values(consent).some((v) => v === 'yes' || v === 'no'));
174
+ };
175
+ const waitForCustomerPrivacy = () => {
176
+ return new Promise((resolve) => {
177
+ if (hasConsentDecision()) {
178
+ resolve(true);
179
+ return;
180
+ }
181
+ let attempts = 0;
182
+ const interval = setInterval(() => {
183
+ attempts++;
184
+ if (hasConsentDecision()) {
185
+ clearInterval(interval);
186
+ resolve(true);
187
+ }
188
+ else if (attempts >= 20) {
189
+ clearInterval(interval);
190
+ resolve(false);
191
+ }
192
+ }, 250);
193
+ });
194
+ };
195
+ const mapVisitorConsent = (value) => {
196
+ if (value === 'yes') {
197
+ return '1';
198
+ }
199
+ if (value === 'no') {
200
+ return '0';
201
+ }
202
+ return 'unknown';
203
+ };
204
+ const getConsentData = () => {
205
+ var _a, _b, _c;
206
+ const cp = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.customerPrivacy;
207
+ const consent = (_b = cp === null || cp === void 0 ? void 0 : cp.currentVisitorConsent) === null || _b === void 0 ? void 0 : _b.call(cp);
208
+ return {
209
+ analyticsAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['analytics']),
210
+ marketingAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['marketing']),
211
+ saleOfDataAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['sale_of_data']),
212
+ preferencesAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['preferences']),
213
+ saleOfDataRegion: processConsent((_c = cp === null || cp === void 0 ? void 0 : cp.saleOfDataRegion) === null || _c === void 0 ? void 0 : _c.call(cp)),
214
+ };
215
+ };
216
+ /**
217
+ * Collects consent from Shopify's Customer Privacy API and forwards it to the
218
+ * Blotout backend. On a new session it waits for the privacy API to become ready
219
+ * (via {@link waitForCustomerPrivacy}) and sends the current state; it then
220
+ * registers a `visitorConsentCollected` listener so later changes are forwarded
221
+ * in real time.
222
+ *
223
+ * @param ctx - {@link CollectAnalyticsContext} with the backend `baseUrl` and
224
+ * whether this is a new session.
225
+ */
226
+ const collectShopifyConsentAnalytics = ({ baseUrl, isNewSession, }) => {
227
+ if (isNewSession) {
228
+ waitForCustomerPrivacy().then(() => {
229
+ sendConsentData(baseUrl, getConsentData());
230
+ });
231
+ }
232
+ const onVisitorConsentCollected = (event) => {
233
+ var _a, _b, _c;
234
+ const ev = event;
235
+ const detail = ((_a = ev.detail) !== null && _a !== void 0 ? _a : {});
236
+ const cp = (_b = window.Shopify) === null || _b === void 0 ? void 0 : _b.customerPrivacy;
237
+ sendConsentData(baseUrl, {
238
+ analyticsAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.analyticsAllowed),
239
+ marketingAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.marketingAllowed),
240
+ saleOfDataAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.saleOfDataAllowed),
241
+ preferencesAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.preferencesAllowed),
242
+ saleOfDataRegion: processConsent((_c = cp === null || cp === void 0 ? void 0 : cp.saleOfDataRegion) === null || _c === void 0 ? void 0 : _c.call(cp)),
243
+ });
244
+ };
245
+ document.addEventListener('visitorConsentCollected', onVisitorConsentCollected);
246
+ };
247
+
248
+ var _a, _b;
249
+ var _c;
250
+ /**
251
+ * Shopify consentIQ store adapter. Loaded as a separate bundle by the CDN
252
+ * `/load` handler only when the platform is Shopify, and self-registers its
253
+ * factory on the shared registry so the core `init` can pick it up.
254
+ */
255
+ const createShopifyStoreAPI = () => ({
256
+ platform: 'shopify',
257
+ collectAnalytics: collectShopifyConsentAnalytics,
258
+ });
259
+ if (typeof window !== 'undefined') {
260
+ (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
261
+ (_b = (_c = window[registryKey]).storeAPIFactory) !== null && _b !== void 0 ? _b : (_c.storeAPIFactory = createShopifyStoreAPI);
262
+ }
@@ -0,0 +1,265 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ /**
5
+ * Global registry key shared across the separately-loaded consentIQ bundles
6
+ * (core `index` and the per-store adapter). `Symbol.for` resolves to the same
7
+ * symbol in every bundle, so the store adapter can register its factory here
8
+ * and the core `init` can read it back.
9
+ */
10
+ const registryKey = Symbol.for('consentiq');
11
+
12
+ const canLog = () => {
13
+ try {
14
+ return localStorage.getItem('edgeTagDebug') === '1';
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ };
20
+ const prefix = `[EdgeTag]`;
21
+ const logger = {
22
+ log: (...args) => {
23
+ if (canLog()) {
24
+ console.log(prefix, ...args);
25
+ }
26
+ },
27
+ error: (...args) => {
28
+ if (canLog()) {
29
+ console.error(prefix, ...args);
30
+ }
31
+ },
32
+ info: (...args) => {
33
+ if (canLog()) {
34
+ console.info(prefix, ...args);
35
+ }
36
+ },
37
+ trace: (...args) => {
38
+ if (canLog()) {
39
+ console.trace(prefix, ...args);
40
+ }
41
+ },
42
+ };
43
+
44
+ const getOnetrustConsent = () => {
45
+ try {
46
+ const name = 'OptanonConsent';
47
+ const cookie = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
48
+ if (cookie && cookie.length > 2) {
49
+ const match = decodeURIComponent(cookie[2]).match(new RegExp('&groups=([^&]+)'));
50
+ if (match && match.length > 1) {
51
+ const data = match[1].split(',');
52
+ if (!data) {
53
+ return null;
54
+ }
55
+ return {
56
+ analyticsSetting: data.some((g) => g === 'C0002:1' || g === '2:1')
57
+ ? 'allowed'
58
+ : 'disallowed',
59
+ marketingSetting: data.some((g) => g === 'C0004:1' || g === '4:1' || g === 'C0005:1' || g === '5:1')
60
+ ? 'allowed'
61
+ : 'disallowed',
62
+ preferencesSetting: data.some((g) => g === 'C0003:1' || g === '3:1')
63
+ ? 'allowed'
64
+ : 'disallowed',
65
+ };
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ };
74
+ const getOsanoConsent = () => {
75
+ try {
76
+ const osano = window.Osano;
77
+ if (!(osano === null || osano === void 0 ? void 0 : osano.cm)) {
78
+ return null;
79
+ }
80
+ return {
81
+ analyticsSetting: osano.cm.analytics ? 'allowed' : 'disallowed',
82
+ marketingSetting: osano.cm.marketing ? 'allowed' : 'disallowed',
83
+ preferencesSetting: osano.cm.personalization ? 'allowed' : 'disallowed',
84
+ saleOfDataSetting: osano.cm.optOut ? 'disallowed' : 'allowed',
85
+ };
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ };
91
+ const getTermlyConsent = () => {
92
+ var _a, _b;
93
+ try {
94
+ const state = (_b = (_a = window.Termly) === null || _a === void 0 ? void 0 : _a.getConsentState) === null || _b === void 0 ? void 0 : _b.call(_a);
95
+ if (!state) {
96
+ return null;
97
+ }
98
+ return {
99
+ analyticsSetting: state.analytics ? 'allowed' : 'disallowed',
100
+ marketingSetting: state.advertising || state.social_networking ? 'allowed' : 'disallowed',
101
+ preferencesSetting: state.performance ? 'allowed' : 'disallowed',
102
+ };
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ };
108
+ /**
109
+ * Reads the current decision from a supported external CMP (OneTrust, Osano, or
110
+ * Termly), if one is present. Applies regardless of host platform and takes
111
+ * precedence over the platform-derived `*Setting` fields in {@link sendConsentData}.
112
+ */
113
+ const getConsentSettings = () => {
114
+ const oneTrustConsent = getOnetrustConsent();
115
+ if (oneTrustConsent) {
116
+ return oneTrustConsent;
117
+ }
118
+ const osanoConsent = getOsanoConsent();
119
+ if (osanoConsent) {
120
+ return osanoConsent;
121
+ }
122
+ const termlyConsent = getTermlyConsent();
123
+ if (termlyConsent) {
124
+ return termlyConsent;
125
+ }
126
+ return {};
127
+ };
128
+ /**
129
+ * POSTs a consent snapshot to the Blotout backend `consentIQ/save-data`
130
+ * endpoint. The platform collector builds `body`; an external CMP still wins via
131
+ * {@link getConsentSettings}. Errors are logged and swallowed so consent capture
132
+ * never breaks the host page.
133
+ */
134
+ const sendConsentData = async (baseUrl, body) => {
135
+ const finalBody = {
136
+ ...body,
137
+ analyticsSetting: 'unknown',
138
+ marketingSetting: 'unknown',
139
+ preferencesSetting: 'unknown',
140
+ saleOfDataSetting: 'unknown',
141
+ // An external CMP (OneTrust/Osano/Termly) wins over the 'unknown' defaults.
142
+ ...getConsentSettings(),
143
+ };
144
+ try {
145
+ const response = await fetch(`${baseUrl}/providers/consentIQ/save-data`, {
146
+ method: 'POST',
147
+ body: JSON.stringify(finalBody),
148
+ headers: { 'Content-Type': 'application/json' },
149
+ credentials: 'include',
150
+ });
151
+ if (!response.ok) {
152
+ throw new Error(`save-data returned ${response.status} ${response.statusText}`);
153
+ }
154
+ }
155
+ catch (e) {
156
+ logger.error('[consentIQ] Failed to send consent data', e);
157
+ }
158
+ };
159
+
160
+ const processConsent = (consent) => {
161
+ if (consent == null) {
162
+ return 'unknown';
163
+ }
164
+ return consent ? '1' : '0';
165
+ };
166
+ // Shopify's boolean helpers (marketingAllowed(), etc.) always return a boolean
167
+ // and coerce an undecided visitor to a region default, so they cannot express
168
+ // "no decision". currentVisitorConsent() reports the truthful per-category state
169
+ // as 'yes' | 'no' | '' (empty = no decision); we read that and map empty to
170
+ // 'unknown'.
171
+ const hasConsentDecision = () => {
172
+ var _a, _b, _c;
173
+ const consent = (_c = (_b = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.customerPrivacy) === null || _b === void 0 ? void 0 : _b.currentVisitorConsent) === null || _c === void 0 ? void 0 : _c.call(_b);
174
+ return (!!consent && Object.values(consent).some((v) => v === 'yes' || v === 'no'));
175
+ };
176
+ const waitForCustomerPrivacy = () => {
177
+ return new Promise((resolve) => {
178
+ if (hasConsentDecision()) {
179
+ resolve(true);
180
+ return;
181
+ }
182
+ let attempts = 0;
183
+ const interval = setInterval(() => {
184
+ attempts++;
185
+ if (hasConsentDecision()) {
186
+ clearInterval(interval);
187
+ resolve(true);
188
+ }
189
+ else if (attempts >= 20) {
190
+ clearInterval(interval);
191
+ resolve(false);
192
+ }
193
+ }, 250);
194
+ });
195
+ };
196
+ const mapVisitorConsent = (value) => {
197
+ if (value === 'yes') {
198
+ return '1';
199
+ }
200
+ if (value === 'no') {
201
+ return '0';
202
+ }
203
+ return 'unknown';
204
+ };
205
+ const getConsentData = () => {
206
+ var _a, _b, _c;
207
+ const cp = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.customerPrivacy;
208
+ const consent = (_b = cp === null || cp === void 0 ? void 0 : cp.currentVisitorConsent) === null || _b === void 0 ? void 0 : _b.call(cp);
209
+ return {
210
+ analyticsAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['analytics']),
211
+ marketingAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['marketing']),
212
+ saleOfDataAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['sale_of_data']),
213
+ preferencesAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['preferences']),
214
+ saleOfDataRegion: processConsent((_c = cp === null || cp === void 0 ? void 0 : cp.saleOfDataRegion) === null || _c === void 0 ? void 0 : _c.call(cp)),
215
+ };
216
+ };
217
+ /**
218
+ * Collects consent from Shopify's Customer Privacy API and forwards it to the
219
+ * Blotout backend. On a new session it waits for the privacy API to become ready
220
+ * (via {@link waitForCustomerPrivacy}) and sends the current state; it then
221
+ * registers a `visitorConsentCollected` listener so later changes are forwarded
222
+ * in real time.
223
+ *
224
+ * @param ctx - {@link CollectAnalyticsContext} with the backend `baseUrl` and
225
+ * whether this is a new session.
226
+ */
227
+ const collectShopifyConsentAnalytics = ({ baseUrl, isNewSession, }) => {
228
+ if (isNewSession) {
229
+ waitForCustomerPrivacy().then(() => {
230
+ sendConsentData(baseUrl, getConsentData());
231
+ });
232
+ }
233
+ const onVisitorConsentCollected = (event) => {
234
+ var _a, _b, _c;
235
+ const ev = event;
236
+ const detail = ((_a = ev.detail) !== null && _a !== void 0 ? _a : {});
237
+ const cp = (_b = window.Shopify) === null || _b === void 0 ? void 0 : _b.customerPrivacy;
238
+ sendConsentData(baseUrl, {
239
+ analyticsAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.analyticsAllowed),
240
+ marketingAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.marketingAllowed),
241
+ saleOfDataAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.saleOfDataAllowed),
242
+ preferencesAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.preferencesAllowed),
243
+ saleOfDataRegion: processConsent((_c = cp === null || cp === void 0 ? void 0 : cp.saleOfDataRegion) === null || _c === void 0 ? void 0 : _c.call(cp)),
244
+ });
245
+ };
246
+ document.addEventListener('visitorConsentCollected', onVisitorConsentCollected);
247
+ };
248
+
249
+ var _a, _b;
250
+ var _c;
251
+ /**
252
+ * Shopify consentIQ store adapter. Loaded as a separate bundle by the CDN
253
+ * `/load` handler only when the platform is Shopify, and self-registers its
254
+ * factory on the shared registry so the core `init` can pick it up.
255
+ */
256
+ const createShopifyStoreAPI = () => ({
257
+ platform: 'shopify',
258
+ collectAnalytics: collectShopifyConsentAnalytics,
259
+ });
260
+ if (typeof window !== 'undefined') {
261
+ (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
262
+ (_b = (_c = window[registryKey]).storeAPIFactory) !== null && _b !== void 0 ? _b : (_c.storeAPIFactory = createShopifyStoreAPI);
263
+ }
264
+
265
+ })();
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Global registry key shared across the separately-loaded consentIQ bundles
3
+ * (core `index` and the per-store adapter). `Symbol.for` resolves to the same
4
+ * symbol in every bundle, so the store adapter can register its factory here
5
+ * and the core `init` can read it back.
6
+ */
7
+ const registryKey = Symbol.for('consentiq');
8
+
9
+ const canLog = () => {
10
+ try {
11
+ return localStorage.getItem('edgeTagDebug') === '1';
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ };
17
+ const prefix = `[EdgeTag]`;
18
+ const logger = {
19
+ log: (...args) => {
20
+ if (canLog()) {
21
+ console.log(prefix, ...args);
22
+ }
23
+ },
24
+ error: (...args) => {
25
+ if (canLog()) {
26
+ console.error(prefix, ...args);
27
+ }
28
+ },
29
+ info: (...args) => {
30
+ if (canLog()) {
31
+ console.info(prefix, ...args);
32
+ }
33
+ },
34
+ trace: (...args) => {
35
+ if (canLog()) {
36
+ console.trace(prefix, ...args);
37
+ }
38
+ },
39
+ };
40
+
41
+ const getOnetrustConsent = () => {
42
+ try {
43
+ const name = 'OptanonConsent';
44
+ const cookie = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
45
+ if (cookie && cookie.length > 2) {
46
+ const match = decodeURIComponent(cookie[2]).match(new RegExp('&groups=([^&]+)'));
47
+ if (match && match.length > 1) {
48
+ const data = match[1].split(',');
49
+ if (!data) {
50
+ return null;
51
+ }
52
+ return {
53
+ analyticsSetting: data.some((g) => g === 'C0002:1' || g === '2:1')
54
+ ? 'allowed'
55
+ : 'disallowed',
56
+ marketingSetting: data.some((g) => g === 'C0004:1' || g === '4:1' || g === 'C0005:1' || g === '5:1')
57
+ ? 'allowed'
58
+ : 'disallowed',
59
+ preferencesSetting: data.some((g) => g === 'C0003:1' || g === '3:1')
60
+ ? 'allowed'
61
+ : 'disallowed',
62
+ };
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ };
71
+ const getOsanoConsent = () => {
72
+ try {
73
+ const osano = window.Osano;
74
+ if (!(osano === null || osano === void 0 ? void 0 : osano.cm)) {
75
+ return null;
76
+ }
77
+ return {
78
+ analyticsSetting: osano.cm.analytics ? 'allowed' : 'disallowed',
79
+ marketingSetting: osano.cm.marketing ? 'allowed' : 'disallowed',
80
+ preferencesSetting: osano.cm.personalization ? 'allowed' : 'disallowed',
81
+ saleOfDataSetting: osano.cm.optOut ? 'disallowed' : 'allowed',
82
+ };
83
+ }
84
+ catch {
85
+ return null;
86
+ }
87
+ };
88
+ const getTermlyConsent = () => {
89
+ var _a, _b;
90
+ try {
91
+ const state = (_b = (_a = window.Termly) === null || _a === void 0 ? void 0 : _a.getConsentState) === null || _b === void 0 ? void 0 : _b.call(_a);
92
+ if (!state) {
93
+ return null;
94
+ }
95
+ return {
96
+ analyticsSetting: state.analytics ? 'allowed' : 'disallowed',
97
+ marketingSetting: state.advertising || state.social_networking ? 'allowed' : 'disallowed',
98
+ preferencesSetting: state.performance ? 'allowed' : 'disallowed',
99
+ };
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ };
105
+ /**
106
+ * Reads the current decision from a supported external CMP (OneTrust, Osano, or
107
+ * Termly), if one is present. Applies regardless of host platform and takes
108
+ * precedence over the platform-derived `*Setting` fields in {@link sendConsentData}.
109
+ */
110
+ const getConsentSettings = () => {
111
+ const oneTrustConsent = getOnetrustConsent();
112
+ if (oneTrustConsent) {
113
+ return oneTrustConsent;
114
+ }
115
+ const osanoConsent = getOsanoConsent();
116
+ if (osanoConsent) {
117
+ return osanoConsent;
118
+ }
119
+ const termlyConsent = getTermlyConsent();
120
+ if (termlyConsent) {
121
+ return termlyConsent;
122
+ }
123
+ return {};
124
+ };
125
+ /**
126
+ * POSTs a consent snapshot to the Blotout backend `consentIQ/save-data`
127
+ * endpoint. The platform collector builds `body`; an external CMP still wins via
128
+ * {@link getConsentSettings}. Errors are logged and swallowed so consent capture
129
+ * never breaks the host page.
130
+ */
131
+ const sendConsentData = async (baseUrl, body) => {
132
+ const finalBody = {
133
+ ...body,
134
+ analyticsSetting: 'unknown',
135
+ marketingSetting: 'unknown',
136
+ preferencesSetting: 'unknown',
137
+ saleOfDataSetting: 'unknown',
138
+ // An external CMP (OneTrust/Osano/Termly) wins over the 'unknown' defaults.
139
+ ...getConsentSettings(),
140
+ };
141
+ try {
142
+ const response = await fetch(`${baseUrl}/providers/consentIQ/save-data`, {
143
+ method: 'POST',
144
+ body: JSON.stringify(finalBody),
145
+ headers: { 'Content-Type': 'application/json' },
146
+ credentials: 'include',
147
+ });
148
+ if (!response.ok) {
149
+ throw new Error(`save-data returned ${response.status} ${response.statusText}`);
150
+ }
151
+ }
152
+ catch (e) {
153
+ logger.error('[consentIQ] Failed to send consent data', e);
154
+ }
155
+ };
156
+
157
+ const processConsent = (consent) => {
158
+ if (consent == null) {
159
+ return 'unknown';
160
+ }
161
+ return consent ? '1' : '0';
162
+ };
163
+ // Shopify's boolean helpers (marketingAllowed(), etc.) always return a boolean
164
+ // and coerce an undecided visitor to a region default, so they cannot express
165
+ // "no decision". currentVisitorConsent() reports the truthful per-category state
166
+ // as 'yes' | 'no' | '' (empty = no decision); we read that and map empty to
167
+ // 'unknown'.
168
+ const hasConsentDecision = () => {
169
+ var _a, _b, _c;
170
+ const consent = (_c = (_b = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.customerPrivacy) === null || _b === void 0 ? void 0 : _b.currentVisitorConsent) === null || _c === void 0 ? void 0 : _c.call(_b);
171
+ return (!!consent && Object.values(consent).some((v) => v === 'yes' || v === 'no'));
172
+ };
173
+ const waitForCustomerPrivacy = () => {
174
+ return new Promise((resolve) => {
175
+ if (hasConsentDecision()) {
176
+ resolve(true);
177
+ return;
178
+ }
179
+ let attempts = 0;
180
+ const interval = setInterval(() => {
181
+ attempts++;
182
+ if (hasConsentDecision()) {
183
+ clearInterval(interval);
184
+ resolve(true);
185
+ }
186
+ else if (attempts >= 20) {
187
+ clearInterval(interval);
188
+ resolve(false);
189
+ }
190
+ }, 250);
191
+ });
192
+ };
193
+ const mapVisitorConsent = (value) => {
194
+ if (value === 'yes') {
195
+ return '1';
196
+ }
197
+ if (value === 'no') {
198
+ return '0';
199
+ }
200
+ return 'unknown';
201
+ };
202
+ const getConsentData = () => {
203
+ var _a, _b, _c;
204
+ const cp = (_a = window.Shopify) === null || _a === void 0 ? void 0 : _a.customerPrivacy;
205
+ const consent = (_b = cp === null || cp === void 0 ? void 0 : cp.currentVisitorConsent) === null || _b === void 0 ? void 0 : _b.call(cp);
206
+ return {
207
+ analyticsAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['analytics']),
208
+ marketingAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['marketing']),
209
+ saleOfDataAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['sale_of_data']),
210
+ preferencesAllowed: mapVisitorConsent(consent === null || consent === void 0 ? void 0 : consent['preferences']),
211
+ saleOfDataRegion: processConsent((_c = cp === null || cp === void 0 ? void 0 : cp.saleOfDataRegion) === null || _c === void 0 ? void 0 : _c.call(cp)),
212
+ };
213
+ };
214
+ /**
215
+ * Collects consent from Shopify's Customer Privacy API and forwards it to the
216
+ * Blotout backend. On a new session it waits for the privacy API to become ready
217
+ * (via {@link waitForCustomerPrivacy}) and sends the current state; it then
218
+ * registers a `visitorConsentCollected` listener so later changes are forwarded
219
+ * in real time.
220
+ *
221
+ * @param ctx - {@link CollectAnalyticsContext} with the backend `baseUrl` and
222
+ * whether this is a new session.
223
+ */
224
+ const collectShopifyConsentAnalytics = ({ baseUrl, isNewSession, }) => {
225
+ if (isNewSession) {
226
+ waitForCustomerPrivacy().then(() => {
227
+ sendConsentData(baseUrl, getConsentData());
228
+ });
229
+ }
230
+ const onVisitorConsentCollected = (event) => {
231
+ var _a, _b, _c;
232
+ const ev = event;
233
+ const detail = ((_a = ev.detail) !== null && _a !== void 0 ? _a : {});
234
+ const cp = (_b = window.Shopify) === null || _b === void 0 ? void 0 : _b.customerPrivacy;
235
+ sendConsentData(baseUrl, {
236
+ analyticsAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.analyticsAllowed),
237
+ marketingAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.marketingAllowed),
238
+ saleOfDataAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.saleOfDataAllowed),
239
+ preferencesAllowed: processConsent(detail === null || detail === void 0 ? void 0 : detail.preferencesAllowed),
240
+ saleOfDataRegion: processConsent((_c = cp === null || cp === void 0 ? void 0 : cp.saleOfDataRegion) === null || _c === void 0 ? void 0 : _c.call(cp)),
241
+ });
242
+ };
243
+ document.addEventListener('visitorConsentCollected', onVisitorConsentCollected);
244
+ };
245
+
246
+ var _a, _b;
247
+ var _c;
248
+ /**
249
+ * Shopify consentIQ store adapter. Loaded as a separate bundle by the CDN
250
+ * `/load` handler only when the platform is Shopify, and self-registers its
251
+ * factory on the shared registry so the core `init` can pick it up.
252
+ */
253
+ const createShopifyStoreAPI = () => ({
254
+ platform: 'shopify',
255
+ collectAnalytics: collectShopifyConsentAnalytics,
256
+ });
257
+ if (typeof window !== 'undefined') {
258
+ (_a = window[registryKey]) !== null && _a !== void 0 ? _a : (window[registryKey] = {});
259
+ (_b = (_c = window[registryKey]).storeAPIFactory) !== null && _b !== void 0 ? _b : (_c.storeAPIFactory = createShopifyStoreAPI);
260
+ }