@formo/analytics 1.35.0 → 1.35.2

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 (62) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +134 -0
  2. package/dist/cjs/src/FormoAnalytics.js +647 -178
  3. package/dist/cjs/src/constants/base.d.ts +0 -1
  4. package/dist/cjs/src/constants/base.js +1 -3
  5. package/dist/cjs/src/event/EventFactory.js +1 -1
  6. package/dist/cjs/src/utils/mergeDeepRight.d.ts +22 -0
  7. package/dist/cjs/src/utils/mergeDeepRight.js +54 -0
  8. package/dist/cjs/src/version.d.ts +1 -1
  9. package/dist/cjs/src/version.js +1 -1
  10. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +193 -1
  11. package/dist/cjs/src/wagmi/WagmiEventHandler.js +1317 -77
  12. package/dist/cjs/src/wagmi/types.d.ts +6 -0
  13. package/dist/esm/src/FormoAnalytics.d.ts +134 -0
  14. package/dist/esm/src/FormoAnalytics.js +647 -178
  15. package/dist/esm/src/constants/base.d.ts +0 -1
  16. package/dist/esm/src/constants/base.js +0 -2
  17. package/dist/esm/src/event/EventFactory.js +1 -1
  18. package/dist/esm/src/utils/mergeDeepRight.d.ts +22 -0
  19. package/dist/esm/src/utils/mergeDeepRight.js +51 -0
  20. package/dist/esm/src/version.d.ts +1 -1
  21. package/dist/esm/src/version.js +1 -1
  22. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +193 -1
  23. package/dist/esm/src/wagmi/WagmiEventHandler.js +1315 -76
  24. package/dist/esm/src/wagmi/types.d.ts +6 -0
  25. package/dist/index.umd.min.js +1 -1
  26. package/package.json +2 -11
  27. package/dist/cjs/src/ramda/internal/_curry1.d.ts +0 -10
  28. package/dist/cjs/src/ramda/internal/_curry1.js +0 -26
  29. package/dist/cjs/src/ramda/internal/_curry2.d.ts +0 -10
  30. package/dist/cjs/src/ramda/internal/_curry2.js +0 -43
  31. package/dist/cjs/src/ramda/internal/_curry3.d.ts +0 -10
  32. package/dist/cjs/src/ramda/internal/_curry3.js +0 -74
  33. package/dist/cjs/src/ramda/internal/_has.d.ts +0 -2
  34. package/dist/cjs/src/ramda/internal/_has.js +0 -7
  35. package/dist/cjs/src/ramda/internal/_isObject.d.ts +0 -2
  36. package/dist/cjs/src/ramda/internal/_isObject.js +0 -7
  37. package/dist/cjs/src/ramda/internal/_isPlaceholder.d.ts +0 -2
  38. package/dist/cjs/src/ramda/internal/_isPlaceholder.js +0 -7
  39. package/dist/cjs/src/ramda/mergeDeepRight.d.ts +0 -3
  40. package/dist/cjs/src/ramda/mergeDeepRight.js +0 -14
  41. package/dist/cjs/src/ramda/mergeDeepWithKey.d.ts +0 -3
  42. package/dist/cjs/src/ramda/mergeDeepWithKey.js +0 -20
  43. package/dist/cjs/src/ramda/mergeWithKey.d.ts +0 -3
  44. package/dist/cjs/src/ramda/mergeWithKey.js +0 -26
  45. package/dist/esm/src/ramda/internal/_curry1.d.ts +0 -10
  46. package/dist/esm/src/ramda/internal/_curry1.js +0 -20
  47. package/dist/esm/src/ramda/internal/_curry2.d.ts +0 -10
  48. package/dist/esm/src/ramda/internal/_curry2.js +0 -37
  49. package/dist/esm/src/ramda/internal/_curry3.d.ts +0 -10
  50. package/dist/esm/src/ramda/internal/_curry3.js +0 -68
  51. package/dist/esm/src/ramda/internal/_has.d.ts +0 -2
  52. package/dist/esm/src/ramda/internal/_has.js +0 -4
  53. package/dist/esm/src/ramda/internal/_isObject.d.ts +0 -2
  54. package/dist/esm/src/ramda/internal/_isObject.js +0 -4
  55. package/dist/esm/src/ramda/internal/_isPlaceholder.d.ts +0 -2
  56. package/dist/esm/src/ramda/internal/_isPlaceholder.js +0 -4
  57. package/dist/esm/src/ramda/mergeDeepRight.d.ts +0 -3
  58. package/dist/esm/src/ramda/mergeDeepRight.js +0 -9
  59. package/dist/esm/src/ramda/mergeDeepWithKey.d.ts +0 -3
  60. package/dist/esm/src/ramda/mergeDeepWithKey.js +0 -15
  61. package/dist/esm/src/ramda/mergeWithKey.d.ts +0 -3
  62. package/dist/esm/src/ramda/mergeWithKey.js +0 -21
