@formo/analytics 1.39.0 → 1.40.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 (44) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +4 -1
  2. package/dist/cjs/src/FormoAnalytics.js +47 -32
  3. package/dist/cjs/src/event/EventFactory.d.ts +9 -1
  4. package/dist/cjs/src/event/EventFactory.js +31 -16
  5. package/dist/cjs/src/event/EventManager.d.ts +3 -1
  6. package/dist/cjs/src/event/EventManager.js +33 -4
  7. package/dist/cjs/src/event/cancellation.d.ts +4 -0
  8. package/dist/cjs/src/event/cancellation.js +7 -0
  9. package/dist/cjs/src/event/type.d.ts +1 -0
  10. package/dist/cjs/src/event/utils.d.ts +5 -1
  11. package/dist/cjs/src/event/utils.js +57 -2
  12. package/dist/cjs/src/evm/EvmEventTracker.d.ts +7 -0
  13. package/dist/cjs/src/evm/EvmEventTracker.js +31 -5
  14. package/dist/cjs/src/queue/EventQueue.d.ts +1 -0
  15. package/dist/cjs/src/queue/EventQueue.js +11 -6
  16. package/dist/cjs/src/storage/StorageManager.d.ts +2 -0
  17. package/dist/cjs/src/storage/StorageManager.js +6 -0
  18. package/dist/cjs/src/storage/index.d.ts +1 -0
  19. package/dist/cjs/src/storage/index.js +7 -1
  20. package/dist/cjs/src/version.d.ts +1 -1
  21. package/dist/cjs/src/version.js +1 -1
  22. package/dist/esm/src/FormoAnalytics.d.ts +4 -1
  23. package/dist/esm/src/FormoAnalytics.js +45 -30
  24. package/dist/esm/src/event/EventFactory.d.ts +9 -1
  25. package/dist/esm/src/event/EventFactory.js +31 -16
  26. package/dist/esm/src/event/EventManager.d.ts +3 -1
  27. package/dist/esm/src/event/EventManager.js +33 -4
  28. package/dist/esm/src/event/cancellation.d.ts +4 -0
  29. package/dist/esm/src/event/cancellation.js +4 -0
  30. package/dist/esm/src/event/type.d.ts +1 -0
  31. package/dist/esm/src/event/utils.d.ts +5 -1
  32. package/dist/esm/src/event/utils.js +56 -3
  33. package/dist/esm/src/evm/EvmEventTracker.d.ts +7 -0
  34. package/dist/esm/src/evm/EvmEventTracker.js +31 -5
  35. package/dist/esm/src/queue/EventQueue.d.ts +1 -0
  36. package/dist/esm/src/queue/EventQueue.js +11 -6
  37. package/dist/esm/src/storage/StorageManager.d.ts +2 -0
  38. package/dist/esm/src/storage/StorageManager.js +6 -0
  39. package/dist/esm/src/storage/index.d.ts +1 -0
  40. package/dist/esm/src/storage/index.js +5 -0
  41. package/dist/esm/src/version.d.ts +1 -1
  42. package/dist/esm/src/version.js +1 -1
  43. package/dist/index.umd.min.js +1 -1
  44. package/package.json +2 -2
@@ -54,6 +54,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
54
54
  private _onPopStateListener?;
55
55
  private _onLocationChangeListener?;
56
56
  private _pageHooksDisposed;
57
+ private _pageGeneration;
57
58
  config: Config;
58
59
  /**
59
60
  * The wallet later events are attributed to, derived from whichever
@@ -87,7 +88,8 @@ export declare class FormoAnalytics implements IFormoAnalytics {
87
88
  */
88
89
  page(category?: string, name?: string, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
89
90
  /**
90
- * Reset the current user session.
91
+ * Reset user and wallet state while preserving the browser's anonymous id.
92
+ * Use `optOutTracking()` to clear the anonymous id and the attribution.
91
93
  * @returns {void}
92
94
  */
93
95
  reset(): void;
@@ -142,6 +144,7 @@ export declare class FormoAnalytics implements IFormoAnalytics {
142
144
  chainId?: ChainID;
143
145
  address?: Address;
144
146
  }): void;
147
+ private retryWalletDetection;
145
148
  /** @see WalletStateStore.clearProvider */
146
149
  private clearActiveProvider;
147
150
  /** @see WalletStateStore.backfill */
@@ -52,6 +52,7 @@ var constants_1 = require("./constants");
52
52
  var storage_1 = require("./storage");
