@blotoutio/providers-awin-sdk 0.0.0-stage → 1.76.1

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 CHANGED
@@ -1,3 +1,11 @@
1
- # Temporary Holding Version
1
+ # providers-awin-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build providers-awin-sdk` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test providers-awin-sdk` to execute the unit tests via [Jest](https://jestjs.io).
package/index.cjs.js ADDED
@@ -0,0 +1,373 @@
1
+ 'use strict';
2
+
3
+ const packageName = 'awin';
4
+ const fallbackPixelUrl = 'https://www.awin1.com/sread.img';
5
+ const masterTagUrl = 'https://www.dwin1.com';
6
+ // Click checksum Awin appends to the landing page URL.
7
+ const clickIdQueryKey = 'awc';
8
+ const awinChannel = 'aw';
9
+ const defaultCommissionGroup = 'DEFAULT';
10
+ // Stores the last-click channel for Awin's deduplication.
11
+ const channelKey = 'channel';
12
+ // `aw` for an Awin click, otherwise the name of the other channel's click ID
13
+ // (e.g. `google` for gclid). Null when the landing URL carries no click.
14
+ const getLandingChannel = (search, clickIds) => {
15
+ const params = new URLSearchParams(search);
16
+ if (params.get(clickIdQueryKey)) {
17
+ return awinChannel;
18
+ }
19
+ const click = clickIds.find(({ param }) => params.get(param));
20
+ return click ? click.label.toLowerCase().replace(/\s+/g, '') : null;
21
+ };
22
+ // Purchase is always a conversion; any other event only when it is mapped.
23
+ // Returns null for events that must not be sent to Awin.
24
+ const getCommissionGroup = (eventName, conversions, defaultGroup) => {
25
+ var _a;
26
+ const fallback = (defaultGroup === null || defaultGroup === void 0 ? void 0 : defaultGroup.trim()) || defaultCommissionGroup;
27
+ const mapping = conversions.find((item) => item.event === eventName);
28
+ if (mapping) {
29
+ return ((_a = mapping.commissionGroup) === null || _a === void 0 ? void 0 : _a.trim()) || fallback;
30
+ }
31
+ return eventName === 'Purchase' ? fallback : null;
32
+ };
33
+ const toNumber = (value) => {
34
+ const number = Number(value);
35
+ return value === undefined || value === null || isNaN(number)
36
+ ? undefined
37
+ : number;
38
+ };
39
+ // When the checkout total is in another currency than the line items, the
40
+ // Shopify pixel sends value = grossValue = Σ(line price × qty): no tax or
41
+ // shipping in it, while taxCost / shippingCost stay in the checkout currency.
42
+ // Matched with a small tolerance for the worker's per-field currency rounding.
43
+ const isLineSubtotal = (gross, contents) => {
44
+ var _a;
45
+ if (!(contents === null || contents === void 0 ? void 0 : contents.length)) {
46
+ return false;
47
+ }
48
+ let subtotal = 0;
49
+ let units = 0;
50
+ for (const content of contents) {
51
+ const price = toNumber(content.item_price);
52
+ if (price === undefined) {
53
+ return false;
54
+ }
55
+ const quantity = (_a = toNumber(content.quantity)) !== null && _a !== void 0 ? _a : 1;
56
+ subtotal += price * quantity;
57
+ units += quantity;
58
+ }
59
+ return Math.abs(gross - subtotal) <= 0.01 + units * 0.001;
60
+ };
61
+ // Awin expects the order subtotal excluding tax and shipping, or the number of
62
+ // leads for a valueless lead. Derived here from the gross breakdown when it is
63
+ // present, so the browser and server send the same amount whatever the
64
+ // channel's purchase value mode (which only the worker applies to `value`).
65
+ const getAmount = (eventName, data) => {
66
+ var _a, _b;
67
+ const gross = toNumber(data.grossValue);
68
+ let amount = toNumber(data.value);
69
+ if (gross !== undefined) {
70
+ // A line subtotal has nothing to subtract (and the costs may be in another
71
+ // currency); gross is used as the worker's Net mode rewrites `value`.
72
+ amount = isLineSubtotal(gross, data.contents)
73
+ ? gross
74
+ : Math.max(0, gross -
75
+ ((_a = toNumber(data.taxCost)) !== null && _a !== void 0 ? _a : 0) -
76
+ ((_b = toNumber(data.shippingCost)) !== null && _b !== void 0 ? _b : 0));
77
+ }
78
+ if (amount === undefined) {
79
+ return eventName === 'Purchase' ? 0 : 1;
80
+ }
81
+ return Number(amount.toFixed(2));
82
+ };
83
+
84
+ const keyPrefix = `_worker`;
85
+
86
+ const canLog = () => {
87
+ try {
88
+ return localStorage.getItem('edgeTagDebug') === '1';
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ };
94
+ const prefix = `[EdgeTag]`;
95
+ const logger = {
96
+ log: (...args) => {
97
+ if (canLog()) {
98
+ console.log(prefix, ...args);
99
+ }
100
+ },
101
+ error: (...args) => {
102
+ if (canLog()) {
103
+ console.error(prefix, ...args);
104
+ }
105
+ },
106
+ info: (...args) => {
107
+ if (canLog()) {
108
+ console.info(prefix, ...args);
109
+ }
110
+ },
111
+ trace: (...args) => {
112
+ if (canLog()) {
113
+ console.trace(prefix, ...args);
114
+ }
115
+ },
116
+ };
117
+
118
+ const initKey = `${keyPrefix}StoreMultiple`;
119
+ const saveDataPerKey = (destination, persistType, provider, value, key) => {
120
+ const storage = getData(destination, persistType);
121
+ if (!storage['data']) {
122
+ storage['data'] = {};
123
+ }
124
+ if (!storage['data'][provider]) {
125
+ storage['data'][provider] = {};
126
+ }
127
+ storage['data'][provider][key] = value;
128
+ saveData(destination, persistType, storage);
129
+ };
130
+ const saveData = (destination, persistType, value, key = initKey) => {
131
+ if (persistType === 'session') {
132
+ const data = getSession(key);
133
+ data[destination] = value;
134
+ saveSession(data, key);
135
+ return;
136
+ }
137
+ const data = getLocal(key);
138
+ data[destination] = value;
139
+ saveLocal(data, key);
140
+ };
141
+ const getData = (destination, persistType, key = initKey) => {
142
+ let data;
143
+ if (persistType === 'session') {
144
+ data = getSession(key);
145
+ }
146
+ else {
147
+ data = getLocal(key);
148
+ }
149
+ return (data === null || data === void 0 ? void 0 : data[destination]) || {};
150
+ };
151
+ const saveLocal = (value, key) => {
152
+ try {
153
+ if (!localStorage) {
154
+ return;
155
+ }
156
+ localStorage.setItem(key, JSON.stringify(value));
157
+ }
158
+ catch {
159
+ logger.log('Local storage not supported.');
160
+ }
161
+ };
162
+ const getLocal = (key) => {
163
+ try {
164
+ if (!localStorage) {
165
+ return {};
166
+ }
167
+ const data = localStorage.getItem(key);
168
+ if (!data) {
169
+ return {};
170
+ }
171
+ return JSON.parse(data) || {};
172
+ }
173
+ catch {
174
+ return {};
175
+ }
176
+ };
177
+ const saveSession = (value, key) => {
178
+ try {
179
+ if (!sessionStorage) {
180
+ return;
181
+ }
182
+ sessionStorage.setItem(key, JSON.stringify(value));
183
+ }
184
+ catch {
185
+ logger.log('Session storage not supported.');
186
+ }
187
+ };
188
+ const getSession = (key) => {
189
+ try {
190
+ if (!sessionStorage) {
191
+ return {};
192
+ }
193
+ const data = sessionStorage.getItem(key);
194
+ if (!data) {
195
+ return {};
196
+ }
197
+ return JSON.parse(data) || {};
198
+ }
199
+ catch {
200
+ return {};
201
+ }
202
+ };
203
+
204
+ const getUrl = (pageUrl) => {
205
+ try {
206
+ return new URL(pageUrl || window.location.href);
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ };
212
+ // Records the last-click channel so conversions carry `aw` only when Awin won.
213
+ // Kept on the edge for server-side calls and locally for the conversion tag.
214
+ const saveChannel = ({ destination, sendEdgeData, manifest }, url) => {
215
+ var _a;
216
+ let clickIds = [];
217
+ try {
218
+ clickIds = JSON.parse(((_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['clickIds']) || '[]');
219
+ }
220
+ catch {
221
+ // keep detecting Awin clicks only
222
+ }
223
+ const channel = url && getLandingChannel(url.search, clickIds);
224
+ if (!channel) {
225
+ return;
226
+ }
227
+ sendEdgeData({ [`${packageName}::${channelKey}`]: channel }, {});
228
+ saveDataPerKey(destination, 'local', packageName, channel, channelKey);
229
+ };
230
+ const isExcluded = (url, excludedPaths) => !!url &&
231
+ (excludedPaths || '')
232
+ .split(',')
233
+ .map((path) => path.trim())
234
+ .some((path) => path && url.pathname.startsWith(path));
235
+ // Loads the Awin MasterTag on every page except the excluded (payment / PII)
236
+ // paths.
237
+ const init = (params) => {
238
+ var _a, _b, _c;
239
+ const { manifest, pageUrl } = params;
240
+ const advertiserId = (_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['advertiserId'];
241
+ if (!advertiserId || typeof document === 'undefined') {
242
+ return;
243
+ }
244
+ try {
245
+ const url = getUrl(pageUrl);
246
+ if (((_b = manifest.variables) === null || _b === void 0 ? void 0 : _b['deduplication']) === '1') {
247
+ saveChannel(params, url);
248
+ }
249
+ if (isExcluded(url, (_c = manifest.variables) === null || _c === void 0 ? void 0 : _c['excludedPaths'])) {
250
+ return;
251
+ }
252
+ const scriptId = `awin-mastertag-${advertiserId}`;
253
+ if (document.getElementById(scriptId)) {
254
+ return;
255
+ }
256
+ const script = document.createElement('script');
257
+ script.id = scriptId;
258
+ script.defer = true;
259
+ script.src = `${masterTagUrl}/${advertiserId}.js`;
260
+ (document.body || document.head).appendChild(script);
261
+ }
262
+ catch (e) {
263
+ logger.error(`[${packageName}] MasterTag injection failed`, e);
264
+ }
265
+ };
266
+
267
+ const parseConversions = (value) => {
268
+ try {
269
+ const parsed = JSON.parse(value || '[]');
270
+ return Array.isArray(parsed) ? parsed : [];
271
+ }
272
+ catch {
273
+ return [];
274
+ }
275
+ };
276
+ // Last-click channel saved on landing when deduplication is on, else `aw`.
277
+ const getChannel = (destination, deduplication) => {
278
+ var _a, _b;
279
+ if (deduplication !== '1') {
280
+ return awinChannel;
281
+ }
282
+ const storage = getData(destination, 'local');
283
+ return ((_b = (_a = storage.data) === null || _a === void 0 ? void 0 : _a[packageName]) === null || _b === void 0 ? void 0 : _b[channelKey]) || awinChannel;
284
+ };
285
+ // The pixel attributes the order from the awin1.com cookies, so they must be
286
+ // sent: an Image does that like Awin's own snippet, and fetch with credentials
287
+ // covers sandboxes without a DOM (Shopify custom pixel).
288
+ const sendPixel = (url) => {
289
+ if (typeof Image !== 'undefined') {
290
+ new Image(0, 0).src = url;
291
+ return;
292
+ }
293
+ fetch(url, {
294
+ mode: 'no-cors',
295
+ credentials: 'include',
296
+ keepalive: true,
297
+ }).catch((e) => logger.error(`[${packageName}] Pixel failed`, e));
298
+ };
299
+ // Declares the Awin Conversion Tag and fires the mandatory Fall-back
300
+ // Conversion Pixel. If the MasterTag is still loading it picks the sale up on
301
+ // load; if it already ran, AWIN.Tracking.run() re-triggers it.
302
+ const tag = ({ data, eventName, eventId, manifestVariables, destination, }) => {
303
+ var _a;
304
+ const payload = { sdkVersion: "1.76.1" };
305
+ const advertiserId = manifestVariables['advertiserId'];
306
+ if (typeof window === 'undefined' || !advertiserId) {
307
+ return payload;
308
+ }
309
+ const commissionGroup = getCommissionGroup(eventName, parseConversions(manifestVariables['conversions']), manifestVariables['commissionGroup']);
310
+ if (!commissionGroup) {
311
+ return payload;
312
+ }
313
+ try {
314
+ const amount = getAmount(eventName, data).toFixed(2);
315
+ const discounts = data['discounts'];
316
+ const orderRef = String(data['orderId'] || eventId);
317
+ const parts = `${commissionGroup}:${amount}`;
318
+ const currency = String(data['currency'] || 'USD');
319
+ const voucher = ((_a = discounts === null || discounts === void 0 ? void 0 : discounts[0]) === null || _a === void 0 ? void 0 : _a.code) || '';
320
+ const channel = getChannel(destination, manifestVariables['deduplication']);
321
+ const test = manifestVariables['testMode'] === '1' ? '1' : '0';
322
+ const pixel = new URL(fallbackPixelUrl);
323
+ pixel.searchParams.set('tt', 'ns');
324
+ pixel.searchParams.set('tv', '2');
325
+ pixel.searchParams.set('merchant', advertiserId);
326
+ pixel.searchParams.set('amount', amount);
327
+ pixel.searchParams.set('ch', channel);
328
+ pixel.searchParams.set('parts', parts);
329
+ pixel.searchParams.set('ref', orderRef);
330
+ pixel.searchParams.set('cr', currency);
331
+ pixel.searchParams.set('vc', voucher);
332
+ pixel.searchParams.set('testmode', test);
333
+ sendPixel(pixel.toString());
334
+ window.AWIN = window.AWIN || {};
335
+ window.AWIN.Tracking = window.AWIN.Tracking || {};
336
+ window.AWIN.Tracking.Sale = {
337
+ amount,
338
+ channel,
339
+ orderRef,
340
+ parts,
341
+ currency,
342
+ voucher,
343
+ test,
344
+ };
345
+ if (typeof window.AWIN.Tracking.run === 'function') {
346
+ window.AWIN.Tracking.run();
347
+ }
348
+ }
349
+ catch (e) {
350
+ logger.error(`[${packageName}] Conversion tag failed`, e);
351
+ }
352
+ return payload;
353
+ };
354
+
355
+ // eslint-disable-next-line @nx/enforce-module-boundaries
356
+ const data = {
357
+ name: packageName,
358
+ init,
359
+ tag,
360
+ };
361
+ try {
362
+ if (typeof window !== 'undefined') {
363
+ if (!window.edgetagProviders) {
364
+ window.edgetagProviders = [];
365
+ }
366
+ window.edgetagProviders.push(data);
367
+ }
368
+ }
369
+ catch {
370
+ // No window
371
+ }
372
+
373
+ module.exports = data;
package/index.js ADDED
@@ -0,0 +1,376 @@
1
+ var ProvidersAwinSdk = (function () {
2
+ 'use strict';
3
+
4
+ const packageName = 'awin';
5
+ const fallbackPixelUrl = 'https://www.awin1.com/sread.img';
6
+ const masterTagUrl = 'https://www.dwin1.com';
7
+ // Click checksum Awin appends to the landing page URL.
8
+ const clickIdQueryKey = 'awc';
9
+ const awinChannel = 'aw';
10
+ const defaultCommissionGroup = 'DEFAULT';
11
+ // Stores the last-click channel for Awin's deduplication.
12
+ const channelKey = 'channel';
13
+ // `aw` for an Awin click, otherwise the name of the other channel's click ID
14
+ // (e.g. `google` for gclid). Null when the landing URL carries no click.
15
+ const getLandingChannel = (search, clickIds) => {
16
+ const params = new URLSearchParams(search);
17
+ if (params.get(clickIdQueryKey)) {
18
+ return awinChannel;
19
+ }
20
+ const click = clickIds.find(({ param }) => params.get(param));
21
+ return click ? click.label.toLowerCase().replace(/\s+/g, '') : null;
22
+ };
23
+ // Purchase is always a conversion; any other event only when it is mapped.
24
+ // Returns null for events that must not be sent to Awin.
25
+ const getCommissionGroup = (eventName, conversions, defaultGroup) => {
26
+ var _a;
27
+ const fallback = (defaultGroup === null || defaultGroup === void 0 ? void 0 : defaultGroup.trim()) || defaultCommissionGroup;
28
+ const mapping = conversions.find((item) => item.event === eventName);
29
+ if (mapping) {
30
+ return ((_a = mapping.commissionGroup) === null || _a === void 0 ? void 0 : _a.trim()) || fallback;
31
+ }
32
+ return eventName === 'Purchase' ? fallback : null;
33
+ };
34
+ const toNumber = (value) => {
35
+ const number = Number(value);
36
+ return value === undefined || value === null || isNaN(number)
37
+ ? undefined
38
+ : number;
39
+ };
40
+ // When the checkout total is in another currency than the line items, the
41
+ // Shopify pixel sends value = grossValue = Σ(line price × qty): no tax or
42
+ // shipping in it, while taxCost / shippingCost stay in the checkout currency.
43
+ // Matched with a small tolerance for the worker's per-field currency rounding.
44
+ const isLineSubtotal = (gross, contents) => {
45
+ var _a;
46
+ if (!(contents === null || contents === void 0 ? void 0 : contents.length)) {
47
+ return false;
48
+ }
49
+ let subtotal = 0;
50
+ let units = 0;
51
+ for (const content of contents) {
52
+ const price = toNumber(content.item_price);
53
+ if (price === undefined) {
54
+ return false;
55
+ }
56
+ const quantity = (_a = toNumber(content.quantity)) !== null && _a !== void 0 ? _a : 1;
57
+ subtotal += price * quantity;
58
+ units += quantity;
59
+ }
60
+ return Math.abs(gross - subtotal) <= 0.01 + units * 0.001;
61
+ };
62
+ // Awin expects the order subtotal excluding tax and shipping, or the number of
63
+ // leads for a valueless lead. Derived here from the gross breakdown when it is
64
+ // present, so the browser and server send the same amount whatever the
65
+ // channel's purchase value mode (which only the worker applies to `value`).
66
+ const getAmount = (eventName, data) => {
67
+ var _a, _b;
68
+ const gross = toNumber(data.grossValue);
69
+ let amount = toNumber(data.value);
70
+ if (gross !== undefined) {
71
+ // A line subtotal has nothing to subtract (and the costs may be in another
72
+ // currency); gross is used as the worker's Net mode rewrites `value`.
73
+ amount = isLineSubtotal(gross, data.contents)
74
+ ? gross
75
+ : Math.max(0, gross -
76
+ ((_a = toNumber(data.taxCost)) !== null && _a !== void 0 ? _a : 0) -
77
+ ((_b = toNumber(data.shippingCost)) !== null && _b !== void 0 ? _b : 0));
78
+ }
79
+ if (amount === undefined) {
80
+ return eventName === 'Purchase' ? 0 : 1;
81
+ }
82
+ return Number(amount.toFixed(2));
83
+ };
84
+
85
+ const keyPrefix = `_worker`;
86
+
87
+ const canLog = () => {
88
+ try {
89
+ return localStorage.getItem('edgeTagDebug') === '1';
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ };
95
+ const prefix = `[EdgeTag]`;
96
+ const logger = {
97
+ log: (...args) => {
98
+ if (canLog()) {
99
+ console.log(prefix, ...args);
100
+ }
101
+ },
102
+ error: (...args) => {
103
+ if (canLog()) {
104
+ console.error(prefix, ...args);
105
+ }
106
+ },
107
+ info: (...args) => {
108
+ if (canLog()) {
109
+ console.info(prefix, ...args);
110
+ }
111
+ },
112
+ trace: (...args) => {
113
+ if (canLog()) {
114
+ console.trace(prefix, ...args);
115
+ }
116
+ },
117
+ };
118
+
119
+ const initKey = `${keyPrefix}StoreMultiple`;
120
+ const saveDataPerKey = (destination, persistType, provider, value, key) => {
121
+ const storage = getData(destination, persistType);
122
+ if (!storage['data']) {
123
+ storage['data'] = {};
124
+ }
125
+ if (!storage['data'][provider]) {
126
+ storage['data'][provider] = {};
127
+ }
128
+ storage['data'][provider][key] = value;
129
+ saveData(destination, persistType, storage);
130
+ };
131
+ const saveData = (destination, persistType, value, key = initKey) => {
132
+ if (persistType === 'session') {
133
+ const data = getSession(key);
134
+ data[destination] = value;
135
+ saveSession(data, key);
136
+ return;
137
+ }
138
+ const data = getLocal(key);
139
+ data[destination] = value;
140
+ saveLocal(data, key);
141
+ };
142
+ const getData = (destination, persistType, key = initKey) => {
143
+ let data;
144
+ if (persistType === 'session') {
145
+ data = getSession(key);
146
+ }
147
+ else {
148
+ data = getLocal(key);
149
+ }
150
+ return (data === null || data === void 0 ? void 0 : data[destination]) || {};
151
+ };
152
+ const saveLocal = (value, key) => {
153
+ try {
154
+ if (!localStorage) {
155
+ return;
156
+ }
157
+ localStorage.setItem(key, JSON.stringify(value));
158
+ }
159
+ catch {
160
+ logger.log('Local storage not supported.');
161
+ }
162
+ };
163
+ const getLocal = (key) => {
164
+ try {
165
+ if (!localStorage) {
166
+ return {};
167
+ }
168
+ const data = localStorage.getItem(key);
169
+ if (!data) {
170
+ return {};
171
+ }
172
+ return JSON.parse(data) || {};
173
+ }
174
+ catch {
175
+ return {};
176
+ }
177
+ };
178
+ const saveSession = (value, key) => {
179
+ try {
180
+ if (!sessionStorage) {
181
+ return;
182
+ }
183
+ sessionStorage.setItem(key, JSON.stringify(value));
184
+ }
185
+ catch {
186
+ logger.log('Session storage not supported.');
187
+ }
188
+ };
189
+ const getSession = (key) => {
190
+ try {
191
+ if (!sessionStorage) {
192
+ return {};
193
+ }
194
+ const data = sessionStorage.getItem(key);
195
+ if (!data) {
196
+ return {};
197
+ }
198
+ return JSON.parse(data) || {};
199
+ }
200
+ catch {
201
+ return {};
202
+ }
203
+ };
204
+
205
+ const getUrl = (pageUrl) => {
206
+ try {
207
+ return new URL(pageUrl || window.location.href);
208
+ }
209
+ catch {
210
+ return null;
211
+ }
212
+ };
213
+ // Records the last-click channel so conversions carry `aw` only when Awin won.
214
+ // Kept on the edge for server-side calls and locally for the conversion tag.
215
+ const saveChannel = ({ destination, sendEdgeData, manifest }, url) => {
216
+ var _a;
217
+ let clickIds = [];
218
+ try {
219
+ clickIds = JSON.parse(((_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['clickIds']) || '[]');
220
+ }
221
+ catch {
222
+ // keep detecting Awin clicks only
223
+ }
224
+ const channel = url && getLandingChannel(url.search, clickIds);
225
+ if (!channel) {
226
+ return;
227
+ }
228
+ sendEdgeData({ [`${packageName}::${channelKey}`]: channel }, {});
229
+ saveDataPerKey(destination, 'local', packageName, channel, channelKey);
230
+ };
231
+ const isExcluded = (url, excludedPaths) => !!url &&
232
+ (excludedPaths || '')
233
+ .split(',')
234
+ .map((path) => path.trim())
235
+ .some((path) => path && url.pathname.startsWith(path));
236
+ // Loads the Awin MasterTag on every page except the excluded (payment / PII)
237
+ // paths.
238
+ const init = (params) => {
239
+ var _a, _b, _c;
240
+ const { manifest, pageUrl } = params;
241
+ const advertiserId = (_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['advertiserId'];
242
+ if (!advertiserId || typeof document === 'undefined') {
243
+ return;
244
+ }
245
+ try {
246
+ const url = getUrl(pageUrl);
247
+ if (((_b = manifest.variables) === null || _b === void 0 ? void 0 : _b['deduplication']) === '1') {
248
+ saveChannel(params, url);
249
+ }
250
+ if (isExcluded(url, (_c = manifest.variables) === null || _c === void 0 ? void 0 : _c['excludedPaths'])) {
251
+ return;
252
+ }
253
+ const scriptId = `awin-mastertag-${advertiserId}`;
254
+ if (document.getElementById(scriptId)) {
255
+ return;
256
+ }
257
+ const script = document.createElement('script');
258
+ script.id = scriptId;
259
+ script.defer = true;
260
+ script.src = `${masterTagUrl}/${advertiserId}.js`;
261
+ (document.body || document.head).appendChild(script);
262
+ }
263
+ catch (e) {
264
+ logger.error(`[${packageName}] MasterTag injection failed`, e);
265
+ }
266
+ };
267
+
268
+ const parseConversions = (value) => {
269
+ try {
270
+ const parsed = JSON.parse(value || '[]');
271
+ return Array.isArray(parsed) ? parsed : [];
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ };
277
+ // Last-click channel saved on landing when deduplication is on, else `aw`.
278
+ const getChannel = (destination, deduplication) => {
279
+ var _a, _b;
280
+ if (deduplication !== '1') {
281
+ return awinChannel;
282
+ }
283
+ const storage = getData(destination, 'local');
284
+ return ((_b = (_a = storage.data) === null || _a === void 0 ? void 0 : _a[packageName]) === null || _b === void 0 ? void 0 : _b[channelKey]) || awinChannel;
285
+ };
286
+ // The pixel attributes the order from the awin1.com cookies, so they must be
287
+ // sent: an Image does that like Awin's own snippet, and fetch with credentials
288
+ // covers sandboxes without a DOM (Shopify custom pixel).
289
+ const sendPixel = (url) => {
290
+ if (typeof Image !== 'undefined') {
291
+ new Image(0, 0).src = url;
292
+ return;
293
+ }
294
+ fetch(url, {
295
+ mode: 'no-cors',
296
+ credentials: 'include',
297
+ keepalive: true,
298
+ }).catch((e) => logger.error(`[${packageName}] Pixel failed`, e));
299
+ };
300
+ // Declares the Awin Conversion Tag and fires the mandatory Fall-back
301
+ // Conversion Pixel. If the MasterTag is still loading it picks the sale up on
302
+ // load; if it already ran, AWIN.Tracking.run() re-triggers it.
303
+ const tag = ({ data, eventName, eventId, manifestVariables, destination, }) => {
304
+ var _a;
305
+ const payload = { sdkVersion: "1.76.1" };
306
+ const advertiserId = manifestVariables['advertiserId'];
307
+ if (typeof window === 'undefined' || !advertiserId) {
308
+ return payload;
309
+ }
310
+ const commissionGroup = getCommissionGroup(eventName, parseConversions(manifestVariables['conversions']), manifestVariables['commissionGroup']);
311
+ if (!commissionGroup) {
312
+ return payload;
313
+ }
314
+ try {
315
+ const amount = getAmount(eventName, data).toFixed(2);
316
+ const discounts = data['discounts'];
317
+ const orderRef = String(data['orderId'] || eventId);
318
+ const parts = `${commissionGroup}:${amount}`;
319
+ const currency = String(data['currency'] || 'USD');
320
+ const voucher = ((_a = discounts === null || discounts === void 0 ? void 0 : discounts[0]) === null || _a === void 0 ? void 0 : _a.code) || '';
321
+ const channel = getChannel(destination, manifestVariables['deduplication']);
322
+ const test = manifestVariables['testMode'] === '1' ? '1' : '0';
323
+ const pixel = new URL(fallbackPixelUrl);
324
+ pixel.searchParams.set('tt', 'ns');
325
+ pixel.searchParams.set('tv', '2');
326
+ pixel.searchParams.set('merchant', advertiserId);
327
+ pixel.searchParams.set('amount', amount);
328
+ pixel.searchParams.set('ch', channel);
329
+ pixel.searchParams.set('parts', parts);
330
+ pixel.searchParams.set('ref', orderRef);
331
+ pixel.searchParams.set('cr', currency);
332
+ pixel.searchParams.set('vc', voucher);
333
+ pixel.searchParams.set('testmode', test);
334
+ sendPixel(pixel.toString());
335
+ window.AWIN = window.AWIN || {};
336
+ window.AWIN.Tracking = window.AWIN.Tracking || {};
337
+ window.AWIN.Tracking.Sale = {
338
+ amount,
339
+ channel,
340
+ orderRef,
341
+ parts,
342
+ currency,
343
+ voucher,
344
+ test,
345
+ };
346
+ if (typeof window.AWIN.Tracking.run === 'function') {
347
+ window.AWIN.Tracking.run();
348
+ }
349
+ }
350
+ catch (e) {
351
+ logger.error(`[${packageName}] Conversion tag failed`, e);
352
+ }
353
+ return payload;
354
+ };
355
+
356
+ // eslint-disable-next-line @nx/enforce-module-boundaries
357
+ const data = {
358
+ name: packageName,
359
+ init,
360
+ tag,
361
+ };
362
+ try {
363
+ if (typeof window !== 'undefined') {
364
+ if (!window.edgetagProviders) {
365
+ window.edgetagProviders = [];
366
+ }
367
+ window.edgetagProviders.push(data);
368
+ }
369
+ }
370
+ catch {
371
+ // No window
372
+ }
373
+
374
+ return data;
375
+
376
+ })();
package/index.mjs ADDED
@@ -0,0 +1,371 @@
1
+ const packageName = 'awin';
2
+ const fallbackPixelUrl = 'https://www.awin1.com/sread.img';
3
+ const masterTagUrl = 'https://www.dwin1.com';
4
+ // Click checksum Awin appends to the landing page URL.
5
+ const clickIdQueryKey = 'awc';
6
+ const awinChannel = 'aw';
7
+ const defaultCommissionGroup = 'DEFAULT';
8
+ // Stores the last-click channel for Awin's deduplication.
9
+ const channelKey = 'channel';
10
+ // `aw` for an Awin click, otherwise the name of the other channel's click ID
11
+ // (e.g. `google` for gclid). Null when the landing URL carries no click.
12
+ const getLandingChannel = (search, clickIds) => {
13
+ const params = new URLSearchParams(search);
14
+ if (params.get(clickIdQueryKey)) {
15
+ return awinChannel;
16
+ }
17
+ const click = clickIds.find(({ param }) => params.get(param));
18
+ return click ? click.label.toLowerCase().replace(/\s+/g, '') : null;
19
+ };
20
+ // Purchase is always a conversion; any other event only when it is mapped.
21
+ // Returns null for events that must not be sent to Awin.
22
+ const getCommissionGroup = (eventName, conversions, defaultGroup) => {
23
+ var _a;
24
+ const fallback = (defaultGroup === null || defaultGroup === void 0 ? void 0 : defaultGroup.trim()) || defaultCommissionGroup;
25
+ const mapping = conversions.find((item) => item.event === eventName);
26
+ if (mapping) {
27
+ return ((_a = mapping.commissionGroup) === null || _a === void 0 ? void 0 : _a.trim()) || fallback;
28
+ }
29
+ return eventName === 'Purchase' ? fallback : null;
30
+ };
31
+ const toNumber = (value) => {
32
+ const number = Number(value);
33
+ return value === undefined || value === null || isNaN(number)
34
+ ? undefined
35
+ : number;
36
+ };
37
+ // When the checkout total is in another currency than the line items, the
38
+ // Shopify pixel sends value = grossValue = Σ(line price × qty): no tax or
39
+ // shipping in it, while taxCost / shippingCost stay in the checkout currency.
40
+ // Matched with a small tolerance for the worker's per-field currency rounding.
41
+ const isLineSubtotal = (gross, contents) => {
42
+ var _a;
43
+ if (!(contents === null || contents === void 0 ? void 0 : contents.length)) {
44
+ return false;
45
+ }
46
+ let subtotal = 0;
47
+ let units = 0;
48
+ for (const content of contents) {
49
+ const price = toNumber(content.item_price);
50
+ if (price === undefined) {
51
+ return false;
52
+ }
53
+ const quantity = (_a = toNumber(content.quantity)) !== null && _a !== void 0 ? _a : 1;
54
+ subtotal += price * quantity;
55
+ units += quantity;
56
+ }
57
+ return Math.abs(gross - subtotal) <= 0.01 + units * 0.001;
58
+ };
59
+ // Awin expects the order subtotal excluding tax and shipping, or the number of
60
+ // leads for a valueless lead. Derived here from the gross breakdown when it is
61
+ // present, so the browser and server send the same amount whatever the
62
+ // channel's purchase value mode (which only the worker applies to `value`).
63
+ const getAmount = (eventName, data) => {
64
+ var _a, _b;
65
+ const gross = toNumber(data.grossValue);
66
+ let amount = toNumber(data.value);
67
+ if (gross !== undefined) {
68
+ // A line subtotal has nothing to subtract (and the costs may be in another
69
+ // currency); gross is used as the worker's Net mode rewrites `value`.
70
+ amount = isLineSubtotal(gross, data.contents)
71
+ ? gross
72
+ : Math.max(0, gross -
73
+ ((_a = toNumber(data.taxCost)) !== null && _a !== void 0 ? _a : 0) -
74
+ ((_b = toNumber(data.shippingCost)) !== null && _b !== void 0 ? _b : 0));
75
+ }
76
+ if (amount === undefined) {
77
+ return eventName === 'Purchase' ? 0 : 1;
78
+ }
79
+ return Number(amount.toFixed(2));
80
+ };
81
+
82
+ const keyPrefix = `_worker`;
83
+
84
+ const canLog = () => {
85
+ try {
86
+ return localStorage.getItem('edgeTagDebug') === '1';
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ };
92
+ const prefix = `[EdgeTag]`;
93
+ const logger = {
94
+ log: (...args) => {
95
+ if (canLog()) {
96
+ console.log(prefix, ...args);
97
+ }
98
+ },
99
+ error: (...args) => {
100
+ if (canLog()) {
101
+ console.error(prefix, ...args);
102
+ }
103
+ },
104
+ info: (...args) => {
105
+ if (canLog()) {
106
+ console.info(prefix, ...args);
107
+ }
108
+ },
109
+ trace: (...args) => {
110
+ if (canLog()) {
111
+ console.trace(prefix, ...args);
112
+ }
113
+ },
114
+ };
115
+
116
+ const initKey = `${keyPrefix}StoreMultiple`;
117
+ const saveDataPerKey = (destination, persistType, provider, value, key) => {
118
+ const storage = getData(destination, persistType);
119
+ if (!storage['data']) {
120
+ storage['data'] = {};
121
+ }
122
+ if (!storage['data'][provider]) {
123
+ storage['data'][provider] = {};
124
+ }
125
+ storage['data'][provider][key] = value;
126
+ saveData(destination, persistType, storage);
127
+ };
128
+ const saveData = (destination, persistType, value, key = initKey) => {
129
+ if (persistType === 'session') {
130
+ const data = getSession(key);
131
+ data[destination] = value;
132
+ saveSession(data, key);
133
+ return;
134
+ }
135
+ const data = getLocal(key);
136
+ data[destination] = value;
137
+ saveLocal(data, key);
138
+ };
139
+ const getData = (destination, persistType, key = initKey) => {
140
+ let data;
141
+ if (persistType === 'session') {
142
+ data = getSession(key);
143
+ }
144
+ else {
145
+ data = getLocal(key);
146
+ }
147
+ return (data === null || data === void 0 ? void 0 : data[destination]) || {};
148
+ };
149
+ const saveLocal = (value, key) => {
150
+ try {
151
+ if (!localStorage) {
152
+ return;
153
+ }
154
+ localStorage.setItem(key, JSON.stringify(value));
155
+ }
156
+ catch {
157
+ logger.log('Local storage not supported.');
158
+ }
159
+ };
160
+ const getLocal = (key) => {
161
+ try {
162
+ if (!localStorage) {
163
+ return {};
164
+ }
165
+ const data = localStorage.getItem(key);
166
+ if (!data) {
167
+ return {};
168
+ }
169
+ return JSON.parse(data) || {};
170
+ }
171
+ catch {
172
+ return {};
173
+ }
174
+ };
175
+ const saveSession = (value, key) => {
176
+ try {
177
+ if (!sessionStorage) {
178
+ return;
179
+ }
180
+ sessionStorage.setItem(key, JSON.stringify(value));
181
+ }
182
+ catch {
183
+ logger.log('Session storage not supported.');
184
+ }
185
+ };
186
+ const getSession = (key) => {
187
+ try {
188
+ if (!sessionStorage) {
189
+ return {};
190
+ }
191
+ const data = sessionStorage.getItem(key);
192
+ if (!data) {
193
+ return {};
194
+ }
195
+ return JSON.parse(data) || {};
196
+ }
197
+ catch {
198
+ return {};
199
+ }
200
+ };
201
+
202
+ const getUrl = (pageUrl) => {
203
+ try {
204
+ return new URL(pageUrl || window.location.href);
205
+ }
206
+ catch {
207
+ return null;
208
+ }
209
+ };
210
+ // Records the last-click channel so conversions carry `aw` only when Awin won.
211
+ // Kept on the edge for server-side calls and locally for the conversion tag.
212
+ const saveChannel = ({ destination, sendEdgeData, manifest }, url) => {
213
+ var _a;
214
+ let clickIds = [];
215
+ try {
216
+ clickIds = JSON.parse(((_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['clickIds']) || '[]');
217
+ }
218
+ catch {
219
+ // keep detecting Awin clicks only
220
+ }
221
+ const channel = url && getLandingChannel(url.search, clickIds);
222
+ if (!channel) {
223
+ return;
224
+ }
225
+ sendEdgeData({ [`${packageName}::${channelKey}`]: channel }, {});
226
+ saveDataPerKey(destination, 'local', packageName, channel, channelKey);
227
+ };
228
+ const isExcluded = (url, excludedPaths) => !!url &&
229
+ (excludedPaths || '')
230
+ .split(',')
231
+ .map((path) => path.trim())
232
+ .some((path) => path && url.pathname.startsWith(path));
233
+ // Loads the Awin MasterTag on every page except the excluded (payment / PII)
234
+ // paths.
235
+ const init = (params) => {
236
+ var _a, _b, _c;
237
+ const { manifest, pageUrl } = params;
238
+ const advertiserId = (_a = manifest.variables) === null || _a === void 0 ? void 0 : _a['advertiserId'];
239
+ if (!advertiserId || typeof document === 'undefined') {
240
+ return;
241
+ }
242
+ try {
243
+ const url = getUrl(pageUrl);
244
+ if (((_b = manifest.variables) === null || _b === void 0 ? void 0 : _b['deduplication']) === '1') {
245
+ saveChannel(params, url);
246
+ }
247
+ if (isExcluded(url, (_c = manifest.variables) === null || _c === void 0 ? void 0 : _c['excludedPaths'])) {
248
+ return;
249
+ }
250
+ const scriptId = `awin-mastertag-${advertiserId}`;
251
+ if (document.getElementById(scriptId)) {
252
+ return;
253
+ }
254
+ const script = document.createElement('script');
255
+ script.id = scriptId;
256
+ script.defer = true;
257
+ script.src = `${masterTagUrl}/${advertiserId}.js`;
258
+ (document.body || document.head).appendChild(script);
259
+ }
260
+ catch (e) {
261
+ logger.error(`[${packageName}] MasterTag injection failed`, e);
262
+ }
263
+ };
264
+
265
+ const parseConversions = (value) => {
266
+ try {
267
+ const parsed = JSON.parse(value || '[]');
268
+ return Array.isArray(parsed) ? parsed : [];
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ };
274
+ // Last-click channel saved on landing when deduplication is on, else `aw`.
275
+ const getChannel = (destination, deduplication) => {
276
+ var _a, _b;
277
+ if (deduplication !== '1') {
278
+ return awinChannel;
279
+ }
280
+ const storage = getData(destination, 'local');
281
+ return ((_b = (_a = storage.data) === null || _a === void 0 ? void 0 : _a[packageName]) === null || _b === void 0 ? void 0 : _b[channelKey]) || awinChannel;
282
+ };
283
+ // The pixel attributes the order from the awin1.com cookies, so they must be
284
+ // sent: an Image does that like Awin's own snippet, and fetch with credentials
285
+ // covers sandboxes without a DOM (Shopify custom pixel).
286
+ const sendPixel = (url) => {
287
+ if (typeof Image !== 'undefined') {
288
+ new Image(0, 0).src = url;
289
+ return;
290
+ }
291
+ fetch(url, {
292
+ mode: 'no-cors',
293
+ credentials: 'include',
294
+ keepalive: true,
295
+ }).catch((e) => logger.error(`[${packageName}] Pixel failed`, e));
296
+ };
297
+ // Declares the Awin Conversion Tag and fires the mandatory Fall-back
298
+ // Conversion Pixel. If the MasterTag is still loading it picks the sale up on
299
+ // load; if it already ran, AWIN.Tracking.run() re-triggers it.
300
+ const tag = ({ data, eventName, eventId, manifestVariables, destination, }) => {
301
+ var _a;
302
+ const payload = { sdkVersion: "1.76.1" };
303
+ const advertiserId = manifestVariables['advertiserId'];
304
+ if (typeof window === 'undefined' || !advertiserId) {
305
+ return payload;
306
+ }
307
+ const commissionGroup = getCommissionGroup(eventName, parseConversions(manifestVariables['conversions']), manifestVariables['commissionGroup']);
308
+ if (!commissionGroup) {
309
+ return payload;
310
+ }
311
+ try {
312
+ const amount = getAmount(eventName, data).toFixed(2);
313
+ const discounts = data['discounts'];
314
+ const orderRef = String(data['orderId'] || eventId);
315
+ const parts = `${commissionGroup}:${amount}`;
316
+ const currency = String(data['currency'] || 'USD');
317
+ const voucher = ((_a = discounts === null || discounts === void 0 ? void 0 : discounts[0]) === null || _a === void 0 ? void 0 : _a.code) || '';
318
+ const channel = getChannel(destination, manifestVariables['deduplication']);
319
+ const test = manifestVariables['testMode'] === '1' ? '1' : '0';
320
+ const pixel = new URL(fallbackPixelUrl);
321
+ pixel.searchParams.set('tt', 'ns');
322
+ pixel.searchParams.set('tv', '2');
323
+ pixel.searchParams.set('merchant', advertiserId);
324
+ pixel.searchParams.set('amount', amount);
325
+ pixel.searchParams.set('ch', channel);
326
+ pixel.searchParams.set('parts', parts);
327
+ pixel.searchParams.set('ref', orderRef);
328
+ pixel.searchParams.set('cr', currency);
329
+ pixel.searchParams.set('vc', voucher);
330
+ pixel.searchParams.set('testmode', test);
331
+ sendPixel(pixel.toString());
332
+ window.AWIN = window.AWIN || {};
333
+ window.AWIN.Tracking = window.AWIN.Tracking || {};
334
+ window.AWIN.Tracking.Sale = {
335
+ amount,
336
+ channel,
337
+ orderRef,
338
+ parts,
339
+ currency,
340
+ voucher,
341
+ test,
342
+ };
343
+ if (typeof window.AWIN.Tracking.run === 'function') {
344
+ window.AWIN.Tracking.run();
345
+ }
346
+ }
347
+ catch (e) {
348
+ logger.error(`[${packageName}] Conversion tag failed`, e);
349
+ }
350
+ return payload;
351
+ };
352
+
353
+ // eslint-disable-next-line @nx/enforce-module-boundaries
354
+ const data = {
355
+ name: packageName,
356
+ init,
357
+ tag,
358
+ };
359
+ try {
360
+ if (typeof window !== 'undefined') {
361
+ if (!window.edgetagProviders) {
362
+ window.edgetagProviders = [];
363
+ }
364
+ window.edgetagProviders.push(data);
365
+ }
366
+ }
367
+ catch {
368
+ // No window
369
+ }
370
+
371
+ export { data as default };
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
2
  "name": "@blotoutio/providers-awin-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.76.1",
4
+ "description": "Awin 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
+ "main": "./index.cjs.js",
12
+ "module": "./index.mjs",
13
+ "types": "./index.d.ts",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/blotoutio/edgetag-sdk.git"
17
+ },
18
+ "files": [
19
+ "index.js",
20
+ "index.d.ts",
21
+ "index.cjs.js",
22
+ "index.mjs",
23
+ "package.json",
24
+ "README.md"
25
+ ]
26
+ }