@formo/analytics 1.29.0 → 1.30.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 (48) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +52 -3
  2. package/dist/cjs/src/FormoAnalytics.js +262 -51
  3. package/dist/cjs/src/constants/base.d.ts +9 -0
  4. package/dist/cjs/src/constants/base.js +10 -1
  5. package/dist/cjs/src/constants/config.d.ts +3 -58
  6. package/dist/cjs/src/constants/config.js +3 -73
  7. package/dist/cjs/src/event/EventFactory.d.ts +1 -1
  8. package/dist/cjs/src/event/EventFactory.js +12 -6
  9. package/dist/cjs/src/event/EventManager.d.ts +2 -0
  10. package/dist/cjs/src/event/EventManager.js +4 -0
  11. package/dist/cjs/src/event/type.d.ts +1 -0
  12. package/dist/cjs/src/event/utils.js +1 -1
  13. package/dist/cjs/src/queue/EventQueue.d.ts +8 -0
  14. package/dist/cjs/src/queue/EventQueue.js +52 -6
  15. package/dist/cjs/src/queue/type.d.ts +1 -0
  16. package/dist/cjs/src/session/index.js +16 -8
  17. package/dist/cjs/src/storage/cookiePolicy.d.ts +16 -0
  18. package/dist/cjs/src/storage/cookiePolicy.js +19 -0
  19. package/dist/cjs/src/types/base.d.ts +0 -1
  20. package/dist/cjs/src/types/events.d.ts +0 -1
  21. package/dist/cjs/src/version.d.ts +1 -1
  22. package/dist/cjs/src/version.js +1 -1
  23. package/dist/cjs/src/wagmi/WagmiEventHandler.js +38 -15
  24. package/dist/esm/src/FormoAnalytics.d.ts +52 -3
  25. package/dist/esm/src/FormoAnalytics.js +265 -54
  26. package/dist/esm/src/constants/base.d.ts +9 -0
  27. package/dist/esm/src/constants/base.js +9 -0
  28. package/dist/esm/src/constants/config.d.ts +3 -58
  29. package/dist/esm/src/constants/config.js +3 -73
  30. package/dist/esm/src/event/EventFactory.d.ts +1 -1
  31. package/dist/esm/src/event/EventFactory.js +12 -6
  32. package/dist/esm/src/event/EventManager.d.ts +2 -0
  33. package/dist/esm/src/event/EventManager.js +4 -0
  34. package/dist/esm/src/event/type.d.ts +1 -0
  35. package/dist/esm/src/event/utils.js +2 -2
  36. package/dist/esm/src/queue/EventQueue.d.ts +8 -0
  37. package/dist/esm/src/queue/EventQueue.js +52 -6
  38. package/dist/esm/src/queue/type.d.ts +1 -0
  39. package/dist/esm/src/session/index.js +16 -8
  40. package/dist/esm/src/storage/cookiePolicy.d.ts +16 -0
  41. package/dist/esm/src/storage/cookiePolicy.js +18 -0
  42. package/dist/esm/src/types/base.d.ts +0 -1
  43. package/dist/esm/src/types/events.d.ts +0 -1
  44. package/dist/esm/src/version.d.ts +1 -1
  45. package/dist/esm/src/version.js +1 -1
  46. package/dist/esm/src/wagmi/WagmiEventHandler.js +38 -15
  47. package/dist/index.umd.min.js +1 -1
  48. package/package.json +13 -23
