@formo/analytics 1.37.0 → 1.38.1

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 (42) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +66 -0
  2. package/dist/cjs/src/FormoAnalytics.js +176 -5
  3. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +8 -0
  4. package/dist/cjs/src/FormoAnalyticsProvider.js +42 -32
  5. package/dist/cjs/src/evm/EvmEventTracker.d.ts +38 -10
  6. package/dist/cjs/src/evm/EvmEventTracker.js +182 -0
  7. package/dist/cjs/src/evm/EvmProviderRegistry.js +26 -4
  8. package/dist/cjs/src/evm/EvmRequestTracker.d.ts +40 -10
  9. package/dist/cjs/src/evm/EvmRequestTracker.js +219 -59
  10. package/dist/cjs/src/provider/detection.d.ts +30 -0
  11. package/dist/cjs/src/provider/detection.js +62 -0
  12. package/dist/cjs/src/provider/index.d.ts +1 -1
  13. package/dist/cjs/src/provider/index.js +3 -1
  14. package/dist/cjs/src/types/base.d.ts +23 -0
  15. package/dist/cjs/src/types/provider.d.ts +16 -0
  16. package/dist/cjs/src/types/provider.js +17 -1
  17. package/dist/cjs/src/version.d.ts +1 -1
  18. package/dist/cjs/src/version.js +1 -1
  19. package/dist/cjs/src/wagmi/WagmiEventHandler.d.ts +81 -0
  20. package/dist/cjs/src/wagmi/WagmiEventHandler.js +478 -38
  21. package/dist/esm/src/FormoAnalytics.d.ts +66 -0
  22. package/dist/esm/src/FormoAnalytics.js +176 -5
  23. package/dist/esm/src/FormoAnalyticsProvider.d.ts +8 -0
  24. package/dist/esm/src/FormoAnalyticsProvider.js +40 -31
  25. package/dist/esm/src/evm/EvmEventTracker.d.ts +38 -10
  26. package/dist/esm/src/evm/EvmEventTracker.js +183 -1
  27. package/dist/esm/src/evm/EvmProviderRegistry.js +27 -5
  28. package/dist/esm/src/evm/EvmRequestTracker.d.ts +40 -10
  29. package/dist/esm/src/evm/EvmRequestTracker.js +218 -58
  30. package/dist/esm/src/provider/detection.d.ts +30 -0
  31. package/dist/esm/src/provider/detection.js +60 -0
  32. package/dist/esm/src/provider/index.d.ts +1 -1
  33. package/dist/esm/src/provider/index.js +1 -1
  34. package/dist/esm/src/types/base.d.ts +23 -0
  35. package/dist/esm/src/types/provider.d.ts +16 -0
  36. package/dist/esm/src/types/provider.js +16 -0
  37. package/dist/esm/src/version.d.ts +1 -1
  38. package/dist/esm/src/version.js +1 -1
  39. package/dist/esm/src/wagmi/WagmiEventHandler.d.ts +81 -0
  40. package/dist/esm/src/wagmi/WagmiEventHandler.js +478 -38
  41. package/dist/index.umd.min.js +1 -1
  42. package/package.json +2 -2