@@ -97,6 +97,39 @@ var FormoAnalytics = /** @class */ (function () {
97
97
  evm: {},
98
98
  solana: {},
99
99
  };
100
+ /**
101
+ * Last known chain per tracked provider, fed by `chainChanged` / `connect`
102
+ * and by a one-off probe at tracking time. Read synchronously when labelling
103
+ * an autocaptured event, so the wallet's request transport is never used for
104
+ * analytics. Weak so an untracked provider can be collected.
105
+ */
106
+ /**
107
+ * The connect this SDK has actually REPORTED for a provider.
108
+ *
109
+ * Deduplicating on "is an address known" was wrong in both directions.
110
+ * `onConnected` and `onAccountsChanged` both observe one connection, so
111
+ * something has to stop them double-reporting - but an address can be
112
+ * present without a connect ever having been sent: restored from the
113
+ * active-wallet cookie, or reported with an unresolved chain and then
114
+ * refused by `tracking.excludeChains`. Suppressing on address presence lost
115
+ * the connect entirely in those cases.
116
+ *
117
+ * Records what was reported, so a later event can be recognised as a
118
+ * genuine improvement on it rather than a duplicate.
119
+ */
120
+ this._announcedConnect = new WeakMap();
121
+ this._providerChainIds = new WeakMap();
122
+ /**
123
+ * Bumped on every chain observation, PER PROVIDER. An `eth_chainId` answer
124
+ * that resolves after a newer observation for the same provider must not
125
+ * overwrite it.
126
+ *
127
+ * Deliberately per provider, not per SDK instance: a global counter meant
128
+ * any activity on wallet B discarded a perfectly valid in-flight answer for
129
+ * wallet A, leaving A at chain 0 - and, since an unresolved chain fails
130
+ * closed, dropping all of A's events whenever `excludeChains` is set.
131
+ */
132
+ this._providerChainGenerations = new WeakMap();
100
133
  this._providerListenersMap = new Map();
101
134
  /**
102
135
  * EIP-6963 provider details discovered through the browser
@@ -151,7 +184,18 @@ var FormoAnalytics = /** @class */ (function () {
151
184
  this.transaction = this.transaction.bind(this);
152
185
  this.detect = this.detect.bind(this);
153
186
  this.track = this.track.bind(this);
187
+ this.page = this.page.bind(this);
188
+ this.reset = this.reset.bind(this);
189
+ this.cleanup = this.cleanup.bind(this);
190
+ this.optOutTracking = this.optOutTracking.bind(this);
191
+ this.optInTracking = this.optInTracking.bind(this);
192
+ this.hasOptedOutTracking = this.hasOptedOutTracking.bind(this);
154
193
  this.isAutocaptureEnabled = this.isAutocaptureEnabled.bind(this);
194
+ this.syncPrivyActiveChain = this.syncPrivyActiveChain.bind(this);
195
+ this.isTrackingSuppressed = this.isTrackingSuppressed.bind(this);
196
+ this.getTrackedProvidersCount = this.getTrackedProvidersCount.bind(this);
197
+ this.getProviderState = this.getProviderState.bind(this);
198
+ this.syncWalletState = this.syncWalletState.bind(this);
155
199
  // Initialize logger with configuration from options
156
200
  logger_1.Logger.init({
157
201
  enabled: ((_b = options.logger) === null || _b === void 0 ? void 0 : _b.enabled) || false,
@@ -214,11 +258,37 @@ var FormoAnalytics = /** @class */ (function () {
214
258
  return this._chainState.evm.provider;
215
259
  },
216
260
  set: function (value) {
261
+ var previous = this._chainState.evm.provider;
262
+ // A provider that stops being the active one has, from this SDK's point of
263
+ // view, ended its connection - so the connect it reported must stop
264
+ // counting. Otherwise toggling between two installed wallets silently
265
+ // loses every connect after the first: A's stale record suppresses the
266
+ // connect when the user comes back to it.
267
+ //
268
+ // Done in the setter rather than at each switch site because the active
269
+ // provider is reassigned from several paths - `accountsChanged`,
270
+ // `chainChanged`, `connect`, `handleProviderMismatch`, `untrackProvider` -
271
+ // and any one of them missed would reopen the same hole.
272
+ this.forgetConnectRecord(previous, value);
217
273
  this._chainState.evm.provider = value;
218
274
  },
219
275
  enumerable: false,
220
276
  configurable: true
221
277
  });
278
+ /**
279
+ * Drop a provider's reported-connect record when it stops being active.
280
+ *
281
+ * The active provider is displaced from several places, and not all of them
282
+ * go through the `_provider` setter: `setChainState()` writes
283
+ * `_chainState.evm.provider` directly and `clearChainState()` replaces the
284
+ * whole namespace. Guarding only the setter left A's record alive across a
285
+ * real A-to-B switch, so returning to A was suppressed.
286
+ */
287
+ FormoAnalytics.prototype.forgetConnectRecord = function (previous, next) {
288
+ if (previous && previous !== next) {
289
+ this._announcedConnect.delete(previous);
290
+ }
291
+ };
222
292
  Object.defineProperty(FormoAnalytics.prototype, "_evmAddress", {
223
293
  get: function () {
224
294
  return this._chainState.evm.address;
@@ -835,6 +905,18 @@ var FormoAnalytics = /** @class */ (function () {
835
905
  * Opt out of tracking.
836
906
  * @returns {void}
837
907
  */
908
+ /**
909
+ * Whether an event carrying this chain would currently be sent.
910
+ *
911
+ * Exposed for integrations that keep their own "already reported" state.
912
+ * `syncWalletState()` can accept a wallet that `trackEvent()` then drops -
913
+ * `tracking: false`, or a chain in `excludeChains` - and an integration that
914
+ * marked it as reported would stay silent about that wallet even after the
915
+ * configuration changed to allow it.
916
+ */
917
+ FormoAnalytics.prototype.willTrackEvent = function (chainId) {
918
+ return this.shouldTrack(chainId);
919
+ };
838
920
  FormoAnalytics.prototype.optOutTracking = function () {
839
921
  logger_1.logger.info("Opting out of tracking");
840
922
  // Set opt-out flag in persistent storage using direct cookie access
@@ -852,9 +934,15 @@ var FormoAnalytics = /** @class */ (function () {
852
934
  * @returns {void}
853
935
  */
854
936
  FormoAnalytics.prototype.optInTracking = function () {
937
+ var _a;
855
938
  logger_1.logger.info("Opting back into tracking");
856
939
  // Remove opt-out flag
857
940
  (0, consent_1.removeConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY);
941
+ // A wallet connected while opted out was declined by syncWalletState, and
942
+ // an unchanged wagmi connection produces no status or chain update to
943
+ // retry on. Without this, opting back in leaves that wallet invisible for
944
+ // the rest of the page load.
945
+ (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
858
946
  logger_1.logger.info("Successfully opted back into tracking");
859
947
  };
860
948
  /**
@@ -896,20 +984,43 @@ var FormoAnalytics = /** @class */ (function () {
896
984
  // This ensures currentAddress, currentChainId, and _provider are always up-to-date
897
985
  // Event emission is controlled conditionally inside the handlers
898
986
  this.registerAccountsChangedListener(provider);
899
- // Register other listeners based on autocapture configuration
900
- if (this.isAutocaptureEnabled("chain")) {
901
- this.registerChainChangedListener(provider);
902
- }
987
+ // `chainChanged` and `connect` are registered UNCONDITIONALLY: they are
988
+ // how this provider's chain is observed, and every signature and
989
+ // transaction has to be labelled with it. Gating registration on
990
+ // `autocapture.chain` conflated observing a chain with reporting one, so
991
+ // `{ chain: false, signature: true }` left the chain frozen at whatever
992
+ // was first seen - a switch to an excluded chain went unnoticed and its
993
+ // signatures were emitted under the old, allowed chain. Whether an
994
+ // event is emitted is decided inside each handler.
995
+ this.registerChainChangedListener(provider);
903
996
  if (this.isAutocaptureEnabled("connect")) {
904
997
  this.registerConnectListener(provider);
905
998
  }
999
+ else {
1000
+ // Observation only. The full connect handler calls `getAddress()`,
1001
+ // which issues `eth_accounts`, and nothing analytics-only may go on
1002
+ // the wallet's transport - a stalled request there sits in front of
1003
+ // the next signature the dapp makes. The chain rides along on the
1004
+ // event itself, so it costs nothing to record.
1005
+ this.registerConnectChainObserver(provider);
1006
+ }
1007
+ // Seed the chain from the provider's own synchronous state if it exposes
1008
+ // one. Deliberately a property read and never an RPC: see
1009
+ // resolveChainIdForProvider for why nothing analytics-only may go on the
1010
+ // wallet's transport.
1011
+ this.seedProviderChainFromState(provider);
906
1012
  if (this.isAutocaptureEnabled("signature") || this.isAutocaptureEnabled("transaction")) {
907
1013
  this.registerRequestListeners(provider);
908
1014
  }
909
1015
  else {
910
1016
  logger_1.logger.debug("TrackProvider: Skipping request wrapping (both signature and transaction autocapture disabled)");
911
1017
  }
912
- if (this.isAutocaptureEnabled("disconnect")) {
1018
+ // Registered UNCONDITIONALLY: this listener also ends the provider's
1019
+ // reported-connect record, and with `{ connect: true, disconnect: false }`
1020
+ // a wallet that disconnected and reconnected would otherwise find its
1021
+ // old record still standing and have the new connect suppressed. Whether
1022
+ // a disconnect EVENT is emitted is decided inside the handler.
1023
+ {
913
1024
  this.registerDisconnectListener(provider);
914
1025
  }
915
1026
  // Only add to tracked providers after all listeners are successfully registered
@@ -1003,6 +1114,10 @@ var FormoAnalytics = /** @class */ (function () {
1003
1114
  evmChainId: this._evmChainId,
1004
1115
  providerMatch: this._provider === provider,
1005
1116
  });
1117
+ // The reported connect ends with the connection, so a genuine
1118
+ // reconnect later reports again rather than being taken for a
1119
+ // duplicate.
1120
+ this._announcedConnect.delete(provider);
1006
1121
  if (!this.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 5];
1007
1122
  _a.label = 1;
1008
1123
  case 1:
@@ -1151,9 +1266,7 @@ var FormoAnalytics = /** @class */ (function () {
1151
1266
  if (this._provider === provider && address === this._evmAddress) {
1152
1267
  return [2 /*return*/];
1153
1268
  }
1154
- return [4 /*yield*/, this.getCurrentChainId(provider)];
1155
- case 26:
1156
- nextChainId = _a.sent();
1269
+ nextChainId = this.resolveChainIdForProvider(provider);
1157
1270
  wasDisconnected = !this._evmAddress;
1158
1271
  // Update state regardless of whether connect *event* tracking is enabled,
1159
1272
  // so disconnect events keep valid address/chainId values. (excludeChains is
@@ -1167,7 +1280,8 @@ var FormoAnalytics = /** @class */ (function () {
1167
1280
  }
1168
1281
  providerInfo = this.getProviderInfo(provider);
1169
1282
  effectiveChainId = nextChainId || 0;
1170
- if (this.isAutocaptureEnabled("connect")) {
1283
+ if (this.isAutocaptureEnabled("connect") &&
1284
+ this.shouldReportConnect(provider, address)) {
1171
1285
  logger_1.logger.info("OnAccountsChanged: Detected wallet connection, emitting connect event", {
1172
1286
  chainId: nextChainId,
1173
1287
  address: address,
@@ -1179,6 +1293,7 @@ var FormoAnalytics = /** @class */ (function () {
1179
1293
  if (effectiveChainId === 0) {
1180
1294
  logger_1.logger.info("OnAccountsChanged: Using fallback chainId 0 for connect event");
1181
1295
  }
1296
+ this.markConnectReported(provider, address, effectiveChainId);
1182
1297
  this.connect({
1183
1298
  chainId: effectiveChainId,
1184
1299
  address: address,
@@ -1216,46 +1331,140 @@ var FormoAnalytics = /** @class */ (function () {
1216
1331
  };
1217
1332
  FormoAnalytics.prototype.onChainChanged = function (provider, chainIdHex) {
1218
1333
  return __awaiter(this, void 0, void 0, function () {
1219
- var nextChainId;
1334
+ var nextChainId, error_3;
1220
1335
  return __generator(this, function (_a) {
1221
- logger_1.logger.info("onChainChanged", chainIdHex);
1222
- nextChainId = (0, chain_1.parseChainId)(chainIdHex);
1223
- // Only handle chain changes for the active provider (or if none is set yet)
1224
- if (this.isProviderMismatch(provider)) {
1225
- this.handleProviderMismatch(provider);
1226
- }
1227
- // Chain changes only matter for connected users
1228
- if (!this._evmAddress) {
1229
- logger_1.logger.info("OnChainChanged: No current address, user appears disconnected");
1230
- return [2 /*return*/, Promise.resolve()];
1231
- }
1232
- // Set provider if none exists
1233
- if (!this._provider) {
1234
- this._provider = provider;
1235
- }
1236
- this.setChainState('evm', { chainId: nextChainId });
1237
- try {
1238
- // This is just a chain change since we already confirmed _evmAddress exists
1239
- if (this.isAutocaptureEnabled("chain")) {
1240
- return [2 /*return*/, this.chain({
1336
+ switch (_a.label) {
1337
+ case 0:
1338
+ logger_1.logger.info("onChainChanged", chainIdHex);
1339
+ nextChainId = (0, chain_1.parseChainId)(chainIdHex);
1340
+ // Record it for THIS provider regardless of which one is active. This is
1341
+ // the only way an autocaptured event from a non-active wallet can learn
1342
+ // its chain without putting an RPC on that wallet's transport.
1343
+ this.rememberProviderChain(provider, nextChainId);
1344
+ // Beyond that, a chain event from a NON-active provider is observation
1345
+ // only when chain autocapture is off.
1346
+ //
1347
+ // This listener is now registered unconditionally, so that a signature can
1348
+ // be labelled with its signer's chain. `handleProviderMismatch()` treats a
1349
+ // chain event from another wallet as a wallet switch and clears the active
1350
+ // wallet's address and chain. That is the established behaviour of the
1351
+ // chain feature and stays exactly as it was, but it must not start firing
1352
+ // for apps that never asked for chain tracking: a second wallet switching
1353
+ // network would silently erase the active wallet's attribution.
1354
+ // Observation only when chain autocapture is off, whether or not an
1355
+ // active provider has been established yet.
1356
+ //
1357
+ // `isProviderMismatch()` is false while `_provider` is undefined, which is
1358
+ // exactly the state left by restoring a wallet from the active-wallet
1359
+ // cookie. A background wallet's `chainChanged` could therefore claim the
1360
+ // active slot and overwrite the restored wallet's chain - suppressing
1361
+ // allowed events, or letting excluded ones through. The active provider is
1362
+ // established by an actual account/connect/request association, not by
1363
+ // another wallet changing network.
1364
+ if (!this.isAutocaptureEnabled("chain") && provider !== this._provider) {
1365
+ return [2 /*return*/];
1366
+ }
1367
+ // Only handle chain changes for the active provider (or if none is set yet)
1368
+ if (this.isProviderMismatch(provider)) {
1369
+ this.handleProviderMismatch(provider);
1370
+ }
1371
+ // Chain changes only matter for connected users
1372
+ if (!this._evmAddress) {
1373
+ logger_1.logger.info("OnChainChanged: No current address, user appears disconnected");
1374
+ return [2 /*return*/, Promise.resolve()];
1375
+ }
1376
+ // Set provider if none exists
1377
+ if (!this._provider) {
1378
+ this._provider = provider;
1379
+ }
1380
+ this.setChainState('evm', { chainId: nextChainId });
1381
+ _a.label = 1;
1382
+ case 1:
1383
+ _a.trys.push([1, 5, , 6]);
1384
+ if (!this.isAutocaptureEnabled("chain")) return [3 /*break*/, 3];
1385
+ // Awaited, so a failing emission is caught below rather than escaping
1386
+ // as an unhandled rejection out of the provider's event listener.
1387
+ // `return`ing the promise left the catch here unreachable.
1388
+ return [4 /*yield*/, this.chain({
1241
1389
  chainId: nextChainId,
1242
1390
  address: this._evmAddress,
1243
1391
  })];
1244
- }
1245
- else {
1392
+ case 2:
1393
+ // Awaited, so a failing emission is caught below rather than escaping
1394
+ // as an unhandled rejection out of the provider's event listener.
1395
+ // `return`ing the promise left the catch here unreachable.
1396
+ _a.sent();
1397
+ return [3 /*break*/, 4];
1398
+ case 3:
1246
1399
  logger_1.logger.debug("OnChainChanged: Chain event skipped (autocapture.chain: false)", {
1247
1400
  chainId: this._evmChainId,
1248
1401
  address: this._evmAddress,
1249
1402
  });
1250
- }
1251
- }
1252
- catch (error) {
1253
- logger_1.logger.error("OnChainChanged: Failed to emit chain event:", error);
1403
+ _a.label = 4;
1404
+ case 4: return [3 /*break*/, 6];
1405
+ case 5:
1406
+ error_3 = _a.sent();
1407
+ logger_1.logger.error("OnChainChanged: Failed to emit chain event:", error_3);
1408
+ return [3 /*break*/, 6];
1409
+ case 6: return [2 /*return*/];
1254
1410
  }
1255
- return [2 /*return*/];
1256
1411
  });
1257
1412
  });
1258
1413
  };
1414
+ /**
1415
+ * Record a provider's chain from its `connect` event, and nothing else.
1416
+ *
1417
+ * Used when connect autocapture is off. `connect` carries `chainId` in its
1418
+ * payload, so this needs no RPC - unlike the full handler, which resolves
1419
+ * the account.
1420
+ */
1421
+ FormoAnalytics.prototype.registerConnectChainObserver = function (provider) {
1422
+ var _this = this;
1423
+ var listener = function () {
1424
+ var args = [];
1425
+ for (var _i = 0; _i < arguments.length; _i++) {
1426
+ args[_i] = arguments[_i];
1427
+ }
1428
+ var connection = args[0];
1429
+ if (typeof (connection === null || connection === void 0 ? void 0 : connection.chainId) !== "string")
1430
+ return;
1431
+ _this.rememberProviderChain(provider, (0, chain_1.parseChainId)(connection.chainId));
1432
+ };
1433
+ provider.on("connect", listener);
1434
+ this.addProviderListener(provider, "connect", listener);
1435
+ };
1436
+ /**
1437
+ * Whether a connect for this wallet still needs reporting.
1438
+ *
1439
+ * True when nothing has been reported for this provider, or when the account
1440
+ * changed. A wallet already reported is not reported again.
1441
+ *
1442
+ * Deliberately does NOT re-report to correct a chain. When `accountsChanged`
1443
+ * wins the race on a provider that exposes no synchronous `chainId`, the
1444
+ * connect carries 0 - honestly, since the chain is unknown at that instant -
1445
+ * and the `connect` payload that follows knows the real one. Emitting again
1446
+ * to relabel would mean two connects for one connection, which is the bug
1447
+ * this whole path exists to prevent. That payload still corrects
1448
+ * `currentChainId`, so everything after it is attributed properly.
1449
+ */
1450
+ FormoAnalytics.prototype.shouldReportConnect = function (provider, address) {
1451
+ var reported = this._announcedConnect.get(provider);
1452
+ if (!reported)
1453
+ return true;
1454
+ return reported.address.toLowerCase() !== address.toLowerCase();
1455
+ };
1456
+ /**
1457
+ * Record a connect as reported - but only if it will actually be sent.
1458
+ *
1459
+ * `connect()` passes through `shouldTrack()`, which refuses an unresolvable
1460
+ * chain when `tracking.excludeChains` is configured. Marking a refused event
1461
+ * as reported would suppress the authoritative one that follows.
1462
+ */
1463
+ FormoAnalytics.prototype.markConnectReported = function (provider, address, chainId) {
1464
+ if (!this.willTrackEvent(chainId))
1465
+ return;
1466
+ this._announcedConnect.set(provider, { address: address, chainId: chainId });
1467
+ };
1259
1468
  FormoAnalytics.prototype.registerConnectListener = function (provider) {
1260
1469
  var _this = this;
1261
1470
  logger_1.logger.info("registerConnectListener");
@@ -1280,6 +1489,9 @@ var FormoAnalytics = /** @class */ (function () {
1280
1489
  case 0:
1281
1490
  if (this._provider !== provider)
1282
1491
  return [2 /*return*/];
1492
+ // As in the accountsChanged disconnect path: the reported connect ends
1493
+ // with the connection.
1494
+ this._announcedConnect.delete(provider);
1283
1495
  logger_1.logger.info("OnDisconnect: Wallet disconnect event received, current state:", {
1284
1496
  currentAddress: this._evmAddress,
1285
1497
  currentChainId: this._evmChainId,
@@ -1327,6 +1539,8 @@ var FormoAnalytics = /** @class */ (function () {
1327
1539
  if (!(connection === null || connection === void 0 ? void 0 : connection.chainId) || typeof connection.chainId !== "string")
1328
1540
  return [2 /*return*/];
1329
1541
  chainId = (0, chain_1.parseChainId)(connection.chainId);
1542
+ // Record it for this provider before anything can bail out below.
1543
+ this.rememberProviderChain(provider, chainId);
1330
1544
  return [4 /*yield*/, this.getAddress(provider)];
1331
1545
  case 2:
1332
1546
  address = _a.sent();
@@ -1351,8 +1565,18 @@ var FormoAnalytics = /** @class */ (function () {
1351
1565
  });
1352
1566
  }
1353
1567
  }
1354
- // Conditionally emit connect event based on tracking configuration
1355
- if (isActiveProvider && this._evmAddress) {
1568
+ // Conditionally emit connect event based on tracking configuration.
1569
+ //
1570
+ // Both handlers observe one connection, so `shouldReportConnect()`
1571
+ // decides which of them reports it. It keys on what was actually
1572
+ // REPORTED, not on whether an address is known: an address can be
1573
+ // present with no connect ever sent - restored from the active-wallet
1574
+ // cookie, or reported with an unresolved chain and then refused by
1575
+ // `excludeChains` - and this payload carries the authoritative chain,
1576
+ // so it must be able to supersede such a report.
1577
+ if (isActiveProvider &&
1578
+ this._evmAddress &&
1579
+ this.shouldReportConnect(provider, address)) {
1356
1580
  providerInfo = this.getProviderInfo(provider);
1357
1581
  effectiveChainId = chainId || 0;
1358
1582
  if (this.isAutocaptureEnabled("connect")) {
@@ -1367,6 +1591,7 @@ var FormoAnalytics = /** @class */ (function () {
1367
1591
  if (effectiveChainId === 0) {
1368
1592
  logger_1.logger.info("OnConnected: Using fallback chainId 0 for connect event");
1369
1593
  }
1594
+ this.markConnectReported(provider, address, effectiveChainId);
1370
1595
  this.connect({
1371
1596
  chainId: effectiveChainId,
1372
1597
  address: address,
@@ -1423,162 +1648,209 @@ var FormoAnalytics = /** @class */ (function () {
1423
1648
  }
1424
1649
  var request = provider.request.bind(provider);
1425
1650
  var wrappedRequest = function (_a) { return __awaiter(_this, [_a], void 0, function (_b) {
1426
- var capturedChainId_1, _c, response_1, error_3, rpcError, transactionHash_1, error_4, rpcError;
1651
+ var generation_1, responsePromise, capturedChainId_1, response_1, error_4, rpcError, txPromise, txChainId_1, transactionHash_1, error_5, rpcError;
1427
1652
  var _this = this;
1653
+ var _c;
1428
1654
  var method = _b.method, params = _b.params;
1429
1655
  return __generator(this, function (_d) {
1430
1656
  switch (_d.label) {
1431
1657
  case 0:
1658
+ // Learn the chain from a call the APP was making anyway.
1659
+ //
1660
+ // A standards-compliant provider need not expose a synchronous `chainId`
1661
+ // property, and if it connected before the SDK initialised, its
1662
+ // `connect` event is never replayed. Such a provider stayed unknown -
1663
+ // reported as chain 0, and with `excludeChains` configured its events
1664
+ // were dropped even on an allowed chain. This adds no request of its
1665
+ // own; it only reads the answer to one the dapp already sent.
1666
+ if (method === "eth_chainId") {
1667
+ generation_1 = (_c = this._providerChainGenerations.get(provider)) !== null && _c !== void 0 ? _c : 0;
1668
+ return [2 /*return*/, request({ method: method, params: params }).then(function (result) {
1669
+ var _a;
1670
+ // A `chainChanged` for THIS provider may have landed while this was
1671
+ // in flight. It is newer by definition, so it must not be
1672
+ // overwritten by this answer.
1673
+ if (generation_1 === ((_a = _this._providerChainGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) &&
1674
+ typeof result === "string") {
1675
+ _this.rememberProviderChain(provider, (0, chain_1.parseChainId)(result));
1676
+ }
1677
+ return result;
1678
+ })];
1679
+ }
1432
1680
  if (!(Array.isArray(params) &&
1433
- ["eth_signTypedData_v4", "personal_sign"].includes(method))) return [3 /*break*/, 6];
1681
+ ["eth_signTypedData_v4", "personal_sign"].includes(method))) return [3 /*break*/, 4];
1434
1682
  if (!this.isAutocaptureEnabled("signature")) {
1435
1683
  logger_1.logger.debug("Signature event skipped (autocapture.signature: false)", { method: method });
1436
1684
  return [2 /*return*/, request({ method: method, params: params })];
1437
1685
  }
1438
- _c = this._evmChainId;
1439
- if (_c) return [3 /*break*/, 2];
1440
- return [4 /*yield*/, this.getCurrentChainId(provider)];
1441
- case 1:
1442
- _c = (_d.sent());
1443
- _d.label = 2;
1444
- case 2:
1445
- capturedChainId_1 = _c;
1686
+ responsePromise = request({ method: method, params: params });
1687
+ // Attach a no-op handler now so a rejection arriving before the await
1688
+ // below is never reported as unhandled. The real handling is there.
1689
+ responsePromise.catch(function () { return undefined; });
1690
+ capturedChainId_1 = this.resolveChainIdForProvider(provider);
1446
1691
  // Fire-and-forget tracking
1447
1692
  (function () { return __awaiter(_this, void 0, void 0, function () {
1693
+ var e_4;
1448
1694
  return __generator(this, function (_a) {
1449
- try {
1450
- this.signature(__assign({ status: types_1.SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1)));
1451
- }
1452
- catch (e) {
1453
- logger_1.logger.error("Formo: Failed to track signature request", e);
1695
+ switch (_a.label) {
1696
+ case 0:
1697
+ _a.trys.push([0, 2, , 3]);
1698
+ return [4 /*yield*/, this.signature(__assign({ status: types_1.SignatureStatus.REQUESTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
1699
+ case 1:
1700
+ _a.sent();
1701
+ return [3 /*break*/, 3];
1702
+ case 2:
1703
+ e_4 = _a.sent();
1704
+ logger_1.logger.error("Formo: Failed to track signature request", e_4);
1705
+ return [3 /*break*/, 3];
1706
+ case 3: return [2 /*return*/];
1454
1707
  }
1455
- return [2 /*return*/];
1456
1708
  });
1457
1709
  }); })();
1458
- _d.label = 3;
1459
- case 3:
1460
- _d.trys.push([3, 5, , 6]);
1461
- return [4 /*yield*/, request({ method: method, params: params })];
1462
- case 4:
1463
- response_1 = (_d.sent());
1710
+ _d.label = 1;
1711
+ case 1:
1712
+ _d.trys.push([1, 3, , 4]);
1713
+ return [4 /*yield*/, responsePromise];
1714
+ case 2:
1715
+ response_1 = _d.sent();
1464
1716
  // Track signature confirmation only for truthy responses
1465
1717
  if (response_1) {
1466
1718
  (function () { return __awaiter(_this, void 0, void 0, function () {
1719
+ var e_5;
1467
1720
  return __generator(this, function (_a) {
1468
- try {
1469
- this.signature(__assign({ status: types_1.SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1)));
1470
- }
1471
- catch (e) {
1472
- logger_1.logger.error("Formo: Failed to track signature confirmation", e);
1721
+ switch (_a.label) {
1722
+ case 0:
1723
+ _a.trys.push([0, 2, , 3]);
1724
+ return [4 /*yield*/, this.signature(__assign({ status: types_1.SignatureStatus.CONFIRMED }, this.buildSignatureEventPayload(method, params, response_1, capturedChainId_1, provider)))];
1725
+ case 1:
1726
+ _a.sent();
1727
+ return [3 /*break*/, 3];
1728
+ case 2:
1729
+ e_5 = _a.sent();
1730
+ logger_1.logger.error("Formo: Failed to track signature confirmation", e_5);
1731
+ return [3 /*break*/, 3];
1732
+ case 3: return [2 /*return*/];
1473
1733
  }
1474
- return [2 /*return*/];
1475
1734
  });
1476
1735
  }); })();
1477
1736
  }
1478
1737
  return [2 /*return*/, response_1];
1479
- case 5:
1480
- error_3 = _d.sent();
1481
- rpcError = error_3;
1738
+ case 3:
1739
+ error_4 = _d.sent();
1740
+ rpcError = error_4;
1482
1741
  if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
1483
1742
  // Use the already cast rpcError to avoid duplication
1484
1743
  (function () { return __awaiter(_this, void 0, void 0, function () {
1744
+ var e_6;
1485
1745
  return __generator(this, function (_a) {
1486
- try {
1487
- this.signature(__assign({ status: types_1.SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1)));
1488
- }
1489
- catch (e) {
1490
- logger_1.logger.error("Formo: Failed to track signature rejection", e);
1746
+ switch (_a.label) {
1747
+ case 0:
1748
+ _a.trys.push([0, 2, , 3]);
1749
+ return [4 /*yield*/, this.signature(__assign({ status: types_1.SignatureStatus.REJECTED }, this.buildSignatureEventPayload(method, params, undefined, capturedChainId_1, provider)))];
1750
+ case 1:
1751
+ _a.sent();
1752
+ return [3 /*break*/, 3];
1753
+ case 2:
1754
+ e_6 = _a.sent();
1755
+ logger_1.logger.error("Formo: Failed to track signature rejection", e_6);
1756
+ return [3 /*break*/, 3];
1757
+ case 3: return [2 /*return*/];
1491
1758
  }
1492
- return [2 /*return*/];
1493
1759
  });
1494
1760
  }); })();
1495
1761
  }
1496
- throw error_3;
1497
- case 6:
1762
+ throw error_4;
1763
+ case 4:
1498
1764
  if (!(Array.isArray(params) &&
1499
1765
  method === "eth_sendTransaction" &&
1500
- params[0])) return [3 /*break*/, 10];
1766
+ params[0])) return [3 /*break*/, 8];
1501
1767
  if (!this.isAutocaptureEnabled("transaction")) {
1502
1768
  logger_1.logger.debug("Transaction event skipped (autocapture.transaction: false)", { method: method });
1503
1769
  return [2 /*return*/, request({ method: method, params: params })];
1504
1770
  }
1771
+ txPromise = request({ method: method, params: params });
1772
+ txPromise.catch(function () { return undefined; });
1773
+ txChainId_1 = this.resolveChainIdForProvider(provider);
1505
1774
  (function () { return __awaiter(_this, void 0, void 0, function () {
1506
- var payload, e_4;
1775
+ var payload, e_7;
1507
1776
  return __generator(this, function (_a) {
1508
1777
  switch (_a.label) {
1509
1778
  case 0:
1510
- _a.trys.push([0, 2, , 3]);
1511
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider)];
1779
+ _a.trys.push([0, 3, , 4]);
1780
+ return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1512
1781
  case 1:
1513
1782
  payload = _a.sent();
1514
- this.transaction(__assign({ status: types_1.TransactionStatus.STARTED }, payload));
1515
- return [3 /*break*/, 3];
1783
+ return [4 /*yield*/, this.transaction(__assign({ status: types_1.TransactionStatus.STARTED }, payload))];
1516
1784
  case 2:
1517
- e_4 = _a.sent();
1518
- logger_1.logger.error("Formo: Failed to track transaction start", e_4);
1519
- return [3 /*break*/, 3];
1520
- case 3: return [2 /*return*/];
1785
+ _a.sent();
1786
+ return [3 /*break*/, 4];
1787
+ case 3:
1788
+ e_7 = _a.sent();
1789
+ logger_1.logger.error("Formo: Failed to track transaction start", e_7);
1790
+ return [3 /*break*/, 4];
1791
+ case 4: return [2 /*return*/];
1521
1792
  }
1522
1793
  });
1523
1794
  }); })();
1524
- _d.label = 7;
1525
- case 7:
1526
- _d.trys.push([7, 9, , 10]);
1527
- return [4 /*yield*/, request({
1528
- method: method,
1529
- params: params,
1530
- })];
1531
- case 8:
1532
- transactionHash_1 = (_d.sent());
1795
+ _d.label = 5;
1796
+ case 5:
1797
+ _d.trys.push([5, 7, , 8]);
1798
+ return [4 /*yield*/, txPromise];
1799
+ case 6:
1800
+ transactionHash_1 = _d.sent();
1533
1801
  (function () { return __awaiter(_this, void 0, void 0, function () {
1534
- var payload, e_5;
1802
+ var payload, e_8;
1535
1803
  return __generator(this, function (_a) {
1536
1804
  switch (_a.label) {
1537
1805
  case 0:
1538
- _a.trys.push([0, 2, , 3]);
1539
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider)];
1806
+ _a.trys.push([0, 3, , 4]);
1807
+ return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1540
1808
  case 1:
1541
1809
  payload = _a.sent();
1542
- this.transaction(__assign(__assign({ status: types_1.TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }));
1810
+ return [4 /*yield*/, this.transaction(__assign(__assign({ status: types_1.TransactionStatus.BROADCASTED }, payload), { transactionHash: transactionHash_1 }))];
1811
+ case 2:
1812
+ _a.sent();
1543
1813
  // Start async polling for transaction receipt
1544
1814
  this.pollTransactionReceipt(provider, transactionHash_1, payload);
1545
- return [3 /*break*/, 3];
1546
- case 2:
1547
- e_5 = _a.sent();
1548
- logger_1.logger.error("Formo: Failed to track transaction broadcast", e_5);
1549
- return [3 /*break*/, 3];
1550
- case 3: return [2 /*return*/];
1815
+ return [3 /*break*/, 4];
1816
+ case 3:
1817
+ e_8 = _a.sent();
1818
+ logger_1.logger.error("Formo: Failed to track transaction broadcast", e_8);
1819
+ return [3 /*break*/, 4];
1820
+ case 4: return [2 /*return*/];
1551
1821
  }
1552
1822
  });
1553
1823
  }); })();
1554
1824
  return [2 /*return*/, transactionHash_1];
1555
- case 9:
1556
- error_4 = _d.sent();
1557
- rpcError = error_4;
1825
+ case 7:
1826
+ error_5 = _d.sent();
1827
+ rpcError = error_5;
1558
1828
  if ((rpcError === null || rpcError === void 0 ? void 0 : rpcError.code) === 4001) {
1559
1829
  // Use the already cast rpcError to avoid duplication
1560
1830
  (function () { return __awaiter(_this, void 0, void 0, function () {
1561
- var payload, e_6;
1831
+ var payload, e_9;
1562
1832
  return __generator(this, function (_a) {
1563
1833
  switch (_a.label) {
1564
1834
  case 0:
1565
- _a.trys.push([0, 2, , 3]);
1566
- return [4 /*yield*/, this.buildTransactionEventPayload(params, provider)];
1835
+ _a.trys.push([0, 3, , 4]);
1836
+ return [4 /*yield*/, this.buildTransactionEventPayload(params, provider, txChainId_1)];
1567
1837
  case 1:
1568
1838
  payload = _a.sent();
1569
- this.transaction(__assign({ status: types_1.TransactionStatus.REJECTED }, payload));
1570
- return [3 /*break*/, 3];
1839
+ return [4 /*yield*/, this.transaction(__assign({ status: types_1.TransactionStatus.REJECTED }, payload))];
1571
1840
  case 2:
1572
- e_6 = _a.sent();
1573
- logger_1.logger.error("Formo: Failed to track transaction rejection", e_6);
1574
- return [3 /*break*/, 3];
1575
- case 3: return [2 /*return*/];
1841
+ _a.sent();
1842
+ return [3 /*break*/, 4];
1843
+ case 3:
1844
+ e_9 = _a.sent();
1845
+ logger_1.logger.error("Formo: Failed to track transaction rejection", e_9);
1846
+ return [3 /*break*/, 4];
1847
+ case 4: return [2 /*return*/];
1576
1848
  }
1577
1849
  });
1578
1850
  }); })();
1579
1851
  }
1580
- throw error_4;
1581
- case 10: return [2 /*return*/, request({ method: method, params: params })];
1852
+ throw error_5;
1853
+ case 8: return [2 /*return*/, request({ method: method, params: params })];
1582
1854
  }
1583
1855
  });
1584
1856
  }); };
@@ -1596,9 +1868,16 @@ var FormoAnalytics = /** @class */ (function () {
1596
1868
  };
1597
1869
  FormoAnalytics.prototype.onLocationChange = function () {
1598
1870
  return __awaiter(this, void 0, void 0, function () {
1599
- return __generator(this, function (_a) {
1871
+ var _a;
1872
+ return __generator(this, function (_b) {
1600
1873
  if (this._currentUrl !== window.location.href) {
1601
1874
  this._currentUrl = window.location.href;
1875
+ // Host/path exclusions are evaluated per navigation, so a SPA can leave
1876
+ // an excluded route and become trackable without any wallet event
1877
+ // firing. The wagmi handler only observes *changes*, so an unchanged
1878
+ // connection it was forced to decline earlier would stay invisible for
1879
+ // the rest of the page load. Give it a chance to adopt it now.
1880
+ (_a = this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.retryAdoption();
1602
1881
  this.trackPageHit();
1603
1882
  }
1604
1883
  return [2 /*return*/];
@@ -1669,7 +1948,7 @@ var FormoAnalytics = /** @class */ (function () {
1669
1948
  if (_this._pageHooksDisposed)
1670
1949
  return;
1671
1950
  (function () { return __awaiter(_this, void 0, void 0, function () {
1672
- var e_7;
1951
+ var e_10;
1673
1952
  return __generator(this, function (_a) {
1674
1953
  switch (_a.label) {
1675
1954
  case 0:
@@ -1682,8 +1961,8 @@ var FormoAnalytics = /** @class */ (function () {
1682
1961
  _a.sent();
1683
1962
  return [3 /*break*/, 3];
1684
1963
  case 2:
1685
- e_7 = _a.sent();
1686
- logger_1.logger.error("Formo: Failed to track page hit", e_7);
1964
+ e_10 = _a.sent();
1965
+ logger_1.logger.error("Formo: Failed to track page hit", e_10);
1687
1966
  return [3 /*break*/, 3];
1688
1967
  case 3: return [2 /*return*/];
1689
1968
  }
@@ -1696,12 +1975,17 @@ var FormoAnalytics = /** @class */ (function () {
1696
1975
  };
1697
1976
  FormoAnalytics.prototype.trackEvent = function (type, payload, properties, context, callback) {
1698
1977
  return __awaiter(this, void 0, void 0, function () {
1699
- var error_5;
1978
+ var error_6;
1700
1979
  return __generator(this, function (_a) {
1701
1980
  switch (_a.label) {
1702
1981
  case 0:
1703
1982
  _a.trys.push([0, 2, , 3]);
1704
- if (!this.shouldTrack()) {
1983
+ // Gate on the chain the event actually carries. `connect`, `disconnect`,
1984
+ // `chain`, `signature` and `transaction` all put one in the payload, and
1985
+ // it is authoritative: it can name a provider or a wagmi mutation chain
1986
+ // that is not the active one. Events without a chain (page, track,
1987
+ // identify) fall back to the central value.
1988
+ if (!this.shouldTrack(payload === null || payload === void 0 ? void 0 : payload.chainId)) {
1705
1989
  logger_1.logger.info("Skipping ".concat(type, " event due to tracking configuration"));
1706
1990
  return [2 /*return*/];
1707
1991
  }
@@ -1710,8 +1994,8 @@ var FormoAnalytics = /** @class */ (function () {
1710
1994
  _a.sent();
1711
1995
  return [3 /*break*/, 3];
1712
1996
  case 2:
1713
- error_5 = _a.sent();
1714
- logger_1.logger.error("Error tracking event:", error_5);
1997
+ error_6 = _a.sent();
1998
+ logger_1.logger.error("Error tracking event:", error_6);
1715
1999
  return [3 /*break*/, 3];
1716
2000
  case 3: return [2 /*return*/];
1717
2001
  }
@@ -1748,16 +2032,24 @@ var FormoAnalytics = /** @class */ (function () {
1748
2032
  * an options object with `excludeChains` set, and only once a chain id is
1749
2033
  * known.
1750
2034
  */
1751
- FormoAnalytics.prototype.isCurrentChainExcluded = function () {
2035
+ FormoAnalytics.prototype.isCurrentChainExcluded = function (eventChainId) {
1752
2036
  if (this.options.tracking === null ||
1753
2037
  typeof this.options.tracking !== "object" ||
1754
2038
  Array.isArray(this.options.tracking)) {
1755
2039
  return false;
1756
2040
  }
1757
2041
  var _a = this.options.tracking.excludeChains, excludeChains = _a === void 0 ? [] : _a;
1758
- return (excludeChains.length > 0 &&
1759
- !!this.currentChainId &&
1760
- excludeChains.includes(this.currentChainId));
2042
+ if (excludeChains.length === 0)
2043
+ return false;
2044
+ // Mirrors `shouldTrack()`: the event's own chain wins when it has one,
2045
+ // and an unresolvable chain on a known wallet counts as excluded rather
2046
+ // than allowed.
2047
+ var chainToCheck = eventChainId !== null && eventChainId !== void 0 ? eventChainId : this.currentChainId;
2048
+ if (chainToCheck === 0)
2049
+ return true;
2050
+ if (chainToCheck === undefined)
2051
+ return false;
2052
+ return excludeChains.includes(chainToCheck);
1761
2053
  };
1762
2054
  /**
1763
2055
  * Whether the current environment is excluded from tracking - the visitor's
@@ -1852,7 +2144,7 @@ var FormoAnalytics = /** @class */ (function () {
1852
2144
  * Determines if tracking should be enabled based on configuration and consent
1853
2145
  * @returns {boolean} True if tracking should be enabled
1854
2146
  */
1855
- FormoAnalytics.prototype.shouldTrack = function () {
2147
+ FormoAnalytics.prototype.shouldTrack = function (eventChainId) {
1856
2148
  // First check if user has opted out of tracking
1857
2149
  if (this.hasOptedOutTracking()) {
1858
2150
  return false;
@@ -1871,11 +2163,42 @@ var FormoAnalytics = /** @class */ (function () {
1871
2163
  if (this.isCurrentEnvironmentExcluded()) {
1872
2164
  return false;
1873
2165
  }
1874
- // Check chainId exclusions
1875
- if (excludeChains.length > 0 &&
1876
- this.currentChainId &&
1877
- excludeChains.includes(this.currentChainId)) {
1878
- return false;
2166
+ // Check chainId exclusions.
2167
+ //
2168
+ // The event's OWN chain wins over `currentChainId`. They differ whenever
2169
+ // the event did not come from the active provider: a second wallet
2170
+ // signing through its own provider, or a wagmi mutation that names an
2171
+ // explicit chain. Reading only the central field there would emit an
2172
+ // event that is labelled with an excluded chain, which is precisely what
2173
+ // the exclusion forbids - and would drop an allowed event whenever the
2174
+ // active provider happened to sit on an excluded chain.
2175
+ var chainToCheck = eventChainId !== null && eventChainId !== void 0 ? eventChainId : this.currentChainId;
2176
+ if (excludeChains.length > 0) {
2177
+ // Fail CLOSED on an unknown chain. `resolveChainIdForProvider` reports
2178
+ // 0 when it has never heard a chain from the signing wallet, and 0 is
2179
+ // in no exclusion list, so treating it as "not excluded" would let
2180
+ // exactly the events an operator excluded through - the wallet on the
2181
+ // excluded chain is often the one we know least about. An explicit
2182
+ // exclusion is a directive, so an unresolvable chain is refused.
2183
+ //
2184
+ // This covers the central value too, not just an explicit event
2185
+ // chain: `page`, `track` and `identify` carry no chain of their own
2186
+ // and fall back to `currentChainId`, so an unknown chain there would
2187
+ // otherwise send wallet-attributed events for a wallet that may well
2188
+ // be sitting on an excluded chain.
2189
+ // Deliberately keyed on 0 and NOT on `undefined`. 0 is the explicit
2190
+ // "we asked and could not tell" marker. `undefined` means no chain
2191
+ // state yet, which is a legitimate transient - the Privy path
2192
+ // reconciles a Solana wallet through exactly that state - and
2193
+ // refusing it would drop real events.
2194
+ //
2195
+ // `backfillActiveWallet()` never persists 0, so an unresolvable chain
2196
+ // cannot leak into `currentChainId` and reach the unscoped events
2197
+ // (page / track / identify) that fall back to it.
2198
+ if (chainToCheck === 0)
2199
+ return false;
2200
+ if (chainToCheck && excludeChains.includes(chainToCheck))
2201
+ return false;
1879
2202
  }
1880
2203
  // If nothing is excluded, tracking is enabled
1881
2204
  return true;
@@ -1959,7 +2282,7 @@ var FormoAnalytics = /** @class */ (function () {
1959
2282
  _this.trackProviders(newDetails);
1960
2283
  // Detect newly discovered wallets (session de-dupes) with error handling
1961
2284
  (function () { return __awaiter(_this, void 0, void 0, function () {
1962
- var e_8;
2285
+ var e_11;
1963
2286
  return __generator(this, function (_a) {
1964
2287
  switch (_a.label) {
1965
2288
  case 0:
@@ -1969,8 +2292,8 @@ var FormoAnalytics = /** @class */ (function () {
1969
2292
  _a.sent();
1970
2293
  return [3 /*break*/, 3];
1971
2294
  case 2:
1972
- e_8 = _a.sent();
1973
- logger_1.logger.error("Formo: Failed to detect wallets", e_8);
2295
+ e_11 = _a.sent();
2296
+ logger_1.logger.error("Formo: Failed to detect wallets", e_11);
1974
2297
  return [3 /*break*/, 3];
1975
2298
  case 3: return [2 /*return*/];
1976
2299
  }
@@ -2163,6 +2486,105 @@ var FormoAnalytics = /** @class */ (function () {
2163
2486
  });
2164
2487
  });
2165
2488
  };
2489
+ /**
2490
+ * Resolve the chain an autocaptured request actually ran on.
2491
+ *
2492
+ * `_evmChainId` is maintained by `chainChanged` from whichever provider is
2493
+ * currently active. When a request arrives from a *different* tracked
2494
+ * provider - which happens whenever a visitor has two wallets installed -
2495
+ * that cached value describes the wrong wallet, and tagging the event with
2496
+ * it silently mis-attributes the chain.
2497
+ *
2498
+ * Answered entirely from a per-provider snapshot. This is deliberately
2499
+ * SYNCHRONOUS and never issues an RPC.
2500
+ *
2501
+ * An earlier version called `eth_chainId` on the signing provider and
2502
+ * time-boxed it with `Promise.race`. That is not safe: the race abandons the
2503
+ * SDK's promise but cannot cancel the provider's request. On a transport
2504
+ * that serializes - WalletConnect's relay socket, the very case this path
2505
+ * exists to serve - an abandoned lookup stays at the head of the wallet's
2506
+ * queue, and every later RPC the dapp makes queues behind it until reload.
2507
+ * Mislabelling a chain is a reporting defect; wedging the user's wallet is
2508
+ * not acceptable to avoid one.
2509
+ *
2510
+ * When nothing is known, this reports 0 ("unknown") rather than guessing
2511
+ * with the active provider's chain, which is known-wrong for another wallet.
2512
+ */
2513
+ FormoAnalytics.prototype.resolveChainIdForProvider = function (provider) {
2514
+ if (provider) {
2515
+ var known = this._providerChainIds.get(provider);
2516
+ if (known)
2517
+ return known;
2518
+ // Only the active provider's chain is described by the central cache.
2519
+ if (provider === this._provider && this._evmChainId) {
2520
+ return this._evmChainId;
2521
+ }
2522
+ // A tracked provider we have never heard a chain from. Deliberately no
2523
+ // fall back to `_evmChainId`: it belongs to a different wallet.
2524
+ return 0;
2525
+ }
2526
+ return this._evmChainId || 0;
2527
+ };
2528
+ /**
2529
+ * Record a provider's chain. Fed by `chainChanged` and `connect`, and by the
2530
+ * one-off probe at tracking time - never from inside a user request.
2531
+ */
2532
+ FormoAnalytics.prototype.rememberProviderChain = function (provider, chainId) {
2533
+ if (!provider || !chainId)
2534
+ return;
2535
+ // Any observation is newer than an `eth_chainId` still in flight for this
2536
+ // provider.
2537
+ this.bumpProviderChainGeneration(provider);
2538
+ this._providerChainIds.set(provider, chainId);
2539
+ // If this IS the active provider, central state has to follow. Recording
2540
+ // it only per provider left `currentChainId` on a chain restored from a
2541
+ // previous session, and the unscoped events that fall back to it were sent
2542
+ // despite an exclusion covering the chain the wallet was really on.
2543
+ //
2544
+ // Guarded on EVM already being the active namespace. `setChainState()`
2545
+ // makes whichever namespace it touches active, so syncing here
2546
+ // unconditionally let a background EVM wallet's chain report steal the
2547
+ // active slot from a live Solana wallet, and every later page or track
2548
+ // event was attributed to the wrong wallet entirely.
2549
+ if (provider === this._provider &&
2550
+ this._activeNamespace === "evm" &&
2551
+ this._evmChainId !== chainId) {
2552
+ this.setChainState('evm', { chainId: chainId });
2553
+ }
2554
+ };
2555
+ /** Advance and return this provider's chain-observation generation. */
2556
+ FormoAnalytics.prototype.bumpProviderChainGeneration = function (provider) {
2557
+ var _a;
2558
+ var next = ((_a = this._providerChainGenerations.get(provider)) !== null && _a !== void 0 ? _a : 0) + 1;
2559
+ this._providerChainGenerations.set(provider, next);
2560
+ return next;
2561
+ };
2562
+ /**
2563
+ * Seed a provider's chain from whatever it already exposes synchronously.
2564
+ *
2565
+ * Most EIP-1193 implementations carry a `chainId` property (MetaMask,
2566
+ * WalletConnect, Coinbase). Reading it costs nothing and cannot block.
2567
+ *
2568
+ * There is deliberately no RPC fallback. An earlier version probed with
2569
+ * `eth_chainId` when a provider was first tracked, on the theory that
2570
+ * tracking time is off the user's critical path. It is not: a serialized
2571
+ * transport has ONE queue, so a stalled probe sits in front of every later
2572
+ * signature and transaction the dapp makes. It could also land out of order
2573
+ * - a slow probe response overwriting a newer `chainChanged` - and relabel
2574
+ * events onto a chain the wallet had already left.
2575
+ *
2576
+ * A provider that exposes nothing stays unknown until it emits
2577
+ * `chainChanged` or `connect`, and unknown is reported honestly as 0.
2578
+ */
2579
+ FormoAnalytics.prototype.seedProviderChainFromState = function (provider) {
2580
+ var raw = provider.chainId;
2581
+ var chainId = typeof raw === "string"
2582
+ ? (0, chain_1.parseChainId)(raw)
2583
+ : typeof raw === "number"
2584
+ ? raw
2585
+ : undefined;
2586
+ this.rememberProviderChain(provider, chainId);
2587
+ };
2166
2588
  FormoAnalytics.prototype.getCurrentChainId = function (provider) {
2167
2589
  return __awaiter(this, void 0, void 0, function () {
2168
2590
  var p, chainIdHex, err_5;
@@ -2198,7 +2620,7 @@ var FormoAnalytics = /** @class */ (function () {
2198
2620
  };
2199
2621
  FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
2200
2622
  // Intentionally not read. Kept for positional call-site arity.
2201
- _response, chainId) {
2623
+ _response, chainId, provider) {
2202
2624
  var _a;
2203
2625
  var rawAddress = method === "personal_sign"
2204
2626
  ? params[1]
@@ -2208,7 +2630,11 @@ var FormoAnalytics = /** @class */ (function () {
2208
2630
  throw new Error("Invalid address in signature payload: ".concat(rawAddress));
2209
2631
  }
2210
2632
  var effectiveChainId = (_a = chainId !== null && chainId !== void 0 ? chainId : this._evmChainId) !== null && _a !== void 0 ? _a : undefined;
2211
- this.backfillActiveWallet(validAddress, effectiveChainId);
2633
+ // Only the active provider may write central wallet state - see the same
2634
+ // guard in buildTransactionEventPayload.
2635
+ if (!provider || provider === this._provider || !this._provider) {
2636
+ this.backfillActiveWallet(validAddress, effectiveChainId, provider);
2637
+ }
2212
2638
  var basePayload = {
2213
2639
  chainId: effectiveChainId,
2214
2640
  address: validAddress,
@@ -2220,34 +2646,37 @@ var FormoAnalytics = /** @class */ (function () {
2220
2646
  // eth_signTypedData*: params[1] is the full EIP-712 struct.
2221
2647
  return __assign(__assign({}, basePayload), { message: params[1] });
2222
2648
  };
2223
- FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider) {
2649
+ FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider,
2650
+ /**
2651
+ * Chain resolved once for this request's whole lifecycle. Passing it keeps
2652
+ * every status of one transaction on the same chain even if the user
2653
+ * switches network while the wallet prompt is open.
2654
+ */
2655
+ capturedChainId) {
2224
2656
  return __awaiter(this, void 0, void 0, function () {
2225
- var _a, data, from, to, value, validAddress, chainId, _b;
2226
- return __generator(this, function (_c) {
2227
- switch (_c.label) {
2228
- case 0:
2229
- _a = params[0], data = _a.data, from = _a.from, to = _a.to, value = _a.value;
2230
- validAddress = (0, address_1.validateAndChecksumAddress)(from);
2231
- if (!validAddress) {
2232
- throw new Error("Invalid address in transaction payload: ".concat(from));
2233
- }
2234
- _b = this._evmChainId;
2235
- if (_b) return [3 /*break*/, 2];
2236
- return [4 /*yield*/, this.getCurrentChainId(provider)];
2237
- case 1:
2238
- _b = (_c.sent());
2239
- _c.label = 2;
2240
- case 2:
2241
- chainId = _b;
2242
- this.backfillActiveWallet(validAddress, chainId);
2243
- return [2 /*return*/, {
2244
- chainId: chainId,
2245
- data: data,
2246
- address: validAddress,
2247
- to: to,
2248
- value: value,
2249
- }];
2657
+ var _a, data, from, to, value, validAddress, chainId;
2658
+ return __generator(this, function (_b) {
2659
+ _a = params[0], data = _a.data, from = _a.from, to = _a.to, value = _a.value;
2660
+ validAddress = (0, address_1.validateAndChecksumAddress)(from);
2661
+ if (!validAddress) {
2662
+ throw new Error("Invalid address in transaction payload: ".concat(from));
2663
+ }
2664
+ chainId = capturedChainId !== null && capturedChainId !== void 0 ? capturedChainId : this.resolveChainIdForProvider(provider);
2665
+ // Only the ACTIVE provider may write central wallet state. A request from
2666
+ // a second, non-active wallet would otherwise overwrite the active
2667
+ // provider's address and chain, and every later request through the active
2668
+ // provider would then trust the other wallet's chain - persistent
2669
+ // mis-attribution, and a way around `excludeChains`.
2670
+ if (!provider || provider === this._provider || !this._provider) {
2671
+ this.backfillActiveWallet(validAddress, chainId, provider);
2250
2672
  }
2673
+ return [2 /*return*/, {
2674
+ chainId: chainId,
2675
+ data: data,
2676
+ address: validAddress,
2677
+ to: to,
2678
+ value: value,
2679
+ }];
2251
2680
  });
2252
2681
  });
2253
2682
  };
@@ -2259,16 +2688,51 @@ var FormoAnalytics = /** @class */ (function () {
2259
2688
  * social-login wrappers). If `accountsChanged` later fires it overwrites this
2260
2689
  * value in the normal way; existing connections are never clobbered.
2261
2690
  */
2262
- FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId) {
2691
+ FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId, provider) {
2692
+ // Refuse a chain the provider has since moved off.
2693
+ //
2694
+ // A request captures its chain once and reuses that snapshot for every
2695
+ // status it emits, which is right for the event payload - a confirmation
2696
+ // must not be relabelled mid-flight. But writing that captured value back
2697
+ // into central state on the LATER statuses restored a chain the wallet had
2698
+ // already left, and the unscoped events that fall back to it then bypassed
2699
+ // an exclusion that should have caught them.
2700
+ if (provider && chainId !== undefined && chainId !== 0) {
2701
+ var current = this._providerChainIds.get(provider);
2702
+ if (current !== undefined && current !== chainId) {
2703
+ chainId = current;
2704
+ }
2705
+ }
2706
+ // `0` is kept deliberately. It means "could not resolve", and persisting
2707
+ // it is what lets the exclusion gate refuse the unscoped events - `page`,
2708
+ // `track`, `identify` - that carry no chain of their own and fall back to
2709
+ // `currentChainId`. Erasing it to `undefined` looked tidier but removed
2710
+ // the only marker distinguishing "unknown" from "no wallet yet", and those
2711
+ // events then went out attributed to a wallet that might be sitting on an
2712
+ // excluded chain.
2263
2713
  // Never learn identity while suppressed (opt-out / timezone / excluded host
2264
2714
  // or path). A signature/transaction observed on an excluded route must not
2265
- // populate currentAddress for later allowed-page events. backfill only ever
2266
- // *adds* an address (it no-ops when one is already known), so there is no
2267
- // stale state to clear here.
2715
+ // populate currentAddress for later allowed-page events.
2268
2716
  if (this.isTrackingSuppressed())
2269
2717
  return;
2270
- if (this._evmAddress)
2718
+ var known = this._evmAddress;
2719
+ if (known) {
2720
+ // Same wallet, newer chain: correct it rather than returning.
2721
+ //
2722
+ // A persisted wallet restores a chain from a previous session. If the
2723
+ // provider has since moved - to an excluded chain, or to one we cannot
2724
+ // resolve - the autocaptured event is gated correctly, but the stale
2725
+ // restored chain stayed in `currentChainId` and the unscoped events
2726
+ // that fall back to it went out under an exclusion that should have
2727
+ // caught them.
2728
+ if (chainId !== undefined &&
2729
+ known.toLowerCase() === address.toLowerCase() &&
2730
+ this._evmChainId !== chainId) {
2731
+ this.setChainState('evm', { address: known, chainId: chainId });
2732
+ }
2733
+ // A DIFFERENT address is another wallet's business; never overwrite.
2271
2734
  return;
2735
+ }
2272
2736
  this.setChainState('evm', { address: address, chainId: chainId });
2273
2737
  };
2274
2738
  /**
@@ -2299,7 +2763,7 @@ var FormoAnalytics = /** @class */ (function () {
2299
2763
  if (!provider)
2300
2764
  return [2 /*return*/];
2301
2765
  poll = function () { return __awaiter(_this, void 0, void 0, function () {
2302
- var receipt, e_9;
2766
+ var receipt, e_12;
2303
2767
  return __generator(this, function (_a) {
2304
2768
  switch (_a.label) {
2305
2769
  case 0:
@@ -2323,8 +2787,8 @@ var FormoAnalytics = /** @class */ (function () {
2323
2787
  }
2324
2788
  return [3 /*break*/, 3];
2325
2789
  case 2:
2326
- e_9 = _a.sent();
2327
- logger_1.logger.error("Error polling transaction receipt", e_9);
2790
+ e_12 = _a.sent();
2791
+ logger_1.logger.error("Error polling transaction receipt", e_12);
2328
2792
  return [3 /*break*/, 3];
2329
2793
  case 3:
2330
2794
  attempts++;
@@ -2454,6 +2918,9 @@ var FormoAnalytics = /** @class */ (function () {
2454
2918
  ns.chainId = namespaceOrChainId;
2455
2919
  }
2456
2920
  if (namespace === 'evm' && 'provider' in update) {
2921
+ // Displacing the active provider ends its connection as far as this SDK
2922
+ // is concerned, so the connect it reported stops counting.
2923
+ this.forgetConnectRecord(ns.provider, update.provider);
2457
2924
  ns.provider = update.provider;
2458
2925
  }
2459
2926
  this._activeNamespace = namespace;
@@ -2467,6 +2934,8 @@ var FormoAnalytics = /** @class */ (function () {
2467
2934
  ? namespaceOrChainId
2468
2935
  : this.getNamespace(namespaceOrChainId);
2469
2936
  if (namespace === 'evm') {
2937
+ // Same rule: wiping the namespace drops the active provider.
2938
+ this.forgetConnectRecord(this._chainState.evm.provider, undefined);
2470
2939
  this._chainState.evm = {};
2471
2940
  }
2472
2941
  else {