@formo/analytics 1.29.1 → 1.30.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 (50) hide show
  1. package/dist/cjs/src/FormoAnalytics.d.ts +20 -3
  2. package/dist/cjs/src/FormoAnalytics.js +102 -9
  3. package/dist/cjs/src/FormoAnalyticsProvider.d.ts +8 -2
  4. package/dist/cjs/src/core.d.ts +10 -0
  5. package/dist/cjs/src/core.js +36 -0
  6. package/dist/cjs/src/event/EventFactory.d.ts +1 -1
  7. package/dist/cjs/src/event/EventFactory.js +3 -3
  8. package/dist/cjs/src/event/EventManager.d.ts +2 -0
  9. package/dist/cjs/src/event/EventManager.js +4 -0
  10. package/dist/cjs/src/event/type.d.ts +1 -0
  11. package/dist/cjs/src/event/utils.js +1 -1
  12. package/dist/cjs/src/index.d.ts +1 -8
  13. package/dist/cjs/src/index.js +1 -12
  14. package/dist/cjs/src/queue/EventQueue.d.ts +8 -0
  15. package/dist/cjs/src/queue/EventQueue.js +52 -6
  16. package/dist/cjs/src/queue/type.d.ts +1 -0
  17. package/dist/cjs/src/session/index.js +16 -8
  18. package/dist/cjs/src/storage/cookiePolicy.d.ts +16 -0
  19. package/dist/cjs/src/storage/cookiePolicy.js +19 -0
  20. package/dist/cjs/src/types/base.d.ts +0 -8
  21. package/dist/cjs/src/types/events.d.ts +0 -1
  22. package/dist/cjs/src/version.d.ts +1 -1
  23. package/dist/cjs/src/version.js +1 -1
  24. package/dist/cjs/src/wagmi/WagmiEventHandler.js +38 -15
  25. package/dist/esm/src/FormoAnalytics.d.ts +20 -3
  26. package/dist/esm/src/FormoAnalytics.js +105 -12
  27. package/dist/esm/src/FormoAnalyticsProvider.d.ts +8 -2
  28. package/dist/esm/src/core.d.ts +10 -0
  29. package/dist/esm/src/core.js +13 -0
  30. package/dist/esm/src/event/EventFactory.d.ts +1 -1
  31. package/dist/esm/src/event/EventFactory.js +3 -3
  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/index.d.ts +1 -8
  37. package/dist/esm/src/index.js +1 -6
  38. package/dist/esm/src/queue/EventQueue.d.ts +8 -0
  39. package/dist/esm/src/queue/EventQueue.js +52 -6
  40. package/dist/esm/src/queue/type.d.ts +1 -0
  41. package/dist/esm/src/session/index.js +16 -8
  42. package/dist/esm/src/storage/cookiePolicy.d.ts +16 -0
  43. package/dist/esm/src/storage/cookiePolicy.js +18 -0
  44. package/dist/esm/src/types/base.d.ts +0 -8
  45. package/dist/esm/src/types/events.d.ts +0 -1
  46. package/dist/esm/src/version.d.ts +1 -1
  47. package/dist/esm/src/version.js +1 -1
  48. package/dist/esm/src/wagmi/WagmiEventHandler.js +38 -15
  49. package/dist/index.umd.min.js +1 -1
  50. package/package.json +18 -18
@@ -5,9 +5,21 @@
5
5
  * Handles tracking of detected wallets and identified wallet-address pairs
6
6
  * using cookies to maintain state across page loads within a session.
7
7
  */
8
+ var __assign = (this && this.__assign) || function () {
9
+ __assign = Object.assign || function(t) {
10
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
11
+ s = arguments[i];
12
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
13
+ t[p] = s[p];
14
+ }
15
+ return t;
16
+ };
17
+ return __assign.apply(this, arguments);
18
+ };
8
19
  Object.defineProperty(exports, "__esModule", { value: true });
9
20
  exports.FormoAnalyticsSession = exports.SESSION_WALLET_IDENTIFIED_KEY = exports.SESSION_WALLET_DETECTED_KEY = void 0;
10
21
  var storage_1 = require("../storage");
22
+ var cookiePolicy_1 = require("../storage/cookiePolicy");
11
23
  var logger_1 = require("../logger");