53
53
  var cookiePolicy_1 = require("./storage/cookiePolicy");
54
54
  var event_1 = require("./event");
55
+ var utils_1 = require("./event/utils");
55
56
  var queue_1 = require("./queue");
56
57
  var logger_1 = require("./logger");
57
58
  var consent_1 = require("./consent");
@@ -68,7 +69,7 @@ var wagmi_1 = require("./wagmi");
68
69
  var solana_1 = require("./solana");
69
70
  var SolanaManager_1 = require("./solana/SolanaManager");
70
71
  // Internal: the Privy identify is reached through identify(user), not exported.
71
- var utils_1 = require("./privy/utils");
72
+ var utils_2 = require("./privy/utils");
72
73
  /**
73
74
  * Constants for provider switching reasons
74
75
  */
@@ -93,6 +94,7 @@ var FormoAnalytics = /** @class */ (function () {
93
94
  /** In-memory URL used to deduplicate SPA pageview events. */
94
95
  this._currentUrl = "";
95
96
  this._pageHooksDisposed = false;
97
+ this._pageGeneration = 0;
96
98
  this.currentUserId = "";
97
99
  /**
98
100
  * Clean up resources and event listeners
@@ -190,6 +192,7 @@ var FormoAnalytics = /** @class */ (function () {
190
192
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
191
193
  isTrackingSuppressed: function () { return _this.isTrackingSuppressed(); },
192
194
  willTrackEvent: function (chainId) { return _this.willTrackEvent(chainId); },
195
+ retryDetection: function () { return _this.retryWalletDetection(); },
193
196
  isWagmiMode: function () { return _this.isWagmiMode; },
194
197
  connect: function (params, properties) { return _this.connect(params, properties); },
195
198
  disconnect: function (params) { return _this.disconnect(params); },
@@ -218,7 +221,7 @@ var FormoAnalytics = /** @class */ (function () {
218
221
  // ever sent once the user has opted out, even via a timer or
219
222
  // pagehide flush scheduled before opt-out.
220
223
  canSend: function () { return !_this.hasOptedOutTracking(); },
221
- }), options);
224
+ }), options, function () { return !_this.hasOptedOutTracking(); });
222
225
  // Check consent status on initialization
