@formo/analytics 1.33.1 → 1.34.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 (45) hide show
  1. package/README.md +3 -0
  2. package/dist/cjs/src/FormoAnalytics.d.ts +65 -19
  3. package/dist/cjs/src/FormoAnalytics.js +188 -94
  4. package/dist/cjs/src/event/EventFactory.d.ts +1 -1
  5. package/dist/cjs/src/event/EventFactory.js +32 -16
  6. package/dist/cjs/src/event/sanitize.d.ts +13 -0
  7. package/dist/cjs/src/event/sanitize.js +94 -0
  8. package/dist/cjs/src/privy/index.d.ts +8 -2
  9. package/dist/cjs/src/privy/index.js +8 -2
  10. package/dist/cjs/src/privy/types.d.ts +25 -2
  11. package/dist/cjs/src/privy/utils.d.ts +100 -0
  12. package/dist/cjs/src/privy/utils.js +375 -16
  13. package/dist/cjs/src/session/index.d.ts +73 -6
  14. package/dist/cjs/src/session/index.js +309 -12
  15. package/dist/cjs/src/solana/SolanaManager.d.ts +1 -1
  16. package/dist/cjs/src/solana/SolanaManager.js +1 -1
  17. package/dist/cjs/src/solana/storeTypes.d.ts +1 -1
  18. package/dist/cjs/src/solana/storeTypes.js +1 -1
  19. package/dist/cjs/src/solana/types.d.ts +2 -2
  20. package/dist/cjs/src/types/base.d.ts +17 -1
  21. package/dist/cjs/src/version.d.ts +1 -1
  22. package/dist/cjs/src/version.js +1 -1
  23. package/dist/esm/src/FormoAnalytics.d.ts +65 -19
  24. package/dist/esm/src/FormoAnalytics.js +188 -94
  25. package/dist/esm/src/event/EventFactory.d.ts +1 -1
  26. package/dist/esm/src/event/EventFactory.js +32 -16
  27. package/dist/esm/src/event/sanitize.d.ts +13 -0
  28. package/dist/esm/src/event/sanitize.js +88 -0
  29. package/dist/esm/src/privy/index.d.ts +8 -2
  30. package/dist/esm/src/privy/index.js +8 -2
  31. package/dist/esm/src/privy/types.d.ts +25 -2
  32. package/dist/esm/src/privy/utils.d.ts +100 -0
  33. package/dist/esm/src/privy/utils.js +374 -16
  34. package/dist/esm/src/session/index.d.ts +73 -6
  35. package/dist/esm/src/session/index.js +309 -12
  36. package/dist/esm/src/solana/SolanaManager.d.ts +1 -1
  37. package/dist/esm/src/solana/SolanaManager.js +1 -1
  38. package/dist/esm/src/solana/storeTypes.d.ts +1 -1
  39. package/dist/esm/src/solana/storeTypes.js +1 -1
  40. package/dist/esm/src/solana/types.d.ts +2 -2
  41. package/dist/esm/src/types/base.d.ts +17 -1
  42. package/dist/esm/src/version.d.ts +1 -1
  43. package/dist/esm/src/version.js +1 -1
  44. package/dist/index.umd.min.js +1 -1
  45. package/package.json +7 -7
@@ -1,12 +1,131 @@
1
1
  /**
2
2
  * Utility functions for extracting profile properties from Privy user objects.
3
3
  */