@@ -54,6 +54,10 @@ export declare class FormoAnalytics implements IFormoAnalytics {
54
54
  private crossSubdomainCookies;
55
55
  /** In-memory URL used to deduplicate SPA pageview events. */
56
56
  private _currentUrl;
57
+ /** Page-hit hooks installed in trackPageHits() so cleanup() can undo them. */
58
+ private _onPopStateListener?;
59
+ private _onLocationChangeListener?;
60
+ private _pageHooksDisposed;
57
61
  config: Config;
58
62
  currentChainId?: ChainID;
59
63
  currentAddress?: Address;
@@ -132,18 +136,16 @@ export declare class FormoAnalytics implements IFormoAnalytics {
132
136
  * @param {ChainID} params.chainId
133
137
  * @param {Address} params.address
134
138
  * @param {string} params.message
135
- * @param {string} params.signatureHash - only provided if status is confirmed
136
139
  * @param {IFormoEventProperties} properties
137
140
  * @param {IFormoEventContext} context
138
141
  * @param {(...args: unknown[]) => void} callback
139
142
  * @returns {Promise<void>}
140
143
  */
141
- signature({ status, chainId, address, message, signatureHash, }: {
144
+ signature({ status, chainId, address, message, }: {
142
145
  status: SignatureStatus;
143
146
  chainId?: ChainID;
144
147
  address: Address;
145
148
  message: string;
146
- signatureHash?: string;
147
149
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
148
150
  /**
149
151
  * Emits a transaction event.
@@ -272,6 +274,12 @@ export declare class FormoAnalytics implements IFormoAnalytics {
272
274
  private registerRequestListeners;
273
275
  private onLocationChange;
274
276
  private trackPageHits;
277
+ /**
278
+ * Wrap history.pushState / replaceState exactly once per `history` object,
279
+ * regardless of how many SDK instances are constructed. Uses a Symbol
280
+ * marker so we recognize our own wrapper across module reloads in HMR.
281
+ */
282
+ private static installHistoryHooksOnce;
275
283
  private trackPageHit;
276
284
  private trackEvent;
277
285
  /**
@@ -312,6 +320,15 @@ export declare class FormoAnalytics implements IFormoAnalytics {
312
320
  private getCurrentChainId;
313
321
  private buildSignatureEventPayload;
314
322
  private buildTransactionEventPayload;
323
+ /**
324
+ * Persist an EVM address discovered through autocapture (signature / transaction)
325
+ * as the current EVM address when none is currently set. This lets subsequent
326
+ * track()/page() calls carry the address even when the underlying wallet never
327
+ * fires an EIP-1193 `accountsChanged` event (embedded wallets, smart accounts,
328
+ * social-login wrappers). If `accountsChanged` later fires it overwrites this
329
+ * value in the normal way; existing connections are never clobbered.
330
+ */
331
+ private backfillActiveWallet;
315
332
  /**
316
333
  * Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
317
334
  */
@@ -362,11 +379,43 @@ export declare class FormoAnalytics implements IFormoAnalytics {
362
379
  * Clear per-chain state for a given namespace (or chainId) and sync derived state.
363
380
  */
364
381
  private clearChainState;
382
+ /**
383
+ * Sync validated wallet/chain state into the SDK's central state
384
+ * WITHOUT emitting an event.
385
+ *
386
+ * Integrations (e.g. the wagmi handler) must call this on every
387
+ * connect / chain-change / disconnect — even when the corresponding
388
+ * autocapture event is disabled. Otherwise `currentChainId` stays
389
+ * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
390
+ * check (which keys off `currentChainId`, not the event payload) can
391
+ * be bypassed, letting wallet activity on an excluded chain still be
392
+ * collected.
393
+ *
394
+ * - valid `address` present → record per-chain + derived state
395
+ * - `address` absent → clear chain state (disconnect)
396
+ */
397
+ syncWalletState(params: {
398
+ chainId?: ChainID;
399
+ address?: Address;
400
+ }): void;
365
401
  /**
366
402
  * Synchronize currentAddress/currentChainId from the active namespace.
367
403
  * Last-connected-chain-wins: _activeNamespace takes precedence.
368
404
  */
369
405
  private syncDerivedState;
406
+ /**
407
+ * Persist (or clear) the current wallet snapshot in a cookie so that the
408
+ * SDK can repopulate `currentAddress`/`currentChainId` at init on the next
409
+ * page load — closing the gap between page-show and wagmi/EIP-1193
410
+ * reconnection during which track()/page() events would otherwise ship
411
+ * with an empty address.
412
+ */
413
+ private persistActiveWallet;
414
+ /**
415
+ * Seed `currentAddress`/`currentChainId` from the persisted snapshot, if
416
+ * any. Called once during construction before the first page hit fires.
417
+ */
418
+ private loadActiveWallet;
370
419
  /**
371
420
  * Helper method to clear the active provider state
372
421
  * Centralizes provider clearing logic for consistency
@@ -85,6 +85,7 @@ var PROVIDER_SWITCH_REASONS = {
85
85
  var FormoAnalytics = /** @class */ (function () {
86
86
  function FormoAnalytics(writeKey, options) {
87
87
  if (options === void 0) { options = {}; }
88
+ var _this = this;
88
89
  var _a, _b, _c;
89
90
  this.writeKey = writeKey;
90
91
  this.options = options;
@@ -124,6 +125,7 @@ var FormoAnalytics = /** @class */ (function () {
124
125
  this.isEvmDisabled = false;
125
126
  /** In-memory URL used to deduplicate SPA pageview events. */
126
127
  this._currentUrl = "";
128
+ this._pageHooksDisposed = false;
127
129
  this.currentUserId = "";
128
130
  this.config = {
129
131
  writeKey: writeKey,
@@ -159,6 +161,10 @@ var FormoAnalytics = /** @class */ (function () {
159
161
  maxQueueSize: options.maxQueueSize,
160
162
  flushInterval: options.flushInterval,
161
163
  errorHandler: options.errorHandler,
164
+ // Hard consent gate at the queue boundary: nothing buffered is
165
+ // ever sent once the user has opted out, even via a timer or
166
+ // pagehide flush scheduled before opt-out.
167
+ canSend: function () { return !_this.hasOptedOutTracking(); },
162
168
  }), options);
163
169
  // Check consent status on initialization
164
170
  if (this.hasOptedOutTracking()) {
@@ -191,6 +197,10 @@ var FormoAnalytics = /** @class */ (function () {
191
197
  this.solanaManager = new SolanaManager_1.SolanaManager(this, options.solana);
192
198
  }
193
199
  this._currentUrl = window.location.href;
200
+ // Seed currentAddress/currentChainId from the persisted snapshot before
201
+ // the first page hit queues so reload-time track()/page() carry the
202
+ // wallet even before wagmi/EIP-1193 reconnection completes.
203
+ this.loadActiveWallet();
194
204
  this.trackPageHit();
195
205
  this.trackPageHits();
196
206
  }
@@ -292,10 +302,25 @@ var FormoAnalytics = /** @class */ (function () {
292
302
  */
293
303
  FormoAnalytics.prototype.reset = function () {
294
304
  this.currentUserId = undefined;
305
+ // Clear in-memory wallet identity too. Without this, a logout/reset
306
+ // (also triggered by optOutTracking) still leaks the previous wallet
307
+ // address on subsequent track()/page() events for the rest of the
308
+ // page lifetime, because they fall back to currentAddress. Keep the
309
+ // EVM provider reference so tracking can resume on the next connect.
310
+ this.currentAddress = undefined;
311
+ this.currentChainId = undefined;
312
+ var evmProvider = this._chainState.evm.provider;
313
+ this._chainState = { evm: { provider: evmProvider }, solana: {} };
314
+ this._activeNamespace = undefined;
295
315
  (0, storage_1.cookie)().remove(constants_1.LOCAL_ANONYMOUS_ID_KEY);
296
316
  (0, storage_1.cookie)().remove(constants_1.SESSION_USER_ID_KEY);
297
317
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_DETECTED_KEY);
298
318
  (0, storage_1.cookie)().remove(session_1.SESSION_WALLET_IDENTIFIED_KEY);
319
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
320
+ // Stored traffic-source attribution (referrer/UTM) is tracking data;
321
+ // clear it too so reset()/optOutTracking() don't leave it to be
322
+ // re-attached to the next session's events.
323
+ (0, storage_1.session)().remove(constants_1.SESSION_TRAFFIC_SOURCE_KEY);
299
324
  };
300
325
  /**
301
326
  * Clean up resources and event listeners
@@ -304,6 +329,8 @@ var FormoAnalytics = /** @class */ (function () {
304
329
  */
305
330
  FormoAnalytics.prototype.cleanup = function () {
306
331
  logger_1.logger.debug("FormoAnalytics: Cleaning up resources");
332
+ // Drop buffered events so a torn-down instance can't flush later.
333
+ this.eventManager.clear();
307
334
  // Clean up Wagmi handler if present
308
335
  if (this.wagmiHandler) {
309
336
  this.wagmiHandler.cleanup();
@@ -321,6 +348,21 @@ var FormoAnalytics = /** @class */ (function () {
321
348
  this.untrackProvider(provider);
322
349
  }
323
350
  }
351
+ // Tear down page-hit hooks: remove window listeners and silence the
352
+ // history.pushState/replaceState wrappers so an orphaned instance (e.g.
353
+ // from a re-mount in React Strict Mode / HMR) stops emitting page events
354
+ // with stale state.
355
+ this._pageHooksDisposed = true;
356
+ if (typeof window !== "undefined") {
357
+ if (this._onPopStateListener) {
358
+ window.removeEventListener("popstate", this._onPopStateListener);
359
+ this._onPopStateListener = undefined;
360
+ }
361
+ if (this._onLocationChangeListener) {
362
+ window.removeEventListener("locationchange", this._onLocationChangeListener);
363
+ this._onLocationChangeListener = undefined;
364
+ }
365
+ }
324
366
  logger_1.logger.debug("FormoAnalytics: Cleanup complete");
325
367
  };
326
368
  /**
@@ -452,7 +494,6 @@ var FormoAnalytics = /** @class */ (function () {
452
494
  * @param {ChainID} params.chainId
453
495
  * @param {Address} params.address
454
496
  * @param {string} params.message
455
- * @param {string} params.signatureHash - only provided if status is confirmed
456
497
  * @param {IFormoEventProperties} properties
457
498
  * @param {IFormoEventContext} context
458
499
  * @param {(...args: unknown[]) => void} callback
@@ -460,10 +501,15 @@ var FormoAnalytics = /** @class */ (function () {
460
501
  */
461
502
  FormoAnalytics.prototype.signature = function (_a, properties_1, context_1, callback_1) {
462
503
  return __awaiter(this, arguments, void 0, function (_b, properties, context, callback) {
463
- var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message, signatureHash = _b.signatureHash;
504
+ var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message;
464
505
  return __generator(this, function (_c) {
465
506
  switch (_c.label) {
466
- case 0: return [4 /*yield*/, this.trackEvent(constants_1.EventType.SIGNATURE, __assign({ status: status, chainId: chainId, address: address, message: message }, (signatureHash && { signatureHash: signatureHash })), properties, context, callback)];
507
+ case 0: return [4 /*yield*/, this.trackEvent(constants_1.EventType.SIGNATURE, {
508
+ status: status,
509
+ chainId: chainId,
510
+ address: address,
511
+ message: message,
512
+ }, properties, context, callback)];
467
513
  case 1:
468
514
  _c.sent();
469
515
  return [2 /*return*/];
@@ -534,6 +580,13 @@ var FormoAnalytics = /** @class */ (function () {
534
580
  switch (_d.label) {
535
581
  case 0:
536
582
  _d.trys.push([0, 12, , 13]);
583
+ // identify() writes the user-id cookie and marks wallet
584
+ // identification before trackEvent's consent check — gate the
585
+ // whole method so an opted-out user gets no identity persistence.
586
+ if (this.hasOptedOutTracking()) {
587
+ logger_1.logger.info("identify() skipped: user has opted out of tracking");
588
+ return [2 /*return*/];
589
+ }
537
590
  if (!!params) return [3 /*break*/, 10];
538
591
  // If no params provided, auto-identify
539
592
  logger_1.logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
@@ -600,6 +653,7 @@ var FormoAnalytics = /** @class */ (function () {
600
653
  validAddress = (0, address_1.validateAddress)(address);
601
654
  if (validAddress) {
602
655
  this.currentAddress = validAddress;
656
+ this.persistActiveWallet();
603
657
  }
604
658
  else {
605
659
  (_c = logger_1.logger.warn) === null || _c === void 0 ? void 0 : _c.call(logger_1.logger, "Invalid address provided to identify:", address);
@@ -608,7 +662,7 @@ var FormoAnalytics = /** @class */ (function () {
608
662
  if (userId) {
609
663
  this.currentUserId = userId;
610
664
  domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
611
- (0, storage_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId, __assign({ path: "/" }, (domain ? { domain: domain } : {})));
665
+ (0, storage_1.cookie)().set(constants_1.SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
612
666
  }
613
667
  isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "");
614
668
  logger_1.logger.debug("Identify: Checking deduplication", {
@@ -657,6 +711,12 @@ var FormoAnalytics = /** @class */ (function () {
657
711
  return __generator(this, function (_c) {
658
712
  switch (_c.label) {
659
713
  case 0:
714
+ // detect() marks wallet detection (a cookie write) before
715
+ // trackEvent's consent check — gate it on opt-out.
716
+ if (this.hasOptedOutTracking()) {
717
+ logger_1.logger.info("detect() skipped: user has opted out of tracking");
718
+ return [2 /*return*/];
719
+ }
660
720
  if (this.session.isWalletDetected(rdns))
661
721
  return [2 /*return*/, logger_1.logger.warn("Detect: Wallet ".concat(providerName, " already detected in this session"))];
662
722
  this.session.markWalletDetected(rdns);
@@ -703,6 +763,9 @@ var FormoAnalytics = /** @class */ (function () {
703
763
  // Set opt-out flag in persistent storage using direct cookie access
704
764
  // This must be done before switching storage to ensure persistence
705
765
  (0, consent_1.setConsentFlag)(this.writeKey, constants_1.CONSENT_OPT_OUT_KEY, "true");
766
+ // Drop anything already buffered so a pending timer/pagehide flush
767
+ // cannot ship events after consent withdrawal.
768
+ this.eventManager.clear();
706
769
  this.reset();
707
770
  logger_1.logger.info("Successfully opted out of tracking");
708
771
  };
@@ -1453,35 +1516,52 @@ var FormoAnalytics = /** @class */ (function () {
1453
1516
  });
1454
1517
  };
1455
1518
  FormoAnalytics.prototype.trackPageHits = function () {
1456
- return __awaiter(this, void 0, void 0, function () {
1457
- var oldPushState, oldReplaceState;
1458
- var _this = this;
1459
- return __generator(this, function (_a) {
1460
- oldPushState = history.pushState;
1461
- history.pushState = function pushState() {
1462
- var args = [];
1463
- for (var _i = 0; _i < arguments.length; _i++) {
1464
- args[_i] = arguments[_i];
1465
- }
1466
- var ret = oldPushState.apply(this, args);
1467
- window.dispatchEvent(new window.Event("locationchange"));
1468
- return ret;
1469
- };
1470
- oldReplaceState = history.replaceState;
1471
- history.replaceState = function replaceState() {
1472
- var args = [];
1473
- for (var _i = 0; _i < arguments.length; _i++) {
1474
- args[_i] = arguments[_i];
1475
- }
1476
- var ret = oldReplaceState.apply(this, args);
1477
- window.dispatchEvent(new window.Event("locationchange"));
1478
- return ret;
1479
- };
1480
- window.addEventListener("popstate", function () { return _this.onLocationChange(); });
1481
- window.addEventListener("locationchange", function () { return _this.onLocationChange(); });
1482
- return [2 /*return*/];
1483
- });
1484
- });
1519
+ var _this = this;
1520
+ // Install a single, instance-agnostic wrapper around history.pushState /
1521
+ // replaceState so concurrent SDK instances (React Strict Mode, HMR) don't
1522
+ // each stack their own wrapper — which would dispatch N synthetic events
1523
+ // per navigation and produce O(N^2) onLocationChange calls. The wrapper
1524
+ // dispatches once; per-instance bookkeeping is done by per-instance
1525
+ // listeners that each register/unregister themselves.
1526
+ FormoAnalytics.installHistoryHooksOnce();
1527
+ this._onPopStateListener = function () { return _this.onLocationChange(); };
1528
+ this._onLocationChangeListener = function () { return _this.onLocationChange(); };
1529
+ window.addEventListener("popstate", this._onPopStateListener);
1530
+ window.addEventListener("locationchange", this._onLocationChangeListener);
1531
+ };
1532
+ /**
1533
+ * Wrap history.pushState / replaceState exactly once per `history` object,
1534
+ * regardless of how many SDK instances are constructed. Uses a Symbol
1535
+ * marker so we recognize our own wrapper across module reloads in HMR.
1536
+ */
1537
+ FormoAnalytics.installHistoryHooksOnce = function () {
1538
+ if (typeof history === "undefined" || typeof window === "undefined")
1539
+ return;
1540
+ var marker = Symbol.for("formo.historyWrapped");
1541
+ if (history[marker])
1542
+ return;
1543
+ history[marker] = true;
1544
+ var dispatch = function () { return window.dispatchEvent(new window.Event("locationchange")); };
1545
+ var oldPushState = history.pushState;
1546
+ history.pushState = function pushState() {
1547
+ var args = [];
1548
+ for (var _i = 0; _i < arguments.length; _i++) {
1549
+ args[_i] = arguments[_i];
1550
+ }
1551
+ var ret = oldPushState.apply(this, args);
1552
+ dispatch();
1553
+ return ret;
1554
+ };
1555
+ var oldReplaceState = history.replaceState;
1556
+ history.replaceState = function replaceState() {
1557
+ var args = [];
1558
+ for (var _i = 0; _i < arguments.length; _i++) {
1559
+ args[_i] = arguments[_i];
1560
+ }
1561
+ var ret = oldReplaceState.apply(this, args);
1562
+ dispatch();
1563
+ return ret;
1564
+ };
1485
1565
  };
1486
1566
  FormoAnalytics.prototype.trackPageHit = function (category, name, properties, context, callback) {
1487
1567
  return __awaiter(this, void 0, void 0, function () {
@@ -1492,6 +1572,12 @@ var FormoAnalytics = /** @class */ (function () {
1492
1572
  return [2 /*return*/];
1493
1573
  }
1494
1574
  setTimeout(function () {
1575
+ // Drop in-flight page hits from an SDK instance that was torn down
1576
+ // between scheduling and firing (e.g. provider remount in React Strict
1577
+ // Mode / HMR). Otherwise the orphan instance would queue a page event
1578
+ // here with its stale, never-populated `currentAddress`.
1579
+ if (_this._pageHooksDisposed)
1580
+ return;
1495
1581
  (function () { return __awaiter(_this, void 0, void 0, function () {
1496
1582
  var e_7;
1497
1583
  return __generator(this, function (_a) {
@@ -1899,7 +1985,9 @@ var FormoAnalytics = /** @class */ (function () {
1899
1985
  });
1900
1986
  });
1901
1987
  };
1902
- FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params, response, chainId) {
1988
+ FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
1989
+ // Intentionally not read. Kept for positional call-site arity.
1990
+ _response, chainId) {
1903
1991
  var _a;
1904
1992
  var rawAddress = method === "personal_sign"
1905
1993
  ? params[1]
@@ -1908,45 +1996,63 @@ var FormoAnalytics = /** @class */ (function () {
1908
1996
  if (!validAddress) {
1909
1997
  throw new Error("Invalid address in signature payload: ".concat(rawAddress));
1910
1998
  }
1999
+ var effectiveChainId = (_a = chainId !== null && chainId !== void 0 ? chainId : this._evmChainId) !== null && _a !== void 0 ? _a : undefined;
2000
+ this.backfillActiveWallet(validAddress, effectiveChainId);
1911
2001
  var basePayload = {
1912
- chainId: (_a = chainId !== null && chainId !== void 0 ? chainId : this._evmChainId) !== null && _a !== void 0 ? _a : undefined,
2002
+ chainId: effectiveChainId,
1913
2003
  address: validAddress,
1914
2004
  };
1915
2005
  if (method === "personal_sign") {
1916
2006
  var message = Buffer.from(params[0].slice(2), "hex").toString("utf8");
1917
- return __assign(__assign(__assign({}, basePayload), { message: message }), (response ? { signatureHash: response } : {}));
2007
+ return __assign(__assign({}, basePayload), { message: message });
1918
2008
  }
1919
- return __assign(__assign(__assign({}, basePayload), { message: params[1] }), (response ? { signatureHash: response } : {}));
2009
+ // eth_signTypedData*: params[1] is the full EIP-712 struct.
2010
+ return __assign(__assign({}, basePayload), { message: params[1] });
1920
2011
  };
1921
2012
  FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider) {
1922
2013
  return __awaiter(this, void 0, void 0, function () {
1923
- var _a, data, from, to, value, validAddress, _b;
1924
- var _c;
1925
- return __generator(this, function (_d) {
1926
- switch (_d.label) {
2014
+ var _a, data, from, to, value, validAddress, chainId, _b;
2015
+ return __generator(this, function (_c) {
2016
+ switch (_c.label) {
1927
2017
  case 0:
1928
2018
  _a = params[0], data = _a.data, from = _a.from, to = _a.to, value = _a.value;
1929
2019
  validAddress = (0, address_1.validateAndChecksumAddress)(from);
1930
2020
  if (!validAddress) {
1931
2021
  throw new Error("Invalid address in transaction payload: ".concat(from));
1932
2022
  }
1933
- _c = {};
1934
2023
  _b = this._evmChainId;
1935
2024
  if (_b) return [3 /*break*/, 2];
1936
2025
  return [4 /*yield*/, this.getCurrentChainId(provider)];
1937
2026
  case 1:
1938
- _b = (_d.sent());
1939
- _d.label = 2;
1940
- case 2: return [2 /*return*/, (_c.chainId = _b,
1941
- _c.data = data,
1942
- _c.address = validAddress,
1943
- _c.to = to,
1944
- _c.value = value,
1945
- _c)];
2027
+ _b = (_c.sent());
2028
+ _c.label = 2;
2029
+ case 2:
2030
+ chainId = _b;
2031
+ this.backfillActiveWallet(validAddress, chainId);
2032
+ return [2 /*return*/, {
2033
+ chainId: chainId,
2034
+ data: data,
2035
+ address: validAddress,
2036
+ to: to,
2037
+ value: value,
2038
+ }];
1946
2039
  }
1947
2040
  });
1948
2041
  });
1949
2042
  };
2043
+ /**
2044
+ * Persist an EVM address discovered through autocapture (signature / transaction)
2045
+ * as the current EVM address when none is currently set. This lets subsequent
2046
+ * track()/page() calls carry the address even when the underlying wallet never
2047
+ * fires an EIP-1193 `accountsChanged` event (embedded wallets, smart accounts,
2048
+ * social-login wrappers). If `accountsChanged` later fires it overwrites this
2049
+ * value in the normal way; existing connections are never clobbered.
2050
+ */
2051
+ FormoAnalytics.prototype.backfillActiveWallet = function (address, chainId) {
2052
+ if (this._evmAddress)
2053
+ return;
2054
+ this.setChainState('evm', { address: address, chainId: chainId });
2055
+ };
1950
2056
  /**
1951
2057
  * Polls for transaction receipt and emits tx.status = CONFIRMED or REVERTED.
1952
2058
  */
@@ -2017,7 +2123,8 @@ var FormoAnalytics = /** @class */ (function () {
2017
2123
  }
2018
2124
  this._providerListenersMap.delete(provider);
2019
2125
  };
2020
- // Explicitly untrack a provider: remove listeners, clear wrapper flag and tracking
2126
+ // Explicitly untrack a provider: remove listeners, clear wrapper flag
2127
+ // and tracking
2021
2128
  FormoAnalytics.prototype.untrackProvider = function (provider) {
2022
2129
  try {
2023
2130
  this.removeProviderListeners(provider);
@@ -2135,6 +2242,42 @@ var FormoAnalytics = /** @class */ (function () {
2135
2242
  }
2136
2243
  this.syncDerivedState();
2137
2244
  };
2245
+ /**
2246
+ * Sync validated wallet/chain state into the SDK's central state
2247
+ * WITHOUT emitting an event.
2248
+ *
2249
+ * Integrations (e.g. the wagmi handler) must call this on every
2250
+ * connect / chain-change / disconnect — even when the corresponding
2251
+ * autocapture event is disabled. Otherwise `currentChainId` stays
2252
+ * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2253
+ * check (which keys off `currentChainId`, not the event payload) can
2254
+ * be bypassed, letting wallet activity on an excluded chain still be
2255
+ * collected.
2256
+ *
2257
+ * - valid `address` present → record per-chain + derived state
2258
+ * - `address` absent → clear chain state (disconnect)
2259
+ */
2260
+ FormoAnalytics.prototype.syncWalletState = function (params) {
2261
+ var chainId = params.chainId, address = params.address;
2262
+ if (!address) {
2263
+ if (chainId !== undefined && chainId !== null) {
2264
+ this.clearChainState(chainId);
2265
+ }
2266
+ else {
2267
+ this.clearChainState("evm");
2268
+ this.clearChainState("solana");
2269
+ }
2270
+ return;
2271
+ }
2272
+ if (chainId === null || chainId === undefined)
2273
+ return;
2274
+ var validAddress = (0, address_1.validateAddress)(address, chainId);
2275
+ if (!validAddress) {
2276
+ logger_1.logger.warn("syncWalletState: invalid address (\"".concat(address, "\") for chain ").concat(chainId));
2277
+ return;
2278
+ }
2279
+ this.setChainState(chainId, { address: validAddress });
2280
+ };
2138
2281
  /**
2139
2282
  * Synchronize currentAddress/currentChainId from the active namespace.
2140
2283
  * Last-connected-chain-wins: _activeNamespace takes precedence.
@@ -2146,6 +2289,7 @@ var FormoAnalytics = /** @class */ (function () {
2146
2289
  if (state.address || state.chainId) {
2147
2290
  this.currentAddress = state.address;
2148
2291
  this.currentChainId = state.chainId;
2292
+ this.persistActiveWallet();
2149
2293
  return;
2150
2294
  }
2151
2295
  }
@@ -2156,10 +2300,77 @@ var FormoAnalytics = /** @class */ (function () {
2156
2300
  this.currentAddress = otherState.address;
2157
2301
  this.currentChainId = otherState.chainId;
2158
2302
  this._activeNamespace = other;
2303
+ this.persistActiveWallet();
2159
2304
  return;
2160
2305
  }
2161
2306
  this.currentAddress = undefined;
2162
2307
  this.currentChainId = undefined;
2308
+ this.persistActiveWallet();
2309
+ };
2310
+ /**
2311
+ * Persist (or clear) the current wallet snapshot in a cookie so that the
2312
+ * SDK can repopulate `currentAddress`/`currentChainId` at init on the next
2313
+ * page load — closing the gap between page-show and wagmi/EIP-1193
2314
+ * reconnection during which track()/page() events would otherwise ship
2315
+ * with an empty address.
2316
+ */
2317
+ FormoAnalytics.prototype.persistActiveWallet = function () {
2318
+ try {
2319
+ // Never write an identity cookie for an opted-out user; ensure any
2320
+ // prior snapshot is removed instead.
2321
+ if (this.hasOptedOutTracking()) {
2322
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2323
+ return;
2324
+ }
2325
+ if (this.currentAddress) {
2326
+ var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
2327
+ var domain = (0, cookiePolicy_1.getIdentityCookieDomain)(this.crossSubdomainCookies);
2328
+ (0, storage_1.cookie)().set(constants_1.ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + constants_1.ACTIVE_WALLET_TTL_MS).toUTCString() }, (0, cookiePolicy_1.getIdentityCookieSecurity)()), (domain ? { domain: domain } : {})));
2329
+ }
2330
+ else {
2331
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2332
+ }
2333
+ }
2334
+ catch (err) {
2335
+ logger_1.logger.warn("Failed to persist current wallet snapshot", err);
2336
+ }
2337
+ };
2338
+ /**
2339
+ * Seed `currentAddress`/`currentChainId` from the persisted snapshot, if
2340
+ * any. Called once during construction before the first page hit fires.
2341
+ */
2342
+ FormoAnalytics.prototype.loadActiveWallet = function () {
2343
+ try {
2344
+ // Never restore wallet identity into memory for an opted-out user
2345
+ // (mirrors persistActiveWallet's guard); drop any stale snapshot.
2346
+ if (this.hasOptedOutTracking()) {
2347
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2348
+ return;
2349
+ }
2350
+ var raw = (0, storage_1.cookie)().get(constants_1.ACTIVE_WALLET_KEY);
2351
+ if (!raw)
2352
+ return;
2353
+ var parsed = JSON.parse(raw);
2354
+ if (!(parsed === null || parsed === void 0 ? void 0 : parsed.address))
2355
+ return;
2356
+ var namespace = (0, solana_1.isSolanaChainId)(parsed.chainId) ? "solana" : "evm";
2357
+ var validated = (0, address_1.validateAddress)(parsed.address, parsed.chainId);
2358
+ if (!validated) {
2359
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2360
+ return;
2361
+ }
2362
+ var ns = this._chainState[namespace];
2363
+ ns.address = validated;
2364
+ if (parsed.chainId !== undefined)
2365
+ ns.chainId = parsed.chainId;
2366
+ this._activeNamespace = namespace;
2367
+ this.currentAddress = validated;
2368
+ this.currentChainId = parsed.chainId;
2369
+ }
2370
+ catch (err) {
2371
+ logger_1.logger.warn("Failed to restore persisted wallet snapshot", err);
2372
+ (0, storage_1.cookie)().remove(constants_1.ACTIVE_WALLET_KEY);
2373
+ }
2163
2374
  };
2164
2375
  /**
2165
2376
  * Helper method to clear the active provider state
@@ -1,5 +1,14 @@
1
1
  export declare const SESSION_TRAFFIC_SOURCE_KEY = "traffic-source";
2
2
  export declare const SESSION_USER_ID_KEY = "user-id";
3
+ /**
4
+ * Persisted snapshot of the currently-active EVM/Solana wallet
5
+ * (address + chainId). Lets the SDK seed `currentAddress`/`currentChainId`
6
+ * at init so the first page hit after a reload carries the address, without
7
+ * having to wait for wagmi/EIP-1193 reconnection to fire.
8
+ */
9
+ export declare const ACTIVE_WALLET_KEY = "active-wallet";
10
+ /** TTL for the persisted active-wallet snapshot cookie. */
11
+ export declare const ACTIVE_WALLET_TTL_MS: number;
3
12
  export declare const LOCAL_ANONYMOUS_ID_KEY = "anonymous-id";
4
13
  export declare const CONSENT_OPT_OUT_KEY = "opt-out-tracking";
5
14
  export declare const DEFAULT_PROVIDER_ICON = "data:image/svg+xml;base64,";
@@ -1,10 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BLOCKED_ADDRESSES = exports.DEAD_ADDRESS = exports.ZERO_ADDRESS = exports.DEFAULT_PROVIDER_ICON = exports.CONSENT_OPT_OUT_KEY = exports.LOCAL_ANONYMOUS_ID_KEY = exports.SESSION_USER_ID_KEY = exports.SESSION_TRAFFIC_SOURCE_KEY = void 0;
3
+ exports.BLOCKED_ADDRESSES = exports.DEAD_ADDRESS = exports.ZERO_ADDRESS = exports.DEFAULT_PROVIDER_ICON = exports.CONSENT_OPT_OUT_KEY = exports.LOCAL_ANONYMOUS_ID_KEY = exports.ACTIVE_WALLET_TTL_MS = exports.ACTIVE_WALLET_KEY = exports.SESSION_USER_ID_KEY = exports.SESSION_TRAFFIC_SOURCE_KEY = void 0;
4
4
  exports.SESSION_TRAFFIC_SOURCE_KEY = "traffic-source";
5
5
  // SESSION_WALLET_DETECTED_KEY and SESSION_WALLET_IDENTIFIED_KEY
6
6
  // are now defined in src/session/index.ts
7
7
  exports.SESSION_USER_ID_KEY = "user-id";
8
+ /**
9
+ * Persisted snapshot of the currently-active EVM/Solana wallet
10
+ * (address + chainId). Lets the SDK seed `currentAddress`/`currentChainId`
11
+ * at init so the first page hit after a reload carries the address, without
12
+ * having to wait for wagmi/EIP-1193 reconnection to fire.
13
+ */
14
+ exports.ACTIVE_WALLET_KEY = "active-wallet";
15
+ /** TTL for the persisted active-wallet snapshot cookie. */
16
+ exports.ACTIVE_WALLET_TTL_MS = 24 * 60 * 60 * 1000;
8
17
  exports.LOCAL_ANONYMOUS_ID_KEY = "anonymous-id";
9
18
  // Consent management keys
10
19
  exports.CONSENT_OPT_OUT_KEY = "opt-out-tracking";