223
226
  if (this.hasOptedOutTracking()) {
224
227
  logger_1.logger.info("User has previously opted out of tracking");
@@ -320,7 +323,7 @@ var FormoAnalytics = /** @class */ (function () {
320
323
  });
321
324
  FormoAnalytics.init = function (writeKey, options) {
322
325
  return __awaiter(this, void 0, void 0, function () {
323
- var analytics, discovered;
326
+ var analytics, discovered, error_1;
324
327
  return __generator(this, function (_a) {
325
328
  switch (_a.label) {
326
329
  case 0:
@@ -330,18 +333,21 @@ var FormoAnalytics = /** @class */ (function () {
330
333
  logger_1.logger.info("FormoAnalytics: Skipping provider detection (EVM disabled)");
331
334
  return [3 /*break*/, 5];
332
335
  case 1:
333
- if (!!analytics.isWagmiMode) return [3 /*break*/, 4];
336
+ _a.trys.push([1, 4, , 5]);
334
337
  return [4 /*yield*/, analytics.evmEvents.getProviders()];
335
338
  case 2:
336
339
  discovered = _a.sent();
337
340
  return [4 /*yield*/, analytics.evmEvents.detectWallets(discovered)];
338
341
  case 3:
339
342
  _a.sent();
340
- analytics.evmEvents.trackProviders(discovered);
343
+ if (!analytics.isWagmiMode) {
344
+ analytics.evmEvents.trackProviders(discovered);
345
+ }
341
346
  return [3 /*break*/, 5];
342
347
  case 4:
343
- logger_1.logger.info("FormoAnalytics: Skipping provider detection (Wagmi mode)");
344
- _a.label = 5;
348
+ error_1 = _a.sent();
349
+ logger_1.logger.warn("FormoAnalytics: Provider discovery failed", error_1);
350
+ return [3 /*break*/, 5];
345
351
  case 5: return [2 /*return*/, analytics];
346
352
  }
347
353
  });
@@ -372,7 +378,8 @@ var FormoAnalytics = /** @class */ (function () {
372
378
  });
373
379
  };
374
380
  /**
375
- * Reset the current user session.
381
+ * Reset user and wallet state while preserving the browser's anonymous id.
382
+ * Use `optOutTracking()` to clear the anonymous id and the attribution.
376
383
  * @returns {void}
377
384
  */
378
385
  FormoAnalytics.prototype.reset = function () {
@@ -383,15 +390,11 @@ var FormoAnalytics = /** @class */ (function () {
383
390
  // page lifetime, because they fall back to currentAddress. Keep the
384
391
  // EVM provider reference so tracking can resume on the next connect.
385
392
  this.wallet.reset();
386
- (0, storage_1.cookie)().remove(constants_1.LOCAL_ANONYMOUS_ID_KEY);
387
393
  (0, storage_1.cookie)().remove(constants_1.SESSION_USER_ID_KEY);
388
394
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_DETECTED_KEY);
389
395
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_IDENTIFIED_KEY);
390
396
  (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
391
- // Stored traffic-source attribution (referrer/UTM) is tracking data;
392
- // clear it too so reset()/optOutTracking() don't leave it to be
393
- // re-attached to the next session's events.
394
- (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
397
+ // Attribution belongs to the visit, so reset preserves it.
395
398
  };
396
399
  FormoAnalytics.prototype.cleanup = function () {
397
400
  this.isCleanedUp = true;
@@ -575,6 +578,12 @@ var FormoAnalytics = /** @class */ (function () {
575
578
  */
576
579
  FormoAnalytics.prototype.syncWalletState = function (params) {
577
580
  this.wallet.syncWalletState(params);
581
+ this.retryWalletDetection();
582
+ };
583
+ FormoAnalytics.prototype.retryWalletDetection = function () {
584
+ if (this.isCleanedUp)
585
+ return;
586
+ void this.evmEvents.detectWallets(this.evmEvents.detectableProviders());
578
587
  };
579
588
  /** @see WalletStateStore.clearProvider */
580
589
  FormoAnalytics.prototype.clearActiveProvider = function () {
@@ -742,7 +751,7 @@ var FormoAnalytics = /** @class */ (function () {
742
751
  // namespace before emitting. It reads this.currentAddress itself to
743
752
  // preserve an already-connected wallet, so this dispatch is a thin
744
753
  // pass-through and both entry points behave identically.
745
- return [4 /*yield*/, (0, utils_1.identifyPrivyUser)(this, maybeUser, {
754
+ return [4 /*yield*/, (0, utils_2.identifyPrivyUser)(this, maybeUser, {
746
755
  activeAddress: opts.activeAddress,
747
756
  properties: opts.properties,
748
757
  })];
@@ -774,6 +783,11 @@ var FormoAnalytics = /** @class */ (function () {
774
783
  return [2 /*return*/];
775
784
  }
776
785
  if (!!params) return [3 /*break*/, 12];
786
+ // Wagmi owns wallet identification.
787
+ if (this.isWagmiMode) {
788
+ logger_1.logger.info("identify() without params is a no-op in Wagmi mode");
789
+ return [2 /*return*/];
790
+ }
777
791
  // If no params provided, auto-identify
778
792
  logger_1.logger.info("Auto-identifying with providers:", this.evm.all.map(function (p) { return p.info.name; }));
779
793
  _i = 0, _a = this.evm.all;
@@ -953,10 +967,8 @@ var FormoAnalytics = /** @class */ (function () {
953
967
  return __generator(this, function (_c) {
954
968
  switch (_c.label) {
955
969
  case 0:
956
- // detect() marks wallet detection (a cookie write) before
957
- // trackEvent's consent check - gate it for a suppressed visitor or
958
- // excluded environment (opt-out / timezone / host / path).
959
- if (this.isTrackingSuppressed()) {
970
+ // Apply all policy checks before persisting the detection marker.
971
+ if (!this.shouldTrack()) {
960
972
  logger_1.logger.info("detect() skipped: tracking is suppressed for this visitor or environment");
961
973
  return [2 /*return*/];
962
974
  }
@@ -1018,6 +1030,7 @@ var FormoAnalytics = /** @class */ (function () {
1018
1030
  // Set opt-out flag in persistent storage using direct cookie access
1019
1031
  // This must be done before switching storage to ensure persistence
1020
1032
  (0, consent_1.setConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY, "true");
1033
+ this._pageGeneration++;
1021
1034
  // Drop anything already buffered so a pending timer/pagehide flush
1022
1035
  // cannot ship events after consent withdrawal.
1023
1036
  this.eventManager.clear();
@@ -1025,6 +1038,9 @@ var FormoAnalytics = /** @class */ (function () {
1025
1038
  // on opt-in, and nothing else would retry an already-adopted one.
1026
1039
  this.evmEvents.markRegisteredAdoptionsPending();
1027
1040
  this.reset();
1041
+ // Consent withdrawal also clears the browser id and the attribution.
1042
+ (0, utils_1.clearAnonymousId)(constants_1.LOCAL_ANONYMOUS_ID_KEY);
1043
+ (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
1028
1044
  logger_1.logger.info("Successfully opted out of tracking");
1029
1045
  };
1030
1046
  /**
@@ -1051,11 +1067,9 @@ var FormoAnalytics = /** @class */ (function () {
1051
1067
  logger_1.logger.info("Opting back into tracking");
1052
1068
  // Remove opt-out flag
1053
1069
  (0, consent_1.removeConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY);
1054
- // A wallet connected while opted out was declined by syncWalletState, and
1055
- // an unchanged wagmi connection produces no status or chain update to
1056
- // retry on. Without this, opting back in leaves that wallet invisible for
1057
- // the rest of the page load.
1070
+ // Retry wallet adoption skipped while opted out.
1058
1071
  (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1072
+ this.retryWalletDetection();
1059
1073
  logger_1.logger.info("Successfully opted back into tracking");
1060
1074
  };
1061
1075
  /**
@@ -1136,29 +1150,30 @@ var FormoAnalytics = /** @class */ (function () {
1136
1150
  };
1137
1151
  FormoAnalytics.prototype.trackPageHit = function (category, name, properties, context, callback) {
1138
1152
  return __awaiter(this, void 0, void 0, function () {
1153
+ var canTrack, generation;
1139
1154
  var _this = this;
1140
1155
  return __generator(this, function (_a) {
1141
- // A route change can end path-based suppression; a provider registered
1142
- // while suppressed gets its refused session adoption retried here.
1143
- // Idempotent and cheap when nothing is pending.
1144
- if (!this.isCleanedUp) {
1156
+ canTrack = this.shouldTrack();
1157
+ if (!this.isCleanedUp && canTrack) {
1145
1158
  try {
1146
1159
  this.evmEvents.retryExternalAdoptions();
1160
+ this.retryWalletDetection();
1147
1161
  }
1148
1162
  catch (_b) {
1149
- /* never let the retry break a page hit */
1163
+ // Detection retries must not break page tracking.
1150
1164
  }
1151
1165
  }
1152
- if (!this.shouldTrack()) {
1166
+ if (!canTrack) {
1153
1167
  logger_1.logger.info("Track page hit: Skipping event due to tracking configuration");
1154
1168
  return [2 /*return*/];
1155
1169
  }
1170
+ generation = this._pageGeneration;
1156
1171
  setTimeout(function () {
1157
1172
  // Drop in-flight page hits from an SDK instance that was torn down
1158
1173
  // between scheduling and firing (e.g. provider remount in React Strict
1159
1174
  // Mode / HMR). Otherwise the orphan instance would queue a page event
1160
1175
  // here with its stale, never-populated `currentAddress`.
1161
- if (_this._pageHooksDisposed)
1176
+ if (_this._pageHooksDisposed || generation !== _this._pageGeneration)
1162
1177
  return;
1163
1178
  (function () { return __awaiter(_this, void 0, void 0, function () {
1164
1179
  var e_2;
@@ -1188,7 +1203,7 @@ var FormoAnalytics = /** @class */ (function () {
1188
1203
  };
1189
1204
  FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1190
1205
  return __awaiter(this, void 0, void 0, function () {
1191
- var error_1;
1206
+ var error_2;
1192
1207
  return __generator(this, function (_a) {
1193
1208
  switch (_a.label) {
1194
1209
  case 0:
@@ -1207,8 +1222,8 @@ var FormoAnalytics = /** @class */ (function () {
1207
1222
  _a.sent();
1208
1223
  return [3 /*break*/, 3];
1209
1224
  case 2:
1210
- error_1 = _a.sent();
1211
- logger_1.logger.error("Error tracking event:", error_1);
1225
+ error_2 = _a.sent();
1226
+ logger_1.logger.error("Error tracking event:", error_2);
1212
1227
  return [3 /*break*/, 3];
1213
1228
  case 3: return [2 /*return*/];
1214
1229
  }
@@ -1,10 +1,16 @@
1
1
  import { Address, APIEvent, ChainID, IFormoEvent, IFormoEventContext, IFormoEventProperties, Nullable, Options, SignatureStatus, TransactionStatus } from "../types";
2
2
  import { IEventFactory } from "./type";
3
3
  declare class EventFactory implements IEventFactory {
4
+ /** Consulted once per event, after the only await in creation. */
5
+ private readonly canCreate;
4
6
  private options?;
5
7
  private compiledPathPattern?;
6
8
  private excludedQueryParams;
7
- constructor(options?: Options);
9
+ /** Bumped by invalidate(); an event created across a bump is dropped. */
10
+ private generation;
11
+ constructor(options?: Options,
12
+ /** Consulted once per event, after the only await in creation. */
13
+ canCreate?: () => boolean);
8
14
  /**
9
15
  * Validate an address for both EVM and Solana chains.
10
16
  * Uses chainId for strict validation when available.
@@ -56,6 +62,8 @@ declare class EventFactory implements IEventFactory {
56
62
  private redactStoredTrafficSources;
57
63
  private getTrafficSources;
58
64
  private getScreen;
65
+ /** Drop every event still being created. Called on clear() and close(). */
66
+ invalidate(): void;
59
67
  private generateContext;
60
68
  /**
61
69
  * Add any missing default page properties using values from options and defaults
@@ -73,11 +73,18 @@ var constants_2 = require("./constants");
73
73
  var sanitize_1 = require("./sanitize");
74
74
  var utils_2 = require("./utils");
75
75
  var browsers_1 = require("../browser/browsers");
76
+ var cancellation_1 = require("./cancellation");
76
77
  var ISO_3166_ALPHA_2_REGEX = /^[A-Z]{2}$/;
77
78
  var EventFactory = /** @class */ (function () {
78
- function EventFactory(options) {
79
+ function EventFactory(options,
80
+ /** Consulted once per event, after the only await in creation. */
81
+ canCreate) {
82
+ if (canCreate === void 0) { canCreate = function () { return true; }; }
79
83
  var _this = this;
80
84
  var _a, _b;
85
+ this.canCreate = canCreate;
86
+ /** Bumped by invalidate(); an event created across a bump is dropped. */
87
+ this.generation = 0;
81
88
  this.extractUTMParameters = function (url) {
82
89
  var result = {
83
90
  utm_campaign: "",
@@ -410,6 +417,10 @@ var EventFactory = /** @class */ (function () {
410
417
  return safeDefaults;
411
418
  }
412
419
  };
420
+ /** Drop every event still being created. Called on clear() and close(). */
421
+ EventFactory.prototype.invalidate = function () {
422
+ this.generation++;
423
+ };
413
424
  // Contextual fields that are automatically collected and populated by the Formo SDK
414
425
  EventFactory.prototype.generateContext = function (context) {
415
426
  return __awaiter(this, void 0, void 0, function () {
@@ -433,24 +444,28 @@ var EventFactory = /** @class */ (function () {
433
444
  };
434
445
  EventFactory.prototype.getEnrichedEvent = function (formoEvent, context) {
435
446
  return __awaiter(this, void 0, void 0, function () {
436
- var commonEventData, eventChainId, validAddress, processedEvent;
437
- var _a;
438
- var _b, _c;
439
- return __generator(this, function (_d) {
440
- switch (_d.label) {
447
+ var generation, enrichedContext, commonEventData, eventChainId, validAddress, processedEvent;
448
+ var _a, _b;
449
+ return __generator(this, function (_c) {
450
+ switch (_c.label) {
441
451
  case 0:
442
- _a = {};
452
+ generation = this.generation;
443
453
  return [4 /*yield*/, this.generateContext(context)];
444
454
  case 1:
445
- commonEventData = (_a.context = _d.sent(),
446
- _a.original_timestamp = (0, timestamp_1.getCurrentTimeFormatted)(),
447
- _a.user_id = formoEvent.user_id,
448
- _a.type = formoEvent.type,
449
- _a.channel = constants_2.CHANNEL,
450
- _a.version = constants_2.VERSION,
451
- _a);
452
- commonEventData.anonymous_id = (0, utils_2.generateAnonymousId)(constants_1.LOCAL_ANONYMOUS_ID_KEY, (_b = this.options) === null || _b === void 0 ? void 0 : _b.crossSubdomainCookies);
453
- eventChainId = (_c = formoEvent.properties) === null || _c === void 0 ? void 0 : _c.chainId;
455
+ enrichedContext = _c.sent();
456
+ if (generation !== this.generation || !this.canCreate()) {
457
+ throw cancellation_1.EVENT_CREATION_CANCELLED;
458
+ }
459
+ commonEventData = {
460
+ context: enrichedContext,
461
+ original_timestamp: (0, timestamp_1.getCurrentTimeFormatted)(),
462
+ user_id: formoEvent.user_id,
463
+ type: formoEvent.type,
464
+ channel: constants_2.CHANNEL,
465
+ version: constants_2.VERSION,
466
+ };
467
+ commonEventData.anonymous_id = (0, utils_2.generateAnonymousId)(constants_1.LOCAL_ANONYMOUS_ID_KEY, (_a = this.options) === null || _a === void 0 ? void 0 : _a.crossSubdomainCookies);
468
+ eventChainId = (_b = formoEvent.properties) === null || _b === void 0 ? void 0 : _b.chainId;
454
469
  validAddress = this.validateEventAddress(formoEvent.address, eventChainId);
455
470
  commonEventData.address = validAddress;
456
471
  processedEvent = (0, mergeDeepRight_1.default)(formoEvent, commonEventData);
@@ -5,14 +5,16 @@ import { IEventFactory, IEventManager } from "./type";
5
5
  * A service to generate valid event payloads and queue them for processing
6
6
  */
7
7
  declare class EventManager implements IEventManager {
8
+ private readonly canAcceptEvent;
8
9
  eventQueue: IEventQueue;
9
10
  eventFactory: IEventFactory;
11
+ private generation;
10
12
  /**
11
13
  *
12
14
  * @param eventQueue Event queue instance
13
15
  * @param options Optional configuration (referral parsing, etc.)
14
16
  */
15
- constructor(eventQueue: IEventQueue, options?: Options);
17
+ constructor(eventQueue: IEventQueue, options?: Options, canAcceptEvent?: () => boolean);
16
18
  /**
17
19
  * Consumes a new incoming event
18
20
  * @param event Incoming event data
@@ -50,6 +50,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.EventManager = void 0;
51
51
  var logger_1 = require("../logger");
52
52
  var EventFactory_1 = require("./EventFactory");
53
+ var cancellation_1 = require("./cancellation");
53
54
  var address_1 = require("../utils/address");
54
55
  /**
55
56
  * A service to generate valid event payloads and queue them for processing
@@ -60,9 +61,13 @@ var EventManager = /** @class */ (function () {
60
61
  * @param eventQueue Event queue instance
61
62
  * @param options Optional configuration (referral parsing, etc.)
62
63
  */
63
- function EventManager(eventQueue, options) {
64
+ function EventManager(eventQueue, options, canAcceptEvent) {
65
+ if (canAcceptEvent === void 0) { canAcceptEvent = function () { return true; }; }
66
+ var _this = this;
67
+ this.canAcceptEvent = canAcceptEvent;
68
+ this.generation = 0;
64
69
  this.eventQueue = eventQueue;
65
- this.eventFactory = new EventFactory_1.EventFactory(options);
70
+ this.eventFactory = new EventFactory_1.EventFactory(options, function () { return _this.canAcceptEvent(); });
66
71
  }
67
72
  /**
68
73
  * Consumes a new incoming event
@@ -70,14 +75,34 @@ var EventManager = /** @class */ (function () {
70
75
  */
71
76
  EventManager.prototype.addEvent = function (event, address, userId) {
72
77
  return __awaiter(this, void 0, void 0, function () {
73
- var callback, _event, formoEvent;
78
+ var callback, _event, generation, shouldContinue, formoEvent, error_1;
79
+ var _this = this;
74
80
  return __generator(this, function (_a) {
75
81
  switch (_a.label) {
76
82
  case 0:
77
83
  callback = event.callback, _event = __rest(event, ["callback"]);
78
- return [4 /*yield*/, this.eventFactory.create(_event, address, userId)];
84
+ generation = this.generation;
85
+ shouldContinue = function () {
86
+ return generation === _this.generation && _this.canAcceptEvent();
87
+ };
88
+ if (!shouldContinue())
89
+ return [2 /*return*/];
90
+ _a.label = 1;
79
91
  case 1:
92
+ _a.trys.push([1, 3, , 4]);
93
+ return [4 /*yield*/, this.eventFactory.create(_event, address, userId)];
94
+ case 2:
80
95
  formoEvent = _a.sent();
96
+ return [3 /*break*/, 4];
97
+ case 3:
98
+ error_1 = _a.sent();
99
+ if (error_1 === cancellation_1.EVENT_CREATION_CANCELLED)
100
+ return [2 /*return*/];
101
+ throw error_1;
102
+ case 4:
103
+ // Reject work invalidated while enrichment was pending.
104
+ if (!shouldContinue())
105
+ return [2 /*return*/];
81
106
  // Check if the final event has a blocked address - don't queue it
82
107
  if (formoEvent.address && (0, address_1.isBlockedAddress)(formoEvent.address)) {
83
108
  logger_1.logger.warn("Event blocked: Address ".concat(formoEvent.address, " is in the blocked list and cannot emit events"));
@@ -98,10 +123,14 @@ var EventManager = /** @class */ (function () {
98
123
  };
99
124
  /** Drop any buffered events (consent withdrawal). Recoverable. */
100
125
  EventManager.prototype.clear = function () {
126
+ this.generation++;
127
+ this.eventFactory.invalidate();
101
128
  this.eventQueue.clear();
102
129
  };
103
130
  /** Terminal shutdown on teardown: nothing can be sent after this. */
104
131
  EventManager.prototype.close = function () {
132
+ this.generation++;
133
+ this.eventFactory.invalidate();
105
134
  this.eventQueue.close();
106
135
  };
107
136
  return EventManager;
@@ -0,0 +1,4 @@
1
+ /** Internal control flow: an event invalidated by consent withdrawal. */
2
+ declare const EVENT_CREATION_CANCELLED: unique symbol;
3
+ export { EVENT_CREATION_CANCELLED };
4
+ //# sourceMappingURL=cancellation.d.ts.map
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EVENT_CREATION_CANCELLED = void 0;
4
+ /** Internal control flow: an event invalidated by consent withdrawal. */
5
+ var EVENT_CREATION_CANCELLED = Symbol();
6
+ exports.EVENT_CREATION_CANCELLED = EVENT_CREATION_CANCELLED;
7
+ //# sourceMappingURL=cancellation.js.map
@@ -5,6 +5,7 @@ export interface IEventManager {
5
5
  close(): void;
6
6
  }
7
7
  export interface IEventFactory {
8
+ invalidate(): void;
8
9
  create(event: APIEvent, address?: Address, userId?: string): Promise<IFormoEvent>;
9
10
  }
10
11
  //# sourceMappingURL=type.d.ts.map
@@ -1,4 +1,8 @@
1
1
  import { AnonymousID } from "../types";
2
2
  declare const generateAnonymousId: (key: string, crossSubdomainCookies?: boolean) => AnonymousID;
3
- export { generateAnonymousId };
3
+ /** Clear the anonymous id from every storage layer. */
4
+ declare const clearAnonymousId: (key: string) => void;
5
+ /** Test hook: forget the page-lifetime memory, as a new page load would. */
6
+ declare const __resetAnonymousIdMemory: () => void;
7
+ export { generateAnonymousId, clearAnonymousId, __resetAnonymousIdMemory };
4
8
  //# sourceMappingURL=utils.d.ts.map
@@ -11,15 +11,28 @@ var __assign = (this && this.__assign) || function () {
11
11
  return __assign.apply(this, arguments);
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.generateAnonymousId = void 0;
14
+ exports.__resetAnonymousIdMemory = exports.clearAnonymousId = exports.generateAnonymousId = void 0;
15
15
  var utils_1 = require("../utils");
16
16
  var storage_1 = require("../storage");
17
17
  var cookiePolicy_1 = require("../storage/cookiePolicy");
18
+ /** Page-lifetime fallback when persistent storage is unavailable. */
19
+ var volatileAnonymousId;
20
+ /** Persistent fallback when cookies are unavailable. */
21
+ var readLocal = function (key) {
22
+ try {
23
+ var value = (0, storage_1.local)().get(key);
24
+ return typeof value === "string" && value ? value : undefined;
25
+ }
26
+ catch (_a) {
27
+ return undefined;
28
+ }
29
+ };
18
30
  var generateAnonymousId = function (key, crossSubdomainCookies) {
31
+ var _a, _b;
19
32
  var storedAnonymousId = (0, storage_1.cookie)().get(key);
20
33
  var anonymousId = (storedAnonymousId && typeof storedAnonymousId === "string"
21
34
  ? storedAnonymousId
22
- : (0, utils_1.generateNativeUUID)());
35
+ : (_b = (_a = readLocal(key)) !== null && _a !== void 0 ? _a : volatileAnonymousId) !== null && _b !== void 0 ? _b : (0, utils_1.generateNativeUUID)());
23
36
  var domain = (0, cookiePolicy_1.getIdentityCookieDomain)(crossSubdomainCookies);
24
37
  // Re-set the cookie with the configured scope. When crossSubdomainCookies
25
38
  // is true, this migrates legacy host-only cookies on the current host to the apex
@@ -27,7 +40,49 @@ var generateAnonymousId = function (key, crossSubdomainCookies) {
27
40
  // example.com is not visible from app.example.com) cannot be migrated
28
41
  // until the user revisits that host.
29
42
  (0, storage_1.cookie)().set(key, anonymousId, __assign(__assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
43
+ // Retry host-only when a domain-scoped write is rejected.
44
+ if (domain && (0, storage_1.cookie)().get(key) !== anonymousId) {
45
+ (0, storage_1.cookie)().set(key, anonymousId, __assign({ expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
46
+ }
47
+ if ((0, storage_1.cookie)().get(key) === anonymousId) {
48
+ // Prefer the cookie and discard the fallback copy.
49
+ volatileAnonymousId = undefined;
50
+ if ((0, storage_1.usesCookieStorage)() && readLocal(key)) {
51
+ try {
52
+ (0, storage_1.local)().remove(key);
53
+ }
54
+ catch (_c) {
55
+ // Storage is unavailable.
56
+ }
57
+ }
58
+ return anonymousId;
59
+ }
60
+ // Persist across embedded page loads when cookies are blocked.
61
+ try {
62
+ (0, storage_1.local)().set(key, anonymousId);
63
+ }
64
+ catch (_d) {
65
+ // Memory remains the last fallback.
66
+ }
67
+ volatileAnonymousId = anonymousId;
30
68
  return anonymousId;
31
69
  };
32
70
  exports.generateAnonymousId = generateAnonymousId;
71
+ /** Clear the anonymous id from every storage layer. */
72
+ var clearAnonymousId = function (key) {
73
+ (0, storage_1.cookie)().remove(key);
74
+ try {
75
+ (0, storage_1.local)().remove(key);
76
+ }
77
+ catch (_a) {
78
+ // Storage is unavailable.
79
+ }
80
+ volatileAnonymousId = undefined;
81
+ };
82
+ exports.clearAnonymousId = clearAnonymousId;
83
+ /** Test hook: forget the page-lifetime memory, as a new page load would. */
84
+ var __resetAnonymousIdMemory = function () {
85
+ volatileAnonymousId = undefined;
86
+ };
87
+ exports.__resetAnonymousIdMemory = __resetAnonymousIdMemory;
33
88
  //# sourceMappingURL=utils.js.map
@@ -11,6 +11,8 @@ export interface EvmEventTrackerDeps {
11
11
  isTrackingSuppressed(): boolean;
12
12
  /** Whether an event on this chain would actually be sent. */
13
13
  willTrackEvent(chainId?: ChainID): boolean;
14
+ /** Retry detect events after the active chain changes. */
15
+ retryDetection(): void;
14
16
  /** In wagmi mode the SDK does not wrap providers itself. */
15
17
  isWagmiMode(): boolean;
16
18
  /** Emission. The tracker decides WHEN; the SDK owns the event API. */
@@ -152,6 +154,8 @@ export declare class EvmEventTracker {
152
154
  * had already replaced.
153
155
  */
154
156
  private unsubscribeDiscovery?;
157
+ /** Discovery store retained for complete listener cleanup. */
158
+ private discoveryStore?;
155
159
  constructor(wallet: WalletStateStore, registry: EvmProviderRegistry, deps: EvmEventTrackerDeps);
156
160
  /** Stop listening for wallet announcements. Called from SDK teardown. */
157
161
  cleanup(): void;
@@ -259,8 +263,11 @@ export declare class EvmEventTracker {
259
263
  private registerConnectListener;
260
264
  private registerDisconnectListener;
261
265
  private onConnected;
266
+ /** Whether Formo owns provider lifecycle tracking. */
267
+ private tracksDiscovered;
262
268
  getProviders(): Promise<readonly EIP6963ProviderDetail[]>;
263
269
  detectWallets(providers: readonly EIP6963ProviderDetail[]): Promise<void>;
270
+ detectableProviders(): readonly EIP6963ProviderDetail[];
264
271
  /**
265
272
  * Seed a provider's chain from whatever it already exposes synchronously.
266
273
  *