4
+ var __assign = (this && this.__assign) || function () {
5
+ __assign = Object.assign || function(t) {
6
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
7
+ s = arguments[i];
8
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
9
+ t[p] = s[p];
10
+ }
11
+ return t;
12
+ };
13
+ return __assign.apply(this, arguments);
14
+ };
15
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
16
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
17
+ return new (P || (P = Promise))(function (resolve, reject) {
18
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
19
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
20
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
21
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
22
+ });
23
+ };
24
+ var __generator = (this && this.__generator) || function (thisArg, body) {
25
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
26
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
27
+ function verb(n) { return function (v) { return step([n, v]); }; }
28
+ function step(op) {
29
+ if (f) throw new TypeError("Generator is already executing.");
30
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
31
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
32
+ if (y = 0, t) op = [op[0] & 2, t.value];
33
+ switch (op[0]) {
34
+ case 0: case 1: t = op; break;
35
+ case 4: _.label++; return { value: op[1], done: false };
36
+ case 5: _.label++; y = op[1]; op = [0]; continue;
37
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
38
+ default:
39
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
40
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
41
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
42
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
43
+ if (t[2]) _.ops.pop();
44
+ _.trys.pop(); continue;
45
+ }
46
+ op = body.call(thisArg, _);
47
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
48
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
49
+ }
50
+ };
51
+ var __rest = (this && this.__rest) || function (s, e) {
52
+ var t = {};
53
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
54
+ t[p] = s[p];
55
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
56
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
57
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
58
+ t[p[i]] = s[p[i]];
59
+ }
60
+ return t;
61
+ };
62
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
63
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
64
+ if (ar || !(i in from)) {
65
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
66
+ ar[i] = from[i];
67
+ }
68
+ }
69
+ return to.concat(ar || Array.prototype.slice.call(from));
70
+ };
71
+ import { logger } from "../logger";
72
+ /**
73
+ * Whether a Privy linked account is a usable wallet - an EVM/Solana wallet or
74
+ * smart wallet with an address.
75
+ */
76
+ function isPrivyWalletAccount(account) {
77
+ return ((account.type === "wallet" || account.type === "smart_wallet") &&
78
+ !!account.address);
79
+ }
80
+ /**
81
+ * Coerce a Privy timestamp to epoch milliseconds, tolerating the forms a
82
+ * `PrivyUser` can actually arrive in: a `Date` (React SDK), an ISO string or
83
+ * epoch number (REST API / JSON round-trip). Returns undefined for anything
84
+ * unparseable rather than throwing or emitting NaN.
85
+ */
86
+ function toEpochMillis(value) {
87
+ if (value instanceof Date) {
88
+ var time = value.getTime();
89
+ return Number.isNaN(time) ? undefined : time;
90
+ }
91
+ if (typeof value === "number") {
92
+ // Privy's REST API reports seconds; a Date-derived value is milliseconds.
93
+ // Anything below this threshold is far too old to be a millisecond
94
+ // timestamp, so treat it as seconds.
95
+ if (!Number.isFinite(value))
96
+ return undefined;
97
+ return value < 1e11 ? value * 1000 : value;
98
+ }
99
+ if (typeof value === "string") {
100
+ var parsed = Date.parse(value);
101
+ return Number.isNaN(parsed) ? undefined : parsed;
102
+ }
103
+ return undefined;
104
+ }
105
+ /** A 0x-prefixed 20-byte hex string (prefix and hex are case-insensitive). */
106
+ var EVM_ADDRESS_RE = /^0x[0-9a-f]{40}$/i;
107
+ /**
108
+ * Compare two wallet addresses for equality. EVM addresses are hex and
109
+ * case-insensitive, so they are folded to lowercase; all other chains (notably
110
+ * Solana, whose Base58 addresses are case-sensitive) are compared exactly, so a
111
+ * case difference never matches the wrong wallet.
112
+ */
113
+ function sameAddress(a, b) {
114
+ return EVM_ADDRESS_RE.test(a) && EVM_ADDRESS_RE.test(b)
115
+ ? a.toLowerCase() === b.toLowerCase()
116
+ : a === b;
117
+ }
4
118
  /**
5
119
  * Extract profile properties and wallet addresses from a Privy user object.
6
120
  *
7
121
  * Parses the Privy user's linked accounts into a flat properties object
8
122
  * (email, social accounts, etc.) and extracts all linked wallet addresses.
9
123
  *
124
+ * For most apps prefer the {@link identifyPrivyUser} one-liner, which builds on
125
+ * this function and also forwards per-wallet metadata and handles event
126
+ * attribution. Use `parsePrivyProperties` directly only for advanced/custom
127
+ * flows.
128
+ *
10
129
  * @param user - The Privy user object from `usePrivy()`
11
130
  * @returns An object with `properties` and `wallets`
12
131
  *
@@ -25,17 +144,28 @@
25
144
  * ```
26
145
  */
