@formo/analytics 1.17.1 → 1.17.3
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/.env.example +1 -0
- package/.github/workflows/ci.yml +50 -0
- package/.github/workflows/release.yml +54 -0
- package/.husky/post-commit +7 -0
- package/.husky/pre-commit +11 -0
- package/.releaserc.js +35 -0
- package/CONTRIBUTING.md +83 -0
- package/package.json +1 -4
- package/scripts/generate-sri.sh +52 -0
- package/src/FormoAnalytics.ts +1031 -0
- package/src/FormoAnalyticsProvider.tsx +84 -0
- package/src/constants/base.ts +6 -0
- package/src/constants/config.ts +660 -0
- package/src/constants/events.ts +21 -0
- package/src/constants/index.ts +3 -0
- package/src/global.d.ts +12 -0
- package/src/index.ts +3 -0
- package/src/lib/event/EventFactory.ts +519 -0
- package/src/lib/event/EventManager.ts +39 -0
- package/src/lib/event/constants.ts +4 -0
- package/src/lib/event/index.ts +3 -0
- package/src/lib/event/type.ts +9 -0
- package/src/lib/event/utils.ts +33 -0
- package/src/lib/fetch.ts +3 -0
- package/src/lib/index.ts +5 -0
- package/src/lib/logger/Logger.ts +115 -0
- package/src/lib/logger/index.ts +2 -0
- package/src/lib/logger/type.ts +14 -0
- package/src/lib/queue/EventQueue.ts +306 -0
- package/src/lib/queue/index.ts +2 -0
- package/src/lib/queue/type.ts +6 -0
- package/src/lib/ramda/internal/_curry1.ts +19 -0
- package/src/lib/ramda/internal/_curry2.ts +37 -0
- package/src/lib/ramda/internal/_curry3.ts +68 -0
- package/src/lib/ramda/internal/_has.ts +3 -0
- package/src/lib/ramda/internal/_isObject.ts +3 -0
- package/src/lib/ramda/internal/_isPlaceholder.ts +5 -0
- package/src/lib/ramda/mergeDeepRight.ts +13 -0
- package/src/lib/ramda/mergeDeepWithKey.ts +22 -0
- package/src/lib/ramda/mergeWithKey.ts +28 -0
- package/src/lib/storage/StorageManager.ts +51 -0
- package/src/lib/storage/built-in/blueprint.ts +17 -0
- package/src/lib/storage/built-in/cookie.ts +60 -0
- package/src/lib/storage/built-in/memory.ts +23 -0
- package/src/lib/storage/built-in/web.ts +57 -0
- package/src/lib/storage/constant.ts +2 -0
- package/src/lib/storage/index.ts +25 -0
- package/src/lib/storage/type.ts +21 -0
- package/src/lib/version.ts +2 -0
- package/src/types/base.ts +120 -0
- package/src/types/events.ts +126 -0
- package/src/types/index.ts +3 -0
- package/src/types/provider.ts +17 -0
- package/src/utils/address.ts +43 -0
- package/src/utils/base.ts +3 -0
- package/src/utils/converter.ts +44 -0
- package/src/utils/generate.ts +16 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/timestamp.ts +9 -0
- package/src/validators/address.ts +69 -0
- package/src/validators/agent.ts +4 -0
- package/src/validators/checks.ts +160 -0
- package/src/validators/index.ts +7 -0
- package/src/validators/network.ts +34 -0
- package/src/validators/object.ts +4 -0
- package/src/validators/string.ts +4 -0
- package/src/validators/uint8array.ts +17 -0
- package/test/lib/events.spec.ts +12 -0
- package/test/utils/address.spec.ts +14 -0
- package/test/utils/converter.spec.ts +31 -0
- package/test/validators/address.spec.ts +15 -0
- package/tsconfig.json +28 -0
- package/webpack.config.ts +23 -0
|
@@ -0,0 +1,1031 @@
|
|
|
1
|
+
import { createStore, EIP6963ProviderDetail } from "mipd";
|
|
2
|
+
import {
|
|
3
|
+
EVENTS_API_URL,
|
|
4
|
+
EventType,
|
|
5
|
+
LOCAL_ANONYMOUS_ID_KEY,
|
|
6
|
+
SESSION_CURRENT_URL_KEY,
|
|
7
|
+
SESSION_USER_ID_KEY,
|
|
8
|
+
SESSION_WALLET_DETECTED_KEY,
|
|
9
|
+
TEventType,
|
|
10
|
+
} from "./constants";
|
|
11
|
+
import {
|
|
12
|
+
cookie,
|
|
13
|
+
EventManager,
|
|
14
|
+
EventQueue,
|
|
15
|
+
IEventManager,
|
|
16
|
+
initStorageManager,
|
|
17
|
+
logger,
|
|
18
|
+
Logger,
|
|
19
|
+
} from "./lib";
|
|
20
|
+
import {
|
|
21
|
+
Address,
|
|
22
|
+
ChainID,
|
|
23
|
+
Config,
|
|
24
|
+
EIP1193Provider,
|
|
25
|
+
IFormoAnalytics,
|
|
26
|
+
IFormoEventContext,
|
|
27
|
+
IFormoEventProperties,
|
|
28
|
+
Options,
|
|
29
|
+
RequestArguments,
|
|
30
|
+
RPCError,
|
|
31
|
+
SignatureStatus,
|
|
32
|
+
TransactionStatus,
|
|
33
|
+
} from "./types";
|
|
34
|
+
import { isAddress, isLocalhost } from "./validators";
|
|
35
|
+
|
|
36
|
+
export class FormoAnalytics implements IFormoAnalytics {
|
|
37
|
+
private _provider?: EIP1193Provider;
|
|
38
|
+
private _providerListeners: Record<string, (...args: unknown[]) => void> = {};
|
|
39
|
+
private session: FormoAnalyticsSession;
|
|
40
|
+
private eventManager: IEventManager;
|
|
41
|
+
private _providers: readonly EIP6963ProviderDetail[] = [];
|
|
42
|
+
|
|
43
|
+
config: Config;
|
|
44
|
+
currentChainId?: ChainID;
|
|
45
|
+
currentAddress?: Address = "";
|
|
46
|
+
currentUserId?: string = "";
|
|
47
|
+
|
|
48
|
+
private constructor(
|
|
49
|
+
public readonly writeKey: string,
|
|
50
|
+
public options: Options = {}
|
|
51
|
+
) {
|
|
52
|
+
this.config = {
|
|
53
|
+
writeKey,
|
|
54
|
+
trackLocalhost: options.trackLocalhost || false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.session = new FormoAnalyticsSession();
|
|
58
|
+
this.currentUserId =
|
|
59
|
+
(cookie().get(SESSION_USER_ID_KEY) as string) || undefined;
|
|
60
|
+
|
|
61
|
+
this.identify = this.identify.bind(this);
|
|
62
|
+
this.connect = this.connect.bind(this);
|
|
63
|
+
this.disconnect = this.disconnect.bind(this);
|
|
64
|
+
this.chain = this.chain.bind(this);
|
|
65
|
+
this.signature = this.signature.bind(this);
|
|
66
|
+
this.transaction = this.transaction.bind(this);
|
|
67
|
+
this.detect = this.detect.bind(this);
|
|
68
|
+
this.track = this.track.bind(this);
|
|
69
|
+
|
|
70
|
+
// Initialize logger with configuration from options
|
|
71
|
+
Logger.init({
|
|
72
|
+
enabled: options.logger?.enabled || false,
|
|
73
|
+
enabledLevels: options.logger?.levels || [],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
this.eventManager = new EventManager(
|
|
77
|
+
new EventQueue(this.config.writeKey, {
|
|
78
|
+
url: EVENTS_API_URL,
|
|
79
|
+
flushAt: options.flushAt,
|
|
80
|
+
retryCount: options.retryCount,
|
|
81
|
+
maxQueueSize: options.maxQueueSize,
|
|
82
|
+
flushInterval: options.flushInterval,
|
|
83
|
+
})
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// TODO: replace with eip6963
|
|
87
|
+
const provider = options.provider || window?.ethereum;
|
|
88
|
+
if (provider) {
|
|
89
|
+
this.trackProvider(provider);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.trackFirstPageHit();
|
|
93
|
+
this.trackPageHits();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static async init(
|
|
97
|
+
writeKey: string,
|
|
98
|
+
options?: Options
|
|
99
|
+
): Promise<FormoAnalytics> {
|
|
100
|
+
initStorageManager(writeKey);
|
|
101
|
+
const analytics = new FormoAnalytics(writeKey, options);
|
|
102
|
+
|
|
103
|
+
// Auto-detect wallet provider
|
|
104
|
+
analytics._providers = await analytics.getProviders();
|
|
105
|
+
await analytics.detectWallets(analytics._providers);
|
|
106
|
+
|
|
107
|
+
return analytics;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
Public SDK functions
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Emits a page visit event with the current URL information, fire on page change.
|
|
116
|
+
* @param {string} category - The category of the page
|
|
117
|
+
* @param {string} name - The name of the page
|
|
118
|
+
* @param {Record<string, any>} properties - Additional properties to include
|
|
119
|
+
* @param {Record<string, any>} context - Additional context to include
|
|
120
|
+
* @returns {Promise<void>}
|
|
121
|
+
*/
|
|
122
|
+
public async page(
|
|
123
|
+
category?: string,
|
|
124
|
+
name?: string,
|
|
125
|
+
properties?: IFormoEventProperties,
|
|
126
|
+
context?: IFormoEventContext
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
await this.trackPageHit(category, name, properties, context);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Reset the current user session.
|
|
133
|
+
* @returns {void}
|
|
134
|
+
*/
|
|
135
|
+
public reset(): void {
|
|
136
|
+
this.currentUserId = undefined;
|
|
137
|
+
cookie().remove(LOCAL_ANONYMOUS_ID_KEY);
|
|
138
|
+
cookie().remove(SESSION_USER_ID_KEY);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Emits a connect wallet event.
|
|
143
|
+
* @param {ChainID} params.chainId
|
|
144
|
+
* @param {Address} params.address
|
|
145
|
+
* @param {IFormoEventProperties} properties
|
|
146
|
+
* @param {IFormoEventContext} context
|
|
147
|
+
* @param {(...args: unknown[]) => void} callback
|
|
148
|
+
* @throws {Error} If chainId or address is empty
|
|
149
|
+
* @returns {Promise<void>}
|
|
150
|
+
*/
|
|
151
|
+
async connect(
|
|
152
|
+
{
|
|
153
|
+
chainId,
|
|
154
|
+
address,
|
|
155
|
+
}: {
|
|
156
|
+
chainId: ChainID;
|
|
157
|
+
address: Address;
|
|
158
|
+
},
|
|
159
|
+
properties?: IFormoEventProperties,
|
|
160
|
+
context?: IFormoEventContext,
|
|
161
|
+
callback?: (...args: unknown[]) => void
|
|
162
|
+
): Promise<void> {
|
|
163
|
+
if (!chainId) {
|
|
164
|
+
logger.warn("Connect: Chain ID cannot be empty");
|
|
165
|
+
}
|
|
166
|
+
if (!address) {
|
|
167
|
+
logger.warn("Connect: Address cannot be empty");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.currentChainId = chainId;
|
|
171
|
+
this.currentAddress = address;
|
|
172
|
+
|
|
173
|
+
await this.trackEvent(
|
|
174
|
+
EventType.CONNECT,
|
|
175
|
+
{
|
|
176
|
+
chainId,
|
|
177
|
+
address,
|
|
178
|
+
},
|
|
179
|
+
properties,
|
|
180
|
+
context,
|
|
181
|
+
callback
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Emits a wallet disconnect event.
|
|
187
|
+
* @param {ChainID} params.chainId
|
|
188
|
+
* @param {Address} params.address
|
|
189
|
+
* @param {IFormoEventProperties} properties
|
|
190
|
+
* @param {IFormoEventContext} context
|
|
191
|
+
* @param {(...args: unknown[]) => void} callback
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*/
|
|
194
|
+
async disconnect(
|
|
195
|
+
params?: {
|
|
196
|
+
chainId?: ChainID;
|
|
197
|
+
address?: Address;
|
|
198
|
+
},
|
|
199
|
+
properties?: IFormoEventProperties,
|
|
200
|
+
context?: IFormoEventContext,
|
|
201
|
+
callback?: (...args: unknown[]) => void
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const address = params?.address || this.currentAddress;
|
|
204
|
+
const chainId = params?.chainId || this.currentChainId;
|
|
205
|
+
|
|
206
|
+
await this.handleDisconnect(
|
|
207
|
+
chainId,
|
|
208
|
+
address,
|
|
209
|
+
properties,
|
|
210
|
+
context,
|
|
211
|
+
callback
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Emits a chain network change event.
|
|
217
|
+
* @param {ChainID} params.chainId
|
|
218
|
+
* @param {Address} params.address
|
|
219
|
+
* @param {IFormoEventProperties} properties
|
|
220
|
+
* @param {IFormoEventContext} context
|
|
221
|
+
* @param {(...args: unknown[]) => void} callback
|
|
222
|
+
* @throws {Error} If chainId is empty, zero, or not a valid number
|
|
223
|
+
* @throws {Error} If no address is provided and no previous address is recorded
|
|
224
|
+
* @returns {Promise<void>}
|
|
225
|
+
*/
|
|
226
|
+
async chain(
|
|
227
|
+
{
|
|
228
|
+
chainId,
|
|
229
|
+
address,
|
|
230
|
+
}: {
|
|
231
|
+
chainId: ChainID;
|
|
232
|
+
address?: Address;
|
|
233
|
+
},
|
|
234
|
+
properties?: IFormoEventProperties,
|
|
235
|
+
context?: IFormoEventContext,
|
|
236
|
+
callback?: (...args: unknown[]) => void
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
if (!chainId || Number(chainId) === 0) {
|
|
239
|
+
throw new Error("FormoAnalytics::chain: chainId cannot be empty or 0");
|
|
240
|
+
}
|
|
241
|
+
if (isNaN(Number(chainId))) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
"FormoAnalytics::chain: chainId must be a valid decimal number"
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (!address && !this.currentAddress) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"FormoAnalytics::chain: address was empty and no previous address has been recorded"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.currentChainId = chainId;
|
|
253
|
+
|
|
254
|
+
await this.trackEvent(
|
|
255
|
+
EventType.CHAIN,
|
|
256
|
+
{
|
|
257
|
+
chainId,
|
|
258
|
+
address: address || this.currentAddress,
|
|
259
|
+
},
|
|
260
|
+
properties,
|
|
261
|
+
context,
|
|
262
|
+
callback
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Emits a signature event.
|
|
268
|
+
* @param {SignatureStatus} params.status - requested, confirmed, rejected
|
|
269
|
+
* @param {ChainID} params.chainId
|
|
270
|
+
* @param {Address} params.address
|
|
271
|
+
* @param {string} params.message
|
|
272
|
+
* @param {string} params.signatureHash - only provided if status is confirmed
|
|
273
|
+
* @param {IFormoEventProperties} properties
|
|
274
|
+
* @param {IFormoEventContext} context
|
|
275
|
+
* @param {(...args: unknown[]) => void} callback
|
|
276
|
+
* @returns {Promise<void>}
|
|
277
|
+
*/
|
|
278
|
+
async signature(
|
|
279
|
+
{
|
|
280
|
+
status,
|
|
281
|
+
chainId,
|
|
282
|
+
address,
|
|
283
|
+
message,
|
|
284
|
+
signatureHash,
|
|
285
|
+
}: {
|
|
286
|
+
status: SignatureStatus;
|
|
287
|
+
chainId?: ChainID;
|
|
288
|
+
address: Address;
|
|
289
|
+
message: string;
|
|
290
|
+
signatureHash?: string;
|
|
291
|
+
},
|
|
292
|
+
properties?: IFormoEventProperties,
|
|
293
|
+
context?: IFormoEventContext,
|
|
294
|
+
callback?: (...args: unknown[]) => void
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
await this.trackEvent(
|
|
297
|
+
EventType.SIGNATURE,
|
|
298
|
+
{
|
|
299
|
+
status,
|
|
300
|
+
chainId,
|
|
301
|
+
address,
|
|
302
|
+
message,
|
|
303
|
+
...(signatureHash && { signatureHash }),
|
|
304
|
+
},
|
|
305
|
+
properties,
|
|
306
|
+
context,
|
|
307
|
+
callback
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Emits a transaction event.
|
|
313
|
+
* @param {TransactionStatus} params.status - started, broadcasted, rejected
|
|
314
|
+
* @param {ChainID} params.chainId
|
|
315
|
+
* @param {Address} params.address
|
|
316
|
+
* @param {string} params.data
|
|
317
|
+
* @param {string} params.to
|
|
318
|
+
* @param {string} params.value
|
|
319
|
+
* @param {string} params.transactionHash - only provided if status is broadcasted
|
|
320
|
+
* @param {IFormoEventProperties} properties
|
|
321
|
+
* @param {IFormoEventContext} context
|
|
322
|
+
* @param {(...args: unknown[]) => void} callback
|
|
323
|
+
* @returns {Promise<void>}
|
|
324
|
+
*/
|
|
325
|
+
async transaction(
|
|
326
|
+
{
|
|
327
|
+
status,
|
|
328
|
+
chainId,
|
|
329
|
+
address,
|
|
330
|
+
data,
|
|
331
|
+
to,
|
|
332
|
+
value,
|
|
333
|
+
transactionHash,
|
|
334
|
+
}: {
|
|
335
|
+
status: TransactionStatus;
|
|
336
|
+
chainId: ChainID;
|
|
337
|
+
address: Address;
|
|
338
|
+
data?: string;
|
|
339
|
+
to?: string;
|
|
340
|
+
value?: string;
|
|
341
|
+
transactionHash?: string;
|
|
342
|
+
},
|
|
343
|
+
properties?: IFormoEventProperties,
|
|
344
|
+
context?: IFormoEventContext,
|
|
345
|
+
callback?: (...args: unknown[]) => void
|
|
346
|
+
): Promise<void> {
|
|
347
|
+
await this.trackEvent(
|
|
348
|
+
EventType.TRANSACTION,
|
|
349
|
+
{
|
|
350
|
+
status,
|
|
351
|
+
chainId,
|
|
352
|
+
address,
|
|
353
|
+
data,
|
|
354
|
+
to,
|
|
355
|
+
value,
|
|
356
|
+
...(transactionHash && { transactionHash }),
|
|
357
|
+
},
|
|
358
|
+
properties,
|
|
359
|
+
context,
|
|
360
|
+
callback
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Emits an identify event with current wallet address and provider info.
|
|
366
|
+
* @param {string} params.address
|
|
367
|
+
* @param {string} params.userId
|
|
368
|
+
* @param {string} params.rdns
|
|
369
|
+
* @param {string} params.providerName
|
|
370
|
+
* @param {IFormoEventProperties} properties
|
|
371
|
+
* @param {IFormoEventContext} context
|
|
372
|
+
* @param {(...args: unknown[]) => void} callback
|
|
373
|
+
* @returns {Promise<void>}
|
|
374
|
+
*/
|
|
375
|
+
async identify(
|
|
376
|
+
params?: {
|
|
377
|
+
address?: Address;
|
|
378
|
+
providerName?: string;
|
|
379
|
+
userId?: string;
|
|
380
|
+
rdns?: string;
|
|
381
|
+
},
|
|
382
|
+
properties?: IFormoEventProperties,
|
|
383
|
+
context?: IFormoEventContext,
|
|
384
|
+
callback?: (...args: unknown[]) => void
|
|
385
|
+
): Promise<void> {
|
|
386
|
+
try {
|
|
387
|
+
if (!params) {
|
|
388
|
+
// If no params provided, auto-identify
|
|
389
|
+
logger.info(
|
|
390
|
+
"Auto-identifying with providers:",
|
|
391
|
+
this._providers.map((p) => p.info.name)
|
|
392
|
+
);
|
|
393
|
+
for (const providerDetail of this._providers) {
|
|
394
|
+
const provider = providerDetail.provider;
|
|
395
|
+
if (!provider) continue;
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const address = await this.getAddress(provider);
|
|
399
|
+
if (address) {
|
|
400
|
+
logger.info(
|
|
401
|
+
"Auto-identifying",
|
|
402
|
+
address,
|
|
403
|
+
providerDetail.info.name,
|
|
404
|
+
providerDetail.info.rdns
|
|
405
|
+
);
|
|
406
|
+
// NOTE: do not set this.currentAddress without explicit connect or identify
|
|
407
|
+
await this.identify(
|
|
408
|
+
{
|
|
409
|
+
address,
|
|
410
|
+
providerName: providerDetail.info.name,
|
|
411
|
+
rdns: providerDetail.info.rdns,
|
|
412
|
+
},
|
|
413
|
+
properties,
|
|
414
|
+
context,
|
|
415
|
+
callback
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
} catch (err) {
|
|
419
|
+
logger.error(
|
|
420
|
+
`Failed to identify provider ${providerDetail.info.name}:`,
|
|
421
|
+
err
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Explicit identify
|
|
429
|
+
const { userId, address, providerName, rdns } = params;
|
|
430
|
+
logger.info("Identify", address, userId, providerName, rdns);
|
|
431
|
+
if (address) this.currentAddress = address;
|
|
432
|
+
if (userId) {
|
|
433
|
+
this.currentUserId = userId;
|
|
434
|
+
cookie().set(SESSION_USER_ID_KEY, userId);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
await this.trackEvent(
|
|
438
|
+
EventType.IDENTIFY,
|
|
439
|
+
{
|
|
440
|
+
address,
|
|
441
|
+
providerName,
|
|
442
|
+
userId,
|
|
443
|
+
rdns,
|
|
444
|
+
},
|
|
445
|
+
properties,
|
|
446
|
+
context,
|
|
447
|
+
callback
|
|
448
|
+
);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
logger.log("identify error", e);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Emits a detect wallet event with current wallet provider info.
|
|
456
|
+
* @param {string} params.providerName
|
|
457
|
+
* @param {string} params.rdns
|
|
458
|
+
* @param {IFormoEventProperties} properties
|
|
459
|
+
* @param {IFormoEventContext} context
|
|
460
|
+
* @param {(...args: unknown[]) => void} callback
|
|
461
|
+
* @returns {Promise<void>}
|
|
462
|
+
*/
|
|
463
|
+
async detect(
|
|
464
|
+
{
|
|
465
|
+
providerName,
|
|
466
|
+
rdns,
|
|
467
|
+
}: {
|
|
468
|
+
providerName: string;
|
|
469
|
+
rdns: string;
|
|
470
|
+
},
|
|
471
|
+
properties?: IFormoEventProperties,
|
|
472
|
+
context?: IFormoEventContext,
|
|
473
|
+
callback?: (...args: unknown[]) => void
|
|
474
|
+
): Promise<void> {
|
|
475
|
+
if (this.session.isWalletDetected(rdns))
|
|
476
|
+
return logger.warn(
|
|
477
|
+
`Detect: Wallet ${providerName} already detected in this session`
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
this.session.markWalletDetected(rdns);
|
|
481
|
+
await this.trackEvent(
|
|
482
|
+
EventType.DETECT,
|
|
483
|
+
{
|
|
484
|
+
providerName,
|
|
485
|
+
rdns,
|
|
486
|
+
},
|
|
487
|
+
properties,
|
|
488
|
+
context,
|
|
489
|
+
callback
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Emits a custom user event with custom properties.
|
|
495
|
+
* @param {string} event The name of the tracked event
|
|
496
|
+
* @param {IFormoEventProperties} properties
|
|
497
|
+
* @param {IFormoEventContext} context
|
|
498
|
+
* @param {(...args: unknown[]) => void} callback
|
|
499
|
+
* @returns {Promise<void>}
|
|
500
|
+
*/
|
|
501
|
+
async track(
|
|
502
|
+
event: string,
|
|
503
|
+
properties?: IFormoEventProperties,
|
|
504
|
+
context?: IFormoEventContext,
|
|
505
|
+
callback?: (...args: unknown[]) => void
|
|
506
|
+
): Promise<void> {
|
|
507
|
+
await this.trackEvent(
|
|
508
|
+
EventType.TRACK,
|
|
509
|
+
{ event },
|
|
510
|
+
properties,
|
|
511
|
+
context,
|
|
512
|
+
callback
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/*
|
|
517
|
+
SDK tracking and event listener functions
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
private trackProvider(provider: EIP1193Provider): void {
|
|
521
|
+
try {
|
|
522
|
+
if (provider === this._provider) {
|
|
523
|
+
logger.warn("TrackProvider: Provider already tracked.");
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
this.currentChainId = undefined;
|
|
528
|
+
this.currentAddress = undefined;
|
|
529
|
+
|
|
530
|
+
if (this._provider) {
|
|
531
|
+
const actions = Object.keys(this._providerListeners);
|
|
532
|
+
for (const action of actions) {
|
|
533
|
+
this._provider.removeListener(
|
|
534
|
+
action,
|
|
535
|
+
this._providerListeners[action]
|
|
536
|
+
);
|
|
537
|
+
delete this._providerListeners[action];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
this._provider = provider;
|
|
542
|
+
|
|
543
|
+
// Register listeners for web3 provider events
|
|
544
|
+
this.registerAddressChangedListener();
|
|
545
|
+
this.registerChainChangedListener();
|
|
546
|
+
this.registerSignatureListener();
|
|
547
|
+
this.registerTransactionListener();
|
|
548
|
+
} catch (error) {
|
|
549
|
+
logger.error("Error tracking provider:", error);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private registerAddressChangedListener(): void {
|
|
554
|
+
const listener = (...args: unknown[]) =>
|
|
555
|
+
this.onAddressChanged(args[0] as string[]);
|
|
556
|
+
|
|
557
|
+
this._provider?.on("accountsChanged", listener);
|
|
558
|
+
this._providerListeners["accountsChanged"] = listener;
|
|
559
|
+
|
|
560
|
+
const onAddressDisconnected = this.onAddressDisconnected.bind(this);
|
|
561
|
+
this._provider?.on("disconnect", onAddressDisconnected);
|
|
562
|
+
this._providerListeners["disconnect"] = onAddressDisconnected;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private registerChainChangedListener(): void {
|
|
566
|
+
const listener = (...args: unknown[]) =>
|
|
567
|
+
this.onChainChanged(args[0] as string);
|
|
568
|
+
this.provider?.on("chainChanged", listener);
|
|
569
|
+
this._providerListeners["chainChanged"] = listener;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private registerSignatureListener(): void {
|
|
573
|
+
if (!this.provider) {
|
|
574
|
+
logger.error("Provider not found for signature tracking");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (
|
|
578
|
+
Object.getOwnPropertyDescriptor(this.provider, "request")?.writable ===
|
|
579
|
+
false
|
|
580
|
+
) {
|
|
581
|
+
logger.warn("Provider.request is not writable");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const request = this.provider.request.bind(this.provider);
|
|
586
|
+
this.provider.request = async <T>({
|
|
587
|
+
method,
|
|
588
|
+
params,
|
|
589
|
+
}: RequestArguments): Promise<T | null | undefined> => {
|
|
590
|
+
if (
|
|
591
|
+
Array.isArray(params) &&
|
|
592
|
+
["eth_signTypedData_v4", "personal_sign"].includes(method)
|
|
593
|
+
) {
|
|
594
|
+
// Emit signature request event
|
|
595
|
+
this.signature({
|
|
596
|
+
status: SignatureStatus.REQUESTED,
|
|
597
|
+
...this.buildSignatureEventPayload(method, params),
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const response = (await request({ method, params })) as T;
|
|
602
|
+
if (response) {
|
|
603
|
+
// Emit signature confirmed event
|
|
604
|
+
this.signature({
|
|
605
|
+
status: SignatureStatus.CONFIRMED,
|
|
606
|
+
...this.buildSignatureEventPayload(method, params, response),
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
return response;
|
|
610
|
+
} catch (error) {
|
|
611
|
+
const rpcError = error as RPCError;
|
|
612
|
+
if (rpcError && rpcError?.code === 4001) {
|
|
613
|
+
// Emit signature rejected event
|
|
614
|
+
this.signature({
|
|
615
|
+
status: SignatureStatus.REJECTED,
|
|
616
|
+
...this.buildSignatureEventPayload(method, params),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return request({ method, params });
|
|
623
|
+
};
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private registerTransactionListener(): void {
|
|
628
|
+
if (!this.provider) {
|
|
629
|
+
logger.error("Provider not found for transaction tracking");
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (
|
|
633
|
+
Object.getOwnPropertyDescriptor(this.provider, "request")?.writable ===
|
|
634
|
+
false
|
|
635
|
+
) {
|
|
636
|
+
logger.warn("Provider.request is not writable");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const request = this.provider.request.bind(this.provider);
|
|
640
|
+
this.provider.request = async <T>({
|
|
641
|
+
method,
|
|
642
|
+
params,
|
|
643
|
+
}: RequestArguments): Promise<T | null | undefined> => {
|
|
644
|
+
if (
|
|
645
|
+
Array.isArray(params) &&
|
|
646
|
+
method === "eth_sendTransaction" &&
|
|
647
|
+
params[0]
|
|
648
|
+
) {
|
|
649
|
+
// Track transaction start
|
|
650
|
+
const payload = await this.buildTransactionEventPayload(params);
|
|
651
|
+
this.transaction({ status: TransactionStatus.STARTED, ...payload });
|
|
652
|
+
|
|
653
|
+
try {
|
|
654
|
+
// Wait for the transaction hash
|
|
655
|
+
const transactionHash = (await request({ method, params })) as string;
|
|
656
|
+
|
|
657
|
+
// Track transaction broadcast
|
|
658
|
+
this.transaction({
|
|
659
|
+
status: TransactionStatus.BROADCASTED,
|
|
660
|
+
...payload,
|
|
661
|
+
transactionHash,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
return;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
logger.error("Transaction error:", error);
|
|
667
|
+
const rpcError = error as RPCError;
|
|
668
|
+
if (rpcError && rpcError?.code === 4001) {
|
|
669
|
+
// Emit transaction rejected event
|
|
670
|
+
this.transaction({
|
|
671
|
+
status: TransactionStatus.REJECTED,
|
|
672
|
+
...payload,
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
throw error;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return request({ method, params });
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
private async onAddressChanged(addresses: Address[]): Promise<void> {
|
|
686
|
+
if (addresses.length > 0) {
|
|
687
|
+
this.onAddressConnected(addresses[0]);
|
|
688
|
+
} else {
|
|
689
|
+
this.onAddressDisconnected();
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private async onAddressConnected(address: Address): Promise<void> {
|
|
694
|
+
if (address === this.currentAddress)
|
|
695
|
+
// We have already reported this address
|
|
696
|
+
return;
|
|
697
|
+
|
|
698
|
+
this.currentAddress = address;
|
|
699
|
+
|
|
700
|
+
this.currentChainId = await this.getCurrentChainId();
|
|
701
|
+
this.connect({ chainId: this.currentChainId, address });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private async handleDisconnect(
|
|
705
|
+
chainId?: ChainID,
|
|
706
|
+
address?: Address,
|
|
707
|
+
properties?: IFormoEventProperties,
|
|
708
|
+
context?: IFormoEventContext,
|
|
709
|
+
callback?: (...args: unknown[]) => void
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
const payload = {
|
|
712
|
+
chainId: chainId || this.currentChainId,
|
|
713
|
+
address: address || this.currentAddress,
|
|
714
|
+
};
|
|
715
|
+
this.currentChainId = undefined;
|
|
716
|
+
this.currentAddress = undefined;
|
|
717
|
+
cookie().remove(SESSION_USER_ID_KEY);
|
|
718
|
+
|
|
719
|
+
await this.trackEvent(
|
|
720
|
+
EventType.DISCONNECT,
|
|
721
|
+
payload,
|
|
722
|
+
properties,
|
|
723
|
+
context,
|
|
724
|
+
callback
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private async onAddressDisconnected(): Promise<void> {
|
|
729
|
+
await this.handleDisconnect(this.currentChainId, this.currentAddress);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private async onChainChanged(chainIdHex: string): Promise<void> {
|
|
733
|
+
this.currentChainId = parseInt(chainIdHex);
|
|
734
|
+
if (!this.currentAddress) {
|
|
735
|
+
if (!this.provider) {
|
|
736
|
+
logger.info(
|
|
737
|
+
"OnChainChanged: Provider not found. CHAIN_CHANGED not reported"
|
|
738
|
+
);
|
|
739
|
+
return Promise.resolve();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const address = await this.getAddress();
|
|
743
|
+
if (!address) {
|
|
744
|
+
logger.info(
|
|
745
|
+
"OnChainChanged: Unable to fetch or store connected address"
|
|
746
|
+
);
|
|
747
|
+
return Promise.resolve();
|
|
748
|
+
}
|
|
749
|
+
this.currentAddress = address;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Proceed only if the address exists
|
|
753
|
+
if (this.currentAddress) {
|
|
754
|
+
return this.chain({
|
|
755
|
+
chainId: this.currentChainId,
|
|
756
|
+
address: this.currentAddress,
|
|
757
|
+
});
|
|
758
|
+
} else {
|
|
759
|
+
logger.info(
|
|
760
|
+
"OnChainChanged: Current connected address is null despite fetch attempt"
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private async trackFirstPageHit(): Promise<void> {
|
|
766
|
+
if (cookie().get(SESSION_CURRENT_URL_KEY) === null) {
|
|
767
|
+
cookie().set(SESSION_CURRENT_URL_KEY, window.location.href);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return this.trackPageHit();
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private async trackPageHits(): Promise<void> {
|
|
774
|
+
const oldPushState = history.pushState;
|
|
775
|
+
history.pushState = function pushState(...args) {
|
|
776
|
+
const ret = oldPushState.apply(this, args);
|
|
777
|
+
window.dispatchEvent(new window.Event("locationchange"));
|
|
778
|
+
return ret;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const oldReplaceState = history.replaceState;
|
|
782
|
+
history.replaceState = function replaceState(...args) {
|
|
783
|
+
const ret = oldReplaceState.apply(this, args);
|
|
784
|
+
window.dispatchEvent(new window.Event("locationchange"));
|
|
785
|
+
return ret;
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
window.addEventListener("popstate", () => this.onLocationChange());
|
|
789
|
+
window.addEventListener("locationchange", () => this.onLocationChange());
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
private async onLocationChange(): Promise<void> {
|
|
793
|
+
const currentUrl = cookie().get(SESSION_CURRENT_URL_KEY);
|
|
794
|
+
|
|
795
|
+
if (currentUrl !== window.location.href) {
|
|
796
|
+
cookie().set(SESSION_CURRENT_URL_KEY, window.location.href);
|
|
797
|
+
this.trackPageHit();
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
private async trackPageHit(
|
|
802
|
+
category?: string,
|
|
803
|
+
name?: string,
|
|
804
|
+
properties?: IFormoEventProperties,
|
|
805
|
+
context?: IFormoEventContext,
|
|
806
|
+
callback?: (...args: unknown[]) => void
|
|
807
|
+
): Promise<void> {
|
|
808
|
+
if (!this.config.trackLocalhost && isLocalhost()) {
|
|
809
|
+
return logger.warn(
|
|
810
|
+
"Track page hit: Ignoring event because website is running locally"
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
setTimeout(async () => {
|
|
815
|
+
this.trackEvent(
|
|
816
|
+
EventType.PAGE,
|
|
817
|
+
{
|
|
818
|
+
category,
|
|
819
|
+
name,
|
|
820
|
+
},
|
|
821
|
+
properties,
|
|
822
|
+
context,
|
|
823
|
+
callback
|
|
824
|
+
);
|
|
825
|
+
}, 300);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
private async trackEvent(
|
|
829
|
+
type: TEventType,
|
|
830
|
+
payload?: any,
|
|
831
|
+
properties?: IFormoEventProperties,
|
|
832
|
+
context?: IFormoEventContext,
|
|
833
|
+
callback?: (...args: unknown[]) => void
|
|
834
|
+
): Promise<void> {
|
|
835
|
+
try {
|
|
836
|
+
this.eventManager.addEvent(
|
|
837
|
+
{
|
|
838
|
+
type,
|
|
839
|
+
...payload,
|
|
840
|
+
properties,
|
|
841
|
+
context,
|
|
842
|
+
callback,
|
|
843
|
+
},
|
|
844
|
+
this.currentAddress,
|
|
845
|
+
this.currentUserId
|
|
846
|
+
);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
logger.error("Error tracking event:", error);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/*
|
|
853
|
+
Utility functions
|
|
854
|
+
*/
|
|
855
|
+
|
|
856
|
+
private async getProviders(): Promise<readonly EIP6963ProviderDetail[]> {
|
|
857
|
+
const store = createStore();
|
|
858
|
+
let providers = store.getProviders();
|
|
859
|
+
store.subscribe((providerDetails) => {
|
|
860
|
+
providers = providerDetails;
|
|
861
|
+
this._providers = providers;
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
// Fallback to injected provider if no providers are found
|
|
865
|
+
if (providers.length === 0) {
|
|
866
|
+
this._providers = window?.ethereum ? [window.ethereum] : [];
|
|
867
|
+
return this._providers;
|
|
868
|
+
}
|
|
869
|
+
this._providers = providers;
|
|
870
|
+
return providers;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
get providers(): readonly EIP6963ProviderDetail[] {
|
|
874
|
+
return this._providers;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private async detectWallets(
|
|
878
|
+
providers: readonly EIP6963ProviderDetail[]
|
|
879
|
+
): Promise<void> {
|
|
880
|
+
try {
|
|
881
|
+
for (const eip6963ProviderDetail of providers) {
|
|
882
|
+
await this.detect({
|
|
883
|
+
providerName: eip6963ProviderDetail?.info.name,
|
|
884
|
+
rdns: eip6963ProviderDetail?.info.rdns,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
} catch (err) {
|
|
888
|
+
logger.error("Error detect all wallets:", err);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
get provider(): EIP1193Provider | undefined {
|
|
893
|
+
return this._provider;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
private async getAddress(
|
|
897
|
+
provider?: EIP1193Provider
|
|
898
|
+
): Promise<Address | null> {
|
|
899
|
+
if (this.currentAddress) return this.currentAddress;
|
|
900
|
+
const p = provider || this.provider;
|
|
901
|
+
if (!p) {
|
|
902
|
+
logger.info("The provider is not set");
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
const accounts = await this.getAccounts(p);
|
|
908
|
+
if (accounts && accounts.length > 0) {
|
|
909
|
+
if (isAddress(accounts[0])) {
|
|
910
|
+
return accounts[0];
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
} catch (err) {
|
|
914
|
+
logger.error("Failed to fetch accounts from provider:", err);
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private async getAccounts(
|
|
921
|
+
provider?: EIP1193Provider
|
|
922
|
+
): Promise<Address[] | null> {
|
|
923
|
+
const p = provider || this.provider;
|
|
924
|
+
try {
|
|
925
|
+
const res: string[] | null | undefined = await p?.request({
|
|
926
|
+
method: "eth_accounts",
|
|
927
|
+
});
|
|
928
|
+
if (!res || res.length === 0) return null;
|
|
929
|
+
return res.filter((e) => isAddress(e));
|
|
930
|
+
} catch (err) {
|
|
931
|
+
if ((err as any).code !== 4001) {
|
|
932
|
+
logger.error(
|
|
933
|
+
"FormoAnalytics::getAccounts: eth_accounts threw an error",
|
|
934
|
+
err
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
private async getCurrentChainId(): Promise<number> {
|
|
942
|
+
if (!this.provider) {
|
|
943
|
+
logger.error("Provider not set for chain ID");
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let chainIdHex;
|
|
947
|
+
try {
|
|
948
|
+
chainIdHex = await this.provider?.request<string>({
|
|
949
|
+
method: "eth_chainId",
|
|
950
|
+
});
|
|
951
|
+
if (!chainIdHex) {
|
|
952
|
+
logger.info("Chain id not found");
|
|
953
|
+
return 0;
|
|
954
|
+
}
|
|
955
|
+
return parseInt(chainIdHex as string, 16);
|
|
956
|
+
} catch (err) {
|
|
957
|
+
logger.error("eth_chainId threw an error:", err);
|
|
958
|
+
return 0;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
private buildSignatureEventPayload(
|
|
963
|
+
method: string,
|
|
964
|
+
params: unknown[],
|
|
965
|
+
response?: unknown
|
|
966
|
+
) {
|
|
967
|
+
const basePayload = {
|
|
968
|
+
chainId: this.currentChainId,
|
|
969
|
+
address:
|
|
970
|
+
method === "personal_sign"
|
|
971
|
+
? (params[1] as Address)
|
|
972
|
+
: (params[0] as Address),
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
if (method === "personal_sign") {
|
|
976
|
+
const message = Buffer.from(
|
|
977
|
+
(params[0] as string).slice(2),
|
|
978
|
+
"hex"
|
|
979
|
+
).toString("utf8");
|
|
980
|
+
return {
|
|
981
|
+
...basePayload,
|
|
982
|
+
message,
|
|
983
|
+
...(response ? { signatureHash: response as string } : {}),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return {
|
|
988
|
+
...basePayload,
|
|
989
|
+
message: params[1] as string,
|
|
990
|
+
...(response ? { signatureHash: response as string } : {}),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
private async buildTransactionEventPayload(params: unknown[]) {
|
|
995
|
+
const { data, from, to, value } = params[0] as {
|
|
996
|
+
data: string;
|
|
997
|
+
from: string;
|
|
998
|
+
to: string;
|
|
999
|
+
value: string;
|
|
1000
|
+
};
|
|
1001
|
+
return {
|
|
1002
|
+
chainId: this.currentChainId || (await this.getCurrentChainId()),
|
|
1003
|
+
data,
|
|
1004
|
+
address: from,
|
|
1005
|
+
to,
|
|
1006
|
+
value,
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
interface IFormoAnalyticsSession {
|
|
1012
|
+
isWalletDetected(rdns: string): boolean;
|
|
1013
|
+
markWalletDetected(rdns: string): void;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
class FormoAnalyticsSession implements IFormoAnalyticsSession {
|
|
1017
|
+
public isWalletDetected(rdns: string): boolean {
|
|
1018
|
+
const rdnses = cookie().get(SESSION_WALLET_DETECTED_KEY)?.split(",") || [];
|
|
1019
|
+
return rdnses.includes(rdns);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
public markWalletDetected(rdns: string): void {
|
|
1023
|
+
const rdnses = cookie().get(SESSION_WALLET_DETECTED_KEY)?.split(",") || [];
|
|
1024
|
+
rdnses.push(rdns);
|
|
1025
|
+
cookie().set(SESSION_WALLET_DETECTED_KEY, rdnses.join(","), {
|
|
1026
|
+
// by the end of the day
|
|
1027
|
+
expires: new Date(Date.now() + 86400 * 1000).toUTCString(),
|
|
1028
|
+
path: "/",
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|