12
24
  /**
13
25
  * Cookie keys for session tracking
@@ -66,11 +78,9 @@ var FormoAnalyticsSession = /** @class */ (function () {
66
78
  if (rdnses.length > MAX_SESSION_ENTRIES) {
67
79
  rdnses.splice(0, rdnses.length - MAX_SESSION_ENTRIES);
68
80
  }
69
- (0, storage_1.cookie)().set(exports.SESSION_WALLET_DETECTED_KEY, rdnses.join(","), {
81
+ (0, storage_1.cookie)().set(exports.SESSION_WALLET_DETECTED_KEY, rdnses.join(","), __assign({
70
82
  // Expires by the end of the day
71
- expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
72
- path: "/",
73
- });
83
+ expires: new Date(Date.now() + 86400 * 1000).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
74
84
  }
75
85
  };
76
86
  /**
@@ -110,11 +120,9 @@ var FormoAnalyticsSession = /** @class */ (function () {
110
120
  identifiedWallets.splice(0, identifiedWallets.length - MAX_SESSION_ENTRIES);
111
121
  }
112
122
  var newValue = identifiedWallets.join(",");
113
- (0, storage_1.cookie)().set(exports.SESSION_WALLET_IDENTIFIED_KEY, newValue, {
123
+ (0, storage_1.cookie)().set(exports.SESSION_WALLET_IDENTIFIED_KEY, newValue, __assign({
114
124
  // Expires by the end of the day
115
- expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
116
- path: "/",
117
- });
125
+ expires: new Date(Date.now() + 86400 * 1000).toUTCString(), path: "/" }, (0, cookiePolicy_1.getIdentityCookieSecurity)()));
118
126
  logger_1.logger.debug("Session: Marked wallet as identified", {
119
127
  identifiedKey: identifiedKey,
120
128
  hasRdns: !!rdns,
@@ -7,4 +7,20 @@
7
7
  * @param crossSubdomain Whether cookies should be shared across subdomains.
8
8
  */
9
9
  export declare function getIdentityCookieDomain(crossSubdomain?: boolean): string;
10
+ /**
11
+ * Security attributes for identity/session cookies (user-id,
12
+ * active-wallet, wallet-detected/identified).
13
+ *
14
+ * - `sameSite: "lax"` — first-party analytics identity. Lax keeps the
15
+ * cookie on top-level navigations (so attribution survives a click-in
16
+ * from another site) while blocking it on cross-site subrequests.
17
+ * Strict would silently drop identity on inbound navigation.
18
+ * - `secure` — only on HTTPS. Setting Secure over plain HTTP (local dev)
19
+ * makes the browser reject the cookie outright, so it must be
20
+ * conditional.
21
+ */
22
+ export declare function getIdentityCookieSecurity(): {
23
+ sameSite: "lax";
24
+ secure: boolean;
25
+ };
10
26
  //# sourceMappingURL=cookiePolicy.d.ts.map
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getIdentityCookieDomain = getIdentityCookieDomain;
4
+ exports.getIdentityCookieSecurity = getIdentityCookieSecurity;
4
5
  /**
5
6
  * Cookie domain policy — centralizes the decision of whether identity
6
7
  * cookies should be host-scoped or apex-scoped.
@@ -24,4 +25,22 @@ function getIdentityCookieDomain(crossSubdomain) {
24
25
  var domain = (0, domain_1.getApexDomain)();
25
26
  return domain ? ".".concat(domain) : "";
26
27
  }
28
+ /**
29
+ * Security attributes for identity/session cookies (user-id,
30
+ * active-wallet, wallet-detected/identified).
31
+ *
32
+ * - `sameSite: "lax"` — first-party analytics identity. Lax keeps the
33
+ * cookie on top-level navigations (so attribution survives a click-in
34
+ * from another site) while blocking it on cross-site subrequests.
35
+ * Strict would silently drop identity on inbound navigation.
36
+ * - `secure` — only on HTTPS. Setting Secure over plain HTTP (local dev)
37
+ * makes the browser reject the cookie outright, so it must be
38
+ * conditional.
39
+ */
40
+ function getIdentityCookieSecurity() {
41
+ var _a;
42
+ var isHttps = typeof window !== "undefined" &&
43
+ ((_a = window.location) === null || _a === void 0 ? void 0 : _a.protocol) === "https:";
44
+ return { sameSite: "lax", secure: isHttps };
45
+ }
27
46
  //# sourceMappingURL=cookiePolicy.js.map
@@ -1,7 +1,6 @@
1
1
  import { LogLevel } from "../logger";
2
2
  import { IFormoEventContext, IFormoEventProperties, SignatureStatus, TransactionStatus } from "./events";
3
3
  import { EIP1193Provider } from "./provider";
4
- import { ReactNode } from "react";
5
4
  import { SolanaOptions } from "../solana/types";
6
5
  export type Nullable<T> = T | null;
7
6
  export type ChainID = number;
@@ -40,7 +39,6 @@ export interface IFormoAnalytics {
40
39
  chainId?: ChainID;
41
40
  address: Address;
42
41
  message: string;
43
- signatureHash?: string;
44
42
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
45
43
  transaction(params: {
46
44
  status: TransactionStatus;
@@ -222,10 +220,4 @@ export interface Options {
222
220
  errorHandler?: (err: Error) => void;
223
221
  ready?: (formo: IFormoAnalytics) => void;
224
222
  }
225
- export interface FormoAnalyticsProviderProps {
226
- writeKey: string;
227
- options?: Options;
228
- disabled?: boolean;
229
- children: ReactNode;
230
- }
231
223
  //# sourceMappingURL=base.d.ts.map
@@ -76,7 +76,6 @@ export interface SignatureAPIEvent {
76
76
  chainId: ChainID;
77
77
  address: Address;
78
78
  message: string;
79
- signatureHash?: string;
80
79
  }
81
80
  export interface ConnectAPIEvent {
82
81
  type: "connect";
@@ -1,2 +1,2 @@
1
- export declare const version = "1.29.1";
1
+ export declare const version = "1.30.1";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.version = void 0;
4
4
  // This file is auto-generated by scripts/update-version.js during npm version
5
5
  // Do not edit manually - it will be overwritten
6
- exports.version = '1.29.1';
6
+ exports.version = '1.30.1';
7
7
  //# sourceMappingURL=version.js.map
@@ -173,6 +173,11 @@ var WagmiEventHandler = /** @class */ (function () {
173
173
  chainId: chainId,
174
174
  });
175
175
  if (!(status === "disconnected" && prevStatus === "connected")) return [3 /*break*/, 4];
176
+ // Clear central chain state regardless of autocapture so a later
177
+ // event can't carry a stale excluded/!excluded chainId.
178
+ this.formo.syncWalletState({
179
+ chainId: this.trackingState.lastChainId,
180
+ });
176
181
  if (!this.formo.isAutocaptureEnabled("disconnect")) return [3 /*break*/, 3];
177
182
  return [4 /*yield*/, this.formo.disconnect({
178
183
  chainId: this.trackingState.lastChainId,
@@ -190,6 +195,9 @@ var WagmiEventHandler = /** @class */ (function () {
190
195
  if (!(address && chainId !== undefined)) return [3 /*break*/, 6];
191
196
  this.trackingState.lastAddress = address;
192
197
  this.trackingState.lastChainId = chainId;
198
+ // Sync central state unconditionally so tracking.excludeChains
199
+ // is enforced even when connect autocapture is disabled.
200
+ this.formo.syncWalletState({ chainId: chainId, address: address });
193
201
  if (!this.formo.isAutocaptureEnabled("connect")) return [3 /*break*/, 6];
194
202
  connectorName = this.getConnectorName(state);
195
203
  return [4 /*yield*/, this.formo.connect({ chainId: chainId, address: address }, __assign({}, (connectorName && { providerName: connectorName })))];
@@ -238,6 +246,9 @@ var WagmiEventHandler = /** @class */ (function () {
238
246
  address: address,
239
247
  });
240
248
  this.trackingState.lastChainId = chainId;
249
+ // Sync central state unconditionally so a chain switch to an
250
+ // excluded chain is honored even when chain autocapture is disabled.
251
+ this.formo.syncWalletState({ chainId: chainId, address: address });
241
252
  if (!this.formo.isAutocaptureEnabled("chain")) return [3 /*break*/, 4];
242
253
  _a.label = 1;
243
254
  case 1:
@@ -359,14 +370,21 @@ var WagmiEventHandler = /** @class */ (function () {
359
370
  logger_1.logger.warn("WagmiEventHandler: Transaction receipt query but no hash found");
360
371
  return;
361
372
  }
362
- // Retrieve stored transaction details from BROADCASTED event
363
- // Normalize hash to lowercase for consistent lookup
373
+ // Retrieve stored transaction details from the BROADCASTED event.
374
+ // Normalize hash to lowercase for consistent lookup.
364
375
  var normalizedHash = transactionHash.toLowerCase();
365
376
  var pendingTx = this.pendingTransactions.get(normalizedHash);
366
- // Use the original sender address from BROADCASTED event if available,
367
- // otherwise fall back to current connected address.
368
- // This handles wallet switches between broadcast and confirmation.
369
- var address = (pendingTx === null || pendingTx === void 0 ? void 0 : pendingTx.address) || this.trackingState.lastAddress;
377
+ // Only emit receipt-derived events for a hash we actually observed
378
+ // being broadcast through this handler. The QueryClient is supplied
379
+ // by the host app and its cache can be written by app code or other
380
+ // deps, so without this gate a forged waitForTransactionReceipt
381
+ // entry could fabricate a confirmed/reverted transaction for an
382
+ // arbitrary hash. No pendingTx → not our broadcast → ignore.
383
+ if (!pendingTx) {
384
+ logger_1.logger.debug("WagmiEventHandler: Receipt for unobserved tx hash; ignoring", { transactionHash: transactionHash });
385
+ return;
386
+ }
387
+ var address = pendingTx.address;
370
388
  if (!address) {
371
389
  logger_1.logger.warn("WagmiEventHandler: Transaction receipt query but no address available");
372
390
  return;
@@ -374,9 +392,13 @@ var WagmiEventHandler = /** @class */ (function () {
374
392
  try {
375
393
  // Extract receipt data
376
394
  var receipt = state.data;
377
- // Determine transaction status from receipt
378
- // receipt.status is 'success' or 'reverted' in viem
379
- var txStatus = (receipt === null || receipt === void 0 ? void 0 : receipt.status) === "reverted"
395
+ // Only act on an explicit on-chain outcome. A missing/unknown
396
+ // status must NOT be treated as a confirmation.
397
+ if ((receipt === null || receipt === void 0 ? void 0 : receipt.status) !== "success" && (receipt === null || receipt === void 0 ? void 0 : receipt.status) !== "reverted") {
398
+ logger_1.logger.debug("WagmiEventHandler: Receipt without explicit success/reverted status; ignoring", { transactionHash: transactionHash, status: receipt === null || receipt === void 0 ? void 0 : receipt.status });
399
+ return;
400
+ }
401
+ var txStatus = receipt.status === "reverted"
380
402
  ? events_1.TransactionStatus.REVERTED
381
403
  : events_1.TransactionStatus.CONFIRMED;
382
404
  logger_1.logger.info("WagmiEventHandler: Tracking transaction confirmation", {
@@ -455,15 +477,13 @@ var WagmiEventHandler = /** @class */ (function () {
455
477
  return;
456
478
  }
457
479
  try {
458
- // Map Wagmi mutation status to Formo signature status
480
+ // Map Wagmi mutation status to Formo signature status.
459
481
  var status_1;
460
- var signatureHash = void 0;
461
482
  if (state.status === "pending") {
462
483
  status_1 = events_1.SignatureStatus.REQUESTED;
463
484
  }
464
485
  else if (state.status === "success") {
465
486
  status_1 = events_1.SignatureStatus.CONFIRMED;
466
- signatureHash = state.data;
467
487
  }
468
488
  else if (state.status === "error") {
469
489
  status_1 = events_1.SignatureStatus.REJECTED;
@@ -471,13 +491,11 @@ var WagmiEventHandler = /** @class */ (function () {
471
491
  else {
472
492
  return; // Ignore idle state
473
493
  }
474
- // Extract message from variables
475
494
  var message = void 0;
476
495
  if (mutationType === "signMessage") {
477
496
  message = variables.message || "";
478
497
  }
479
498
  else {
480
- // For signTypedData, stringify the typed data
481
499
  message = JSON.stringify(variables.message || variables.types || {});
482
500
  }
483
501
  logger_1.logger.info("WagmiEventHandler: Tracking signature event", {
@@ -486,7 +504,12 @@ var WagmiEventHandler = /** @class */ (function () {
486
504
  address: address,
487
505
  chainId: chainId,
488
506
  });
489
- this.formo.signature(__assign({ status: status_1, chainId: chainId, address: address, message: message }, (signatureHash && { signatureHash: signatureHash })));
507
+ this.formo.signature({
508
+ status: status_1,
509
+ chainId: chainId,
510
+ address: address,
511
+ message: message,
512
+ });
490
513
  }
491
514
  catch (error) {
492
515
  logger_1.logger.error("WagmiEventHandler: Error handling signature mutation:", error);
@@ -136,18 +136,16 @@ export declare class FormoAnalytics implements IFormoAnalytics {
136
136
  * @param {ChainID} params.chainId
137
137
  * @param {Address} params.address
138
138
  * @param {string} params.message
139
- * @param {string} params.signatureHash - only provided if status is confirmed
140
139
  * @param {IFormoEventProperties} properties
141
140
  * @param {IFormoEventContext} context
142
141
  * @param {(...args: unknown[]) => void} callback
143
142
  * @returns {Promise<void>}
144
143
  */
145
- signature({ status, chainId, address, message, signatureHash, }: {
144
+ signature({ status, chainId, address, message, }: {
146
145
  status: SignatureStatus;
147
146
  chainId?: ChainID;
148
147
  address: Address;
149
148
  message: string;
150
- signatureHash?: string;
151
149
  }, properties?: IFormoEventProperties, context?: IFormoEventContext, callback?: (...args: unknown[]) => void): Promise<void>;
152
150
  /**
153
151
  * Emits a transaction event.
@@ -381,6 +379,25 @@ export declare class FormoAnalytics implements IFormoAnalytics {
381
379
  * Clear per-chain state for a given namespace (or chainId) and sync derived state.
382
380
  */
383
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;
384
401
  /**
385
402
  * Synchronize currentAddress/currentChainId from the active namespace.
386
403
  * Last-connected-chain-wins: _activeNamespace takes precedence.
@@ -55,9 +55,9 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
55
55
  return to.concat(ar || Array.prototype.slice.call(from));
56
56
  };
57
57
  import { createStore } from "mipd";
58
- import { EVENTS_API_HOST, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_USER_ID_KEY, ACTIVE_WALLET_KEY, ACTIVE_WALLET_TTL_MS, CONSENT_OPT_OUT_KEY, } from "./constants";
59
- import { cookie, initStorageManager } from "./storage";
60
- import { getIdentityCookieDomain } from "./storage/cookiePolicy";
58
+ import { EVENTS_API_HOST, EventType, LOCAL_ANONYMOUS_ID_KEY, SESSION_USER_ID_KEY, SESSION_TRAFFIC_SOURCE_KEY, ACTIVE_WALLET_KEY, ACTIVE_WALLET_TTL_MS, CONSENT_OPT_OUT_KEY, } from "./constants";
59
+ import { cookie, session, initStorageManager } from "./storage";
60
+ import { getIdentityCookieDomain, getIdentityCookieSecurity, } from "./storage/cookiePolicy";
61
61
  import { EventManager } from "./event";
62
62
  import { EventQueue } from "./queue";
63
63
  import { logger, Logger } from "./logger";
@@ -82,6 +82,7 @@ var PROVIDER_SWITCH_REASONS = {
82
82
  var FormoAnalytics = /** @class */ (function () {
83
83
  function FormoAnalytics(writeKey, options) {
84
84
  if (options === void 0) { options = {}; }
85
+ var _this = this;
85
86
  var _a, _b, _c;
86
87
  this.writeKey = writeKey;
87
88
  this.options = options;
@@ -157,6 +158,10 @@ var FormoAnalytics = /** @class */ (function () {
157
158
  maxQueueSize: options.maxQueueSize,
158
159
  flushInterval: options.flushInterval,
159
160
  errorHandler: options.errorHandler,
161
+ // Hard consent gate at the queue boundary: nothing buffered is
162
+ // ever sent once the user has opted out, even via a timer or
163
+ // pagehide flush scheduled before opt-out.
164
+ canSend: function () { return !_this.hasOptedOutTracking(); },
160
165
  }), options);
161
166
  // Check consent status on initialization
162
167
  if (this.hasOptedOutTracking()) {
@@ -294,11 +299,25 @@ var FormoAnalytics = /** @class */ (function () {
294
299
  */
295
300
  FormoAnalytics.prototype.reset = function () {
296
301
  this.currentUserId = undefined;
302
+ // Clear in-memory wallet identity too. Without this, a logout/reset
303
+ // (also triggered by optOutTracking) still leaks the previous wallet
304
+ // address on subsequent track()/page() events for the rest of the
305
+ // page lifetime, because they fall back to currentAddress. Keep the
306
+ // EVM provider reference so tracking can resume on the next connect.
307
+ this.currentAddress = undefined;
308
+ this.currentChainId = undefined;
309
+ var evmProvider = this._chainState.evm.provider;
310
+ this._chainState = { evm: { provider: evmProvider }, solana: {} };
311
+ this._activeNamespace = undefined;
297
312
  cookie().remove(LOCAL_ANONYMOUS_ID_KEY);
298
313
  cookie().remove(SESSION_USER_ID_KEY);
299
314
  cookie().remove(SESSION_WALLET_DETECTED_KEY);
300
315
  cookie().remove(SESSION_WALLET_IDENTIFIED_KEY);
301
316
  cookie().remove(ACTIVE_WALLET_KEY);
317
+ // Stored traffic-source attribution (referrer/UTM) is tracking data;
318
+ // clear it too so reset()/optOutTracking() don't leave it to be
319
+ // re-attached to the next session's events.
320
+ session().remove(SESSION_TRAFFIC_SOURCE_KEY);
302
321
  };
303
322
  /**
304
323
  * Clean up resources and event listeners
@@ -307,6 +326,8 @@ var FormoAnalytics = /** @class */ (function () {
307
326
  */
308
327
  FormoAnalytics.prototype.cleanup = function () {
309
328
  logger.debug("FormoAnalytics: Cleaning up resources");
329
+ // Drop buffered events so a torn-down instance can't flush later.
330
+ this.eventManager.clear();
310
331
  // Clean up Wagmi handler if present
311
332
  if (this.wagmiHandler) {
312
333
  this.wagmiHandler.cleanup();
@@ -470,7 +491,6 @@ var FormoAnalytics = /** @class */ (function () {
470
491
  * @param {ChainID} params.chainId
471
492
  * @param {Address} params.address
472
493
  * @param {string} params.message
473
- * @param {string} params.signatureHash - only provided if status is confirmed
474
494
  * @param {IFormoEventProperties} properties
475
495
  * @param {IFormoEventContext} context
476
496
  * @param {(...args: unknown[]) => void} callback
@@ -478,10 +498,15 @@ var FormoAnalytics = /** @class */ (function () {
478
498
  */
479
499
  FormoAnalytics.prototype.signature = function (_a, properties_1, context_1, callback_1) {
480
500
  return __awaiter(this, arguments, void 0, function (_b, properties, context, callback) {
481
- var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message, signatureHash = _b.signatureHash;
501
+ var status = _b.status, chainId = _b.chainId, address = _b.address, message = _b.message;
482
502
  return __generator(this, function (_c) {
483
503
  switch (_c.label) {
484
- case 0: return [4 /*yield*/, this.trackEvent(EventType.SIGNATURE, __assign({ status: status, chainId: chainId, address: address, message: message }, (signatureHash && { signatureHash: signatureHash })), properties, context, callback)];
504
+ case 0: return [4 /*yield*/, this.trackEvent(EventType.SIGNATURE, {
505
+ status: status,
506
+ chainId: chainId,
507
+ address: address,
508
+ message: message,
509
+ }, properties, context, callback)];
485
510
  case 1:
486
511
  _c.sent();
487
512
  return [2 /*return*/];
@@ -552,6 +577,13 @@ var FormoAnalytics = /** @class */ (function () {
552
577
  switch (_d.label) {
553
578
  case 0:
554
579
  _d.trys.push([0, 12, , 13]);
580
+ // identify() writes the user-id cookie and marks wallet
581
+ // identification before trackEvent's consent check — gate the
582
+ // whole method so an opted-out user gets no identity persistence.
583
+ if (this.hasOptedOutTracking()) {
584
+ logger.info("identify() skipped: user has opted out of tracking");
585
+ return [2 /*return*/];
586
+ }
555
587
  if (!!params) return [3 /*break*/, 10];
556
588
  // If no params provided, auto-identify
557
589
  logger.info("Auto-identifying with providers:", this._providers.map(function (p) { return p.info.name; }));
@@ -627,7 +659,7 @@ var FormoAnalytics = /** @class */ (function () {
627
659
  if (userId) {
628
660
  this.currentUserId = userId;
629
661
  domain = getIdentityCookieDomain(this.crossSubdomainCookies);
630
- cookie().set(SESSION_USER_ID_KEY, userId, __assign({ path: "/" }, (domain ? { domain: domain } : {})));
662
+ cookie().set(SESSION_USER_ID_KEY, userId, __assign(__assign({ path: "/" }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
631
663
  }
632
664
  isAlreadyIdentified = this.session.isWalletIdentified(validAddress, rdns || "");
633
665
  logger.debug("Identify: Checking deduplication", {
@@ -676,6 +708,12 @@ var FormoAnalytics = /** @class */ (function () {
676
708
  return __generator(this, function (_c) {
677
709
  switch (_c.label) {
678
710
  case 0:
711
+ // detect() marks wallet detection (a cookie write) before
712
+ // trackEvent's consent check — gate it on opt-out.
713
+ if (this.hasOptedOutTracking()) {
714
+ logger.info("detect() skipped: user has opted out of tracking");
715
+ return [2 /*return*/];
716
+ }
679
717
  if (this.session.isWalletDetected(rdns))
680
718
  return [2 /*return*/, logger.warn("Detect: Wallet ".concat(providerName, " already detected in this session"))];
681
719
  this.session.markWalletDetected(rdns);
@@ -722,6 +760,9 @@ var FormoAnalytics = /** @class */ (function () {
722
760
  // Set opt-out flag in persistent storage using direct cookie access
723
761
  // This must be done before switching storage to ensure persistence
724
762
  setConsentFlag(this.writeKey, CONSENT_OPT_OUT_KEY, "true");
763
+ // Drop anything already buffered so a pending timer/pagehide flush
764
+ // cannot ship events after consent withdrawal.
765
+ this.eventManager.clear();
725
766
  this.reset();
726
767
  logger.info("Successfully opted out of tracking");
727
768
  };
@@ -1941,7 +1982,9 @@ var FormoAnalytics = /** @class */ (function () {
1941
1982
  });
1942
1983
  });
1943
1984
  };
1944
- FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params, response, chainId) {
1985
+ FormoAnalytics.prototype.buildSignatureEventPayload = function (method, params,
1986
+ // Intentionally not read. Kept for positional call-site arity.
1987
+ _response, chainId) {
1945
1988
  var _a;
1946
1989
  var rawAddress = method === "personal_sign"
1947
1990
  ? params[1]
@@ -1958,9 +2001,10 @@ var FormoAnalytics = /** @class */ (function () {
1958
2001
  };
1959
2002
  if (method === "personal_sign") {
1960
2003
  var message = Buffer.from(params[0].slice(2), "hex").toString("utf8");
1961
- return __assign(__assign(__assign({}, basePayload), { message: message }), (response ? { signatureHash: response } : {}));
2004
+ return __assign(__assign({}, basePayload), { message: message });
1962
2005
  }
1963
- return __assign(__assign(__assign({}, basePayload), { message: params[1] }), (response ? { signatureHash: response } : {}));
2006
+ // eth_signTypedData*: params[1] is the full EIP-712 struct.
2007
+ return __assign(__assign({}, basePayload), { message: params[1] });
1964
2008
  };
1965
2009
  FormoAnalytics.prototype.buildTransactionEventPayload = function (params, provider) {
1966
2010
  return __awaiter(this, void 0, void 0, function () {
@@ -2076,7 +2120,8 @@ var FormoAnalytics = /** @class */ (function () {
2076
2120
  }
2077
2121
  this._providerListenersMap.delete(provider);
2078
2122
  };
2079
- // Explicitly untrack a provider: remove listeners, clear wrapper flag and tracking
2123
+ // Explicitly untrack a provider: remove listeners, clear wrapper flag
2124
+ // and tracking
2080
2125
  FormoAnalytics.prototype.untrackProvider = function (provider) {
2081
2126
  try {
2082
2127
  this.removeProviderListeners(provider);
@@ -2194,6 +2239,42 @@ var FormoAnalytics = /** @class */ (function () {
2194
2239
  }
2195
2240
  this.syncDerivedState();
2196
2241
  };
2242
+ /**
2243
+ * Sync validated wallet/chain state into the SDK's central state
2244
+ * WITHOUT emitting an event.
2245
+ *
2246
+ * Integrations (e.g. the wagmi handler) must call this on every
2247
+ * connect / chain-change / disconnect — even when the corresponding
2248
+ * autocapture event is disabled. Otherwise `currentChainId` stays
2249
+ * stale/undefined and `shouldTrack()`'s `tracking.excludeChains`
2250
+ * check (which keys off `currentChainId`, not the event payload) can
2251
+ * be bypassed, letting wallet activity on an excluded chain still be
2252
+ * collected.
2253
+ *
2254
+ * - valid `address` present → record per-chain + derived state
2255
+ * - `address` absent → clear chain state (disconnect)
2256
+ */
2257
+ FormoAnalytics.prototype.syncWalletState = function (params) {
2258
+ var chainId = params.chainId, address = params.address;
2259
+ if (!address) {
2260
+ if (chainId !== undefined && chainId !== null) {
2261
+ this.clearChainState(chainId);
2262
+ }
2263
+ else {
2264
+ this.clearChainState("evm");
2265
+ this.clearChainState("solana");
2266
+ }
2267
+ return;
2268
+ }
2269
+ if (chainId === null || chainId === undefined)
2270
+ return;
2271
+ var validAddress = validateAddress(address, chainId);
2272
+ if (!validAddress) {
2273
+ logger.warn("syncWalletState: invalid address (\"".concat(address, "\") for chain ").concat(chainId));
2274
+ return;
2275
+ }
2276
+ this.setChainState(chainId, { address: validAddress });
2277
+ };
2197
2278
  /**
2198
2279
  * Synchronize currentAddress/currentChainId from the active namespace.
2199
2280
  * Last-connected-chain-wins: _activeNamespace takes precedence.
@@ -2232,10 +2313,16 @@ var FormoAnalytics = /** @class */ (function () {
2232
2313
  */
2233
2314
  FormoAnalytics.prototype.persistActiveWallet = function () {
2234
2315
  try {
2316
+ // Never write an identity cookie for an opted-out user; ensure any
2317
+ // prior snapshot is removed instead.
2318
+ if (this.hasOptedOutTracking()) {
2319
+ cookie().remove(ACTIVE_WALLET_KEY);
2320
+ return;
2321
+ }
2235
2322
  if (this.currentAddress) {
2236
2323
  var value = JSON.stringify(__assign({ address: this.currentAddress }, (this.currentChainId !== undefined && { chainId: this.currentChainId })));
2237
2324
  var domain = getIdentityCookieDomain(this.crossSubdomainCookies);
2238
- cookie().set(ACTIVE_WALLET_KEY, value, __assign({ path: "/", expires: new Date(Date.now() + ACTIVE_WALLET_TTL_MS).toUTCString() }, (domain ? { domain: domain } : {})));
2325
+ cookie().set(ACTIVE_WALLET_KEY, value, __assign(__assign({ path: "/", expires: new Date(Date.now() + ACTIVE_WALLET_TTL_MS).toUTCString() }, getIdentityCookieSecurity()), (domain ? { domain: domain } : {})));
2239
2326
  }
2240
2327
  else {
2241
2328
  cookie().remove(ACTIVE_WALLET_KEY);
@@ -2251,6 +2338,12 @@ var FormoAnalytics = /** @class */ (function () {
2251
2338
  */
2252
2339
  FormoAnalytics.prototype.loadActiveWallet = function () {
2253
2340
  try {
2341
+ // Never restore wallet identity into memory for an opted-out user
2342
+ // (mirrors persistActiveWallet's guard); drop any stale snapshot.
2343
+ if (this.hasOptedOutTracking()) {
2344
+ cookie().remove(ACTIVE_WALLET_KEY);
2345
+ return;
2346
+ }
2254
2347
  var raw = cookie().get(ACTIVE_WALLET_KEY);
2255
2348
  if (!raw)
2256
2349
  return;
@@ -1,5 +1,11 @@
1
- import { FC } from "react";
2
- import { FormoAnalyticsProviderProps, IFormoAnalytics } from "./types";
1
+ import { FC, ReactNode } from "react";
2
+ import { IFormoAnalytics, Options } from "./types";
3
+ export interface FormoAnalyticsProviderProps {
4
+ writeKey: string;
5
+ options?: Options;
6
+ disabled?: boolean;
7
+ children: ReactNode;
8
+ }
3
9
  export declare const FormoAnalyticsContext: import("react").Context<IFormoAnalytics>;
4
10
  export declare const FormoAnalyticsProvider: FC<FormoAnalyticsProviderProps>;
5
11
  export declare const useFormo: () => IFormoAnalytics;
@@ -0,0 +1,10 @@
1
+ export * from "./FormoAnalytics";
2
+ export * from "./types";
3
+ export { formofy } from "./initialization";
4
+ export { parsePrivyProperties } from "./privy";
5
+ export type { PrivyUser, PrivyLinkedAccount, PrivyAccountType, PrivyProfileProperties, PrivyWalletInfo, } from "./privy";
6
+ export { SolanaManager } from "./solana";
7
+ export { SOLANA_CHAIN_IDS, DEFAULT_SOLANA_CHAIN_ID, isSolanaChainId, } from "./solana";
8
+ export type { SolanaOptions, SolanaCluster } from "./solana";
9
+ export type { SolanaClientStore, SolanaClientState } from "./solana";
10
+ //# sourceMappingURL=core.d.ts.map
@@ -0,0 +1,13 @@
1
+ // React-free entry point.
2
+ // Use this from non-React frameworks (Angular, Vue, Svelte, vanilla JS) to
3
+ // avoid pulling the React provider into the dependency graph.
4
+ //
5
+ // Equivalent to the package root entry minus FormoAnalyticsProvider/useFormo
6
+ // and the `window.formofy = formofy` side effect.
7
+ export * from "./FormoAnalytics";
8
+ export * from "./types";
9
+ export { formofy } from "./initialization";
10
+ export { parsePrivyProperties } from "./privy";
11
+ export { SolanaManager } from "./solana";
12
+ export { SOLANA_CHAIN_IDS, DEFAULT_SOLANA_CHAIN_ID, isSolanaChainId, } from "./solana";
13
+ //# sourceMappingURL=core.js.map
@@ -35,7 +35,7 @@ declare class EventFactory implements IEventFactory {
35
35
  generateConnectEvent(chainId: ChainID, address: Address, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
36
36
  generateDisconnectEvent(chainId?: ChainID, address?: Address, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
37
37
  generateChainChangedEvent(chainId: ChainID, address: Address, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
38
- generateSignatureEvent(status: SignatureStatus, chainId: ChainID, address: Address, message: string, signatureHash?: string, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
38
+ generateSignatureEvent(status: SignatureStatus, chainId: ChainID, address: Address, message: string, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
39
39
  generateTransactionEvent(status: TransactionStatus, chainId: ChainID, address: Address, data?: string, to?: string, value?: string, transactionHash?: string, function_name?: string, function_args?: Record<string, unknown>, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
40
40
  generateTrackEvent(event: string, properties?: IFormoEventProperties, context?: IFormoEventContext): Promise<IFormoEvent>;
41
41
  create(event: APIEvent, address?: Address, userId?: string): Promise<IFormoEvent>;
@@ -413,12 +413,12 @@ var EventFactory = /** @class */ (function () {
413
413
  });
414
414
  });
415
415
  };
416
- EventFactory.prototype.generateSignatureEvent = function (status, chainId, address, message, signatureHash, properties, context) {
416
+ EventFactory.prototype.generateSignatureEvent = function (status, chainId, address, message, properties, context) {
417
417
  return __awaiter(this, void 0, void 0, function () {
418
418
  var signatureEvent;
419
419
  return __generator(this, function (_a) {
420
420
  signatureEvent = {
421
- properties: __assign(__assign({ status: status, chainId: chainId, message: message }, (signatureHash && { signatureHash: signatureHash })), properties),
421
+ properties: __assign({ status: status, chainId: chainId, message: message }, properties),
422
422
  address: address,
423
423
  type: "signature",
424
424
  };
@@ -506,7 +506,7 @@ var EventFactory = /** @class */ (function () {
506
506
  case 12:
507
507
  formoEvent = _b.sent();
508
508
  return [3 /*break*/, 19];
509
- case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.signatureHash, event.properties, event.context)];
509
+ case 13: return [4 /*yield*/, this.generateSignatureEvent(event.status, event.chainId, event.address, event.message, event.properties, event.context)];
510
510
  case 14:
511
511
  formoEvent = _b.sent();
512
512
  return [3 /*break*/, 19];