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