@formo/analytics 1.19.9 → 1.20.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.
Files changed (43) hide show
  1. package/README.md +3 -1
  2. package/dist/cjs/src/FormoAnalytics.d.ts +17 -1
  3. package/dist/cjs/src/FormoAnalytics.js +91 -13
  4. package/dist/cjs/src/FormoAnalyticsProvider.js +5 -1
  5. package/dist/cjs/src/constants/base.d.ts +4 -0
  6. package/dist/cjs/src/constants/base.js +8 -1
  7. package/dist/cjs/src/lib/consent.d.ts +35 -0
  8. package/dist/cjs/src/lib/consent.js +102 -0
  9. package/dist/cjs/src/lib/event/EventManager.js +6 -0
  10. package/dist/cjs/src/lib/index.d.ts +1 -0
  11. package/dist/cjs/src/lib/index.js +1 -0
  12. package/dist/cjs/src/lib/storage/built-in/blueprint.js +2 -1
  13. package/dist/cjs/src/lib/version.d.ts +1 -1
  14. package/dist/cjs/src/lib/version.js +1 -1
  15. package/dist/cjs/src/types/base.d.ts +3 -0
  16. package/dist/cjs/src/utils/address.d.ts +7 -0
  17. package/dist/cjs/src/utils/address.js +21 -1
  18. package/dist/cjs/src/utils/hash.d.ts +10 -0
  19. package/dist/cjs/src/utils/hash.js +21 -0
  20. package/dist/cjs/src/utils/index.d.ts +1 -0
  21. package/dist/cjs/src/utils/index.js +1 -0
  22. package/dist/esm/src/FormoAnalytics.d.ts +17 -1
  23. package/dist/esm/src/FormoAnalytics.js +93 -15
  24. package/dist/esm/src/FormoAnalyticsProvider.js +5 -1
  25. package/dist/esm/src/constants/base.d.ts +4 -0
  26. package/dist/esm/src/constants/base.js +7 -0
  27. package/dist/esm/src/lib/consent.d.ts +35 -0
  28. package/dist/esm/src/lib/consent.js +97 -0
  29. package/dist/esm/src/lib/event/EventManager.js +6 -0
  30. package/dist/esm/src/lib/index.d.ts +1 -0
  31. package/dist/esm/src/lib/index.js +1 -0
  32. package/dist/esm/src/lib/storage/built-in/blueprint.js +2 -1
  33. package/dist/esm/src/lib/version.d.ts +1 -1
  34. package/dist/esm/src/lib/version.js +1 -1
  35. package/dist/esm/src/types/base.d.ts +3 -0
  36. package/dist/esm/src/utils/address.d.ts +7 -0
  37. package/dist/esm/src/utils/address.js +19 -0
  38. package/dist/esm/src/utils/hash.d.ts +10 -0
  39. package/dist/esm/src/utils/hash.js +18 -0
  40. package/dist/esm/src/utils/index.d.ts +1 -0
  41. package/dist/esm/src/utils/index.js +1 -0
  42. package/dist/index.umd.min.js +1 -1
  43. package/package.json +2 -2