27
146
  export function parsePrivyProperties(user) {
28
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
147
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t;
29
148
  var accounts = user.linkedAccounts || [];
30
- // Extract profile properties
149
+ // Extract profile properties.
150
+ //
151
+ // Privy's React SDK types `createdAt` as a Date, but PrivyUser is a
152
+ // structural interface - a user object from the REST API, or one that made a
153
+ // JSON round-trip through a cache, carries a string or epoch number instead.
154
+ // Calling .getTime() on those throws, and identify()'s outer catch would
155
+ // swallow it, so the whole one-liner would silently emit nothing. Normalize
156
+ // instead of trusting the declared type.
31
157
  var properties = {
32
158
  privyDid: user.id,
33
- privyCreatedAt: (_a = user.createdAt) === null || _a === void 0 ? void 0 : _a.getTime(),
159
+ privyCreatedAt: toEpochMillis(user.createdAt),
34
160
  };
35
161
  // Email
36
- if ((_b = user.email) === null || _b === void 0 ? void 0 : _b.address) {
162
+ if ((_a = user.email) === null || _a === void 0 ? void 0 : _a.address) {
37
163
  properties.email = user.email.address;
38
164
  }
165
+ // Phone
166
+ if ((_b = user.phone) === null || _b === void 0 ? void 0 : _b.number) {
167
+ properties.phone = user.phone.number;
168
+ }
39
169
  // Social accounts - extract usernames/identifiers
40
170
  if ((_c = user.apple) === null || _c === void 0 ? void 0 : _c.email) {
41
171
  properties.apple = user.apple.email;
@@ -70,7 +200,10 @@ export function parsePrivyProperties(user) {
70
200
  if ((_o = user.tiktok) === null || _o === void 0 ? void 0 : _o.username) {
71
201
  properties.tiktok = user.tiktok.username;
72
202
  }
73
- if ((_p = user.twitter) === null || _p === void 0 ? void 0 : _p.username) {
203
+ if ((_p = user.twitch) === null || _p === void 0 ? void 0 : _p.username) {
204
+ properties.twitch = user.twitch.username;
205
+ }
206
+ if ((_q = user.twitter) === null || _q === void 0 ? void 0 : _q.username) {
74
207
  properties.twitter = user.twitter.username;
75
208
  }
76
209
  // Fallback to linkedAccounts if convenience accessors are not populated
@@ -80,6 +213,13 @@ export function parsePrivyProperties(user) {
80
213
  properties.email = emailAccount.address;
81
214
  }
82
215
  }
216
+ if (!properties.phone) {
217
+ // A phone account carries the value in `number`, not `address`.
218
+ var phoneAccount = accounts.find(function (a) { return a.type === "phone"; });
219
+ if (phoneAccount === null || phoneAccount === void 0 ? void 0 : phoneAccount.number) {
220
+ properties.phone = phoneAccount.number;
221
+ }
222
+ }
83
223
  if (!properties.apple) {
84
224
  var appleAccount = accounts.find(function (a) { return a.type === "apple_oauth"; });
85
225
  if (appleAccount === null || appleAccount === void 0 ? void 0 : appleAccount.email) {
@@ -152,12 +292,26 @@ export function parsePrivyProperties(user) {
152
292
  properties.tiktok = tiktokAccount.username;
153
293
  }
154
294
  }
295
+ if (!properties.twitch) {
296
+ var twitchAccount = accounts.find(function (a) { return a.type === "twitch_oauth"; });
297
+ if (twitchAccount === null || twitchAccount === void 0 ? void 0 : twitchAccount.username) {
298
+ properties.twitch = twitchAccount.username;
299
+ }
300
+ else if (twitchAccount === null || twitchAccount === void 0 ? void 0 : twitchAccount.email) {
301
+ properties.twitch = twitchAccount.email;
302
+ }
303
+ }
155
304
  if (!properties.twitter) {
156
305
  var twitterAccount = accounts.find(function (a) { return a.type === "twitter_oauth"; });
157
306
  if (twitterAccount === null || twitterAccount === void 0 ? void 0 : twitterAccount.username) {
158
307
  properties.twitter = twitterAccount.username;
159
308
  }
160
309
  }
310
+ // Custom auth: the app's own user id for this Privy user.
311
+ var customAuthAccount = accounts.find(function (a) { return a.type === "custom_auth"; });
312
+ if (customAuthAccount === null || customAuthAccount === void 0 ? void 0 : customAuthAccount.customUserId) {
313
+ properties.customUserId = customAuthAccount.customUserId;
314
+ }
161
315
  // Use OAuth emails as fallback for email if still blank
162
316
  // Priority: email -> google -> apple -> linkedin
163
317
  if (!properties.email) {
@@ -171,18 +325,222 @@ export function parsePrivyProperties(user) {
171
325
  properties.email = properties.linkedin;
172
326
  }
173
327
  }
174
- // Extract wallet addresses
175
- var wallets = accounts
176
- .filter(function (a) { return (a.type === "wallet" || a.type === "smart_wallet") && a.address; })
177
- .map(function (a) {
178
- var _a, _b;
179
- return ({
180
- address: a.address,
181
- walletClient: (_a = (a.walletClientType || a.walletClient)) !== null && _a !== void 0 ? _a : undefined,
182
- chainType: (_b = a.chainType) !== null && _b !== void 0 ? _b : undefined,
183
- isEmbedded: a.walletClientType === "privy" || a.walletClient === "privy",
328
+ // Extract wallet addresses. `fromCrossApp` is internal bookkeeping for the
329
+ // dedup below and is stripped from the returned wallets.
330
+ var wallets = [];
331
+ for (var _i = 0, accounts_1 = accounts; _i < accounts_1.length; _i++) {
332
+ var account = accounts_1[_i];
333
+ if (isPrivyWalletAccount(account)) {
334
+ wallets.push({
335
+ address: account.address,
336
+ walletClient: (_r = (account.walletClientType || account.walletClient)) !== null && _r !== void 0 ? _r : undefined,
337
+ chainType: (_s = account.chainType) !== null && _s !== void 0 ? _s : undefined,
338
+ isEmbedded: account.walletClientType === "privy" ||
339
+ account.walletClient === "privy",
340
+ });
341
+ continue;
342
+ }
343
+ // A cross_app account (e.g. Abstract Global Wallet) has no top-level
344
+ // address - its wallets live in `embeddedWallets`/`smartWallets`. They are
345
+ // still wallets the user owns, so identify them for clustering too.
346
+ if (account.type === "cross_app") {
347
+ var crossAppWallets = __spreadArray(__spreadArray([], (account.embeddedWallets || []), true), (account.smartWallets || []), true);
348
+ for (var _u = 0, crossAppWallets_1 = crossAppWallets; _u < crossAppWallets_1.length; _u++) {
349
+ var w = crossAppWallets_1[_u];
350
+ if (!(w === null || w === void 0 ? void 0 : w.address))
351
+ continue;
352
+ wallets.push({
353
+ address: w.address,
354
+ walletClient: "cross_app",
355
+ chainType: (_t = w.chainType) !== null && _t !== void 0 ? _t : undefined,
356
+ isEmbedded: true,
357
+ fromCrossApp: true,
358
+ });
359
+ }
360
+ }
361
+ }
362
+ // Deduplicate: the same address can appear as both a `wallet` and a
363
+ // `smart_wallet`/cross-app entry, and identifying it twice would emit a
364
+ // redundant event (and, for the active wallet, a redundant promotion).
365
+ //
366
+ // Which duplicate wins is not just first-come: a cross_app entry carries only
367
+ // an address (so it's recorded as `cross_app`/embedded by default), while a
368
+ // top-level wallet entry carries the real walletClient, chainType, and
369
+ // embedded classification. Prefer the richer entry regardless of the order
370
+ // Privy happened to list the accounts in, so the metadata - and the
371
+ // last-external active-wallet fallback, which keys on isEmbedded - don't
372
+ // depend on account ordering.
373
+ var indexByAddress = new Map();
374
+ var deduped = [];
375
+ for (var _v = 0, wallets_1 = wallets; _v < wallets_1.length; _v++) {
376
+ var wallet = wallets_1[_v];
377
+ var key = EVM_ADDRESS_RE.test(wallet.address)
378
+ ? wallet.address.toLowerCase()
379
+ : wallet.address;
380
+ var existingIndex = indexByAddress.get(key);
381
+ if (existingIndex === undefined) {
382
+ indexByAddress.set(key, deduped.length);
383
+ deduped.push(wallet);
384
+ continue;
385
+ }
386
+ // Only upgrade a placeholder cross-app entry to a real wallet entry; never
387
+ // downgrade, and never let a second cross-app entry replace anything. The
388
+ // position is kept so wallet order stays stable.
389
+ if (!wallet.fromCrossApp && deduped[existingIndex].fromCrossApp) {
390
+ deduped[existingIndex] = wallet;
391
+ }
392
+ }
393
+ var uniqueWallets = deduped.map(function (_a) {
394
+ var _fromCrossApp = _a.fromCrossApp, info = __rest(_a, ["fromCrossApp"]);
395
+ return info;
396
+ });
397
+ return { properties: properties, wallets: uniqueWallets };
398
+ }
399
+ /**
400
+ * Identify every wallet linked to a Privy user under that user's Privy DID.
401
+ *
402
+ * This is the one-liner replacement for hand-rolling a loop over
403
+ * {@link parsePrivyProperties}. For each linked wallet it calls
404
+ * `analytics.identify({ address, userId: user.id }, …)` with the shared
405
+ * profile properties plus that wallet's `wallet_client`, `chain_type`, and
406
+ * `is_embedded` metadata. Because every wallet is tagged with the same Privy
407
+ * `userId`, Formo can cluster them server-side into a single user.
408
+ *
409
+ * Attribution: only the active wallet (see
410
+ * {@link IdentifyPrivyUserOptions.activeAddress}) promotes the SDK's current
411
+ * address/user; every other linked wallet is recorded purely for clustering and
412
+ * does not repoint attribution. Because the clustering identifies don't touch
413
+ * active state, a connected wallet that isn't linked in Privy is left untouched
414
+ * rather than overwritten. This does not change the public `identify()` API.
415
+ *
416
+ * Before emitting, it reconciles the SDK's chain id with the active wallet's
417
+ * chain namespace (clearing a stale EVM chain id when a Solana wallet becomes
418
+ * active, and vice versa), so identifies aren't dropped by an `excludeChains`
419
+ * gate and later events aren't paired with the wrong chain. This happens here,
420
+ * so the direct helper and the `formo.identify(user)` form
421
+ * behave identically.
422
+ *
423
+ * Returns the active linked wallet's `{ address, chainType }` (the one now
424
+ * owning attribution), or `undefined` when no linked wallet matched the
425
+ * requested active address (or the user had no wallets).
426
+ *
427
+ * All linked wallet addresses used here come from `user.linkedAccounts`, which
428
+ * is fully available on the frontend from Privy's `usePrivy()` hook.
429
+ *
430
+ * Note: `identify()` is keyed on a wallet address, so a Privy user with no
431
+ * linked wallet is a no-op (nothing is emitted). Attaching a user identity that
432
+ * has no wallet is out of scope for this address-keyed helper.
433
+ *
434
+ * @param analytics - The Formo analytics instance (e.g. from `useFormo()`)
435
+ * @param user - The Privy user object from `usePrivy()`
436
+ * @param options - See {@link IdentifyPrivyUserOptions}
437
+ *
438
+ * @example
439
+ * ```ts
440
+ * import { identifyPrivyUser } from '@formo/analytics';
441
+ *
442
+ * const { user } = usePrivy();
443
+ * const { wallets } = useWallets();
444
+ * if (user) {
445
+ * // activeAddress is optional - omit it if the SDK already tracks the
446
+ * // connected wallet via a wagmi/EIP-1193 connect.
447
+ * await identifyPrivyUser(formo, user, {
448
+ * activeAddress: wallets[0]?.address,
449
+ * });
450
+ * }
451
+ * ```
452
+ */
453
+ export function identifyPrivyUser(analytics_1, user_1) {
454
+ return __awaiter(this, arguments, void 0, function (analytics, user, options) {
455
+ var target, _a, properties, wallets, baseProperties, activeWallet, identify, _i, wallets_2, wallet, walletProperties;
456
+ var _b, _c, _d, _e;
457
+ if (options === void 0) { options = {}; }
458
+ return __generator(this, function (_f) {
459
+ switch (_f.label) {
460
+ case 0:
461
+ if (!analytics || !user)
462
+ return [2 /*return*/, undefined];
463
+ target = analytics;
464
+ // If tracking is suppressed for this visitor/route (opt-out / timezone / host /
465
+ // path), do nothing. The inner identify() calls would each be gated anyway,
466
+ // but the chain reconciliation below runs BEFORE them - so without this guard
467
+ // it would clear an excluded chain id while no identify actually happens,
468
+ // leaving later events on an allowed route unable to apply `excludeChains`.
469
+ if ((_b = target.isTrackingSuppressed) === null || _b === void 0 ? void 0 : _b.call(target))
470
+ return [2 /*return*/, undefined];
471
+ _a = parsePrivyProperties(user), properties = _a.properties, wallets = _a.wallets;
472
+ // identify() is keyed on a wallet address, so with no linked wallets there is
473
+ // nothing to attach the Privy identity to. Log it so a walletless user (or a
474
+ // pre-wallet account-creation flow) doesn't silently disappear.
475
+ if (wallets.length === 0) {
476
+ logger.info("identifyPrivyUser: user has no linked wallets; nothing to identify", user.id);
477
+ return [2 /*return*/, undefined];
478
+ }
479
+ baseProperties = __assign(__assign({}, properties), options.properties);
480
+ activeWallet = resolveActiveWallet(wallets, (_c = options.activeAddress) !== null && _c !== void 0 ? _c : target.currentAddress, (_d = user.wallet) === null || _d === void 0 ? void 0 : _d.address);
481
+ // Reconcile the chain BEFORE emitting any identify. identify() runs each event
482
+ // through the tracking gate (which enforces `excludeChains` against the
483
+ // current chain id); if the active wallet is on a different chain namespace
484
+ // than the stale current chain id (e.g. a Solana wallet while an EVM chain was
485
+ // current, and that EVM chain is excluded), reconciling first prevents the
486
+ // clustering identifies from being silently dropped. Doing it here - rather
487
+ // than in the identify(user,{privy:true}) dispatch - means the direct
488
+ // identifyPrivyUser() entry point gets the same treatment.
489
+ // Pass the address too: Privy omits chainType on smart_wallet and cross_app
490
+ // entries, and a 0x address is enough to identify the EVM namespace.
491
+ (_e = target.syncPrivyActiveChain) === null || _e === void 0 ? void 0 : _e.call(target, activeWallet === null || activeWallet === void 0 ? void 0 : activeWallet.chainType, activeWallet === null || activeWallet === void 0 ? void 0 : activeWallet.address);
492
+ identify = target.identify.bind(analytics);
493
+ _i = 0, wallets_2 = wallets;
494
+ _f.label = 1;
495
+ case 1:
496
+ if (!(_i < wallets_2.length)) return [3 /*break*/, 4];
497
+ wallet = wallets_2[_i];
498
+ walletProperties = __assign(__assign({}, baseProperties), { is_embedded: wallet.isEmbedded });
499
+ if (wallet.walletClient)
500
+ walletProperties.wallet_client = wallet.walletClient;
501
+ if (wallet.chainType)
502
+ walletProperties.chain_type = wallet.chainType;
503
+ return [4 /*yield*/, identify({
504
+ address: wallet.address,
505
+ userId: user.id,
506
+ setActive: wallet === activeWallet,
507
+ }, walletProperties)];
508
+ case 2:
509
+ _f.sent();
510
+ _f.label = 3;
511
+ case 3:
512
+ _i++;
513
+ return [3 /*break*/, 1];
514
+ case 4: return [2 /*return*/, activeWallet
515
+ ? { address: activeWallet.address, chainType: activeWallet.chainType }
516
+ : undefined];
517
+ }
184
518
  });
185
519
  });
186
- return { properties: properties, wallets: wallets };
520
+ }
521
+ /**
522
+ * Choose the linked wallet that should own event attribution.
523
+ *
524
+ * An explicit `activeAddress` (a caller override, or the SDK's already-connected
525
+ * wallet) is matched strictly and never falls back: a connected wallet that is
526
+ * not in `linkedAccounts` resolves to `undefined`, so the sync leaves the
527
+ * current wallet untouched. With no active address, prefer Privy's surfaced
528
+ * primary (`user.wallet`), then a best-effort guess (embedded wallets first, so
529
+ * the last external wallet). Address comparison is chain-appropriate so Solana
530
+ * casing isn't mismatched.
531
+ */
532
+ function resolveActiveWallet(wallets, activeAddress, primaryAddress) {
533
+ if (activeAddress) {
534
+ return wallets.find(function (w) { return sameAddress(w.address, activeAddress); });
535
+ }
536
+ if (primaryAddress) {
537
+ var primary = wallets.find(function (w) { return sameAddress(w.address, primaryAddress); });
538
+ if (primary)
539
+ return primary;
540
+ }
541
+ var external = wallets.filter(function (w) { return !w.isEmbedded; });
542
+ return external.length > 0
543
+ ? external[external.length - 1]
544
+ : wallets[wallets.length - 1];
187
545
  }
188
546
  //# sourceMappingURL=utils.js.map
@@ -4,6 +4,7 @@
4
4
  * Handles tracking of detected wallets and identified wallet-address pairs
5
5
  * using cookies to maintain state across page loads within a session.
6
6
  */
7
+ import type { IFormoEventProperties } from "../types/events";
7
8
  /**
8
9
  * Cookie keys for session tracking
9
10
  * NOTE: These values must match the original constants in constants/base.ts
@@ -29,25 +30,91 @@ export interface IFormoAnalyticsSession {
29
30
  * Check if a wallet-address pair has been identified in this session
30
31
  * @param address The wallet address
31
32
  * @param rdns The reverse domain name (RDNS) of the wallet provider
33
+ * @param userId Optional external user ID (e.g. a Privy DID). When provided,
34
+ * it is folded into the dedup key so attaching a new user ID to an
35
+ * already-identified wallet re-emits instead of being silently deduped.
36
+ * @param properties Optional identify properties. Fingerprinted into the
37
+ * dedup key so an identify whose properties have *changed* (e.g. a Privy
38
+ * user linked a new social account) re-emits, while an unchanged repeat
39
+ * still dedupes.
32
40
  */
33
- isWalletIdentified(address: string, rdns: string): boolean;
41
+ isWalletIdentified(address: string, rdns: string, userId?: string, properties?: IFormoEventProperties): boolean;
34
42
  /**
35
43
  * Mark a wallet-address pair as identified in this session
36
44
  * @param address The wallet address
37
45
  * @param rdns The reverse domain name (RDNS) of the wallet provider
46
+ * @param userId Optional external user ID (e.g. a Privy DID). See
47
+ * {@link isWalletIdentified} for how it affects the dedup key.
48
+ * @param properties Optional identify properties. See
49
+ * {@link isWalletIdentified}.
38
50
  */
39
- markWalletIdentified(address: string, rdns: string): void;
51
+ markWalletIdentified(address: string, rdns: string, userId?: string, properties?: IFormoEventProperties): void;
40
52
  }
41
53
  export declare class FormoAnalyticsSession implements IFormoAnalyticsSession {
42
54
  /**
43
- * Generate a unique key for wallet identification tracking
44
- * Combines address and RDNS to track specific wallet-address combinations
55
+ * Generate a unique key for wallet identification tracking.
56
+ *
57
+ * Combines address, RDNS, and (optionally) the external user ID and a
58
+ * fingerprint of the identify's properties, so the key identifies a specific
59
+ * wallet-user-profile combination rather than just an address.
60
+ *
61
+ * Folding the user ID in means the same wallet identified first anonymously
62
+ * and later with a user ID (e.g. after a Privy login attaches a DID) produces
63
+ * two distinct keys, so the second identify is not deduped.
64
+ *
65
+ * Folding the properties hash in means a *changed* profile re-emits. This
66
+ * matters for account linking: a Privy user who links a Google account keeps
67
+ * the same wallets and the same DID, so without the hash every already-seen
68
+ * wallet would dedupe and the new `google` property would never reach Formo
69
+ * until the session expired. An identify repeated with identical properties
70
+ * still dedupes, so this does not turn a re-render into an event.
71
+ *
72
+ * Key shapes, by component count - each is unambiguous, so they cannot
73
+ * collide with one another:
74
+ *
75
+ * | Components | Shape | When |
76
+ * | --- | --- | --- |
77
+ * | 1 | `address` | no rdns, no userId, no properties |
78
+ * | 2 | `address:rdns` | rdns only |
79
+ * | 3 | `address:rdns:userId` | userId, no properties |
80
+ * | 4 | `address:rdns:userId:hash` | properties present |
81
+ *
82
+ * Shapes 1 and 2 are unchanged from before user IDs and property hashes
83
+ * existed, so keys already stored in browsers still match (backward
84
+ * compatible). An identify that carries properties moves to shape 4, so the
85
+ * first identify after an upgrade re-emits once per wallet - a one-off, and
86
+ * the correct outcome, since those properties were never recorded under the
87
+ * new key.
45
88
  *
46
89
  * @param address The wallet address
47
90
  * @param rdns The reverse domain name of the wallet provider
91
+ * @param userId Optional external user ID (e.g. a Privy DID)
92
+ * @param properties Optional identify properties, fingerprinted into the key
48
93
  * @returns A unique identification key
49
94
  */
50
95
  private generateIdentificationKey;
96
+ /**
97
+ * Build the dedup key plus the **identity prefix** it belongs to.
98
+ *
99
+ * The identity prefix is the key with the properties hash stripped -
100
+ * `address:rdns:userId` - i.e. *which wallet-user this is*, independent of
101
+ * *what profile it last had*. `markWalletIdentified` uses it to drop that
102
+ * identity's previous state before storing the new one, which matters twice:
103
+ *
104
+ * - **Reversion.** Keeping every state seen would make dedup mean "have I
105
+ * ever seen this exact profile", so a profile that goes A → B → A (link
106
+ * then unlink an account) would find the old A key and emit nothing. Dedup
107
+ * should mean "is this the same as this wallet's *last* identify", so only
108
+ * the current state is retained.
109
+ * - **Growth.** Otherwise each profile change adds a key per wallet, and an
110
+ * 8-wallet user linking a few accounts would push the cookie into eviction.
111
+ * Superseding keeps it at one entry per wallet-user.
112
+ *
113
+ * The prefix is only defined for keys that have a userId and/or a properties
114
+ * hash (3+ components). The legacy 1- and 2-component shapes are the whole
115
+ * identity already, so there is nothing to supersede.
116
+ */
117
+ private buildIdentificationKey;
51
118
  /**
52
119
  * Check if a wallet provider has been detected in this session
53
120
  *
@@ -69,7 +136,7 @@ export declare class FormoAnalyticsSession implements IFormoAnalyticsSession {
69
136
  * @param rdns The reverse domain name of the wallet provider
70
137
  * @returns true if this wallet-address pair has been identified
71
138
  */
72
- isWalletIdentified(address: string, rdns: string): boolean;
139
+ isWalletIdentified(address: string, rdns: string, userId?: string, properties?: IFormoEventProperties): boolean;
73
140
  /**
74
141
  * Mark a wallet-address combination as identified in this session
75
142
  * Prevents duplicate identification events from being emitted
@@ -77,6 +144,6 @@ export declare class FormoAnalyticsSession implements IFormoAnalyticsSession {
77
144
  * @param address The wallet address
78
145
  * @param rdns The reverse domain name of the wallet provider
79
146
  */
80
- markWalletIdentified(address: string, rdns: string): void;
147
+ markWalletIdentified(address: string, rdns: string, userId?: string, properties?: IFormoEventProperties): void;
81
148
  }
82
149
  //# sourceMappingURL=index.d.ts.map