@formo/analytics 1.35.2 → 1.36.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/dist/cjs/src/FormoAnalytics.d.ts +79 -363
  2. package/dist/cjs/src/FormoAnalytics.js +368 -2234
  3. package/dist/cjs/src/event/EventManager.d.ts +3 -1
  4. package/dist/cjs/src/event/EventManager.js +5 -1
  5. package/dist/cjs/src/event/type.d.ts +1 -0
  6. package/dist/cjs/src/evm/EvmEventTracker.d.ts +175 -0
  7. package/dist/cjs/src/evm/EvmEventTracker.js +1030 -0
  8. package/dist/cjs/src/evm/EvmProviderRegistry.d.ts +132 -0
  9. package/dist/cjs/src/evm/EvmProviderRegistry.js +348 -0
  10. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +120 -0
  11. package/dist/cjs/src/evm/EvmRequestTracker.js +756 -0
  12. package/dist/cjs/src/queue/EventQueue.d.ts +28 -0
  13. package/dist/cjs/src/queue/EventQueue.js +97 -19
  14. package/dist/cjs/src/queue/type.d.ts +1 -0
  15. package/dist/cjs/src/tracking/TrackingPolicy.d.ts +146 -0
  16. package/dist/cjs/src/tracking/TrackingPolicy.js +200 -0
  17. package/dist/cjs/src/validators/address.d.ts +1 -1
  18. package/dist/cjs/src/version.d.ts +1 -1
  19. package/dist/cjs/src/version.js +1 -1
  20. package/dist/cjs/src/wallet/WalletStateStore.d.ts +223 -0
  21. package/dist/cjs/src/wallet/WalletStateStore.js +515 -0
  22. package/dist/esm/src/FormoAnalytics.d.ts +79 -363
  23. package/dist/esm/src/FormoAnalytics.js +369 -2235
  24. package/dist/esm/src/event/EventManager.d.ts +3 -1
  25. package/dist/esm/src/event/EventManager.js +5 -1
  26. package/dist/esm/src/event/type.d.ts +1 -0
  27. package/dist/esm/src/evm/EvmEventTracker.d.ts +175 -0
  28. package/dist/esm/src/evm/EvmEventTracker.js +1027 -0
  29. package/dist/esm/src/evm/EvmProviderRegistry.d.ts +132 -0
  30. package/dist/esm/src/evm/EvmProviderRegistry.js +345 -0
  31. package/dist/esm/src/evm/EvmRequestTracker.d.ts +120 -0
  32. package/dist/esm/src/evm/EvmRequestTracker.js +753 -0
  33. package/dist/esm/src/queue/EventQueue.d.ts +28 -0
  34. package/dist/esm/src/queue/EventQueue.js +97 -19
  35. package/dist/esm/src/queue/type.d.ts +1 -0
  36. package/dist/esm/src/tracking/TrackingPolicy.d.ts +146 -0
  37. package/dist/esm/src/tracking/TrackingPolicy.js +197 -0
  38. package/dist/esm/src/version.d.ts +1 -1
  39. package/dist/esm/src/version.js +1 -1
  40. package/dist/esm/src/wallet/WalletStateStore.d.ts +223 -0
  41. package/dist/esm/src/wallet/WalletStateStore.js +512 -0
  42. package/dist/index.umd.min.js +1 -1
  43. package/package.json +5 -4
@@ -46,18 +46,8 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
46
46
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
47
  }
48
48
  };
49
- var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
50
- if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
51
- if (ar || !(i in from)) {
52
- if (!ar) ar = Array.prototype.slice.call(from, 0, i);
53
- ar[i] = from[i];
54
- }
55
- }
56
- return to.concat(ar || Array.prototype.slice.call(from));
57
- };
58
49
  Object.defineProperty(exports, "__esModule", { value: true });
59
50
  exports.FormoAnalytics = void 0;
60
- var mipd_1 = require("mipd");
61
51
  var constants_1 = require("./constants");
62
52
  var storage_1 = require("./storage");
63
53
  var cookiePolicy_1 = require("./storage/cookiePolicy");
@@ -65,12 +55,13 @@ var event_1 = require("./event");
65
55
  var queue_1 = require("./queue");
66
56
  var logger_1 = require("./logger");
67
57
  var consent_1 = require("./consent");
68
- var provider_1 = require("./provider");
69
58
  var session_1 = require("./session");
70
- var types_1 = require("./types");
71
59
  var address_1 = require("./utils/address");
72
- var timezone_1 = require("./utils/timezone");
73
- var validators_1 = require("./validators");
60
+ var TrackingPolicy_1 = require("./tracking/TrackingPolicy");
61
+ var WalletStateStore_1 = require("./wallet/WalletStateStore");
62
+ var EvmProviderRegistry_1 = require("./evm/EvmProviderRegistry");
63
+ var EvmEventTracker_1 = require("./evm/EvmEventTracker");
64
+ var EvmRequestTracker_1 = require("./evm/EvmRequestTracker");
74
65
  var chain_1 = require("./utils/chain");
75
66
  var wagmi_1 = require("./wagmi");
76
67
  var solana_1 = require("./solana");
@@ -80,11 +71,6 @@ var utils_1 = require("./privy/utils");
80
71
  /**
81
72
  * Constants for provider switching reasons
82
73
  */