package/README.md CHANGED
@@ -24,7 +24,9 @@ You can install Formo on:
24
24
  - [React apps](https://docs.formo.so/install#react)
25
25
  - [Next.js apps](https://docs.formo.so/install#next-js-app-router)
26
26
 
27
- Visit Formo's [Developer Docs](https://docs.formo.so) for detailed guides and installation instructions.
27
+ ## Configuration
28
+
29
+ Visit Formo's [Developer Docs](https://docs.formo.so) for detailed guides on local testing, debugging, and consent management.
28
30
 
29
31
  ## Methodology
30
32
 
@@ -177,6 +177,22 @@ export declare class FormoAnalytics implements IFormoAnalytics {
177
177
  * @returns {Promise<void>}
178
178
  */
179
179
  track(event: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
180
+ /**
181
+ * Opt out of tracking.
182
+ * @returns {void}
183
+ */
184
+ optOutTracking(): void;
185
+ /**
186
+ * Opt back into tracking after previously opting out. This will re-enable analytics tracking
187
+ * and switch back to persistent storage.
188
+ * @returns {void}
189
+ */
190
+ optInTracking(): void;
191
+ /**
192
+ * Check if the user has opted out of tracking.
193
+ * @returns {boolean} True if the user has opted out
194
+ */
195
+ hasOptedOutTracking(): boolean;
180
196
  private trackProvider;
181
197
  private trackProviders;
182
198
  private addProviderListener;
@@ -204,7 +220,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
204
220
  private trackPageHit;
205
221
  private trackEvent;
206
222
  /**
207
- * Determines if tracking should be enabled based on configuration
223
+ * Determines if tracking should be enabled based on configuration and consent
208
224
  * @returns {boolean} True if tracking should be enabled
209
225
  */
210
226
  private shouldTrack;
@@ -126,6 +126,10 @@ var FormoAnalytics = /** @class */ (function () {
126
126
  maxQueueSize: options.maxQueueSize,
127
127
  flushInterval: options.flushInterval,
128
128
  }));
129
+ // Check consent status on initialization
130
+ if (this.hasOptedOutTracking()) {
131
+ lib_1.logger.info("User has previously opted out of tracking");
132
+ }
129
133
  // Handle initial provider (injected) as fallback; listeners for EIP-6963 are added later
130
134
  var provider = undefined;
131
135
  var optProvider = options.provider;
@@ -407,7 +411,7 @@ var FormoAnalytics = /** @class */ (function () {
407
411
  */
408
412
  FormoAnalytics.prototype.identify = function (params, properties, context, callback) {
409
413
  return __awaiter(this, void 0, void 0, function () {
410
- var _i, _a, providerDetail, provider, address_2, validAddress_1, err_1, userId, address, providerName, rdns, validAddress, e_1;
414
+ var _i, _a, providerDetail, provider, address_2, validAddress_1, err_1, userId, address, providerName, rdns, validAddress, isAlreadyIdentified, e_1;
411
415
  var _b;
412
416
  return __generator(this, function (_c) {
413
417
  switch (_c.label) {
@@ -432,6 +436,12 @@ var FormoAnalytics = /** @class */ (function () {
432
436
  address_2 = _c.sent();
433
437
  if (!address_2) return [3 /*break*/, 6];
434
438
  validAddress_1 = this.validateAndChecksumAddress(address_2);
439
+ lib_1.logger.info("Auto-identify: Checking deduplication", {
440
+ validAddress: validAddress_1,
441
+ rdns: providerDetail.info.rdns,
442
+ providerName: providerDetail.info.name,
443
+ isAlreadyIdentified: validAddress_1 ? this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns) : false
444
+ });
435
445
  if (!(validAddress_1 && !this.session.isWalletIdentified(validAddress_1, providerDetail.info.rdns))) return [3 /*break*/, 5];
436
446
  lib_1.logger.info("Auto-identifying", validAddress_1, providerDetail.info.name, providerDetail.info.rdns);
437
447
  // NOTE: do not set this.currentAddress without explicit connect or identify
@@ -476,14 +486,23 @@ var FormoAnalytics = /** @class */ (function () {
476
486
  this.currentUserId = userId;
477
487
  (0, lib_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId);
478
488
  }
479
- // Check for duplicate identify events in this session
480
- if (validAddress && rdns && this.session.isWalletIdentified(validAddress, rdns)) {
481
- lib_1.logger.warn("Identify: Wallet ".concat(providerName || 'Unknown', " with address ").concat(validAddress, " already identified in this session"));
489
+ isAlreadyIdentified = validAddress ? this.session.isWalletIdentified(validAddress, rdns || '') : false;
490
+ lib_1.logger.debug("Identify: Checking deduplication", {
491
+ validAddress: validAddress,
492
+ rdns: rdns,
493
+ providerName: providerName,
494
+ hasValidAddress: !!validAddress,
495
+ hasRdns: !!rdns,
496
+ isAlreadyIdentified: isAlreadyIdentified
497
+ });
498
+ if (isAlreadyIdentified) {
499
+ lib_1.logger.info("Identify: Wallet ".concat(providerName || 'Unknown', " with address ").concat(validAddress, " already identified in this session (rdns: ").concat(rdns || 'empty', ")"));
482
500
  return [2 /*return*/];
483
501
  }
484
502
  // Mark as identified before emitting the event
485
- if (validAddress && rdns) {
486
- this.session.markWalletIdentified(validAddress, rdns);
503
+ // Mark even if rdns is empty to prevent duplicate empty identifies
504
+ if (validAddress) {
505
+ this.session.markWalletIdentified(validAddress, rdns || '');
487
506
  }
488
507
  return [4 /*yield*/, this.trackEvent(constants_1.EventType.IDENTIFY, {
489
508
  address: validAddress,
@@ -552,6 +571,39 @@ var FormoAnalytics = /** @class */ (function () {
552
571
  });
553
572
  });
554
573
  };
574
+ /*
575
+ Consent management functions
576
+ */
577
+ /**
578
+ * Opt out of tracking.
579
+ * @returns {void}
580
+ */
581
+ FormoAnalytics.prototype.optOutTracking = function () {
582
+ lib_1.logger.info("Opting out of tracking");
583
+ // Set opt-out flag in persistent storage using direct cookie access
584
+ // This must be done before switching storage to ensure persistence
585
+ (0, lib_1.setConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY, "true");
586
+ this.reset();
587
+ lib_1.logger.info("Successfully opted out of tracking");
588
+ };
589
+ /**
590
+ * Opt back into tracking after previously opting out. This will re-enable analytics tracking
591
+ * and switch back to persistent storage.
592
+ * @returns {void}
593
+ */
594
+ FormoAnalytics.prototype.optInTracking = function () {
595
+ lib_1.logger.info("Opting back into tracking");
596
+ // Remove opt-out flag
597
+ (0, lib_1.removeConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY);
598
+ lib_1.logger.info("Successfully opted back into tracking");
599
+ };
600
+ /**
601
+ * Check if the user has opted out of tracking.
602
+ * @returns {boolean} True if the user has opted out
603
+ */
604
+ FormoAnalytics.prototype.hasOptedOutTracking = function () {
605
+ return (0, lib_1.getConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY) === "true";
606
+ };
555
607
  /*
556
608
  SDK tracking and event listener functions
557
609
  */
@@ -1262,10 +1314,14 @@ var FormoAnalytics = /** @class */ (function () {
1262
1314
  });
1263
1315
  };
1264
1316
  /**
1265
- * Determines if tracking should be enabled based on configuration
1317
+ * Determines if tracking should be enabled based on configuration and consent
1266
1318
  * @returns {boolean} True if tracking should be enabled
1267
1319
  */
1268
1320
  FormoAnalytics.prototype.shouldTrack = function () {
1321
+ // First check if user has opted out of tracking
1322
+ if (this.hasOptedOutTracking()) {
1323
+ return false;
1324
+ }
1269
1325
  // Check if tracking is explicitly provided as a boolean
1270
1326
  if (typeof this.options.tracking === 'boolean') {
1271
1327
  return this.options.tracking;
@@ -1847,6 +1903,10 @@ exports.FormoAnalytics = FormoAnalytics;
1847
1903
  var FormoAnalyticsSession = /** @class */ (function () {
1848
1904
  function FormoAnalyticsSession() {
1849
1905
  }
1906
+ FormoAnalyticsSession.prototype.generateIdentificationKey = function (address, rdns) {
1907
+ // If rdns is missing, use address-only key as fallback for empty identifies
1908
+ return rdns ? "".concat(address, ":").concat(rdns) : address;
1909
+ };
1850
1910
  FormoAnalyticsSession.prototype.isWalletDetected = function (rdns) {
1851
1911
  var _a;
1852
1912
  var rdnses = ((_a = (0, lib_1.cookie)().get(constants_1.SESSION_WALLET_DETECTED_KEY)) === null || _a === void 0 ? void 0 : _a.split(",")) || [];
@@ -1865,22 +1925,40 @@ var FormoAnalyticsSession = /** @class */ (function () {
1865
1925
  }
1866
1926
  };
1867
1927
  FormoAnalyticsSession.prototype.isWalletIdentified = function (address, rdns) {
1868
- var _a;
1869
- var identifiedKey = "".concat(address, ":").concat(rdns);
1870
- var identifiedWallets = ((_a = (0, lib_1.cookie)().get(constants_1.SESSION_WALLET_IDENTIFIED_KEY)) === null || _a === void 0 ? void 0 : _a.split(",")) || [];
1871
- return identifiedWallets.includes(identifiedKey);
1928
+ var identifiedKey = this.generateIdentificationKey(address, rdns);
1929
+ var cookieValue = (0, lib_1.cookie)().get(constants_1.SESSION_WALLET_IDENTIFIED_KEY);
1930
+ var identifiedWallets = (cookieValue === null || cookieValue === void 0 ? void 0 : cookieValue.split(",")) || [];
1931
+ var isIdentified = identifiedWallets.includes(identifiedKey);
1932
+ lib_1.logger.debug("Session: Checking wallet identification", {
1933
+ identifiedKey: identifiedKey,
1934
+ isIdentified: isIdentified,
1935
+ hasRdns: !!rdns
1936
+ });
1937
+ return isIdentified;
1872
1938
  };
1873
1939
  FormoAnalyticsSession.prototype.markWalletIdentified = function (address, rdns) {
1874
1940
  var _a;
1875
- var identifiedKey = "".concat(address, ":").concat(rdns);
1941
+ var identifiedKey = this.generateIdentificationKey(address, rdns);
1876
1942
  var identifiedWallets = ((_a = (0, lib_1.cookie)().get(constants_1.SESSION_WALLET_IDENTIFIED_KEY)) === null || _a === void 0 ? void 0 : _a.split(",")) || [];
1877
1943
  if (!identifiedWallets.includes(identifiedKey)) {
1878
1944
  identifiedWallets.push(identifiedKey);
1879
- (0, lib_1.cookie)().set(constants_1.SESSION_WALLET_IDENTIFIED_KEY, identifiedWallets.join(","), {
1945
+ var newValue = identifiedWallets.join(",");
1946
+ (0, lib_1.cookie)().set(constants_1.SESSION_WALLET_IDENTIFIED_KEY, newValue, {
1880
1947
  // by the end of the day
1881
1948
  expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
1882
1949
  path: "/",
1883
1950
  });
1951
+ lib_1.logger.debug("Session: Marked wallet as identified", {
1952
+ identifiedKey: identifiedKey,
1953
+ hasRdns: !!rdns
1954
+ });
1955
+ }
1956
+ else {
1957
+ lib_1.logger.info("Session: Wallet already marked as identified", {
1958
+ identifiedKey: identifiedKey,
1959
+ existingWallets: identifiedWallets,
1960
+ hasRdns: !!rdns
1961
+ });
1884
1962
  }
1885
1963
  };
1886
1964
  return FormoAnalyticsSession;
@@ -55,7 +55,7 @@ var lib_1 = require("./lib");
55
55
  var defaultContext = {
56
56
  chain: function () { return Promise.resolve(); },
57
57
  page: function () { return Promise.resolve(); },
58
- reset: function () { return Promise.resolve(); },
58
+ reset: function () { },
59
59
  detect: function () { return Promise.resolve(); },
60
60
  connect: function () { return Promise.resolve(); },
61
61
  disconnect: function () { return Promise.resolve(); },
@@ -63,6 +63,10 @@ var defaultContext = {
63
63
  transaction: function () { return Promise.resolve(); },
64
64
  identify: function () { return Promise.resolve(); },
65
65
  track: function () { return Promise.resolve(); },
66
+ // Consent management methods
67
+ optOutTracking: function () { },
68
+ optInTracking: function () { },
69
+ hasOptedOutTracking: function () { return false; },
66
70
  };
67
71
  exports.FormoAnalyticsContext = (0, react_1.createContext)(defaultContext);
68
72
  var FormoAnalyticsProvider = function (props) {
@@ -4,5 +4,9 @@ export declare const SESSION_WALLET_IDENTIFIED_KEY = "wallet-identified";
4
4
  export declare const SESSION_CURRENT_URL_KEY = "analytics-current-url";
5
5
  export declare const SESSION_USER_ID_KEY = "user-id";
6
6
  export declare const LOCAL_ANONYMOUS_ID_KEY = "anonymous-id";
7
+ export declare const CONSENT_OPT_OUT_KEY = "opt-out-tracking";
7
8
  export declare const DEFAULT_PROVIDER_ICON = "data:image/svg+xml;base64,";
9
+ export declare const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
10
+ export declare const DEAD_ADDRESS = "0x000000000000000000000000000000000000dEaD";
11
+ export declare const BLOCKED_ADDRESSES: readonly ["0x0000000000000000000000000000000000000000", "0x000000000000000000000000000000000000dEaD"];
8
12
  //# sourceMappingURL=base.d.ts.map
@@ -1,12 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_PROVIDER_ICON = exports.LOCAL_ANONYMOUS_ID_KEY = exports.SESSION_USER_ID_KEY = exports.SESSION_CURRENT_URL_KEY = exports.SESSION_WALLET_IDENTIFIED_KEY = exports.SESSION_WALLET_DETECTED_KEY = exports.SESSION_TRAFFIC_SOURCE_KEY = void 0;
3
+ exports.BLOCKED_ADDRESSES = exports.DEAD_ADDRESS = exports.ZERO_ADDRESS = exports.DEFAULT_PROVIDER_ICON = exports.CONSENT_OPT_OUT_KEY = exports.LOCAL_ANONYMOUS_ID_KEY = exports.SESSION_USER_ID_KEY = exports.SESSION_CURRENT_URL_KEY = exports.SESSION_WALLET_IDENTIFIED_KEY = exports.SESSION_WALLET_DETECTED_KEY = exports.SESSION_TRAFFIC_SOURCE_KEY = void 0;
4
4
  exports.SESSION_TRAFFIC_SOURCE_KEY = "traffic-source";
5
5
  exports.SESSION_WALLET_DETECTED_KEY = "wallet-detected";
6
6
  exports.SESSION_WALLET_IDENTIFIED_KEY = "wallet-identified";
7
7
  exports.SESSION_CURRENT_URL_KEY = "analytics-current-url";
8
8
  exports.SESSION_USER_ID_KEY = "user-id";
9
9
  exports.LOCAL_ANONYMOUS_ID_KEY = "anonymous-id";
10
+ // Consent management keys
11
+ exports.CONSENT_OPT_OUT_KEY = "opt-out-tracking";
10
12
  // Default provider icon (empty data URL)
11
13
  exports.DEFAULT_PROVIDER_ICON = 'data:image/svg+xml;base64,';
14
+ // Blocked addresses that should not emit events
15
+ exports.ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
16
+ exports.DEAD_ADDRESS = "0x000000000000000000000000000000000000dEaD";
17
+ // Array of all blocked addresses for easy checking
18
+ exports.BLOCKED_ADDRESSES = [exports.ZERO_ADDRESS, exports.DEAD_ADDRESS];
12
19
  //# sourceMappingURL=base.js.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Consent management utilities for handling tracking consent flags in cookies.
3
+ * These functions bypass the consent-aware storage system to ensure consent
4
+ * preferences are always stored persistently for compliance purposes.
5
+ *
6
+ * All consent cookies are project-specific to avoid conflicts between different
7
+ * Formo projects on the same domain.
8
+ */
9
+ /**
10
+ * Set a consent flag directly in cookies, bypassing the consent-aware storage system.
11
+ * Uses cookies for consent storage to ensure:
12
+ * - Cross-domain/subdomain compatibility
13
+ * - Server-side accessibility for compliance auditing
14
+ * - Regulatory compliance (GDPR/CCPA requirements)
15
+ * - Explicit expiration handling
16
+ * - Project isolation (no conflicts between different Formo projects)
17
+ * @param projectId - The project identifier (writeKey)
18
+ * @param key - The cookie key
19
+ * @param value - The cookie value
20
+ */
21
+ export declare function setConsentFlag(projectId: string, key: string, value: string): void;
22
+ /**
23
+ * Get a consent flag directly from cookies, bypassing the consent-aware storage system
24
+ * @param projectId - The project identifier (writeKey)
25
+ * @param key - The cookie key
26
+ * @returns The cookie value or null if not found
27
+ */
28
+ export declare function getConsentFlag(projectId: string, key: string): string | null;
29
+ /**
30
+ * Remove a consent flag directly from cookies, bypassing the consent-aware storage system
31
+ * @param projectId - The project identifier (writeKey)
32
+ * @param key - The cookie key
33
+ */
34
+ export declare function removeConsentFlag(projectId: string, key: string): void;
35
+ //# sourceMappingURL=consent.d.ts.map
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Consent management utilities for handling tracking consent flags in cookies.
4
+ * These functions bypass the consent-aware storage system to ensure consent
5
+ * preferences are always stored persistently for compliance purposes.
6
+ *
7
+ * All consent cookies are project-specific to avoid conflicts between different
8
+ * Formo projects on the same domain.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.setConsentFlag = setConsentFlag;
12
+ exports.getConsentFlag = getConsentFlag;
13
+ exports.removeConsentFlag = removeConsentFlag;
14
+ var hash_1 = require("../utils/hash");
15
+ /**
16
+ * Generate a project-specific cookie key to avoid conflicts between different Formo projects
17
+ * Uses hashed writeKey for privacy and security
18
+ * @param projectId - The project identifier (writeKey)
19
+ * @param key - The base cookie key
20
+ * @returns Project-specific cookie key
21
+ */
22
+ function getProjectSpecificKey(projectId, key) {
23
+ return "formo_".concat((0, hash_1.secureHash)(projectId), "_").concat(key);
24
+ }
25
+ /**
26
+ * Set a consent flag directly in cookies, bypassing the consent-aware storage system.
27
+ * Uses cookies for consent storage to ensure:
28
+ * - Cross-domain/subdomain compatibility
29
+ * - Server-side accessibility for compliance auditing
30
+ * - Regulatory compliance (GDPR/CCPA requirements)
31
+ * - Explicit expiration handling
32
+ * - Project isolation (no conflicts between different Formo projects)
33
+ * @param projectId - The project identifier (writeKey)
34
+ * @param key - The cookie key
35
+ * @param value - The cookie value
36
+ */
37
+ function setConsentFlag(projectId, key, value) {
38
+ var _a;
39
+ if (typeof document !== 'undefined') {
40
+ var projectSpecificKey = getProjectSpecificKey(projectId, key);
41
+ var expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString(); // 1 year (GDPR compliant)
42
+ var isSecure = ((_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.protocol) === 'https:';
43
+ // Enhanced privacy settings: Secure (HTTPS), SameSite=Strict for consent cookies
44
+ document.cookie = "".concat(projectSpecificKey, "=").concat(encodeURIComponent(value), "; expires=").concat(expires, "; path=/; SameSite=Strict").concat(isSecure ? '; Secure' : '');
45
+ }
46
+ }
47
+ /**
48
+ * Get a consent flag directly from cookies, bypassing the consent-aware storage system
49
+ * @param projectId - The project identifier (writeKey)
50
+ * @param key - The cookie key
51
+ * @returns The cookie value or null if not found
52
+ */
53
+ function getConsentFlag(projectId, key) {
54
+ if (typeof document === 'undefined')
55
+ return null;
56
+ var projectSpecificKey = getProjectSpecificKey(projectId, key);
57
+ var cookies = document.cookie.split(';');
58
+ for (var _i = 0, cookies_1 = cookies; _i < cookies_1.length; _i++) {
59
+ var cookie = cookies_1[_i];
60
+ var trimmed = cookie.trim();
61
+ var eqIdx = trimmed.indexOf('=');
62
+ if (eqIdx === -1)
63
+ continue;
64
+ var cookieKey = trimmed.substring(0, eqIdx);
65
+ var cookieValue = trimmed.substring(eqIdx + 1);
66
+ if (cookieKey === projectSpecificKey) {
67
+ return decodeURIComponent(cookieValue || '');
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Remove a consent flag directly from cookies, bypassing the consent-aware storage system
74
+ * @param projectId - The project identifier (writeKey)
75
+ * @param key - The cookie key
76
+ */
77
+ function removeConsentFlag(projectId, key) {
78
+ var projectSpecificKey = getProjectSpecificKey(projectId, key);
79
+ deleteCookieDirectly(projectSpecificKey);
80
+ }
81
+ /**
82
+ * Delete a cookie directly, handling various domain scenarios
83
+ * @param cookieName - The name of the cookie to delete
84
+ */
85
+ function deleteCookieDirectly(cookieName) {
86
+ if (typeof document === 'undefined')
87
+ return;
88
+ // Clear from current domain/path
89
+ document.cookie = "".concat(cookieName, "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;");
90
+ // Try to clear from parent domain if it's a proper multi-level domain
91
+ if (typeof window !== 'undefined') {
92
+ var hostname = window.location.hostname;
93
+ var parts = hostname.split('.');
94
+ // Only try parent domain deletion for proper domains with multiple parts
95
+ // Skip localhost and single-level domains
96
+ if (parts.length >= 2 && hostname !== 'localhost') {
97
+ var domain = parts.slice(-2).join('.');
98
+ document.cookie = "".concat(cookieName, "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.").concat(domain, ";");
99
+ }
100
+ }
101
+ }
102
+ //# sourceMappingURL=consent.js.map
@@ -14,6 +14,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.EventManager = void 0;
15
15
  var logger_1 = require("../logger");
16
16
  var EventFactory_1 = require("./EventFactory");
17
+ var address_1 = require("../../utils/address");
17
18
  /**
18
19
  * A service to generate valid event payloads and queue them for processing
19
20
  */
@@ -33,6 +34,11 @@ var EventManager = /** @class */ (function () {
33
34
  EventManager.prototype.addEvent = function (event, address, userId) {
34
35
  var callback = event.callback, _event = __rest(event, ["callback"]);
35
36
  var formoEvent = this.eventFactory.create(_event, address, userId);
37
+ // Check if the final event has a blocked address - don't queue it
38
+ if (formoEvent.address && (0, address_1.isBlockedAddress)(formoEvent.address)) {
39
+ logger_1.logger.warn("Event blocked: Address ".concat(formoEvent.address, " is in the blocked list and cannot emit events"));
40
+ return;
41
+ }
36
42
  this.eventQueue.enqueue(formoEvent, function (err, _, data) {
37
43
  if (err) {
38
44
  logger_1.logger.error("Error sending events:", err);
@@ -2,5 +2,6 @@ export * from "./event";
2
2
  export * from "./logger";
3
3
  export * from "./queue";
4
4
  export * from "./storage";
5
+ export * from "./consent";
5
6
  export { default as fetch } from "./fetch";
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -22,6 +22,7 @@ __exportStar(require("./event"), exports);
22
22
  __exportStar(require("./logger"), exports);
23
23
  __exportStar(require("./queue"), exports);
24
24
  __exportStar(require("./storage"), exports);
25
+ __exportStar(require("./consent"), exports);
25
26
  var fetch_1 = require("./fetch");
26
27
  Object.defineProperty(exports, "fetch", { enumerable: true, get: function () { return __importDefault(fetch_1).default; } });
27
28
  //# sourceMappingURL=index.js.map
@@ -1,12 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  var constant_1 = require("../constant");
4
+ var hash_1 = require("../../../utils/hash");
4
5
  var StorageBlueprint = /** @class */ (function () {
5
6
  function StorageBlueprint(writeKey) {
6
7
  this.writeKey = writeKey;
7
8
  }
8
9
  StorageBlueprint.prototype.getKey = function (key) {
9
- return "".concat(constant_1.KEY_PREFIX, "_").concat(this.writeKey, ".").concat(key);
10
+ return "".concat(constant_1.KEY_PREFIX, "_").concat((0, hash_1.secureHash)(this.writeKey), "_").concat(key);
10
11
  };
11
12
  return StorageBlueprint;
12
13
  }());
@@ -1,2 +1,2 @@
1
- export declare const version = "1.19.7";
1
+ export declare const version = "1.20.0";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
4
  // Generated by genversion.
5
- exports.version = '1.19.7';
5
+ exports.version = '1.20.0';
6
6
  //# sourceMappingURL=version.js.map
@@ -48,6 +48,9 @@ export interface IFormoAnalytics {
48
48
  rdns?: string;
49
49
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
50
50
  track(event: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
51
+ optOutTracking(): void;
52
+ optInTracking(): void;
53
+ hasOptedOutTracking(): boolean;
51
54
  }
52
55
  export interface Config {
53
56
  writeKey: string;
@@ -18,5 +18,12 @@ export declare const isValidAddress: (address: Address | null | undefined) => ad
18
18
  * This function is the preferred way to get a validated, trimmed address for use in your application.
19
19
  */
20
20
  export declare const getValidAddress: (address: Address | null | undefined) => string | null;
21
+ /**
22
+ * Checks if an address is in the blocked list and should not emit events.
23
+ * Blocked addresses include the zero address and dead address which are not normal user addresses.
24
+ * @param address The address to check
25
+ * @returns true if the address is blocked, false otherwise
26
+ */
27
+ export declare const isBlockedAddress: (address: Address | null | undefined) => boolean;
21
28
  export declare const toChecksumAddress: (address: Address) => string;
22
29
  //# sourceMappingURL=address.d.ts.map
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toChecksumAddress = exports.getValidAddress = exports.isValidAddress = void 0;
3
+ exports.toChecksumAddress = exports.isBlockedAddress = exports.getValidAddress = exports.isValidAddress = void 0;
4
4
  var keccak_js_1 = require("ethereum-cryptography/keccak.js");
5
5
  var utils_js_1 = require("ethereum-cryptography/utils.js");
6
6
  var validators_1 = require("../validators");
7
7
  var object_1 = require("../validators/object");
8
+ var constants_1 = require("../constants");
8
9
  /**
9
10
  * Private helper function to validate and trim an address
10
11
  * @param address The address to validate and trim
@@ -41,6 +42,25 @@ var getValidAddress = function (address) {
41
42
  return _validateAndTrimAddress(address);
42
43
  };
43
44
  exports.getValidAddress = getValidAddress;
45
+ /**
46
+ * Checks if an address is in the blocked list and should not emit events.
47
+ * Blocked addresses include the zero address and dead address which are not normal user addresses.
48
+ * @param address The address to check
49
+ * @returns true if the address is blocked, false otherwise
50
+ */
51
+ var isBlockedAddress = function (address) {
52
+ if (!address)
53
+ return false;
54
+ var validAddress = (0, exports.getValidAddress)(address);
55
+ if (!validAddress)
56
+ return false;
57
+ // Normalize to checksum format for comparison
58
+ var checksumAddress = (0, exports.toChecksumAddress)(validAddress);
59
+ return constants_1.BLOCKED_ADDRESSES.some(function (blockedAddr) {
60
+ return (0, exports.toChecksumAddress)(blockedAddr) === checksumAddress;
61
+ });
62
+ };
63
+ exports.isBlockedAddress = isBlockedAddress;
44
64
  var toChecksumAddress = function (address) {
45
65
  if (!(0, validators_1.isAddress)(address, false)) {
46
66
  throw new Error("Invalid address " + address);
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Hash utilities using ethereum-cryptography for secure, deterministic hashing
3
+ */
4
+ /**
5
+ * Generate a secure hash of a string using SHA-256 for creating short, consistent identifiers
6
+ * @param str - The string to hash
7
+ * @returns Short hash string (first 8 characters of SHA-256 hex)
8
+ */
9
+ export declare function secureHash(str: string): string;
10
+ //# sourceMappingURL=hash.d.ts.map
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ /**
3
+ * Hash utilities using ethereum-cryptography for secure, deterministic hashing
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.secureHash = secureHash;
7
+ var sha256_1 = require("ethereum-cryptography/sha256");
8
+ var utils_1 = require("ethereum-cryptography/utils");
9
+ /**
10
+ * Generate a secure hash of a string using SHA-256 for creating short, consistent identifiers
11
+ * @param str - The string to hash
12
+ * @returns Short hash string (first 8 characters of SHA-256 hex)
13
+ */
14
+ function secureHash(str) {
15
+ var bytes = (0, utils_1.utf8ToBytes)(str);
16
+ var hashBytes = (0, sha256_1.sha256)(bytes);
17
+ var hashHex = (0, utils_1.bytesToHex)(hashBytes);
18
+ // Return first 8 characters for reasonable cookie name length
19
+ return hashHex.slice(0, 8);
20
+ }
21
+ //# sourceMappingURL=hash.js.map
@@ -2,4 +2,5 @@ export * from "./address";
2
2
  export * from "./base";
3
3
  export * from "./converter";
4
4
  export * from "./generate";
5
+ export * from "./hash";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -18,4 +18,5 @@ __exportStar(require("./address"), exports);
18
18
  __exportStar(require("./base"), exports);
19
19
  __exportStar(require("./converter"), exports);
20
20
  __exportStar(require("./generate"), exports);
21
+ __exportStar(require("./hash"), exports);
21
22
  //# sourceMappingURL=index.js.map
@@ -177,6 +177,22 @@ export declare class FormoAnalytics implements IFormoAnalytics {
177
177
  * @returns {Promise<void>}
178
178
  */
179
179
  track(event: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
180
+ /**
181
+ * Opt out of tracking.
182
+ * @returns {void}
183
+ */
184
+ optOutTracking(): void;
185
+ /**
186
+ * Opt back into tracking after previously opting out. This will re-enable analytics tracking
187
+ * and switch back to persistent storage.
188
+ * @returns {void}
189
+ */
190
+ optInTracking(): void;
191
+ /**
192
+ * Check if the user has opted out of tracking.
193
+ * @returns {boolean} True if the user has opted out
194
+ */
195
+ hasOptedOutTracking(): boolean;
180
196
  private trackProvider;
181
197
  private trackProviders;
182
198
  private addProviderListener;
@@ -204,7 +220,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
204
220
  private trackPageHit;
205
221
  private trackEvent;
206
222
  /**
207
- * Determines if tracking should be enabled based on configuration
223
+ * Determines if tracking should be enabled based on configuration and consent
208
224
  * @returns {boolean} True if tracking should be enabled
209
225
  */
210
226
  private shouldTrack;