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