83
- var PROVIDER_SWITCH_REASONS = {
84
- ADDRESS_MISMATCH: "Address mismatch indicates wallet switch",
85
- NO_ACCOUNTS: "Current provider has no accounts",
86
- CHECK_FAILED: "Could not check current provider accounts",
87
- };
88
74
  var FormoAnalytics = /** @class */ (function () {
89
75
  function FormoAnalytics(writeKey, options) {
90
76
  if (options === void 0) { options = {}; }
@@ -92,63 +78,7 @@ var FormoAnalytics = /** @class */ (function () {
92
78
  var _a, _b, _c;
93
79
  this.writeKey = writeKey;
94
80
  this.options = options;
95
- // Per-chain namespace state - isolates EVM and Solana connection state
96
- this._chainState = {
97
- evm: {},
98
- solana: {},
99
- };
100
- /**
101
- * Last known chain per tracked provider, fed by `chainChanged` / `connect`
102
- * and by a one-off probe at tracking time. Read synchronously when labelling
103
- * an autocaptured event, so the wallet's request transport is never used for
104
- * analytics. Weak so an untracked provider can be collected.
105
- */
106
- /**
107
- * The connect this SDK has actually REPORTED for a provider.
108
- *
109
- * Deduplicating on "is an address known" was wrong in both directions.
110
- * `onConnected` and `onAccountsChanged` both observe one connection, so
111
- * something has to stop them double-reporting - but an address can be
112
- * present without a connect ever having been sent: restored from the
113
- * active-wallet cookie, or reported with an unresolved chain and then
114
- * refused by `tracking.excludeChains`. Suppressing on address presence lost
115
- * the connect entirely in those cases.
116
- *
117
- * Records what was reported, so a later event can be recognised as a
118
- * genuine improvement on it rather than a duplicate.
119
- */
120
81
  this._announcedConnect = new WeakMap();
121
- this._providerChainIds = new WeakMap();
122
- /**
123
- * Bumped on every chain observation, PER PROVIDER. An `eth_chainId` answer
124
- * that resolves after a newer observation for the same provider must not
125
- * overwrite it.
126
- *
127
- * Deliberately per provider, not per SDK instance: a global counter meant
128
- * any activity on wallet B discarded a perfectly valid in-flight answer for
129
- * wallet A, leaving A at chain 0 - and, since an unresolved chain fails
130
- * closed, dropping all of A's events whenever `excludeChains` is set.
131
- */
132
- this._providerChainGenerations = new WeakMap();
133
- this._providerListenersMap = new Map();
134
- /**
135
- * EIP-6963 provider details discovered through the browser
136
- * This array contains all available providers with their metadata
137
- */
138
- this._providers = [];
139
- /**
140
- * Set of providers that have been tracked with event listeners
141
- * This is separate from _providers because:
142
- * - _providers contains all discovered providers (EIP-6963)
143
- * - _trackedProviders contains only providers that have been set up with listeners
144
- * - A provider can be discovered but not yet tracked (e.g., during initialization)
145
- * - A provider can be tracked but later removed from discovery
146
- */
147
- this._trackedProviders = new Set();
148
- // Flag to prevent concurrent processing of accountsChanged events
149
- this._processingAccountsChanged = false;
150
- // Set to efficiently track seen providers for deduplication and O(1) lookup
151
- this._seenProviders = new Set();
152
82
  /**
153
83
  * Flag indicating if Wagmi mode is enabled
154
84
  * When true, EIP-1193 provider wrapping is skipped
@@ -201,6 +131,68 @@ var FormoAnalytics = /** @class */ (function () {
201
131
  enabled: ((_b = options.logger) === null || _b === void 0 ? void 0 : _b.enabled) || false,
202
132
  enabledLevels: ((_c = options.logger) === null || _c === void 0 ? void 0 : _c.levels) || [],
203
133
  });
134
+ this.evm = new EvmProviderRegistry_1.EvmProviderRegistry({
135
+ activeProvider: function () { return _this.wallet.provider; },
136
+ activeChainId: function () { return _this.wallet.evmChainId; },
137
+ knownEvmAddress: function () { return _this.wallet.evmAddress; },
138
+ // Whether central state should follow a provider's chain report is the
139
+ // SDK's call, not the registry's: it depends on which namespace is
140
+ // active. Recording it per provider is unconditional; syncing is not.
141
+ onChainObserved: function (provider, chainId) {
142
+ // Guarded on EVM already being active. `set()` makes whichever
143
+ // namespace it touches active, so syncing unconditionally let a
144
+ // background EVM wallet's chain report steal the slot from a live
145
+ // Solana wallet, and every later page or track event was attributed
146
+ // to the wrong wallet entirely.
147
+ if (provider === _this.wallet.provider &&
148
+ _this.wallet.activeNamespace === "evm" &&
149
+ _this.wallet.evmChainId !== chainId) {
150
+ _this.wallet.set("evm", { chainId: chainId });
151
+ }
152
+ },
153
+ });
154
+ this.wallet = new WalletStateStore_1.WalletStateStore({
155
+ isPersistedIdentityPurgeRequired: function () {
156
+ return _this.trackingPolicy.isPersistedIdentityPurgeRequired();
157
+ },
158
+ isPageExcluded: function () { return _this.trackingPolicy.isPageExcluded(); },
159
+ isTrackingSuppressed: function () { return _this.trackingPolicy.isTrackingSuppressed(); },
160
+ crossSubdomainCookies: function () { return _this.crossSubdomainCookies; },
161
+ providerChainId: function (provider) { return _this.evm.chainIdOf(provider); },
162
+ // A provider that stops being active has, from this SDK's point of
163
+ // view, ended its connection, so the connect it reported must stop
164
+ // counting. Otherwise toggling between two installed wallets silently
165
+ // loses every connect after the first.
166
+ onProviderDisplaced: function (previous) {
167
+ return _this.evmEvents.forgetAnnouncedConnect(previous);
168
+ },
169
+ });
170
+ this.evmRequests = new EvmRequestTracker_1.EvmRequestTracker(this.wallet, this.evm, {
171
+ isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
172
+ signature: function (params, properties) { return _this.signature(params, properties); },
173
+ transaction: function (params, properties) { return _this.transaction(params, properties); },
174
+ });
175
+ this.evmEvents = new EvmEventTracker_1.EvmEventTracker(this.wallet, this.evm, {
176
+ isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
177
+ isTrackingSuppressed: function () { return _this.isTrackingSuppressed(); },
178
+ willTrackEvent: function (chainId) { return _this.willTrackEvent(chainId); },
179
+ isWagmiMode: function () { return _this.isWagmiMode; },
180
+ connect: function (params, properties) { return _this.connect(params, properties); },
181
+ disconnect: function (params) { return _this.disconnect(params); },
182
+ chain: function (params, properties) { return _this.chain(params, properties); },
183
+ detect: function (params) { return _this.detect(params); },
184
+ registerRequestListeners: function (provider) {
185
+ return _this.evmRequests.registerRequestListeners(provider);
186
+ },
187
+ });
188
+ this.trackingPolicy = new TrackingPolicy_1.TrackingPolicy({
189
+ // All three read lazily. `options` is public and mutable, and the
190
+ // central chain moves as wallets connect and switch, so the policy
191
+ // must see both at decision time rather than at construction.
192
+ options: function () { return _this.options; },
193
+ hasOptedOut: function () { return _this.hasOptedOutTracking(); },
194
+ currentChainId: function () { return _this.currentChainId; },
195
+ });
204
196
  this.eventManager = new event_1.EventManager(new queue_1.EventQueue(this.config.writeKey, {
205
197
  apiHost: options.apiHost || constants_1.EVENTS_API_HOST,
206
198
  flushAt: options.flushAt,
@@ -236,7 +228,7 @@ var FormoAnalytics = /** @class */ (function () {
236
228
  provider = window.ethereum;
237
229
  }
238
230
  if (provider) {
239
- this.trackEIP1193Provider(provider);
231
+ this.evmEvents.trackEIP1193Provider(provider);
240
232
  }
241
233
  }
242
234
  // Initialize Solana manager if Solana options are provided
@@ -247,77 +239,73 @@ var FormoAnalytics = /** @class */ (function () {
247
239
  // Seed currentAddress/currentChainId from the persisted snapshot before
248
240
  // the first page hit queues so reload-time track()/page() carry the
249
241
  // wallet even before wagmi/EIP-1193 reconnection completes.
250
- this.loadActiveWallet();
242
+ this.wallet.load();
251
243
  this.trackPageHit();
252
244
  this.trackPageHits();
253
245
  }
254
246
  Object.defineProperty(FormoAnalytics.prototype, "_provider", {
255
- // EVM state accessors - EVM listener paths must use these instead of
256
- // currentAddress/currentChainId to avoid cross-namespace reads.
257
247
  get: function () {
258
- return this._chainState.evm.provider;
248
+ return this.wallet.provider;
259
249
  },
260
250
  set: function (value) {
261
- var previous = this._chainState.evm.provider;
262
- // A provider that stops being the active one has, from this SDK's point of
263
- // view, ended its connection - so the connect it reported must stop
264
- // counting. Otherwise toggling between two installed wallets silently
265
- // loses every connect after the first: A's stale record suppresses the
266
- // connect when the user comes back to it.
267
- //
268
- // Done in the setter rather than at each switch site because the active
269
- // provider is reassigned from several paths - `accountsChanged`,
270
- // `chainChanged`, `connect`, `handleProviderMismatch`, `untrackProvider` -
271
- // and any one of them missed would reopen the same hole.
272
- this.forgetConnectRecord(previous, value);
273
- this._chainState.evm.provider = value;
251
+ this.wallet.provider = value;
274
252
  },
275
253
  enumerable: false,
276
254
  configurable: true
277
255
  });
278
- /**
279
- * Drop a provider's reported-connect record when it stops being active.
280
- *
281
- * The active provider is displaced from several places, and not all of them
282
- * go through the `_provider` setter: `setChainState()` writes
283
- * `_chainState.evm.provider` directly and `clearChainState()` replaces the
284
- * whole namespace. Guarding only the setter left A's record alive across a
285
- * real A-to-B switch, so returning to A was suppressed.
286
- */
287
- FormoAnalytics.prototype.forgetConnectRecord = function (previous, next) {
288
- if (previous && previous !== next) {
289
- this._announcedConnect.delete(previous);
290
- }
291
- };
292
256
  Object.defineProperty(FormoAnalytics.prototype, "_evmAddress", {
293
257
  get: function () {
294
- return this._chainState.evm.address;
258
+ return this.wallet.evmAddress;
295
259
  },
296
260
  enumerable: false,
297
261
  configurable: true
298
262
  });
299
263
  Object.defineProperty(FormoAnalytics.prototype, "_evmChainId", {
300
264
  get: function () {
301
- return this._chainState.evm.chainId;
265
+ return this.wallet.evmChainId;
266
+ },
267
+ enumerable: false,
268
+ configurable: true
269
+ });
270
+ Object.defineProperty(FormoAnalytics.prototype, "currentAddress", {
271
+ /**
272
+ * The wallet later events are attributed to, derived from whichever
273
+ * namespace last claimed the slot. Read by the wagmi and Privy
274
+ * integrations; the store is the only writer.
275
+ */
276
+ get: function () {
277
+ return this.wallet.address;
278
+ },
279
+ /**
280
+ * These stay writable.
281
+ *
282
+ * Both were public mutable fields, so a consumer assigning one is using a
283
+ * documented public API. Getter-only replacements would break that
284
+ * silently: TypeScript rejects the assignment, and plain JavaScript throws
285
+ * on an accessor with no setter. The writes forward into the store, which
286
+ * keeps a single owner without turning a refactor into a breaking change.
287
+ */
288
+ set: function (value) {
289
+ this.wallet.setActiveAddress(value);
290
+ },
291
+ enumerable: false,
292
+ configurable: true
293
+ });
294
+ Object.defineProperty(FormoAnalytics.prototype, "currentChainId", {
295
+ get: function () {
296
+ return this.wallet.chainId;
297
+ },
298
+ set: function (value) {
299
+ this.wallet.setActiveChainId(value);
302
300
  },
303
301
  enumerable: false,
304
302
  configurable: true
305
303
  });
306
- /**
307
- * Helper method to check if a provider is different from the currently active one
308
- * @param provider The provider to check
309
- * @returns true if there's a provider mismatch, false otherwise
310
- */
311
- FormoAnalytics.prototype.isProviderMismatch = function (provider) {
312
- // Only consider it a mismatch if we have an active provider AND the provider is different
313
- // This allows legitimate provider switching while preventing race conditions
314
- return this._provider != null && this._provider !== provider;
315
- };
316
304
  FormoAnalytics.init = function (writeKey, options) {
317
305
  return __awaiter(this, void 0, void 0, function () {
318
- var analytics, _a;
319
- return __generator(this, function (_b) {
320
- switch (_b.label) {
306
+ var analytics, discovered;
307
+ return __generator(this, function (_a) {
308
+ switch (_a.label) {
321
309
  case 0:
322
310
  (0, storage_1.initStorageManager)(writeKey);
323
311
  analytics = new FormoAnalytics(writeKey, options);
@@ -326,20 +314,17 @@ var FormoAnalytics = /** @class */ (function () {
326
314
  return [3 /*break*/, 5];
327
315
  case 1:
328
316
  if (!!analytics.isWagmiMode) return [3 /*break*/, 4];
329
- // Auto-detect wallet provider
330
- _a = analytics;
331
- return [4 /*yield*/, analytics.getProviders()];
317
+ return [4 /*yield*/, analytics.evmEvents.getProviders()];
332
318
  case 2:
333
- // Auto-detect wallet provider
334
- _a._providers = _b.sent();
335
- return [4 /*yield*/, analytics.detectWallets(analytics._providers)];
319
+ discovered = _a.sent();
320
+ return [4 /*yield*/, analytics.evmEvents.detectWallets(discovered)];
336
321
  case 3:
337
- _b.sent();
338
- analytics.trackProviders(analytics._providers);
322
+ _a.sent();
323
+ analytics.evmEvents.trackProviders(discovered);
339
324
  return [3 /*break*/, 5];
340
325
  case 4:
341
326
  logger_1.logger.info("FormoAnalytics: Skipping provider detection (Wagmi mode)");
342
- _b.label = 5;
327
+ _a.label = 5;
343
328
  case 5: return [2 /*return*/, analytics];
344
329
  }
345
330
  });
@@ -380,11 +365,7 @@ var FormoAnalytics = /** @class */ (function () {
380
365
  // address on subsequent track()/page() events for the rest of the
381
366
  // page lifetime, because they fall back to currentAddress. Keep the
382
367
  // EVM provider reference so tracking can resume on the next connect.
383
- this.currentAddress = undefined;
384
- this.currentChainId = undefined;
385
- var evmProvider = this._chainState.evm.provider;
386
- this._chainState = { evm: { provider: evmProvider }, solana: {} };
387
- this._activeNamespace = undefined;
368
+ this.wallet.reset();
388
369
  (0, storage_1.cookie)().remove(constants_1.LOCAL_ANONYMOUS_ID_KEY);
389
370
  (0, storage_1.cookie)().remove(constants_1.SESSION_USER_ID_KEY);
390
371
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_DETECTED_KEY);
@@ -402,8 +383,19 @@ var FormoAnalytics = /** @class */ (function () {
402
383
  */
403
384
  FormoAnalytics.prototype.cleanup = function () {
404
385
  logger_1.logger.debug("FormoAnalytics: Cleaning up resources");
405
- // Drop buffered events so a torn-down instance can't flush later.
406
- this.eventManager.clear();
386
+ // Close the queue, don't just empty it. clear() only drops what is
387
+ // buffered at this instant; asynchronous work already in flight (event
388
+ // creation is async on every emit path) would still enqueue afterwards,
389
+ // and an empty queue flushes immediately. close() is terminal, so a
390
+ // continuation that outlives this instance cannot send with its stale
391
+ // options. See issue #339.
392
+ this.eventManager.close();
393
+ // Stop any receipt or batch-status polling: a torn-down instance must
394
+ // not keep asking a wallet about transactions nobody is listening for.
395
+ this.evmRequests.cleanup();
396
+ // Stop reacting to wallet announcements: a disposed instance must not
397
+ // wrap new providers or emit detect events.
398
+ this.evmEvents.cleanup();
407
399
  // Clean up Wagmi handler if present
408
400
  if (this.wagmiHandler) {
409
401
  this.wagmiHandler.cleanup();
@@ -416,9 +408,9 @@ var FormoAnalytics = /** @class */ (function () {
416
408
  }
417
409
  // Clean up EIP-1193 providers if not in Wagmi mode
418
410
  if (!this.isWagmiMode) {
419
- for (var _i = 0, _a = Array.from(this._trackedProviders); _i < _a.length; _i++) {
411
+ for (var _i = 0, _a = this.evm.trackedProviders(); _i < _a.length; _i++) {
420
412
  var provider = _a[_i];
421
- this.untrackProvider(provider);
413
+ this.evmEvents.untrackProvider(provider);
422
414
  }
423
415
  }
424
416
  // Tear down page-hit hooks: remove window listeners and silence the
@@ -475,6 +467,11 @@ var FormoAnalytics = /** @class */ (function () {
475
467
  logger_1.logger.info("connect() skipped: tracking is suppressed for this visitor or environment");
476
468
  return [2 /*return*/];
477
469
  }
470
+ // A new wallet now owns this namespace. Anything already tearing down
471
+ // the previous session must not undo this. An emitted connect always
472
+ // counts, even when the address is the one already there: a wallet that
473
+ // disconnects and reconnects leaves identical state.
474
+ this.wallet.observe(this.wallet.namespaceOf(chainId));
478
475
  this.setChainState(chainId, { address: validAddress });
479
476
  return [4 /*yield*/, this.trackEvent(constants_1.EventType.CONNECT, {
480
477
  chainId: chainId,
@@ -498,7 +495,7 @@ var FormoAnalytics = /** @class */ (function () {
498
495
  */
499
496
  FormoAnalytics.prototype.disconnect = function (params, properties, context, callback) {
500
497
  return __awaiter(this, void 0, void 0, function () {
501
- var chainId, address, isSolana, providerInfo, disconnectProperties;
498
+ var chainId, address, isSolana, providerInfo, disconnectProperties, namespace, before;
502
499
  return __generator(this, function (_a) {
503
500
  switch (_a.label) {
504
501
  case 0:
@@ -518,9 +515,25 @@ var FormoAnalytics = /** @class */ (function () {
518
515
  providerName: providerInfo.name,
519
516
  rdns: providerInfo.rdns,
520
517
  })), properties);
518
+ namespace = this.wallet.namespaceOf(chainId);
519
+ // Every teardown announces itself here, before anything is awaited,
520
+ // whichever path reached it - a provider event or a direct call from the
521
+ // host app. A connect observation that began earlier is invalidated; one
522
+ // that begins after this point is a genuine reconnect and still reports.
523
+ this.wallet.beginDisconnect(namespace);
524
+ before = this.wallet.snapshot(namespace);
521
525
  return [4 /*yield*/, this.trackEvent(constants_1.EventType.DISCONNECT, __assign(__assign({}, (chainId && { chainId: chainId })), (address && { address: address })), disconnectProperties, context, callback)];
522
526
  case 1:
523
527
  _a.sent();
528
+ // If a wallet claimed this namespace while the event was being built, a
529
+ // new session owns it and this cleanup is stale. Running it anyway would
530
+ // wipe that session's state AND its reported-connect record, so a later
531
+ // wallet signal would emit a second connect for a connection that is
532
+ // already reported. See issue #344.
533
+ if (!this.wallet.isUnchangedSince(namespace, before)) {
534
+ logger_1.logger.info("Disconnect: A new session claimed this namespace while the event was in flight; leaving its state intact");
535
+ return [2 /*return*/];
536
+ }
524
537
  // Clear the disconnecting chain's namespace state.
525
538
  // Per-chain isolation ensures a Solana disconnect never wipes EVM state (and vice versa).
526
539
  this.clearChainState(chainId);
@@ -530,6 +543,78 @@ var FormoAnalytics = /** @class */ (function () {
530
543
  });
531
544
  });
532
545
  };
546
+ /** @see WalletStateStore.set */
547
+ FormoAnalytics.prototype.setChainState = function (namespaceOrChainId, update) {
548
+ this.wallet.set(namespaceOrChainId, update);
549
+ };
550
+ /** @see WalletStateStore.clear */
551
+ FormoAnalytics.prototype.clearChainState = function (namespaceOrChainId) {
552
+ this.wallet.clear(namespaceOrChainId);
553
+ };
554
+ /**
555
+ * Record validated wallet/chain state WITHOUT emitting an event.
556
+ *
557
+ * Integrations must call this on every connect / chain change / disconnect,
558
+ * even when the matching autocapture event is disabled, or the exclusion
559
+ * gate (which keys off the central chain, not the event payload) can be
560
+ * bypassed. Stays on the class because integrations bind to it.
561
+ * @see WalletStateStore.syncWalletState
562
+ */
563
+ FormoAnalytics.prototype.syncWalletState = function (params) {
564
+ this.wallet.syncWalletState(params);
565
+ };
566
+ /** @see WalletStateStore.clearProvider */
567
+ FormoAnalytics.prototype.clearActiveProvider = function () {
568
+ this.wallet.clearProvider();
569
+ };
570
+ /** @see WalletStateStore.backfill */
571
+ FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId, provider) {
572
+ this.wallet.backfill(address, chainId, provider);
573
+ };
574
+ /** @see WalletStateStore.clearStaleEvmWalletOnSwitchWhileSuppressed */
575
+ FormoAnalytics.prototype.clearStaleEvmWalletOnSwitchWhileSuppressed = function (address) {
576
+ this.wallet.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
577
+ };
578
+ /** @see EvmProviderRegistry.addListener */
579
+ FormoAnalytics.prototype.addProviderListener = function (provider, event, listener) {
580
+ this.evm.addListener(provider, event, listener);
581
+ };
582
+ /** @see EvmProviderRegistry.removeListeners */
583
+ FormoAnalytics.prototype.removeProviderListeners = function (provider) {
584
+ this.evm.removeListeners(provider);
585
+ };
586
+ /** @see EvmProviderRegistry.infoFor */
587
+ FormoAnalytics.prototype.getProviderInfo = function (provider) {
588
+ return this.evm.infoFor(provider);
589
+ };
590
+ /** @see EvmProviderRegistry.isWrapped */
591
+ FormoAnalytics.prototype.isProviderAlreadyWrapped = function (provider, currentRequest) {
592
+ return this.evm.isWrapped(provider, currentRequest);
593
+ };
594
+ /** @see EvmProviderRegistry.resolveChainId */
595
+ FormoAnalytics.prototype.resolveChainIdForProvider = function (provider) {
596
+ return this.evm.resolveChainId(provider);
597
+ };
598
+ /** @see EvmProviderRegistry.rememberChain */
599
+ FormoAnalytics.prototype.rememberProviderChain = function (provider, chainId) {
600
+ this.evm.rememberChain(provider, chainId);
601
+ };
602
+ /** @see EvmProviderRegistry.addressOf */
603
+ FormoAnalytics.prototype.getAddress = function (provider) {
604
+ return __awaiter(this, void 0, void 0, function () {
605
+ return __generator(this, function (_a) {
606
+ return [2 /*return*/, this.evm.addressOf(provider)];
607
+ });
608
+ });
609
+ };
610
+ /** @see EvmProviderRegistry.accountsOf */
611
+ FormoAnalytics.prototype.getAccounts = function (provider) {
612
+ return __awaiter(this, void 0, void 0, function () {
613
+ return __generator(this, function (_a) {
614
+ return [2 /*return*/, this.evm.accountsOf(provider)];
615
+ });
616
+ });
617
+ };
533
618
  /**
534
619
  * Emits a chain network change event.
535
620
  * @param {ChainID} params.chainId
@@ -671,14 +756,14 @@ var FormoAnalytics = /** @class */ (function () {
671
756
  // valid (activating a Solana wallet while an excluded EVM chain id is
672
757
  // still current). By the time the Privy path reaches this guard it has
673
758
  // already reconciled, so each inner identify is judged on the right chain.
674
- if (this.isTrackingSuppressed() || this.isCurrentChainExcluded()) {
759
+ if (this.isTrackingSuppressed() || this.trackingPolicy.isChainExcluded()) {
675
760
  logger_1.logger.info("identify() skipped: tracking is suppressed for this visitor, environment, or chain");
676
761
  return [2 /*return*/];
677
762
  }
678
763
  if (!!params) return [3 /*break*/, 12];
679
764
  // If no params provided, auto-identify
680
- logger_1.logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
681
- _i = 0, _a = this._providers;
765
+ logger_1.logger.info("Auto-identifying with providers:", this.evm.all.map(function (p) { return p.info.name; }));
766
+ _i = 0, _a = this.evm.all;
682
767
  _d.label = 3;
683
768
  case 3:
684
769
  if (!(_i < _a.length)) return [3 /*break*/, 11];
@@ -750,8 +835,7 @@ var FormoAnalytics = /** @class */ (function () {
750
835
  // attribution. Gating address and userId together prevents leaving the
751
836
  // active address paired with a different wallet's user id.
752
837
  if (setActive !== false) {
753
- this.currentAddress = validAddress;
754
- this.persistActiveWallet();
838
+ this.wallet.setActiveAddress(validAddress);
755
839
  if (userId) {
756
840
  this.currentUserId = userId;
757
841
  domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
@@ -838,8 +922,7 @@ var FormoAnalytics = /** @class */ (function () {
838
922
  return;
839
923
  var currentIsSolana = (0, solana_1.isSolanaChainId)(this.currentChainId);
840
924
  if (walletIsSolana !== currentIsSolana) {
841
- this.currentChainId = undefined;
842
- this.persistActiveWallet();
925
+ this.wallet.setActiveChainId(undefined);
843
926
  }
844
927
  };
845
928
  /**
@@ -955,1256 +1038,161 @@ var FormoAnalytics = /** @class */ (function () {
955
1038
  /*
956
1039
  SDK tracking and event listener functions
957
1040
  */
1041
+ FormoAnalytics.prototype.onLocationChange = function () {
1042
+ return __awaiter(this, void 0, void 0, function () {
1043
+ var _a;
1044
+ return __generator(this, function (_b) {
1045
+ if (this._currentUrl !== window.location.href) {
1046
+ this._currentUrl = window.location.href;
1047
+ // Host/path exclusions are evaluated per navigation, so a SPA can leave
1048
+ // an excluded route and become trackable without any wallet event
1049
+ // firing. The wagmi handler only observes *changes*, so an unchanged
1050
+ // connection it was forced to decline earlier would stay invisible for
1051
+ // the rest of the page load. Give it a chance to adopt it now.
1052
+ (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1053
+ this.trackPageHit();
1054
+ }
1055
+ return [2 /*return*/];
1056
+ });
1057
+ });
1058
+ };
1059
+ FormoAnalytics.prototype.trackPageHits = function () {
1060
+ var _this = this;
1061
+ // Install a single, instance-agnostic wrapper around history.pushState /
1062
+ // replaceState so concurrent SDK instances (React Strict Mode, HMR) don't
1063
+ // each stack their own wrapper - which would dispatch N synthetic events
1064
+ // per navigation and produce O(N^2) onLocationChange calls. The wrapper
1065
+ // dispatches once; per-instance bookkeeping is done by per-instance
1066
+ // listeners that each register/unregister themselves.
1067
+ FormoAnalytics.installHistoryHooksOnce();
1068
+ this._onPopStateListener = function () { return _this.onLocationChange(); };
1069
+ this._onLocationChangeListener = function () { return _this.onLocationChange(); };
1070
+ window.addEventListener("popstate", this._onPopStateListener);
1071
+ window.addEventListener("locationchange", this._onLocationChangeListener);
1072
+ };
958
1073
  /**
959
- * Track an EIP-1193 provider by wrapping its request method and adding event listeners
960
- * Note: This is only used in non-Wagmi mode. When Wagmi is enabled, all tracking
961
- * happens through Wagmi's connector system instead of EIP-1193/EIP-6963.
962
- * @param provider The EIP-1193 provider to track
1074
+ * Wrap history.pushState / replaceState exactly once per `history` object,
1075
+ * regardless of how many SDK instances are constructed. Uses a Symbol
1076
+ * marker so we recognize our own wrapper across module reloads in HMR.
963
1077
  */
964
- FormoAnalytics.prototype.trackEIP1193Provider = function (provider) {
965
- logger_1.logger.info("trackEIP1193Provider", provider);
966
- // Defensive check: Skip provider tracking in Wagmi mode
967
- // This should never be called in Wagmi mode due to guards in init(),
968
- // but we check here for safety in case of future code changes
969
- if (this.isWagmiMode) {
970
- logger_1.logger.debug("trackEIP1193Provider: Skipping EIP-1193 provider tracking (Wagmi mode - using connector system instead)");
1078
+ FormoAnalytics.installHistoryHooksOnce = function () {
1079
+ if (typeof history === "undefined" || typeof window === "undefined")
971
1080
  return;
972
- }
973
- try {
974
- // Validate provider exists and has required methods
975
- if (!(0, provider_1.isValidProvider)(provider)) {
976
- logger_1.logger.warn("trackEIP1193Provider: Invalid provider - missing required methods");
977
- return;
978
- }
979
- if (this._trackedProviders.has(provider)) {
980
- logger_1.logger.warn("trackEIP1193Provider: Provider already tracked");
981
- return;
982
- }
983
- // CRITICAL: Always register accountsChanged for state management
984
- // This ensures currentAddress, currentChainId, and _provider are always up-to-date
985
- // Event emission is controlled conditionally inside the handlers
986
- this.registerAccountsChangedListener(provider);
987
- // `chainChanged` and `connect` are registered UNCONDITIONALLY: they are
988
- // how this provider's chain is observed, and every signature and
989
- // transaction has to be labelled with it. Gating registration on
990
- // `autocapture.chain` conflated observing a chain with reporting one, so
991
- // `{ chain: false, signature: true }` left the chain frozen at whatever
992
- // was first seen - a switch to an excluded chain went unnoticed and its
993
- // signatures were emitted under the old, allowed chain. Whether an
994
- // event is emitted is decided inside each handler.
995
- this.registerChainChangedListener(provider);
996
- if (this.isAutocaptureEnabled("connect")) {
997
- this.registerConnectListener(provider);
998
- }
999
- else {
1000
- // Observation only. The full connect handler calls `getAddress()`,
1001
- // which issues `eth_accounts`, and nothing analytics-only may go on
1002
- // the wallet's transport - a stalled request there sits in front of
1003
- // the next signature the dapp makes. The chain rides along on the
1004
- // event itself, so it costs nothing to record.
1005
- this.registerConnectChainObserver(provider);
1006
- }
1007
- // Seed the chain from the provider's own synchronous state if it exposes
1008
- // one. Deliberately a property read and never an RPC: see
1009
- // resolveChainIdForProvider for why nothing analytics-only may go on the
1010
- // wallet's transport.
1011
- this.seedProviderChainFromState(provider);
1012
- if (this.isAutocaptureEnabled("signature") || this.isAutocaptureEnabled("transaction")) {
1013
- this.registerRequestListeners(provider);
1014
- }
1015
- else {
1016
- logger_1.logger.debug("TrackProvider: Skipping request wrapping (both signature and transaction autocapture disabled)");
1017
- }
1018
- // Registered UNCONDITIONALLY: this listener also ends the provider's
1019
- // reported-connect record, and with `{ connect: true, disconnect: false }`
1020
- // a wallet that disconnected and reconnected would otherwise find its
1021
- // old record still standing and have the new connect suppressed. Whether
1022
- // a disconnect EVENT is emitted is decided inside the handler.
1023
- {
1024
- this.registerDisconnectListener(provider);
1025
- }
1026
- // Only add to tracked providers after all listeners are successfully registered
1027
- this._trackedProviders.add(provider);
1028
- }
1029
- catch (error) {
1030
- logger_1.logger.error("Error tracking provider:", error);
1031
- }
1032
- };
1033
- FormoAnalytics.prototype.trackProviders = function (providers) {
1034
- try {
1035
- for (var _i = 0, providers_1 = providers; _i < providers_1.length; _i++) {
1036
- var eip6963ProviderDetail = providers_1[_i];
1037
- var provider = eip6963ProviderDetail === null || eip6963ProviderDetail === void 0 ? void 0 : eip6963ProviderDetail.provider;
1038
- if (provider && !this._trackedProviders.has(provider)) {
1039
- this.trackEIP1193Provider(provider);
1040
- }
1081
+ var marker = Symbol.for("formo.historyWrapped");
1082
+ if (history[marker])
1083
+ return;
1084
+ history[marker] = true;
1085
+ var dispatch = function () { return window.dispatchEvent(new window.Event("locationchange")); };
1086
+ var oldPushState = history.pushState;
1087
+ history.pushState = function pushState() {
1088
+ var args = [];
1089
+ for (var _i = 0; _i < arguments.length; _i++) {
1090
+ args[_i] = arguments[_i];
1041
1091
  }
1042
- }
1043
- catch (error) {
1044
- logger_1.logger.error("Failed to track EIP-6963 providers during initialization:", error);
1045
- }
1046
- };
1047
- FormoAnalytics.prototype.addProviderListener = function (provider, event, listener) {
1048
- var map = this._providerListenersMap.get(provider) || {};
1049
- map[event] = listener;
1050
- this._providerListenersMap.set(provider, map);
1051
- };
1052
- FormoAnalytics.prototype.registerAccountsChangedListener = function (provider) {
1053
- var _this = this;
1054
- logger_1.logger.info("registerAccountsChangedListener");
1055
- var listener = function () {
1092
+ var ret = oldPushState.apply(this, args);
1093
+ dispatch();
1094
+ return ret;
1095
+ };
1096
+ var oldReplaceState = history.replaceState;
1097
+ history.replaceState = function replaceState() {
1056
1098
  var args = [];
1057
1099
  for (var _i = 0; _i < arguments.length; _i++) {
1058
1100
  args[_i] = arguments[_i];
1059
1101
  }
1060
- return _this.onAccountsChanged(provider, args[0]);
1102
+ var ret = oldReplaceState.apply(this, args);
1103
+ dispatch();
1104
+ return ret;
1061
1105
  };
1062
- provider.on("accountsChanged", listener);
1063
- this.addProviderListener(provider, "accountsChanged", listener);
1064
1106
  };
1065
- FormoAnalytics.prototype.onAccountsChanged = function (provider, accounts) {
1107
+ FormoAnalytics.prototype.trackPageHit = function (category, name, properties, context, callback) {
1108
+ return __awaiter(this, void 0, void 0, function () {
1109
+ var _this = this;
1110
+ return __generator(this, function (_a) {
1111
+ if (!this.shouldTrack()) {
1112
+ logger_1.logger.info("Track page hit: Skipping event due to tracking configuration");
1113
+ return [2 /*return*/];
1114
+ }
1115
+ setTimeout(function () {
1116
+ // Drop in-flight page hits from an SDK instance that was torn down
1117
+ // between scheduling and firing (e.g. provider remount in React Strict
1118
+ // Mode / HMR). Otherwise the orphan instance would queue a page event
1119
+ // here with its stale, never-populated `currentAddress`.
1120
+ if (_this._pageHooksDisposed)
1121
+ return;
1122
+ (function () { return __awaiter(_this, void 0, void 0, function () {
1123
+ var e_2;
1124
+ return __generator(this, function (_a) {
1125
+ switch (_a.label) {
1126
+ case 0:
1127
+ _a.trys.push([0, 2, , 3]);
1128
+ return [4 /*yield*/, this.trackEvent(constants_1.EventType.PAGE, {
1129
+ category: category,
1130
+ name: name,
1131
+ }, properties, context, callback)];
1132
+ case 1:
1133
+ _a.sent();
1134
+ return [3 /*break*/, 3];
1135
+ case 2:
1136
+ e_2 = _a.sent();
1137
+ logger_1.logger.error("Formo: Failed to track page hit", e_2);
1138
+ return [3 /*break*/, 3];
1139
+ case 3: return [2 /*return*/];
1140
+ }
1141
+ });
1142
+ }); })();
1143
+ }, 300);
1144
+ return [2 /*return*/];
1145
+ });
1146
+ });
1147
+ };
1148
+ FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1066
1149
  return __awaiter(this, void 0, void 0, function () {
1150
+ var error_1;
1067
1151
  return __generator(this, function (_a) {
1068
1152
  switch (_a.label) {
1069
1153
  case 0:
1070
- logger_1.logger.info("onAccountsChanged", accounts);
1071
- // Prevent concurrent processing of accountsChanged events to avoid race conditions
1072
- if (this._processingAccountsChanged) {
1073
- logger_1.logger.debug("OnAccountsChanged: Already processing accountsChanged, skipping", {
1074
- provider: this.getProviderInfo(provider).name,
1075
- });
1154
+ _a.trys.push([0, 2, , 3]);
1155
+ // Gate on the chain the event actually carries. `connect`, `disconnect`,
1156
+ // `chain`, `signature` and `transaction` all put one in the payload, and
1157
+ // it is authoritative: it can name a provider or a wagmi mutation chain
1158
+ // that is not the active one. Events without a chain (page, track,
1159
+ // identify) fall back to the central value.
1160
+ if (!this.shouldTrack(payload === null || payload === void 0 ? void 0 : payload.chainId)) {
1161
+ logger_1.logger.info("Skipping ".concat(type, " event due to tracking configuration"));
1076
1162
  return [2 /*return*/];
1077
1163
  }
1078
- this._processingAccountsChanged = true;
1079
- _a.label = 1;
1164
+ return [4 /*yield*/, this.eventManager.addEvent(__assign(__assign({ type: type }, payload), { properties: properties, context: context, callback: callback }), this.currentAddress, this.currentUserId)];
1080
1165
  case 1:
1081
- _a.trys.push([1, , 3, 4]);
1082
- return [4 /*yield*/, this._handleAccountsChanged(provider, accounts)];
1083
- case 2:
1084
1166
  _a.sent();
1085
- return [3 /*break*/, 4];
1086
- case 3:
1087
- this._processingAccountsChanged = false;
1088
- return [7 /*endfinally*/];
1089
- case 4: return [2 /*return*/];
1167
+ return [3 /*break*/, 3];
1168
+ case 2:
1169
+ error_1 = _a.sent();
1170
+ logger_1.logger.error("Error tracking event:", error_1);
1171
+ return [3 /*break*/, 3];
1172
+ case 3: return [2 /*return*/];
1090
1173
  }
1091
1174
  });
1092
1175
  });
1093
1176
  };
1094
1177
  /**
1095
- * Handles changes to the accounts of a given EIP-1193 provider.
1178
+ * Visitor-level tracking suppression.
1096
1179
  *
1097
- * @param provider - The EIP-1193 provider whose accounts have changed.
1098
- * @param accounts - The new array of account addresses. An empty array indicates a disconnect.
1099
- * @returns A promise that resolves when the account change has been processed.
1180
+ * True when the SDK must not persist identity/session/chain state or send
1181
+ * events for this visitor. Public entry points that write state before
1182
+ * reaching the `shouldTrack()` gate (identify / connect / detect) check this
1183
+ * first, so a suppressed visitor leaves no cookies or session state.
1100
1184
  *
1101
- * If the accounts array is empty and the provider is the active provider, this method triggers
1102
- * a disconnect flow. Otherwise, it updates the state to reflect the new accounts as needed.
1185
+ * The rule lives in `TrackingPolicy`; this stays on the class because it is
1186
+ * part of the surface integrations bind to.
1187
+ * @internal Also read by `identifyPrivyUser` (via a structural cast) so the
1188
+ * Privy sync skips chain reconciliation and emission for suppressed visitors.
1103
1189
  */
1104
- FormoAnalytics.prototype._handleAccountsChanged = function (provider, accounts) {
1105
- return __awaiter(this, void 0, void 0, function () {
1106
- var error_1, address, currentStoredAddress, newProviderAddress, activeProviderAccounts, error_2, nextChainId, wasDisconnected, providerInfo, effectiveChainId;
1107
- return __generator(this, function (_a) {
1108
- switch (_a.label) {
1109
- case 0:
1110
- if (!(accounts.length === 0)) return [3 /*break*/, 9];
1111
- if (!(this._provider === provider)) return [3 /*break*/, 7];
1112
- logger_1.logger.info("OnAccountsChanged: Detecting disconnect, current state:", {
1113
- evmAddress: this._evmAddress,
1114
- evmChainId: this._evmChainId,
1115
- providerMatch: this._provider === provider,
1116
- });
1117
- // The reported connect ends with the connection, so a genuine
1118
- // reconnect later reports again rather than being taken for a
1119
- // duplicate.
1120
- this._announcedConnect.delete(provider);
1121
- if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 5];
1122
- _a.label = 1;
1123
- case 1:
1124
- _a.trys.push([1, 3, , 4]);
1125
- // Pass EVM state explicitly to ensure we have the data for the disconnect event
1126
- return [4 /*yield*/, this.disconnect({
1127
- chainId: this._evmChainId,
1128
- address: this._evmAddress,
1129
- })];
1130
- case 2:
1131
- // Pass EVM state explicitly to ensure we have the data for the disconnect event
1132
- _a.sent();
1133
- return [3 /*break*/, 4];
1134
- case 3:
1135
- error_1 = _a.sent();
1136
- logger_1.logger.error("Failed to disconnect provider on accountsChanged", error_1);
1137
- return [3 /*break*/, 4];
1138
- case 4: return [3 /*break*/, 6];
1139
- case 5:
1140
- logger_1.logger.debug("OnAccountsChanged: Disconnect event skipped (autocapture.disconnect: false)");
1141
- // Still clear state even if not tracking the event
1142
- this.clearChainState('evm');
1143
- _a.label = 6;
1144
- case 6: return [3 /*break*/, 8];
1145
- case 7:
1146
- logger_1.logger.info("OnAccountsChanged: Ignoring disconnect for non-active provider");
1147
- _a.label = 8;
1148
- case 8: return [2 /*return*/];
1149
- case 9:
1150
- address = (0, address_1.validateAndChecksumAddress)(accounts[0]);
1151
- if (!address) {
1152
- logger_1.logger.warn("onAccountsChanged: Invalid address received", accounts[0]);
1153
- return [2 /*return*/];
1154
- }
1155
- if (!(this._provider && this._provider !== provider)) return [3 /*break*/, 25];
1156
- currentStoredAddress = this._evmAddress;
1157
- newProviderAddress = (0, address_1.validateAndChecksumAddress)(address);
1158
- logger_1.logger.info("OnAccountsChanged: Different provider attempting to connect", {
1159
- activeProvider: this.getProviderInfo(this._provider).name,
1160
- eventProvider: this.getProviderInfo(provider).name,
1161
- currentStoredAddress: currentStoredAddress,
1162
- newProviderAddress: newProviderAddress,
1163
- });
1164
- _a.label = 10;
1165
- case 10:
1166
- _a.trys.push([10, 21, , 25]);
1167
- return [4 /*yield*/, this.getAccounts(this._provider)];
1168
- case 11:
1169
- activeProviderAccounts = _a.sent();
1170
- logger_1.logger.info("OnAccountsChanged: Checking current provider accounts", {
1171
- activeProvider: this.getProviderInfo(this._provider).name,
1172
- accountsLength: activeProviderAccounts
1173
- ? activeProviderAccounts.length
1174
- : 0,
1175
- accounts: activeProviderAccounts,
1176
- });
1177
- if (!(activeProviderAccounts && activeProviderAccounts.length > 0)) return [3 /*break*/, 17];
1178
- if (!(newProviderAddress &&
1179
- currentStoredAddress &&
1180
- newProviderAddress !== currentStoredAddress)) return [3 /*break*/, 15];
1181
- logger_1.logger.info("OnAccountsChanged: Different address detected, switching providers despite current provider having accounts", {
1182
- activeProvider: this.getProviderInfo(this._provider).name,
1183
- eventProvider: this.getProviderInfo(provider).name,
1184
- currentAddress: currentStoredAddress,
1185
- newAddress: newProviderAddress,
1186
- reason: PROVIDER_SWITCH_REASONS.ADDRESS_MISMATCH,
1187
- });
1188
- if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 13];
1189
- return [4 /*yield*/, this.disconnect({
1190
- chainId: this._evmChainId,
1191
- address: this._evmAddress,
1192
- })];
1193
- case 12:
1194
- _a.sent();
1195
- return [3 /*break*/, 14];
1196
- case 13:
1197
- logger_1.logger.debug("OnAccountsChanged: Disconnect event skipped during provider switch (autocapture.disconnect: false)");
1198
- // Still clear state even if not tracking the event
1199
- this.clearChainState('evm');
1200
- _a.label = 14;
1201
- case 14:
1202
- // Clear state and let the new provider become active
1203
- this.clearActiveProvider();
1204
- return [3 /*break*/, 16];
1205
- case 15:
1206
- logger_1.logger.info("OnAccountsChanged: Current provider still has accounts and same address, ignoring new provider", {
1207
- activeProvider: this.getProviderInfo(this._provider).name,
1208
- eventProvider: this.getProviderInfo(provider).name,
1209
- activeProviderAccountsCount: activeProviderAccounts.length,
1210
- currentAddress: currentStoredAddress,
1211
- newAddress: newProviderAddress,
1212
- });
1213
- return [2 /*return*/];
1214
- case 16: return [3 /*break*/, 20];
1215
- case 17:
1216
- logger_1.logger.info("OnAccountsChanged: Current provider has no accounts, switching to new provider", {
1217
- oldProvider: this.getProviderInfo(this._provider).name,
1218
- newProvider: this.getProviderInfo(provider).name,
1219
- reason: PROVIDER_SWITCH_REASONS.NO_ACCOUNTS,
1220
- });
1221
- if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 19];
1222
- return [4 /*yield*/, this.disconnect({
1223
- chainId: this._evmChainId,
1224
- address: this._evmAddress,
1225
- })];
1226
- case 18:
1227
- _a.sent();
1228
- return [3 /*break*/, 20];
1229
- case 19:
1230
- logger_1.logger.debug("OnAccountsChanged: Disconnect event skipped for old provider (autocapture.disconnect: false)");
1231
- // Still clear state even if not tracking the event
1232
- this.clearChainState('evm');
1233
- _a.label = 20;
1234
- case 20: return [3 /*break*/, 25];
1235
- case 21:
1236
- error_2 = _a.sent();
1237
- logger_1.logger.warn("OnAccountsChanged: Could not check current provider accounts, switching to new provider", {
1238
- error: error_2 instanceof Error ? error_2.message : String(error_2),
1239
- errorType: error_2 instanceof Error ? error_2.constructor.name : typeof error_2,
1240
- oldProvider: this._provider
1241
- ? this.getProviderInfo(this._provider).name
1242
- : "unknown",
1243
- newProvider: this.getProviderInfo(provider).name,
1244
- reason: PROVIDER_SWITCH_REASONS.CHECK_FAILED,
1245
- });
1246
- if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 23];
1247
- return [4 /*yield*/, this.disconnect({
1248
- chainId: this._evmChainId,
1249
- address: this._evmAddress,
1250
- })];
1251
- case 22:
1252
- _a.sent();
1253
- return [3 /*break*/, 24];
1254
- case 23:
1255
- logger_1.logger.debug("OnAccountsChanged: Disconnect event skipped for failed provider check (autocapture.disconnect: false)");
1256
- // Still clear state even if not tracking the event
1257
- this.clearChainState('evm');
1258
- _a.label = 24;
1259
- case 24: return [3 /*break*/, 25];
1260
- case 25:
1261
- // Set provider if none exists (first connection)
1262
- if (!this._provider) {
1263
- this._provider = provider;
1264
- }
1265
- // If both the provider and address are the same, no-op
1266
- if (this._provider === provider && address === this._evmAddress) {
1267
- return [2 /*return*/];
1268
- }
1269
- nextChainId = this.resolveChainIdForProvider(provider);
1270
- wasDisconnected = !this._evmAddress;
1271
- // Update state regardless of whether connect *event* tracking is enabled,
1272
- // so disconnect events keep valid address/chainId values. (excludeChains is
1273
- // NOT suppression - it still updates state so currentChainId can gate
1274
- // events.)
1275
- if (this.isTrackingSuppressed()) {
1276
- this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
1277
- }
1278
- else {
1279
- this.setChainState('evm', { address: address, chainId: nextChainId });
1280
- }
1281
- providerInfo = this.getProviderInfo(provider);
1282
- effectiveChainId = nextChainId || 0;
1283
- if (this.isAutocaptureEnabled("connect") &&
1284
- this.shouldReportConnect(provider, address)) {
1285
- logger_1.logger.info("OnAccountsChanged: Detected wallet connection, emitting connect event", {
1286
- chainId: nextChainId,
1287
- address: address,
1288
- wasDisconnected: wasDisconnected,
1289
- providerName: providerInfo.name,
1290
- rdns: providerInfo.rdns,
1291
- hasChainId: !!nextChainId,
1292
- });
1293
- if (effectiveChainId === 0) {
1294
- logger_1.logger.info("OnAccountsChanged: Using fallback chainId 0 for connect event");
1295
- }
1296
- this.markConnectReported(provider, address, effectiveChainId);
1297
- this.connect({
1298
- chainId: effectiveChainId,
1299
- address: address,
1300
- }, {
1301
- providerName: providerInfo.name,
1302
- rdns: providerInfo.rdns,
1303
- }).catch(function (error) {
1304
- logger_1.logger.error("Failed to track connect event during account change:", error);
1305
- });
1306
- }
1307
- else {
1308
- logger_1.logger.debug("OnAccountsChanged: Connect event skipped (autocapture.connect: false)", {
1309
- chainId: nextChainId,
1310
- address: address,
1311
- providerName: providerInfo.name,
1312
- });
1313
- }
1314
- return [2 /*return*/];
1315
- }
1316
- });
1317
- });
1318
- };
1319
- FormoAnalytics.prototype.registerChainChangedListener = function (provider) {
1320
- var _this = this;
1321
- logger_1.logger.info("registerChainChangedListener");
1322
- var listener = function () {
1323
- var args = [];
1324
- for (var _i = 0; _i < arguments.length; _i++) {
1325
- args[_i] = arguments[_i];
1326
- }
1327
- return _this.onChainChanged(provider, args[0]);
1328
- };
1329
- provider.on("chainChanged", listener);
1330
- this.addProviderListener(provider, "chainChanged", listener);
1331
- };
1332
- FormoAnalytics.prototype.onChainChanged = function (provider, chainIdHex) {
1333
- return __awaiter(this, void 0, void 0, function () {
1334
- var nextChainId, error_3;
1335
- return __generator(this, function (_a) {
1336
- switch (_a.label) {
1337
- case 0:
1338
- logger_1.logger.info("onChainChanged", chainIdHex);
1339
- nextChainId = (0, chain_1.parseChainId)(chainIdHex);
1340
- // Record it for THIS provider regardless of which one is active. This is
1341
- // the only way an autocaptured event from a non-active wallet can learn
1342
- // its chain without putting an RPC on that wallet's transport.
1343
- this.rememberProviderChain(provider, nextChainId);
1344
- // Beyond that, a chain event from a NON-active provider is observation
1345
- // only when chain autocapture is off.
1346
- //
1347
- // This listener is now registered unconditionally, so that a signature can
1348
- // be labelled with its signer's chain. `handleProviderMismatch()` treats a
1349
- // chain event from another wallet as a wallet switch and clears the active
1350
- // wallet's address and chain. That is the established behaviour of the
1351
- // chain feature and stays exactly as it was, but it must not start firing
1352
- // for apps that never asked for chain tracking: a second wallet switching
1353
- // network would silently erase the active wallet's attribution.
1354
- // Observation only when chain autocapture is off, whether or not an
1355
- // active provider has been established yet.
1356
- //
1357
- // `isProviderMismatch()` is false while `_provider` is undefined, which is
1358
- // exactly the state left by restoring a wallet from the active-wallet
1359
- // cookie. A background wallet's `chainChanged` could therefore claim the
1360
- // active slot and overwrite the restored wallet's chain - suppressing
1361
- // allowed events, or letting excluded ones through. The active provider is
1362
- // established by an actual account/connect/request association, not by
1363
- // another wallet changing network.
1364
- if (!this.isAutocaptureEnabled("chain") && provider !== this._provider) {
1365
- return [2 /*return*/];
1366
- }
1367
- // Only handle chain changes for the active provider (or if none is set yet)
1368
- if (this.isProviderMismatch(provider)) {
1369
- this.handleProviderMismatch(provider);
1370
- }
1371
- // Chain changes only matter for connected users
1372
- if (!this._evmAddress) {
1373
- logger_1.logger.info("OnChainChanged: No current address, user appears disconnected");
1374
- return [2 /*return*/, Promise.resolve()];
1375
- }
1376
- // Set provider if none exists
1377
- if (!this._provider) {
1378
- this._provider = provider;
1379
- }
1380
- this.setChainState('evm', { chainId: nextChainId });
1381
- _a.label = 1;
1382
- case 1:
1383
- _a.trys.push([1, 5, , 6]);
1384
- if (!this.isAutocaptureEnabled("chain")) return [3 /*break*/, 3];
1385
- // Awaited, so a failing emission is caught below rather than escaping
1386
- // as an unhandled rejection out of the provider's event listener.
1387
- // `return`ing the promise left the catch here unreachable.
1388
- return [4 /*yield*/, this.chain({
1389
- chainId: nextChainId,
1390
- address: this._evmAddress,
1391
- })];
1392
- case 2:
1393
- // Awaited, so a failing emission is caught below rather than escaping
1394
- // as an unhandled rejection out of the provider's event listener.
1395
- // `return`ing the promise left the catch here unreachable.
1396
- _a.sent();
1397
- return [3 /*break*/, 4];
1398
- case 3:
1399
- logger_1.logger.debug("OnChainChanged: Chain event skipped (autocapture.chain: false)", {
1400
- chainId: this._evmChainId,
1401
- address: this._evmAddress,
1402
- });
1403
- _a.label = 4;
1404
- case 4: return [3 /*break*/, 6];
1405
- case 5:
1406
- error_3 = _a.sent();
1407
- logger_1.logger.error("OnChainChanged: Failed to emit chain event:", error_3);
1408
- return [3 /*break*/, 6];
1409
- case 6: return [2 /*return*/];
1410
- }
1411
- });
1412
- });
1413
- };
1414
- /**
1415
- * Record a provider's chain from its `connect` event, and nothing else.
1416
- *
1417
- * Used when connect autocapture is off. `connect` carries `chainId` in its
1418
- * payload, so this needs no RPC - unlike the full handler, which resolves
1419
- * the account.
1420
- */
1421
- FormoAnalytics.prototype.registerConnectChainObserver = function (provider) {
1422
- var _this = this;
1423
- var listener = function () {
1424
- var args = [];
1425
- for (var _i = 0; _i < arguments.length; _i++) {
1426
- args[_i] = arguments[_i];
1427
- }
1428
- var connection = args[0];
1429
- if (typeof (connection === null || connection === void 0 ? void 0 : connection.chainId) !== "string")
1430
- return;
1431
- _this.rememberProviderChain(provider, (0, chain_1.parseChainId)(connection.chainId));
1432
- };
1433
- provider.on("connect", listener);
1434
- this.addProviderListener(provider, "connect", listener);
1435
- };
1436
- /**
1437
- * Whether a connect for this wallet still needs reporting.
1438
- *
1439
- * True when nothing has been reported for this provider, or when the account
1440
- * changed. A wallet already reported is not reported again.
1441
- *
1442
- * Deliberately does NOT re-report to correct a chain. When `accountsChanged`
1443
- * wins the race on a provider that exposes no synchronous `chainId`, the
1444
- * connect carries 0 - honestly, since the chain is unknown at that instant -
1445
- * and the `connect` payload that follows knows the real one. Emitting again
1446
- * to relabel would mean two connects for one connection, which is the bug
1447
- * this whole path exists to prevent. That payload still corrects
1448
- * `currentChainId`, so everything after it is attributed properly.
1449
- */
1450
- FormoAnalytics.prototype.shouldReportConnect = function (provider, address) {
1451
- var reported = this._announcedConnect.get(provider);
1452
- if (!reported)
1453
- return true;
1454
- return reported.address.toLowerCase() !== address.toLowerCase();
1455
- };
1456
- /**
1457
- * Record a connect as reported - but only if it will actually be sent.
1458
- *
1459
- * `connect()` passes through `shouldTrack()`, which refuses an unresolvable
1460
- * chain when `tracking.excludeChains` is configured. Marking a refused event
1461
- * as reported would suppress the authoritative one that follows.
1462
- */
1463
- FormoAnalytics.prototype.markConnectReported = function (provider, address, chainId) {
1464
- if (!this.willTrackEvent(chainId))
1465
- return;
1466
- this._announcedConnect.set(provider, { address: address, chainId: chainId });
1467
- };
1468
- FormoAnalytics.prototype.registerConnectListener = function (provider) {
1469
- var _this = this;
1470
- logger_1.logger.info("registerConnectListener");
1471
- var listener = function () {
1472
- var args = [];
1473
- for (var _i = 0; _i < arguments.length; _i++) {
1474
- args[_i] = arguments[_i];
1475
- }
1476
- var connection = args[0];
1477
- _this.onConnected(provider, connection);
1478
- };
1479
- provider.on("connect", listener);
1480
- this.addProviderListener(provider, "connect", listener);
1481
- };
1482
- FormoAnalytics.prototype.registerDisconnectListener = function (provider) {
1483
- var _this = this;
1484
- logger_1.logger.info("registerDisconnectListener");
1485
- var listener = function (_error) { return __awaiter(_this, void 0, void 0, function () {
1486
- var e_2;
1487
- return __generator(this, function (_a) {
1488
- switch (_a.label) {
1489
- case 0:
1490
- if (this._provider !== provider)
1491
- return [2 /*return*/];
1492
- // As in the accountsChanged disconnect path: the reported connect ends
1493
- // with the connection.
1494
- this._announcedConnect.delete(provider);
1495
- logger_1.logger.info("OnDisconnect: Wallet disconnect event received, current state:", {
1496
- currentAddress: this._evmAddress,
1497
- currentChainId: this._evmChainId,
1498
- });
1499
- if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 5];
1500
- _a.label = 1;
1501
- case 1:
1502
- _a.trys.push([1, 3, , 4]);
1503
- // Pass current state explicitly to ensure we have the data for the disconnect event
1504
- return [4 /*yield*/, this.disconnect({
1505
- chainId: this._evmChainId,
1506
- address: this._evmAddress,
1507
- })];
1508
- case 2:
1509
- // Pass current state explicitly to ensure we have the data for the disconnect event
1510
- _a.sent();
1511
- return [3 /*break*/, 4];
1512
- case 3:
1513
- e_2 = _a.sent();
1514
- logger_1.logger.error("Error during disconnect in disconnect listener", e_2);
1515
- return [3 /*break*/, 4];
1516
- case 4: return [3 /*break*/, 6];
1517
- case 5:
1518
- logger_1.logger.debug("OnDisconnect: Disconnect event skipped (autocapture.disconnect: false)");
1519
- // Still clear state even if not tracking the event
1520
- this.clearChainState('evm');
1521
- _a.label = 6;
1522
- case 6: return [2 /*return*/];
1523
- }
1524
- });
1525
- }); };
1526
- provider.on("disconnect", listener);
1527
- this.addProviderListener(provider, "disconnect", listener);
1528
- };
1529
- FormoAnalytics.prototype.onConnected = function (provider, connection) {
1530
- return __awaiter(this, void 0, void 0, function () {
1531
- var chainId, address, wasDisconnected, isActiveProvider, providerInfo, effectiveChainId, providerInfo, e_3;
1532
- return __generator(this, function (_a) {
1533
- switch (_a.label) {
1534
- case 0:
1535
- logger_1.logger.info("onConnected", connection);
1536
- _a.label = 1;
1537
- case 1:
1538
- _a.trys.push([1, 3, , 4]);
1539
- if (!(connection === null || connection === void 0 ? void 0 : connection.chainId) || typeof connection.chainId !== "string")
1540
- return [2 /*return*/];
1541
- chainId = (0, chain_1.parseChainId)(connection.chainId);
1542
- // Record it for this provider before anything can bail out below.
1543
- this.rememberProviderChain(provider, chainId);
1544
- return [4 /*yield*/, this.getAddress(provider)];
1545
- case 2:
1546
- address = _a.sent();
1547
- if (chainId && address) {
1548
- wasDisconnected = !this._evmAddress;
1549
- // Set provider if none exists
1550
- if (!this._provider) {
1551
- this._provider = provider;
1552
- }
1553
- isActiveProvider = this._provider === provider;
1554
- // Update state from active provider so disconnect events keep valid
1555
- // address/chainId values - except while suppressed, where we must not
1556
- // LEARN identity (only drop a stale EVM wallet on a switch).
1557
- if (isActiveProvider) {
1558
- if (this.isTrackingSuppressed()) {
1559
- this.clearStaleEvmWalletOnSwitchWhileSuppressed(address);
1560
- }
1561
- else {
1562
- this.setChainState('evm', {
1563
- chainId: chainId,
1564
- address: (0, address_1.validateAndChecksumAddress)(address) || undefined,
1565
- });
1566
- }
1567
- }
1568
- // Conditionally emit connect event based on tracking configuration.
1569
- //
1570
- // Both handlers observe one connection, so `shouldReportConnect()`
1571
- // decides which of them reports it. It keys on what was actually
1572
- // REPORTED, not on whether an address is known: an address can be
1573
- // present with no connect ever sent - restored from the active-wallet
1574
- // cookie, or reported with an unresolved chain and then refused by
1575
- // `excludeChains` - and this payload carries the authoritative chain,
1576
- // so it must be able to supersede such a report.
1577
- if (isActiveProvider &&
1578
- this._evmAddress &&
1579
- this.shouldReportConnect(provider, address)) {
1580
- providerInfo = this.getProviderInfo(provider);
1581
- effectiveChainId = chainId || 0;
1582
- if (this.isAutocaptureEnabled("connect")) {
1583
- logger_1.logger.info("OnConnected: Detected wallet connection, emitting connect event", {
1584
- chainId: chainId,
1585
- wasDisconnected: wasDisconnected,
1586
- providerName: providerInfo.name,
1587
- rdns: providerInfo.rdns,
1588
- hasChainId: !!chainId,
1589
- isActiveProvider: isActiveProvider,
1590
- });
1591
- if (effectiveChainId === 0) {
1592
- logger_1.logger.info("OnConnected: Using fallback chainId 0 for connect event");
1593
- }
1594
- this.markConnectReported(provider, address, effectiveChainId);
1595
- this.connect({
1596
- chainId: effectiveChainId,
1597
- address: address,
1598
- }, {
1599
- providerName: providerInfo.name,
1600
- rdns: providerInfo.rdns,
1601
- }).catch(function (error) {
1602
- logger_1.logger.error("Failed to track connect event during provider connection:", error);
1603
- });
1604
- }
1605
- else {
1606
- logger_1.logger.debug("OnConnected: Connect event skipped (autocapture.connect: false)", {
1607
- chainId: chainId,
1608
- address: address,
1609
- providerName: providerInfo.name,
1610
- });
1611
- }
1612
- }
1613
- else if (address && !isActiveProvider) {
1614
- providerInfo = this.getProviderInfo(provider);
1615
- logger_1.logger.debug("OnConnected: Skipping connect event for non-active provider", {
1616
- chainId: chainId,
1617
- providerName: providerInfo.name,
1618
- rdns: providerInfo.rdns,
1619
- isActiveProvider: isActiveProvider,
1620
- activeProviderInfo: this._provider
1621
- ? this.getProviderInfo(this._provider)
1622
- : null,
1623
- });
1624
- }
1625
- }
1626
- return [3 /*break*/, 4];
1627
- case 3:
1628
- e_3 = _a.sent();
1629
- logger_1.logger.error("Error handling connect event", e_3);
1630
- return [3 /*break*/, 4];
1631
- case 4: return [2 /*return*/];
1632
- }
1633
- });
1634
- });
1635
- };
1636
- FormoAnalytics.prototype.registerRequestListeners = function (provider) {
1637
- var _this = this;
1638
- logger_1.logger.info("registerRequestListeners");
1639
- if (!provider) {
1640
- logger_1.logger.error("Provider not found for request (signature, transaction) tracking");
1641
- return;
1642
- }
1643
- // Check if the provider is already wrapped with our SDK's wrapper
1644
- var currentRequest = provider.request;
1645
- if (this.isProviderAlreadyWrapped(provider, currentRequest)) {
1646
- logger_1.logger.info("Provider already wrapped with our SDK; skipping request wrapping.");
1647
- return;
1648
- }
1649
- var request = provider.request.bind(provider);
1650
- var wrappedRequest = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
1651
- var generation_1, responsePromise, capturedChainId_1, response_1, error_4, rpcError, txPromise, txChainId_1, transactionHash_1, error_5, rpcError;
1652
- var _this = this;
1653
- var _c;
1654
- var method = _b.method, params = _b.params;
1655
- return __generator(this, function (_d) {
1656
- switch (_d.label) {
1657
- case 0:
1658
- // Learn the chain from a call the APP was making anyway.
1659
- //
1660
- // A standards-compliant provider need not expose a synchronous `chainId`
1661
- // property, and if it connected before the SDK initialised, its
1662
- // `connect` event is never replayed. Such a provider stayed unknown -
1663
- // reported as chain 0, and with `excludeChains` configured its events
1664
- // were dropped even on an allowed chain. This adds no request of its
1665
- // own; it only reads the answer to one the dapp already sent.
1666
- if (method === "eth_chainId") {
1667
- generation_1 = (_c = this._providerChainGenerations.get(provider)) !== null && _c !== void 0 ? _c : 0;
1668
- return [2 /*return*/, request({ method: method, params: params }).then(function (result) {
1669
- var _a;
1670
- // A `chainChanged` for THIS provider may have landed while this was
1671
- // in flight. It is newer by definition, so it must not be
1672
- // overwritten by this answer.
1673
- if (generation_1 === ((_a = _this._providerChainGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) &&
1674
- typeof result === "string") {
1675
- _this.rememberProviderChain(provider, (0, chain_1.parseChainId)(result));
1676
- }
1677
- return result;
1678
- })];
1679
- }
1680
- if (!(Array.isArray(params) &&
1681
- ["eth_signTypedData_v4", "personal_sign"].includes(method))) return [3 /*break*/, 4];
1682
- if (!this.isAutocaptureEnabled("signature")) {
1683
- logger_1.logger.debug("Signature event skipped (autocapture.signature: false)", { method: method });
1684
- return [2 /*return*/, request({ method: method, params: params })];
1685
- }
1686
- responsePromise = request({ method: method, params: params });
1687
- // Attach a no-op handler now so a rejection arriving before the await
1688
- // below is never reported as unhandled. The real handling is there.
1689
- responsePromise.catch(function () { return undefined; });
1690
- capturedChainId_1 = this.resolveChainIdForProvider(provider);
1691
- // Fire-and-forget tracking
1692
- (function () { return __awaiter(_this, void 0, void 0, function () {
1693
- var e_4;
1694
- return __generator(this, function (_a) {
1695
- switch (_a.label) {
1696
- case 0:
1697
- _a.trys.push([0, 2, , 3]);
1698
- return [4 /*yield*/, this.signature(__assign({ status: types_1.SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
1699
- case 1:
1700
- _a.sent();
1701
- return [3 /*break*/, 3];
1702
- case 2:
1703
- e_4 = _a.sent();
1704
- logger_1.logger.error("Formo: Failed to track signature request", e_4);
1705
- return [3 /*break*/, 3];
1706
- case 3: return [2 /*return*/];
1707
- }
1708
- });
1709
- }); })();
1710
- _d.label = 1;
1711
- case 1:
1712
- _d.trys.push([1, 3, , 4]);
1713
- return [4 /*yield*/, responsePromise];
1714
- case 2:
1715
- response_1 = _d.sent();
1716
- // Track signature confirmation only for truthy responses
1717
- if (response_1) {
1718
- (function () { return __awaiter(_this, void 0, void 0, function () {
1719
- var e_5;
1720
- return __generator(this, function (_a) {
1721
- switch (_a.label) {
1722
- case 0:
1723
- _a.trys.push([0, 2, , 3]);
1724
- return [4 /*yield*/, this.signature(__assign({ status: types_1.SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)))];
1725
- case 1:
1726
- _a.sent();
1727
- return [3 /*break*/, 3];
1728
- case 2:
1729
- e_5 = _a.sent();
1730
- logger_1.logger.error("Formo: Failed to track signature confirmation", e_5);
1731
- return [3 /*break*/, 3];
1732
- case 3: return [2 /*return*/];
1733
- }
1734
- });
1735
- }); })();
1736
- }
1737
- return [2 /*return*/, response_1];
1738
- case 3:
1739
- error_4 = _d.sent();
1740
- rpcError = error_4;
1741
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
1742
- // Use the already cast rpcError to avoid duplication
1743
- (function () { return __awaiter(_this, void 0, void 0, function () {
1744
- var e_6;
1745
- return __generator(this, function (_a) {
1746
- switch (_a.label) {
1747
- case 0:
1748
- _a.trys.push([0, 2, , 3]);
1749
- return [4 /*yield*/, this.signature(__assign({ status: types_1.SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
1750
- case 1:
1751
- _a.sent();
1752
- return [3 /*break*/, 3];
1753
- case 2:
1754
- e_6 = _a.sent();
1755
- logger_1.logger.error("Formo: Failed to track signature rejection", e_6);
1756
- return [3 /*break*/, 3];
1757
- case 3: return [2 /*return*/];
1758
- }
1759
- });
1760
- }); })();
1761
- }
1762
- throw error_4;
1763
- case 4:
1764
- if (!(Array.isArray(params) &&
1765
- method === "eth_sendTransaction" &&
1766
- params[0])) return [3 /*break*/, 8];
1767
- if (!this.isAutocaptureEnabled("transaction")) {
1768
- logger_1.logger.debug("Transaction event skipped (autocapture.transaction: false)", { method: method });
1769
- return [2 /*return*/, request({ method: method, params: params })];
1770
- }
1771
- txPromise = request({ method: method, params: params });
1772
- txPromise.catch(function () { return undefined; });
1773
- txChainId_1 = this.resolveChainIdForProvider(provider);
1774
- (function () { return __awaiter(_this, void 0, void 0, function () {
1775
- var payload, e_7;
1776
- return __generator(this, function (_a) {
1777
- switch (_a.label) {
1778
- case 0:
1779
- _a.trys.push([0, 3, , 4]);
1780
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1781
- case 1:
1782
- payload = _a.sent();
1783
- return [4 /*yield*/, this.transaction(__assign({ status: types_1.TransactionStatus.STARTED }, payload))];
1784
- case 2:
1785
- _a.sent();
1786
- return [3 /*break*/, 4];
1787
- case 3:
1788
- e_7 = _a.sent();
1789
- logger_1.logger.error("Formo: Failed to track transaction start", e_7);
1790
- return [3 /*break*/, 4];
1791
- case 4: return [2 /*return*/];
1792
- }
1793
- });
1794
- }); })();
1795
- _d.label = 5;
1796
- case 5:
1797
- _d.trys.push([5, 7, , 8]);
1798
- return [4 /*yield*/, txPromise];
1799
- case 6:
1800
- transactionHash_1 = _d.sent();
1801
- (function () { return __awaiter(_this, void 0, void 0, function () {
1802
- var payload, e_8;
1803
- return __generator(this, function (_a) {
1804
- switch (_a.label) {
1805
- case 0:
1806
- _a.trys.push([0, 3, , 4]);
1807
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1808
- case 1:
1809
- payload = _a.sent();
1810
- return [4 /*yield*/, this.transaction(__assign(__assign({ status: types_1.TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }))];
1811
- case 2:
1812
- _a.sent();
1813
- // Start async polling for transaction receipt
1814
- this.pollTransactionReceipt(provider, transactionHash_1, payload);
1815
- return [3 /*break*/, 4];
1816
- case 3:
1817
- e_8 = _a.sent();
1818
- logger_1.logger.error("Formo: Failed to track transaction broadcast", e_8);
1819
- return [3 /*break*/, 4];
1820
- case 4: return [2 /*return*/];
1821
- }
1822
- });
1823
- }); })();
1824
- return [2 /*return*/, transactionHash_1];
1825
- case 7:
1826
- error_5 = _d.sent();
1827
- rpcError = error_5;
1828
- if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
1829
- // Use the already cast rpcError to avoid duplication
1830
- (function () { return __awaiter(_this, void 0, void 0, function () {
1831
- var payload, e_9;
1832
- return __generator(this, function (_a) {
1833
- switch (_a.label) {
1834
- case 0:
1835
- _a.trys.push([0, 3, , 4]);
1836
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1837
- case 1:
1838
- payload = _a.sent();
1839
- return [4 /*yield*/, this.transaction(__assign({ status: types_1.TransactionStatus.REJECTED }, payload))];
1840
- case 2:
1841
- _a.sent();
1842
- return [3 /*break*/, 4];
1843
- case 3:
1844
- e_9 = _a.sent();
1845
- logger_1.logger.error("Formo: Failed to track transaction rejection", e_9);
1846
- return [3 /*break*/, 4];
1847
- case 4: return [2 /*return*/];
1848
- }
1849
- });
1850
- }); })();
1851
- }
1852
- throw error_5;
1853
- case 8: return [2 /*return*/, request({ method: method, params: params })];
1854
- }
1855
- });
1856
- }); };
1857
- // Mark the wrapper so we can detect if request is replaced externally and keep a reference on provider
1858
- wrappedRequest[types_1.WRAPPED_REQUEST_SYMBOL] = true;
1859
- provider[types_1.WRAPPED_REQUEST_REF_SYMBOL] =
1860
- wrappedRequest;
1861
- try {
1862
- // Attempt to assign the wrapped request function (rely on try-catch for mutability errors)
1863
- provider.request = wrappedRequest;
1864
- }
1865
- catch (e) {
1866
- logger_1.logger.warn("Failed to wrap provider.request; skipping", e);
1867
- }
1868
- };
1869
- FormoAnalytics.prototype.onLocationChange = function () {
1870
- return __awaiter(this, void 0, void 0, function () {
1871
- var _a;
1872
- return __generator(this, function (_b) {
1873
- if (this._currentUrl !== window.location.href) {
1874
- this._currentUrl = window.location.href;
1875
- // Host/path exclusions are evaluated per navigation, so a SPA can leave
1876
- // an excluded route and become trackable without any wallet event
1877
- // firing. The wagmi handler only observes *changes*, so an unchanged
1878
- // connection it was forced to decline earlier would stay invisible for
1879
- // the rest of the page load. Give it a chance to adopt it now.
1880
- (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1881
- this.trackPageHit();
1882
- }
1883
- return [2 /*return*/];
1884
- });
1885
- });
1886
- };
1887
- FormoAnalytics.prototype.trackPageHits = function () {
1888
- var _this = this;
1889
- // Install a single, instance-agnostic wrapper around history.pushState /
1890
- // replaceState so concurrent SDK instances (React Strict Mode, HMR) don't
1891
- // each stack their own wrapper - which would dispatch N synthetic events
1892
- // per navigation and produce O(N^2) onLocationChange calls. The wrapper
1893
- // dispatches once; per-instance bookkeeping is done by per-instance
1894
- // listeners that each register/unregister themselves.
1895
- FormoAnalytics.installHistoryHooksOnce();
1896
- this._onPopStateListener = function () { return _this.onLocationChange(); };
1897
- this._onLocationChangeListener = function () { return _this.onLocationChange(); };
1898
- window.addEventListener("popstate", this._onPopStateListener);
1899
- window.addEventListener("locationchange", this._onLocationChangeListener);
1900
- };
1901
- /**
1902
- * Wrap history.pushState / replaceState exactly once per `history` object,
1903
- * regardless of how many SDK instances are constructed. Uses a Symbol
1904
- * marker so we recognize our own wrapper across module reloads in HMR.
1905
- */
1906
- FormoAnalytics.installHistoryHooksOnce = function () {
1907
- if (typeof history === "undefined" || typeof window === "undefined")
1908
- return;
1909
- var marker = Symbol.for("formo.historyWrapped");
1910
- if (history[marker])
1911
- return;
1912
- history[marker] = true;
1913
- var dispatch = function () { return window.dispatchEvent(new window.Event("locationchange")); };
1914
- var oldPushState = history.pushState;
1915
- history.pushState = function pushState() {
1916
- var args = [];
1917
- for (var _i = 0; _i < arguments.length; _i++) {
1918
- args[_i] = arguments[_i];
1919
- }
1920
- var ret = oldPushState.apply(this, args);
1921
- dispatch();
1922
- return ret;
1923
- };
1924
- var oldReplaceState = history.replaceState;
1925
- history.replaceState = function replaceState() {
1926
- var args = [];
1927
- for (var _i = 0; _i < arguments.length; _i++) {
1928
- args[_i] = arguments[_i];
1929
- }
1930
- var ret = oldReplaceState.apply(this, args);
1931
- dispatch();
1932
- return ret;
1933
- };
1934
- };
1935
- FormoAnalytics.prototype.trackPageHit = function (category, name, properties, context, callback) {
1936
- return __awaiter(this, void 0, void 0, function () {
1937
- var _this = this;
1938
- return __generator(this, function (_a) {
1939
- if (!this.shouldTrack()) {
1940
- logger_1.logger.info("Track page hit: Skipping event due to tracking configuration");
1941
- return [2 /*return*/];
1942
- }
1943
- setTimeout(function () {
1944
- // Drop in-flight page hits from an SDK instance that was torn down
1945
- // between scheduling and firing (e.g. provider remount in React Strict
1946
- // Mode / HMR). Otherwise the orphan instance would queue a page event
1947
- // here with its stale, never-populated `currentAddress`.
1948
- if (_this._pageHooksDisposed)
1949
- return;
1950
- (function () { return __awaiter(_this, void 0, void 0, function () {
1951
- var e_10;
1952
- return __generator(this, function (_a) {
1953
- switch (_a.label) {
1954
- case 0:
1955
- _a.trys.push([0, 2, , 3]);
1956
- return [4 /*yield*/, this.trackEvent(constants_1.EventType.PAGE, {
1957
- category: category,
1958
- name: name,
1959
- }, properties, context, callback)];
1960
- case 1:
1961
- _a.sent();
1962
- return [3 /*break*/, 3];
1963
- case 2:
1964
- e_10 = _a.sent();
1965
- logger_1.logger.error("Formo: Failed to track page hit", e_10);
1966
- return [3 /*break*/, 3];
1967
- case 3: return [2 /*return*/];
1968
- }
1969
- });
1970
- }); })();
1971
- }, 300);
1972
- return [2 /*return*/];
1973
- });
1974
- });
1975
- };
1976
- FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1977
- return __awaiter(this, void 0, void 0, function () {
1978
- var error_6;
1979
- return __generator(this, function (_a) {
1980
- switch (_a.label) {
1981
- case 0:
1982
- _a.trys.push([0, 2, , 3]);
1983
- // Gate on the chain the event actually carries. `connect`, `disconnect`,
1984
- // `chain`, `signature` and `transaction` all put one in the payload, and
1985
- // it is authoritative: it can name a provider or a wagmi mutation chain
1986
- // that is not the active one. Events without a chain (page, track,
1987
- // identify) fall back to the central value.
1988
- if (!this.shouldTrack(payload === null || payload === void 0 ? void 0 : payload.chainId)) {
1989
- logger_1.logger.info("Skipping ".concat(type, " event due to tracking configuration"));
1990
- return [2 /*return*/];
1991
- }
1992
- return [4 /*yield*/, this.eventManager.addEvent(__assign(__assign({ type: type }, payload), { properties: properties, context: context, callback: callback }), this.currentAddress, this.currentUserId)];
1993
- case 1:
1994
- _a.sent();
1995
- return [3 /*break*/, 3];
1996
- case 2:
1997
- error_6 = _a.sent();
1998
- logger_1.logger.error("Error tracking event:", error_6);
1999
- return [3 /*break*/, 3];
2000
- case 3: return [2 /*return*/];
2001
- }
2002
- });
2003
- });
2004
- };
2005
- /**
2006
- * Visitor-level tracking suppression.
2007
- *
2008
- * Returns true when the SDK must not persist any identity/session/chain
2009
- * state or send any events for this visitor - i.e. an explicit opt-out or a
2010
- * jurisdiction/timezone exclusion. Public entry points that write state
2011
- * before reaching the `shouldTrack()` event gate (identify/connect/detect)
2012
- * check this first so suppressed visitors leave no cookies or session state.
2013
- * @returns {boolean} True if all tracking and persistence must be suppressed
2014
- * @internal Also read by `identifyPrivyUser` (via a structural cast) so the
2015
- * Privy sync skips chain reconciliation and emission for suppressed visitors.
2016
- */
2017
- FormoAnalytics.prototype.isTrackingSuppressed = function () {
2018
- return this.hasOptedOutTracking() || this.isCurrentEnvironmentExcluded();
2019
- };
2020
- /**
2021
- * Whether the current chain id is in `tracking.excludeChains`.
2022
- *
2023
- * Split out from `shouldTrack()` so `identify()` can check it *before*
2024
- * mutating identity state. `trackEvent()` drops an excluded event silently
2025
- * and returns void, but `identify()` marks the wallet as identified first, so
2026
- * without this guard an identify on an excluded chain is dedup-marked and
2027
- * then discarded, and the wallet never re-emits for the rest of the session
2028
- * even after switching to an allowed chain. On the Privy path that loses the
2029
- * user's whole cluster at once rather than a single wallet.
2030
- *
2031
- * Mirrors the chain rule in `shouldTrack()`: only applies when `tracking` is
2032
- * an options object with `excludeChains` set, and only once a chain id is
2033
- * known.
2034
- */
2035
- FormoAnalytics.prototype.isCurrentChainExcluded = function (eventChainId) {
2036
- if (this.options.tracking === null ||
2037
- typeof this.options.tracking !== "object" ||
2038
- Array.isArray(this.options.tracking)) {
2039
- return false;
2040
- }
2041
- var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
2042
- if (excludeChains.length === 0)
2043
- return false;
2044
- // Mirrors `shouldTrack()`: the event's own chain wins when it has one,
2045
- // and an unresolvable chain on a known wallet counts as excluded rather
2046
- // than allowed.
2047
- var chainToCheck = eventChainId !== null && eventChainId !== void 0 ? eventChainId : this.currentChainId;
2048
- if (chainToCheck === 0)
2049
- return true;
2050
- if (chainToCheck === undefined)
2051
- return false;
2052
- return excludeChains.includes(chainToCheck);
2053
- };
2054
- /**
2055
- * Whether the current environment is excluded from tracking - the visitor's
2056
- * timezone, the current hostname, or the current pathname matches a
2057
- * configured exclusion.
2058
- *
2059
- * Timezone is visitor/session-level (stable for the session); host/path are
2060
- * current-page-level and transient - if a SPA navigates to an allowed path,
2061
- * tracking resumes for future actions. Used as the "do not write identity or
2062
- * send events" gate at every entry point that would persist state before the
2063
- * `shouldTrack()` event gate.
2064
- * @returns {boolean} True if the current environment is excluded
2065
- */
2066
- FormoAnalytics.prototype.isCurrentEnvironmentExcluded = function () {
2067
- return (this.isTimezoneExcluded() ||
2068
- this.isHostExcluded() ||
2069
- this.isPathExcluded());
2070
- };
2071
- /**
2072
- * Whether the current hostname matches a configured `tracking.excludeHosts`
2073
- * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
2074
- * @returns {boolean} True if the current hostname is excluded
2075
- */
2076
- FormoAnalytics.prototype.isHostExcluded = function () {
2077
- var tracking = this.options.tracking;
2078
- if (tracking === null ||
2079
- typeof tracking !== "object" ||
2080
- Array.isArray(tracking)) {
2081
- return false;
2082
- }
2083
- if (typeof window === "undefined") {
2084
- return false;
2085
- }
2086
- var _a = tracking.excludeHosts, excludeHosts = _a === void 0 ? [] : _a;
2087
- return excludeHosts.includes(window.location.hostname);
2088
- };
2089
- /**
2090
- * Whether the current pathname matches a configured `tracking.excludePaths`
2091
- * entry (exact match). Current-page-level - see isCurrentEnvironmentExcluded.
2092
- * @returns {boolean} True if the current pathname is excluded
2093
- */
2094
- FormoAnalytics.prototype.isPathExcluded = function () {
2095
- var tracking = this.options.tracking;
2096
- if (tracking === null ||
2097
- typeof tracking !== "object" ||
2098
- Array.isArray(tracking)) {
2099
- return false;
2100
- }
2101
- if (typeof window === "undefined") {
2102
- return false;
2103
- }
2104
- var _a = tracking.excludePaths, excludePaths = _a === void 0 ? [] : _a;
2105
- return excludePaths.includes(window.location.pathname);
2106
- };
2107
- /**
2108
- * Whether the current call is in a visitor-level suppression state - opt-out
2109
- * or excluded timezone - for which any persisted identity cookie should be
2110
- * actively purged (not merely skipped). Host/path exclusions are
2111
- * deliberately excluded here: they are transient current-page states, so a
2112
- * cookie legitimately written on an allowed page must survive a visit to an
2113
- * excluded route.
2114
- * @returns {boolean} True if persisted identity must be purged
2115
- */
2116
- FormoAnalytics.prototype.isPersistedIdentityPurgeRequired = function () {
2117
- return this.hasOptedOutTracking() || this.isTimezoneExcluded();
2118
- };
2119
- /**
2120
- * Whether the visitor's browser-resolved timezone matches a configured
2121
- * `tracking.excludeTimezones` entry (case-insensitive). Client-side and
2122
- * best-effort - see TrackingOptions.excludeTimezones.
2123
- * @returns {boolean} True if the current timezone is excluded
2124
- */
2125
- FormoAnalytics.prototype.isTimezoneExcluded = function () {
2126
- var tracking = this.options.tracking;
2127
- if (tracking === null ||
2128
- typeof tracking !== "object" ||
2129
- Array.isArray(tracking)) {
2130
- return false;
2131
- }
2132
- var _a = tracking.excludeTimezones, excludeTimezones = _a === void 0 ? [] : _a;
2133
- if (excludeTimezones.length === 0) {
2134
- return false;
2135
- }
2136
- var timezone = (0, timezone_1.getTimezone)();
2137
- if (!timezone) {
2138
- return false;
2139
- }
2140
- var lowerTimezone = timezone.toLowerCase();
2141
- return excludeTimezones.some(function (tz) { return typeof tz === "string" && tz.toLowerCase() === lowerTimezone; });
1190
+ FormoAnalytics.prototype.isTrackingSuppressed = function () {
1191
+ return this.trackingPolicy.isTrackingSuppressed();
2142
1192
  };
2143
- /**
2144
- * Determines if tracking should be enabled based on configuration and consent
2145
- * @returns {boolean} True if tracking should be enabled
2146
- */
1193
+ /** @see TrackingPolicy.shouldTrack */
2147
1194
  FormoAnalytics.prototype.shouldTrack = function (eventChainId) {
2148
- // First check if user has opted out of tracking
2149
- if (this.hasOptedOutTracking()) {
2150
- return false;
2151
- }
2152
- // Check if tracking is explicitly provided as a boolean
2153
- if (typeof this.options.tracking === "boolean") {
2154
- return this.options.tracking;
2155
- }
2156
- // Handle object configuration with exclusion rules
2157
- if (this.options.tracking !== null &&
2158
- typeof this.options.tracking === "object" &&
2159
- !Array.isArray(this.options.tracking)) {
2160
- var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
2161
- // Environment exclusions (timezone / host / path) - no identify / connect
2162
- // / track events while excluded. Host/path are exact-match.
2163
- if (this.isCurrentEnvironmentExcluded()) {
2164
- return false;
2165
- }
2166
- // Check chainId exclusions.
2167
- //
2168
- // The event's OWN chain wins over `currentChainId`. They differ whenever
2169
- // the event did not come from the active provider: a second wallet
2170
- // signing through its own provider, or a wagmi mutation that names an
2171
- // explicit chain. Reading only the central field there would emit an
2172
- // event that is labelled with an excluded chain, which is precisely what
2173
- // the exclusion forbids - and would drop an allowed event whenever the
2174
- // active provider happened to sit on an excluded chain.
2175
- var chainToCheck = eventChainId !== null && eventChainId !== void 0 ? eventChainId : this.currentChainId;
2176
- if (excludeChains.length > 0) {
2177
- // Fail CLOSED on an unknown chain. `resolveChainIdForProvider` reports
2178
- // 0 when it has never heard a chain from the signing wallet, and 0 is
2179
- // in no exclusion list, so treating it as "not excluded" would let
2180
- // exactly the events an operator excluded through - the wallet on the
2181
- // excluded chain is often the one we know least about. An explicit
2182
- // exclusion is a directive, so an unresolvable chain is refused.
2183
- //
2184
- // This covers the central value too, not just an explicit event
2185
- // chain: `page`, `track` and `identify` carry no chain of their own
2186
- // and fall back to `currentChainId`, so an unknown chain there would
2187
- // otherwise send wallet-attributed events for a wallet that may well
2188
- // be sitting on an excluded chain.
2189
- // Deliberately keyed on 0 and NOT on `undefined`. 0 is the explicit
2190
- // "we asked and could not tell" marker. `undefined` means no chain
2191
- // state yet, which is a legitimate transient - the Privy path
2192
- // reconciles a Solana wallet through exactly that state - and
2193
- // refusing it would drop real events.
2194
- //
2195
- // `backfillActiveWallet()` never persists 0, so an unresolvable chain
2196
- // cannot leak into `currentChainId` and reach the unscoped events
2197
- // (page / track / identify) that fall back to it.
2198
- if (chainToCheck === 0)
2199
- return false;
2200
- if (chainToCheck && excludeChains.includes(chainToCheck))
2201
- return false;
2202
- }
2203
- // If nothing is excluded, tracking is enabled
2204
- return true;
2205
- }
2206
- // Default behavior: track everywhere except localhost
2207
- return !(0, validators_1.isLocalhost)();
1195
+ return this.trackingPolicy.shouldTrack({ chainId: eventChainId });
2208
1196
  };
2209
1197
  /**
2210
1198
  * Check if a specific wallet event type is enabled for autocapture
@@ -2212,184 +1200,18 @@ var FormoAnalytics = /** @class */ (function () {
2212
1200
  * @returns {boolean} True if the event type should be autocaptured
2213
1201
  */
2214
1202
  FormoAnalytics.prototype.isAutocaptureEnabled = function (eventType) {
2215
- // If no configuration provided, default to enabled
2216
- if (this.options.autocapture === undefined) {
2217
- return true;
2218
- }
2219
- // If boolean, return that value for all events
2220
- if (typeof this.options.autocapture === "boolean") {
2221
- return this.options.autocapture;
2222
- }
2223
- // If it's an object, check the specific event configuration
2224
- if (this.options.autocapture !== null &&
2225
- typeof this.options.autocapture === "object") {
2226
- var eventConfig = this.options.autocapture[eventType];
2227
- // Default to true if not explicitly set to false
2228
- return eventConfig !== false;
2229
- }
2230
- // Default to enabled if no specific configuration
2231
- return true;
2232
- };
2233
- /*
2234
- Utility functions
2235
- */
2236
- /**
2237
- * Get provider information for a given provider
2238
- * @param provider The provider to get info for
2239
- * @returns Provider information
2240
- */
2241
- FormoAnalytics.prototype.getProviderInfo = function (provider) {
2242
- // First check if provider is in our EIP-6963 providers list
2243
- var eip6963Provider = this._providers.find(function (p) { return p.provider === provider; });
2244
- if (eip6963Provider) {
2245
- return {
2246
- name: eip6963Provider.info.name,
2247
- rdns: eip6963Provider.info.rdns,
2248
- };
2249
- }
2250
- // Fallback to injected provider detection
2251
- var injectedInfo = (0, provider_1.detectInjectedProviderInfo)(provider);
2252
- return {
2253
- name: injectedInfo.name,
2254
- rdns: injectedInfo.rdns,
2255
- };
2256
- };
2257
- FormoAnalytics.prototype.getProviders = function () {
2258
- return __awaiter(this, void 0, void 0, function () {
2259
- var store, providers, injected_1, injectedProviderInfo, injectedDetail, uniqueProviders, _i, uniqueProviders_1, detail;
2260
- var _this = this;
2261
- return __generator(this, function (_a) {
2262
- store = (0, mipd_1.createStore)();
2263
- providers = store.getProviders();
2264
- store.subscribe(function (providerDetails) {
2265
- providers = providerDetails;
2266
- // Process newly added providers with proper deduplication
2267
- var newlyAddedDetails = providerDetails.filter(function (detail) {
2268
- var provider = detail === null || detail === void 0 ? void 0 : detail.provider;
2269
- return provider && !_this._seenProviders.has(provider);
2270
- });
2271
- // Add new providers to the array without overwriting existing ones
2272
- for (var _i = 0, newlyAddedDetails_1 = newlyAddedDetails; _i < newlyAddedDetails_1.length; _i++) {
2273
- var detail = newlyAddedDetails_1[_i];
2274
- _this.safeAddProviderDetail(detail);
2275
- }
2276
- // Track listeners for newly discovered providers only
2277
- var newDetails = providerDetails.filter(function (detail) {
2278
- var p = detail === null || detail === void 0 ? void 0 : detail.provider;
2279
- return !!p && !_this._trackedProviders.has(p);
2280
- });
2281
- if (newDetails.length > 0) {
2282
- _this.trackProviders(newDetails);
2283
- // Detect newly discovered wallets (session de-dupes) with error handling
2284
- (function () { return __awaiter(_this, void 0, void 0, function () {
2285
- var e_11;
2286
- return __generator(this, function (_a) {
2287
- switch (_a.label) {
2288
- case 0:
2289
- _a.trys.push([0, 2, , 3]);
2290
- return [4 /*yield*/, this.detectWallets(newDetails)];
2291
- case 1:
2292
- _a.sent();
2293
- return [3 /*break*/, 3];
2294
- case 2:
2295
- e_11 = _a.sent();
2296
- logger_1.logger.error("Formo: Failed to detect wallets", e_11);
2297
- return [3 /*break*/, 3];
2298
- case 3: return [2 /*return*/];
2299
- }
2300
- });
2301
- }); })();
2302
- }
2303
- // Clean up providers that are no longer available
2304
- _this.cleanupUnavailableProviders();
2305
- });
2306
- // Fallback to injected provider if no providers are found
2307
- if (providers.length === 0) {
2308
- injected_1 = typeof window !== "undefined" ? window.ethereum : undefined;
2309
- if (injected_1) {
2310
- // If we have already detected and cached the injected provider, and it's the same instance, return the cached result
2311
- if (this._injectedProviderDetail &&
2312
- this._injectedProviderDetail.provider === injected_1) {
2313
- // Ensure it's tracked
2314
- if (!this._trackedProviders.has(injected_1)) {
2315
- this.trackEIP1193Provider(injected_1);
2316
- }
2317
- // Merge with existing providers instead of overwriting
2318
- if (!this._providers.some(function (existing) { return existing.provider === injected_1; })) {
2319
- this._providers = __spreadArray(__spreadArray([], this._providers, true), [
2320
- this._injectedProviderDetail,
2321
- ], false);
2322
- }
2323
- return [2 /*return*/, this._providers];
2324
- }
2325
- // Re-check if the injected provider is already tracked just before tracking
2326
- if (!this._trackedProviders.has(injected_1)) {
2327
- this.trackEIP1193Provider(injected_1);
2328
- }
2329
- injectedProviderInfo = (0, provider_1.detectInjectedProviderInfo)(injected_1);
2330
- injectedDetail = {
2331
- provider: injected_1,
2332
- info: injectedProviderInfo,
2333
- };
2334
- // Cache the detected injected provider detail
2335
- this._injectedProviderDetail = injectedDetail;
2336
- // Merge with existing providers instead of overwriting
2337
- this.safeAddProviderDetail(injectedDetail);
2338
- }
2339
- return [2 /*return*/, this._providers];
2340
- }
2341
- uniqueProviders = providers.filter(function (detail) {
2342
- var provider = detail === null || detail === void 0 ? void 0 : detail.provider;
2343
- return provider && !_this._seenProviders.has(provider);
2344
- });
2345
- // Add to seen providers and instances, ensuring no duplicates in _providers
2346
- for (_i = 0, uniqueProviders_1 = uniqueProviders; _i < uniqueProviders_1.length; _i++) {
2347
- detail = uniqueProviders_1[_i];
2348
- this.safeAddProviderDetail(detail);
2349
- }
2350
- return [2 /*return*/, this._providers];
2351
- });
2352
- });
1203
+ return this.trackingPolicy.isAutocaptureEnabled(eventType);
2353
1204
  };
2354
1205
  Object.defineProperty(FormoAnalytics.prototype, "providers", {
1206
+ /*
1207
+ Utility functions
1208
+ */
2355
1209
  get: function () {
2356
- return this._providers;
1210
+ return this.evm.all;
2357
1211
  },
2358
1212
  enumerable: false,
2359
1213
  configurable: true
2360
1214
  });
2361
- FormoAnalytics.prototype.detectWallets = function (providers) {
2362
- return __awaiter(this, void 0, void 0, function () {
2363
- var _i, providers_2, eip6963ProviderDetail, err_2;
2364
- return __generator(this, function (_a) {
2365
- switch (_a.label) {
2366
- case 0:
2367
- _a.trys.push([0, 5, , 6]);
2368
- _i = 0, providers_2 = providers;
2369
- _a.label = 1;
2370
- case 1:
2371
- if (!(_i < providers_2.length)) return [3 /*break*/, 4];
2372
- eip6963ProviderDetail = providers_2[_i];
2373
- return [4 /*yield*/, this.detect({
2374
- providerName: eip6963ProviderDetail === null || eip6963ProviderDetail === void 0 ? void 0 : eip6963ProviderDetail.info.name,
2375
- rdns: eip6963ProviderDetail === null || eip6963ProviderDetail === void 0 ? void 0 : eip6963ProviderDetail.info.rdns,
2376
- })];
2377
- case 2:
2378
- _a.sent();
2379
- _a.label = 3;
2380
- case 3:
2381
- _i++;
2382
- return [3 /*break*/, 1];
2383
- case 4: return [3 /*break*/, 6];
2384
- case 5:
2385
- err_2 = _a.sent();
2386
- logger_1.logger.error("Error detect all wallets:", err_2);
2387
- return [3 /*break*/, 6];
2388
- case 6: return [2 /*return*/];
2389
- }
2390
- });
2391
- });
2392
- };
2393
1215
  Object.defineProperty(FormoAnalytics.prototype, "provider", {
2394
1216
  get: function () {
2395
1217
  return this._provider;
@@ -2418,176 +1240,9 @@ var FormoAnalytics = /** @class */ (function () {
2418
1240
  enumerable: false,
2419
1241
  configurable: true
2420
1242
  });
2421
- FormoAnalytics.prototype.getAddress = function (provider) {
2422
- return __awaiter(this, void 0, void 0, function () {
2423
- var p, accounts, err_3, code;
2424
- return __generator(this, function (_a) {
2425
- switch (_a.label) {
2426
- case 0:
2427
- // Use EVM-specific state to avoid returning a Solana address in an EVM context
2428
- if (this._chainState.evm.address)
2429
- return [2 /*return*/, this._chainState.evm.address];
2430
- p = provider || this.provider;
2431
- if (!p) {
2432
- logger_1.logger.info("The provider is not set");
2433
- return [2 /*return*/, null];
2434
- }
2435
- _a.label = 1;
2436
- case 1:
2437
- _a.trys.push([1, 3, , 4]);
2438
- return [4 /*yield*/, this.getAccounts(p)];
2439
- case 2:
2440
- accounts = _a.sent();
2441
- if (accounts && accounts.length > 0) {
2442
- return [2 /*return*/, (0, address_1.validateAndChecksumAddress)(accounts[0]) || null];
2443
- }
2444
- return [3 /*break*/, 4];
2445
- case 3:
2446
- err_3 = _a.sent();
2447
- code = err_3 === null || err_3 === void 0 ? void 0 : err_3.code;
2448
- if (code !== 4001) {
2449
- logger_1.logger.error("FormoAnalytics::getAccounts: eth_accounts threw an error", err_3);
2450
- }
2451
- return [2 /*return*/, null];
2452
- case 4: return [2 /*return*/, null];
2453
- }
2454
- });
2455
- });
2456
- };
2457
- FormoAnalytics.prototype.getAccounts = function (provider) {
2458
- return __awaiter(this, void 0, void 0, function () {
2459
- var p, res, err_4, code;
2460
- return __generator(this, function (_a) {
2461
- switch (_a.label) {
2462
- case 0:
2463
- p = provider || this.provider;
2464
- _a.label = 1;
2465
- case 1:
2466
- _a.trys.push([1, 3, , 4]);
2467
- return [4 /*yield*/, (p === null || p === void 0 ? void 0 : p.request({
2468
- method: "eth_accounts",
2469
- }))];
2470
- case 2:
2471
- res = _a.sent();
2472
- if (!res || res.length === 0)
2473
- return [2 /*return*/, null];
2474
- return [2 /*return*/, res
2475
- .map(function (e) { return (0, address_1.validateAndChecksumAddress)(e); })
2476
- .filter(function (e) { return e !== undefined; })];
2477
- case 3:
2478
- err_4 = _a.sent();
2479
- code = err_4 === null || err_4 === void 0 ? void 0 : err_4.code;
2480
- if (code !== 4001) {
2481
- logger_1.logger.error("FormoAnalytics::getAccounts: eth_accounts threw an error", err_4);
2482
- }
2483
- return [2 /*return*/, null];
2484
- case 4: return [2 /*return*/];
2485
- }
2486
- });
2487
- });
2488
- };
2489
- /**
2490
- * Resolve the chain an autocaptured request actually ran on.
2491
- *
2492
- * `_evmChainId` is maintained by `chainChanged` from whichever provider is
2493
- * currently active. When a request arrives from a *different* tracked
2494
- * provider - which happens whenever a visitor has two wallets installed -
2495
- * that cached value describes the wrong wallet, and tagging the event with
2496
- * it silently mis-attributes the chain.
2497
- *
2498
- * Answered entirely from a per-provider snapshot. This is deliberately
2499
- * SYNCHRONOUS and never issues an RPC.
2500
- *
2501
- * An earlier version called `eth_chainId` on the signing provider and
2502
- * time-boxed it with `Promise.race`. That is not safe: the race abandons the
2503
- * SDK's promise but cannot cancel the provider's request. On a transport
2504
- * that serializes - WalletConnect's relay socket, the very case this path
2505
- * exists to serve - an abandoned lookup stays at the head of the wallet's
2506
- * queue, and every later RPC the dapp makes queues behind it until reload.
2507
- * Mislabelling a chain is a reporting defect; wedging the user's wallet is
2508
- * not acceptable to avoid one.
2509
- *
2510
- * When nothing is known, this reports 0 ("unknown") rather than guessing
2511
- * with the active provider's chain, which is known-wrong for another wallet.
2512
- */
2513
- FormoAnalytics.prototype.resolveChainIdForProvider = function (provider) {
2514
- if (provider) {
2515
- var known = this._providerChainIds.get(provider);
2516
- if (known)
2517
- return known;
2518
- // Only the active provider's chain is described by the central cache.
2519
- if (provider === this._provider && this._evmChainId) {
2520
- return this._evmChainId;
2521
- }
2522
- // A tracked provider we have never heard a chain from. Deliberately no
2523
- // fall back to `_evmChainId`: it belongs to a different wallet.
2524
- return 0;
2525
- }
2526
- return this._evmChainId || 0;
2527
- };
2528
- /**
2529
- * Record a provider's chain. Fed by `chainChanged` and `connect`, and by the
2530
- * one-off probe at tracking time - never from inside a user request.
2531
- */
2532
- FormoAnalytics.prototype.rememberProviderChain = function (provider, chainId) {
2533
- if (!provider || !chainId)
2534
- return;
2535
- // Any observation is newer than an `eth_chainId` still in flight for this
2536
- // provider.
2537
- this.bumpProviderChainGeneration(provider);
2538
- this._providerChainIds.set(provider, chainId);
2539
- // If this IS the active provider, central state has to follow. Recording
2540
- // it only per provider left `currentChainId` on a chain restored from a
2541
- // previous session, and the unscoped events that fall back to it were sent
2542
- // despite an exclusion covering the chain the wallet was really on.
2543
- //
2544
- // Guarded on EVM already being the active namespace. `setChainState()`
2545
- // makes whichever namespace it touches active, so syncing here
2546
- // unconditionally let a background EVM wallet's chain report steal the
2547
- // active slot from a live Solana wallet, and every later page or track
2548
- // event was attributed to the wrong wallet entirely.
2549
- if (provider === this._provider &&
2550
- this._activeNamespace === "evm" &&
2551
- this._evmChainId !== chainId) {
2552
- this.setChainState('evm', { chainId: chainId });
2553
- }
2554
- };
2555
- /** Advance and return this provider's chain-observation generation. */
2556
- FormoAnalytics.prototype.bumpProviderChainGeneration = function (provider) {
2557
- var _a;
2558
- var next = ((_a = this._providerChainGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) + 1;
2559
- this._providerChainGenerations.set(provider, next);
2560
- return next;
2561
- };
2562
- /**
2563
- * Seed a provider's chain from whatever it already exposes synchronously.
2564
- *
2565
- * Most EIP-1193 implementations carry a `chainId` property (MetaMask,
2566
- * WalletConnect, Coinbase). Reading it costs nothing and cannot block.
2567
- *
2568
- * There is deliberately no RPC fallback. An earlier version probed with
2569
- * `eth_chainId` when a provider was first tracked, on the theory that
2570
- * tracking time is off the user's critical path. It is not: a serialized
2571
- * transport has ONE queue, so a stalled probe sits in front of every later
2572
- * signature and transaction the dapp makes. It could also land out of order
2573
- * - a slow probe response overwriting a newer `chainChanged` - and relabel
2574
- * events onto a chain the wallet had already left.
2575
- *
2576
- * A provider that exposes nothing stays unknown until it emits
2577
- * `chainChanged` or `connect`, and unknown is reported honestly as 0.
2578
- */
2579
- FormoAnalytics.prototype.seedProviderChainFromState = function (provider) {
2580
- var raw = provider.chainId;
2581
- var chainId = typeof raw === "string"
2582
- ? (0, chain_1.parseChainId)(raw)
2583
- : typeof raw === "number"
2584
- ? raw
2585
- : undefined;
2586
- this.rememberProviderChain(provider, chainId);
2587
- };
2588
1243
  FormoAnalytics.prototype.getCurrentChainId = function (provider) {
2589
1244
  return __awaiter(this, void 0, void 0, function () {
2590
- var p, chainIdHex, err_5;
1245
+ var p, chainIdHex, err_2;
2591
1246
  return __generator(this, function (_a) {
2592
1247
  switch (_a.label) {
2593
1248
  case 0:
@@ -2610,547 +1265,26 @@ var FormoAnalytics = /** @class */ (function () {
2610
1265
  }
2611
1266
  return [2 /*return*/, (0, chain_1.parseChainId)(chainIdHex)];
2612
1267
  case 3:
2613
- err_5 = _a.sent();
2614
- logger_1.logger.error("eth_chainId threw an error:", err_5);
1268
+ err_2 = _a.sent();
1269
+ logger_1.logger.error("eth_chainId threw an error:", err_2);
2615
1270
  return [2 /*return*/, 0];
2616
1271
  case 4: return [2 /*return*/];
2617
1272
  }
2618
1273
  });
2619
1274
  });
2620
1275
  };
2621
- FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
2622
- // Intentionally not read. Kept for positional call-site arity.
2623
- _response, chainId, provider) {
2624
- var _a;
2625
- var rawAddress = method === "personal_sign"
2626
- ? params[1]
2627
- : params[0];
2628
- var validAddress = (0, address_1.validateAndChecksumAddress)(rawAddress);
2629
- if (!validAddress) {
2630
- throw new Error("Invalid address in signature payload: ".concat(rawAddress));
2631
- }
2632
- var effectiveChainId = (_a = chainId !== null && chainId !== void 0 ? chainId : this._evmChainId) !== null && _a !== void 0 ? _a : undefined;
2633
- // Only the active provider may write central wallet state - see the same
2634
- // guard in buildTransactionEventPayload.
2635
- if (!provider || provider === this._provider || !this._provider) {
2636
- this.backfillActiveWallet(validAddress, effectiveChainId, provider);
2637
- }
2638
- var basePayload = {
2639
- chainId: effectiveChainId,
2640
- address: validAddress,
2641
- };
2642
- if (method === "personal_sign") {
2643
- var message = Buffer.from(params[0].slice(2), "hex").toString("utf8");
2644
- return __assign(__assign({}, basePayload), { message: message });
2645
- }
2646
- // eth_signTypedData*: params[1] is the full EIP-712 struct.
2647
- return __assign(__assign({}, basePayload), { message: params[1] });
2648
- };
2649
- FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider,
2650
- /**
2651
- * Chain resolved once for this request's whole lifecycle. Passing it keeps
2652
- * every status of one transaction on the same chain even if the user
2653
- * switches network while the wallet prompt is open.
2654
- */
2655
- capturedChainId) {
2656
- return __awaiter(this, void 0, void 0, function () {
2657
- var _a, data, from, to, value, validAddress, chainId;
2658
- return __generator(this, function (_b) {
2659
- _a = params[0], data = _a.data, from = _a.from, to = _a.to, value = _a.value;
2660
- validAddress = (0, address_1.validateAndChecksumAddress)(from);
2661
- if (!validAddress) {
2662
- throw new Error("Invalid address in transaction payload: ".concat(from));
2663
- }
2664
- chainId = capturedChainId !== null && capturedChainId !== void 0 ? capturedChainId : this.resolveChainIdForProvider(provider);
2665
- // Only the ACTIVE provider may write central wallet state. A request from
2666
- // a second, non-active wallet would otherwise overwrite the active
2667
- // provider's address and chain, and every later request through the active
2668
- // provider would then trust the other wallet's chain - persistent
2669
- // mis-attribution, and a way around `excludeChains`.
2670
- if (!provider || provider === this._provider || !this._provider) {
2671
- this.backfillActiveWallet(validAddress, chainId, provider);
2672
- }
2673
- return [2 /*return*/, {
2674
- chainId: chainId,
2675
- data: data,
2676
- address: validAddress,
2677
- to: to,
2678
- value: value,
2679
- }];
2680
- });
2681
- });
2682
- };
2683
- /**
2684
- * Persist an EVM address discovered through autocapture (signature / transaction)
2685
- * as the current EVM address when none is currently set. This lets subsequent
2686
- * track()/page() calls carry the address even when the underlying wallet never
2687
- * fires an EIP-1193 `accountsChanged` event (embedded wallets, smart accounts,
2688
- * social-login wrappers). If `accountsChanged` later fires it overwrites this
2689
- * value in the normal way; existing connections are never clobbered.
2690
- */
2691
- FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId, provider) {
2692
- // Refuse a chain the provider has since moved off.
2693
- //
2694
- // A request captures its chain once and reuses that snapshot for every
2695
- // status it emits, which is right for the event payload - a confirmation
2696
- // must not be relabelled mid-flight. But writing that captured value back
2697
- // into central state on the LATER statuses restored a chain the wallet had
2698
- // already left, and the unscoped events that fall back to it then bypassed
2699
- // an exclusion that should have caught them.
2700
- if (provider && chainId !== undefined && chainId !== 0) {
2701
- var current = this._providerChainIds.get(provider);
2702
- if (current !== undefined && current !== chainId) {
2703
- chainId = current;
2704
- }
2705
- }
2706
- // `0` is kept deliberately. It means "could not resolve", and persisting
2707
- // it is what lets the exclusion gate refuse the unscoped events - `page`,
2708
- // `track`, `identify` - that carry no chain of their own and fall back to
2709
- // `currentChainId`. Erasing it to `undefined` looked tidier but removed
2710
- // the only marker distinguishing "unknown" from "no wallet yet", and those
2711
- // events then went out attributed to a wallet that might be sitting on an
2712
- // excluded chain.
2713
- // Never learn identity while suppressed (opt-out / timezone / excluded host
2714
- // or path). A signature/transaction observed on an excluded route must not
2715
- // populate currentAddress for later allowed-page events.
2716
- if (this.isTrackingSuppressed())
2717
- return;
2718
- var known = this._evmAddress;
2719
- if (known) {
2720
- // Same wallet, newer chain: correct it rather than returning.
2721
- //
2722
- // A persisted wallet restores a chain from a previous session. If the
2723
- // provider has since moved - to an excluded chain, or to one we cannot
2724
- // resolve - the autocaptured event is gated correctly, but the stale
2725
- // restored chain stayed in `currentChainId` and the unscoped events
2726
- // that fall back to it went out under an exclusion that should have
2727
- // caught them.
2728
- if (chainId !== undefined &&
2729
- known.toLowerCase() === address.toLowerCase() &&
2730
- this._evmChainId !== chainId) {
2731
- this.setChainState('evm', { address: known, chainId: chainId });
2732
- }
2733
- // A DIFFERENT address is another wallet's business; never overwrite.
2734
- return;
2735
- }
2736
- this.setChainState('evm', { address: address, chainId: chainId });
2737
- };
2738
- /**
2739
- * Apply an EVM autocapture connect/switch while tracking is suppressed
2740
- * (opt-out / timezone / excluded host or path): never LEARN the wallet, but
2741
- * if it is a switch away from an already-learned EVM wallet, drop the stale
2742
- * one (which also clears the active-wallet cookie) so it can't attach to a
2743
- * later allowed-page event.
2744
- */
2745
- FormoAnalytics.prototype.clearStaleEvmWalletOnSwitchWhileSuppressed = function (address) {
2746
- var evmAddress = this._chainState.evm.address;
2747
- var incoming = (0, address_1.validateAndChecksumAddress)(address);
2748
- if (evmAddress && incoming && incoming !== evmAddress) {
2749
- this.clearChainState('evm');
2750
- }
2751
- };
2752
- /**
2753
- * Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
2754
- */
2755
- FormoAnalytics.prototype.pollTransactionReceipt = function (provider_2, transactionHash_2, payload_1) {
2756
- return __awaiter(this, arguments, void 0, function (provider, transactionHash, payload, maxAttempts, intervalMs) {
2757
- var attempts, poll;
2758
- var _this = this;
2759
- if (maxAttempts === void 0) { maxAttempts = 10; }
2760
- if (intervalMs === void 0) { intervalMs = 3000; }
2761
- return __generator(this, function (_a) {
2762
- attempts = 0;
2763
- if (!provider)
2764
- return [2 /*return*/];
2765
- poll = function () { return __awaiter(_this, void 0, void 0, function () {
2766
- var receipt, e_12;
2767
- return __generator(this, function (_a) {
2768
- switch (_a.label) {
2769
- case 0:
2770
- _a.trys.push([0, 2, , 3]);
2771
- return [4 /*yield*/, provider.request({
2772
- method: "eth_getTransactionReceipt",
2773
- params: [transactionHash],
2774
- })];
2775
- case 1:
2776
- receipt = (_a.sent());
2777
- if (receipt) {
2778
- // status: 1 = success, 0 = reverted
2779
- if (receipt.status === "0x1" || receipt.status === 1) {
2780
- this.transaction(__assign(__assign({ status: types_1.TransactionStatus.CONFIRMED }, payload), { transactionHash: transactionHash }));
2781
- return [2 /*return*/];
2782
- }
2783
- else if (receipt.status === "0x0" || receipt.status === 0) {
2784
- this.transaction(__assign(__assign({ status: types_1.TransactionStatus.REVERTED }, payload), { transactionHash: transactionHash }));
2785
- return [2 /*return*/];
2786
- }
2787
- }
2788
- return [3 /*break*/, 3];
2789
- case 2:
2790
- e_12 = _a.sent();
2791
- logger_1.logger.error("Error polling transaction receipt", e_12);
2792
- return [3 /*break*/, 3];
2793
- case 3:
2794
- attempts++;
2795
- if (attempts < maxAttempts) {
2796
- setTimeout(poll, intervalMs);
2797
- }
2798
- return [2 /*return*/];
2799
- }
2800
- });
2801
- }); };
2802
- poll();
2803
- return [2 /*return*/];
2804
- });
2805
- });
2806
- };
2807
- FormoAnalytics.prototype.removeProviderListeners = function (provider) {
2808
- var listeners = this._providerListenersMap.get(provider);
2809
- if (!listeners)
2810
- return;
2811
- for (var _i = 0, _a = Object.entries(listeners); _i < _a.length; _i++) {
2812
- var _b = _a[_i], event_2 = _b[0], fn = _b[1];
2813
- try {
2814
- provider.removeListener(event_2, fn);
2815
- }
2816
- catch (e) {
2817
- logger_1.logger.warn("Failed to remove listener for ".concat(String(event_2)), e);
2818
- }
2819
- }
2820
- this._providerListenersMap.delete(provider);
2821
- };
2822
1276
  // Explicitly untrack a provider: remove listeners, clear wrapper flag
2823
1277
  // and tracking
2824
- FormoAnalytics.prototype.untrackProvider = function (provider) {
2825
- try {
2826
- this.removeProviderListeners(provider);
2827
- this._trackedProviders.delete(provider);
2828
- if (this._provider === provider) {
2829
- this.clearActiveProvider();
2830
- }
2831
- }
2832
- catch (e) {
2833
- logger_1.logger.warn("Failed to untrack provider", e);
2834
- }
2835
- };
2836
1278
  // Debug/monitoring helpers
2837
1279
  FormoAnalytics.prototype.getTrackedProvidersCount = function () {
2838
- return this._trackedProviders.size;
1280
+ return this.evm.counts.trackedProviders;
2839
1281
  };
2840
1282
  /**
2841
1283
  * Get current provider state for debugging
2842
1284
  * @returns Object containing current provider state information
2843
1285
  */
2844
1286
  FormoAnalytics.prototype.getProviderState = function () {
2845
- return {
2846
- totalProviders: this._providers.length,
2847
- trackedProviders: this._trackedProviders.size,
2848
- seenProviders: this._seenProviders.size,
2849
- activeProvider: !!this._provider,
2850
- };
2851
- };
2852
- /**
2853
- * Clean up providers that are no longer available
2854
- * This helps maintain consistent state and prevents memory leaks
2855
- */
2856
- FormoAnalytics.prototype.cleanupUnavailableProviders = function () {
2857
- // Remove providers that are no longer in the current providers list
2858
- var currentProviderInstances = new Set(this._providers.map(function (detail) { return detail.provider; }));
2859
- for (var _i = 0, _a = Array.from(this._trackedProviders); _i < _a.length; _i++) {
2860
- var provider = _a[_i];
2861
- if (!currentProviderInstances.has(provider)) {
2862
- logger_1.logger.info("Cleaning up unavailable provider: ".concat(provider.constructor.name));
2863
- this.untrackProvider(provider);
2864
- }
2865
- }
2866
- };
2867
- /**
2868
- * Helper method to check if a provider is already wrapped
2869
- * @param provider The provider to check
2870
- * @param currentRequest The current request function
2871
- * @returns true if the provider is already wrapped
2872
- */
2873
- FormoAnalytics.prototype.isProviderAlreadyWrapped = function (provider, currentRequest) {
2874
- return !!(currentRequest &&
2875
- typeof currentRequest === "function" &&
2876
- currentRequest[types_1.WRAPPED_REQUEST_SYMBOL] &&
2877
- provider[types_1.WRAPPED_REQUEST_REF_SYMBOL] ===
2878
- currentRequest);
2879
- };
2880
- /**
2881
- * Handle provider mismatch by switching to the new provider and invalidating old tokens
2882
- * @param provider The new provider to switch to
2883
- */
2884
- FormoAnalytics.prototype.handleProviderMismatch = function (provider) {
2885
- // If this is a different provider, allow the switch
2886
- if (this._provider) {
2887
- // Clear any provider-specific state when switching
2888
- this.setChainState('evm', { address: undefined, chainId: undefined, provider: provider });
2889
- }
2890
- else {
2891
- this._provider = provider;
2892
- }
2893
- };
2894
- /**
2895
- * Determine which namespace a chainId belongs to.
2896
- */
2897
- FormoAnalytics.prototype.getNamespace = function (chainId) {
2898
- return (0, solana_1.isSolanaChainId)(chainId) ? 'solana' : 'evm';
2899
- };
2900
- /**
2901
- * Update per-chain state and sync the derived currentAddress/currentChainId.
2902
- * Accepts either a namespace string ('evm'/'solana') or a chainId number
2903
- * to resolve the namespace automatically. When a chainId number is passed,
2904
- * it is also stored as the namespace's chainId (unless explicitly overridden
2905
- * in the update object).
2906
- */
2907
- FormoAnalytics.prototype.setChainState = function (namespaceOrChainId, update) {
2908
- var namespace = typeof namespaceOrChainId === 'string'
2909
- ? namespaceOrChainId
2910
- : this.getNamespace(namespaceOrChainId);
2911
- var ns = this._chainState[namespace];
2912
- if ('address' in update)
2913
- ns.address = update.address;
2914
- if ('chainId' in update) {
2915
- ns.chainId = update.chainId;
2916
- }
2917
- else if (typeof namespaceOrChainId === 'number') {
2918
- ns.chainId = namespaceOrChainId;
2919
- }
2920
- if (namespace === 'evm' && 'provider' in update) {
2921
- // Displacing the active provider ends its connection as far as this SDK
2922
- // is concerned, so the connect it reported stops counting.
2923
- this.forgetConnectRecord(ns.provider, update.provider);
2924
- ns.provider = update.provider;
2925
- }
2926
- this._activeNamespace = namespace;
2927
- this.syncDerivedState();
2928
- };
2929
- /**
2930
- * Clear per-chain state for a given namespace (or chainId) and sync derived state.
2931
- */
2932
- FormoAnalytics.prototype.clearChainState = function (namespaceOrChainId) {
2933
- var namespace = typeof namespaceOrChainId === 'string'
2934
- ? namespaceOrChainId
2935
- : this.getNamespace(namespaceOrChainId);
2936
- if (namespace === 'evm') {
2937
- // Same rule: wiping the namespace drops the active provider.
2938
- this.forgetConnectRecord(this._chainState.evm.provider, undefined);
2939
- this._chainState.evm = {};
2940
- }
2941
- else {
2942
- this._chainState.solana = {};
2943
- }
2944
- this.syncDerivedState();
2945
- };
2946
- /**
2947
- * Sync validated wallet/chain state into the SDK's central state
2948
- * WITHOUT emitting an event.
2949
- *
2950
- * Integrations (e.g. the wagmi handler) must call this on every
2951
- * connect / chain-change / disconnect - even when the corresponding
2952
- * autocapture event is disabled. Otherwise `currentChainId` stays
2953
- * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2954
- * check (which keys off `currentChainId`, not the event payload) can
2955
- * be bypassed, letting wallet activity on an excluded chain still be
2956
- * collected.
2957
- *
2958
- * - valid `address` present → record per-chain + derived state
2959
- * - `address` absent → clear chain state (disconnect)
2960
- */
2961
- FormoAnalytics.prototype.syncWalletState = function (params) {
2962
- var chainId = params.chainId, address = params.address;
2963
- if (this.isTrackingSuppressed()) {
2964
- // While suppressed (opt-out / timezone / excluded host or path) we must
2965
- // never LEARN a new wallet - but we must still CLEAR stale identity.
2966
- // Otherwise a disconnect or wallet switch observed on a suppressed route
2967
- // would leave the previously-learned address in memory and in the
2968
- // active-wallet cookie, attaching it to later allowed-page events.
2969
- if (!address) {
2970
- // Disconnect: drop the affected namespace(s).
2971
- if (chainId !== undefined && chainId !== null) {
2972
- this.clearChainState(chainId);
2973
- }
2974
- else {
2975
- this.clearChainState("evm");
2976
- this.clearChainState("solana");
2977
- }
2978
- return;
2979
- }
2980
- // Address present: a switch away from the wallet already learned in this
2981
- // namespace invalidates it. Drop the stale one without learning the new
2982
- // address; a fresh connect (nothing learned yet) or a re-confirmation of
2983
- // the same address is a no-op.
2984
- if (chainId === null || chainId === undefined)
2985
- return;
2986
- var namespace = this.getNamespace(chainId);
2987
- var namespaceAddress = this._chainState[namespace].address;
2988
- var validIncoming = (0, address_1.validateAddress)(address, chainId);
2989
- if (namespaceAddress &&
2990
- validIncoming &&
2991
- validIncoming !== namespaceAddress) {
2992
- this.clearChainState(chainId);
2993
- }
2994
- return;
2995
- }
2996
- if (!address) {
2997
- if (chainId !== undefined && chainId !== null) {
2998
- this.clearChainState(chainId);
2999
- }
3000
- else {
3001
- this.clearChainState("evm");
3002
- this.clearChainState("solana");
3003
- }
3004
- return;
3005
- }
3006
- if (chainId === null || chainId === undefined)
3007
- return;
3008
- var validAddress = (0, address_1.validateAddress)(address, chainId);
3009
- if (!validAddress) {
3010
- logger_1.logger.warn("syncWalletState: invalid address (\"".concat(address, "\") for chain ").concat(chainId));
3011
- return;
3012
- }
3013
- this.setChainState(chainId, { address: validAddress });
3014
- };
3015
- /**
3016
- * Synchronize currentAddress/currentChainId from the active namespace.
3017
- * Last-connected-chain-wins: _activeNamespace takes precedence.
3018
- */
3019
- FormoAnalytics.prototype.syncDerivedState = function () {
3020
- var active = this._activeNamespace;
3021
- if (active) {
3022
- var state = this._chainState[active];
3023
- if (state.address || state.chainId) {
3024
- this.currentAddress = state.address;
3025
- this.currentChainId = state.chainId;
3026
- this.persistActiveWallet();
3027
- return;
3028
- }
3029
- }
3030
- // Fall through to the other namespace
3031
- var other = active === 'evm' ? 'solana' : 'evm';
3032
- var otherState = this._chainState[other];
3033
- if (otherState.address || otherState.chainId) {
3034
- this.currentAddress = otherState.address;
3035
- this.currentChainId = otherState.chainId;
3036
- this._activeNamespace = other;
3037
- this.persistActiveWallet();
3038
- return;
3039
- }
3040
- this.currentAddress = undefined;
3041
- this.currentChainId = undefined;
3042
- this.persistActiveWallet();
3043
- };
3044
- /**
3045
- * Persist (or clear) the current wallet snapshot in a cookie so that the
3046
- * SDK can repopulate `currentAddress`/`currentChainId` at init on the next
3047
- * page load - closing the gap between page-show and wagmi/EIP-1193
3048
- * reconnection during which track()/page() events would otherwise ship
3049
- * with an empty address.
3050
- */
3051
- FormoAnalytics.prototype.persistActiveWallet = function () {
3052
- try {
3053
- // Visitor-level suppression (opt-out or excluded timezone): purge any
3054
- // prior snapshot - these are stable for the session, so deletion is safe.
3055
- if (this.isPersistedIdentityPurgeRequired()) {
3056
- (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
3057
- return;
3058
- }
3059
- if (this.currentAddress) {
3060
- // Current-page exclusion (host/path): do not write a new snapshot while
3061
- // on an excluded route, but leave any existing cookie intact. A cookie
3062
- // written on an allowed page must survive a transient visit to an
3063
- // excluded one (passive navigation does not call this method).
3064
- if (this.isHostExcluded() || this.isPathExcluded()) {
3065
- return;
3066
- }
3067
- var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
3068
- var domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
3069
- (0, storage_1.cookie)().set(constants_1.ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + constants_1.ACTIVE_WALLET_TTL_MS).toUTCString() }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
3070
- }
3071
- else {
3072
- // No active wallet → clear the snapshot. This runs even on an excluded
3073
- // route, so a disconnect/switch observed while suppressed actively
3074
- // removes stale identity instead of leaving it for later allowed events.
3075
- (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
3076
- }
3077
- }
3078
- catch (err) {
3079
- logger_1.logger.warn("Failed to persist current wallet snapshot", err);
3080
- }
3081
- };
3082
- /**
3083
- * Seed `currentAddress`/`currentChainId` from the persisted snapshot, if
3084
- * any. Called once during construction before the first page hit fires.
3085
- */
3086
- FormoAnalytics.prototype.loadActiveWallet = function () {
3087
- try {
3088
- // Visitor-level suppression (opt-out or excluded timezone): never restore
3089
- // identity into memory; drop the stale snapshot.
3090
- if (this.isPersistedIdentityPurgeRequired()) {
3091
- (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
3092
- return;
3093
- }
3094
- // Current-page exclusion (host/path): don't restore into memory while on
3095
- // an excluded route, but keep the cookie so a later allowed-page load can
3096
- // restore it.
3097
- if (this.isHostExcluded() || this.isPathExcluded()) {
3098
- return;
3099
- }
3100
- var raw = (0, storage_1.cookie)().get(constants_1.ACTIVE_WALLET_KEY);
3101
- if (!raw)
3102
- return;
3103
- var parsed = JSON.parse(raw);
3104
- if (!(parsed === null || parsed === void 0 ? void 0 : parsed.address))
3105
- return;
3106
- var namespace = (0, solana_1.isSolanaChainId)(parsed.chainId) ? "solana" : "evm";
3107
- var validated = (0, address_1.validateAddress)(parsed.address, parsed.chainId);
3108
- if (!validated) {
3109
- (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
3110
- return;
3111
- }
3112
- var ns = this._chainState[namespace];
3113
- ns.address = validated;
3114
- if (parsed.chainId !== undefined)
3115
- ns.chainId = parsed.chainId;
3116
- this._activeNamespace = namespace;
3117
- this.currentAddress = validated;
3118
- this.currentChainId = parsed.chainId;
3119
- }
3120
- catch (err) {
3121
- logger_1.logger.warn("Failed to restore persisted wallet snapshot", err);
3122
- (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
3123
- }
3124
- };
3125
- /**
3126
- * Helper method to clear the active provider state
3127
- * Centralizes provider clearing logic for consistency
3128
- */
3129
- FormoAnalytics.prototype.clearActiveProvider = function () {
3130
- this._provider = undefined;
3131
- };
3132
- /**
3133
- * Helper method to safely add a provider detail to _providers array, ensuring no duplicates
3134
- * @param detail The provider detail to add
3135
- * @returns true if the provider was added, false if it was already present
3136
- */
3137
- FormoAnalytics.prototype.safeAddProviderDetail = function (detail) {
3138
- var provider = detail === null || detail === void 0 ? void 0 : detail.provider;
3139
- if (!provider)
3140
- return false;
3141
- // Check if provider already exists in _providers array
3142
- var alreadyExists = this._providers.some(function (existing) { return existing.provider === provider; });
3143
- if (!alreadyExists) {
3144
- // Add to providers array and mark as seen
3145
- this._providers = __spreadArray(__spreadArray([], this._providers, true), [detail], false);
3146
- this._seenProviders.add(provider);
3147
- return true;
3148
- }
3149
- else {
3150
- // Ensure provider is marked as seen even if it already exists in _providers
3151
- this._seenProviders.add(provider);
3152
- return false;
3153
- }
1287
+ return __assign(__assign({}, this.evm.counts), { activeProvider: !!this._provider });
3154
1288
  };
3155
1289
  return FormoAnalytics;
3156
1290
  }());