@@ -96,6 +96,8 @@ export declare class FormoAnalytics implements IFormoAnalytics {
96
96
  * Call this when destroying the analytics instance
97
97
  * @returns {void}
98
98
  */
99
+ /** Set by cleanup(); a torn-down instance refuses new registrations. */
100
+ private isCleanedUp;
99
101
  cleanup(): void;
100
102
  /**
101
103
  * Emits a connect wallet event.
@@ -373,6 +375,70 @@ export declare class FormoAnalytics implements IFormoAnalytics {
373
375
  */
374
376
  get solana(): SolanaManager;
375
377
  private getCurrentChainId;
378
+ /**
379
+ * Track an EIP-1193 provider the page constructed itself.
380
+ *
381
+ * Discovery covers EIP-6963 announcements and `window.ethereum`, which is
382
+ * every injected wallet and nothing else. WalletConnect and Ledger
383
+ * providers are built by the app (`EthereumProvider.init(...)`) and
384
+ * announce nothing, so their sessions were invisible: connects,
385
+ * signatures, and transactions all silently missing. Hand the provider
386
+ * here once it exists and it takes the exact pipeline a discovered
387
+ * provider takes - detect event, lifecycle listeners, request wrapper -
388
+ * and a session that is already live is adopted from the provider's
389
+ * synchronous state.
390
+ *
391
+ * Metadata resolution order: the caller's `info` overrides win; then a
392
+ * WalletConnect session's peer metadata, which names the REAL wallet on
393
+ * the far side of the transport (for example "Ledger Live"); then flag
394
+ * sniffing; then a generic fallback. One deliberate exception: a caller
395
+ * name of exactly "WalletConnect" is the generic transport name, so the
396
+ * live peer still replaces it on events - name the provider anything
397
+ * else to pin it verbatim.
398
+ *
399
+ * No-op outside the EIP-1193 path: in wagmi mode the connector system
400
+ * already tracks these sessions, and wrapping the same provider twice
401
+ * would double-report every event.
402
+ *
403
+ * With several live SDK instances (multi write-key pages) registering
404
+ * the SAME provider, request-derived events (signatures, transactions)
405
+ * go to the most recently CREATED live instance, regardless of the
406
+ * order registrations happen to land in - the same single-observer
407
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
408
+ * instance. Fanning request observations out to all instances is a
409
+ * separate feature.
410
+ *
411
+ * @returns true when the provider is (now) tracked, false when it was
412
+ * refused (wagmi mode, EVM disabled, or not a valid EIP-1193 provider).
413
+ *
414
+ * @example
415
+ * ```typescript
416
+ * const wcProvider = await EthereumProvider.init({ projectId, chains });
417
+ * formo.registerProvider(wcProvider);
418
+ * ```
419
+ */
420
+ /**
421
+ * INTERNAL. Install the request wrapper on a wagmi connector's provider.
422
+ *
423
+ * Wagmi mode watches the store and caches, which see hook-driven calls
424
+ * only; imperative viem calls (walletClient.sendTransaction,
425
+ * .signMessage, .writeContract, raw request) create no mutation and were
426
+ * silently lost. Every viem client in a wagmi app is built on the
427
+ * connector's EIP-1193 provider, so wrapping that provider closes the
428
+ * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
429
+ * request wrapper installs here. Double counting is prevented in the
430
+ * wrapper via `shouldSkipRequestCapture`.
431
+ */
432
+ _wrapWagmiProvider(provider: EIP1193Provider, chainId?: number): boolean;
433
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
434
+ _rememberWagmiProviderChain(provider: EIP1193Provider, chainId: number | undefined): void;
435
+ registerProvider(provider: EIP1193Provider, info?: {
436
+ name?: string;
437
+ rdns?: string;
438
+ icon?: `data:image/${string}`;
439
+ }): boolean;
440
+ /** Fallback uuid suffix for platforms without crypto.randomUUID. */
441
+ private static registeredProviderSeq;
376
442
  getTrackedProvidersCount(): number;
377
443
  /**
378
444
  * Get current provider state for debugging
@@ -60,6 +60,7 @@ var address_1 = require("./utils/address");
60
60
  var TrackingPolicy_1 = require("./tracking/TrackingPolicy");
61
61
  var WalletStateStore_1 = require("./wallet/WalletStateStore");
62
62
  var EvmProviderRegistry_1 = require("./evm/EvmProviderRegistry");
63
+ var provider_1 = require("./provider");
63
64
  var EvmEventTracker_1 = require("./evm/EvmEventTracker");
64
65
  var EvmRequestTracker_1 = require("./evm/EvmRequestTracker");
65
66
  var chain_1 = require("./utils/chain");
@@ -93,6 +94,13 @@ var FormoAnalytics = /** @class */ (function () {
93
94
  this._currentUrl = "";
94
95
  this._pageHooksDisposed = false;
95
96
  this.currentUserId = "";
97
+ /**
98
+ * Clean up resources and event listeners
99
+ * Call this when destroying the analytics instance
100
+ * @returns {void}
101
+ */
102
+ /** Set by cleanup(); a torn-down instance refuses new registrations. */
103
+ this.isCleanedUp = false;
96
104
  this.config = {
97
105
  writeKey: writeKey,
98
106
  };
@@ -114,6 +122,7 @@ var FormoAnalytics = /** @class */ (function () {
114
122
  this.transaction = this.transaction.bind(this);
115
123
  this.detect = this.detect.bind(this);
116
124
  this.track = this.track.bind(this);
125
+ this.registerProvider = this.registerProvider.bind(this);
117
126
  this.page = this.page.bind(this);
118
127
  this.reset = this.reset.bind(this);
119
128
  this.cleanup = this.cleanup.bind(this);
@@ -171,6 +180,11 @@ var FormoAnalytics = /** @class */ (function () {
171
180
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
172
181
  signature: function (params, properties) { return _this.signature(params, properties); },
173
182
  transaction: function (params, properties) { return _this.transaction(params, properties); },
183
+ // Hybrid capture: in wagmi mode the wrapper skips a request that a
184
+ // PENDING wagmi mutation already covers - the mutation handler
185
+ // captures it with ABI enrichment - and captures everything else
186
+ // (imperative viem calls that create no mutation).
187
+ shouldSkipRequestCapture: function (method, params) { var _a, _b; return (_b = (_a = _this.wagmiHandler) === null || _a === void 0 ? void 0 : _a.hasMatchingPendingMutation(method, params)) !== null && _b !== void 0 ? _b : false; },
174
188
  });
175
189
  this.evmEvents = new EvmEventTracker_1.EvmEventTracker(this.wallet, this.evm, {
176
190
  isAutocaptureEnabled: function (t) { return _this.isAutocaptureEnabled(t); },
@@ -376,12 +390,8 @@ var FormoAnalytics = /** @class */ (function () {
376
390
  // re-attached to the next session's events.
377
391
  (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
378
392
  };
379
- /**
380
- * Clean up resources and event listeners
381
- * Call this when destroying the analytics instance
382
- * @returns {void}
383
- */
384
393
  FormoAnalytics.prototype.cleanup = function () {
394
+ this.isCleanedUp = true;
385
395
  logger_1.logger.debug("FormoAnalytics: Cleaning up resources");
386
396
  // Close the queue, don't just empty it. clear() only drops what is
387
397
  // buffered at this instant; asynchronous work already in flight (event
@@ -1017,7 +1027,21 @@ var FormoAnalytics = /** @class */ (function () {
1017
1027
  * @returns {void}
1018
1028
  */
1019
1029
  FormoAnalytics.prototype.optInTracking = function () {
1030
+ var _this = this;
1020
1031
  var _a;
1032
+ // A provider registered while the visitor was opted out had its
1033
+ // session adoption refused; nothing else retries it. Guarded: a
1034
+ // cleanup() racing this timer must not drive the torn-down tracker.
1035
+ setTimeout(function () {
1036
+ if (_this.isCleanedUp)
1037
+ return;
1038
+ try {
1039
+ _this.evmEvents.retryExternalAdoptions();
1040
+ }
1041
+ catch (_a) {
1042
+ /* adoption retry must never break opt-in */
1043
+ }
1044
+ }, 0);
1021
1045
  logger_1.logger.info("Opting back into tracking");
1022
1046
  // Remove opt-out flag
1023
1047
  (0, consent_1.removeConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY);
@@ -1108,6 +1132,17 @@ var FormoAnalytics = /** @class */ (function () {
1108
1132
  return __awaiter(this, void 0, void 0, function () {
1109
1133
  var _this = this;
1110
1134
  return __generator(this, function (_a) {
1135
+ // A route change can end path-based suppression; a provider registered
1136
+ // while suppressed gets its refused session adoption retried here.
1137
+ // Idempotent and cheap when nothing is pending.
1138
+ if (!this.isCleanedUp) {
1139
+ try {
1140
+ this.evmEvents.retryExternalAdoptions();
1141
+ }
1142
+ catch (_b) {
1143
+ /* never let the retry break a page hit */
1144
+ }
1145
+ }
1111
1146
  if (!this.shouldTrack()) {
1112
1147
  logger_1.logger.info("Track page hit: Skipping event due to tracking configuration");
1113
1148
  return [2 /*return*/];
@@ -1275,6 +1310,140 @@ var FormoAnalytics = /** @class */ (function () {
1275
1310
  };
1276
1311
  // Explicitly untrack a provider: remove listeners, clear wrapper flag
1277
1312
  // and tracking
1313
+ /**
1314
+ * Track an EIP-1193 provider the page constructed itself.
1315
+ *
1316
+ * Discovery covers EIP-6963 announcements and `window.ethereum`, which is
1317
+ * every injected wallet and nothing else. WalletConnect and Ledger
1318
+ * providers are built by the app (`EthereumProvider.init(...)`) and
1319
+ * announce nothing, so their sessions were invisible: connects,
1320
+ * signatures, and transactions all silently missing. Hand the provider
1321
+ * here once it exists and it takes the exact pipeline a discovered
1322
+ * provider takes - detect event, lifecycle listeners, request wrapper -
1323
+ * and a session that is already live is adopted from the provider's
1324
+ * synchronous state.
1325
+ *
1326
+ * Metadata resolution order: the caller's `info` overrides win; then a
1327
+ * WalletConnect session's peer metadata, which names the REAL wallet on
1328
+ * the far side of the transport (for example "Ledger Live"); then flag
1329
+ * sniffing; then a generic fallback. One deliberate exception: a caller
1330
+ * name of exactly "WalletConnect" is the generic transport name, so the
1331
+ * live peer still replaces it on events - name the provider anything
1332
+ * else to pin it verbatim.
1333
+ *
1334
+ * No-op outside the EIP-1193 path: in wagmi mode the connector system
1335
+ * already tracks these sessions, and wrapping the same provider twice
1336
+ * would double-report every event.
1337
+ *
1338
+ * With several live SDK instances (multi write-key pages) registering
1339
+ * the SAME provider, request-derived events (signatures, transactions)
1340
+ * go to the most recently CREATED live instance, regardless of the
1341
+ * order registrations happen to land in - the same single-observer
1342
+ * semantics discovery has always had for the request wrapper. Lifecycle events (connect, chain, disconnect) reach every
1343
+ * instance. Fanning request observations out to all instances is a
1344
+ * separate feature.
1345
+ *
1346
+ * @returns true when the provider is (now) tracked, false when it was
1347
+ * refused (wagmi mode, EVM disabled, or not a valid EIP-1193 provider).
1348
+ *
1349
+ * @example
1350
+ * ```typescript
1351
+ * const wcProvider = await EthereumProvider.init({ projectId, chains });
1352
+ * formo.registerProvider(wcProvider);
1353
+ * ```
1354
+ */
1355
+ /**
1356
+ * INTERNAL. Install the request wrapper on a wagmi connector's provider.
1357
+ *
1358
+ * Wagmi mode watches the store and caches, which see hook-driven calls
1359
+ * only; imperative viem calls (walletClient.sendTransaction,
1360
+ * .signMessage, .writeContract, raw request) create no mutation and were
1361
+ * silently lost. Every viem client in a wagmi app is built on the
1362
+ * connector's EIP-1193 provider, so wrapping that provider closes the
1363
+ * gap. Lifecycle (connect/chain/disconnect) stays store-driven: only the
1364
+ * request wrapper installs here. Double counting is prevented in the
1365
+ * wrapper via `shouldSkipRequestCapture`.
1366
+ */
1367
+ FormoAnalytics.prototype._wrapWagmiProvider = function (provider, chainId) {
1368
+ if (this.isCleanedUp || !(0, provider_1.isValidProvider)(provider))
1369
+ return false;
1370
+ try {
1371
+ // The tracker reports refusals (frozen provider, unrebindable
1372
+ // wrapper) by returning false rather than throwing; treat those as
1373
+ // failures too so the caller can retry later.
1374
+ if (!this.evmRequests.registerRequestListeners(provider)) {
1375
+ return false;
1376
+ }
1377
+ if (chainId !== undefined) {
1378
+ this.evm.rememberChain(provider, chainId);
1379
+ }
1380
+ return true;
1381
+ }
1382
+ catch (e) {
1383
+ logger_1.logger.warn("Failed to wrap wagmi provider for hybrid capture", e);
1384
+ return false;
1385
+ }
1386
+ };
1387
+ /** INTERNAL. Chain updates for the fallback-wrapped provider. */
1388
+ FormoAnalytics.prototype._rememberWagmiProviderChain = function (provider, chainId) {
1389
+ if (this.isCleanedUp)
1390
+ return;
1391
+ try {
1392
+ this.evm.rememberChain(provider, chainId);
1393
+ }
1394
+ catch (_a) {
1395
+ /* bookkeeping only */
1396
+ }
1397
+ };
1398
+ FormoAnalytics.prototype.registerProvider = function (provider, info) {
1399
+ var _a, _b, _c;
1400
+ if (this.isCleanedUp) {
1401
+ // Cleanup terminally closed the event queue; listeners attached now
1402
+ // would hold this instance forever and deliver nothing.
1403
+ logger_1.logger.warn("registerProvider: instance is cleaned up; refusing");
1404
+ return false;
1405
+ }
1406
+ if (this.isEvmDisabled) {
1407
+ logger_1.logger.warn("registerProvider: EVM tracking is disabled; refusing");
1408
+ return false;
1409
+ }
1410
+ if (this.isWagmiMode) {
1411
+ logger_1.logger.warn("registerProvider: wagmi mode tracks connectors already; registering the provider here would double-report its events. Refusing.");
1412
+ return false;
1413
+ }
1414
+ if (!(0, provider_1.isValidProvider)(provider)) {
1415
+ logger_1.logger.warn("registerProvider: not a valid EIP-1193 provider; refusing");
1416
+ return false;
1417
+ }
1418
+ var detected = (0, provider_1.detectInjectedProviderInfo)(provider);
1419
+ var peer = (0, provider_1.readWalletConnectPeer)(provider);
1420
+ // A live peer identifies the session as WalletConnect even when the
1421
+ // provider carries no isWalletConnect flag (v2 providers often do not).
1422
+ var rdns = (_a = info === null || info === void 0 ? void 0 : info.rdns) !== null && _a !== void 0 ? _a : (peer && detected.rdns === "io.injected.provider"
1423
+ ? "com.walletconnect"
1424
+ : detected.rdns);
1425
+ // The peer name is deliberately NOT stored: sessions change wallets,
1426
+ // and metadata frozen at registration would misname every later one.
1427
+ // `infoFor` resolves the peer live on each read, over the generic
1428
+ // transport name; a caller's explicit name still wins everywhere.
1429
+ var name = (_b = info === null || info === void 0 ? void 0 : info.name) !== null && _b !== void 0 ? _b : (peer ? "WalletConnect" : detected.name);
1430
+ // Per-instance uuid: EIP-6963 consumers (mipd included) deduplicate on
1431
+ // it, so two registered instances sharing an rdns-derived uuid would
1432
+ // collapse into one. Random when the platform provides it; a
1433
+ // monotonic suffix otherwise.
1434
+ var uuid = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function"
1435
+ ? crypto.randomUUID()
1436
+ : "registered-".concat(rdns.replace(/[^a-zA-Z0-9]/g, "-"), "-").concat((FormoAnalytics.registeredProviderSeq += 1));
1437
+ return this.evmEvents.adoptExternalProvider({
1438
+ info: {
1439
+ name: name,
1440
+ rdns: rdns,
1441
+ uuid: uuid,
1442
+ icon: (_c = info === null || info === void 0 ? void 0 : info.icon) !== null && _c !== void 0 ? _c : detected.icon,
1443
+ },
1444
+ provider: provider,
1445
+ });
1446
+ };
1278
1447
  // Debug/monitoring helpers
1279
1448
  FormoAnalytics.prototype.getTrackedProvidersCount = function () {
1280
1449
  return this.evm.counts.trackedProviders;
@@ -1286,6 +1455,8 @@ var FormoAnalytics = /** @class */ (function () {
1286
1455
  FormoAnalytics.prototype.getProviderState = function () {
1287
1456
  return __assign(__assign({}, this.evm.counts), { activeProvider: !!this._provider });
1288
1457
  };
1458
+ /** Fallback uuid suffix for platforms without crypto.randomUUID. */
1459
+ FormoAnalytics.registeredProviderSeq = 0;
1289
1460
  return FormoAnalytics;
1290
1461
  }());
1291
1462
  exports.FormoAnalytics = FormoAnalytics;
@@ -7,6 +7,14 @@ export interface FormoAnalyticsProviderProps {
7
7
  children: ReactNode;
8
8
  }
9
9
  export declare const FormoAnalyticsContext: import("react").Context<IFormoAnalytics>;
10
+ /**
11
+ * A stable key over the serializable parts of Options. The provider effect
12
+ * re-initialises the SDK when this key changes; anything that alters SDK
13
+ * behaviour must be represented here, or a runtime change to it is silently
14
+ * ignored. Complex objects are tracked by presence, plus the flags that
15
+ * change what the SDK does with them.
16
+ */
17
+ export declare const computeOptionsKey: (options?: Options) => string;
10
18
  export declare const FormoAnalyticsProvider: FC<FormoAnalyticsProviderProps>;
11
19
  export declare const useFormo: () => IFormoAnalytics;
12
20
  //# sourceMappingURL=FormoAnalyticsProvider.d.ts.map
@@ -47,7 +47,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
47
47
  }
48
48
  };
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
- exports.useFormo = exports.FormoAnalyticsProvider = exports.FormoAnalyticsContext = void 0;
50
+ exports.useFormo = exports.FormoAnalyticsProvider = exports.computeOptionsKey = exports.FormoAnalyticsContext = void 0;
51
51
  var jsx_runtime_1 = require("react/jsx-runtime");
52
52
  var react_1 = require("react");
53
53
  var FormoAnalytics_1 = require("./FormoAnalytics");
@@ -58,6 +58,7 @@ var defaultContext = {
58
58
  page: function () { return Promise.resolve(); },
59
59
  reset: function () { },
60
60
  cleanup: function () { },
61
+ registerProvider: function () { return false; },
61
62
  detect: function () { return Promise.resolve(); },
62
63
  connect: function () { return Promise.resolve(); },
63
64
  disconnect: function () { return Promise.resolve(); },
@@ -71,6 +72,45 @@ var defaultContext = {
71
72
  hasOptedOutTracking: function () { return false; },
72
73
  };
73
74
  exports.FormoAnalyticsContext = (0, react_1.createContext)(defaultContext);
75
+ /**
76
+ * A stable key over the serializable parts of Options. The provider effect
77
+ * re-initialises the SDK when this key changes; anything that alters SDK
78
+ * behaviour must be represented here, or a runtime change to it is silently
79
+ * ignored. Complex objects are tracked by presence, plus the flags that
80
+ * change what the SDK does with them.
81
+ */
82
+ var computeOptionsKey = function (options) {
83
+ var _a;
84
+ if (!options)
85
+ return 'undefined';
86
+ var serializableOptions = {
87
+ tracking: options.tracking,
88
+ autocapture: options.autocapture,
89
+ crossSubdomainCookies: options.crossSubdomainCookies,
90
+ apiHost: options.apiHost,
91
+ flushAt: options.flushAt,
92
+ flushInterval: options.flushInterval,
93
+ retryCount: options.retryCount,
94
+ maxQueueSize: options.maxQueueSize,
95
+ logger: options.logger,
96
+ referral: options.referral,
97
+ evm: options.evm,
98
+ // For complex objects, just track their presence, not their content
99
+ hasProvider: !!options.provider,
100
+ hasWagmi: !!options.wagmi,
101
+ wagmiEip1193Fallback: !!((_a = options.wagmi) === null || _a === void 0 ? void 0 : _a.eip1193Fallback),
102
+ hasReady: !!options.ready,
103
+ };
104
+ try {
105
+ return JSON.stringify(serializableOptions);
106
+ }
107
+ catch (error) {
108
+ // Fallback to timestamp if serialization fails
109
+ logger_1.logger.warn('Failed to serialize options, using timestamp', error);
110
+ return Date.now().toString();
111
+ }
112
+ };
113
+ exports.computeOptionsKey = computeOptionsKey;
74
114
  var FormoAnalyticsProvider = function (props) {
75
115
  var writeKey = props.writeKey, _a = props.disabled, disabled = _a === void 0 ? false : _a, children = props.children;
76
116
  // Keep the app running without analytics if no Write Key is provided or disabled
@@ -90,37 +130,7 @@ var InitializedAnalytics = function (_a) {
90
130
  var _b = (0, react_1.useState)(defaultContext), sdk = _b[0], setSdk = _b[1];
91
131
  var sdkRef = (0, react_1.useRef)(defaultContext);
92
132
  (0, storage_1.initStorageManager)(writeKey);
93
- // Create a stable key from options that ignores complex objects and functions
94
- // We only care about serializable config values that would affect SDK behavior
95
- var optionsKey = (0, react_1.useMemo)(function () {
96
- if (!options)
97
- return 'undefined';
98
- // Extract only the serializable parts of options
99
- var serializableOptions = {
100
- tracking: options.tracking,
101
- autocapture: options.autocapture,
102
- crossSubdomainCookies: options.crossSubdomainCookies,
103
- apiHost: options.apiHost,
104
- flushAt: options.flushAt,
105
- flushInterval: options.flushInterval,
106
- retryCount: options.retryCount,
107
- maxQueueSize: options.maxQueueSize,
108
- logger: options.logger,
109
- referral: options.referral,
110
- // For complex objects, just track their presence, not their content
111
- hasProvider: !!options.provider,
112
- hasWagmi: !!options.wagmi,
113
- hasReady: !!options.ready,
114
- };
115
- try {
116
- return JSON.stringify(serializableOptions);
117
- }
118
- catch (error) {
119
- // Fallback to timestamp if serialization fails
120
- logger_1.logger.warn('Failed to serialize options, using timestamp', error);
121
- return Date.now().toString();
122
- }
123
- }, [options]);
133
+ var optionsKey = (0, react_1.useMemo)(function () { return (0, exports.computeOptionsKey)(options); }, [options]);
124
134
  (0, react_1.useEffect)(function () {
125
135
  var isCleanedUp = false;
126
136
  var initialize = function () { return __awaiter(void 0, void 0, void 0, function () {
@@ -43,20 +43,19 @@ export interface EvmEventTrackerDeps {
43
43
  */
44
44
  registerRequestListeners(provider: EIP1193Provider): boolean;
45
45
  }
46
- /**
47
- * The EIP-1193 side of wallet tracking: which providers to watch, and what
48
- * their events mean.
49
- *
50
- * Split out of `FormoAnalytics` (#336). It holds no wallet state and no
51
- * provider registry of its own - those have owners already - so what is left
52
- * here is the part that is genuinely about interpreting wallet events:
53
- * deciding when a connect has to be reported, when a switch is stale, and
54
- * when a provider has stopped being the one we follow.
55
- */
56
46
  export declare class EvmEventTracker {
57
47
  private readonly wallet;
58
48
  private readonly registry;
59
49
  private readonly deps;
50
+ /**
51
+ * Providers adopted through `registerProvider` rather than discovered.
52
+ * Announcement-driven cleanup must not touch them: they are never in an
53
+ * announcement list, so "missing from the announcement" is their normal
54
+ * state, not evidence of removal. A Set rather than a WeakSet because
55
+ * suppressed adoptions retry from it; the registry holds these providers
56
+ * strongly anyway, and untrack removes them.
57
+ */
58
+ private externallyRegistered;
60
59
  /**
61
60
  * The connect this SDK has already reported for a provider.
62
61
  *
@@ -91,6 +90,35 @@ export declare class EvmEventTracker {
91
90
  */
92
91
  trackEIP1193Provider(provider: EIP1193Provider): void;
93
92
  trackProviders(providers: readonly EIP6963ProviderDetail[]): void;
93
+ /**
94
+ * Adopt a provider the page constructed rather than announced.
95
+ *
96
+ * Discovery only ever sees EIP-6963 announcements and `window.ethereum`.
97
+ * A WalletConnect or Ledger provider is a constructed object that does
98
+ * neither, so without this entry point its whole session is invisible -
99
+ * the P-2403 gap. The pipeline from here on is the same one every
100
+ * discovered provider takes: registry, detect event, listeners, request
101
+ * wrapper.
102
+ *
103
+ * A session that already exists at registration is seeded from the
104
+ * provider's SYNCHRONOUS `accounts` state (WalletConnect exposes it), via
105
+ * the same accounts-arrival path a live `accountsChanged` takes. No RPC:
106
+ * nothing analytics-only may go on a wallet's transport, and
107
+ * WalletConnect's serialised relay socket is the very case that rule
108
+ * exists for.
109
+ */
110
+ adoptExternalProvider(detail: EIP6963ProviderDetail): boolean;
111
+ /**
112
+ * Re-run session adoption for every registered external provider.
113
+ *
114
+ * Registration while tracking was suppressed (opt-out, excluded route)
115
+ * reached the adoption path and was refused - and a provider whose
116
+ * session already exists may never emit another accountsChanged, so
117
+ * nothing would ever retry. Called when suppression can have ended
118
+ * (opt-in, page navigation). Idempotent: an already-adopted wallet is
119
+ * deduplicated by the same state and markers as any repeated signal.
120
+ */
121
+ retryExternalAdoptions(): void;
94
122
  private registerAccountsChangedListener;
95
123
  private onAccountsChanged;
96
124
  /**