@datalyr/react-native 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +24 -141
  2. package/LICENSE +21 -0
  3. package/README.md +405 -217
  4. package/datalyr-react-native.podspec +31 -0
  5. package/ios/DatalyrNative.m +70 -0
  6. package/ios/DatalyrNative.swift +275 -0
  7. package/ios/DatalyrSKAdNetwork.m +26 -0
  8. package/lib/datalyr-sdk.d.ts +64 -3
  9. package/lib/datalyr-sdk.js +322 -3
  10. package/lib/index.d.ts +1 -0
  11. package/lib/index.js +4 -2
  12. package/lib/integrations/index.d.ts +6 -0
  13. package/lib/integrations/index.js +6 -0
  14. package/lib/integrations/meta-integration.d.ts +76 -0
  15. package/lib/integrations/meta-integration.js +218 -0
  16. package/lib/integrations/tiktok-integration.d.ts +82 -0
  17. package/lib/integrations/tiktok-integration.js +356 -0
  18. package/lib/native/DatalyrNativeBridge.d.ts +31 -0
  19. package/lib/native/DatalyrNativeBridge.js +168 -0
  20. package/lib/native/index.d.ts +5 -0
  21. package/lib/native/index.js +5 -0
  22. package/lib/types.d.ts +29 -0
  23. package/package.json +10 -5
  24. package/src/datalyr-sdk-expo.ts +957 -0
  25. package/src/datalyr-sdk.ts +419 -19
  26. package/src/expo.ts +38 -18
  27. package/src/index.ts +5 -2
  28. package/src/integrations/index.ts +7 -0
  29. package/src/integrations/meta-integration.ts +238 -0
  30. package/src/integrations/tiktok-integration.ts +360 -0
  31. package/src/native/DatalyrNativeBridge.ts +271 -0
  32. package/src/native/index.ts +11 -0
  33. package/src/types.ts +39 -0
  34. package/src/utils-expo.ts +25 -3
  35. package/src/utils-interface.ts +38 -0
  36. package/EXPO_INSTALL.md +0 -297
  37. package/INSTALL.md +0 -402
  38. package/examples/attribution-example.tsx +0 -377
  39. package/examples/auto-events-example.tsx +0 -403
  40. package/examples/example.tsx +0 -250
  41. package/examples/skadnetwork-example.tsx +0 -380
  42. package/examples/test-implementation.tsx +0 -163
@@ -0,0 +1,957 @@
1
+ /**
2
+ * Expo-specific SDK implementation
3
+ * This module re-exports the main SDK but configured to use Expo utilities
4
+ */
5
+
6
+ import { Platform, AppState } from 'react-native';
7
+ import {
8
+ DatalyrConfig,
9
+ EventData,
10
+ UserProperties,
11
+ EventPayload,
12
+ SDKState,
13
+ AutoEventConfig,
14
+ } from './types';
15
+ import {
16
+ getOrCreateVisitorId,
17
+ getOrCreateAnonymousId,
18
+ getOrCreateSessionId,
19
+ createFingerprintData,
20
+ generateUUID,
21
+ getDeviceInfo,
22
+ getNetworkType,
23
+ validateEventName,
24
+ validateEventData,
25
+ debugLog,
26
+ errorLog,
27
+ Storage,
28
+ STORAGE_KEYS,
29
+ } from './utils-expo'; // <-- KEY DIFFERENCE: uses Expo utilities
30
+ import { createHttpClient, HttpClient } from './http-client';
31
+ import { createEventQueue, EventQueue } from './event-queue';
32
+ import { attributionManager, AttributionData } from './attribution';
33
+ import { createAutoEventsManager, AutoEventsManager } from './auto-events';
34
+ import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
35
+ import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
36
+ import { metaIntegration, tiktokIntegration } from './integrations';
37
+ import { DeferredDeepLinkResult } from './types';
38
+
39
+ export class DatalyrSDKExpo {
40
+ private state: SDKState;
41
+ private httpClient: HttpClient;
42
+ private eventQueue: EventQueue;
43
+ private autoEventsManager: AutoEventsManager | null = null;
44
+ private appStateSubscription: any = null;
45
+ private static conversionEncoder?: ConversionValueEncoder;
46
+ private static debugEnabled = false;
47
+
48
+ constructor() {
49
+ this.state = {
50
+ initialized: false,
51
+ config: {
52
+ workspaceId: '',
53
+ apiKey: '',
54
+ debug: false,
55
+ endpoint: 'https://api.datalyr.com',
56
+ useServerTracking: true,
57
+ maxRetries: 3,
58
+ retryDelay: 1000,
59
+ batchSize: 10,
60
+ flushInterval: 10000,
61
+ maxQueueSize: 100,
62
+ respectDoNotTrack: true,
63
+ },
64
+ visitorId: '',
65
+ anonymousId: '',
66
+ sessionId: '',
67
+ userProperties: {},
68
+ eventQueue: [],
69
+ isOnline: true,
70
+ };
71
+
72
+ this.httpClient = createHttpClient(this.state.config.endpoint!);
73
+ this.eventQueue = createEventQueue(this.httpClient);
74
+ }
75
+
76
+ async initialize(config: DatalyrConfig): Promise<void> {
77
+ try {
78
+ debugLog('Initializing Datalyr SDK (Expo)...', { workspaceId: config.workspaceId });
79
+
80
+ if (!config.apiKey) {
81
+ throw new Error('apiKey is required for Datalyr SDK');
82
+ }
83
+
84
+ if (!config.workspaceId) {
85
+ debugLog('workspaceId not provided, using server-side tracking only');
86
+ }
87
+
88
+ this.state.config = { ...this.state.config, ...config };
89
+
90
+ this.httpClient = new HttpClient(this.state.config.endpoint || 'https://api.datalyr.com', {
91
+ maxRetries: this.state.config.maxRetries || 3,
92
+ retryDelay: this.state.config.retryDelay || 1000,
93
+ timeout: this.state.config.timeout || 15000,
94
+ apiKey: this.state.config.apiKey!,
95
+ workspaceId: this.state.config.workspaceId,
96
+ debug: this.state.config.debug || false,
97
+ useServerTracking: this.state.config.useServerTracking ?? true,
98
+ });
99
+
100
+ this.eventQueue = new EventQueue(this.httpClient, {
101
+ maxQueueSize: this.state.config.maxQueueSize || 100,
102
+ batchSize: this.state.config.batchSize || 10,
103
+ flushInterval: this.state.config.flushInterval || 30000,
104
+ maxRetryCount: this.state.config.maxRetries || 3,
105
+ });
106
+
107
+ this.state.visitorId = await getOrCreateVisitorId();
108
+ this.state.anonymousId = await getOrCreateAnonymousId();
109
+ this.state.sessionId = await getOrCreateSessionId();
110
+
111
+ await this.loadPersistedUserData();
112
+
113
+ if (this.state.config.enableAttribution) {
114
+ await attributionManager.initialize();
115
+ }
116
+
117
+ if (this.state.config.enableAutoEvents) {
118
+ this.autoEventsManager = new AutoEventsManager(
119
+ this.track.bind(this),
120
+ this.state.config.autoEventConfig
121
+ );
122
+
123
+ setTimeout(async () => {
124
+ try {
125
+ await this.autoEventsManager?.initialize();
126
+ } catch (error) {
127
+ errorLog('Error initializing auto-events:', error as Error);
128
+ }
129
+ }, 100);
130
+ }
131
+
132
+ setTimeout(() => {
133
+ try {
134
+ this.setupAppStateMonitoring();
135
+ } catch (error) {
136
+ errorLog('Error setting up app state monitoring:', error as Error);
137
+ }
138
+ }, 50);
139
+
140
+ if (config.skadTemplate) {
141
+ const template = ConversionTemplates[config.skadTemplate];
142
+ if (template) {
143
+ DatalyrSDKExpo.conversionEncoder = new ConversionValueEncoder(template);
144
+ DatalyrSDKExpo.debugEnabled = config.debug || false;
145
+
146
+ if (DatalyrSDKExpo.debugEnabled) {
147
+ debugLog(`SKAdNetwork encoder initialized with template: ${config.skadTemplate}`);
148
+ }
149
+ }
150
+ }
151
+
152
+ // Initialize platform SDKs (Meta/TikTok) if configured
153
+ if (config.meta) {
154
+ try {
155
+ await metaIntegration.initialize(config.meta, config.debug || false);
156
+ debugLog('Meta SDK initialized');
157
+
158
+ // Fetch deferred deep link data
159
+ if (config.meta.enableDeferredDeepLink) {
160
+ const deferredData = await metaIntegration.fetchDeferredDeepLink();
161
+ if (deferredData) {
162
+ await this.handleDeferredDeepLink(deferredData);
163
+ }
164
+ }
165
+ } catch (error) {
166
+ errorLog('Failed to initialize Meta SDK:', error as Error);
167
+ }
168
+ }
169
+
170
+ if (config.tiktok) {
171
+ try {
172
+ await tiktokIntegration.initialize(config.tiktok, config.debug || false);
173
+ debugLog('TikTok SDK initialized');
174
+ } catch (error) {
175
+ errorLog('Failed to initialize TikTok SDK:', error as Error);
176
+ }
177
+ }
178
+
179
+ this.state.initialized = true;
180
+
181
+ if (attributionManager.isInstall()) {
182
+ const installData = await attributionManager.trackInstall();
183
+ await this.track('app_install', {
184
+ platform: Platform.OS,
185
+ sdk_version: '1.1.0',
186
+ sdk_variant: 'expo',
187
+ ...installData,
188
+ });
189
+ }
190
+
191
+ debugLog('Datalyr SDK (Expo) initialized successfully', {
192
+ workspaceId: this.state.config.workspaceId,
193
+ visitorId: this.state.visitorId,
194
+ anonymousId: this.state.anonymousId,
195
+ sessionId: this.state.sessionId,
196
+ });
197
+
198
+ } catch (error) {
199
+ errorLog('Failed to initialize Datalyr SDK:', error as Error);
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ async track(eventName: string, eventData?: EventData): Promise<void> {
205
+ try {
206
+ if (!this.state.initialized) {
207
+ errorLog('SDK not initialized. Call initialize() first.');
208
+ return;
209
+ }
210
+
211
+ if (!validateEventName(eventName)) {
212
+ errorLog(`Invalid event name: ${eventName}`);
213
+ return;
214
+ }
215
+
216
+ if (!validateEventData(eventData)) {
217
+ errorLog('Invalid event data provided');
218
+ return;
219
+ }
220
+
221
+ debugLog(`Tracking event: ${eventName}`, eventData);
222
+
223
+ const payload = await this.createEventPayload(eventName, eventData);
224
+ await this.eventQueue.enqueue(payload);
225
+
226
+ } catch (error) {
227
+ errorLog(`Error tracking event ${eventName}:`, error as Error);
228
+ }
229
+ }
230
+
231
+ async screen(screenName: string, properties?: EventData): Promise<void> {
232
+ const screenData: EventData = {
233
+ screen: screenName,
234
+ ...properties,
235
+ };
236
+
237
+ await this.track('pageview', screenData);
238
+
239
+ if (this.autoEventsManager) {
240
+ await this.autoEventsManager.trackScreenView(screenName, properties);
241
+ }
242
+ }
243
+
244
+ async identify(userId: string, properties?: UserProperties): Promise<void> {
245
+ try {
246
+ if (!userId || typeof userId !== 'string') {
247
+ errorLog(`Invalid user ID for identify: ${userId}`);
248
+ return;
249
+ }
250
+
251
+ debugLog('Identifying user:', { userId, properties });
252
+
253
+ this.state.currentUserId = userId;
254
+ this.state.userProperties = { ...this.state.userProperties, ...properties };
255
+
256
+ await this.persistUserData();
257
+
258
+ await this.track('$identify', {
259
+ userId,
260
+ anonymous_id: this.state.anonymousId,
261
+ ...properties
262
+ });
263
+
264
+ if (this.state.config.enableWebToAppAttribution !== false) {
265
+ const email = properties?.email || (typeof userId === 'string' && userId.includes('@') ? userId : null);
266
+ if (email) {
267
+ await this.fetchAndMergeWebAttribution(email);
268
+ }
269
+ }
270
+
271
+ // Forward user data to platform SDKs for Advanced Matching
272
+ if (metaIntegration.isAvailable() && properties) {
273
+ metaIntegration.setUserData({
274
+ email: properties.email,
275
+ phone: properties.phone,
276
+ firstName: properties.firstName || properties.first_name,
277
+ lastName: properties.lastName || properties.last_name,
278
+ city: properties.city,
279
+ state: properties.state,
280
+ zip: properties.zip || properties.zipCode || properties.postalCode,
281
+ country: properties.country,
282
+ gender: properties.gender,
283
+ dateOfBirth: properties.dateOfBirth || properties.dob || properties.birthday,
284
+ });
285
+ }
286
+
287
+ if (tiktokIntegration.isAvailable()) {
288
+ tiktokIntegration.identify(
289
+ properties?.email,
290
+ properties?.phone,
291
+ userId
292
+ );
293
+ }
294
+
295
+ } catch (error) {
296
+ errorLog('Error identifying user:', error as Error);
297
+ }
298
+ }
299
+
300
+ private async fetchAndMergeWebAttribution(email: string): Promise<void> {
301
+ try {
302
+ debugLog('Fetching web attribution for email:', email);
303
+
304
+ const response = await fetch('https://api.datalyr.com/attribution/lookup', {
305
+ method: 'POST',
306
+ headers: {
307
+ 'Content-Type': 'application/json',
308
+ 'X-Datalyr-API-Key': this.state.config.apiKey!,
309
+ },
310
+ body: JSON.stringify({ email }),
311
+ });
312
+
313
+ if (!response.ok) {
314
+ debugLog('Failed to fetch web attribution:', response.status);
315
+ return;
316
+ }
317
+
318
+ const result = await response.json() as { found: boolean; attribution?: any };
319
+
320
+ if (!result.found || !result.attribution) {
321
+ debugLog('No web attribution found for user');
322
+ return;
323
+ }
324
+
325
+ const webAttribution = result.attribution;
326
+ debugLog('Web attribution found:', {
327
+ visitor_id: webAttribution.visitor_id,
328
+ has_fbclid: !!webAttribution.fbclid,
329
+ has_gclid: !!webAttribution.gclid,
330
+ utm_source: webAttribution.utm_source,
331
+ });
332
+
333
+ await this.track('$web_attribution_merged', {
334
+ web_visitor_id: webAttribution.visitor_id,
335
+ web_user_id: webAttribution.user_id,
336
+ fbclid: webAttribution.fbclid,
337
+ gclid: webAttribution.gclid,
338
+ ttclid: webAttribution.ttclid,
339
+ gbraid: webAttribution.gbraid,
340
+ wbraid: webAttribution.wbraid,
341
+ fbp: webAttribution.fbp,
342
+ fbc: webAttribution.fbc,
343
+ utm_source: webAttribution.utm_source,
344
+ utm_medium: webAttribution.utm_medium,
345
+ utm_campaign: webAttribution.utm_campaign,
346
+ utm_content: webAttribution.utm_content,
347
+ utm_term: webAttribution.utm_term,
348
+ web_timestamp: webAttribution.timestamp,
349
+ });
350
+
351
+ attributionManager.mergeWebAttribution(webAttribution);
352
+
353
+ debugLog('Successfully merged web attribution into mobile session');
354
+
355
+ } catch (error) {
356
+ errorLog('Error fetching web attribution:', error as Error);
357
+ }
358
+ }
359
+
360
+ async alias(newUserId: string, previousId?: string): Promise<void> {
361
+ try {
362
+ if (!newUserId || typeof newUserId !== 'string') {
363
+ errorLog(`Invalid user ID for alias: ${newUserId}`);
364
+ return;
365
+ }
366
+
367
+ const aliasData = {
368
+ newUserId,
369
+ previousId: previousId || this.state.visitorId,
370
+ visitorId: this.state.visitorId,
371
+ anonymousId: this.state.anonymousId,
372
+ };
373
+
374
+ debugLog('Aliasing user:', aliasData);
375
+
376
+ await this.track('alias', aliasData);
377
+ await this.identify(newUserId);
378
+
379
+ } catch (error) {
380
+ errorLog('Error aliasing user:', error as Error);
381
+ }
382
+ }
383
+
384
+ async reset(): Promise<void> {
385
+ try {
386
+ debugLog('Resetting user data');
387
+
388
+ this.state.currentUserId = undefined;
389
+ this.state.userProperties = {};
390
+
391
+ await Storage.removeItem(STORAGE_KEYS.USER_ID);
392
+ await Storage.removeItem(STORAGE_KEYS.USER_PROPERTIES);
393
+
394
+ this.state.sessionId = await getOrCreateSessionId();
395
+
396
+ // Clear user data from platform SDKs
397
+ if (metaIntegration.isAvailable()) {
398
+ metaIntegration.clearUserData();
399
+ }
400
+
401
+ debugLog('User data reset completed');
402
+
403
+ } catch (error) {
404
+ errorLog('Error resetting user data:', error as Error);
405
+ }
406
+ }
407
+
408
+ async flush(): Promise<void> {
409
+ try {
410
+ debugLog('Flushing events...');
411
+ await this.eventQueue.flush();
412
+ } catch (error) {
413
+ errorLog('Error flushing events:', error as Error);
414
+ }
415
+ }
416
+
417
+ getStatus() {
418
+ return {
419
+ initialized: this.state.initialized,
420
+ workspaceId: this.state.config.workspaceId || '',
421
+ visitorId: this.state.visitorId,
422
+ anonymousId: this.state.anonymousId,
423
+ sessionId: this.state.sessionId,
424
+ currentUserId: this.state.currentUserId,
425
+ queueStats: this.eventQueue.getStats(),
426
+ attribution: attributionManager.getAttributionSummary(),
427
+ variant: 'expo',
428
+ };
429
+ }
430
+
431
+ getAnonymousId(): string {
432
+ return this.state.anonymousId;
433
+ }
434
+
435
+ getAttributionData(): AttributionData {
436
+ return attributionManager.getAttributionData();
437
+ }
438
+
439
+ async setAttributionData(data: Partial<AttributionData>): Promise<void> {
440
+ await attributionManager.setAttributionData(data);
441
+ }
442
+
443
+ getCurrentSession() {
444
+ return this.autoEventsManager?.getCurrentSession() || null;
445
+ }
446
+
447
+ async endSession(): Promise<void> {
448
+ if (this.autoEventsManager) {
449
+ await this.autoEventsManager.forceEndSession();
450
+ }
451
+ }
452
+
453
+ async trackAppUpdate(previousVersion: string, currentVersion: string): Promise<void> {
454
+ if (this.autoEventsManager) {
455
+ await this.autoEventsManager.trackAppUpdate(previousVersion, currentVersion);
456
+ }
457
+ }
458
+
459
+ async trackRevenue(eventName: string, properties?: EventData): Promise<void> {
460
+ if (this.autoEventsManager) {
461
+ await this.autoEventsManager.trackRevenueEvent(eventName, properties);
462
+ }
463
+ }
464
+
465
+ updateAutoEventsConfig(config: Partial<AutoEventConfig>): void {
466
+ if (this.autoEventsManager) {
467
+ this.autoEventsManager.updateConfig(config);
468
+ }
469
+ }
470
+
471
+ async trackWithSKAdNetwork(event: string, properties?: EventData): Promise<void> {
472
+ await this.track(event, properties);
473
+
474
+ if (!DatalyrSDKExpo.conversionEncoder) {
475
+ if (DatalyrSDKExpo.debugEnabled) {
476
+ errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
477
+ }
478
+ return;
479
+ }
480
+
481
+ const conversionValue = DatalyrSDKExpo.conversionEncoder.encode(event, properties);
482
+
483
+ if (conversionValue > 0) {
484
+ const success = await SKAdNetworkBridge.updateConversionValue(conversionValue);
485
+
486
+ if (DatalyrSDKExpo.debugEnabled) {
487
+ debugLog(`Event: ${event}, Conversion Value: ${conversionValue}, Success: ${success}`, properties);
488
+ }
489
+ }
490
+ }
491
+
492
+ async trackPurchase(value: number, currency = 'USD', productId?: string): Promise<void> {
493
+ const properties: Record<string, any> = { revenue: value, currency };
494
+ if (productId) properties.product_id = productId;
495
+
496
+ await this.trackWithSKAdNetwork('purchase', properties);
497
+
498
+ // Forward to platform SDKs
499
+ if (metaIntegration.isAvailable()) {
500
+ metaIntegration.logPurchase(value, currency, { productId });
501
+ }
502
+ if (tiktokIntegration.isAvailable()) {
503
+ tiktokIntegration.logPurchase(value, currency, productId);
504
+ }
505
+ }
506
+
507
+ async trackSubscription(value: number, currency = 'USD', plan?: string): Promise<void> {
508
+ const properties: Record<string, any> = { revenue: value, currency };
509
+ if (plan) properties.plan = plan;
510
+
511
+ await this.trackWithSKAdNetwork('subscribe', properties);
512
+
513
+ // Forward to platform SDKs
514
+ if (metaIntegration.isAvailable()) {
515
+ metaIntegration.logEvent('Subscribe', { value, currency, content_id: plan });
516
+ }
517
+ if (tiktokIntegration.isAvailable()) {
518
+ tiktokIntegration.logSubscription(value, currency, plan);
519
+ }
520
+ }
521
+
522
+ // Standard e-commerce events with platform forwarding
523
+
524
+ async trackAddToCart(value: number, currency: string, contentId?: string, contentName?: string): Promise<void> {
525
+ const properties: Record<string, any> = { value, currency };
526
+ if (contentId) properties.content_id = contentId;
527
+ if (contentName) properties.content_name = contentName;
528
+
529
+ await this.track('add_to_cart', properties);
530
+
531
+ if (metaIntegration.isAvailable()) {
532
+ metaIntegration.logEvent('AddToCart', { value, currency, content_id: contentId, content_name: contentName });
533
+ }
534
+ if (tiktokIntegration.isAvailable()) {
535
+ tiktokIntegration.logAddToCart(value, currency, contentId, contentName);
536
+ }
537
+ }
538
+
539
+ async trackViewContent(contentId: string, contentName?: string, contentType?: string, value?: number, currency?: string): Promise<void> {
540
+ const properties: Record<string, any> = { content_id: contentId };
541
+ if (contentName) properties.content_name = contentName;
542
+ if (contentType) properties.content_type = contentType;
543
+ if (value !== undefined) properties.value = value;
544
+ if (currency) properties.currency = currency;
545
+
546
+ await this.track('view_content', properties);
547
+
548
+ if (metaIntegration.isAvailable()) {
549
+ metaIntegration.logEvent('ViewContent', properties);
550
+ }
551
+ if (tiktokIntegration.isAvailable()) {
552
+ tiktokIntegration.logViewContent(contentId, contentName, contentType, value, currency);
553
+ }
554
+ }
555
+
556
+ async trackInitiateCheckout(value?: number, currency?: string, numItems?: number, contentIds?: string[]): Promise<void> {
557
+ const properties: Record<string, any> = {};
558
+ if (value !== undefined) properties.value = value;
559
+ if (currency) properties.currency = currency;
560
+ if (numItems !== undefined) properties.num_items = numItems;
561
+ if (contentIds) properties.content_ids = contentIds;
562
+
563
+ await this.track('initiate_checkout', properties);
564
+
565
+ if (metaIntegration.isAvailable()) {
566
+ metaIntegration.logEvent('InitiateCheckout', properties);
567
+ }
568
+ if (tiktokIntegration.isAvailable()) {
569
+ tiktokIntegration.logInitiateCheckout(value, currency, numItems, contentIds);
570
+ }
571
+ }
572
+
573
+ async trackCompleteRegistration(registrationMethod?: string): Promise<void> {
574
+ const properties: Record<string, any> = {};
575
+ if (registrationMethod) properties.registration_method = registrationMethod;
576
+
577
+ await this.track('complete_registration', properties);
578
+
579
+ if (metaIntegration.isAvailable()) {
580
+ metaIntegration.logEvent('CompleteRegistration', properties);
581
+ }
582
+ if (tiktokIntegration.isAvailable()) {
583
+ tiktokIntegration.logCompleteRegistration(registrationMethod);
584
+ }
585
+ }
586
+
587
+ async trackSearch(searchString: string, contentIds?: string[]): Promise<void> {
588
+ const properties: Record<string, any> = { search_string: searchString };
589
+ if (contentIds) properties.content_ids = contentIds;
590
+
591
+ await this.track('search', properties);
592
+
593
+ if (metaIntegration.isAvailable()) {
594
+ metaIntegration.logEvent('Search', properties);
595
+ }
596
+ if (tiktokIntegration.isAvailable()) {
597
+ tiktokIntegration.logSearch(searchString, contentIds);
598
+ }
599
+ }
600
+
601
+ async trackLead(value?: number, currency?: string): Promise<void> {
602
+ const properties: Record<string, any> = {};
603
+ if (value !== undefined) properties.value = value;
604
+ if (currency) properties.currency = currency;
605
+
606
+ await this.track('lead', properties);
607
+
608
+ if (metaIntegration.isAvailable()) {
609
+ metaIntegration.logEvent('Lead', properties);
610
+ }
611
+ if (tiktokIntegration.isAvailable()) {
612
+ tiktokIntegration.logLead(value, currency);
613
+ }
614
+ }
615
+
616
+ async trackAddPaymentInfo(success?: boolean): Promise<void> {
617
+ const properties: Record<string, any> = {};
618
+ if (success !== undefined) properties.success = success;
619
+
620
+ await this.track('add_payment_info', properties);
621
+
622
+ if (metaIntegration.isAvailable()) {
623
+ metaIntegration.logEvent('AddPaymentInfo', properties);
624
+ }
625
+ if (tiktokIntegration.isAvailable()) {
626
+ tiktokIntegration.logAddPaymentInfo(success);
627
+ }
628
+ }
629
+
630
+ // Platform integration methods
631
+
632
+ getDeferredAttributionData(): DeferredDeepLinkResult | null {
633
+ if (metaIntegration.isAvailable()) {
634
+ return metaIntegration.getDeferredDeepLinkData();
635
+ }
636
+ return null;
637
+ }
638
+
639
+ getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
640
+ return {
641
+ meta: metaIntegration.isAvailable(),
642
+ tiktok: tiktokIntegration.isAvailable(),
643
+ };
644
+ }
645
+
646
+ async updateTrackingAuthorization(authorized: boolean): Promise<void> {
647
+ if (metaIntegration.isAvailable()) {
648
+ metaIntegration.updateTrackingAuthorization(authorized);
649
+ }
650
+ if (tiktokIntegration.isAvailable()) {
651
+ tiktokIntegration.updateTrackingAuthorization(authorized);
652
+ }
653
+ }
654
+
655
+ private async handleDeferredDeepLink(data: DeferredDeepLinkResult): Promise<void> {
656
+ debugLog('Handling deferred deep link:', data);
657
+
658
+ // Store attribution data
659
+ if (data.fbclid || data.utmSource || data.campaignId) {
660
+ await attributionManager.setAttributionData({
661
+ fbclid: data.fbclid,
662
+ ttclid: data.ttclid,
663
+ utm_source: data.utmSource,
664
+ utm_medium: data.utmMedium,
665
+ utm_campaign: data.utmCampaign,
666
+ utm_content: data.utmContent,
667
+ utm_term: data.utmTerm,
668
+ source: data.source,
669
+ });
670
+ }
671
+
672
+ // Track the deferred deep link event
673
+ await this.track('$deferred_deep_link', {
674
+ url: data.url,
675
+ source: data.source,
676
+ fbclid: data.fbclid,
677
+ ttclid: data.ttclid,
678
+ utm_source: data.utmSource,
679
+ utm_medium: data.utmMedium,
680
+ utm_campaign: data.utmCampaign,
681
+ campaign_id: data.campaignId,
682
+ adset_id: data.adsetId,
683
+ ad_id: data.adId,
684
+ });
685
+ }
686
+
687
+ getConversionValue(event: string, properties?: Record<string, any>): number | null {
688
+ return DatalyrSDKExpo.conversionEncoder?.encode(event, properties) || null;
689
+ }
690
+
691
+ private async createEventPayload(eventName: string, eventData?: EventData): Promise<EventPayload> {
692
+ const deviceInfo = await getDeviceInfo();
693
+ const fingerprintData = await createFingerprintData();
694
+ const attributionData = attributionManager.getAttributionData();
695
+ const networkType = await getNetworkType();
696
+
697
+ const payload: EventPayload = {
698
+ workspaceId: this.state.config.workspaceId || 'mobile_sdk',
699
+ visitorId: this.state.visitorId,
700
+ anonymousId: this.state.anonymousId,
701
+ sessionId: this.state.sessionId,
702
+ eventId: generateUUID(),
703
+ eventName,
704
+ eventData: {
705
+ ...eventData,
706
+ anonymous_id: this.state.anonymousId,
707
+ platform: Platform.OS,
708
+ os_version: deviceInfo.osVersion,
709
+ device_model: deviceInfo.model,
710
+ app_version: deviceInfo.appVersion,
711
+ app_build: deviceInfo.buildNumber,
712
+ network_type: networkType,
713
+ timestamp: Date.now(),
714
+ sdk_variant: 'expo',
715
+ ...attributionData,
716
+ },
717
+ fingerprintData,
718
+ source: 'mobile_app',
719
+ timestamp: new Date().toISOString(),
720
+ };
721
+
722
+ if (this.state.currentUserId) {
723
+ payload.userId = this.state.currentUserId;
724
+ payload.eventData!.userId = this.state.currentUserId;
725
+ }
726
+
727
+ if (Object.keys(this.state.userProperties).length > 0) {
728
+ payload.userProperties = this.state.userProperties;
729
+ }
730
+
731
+ return payload;
732
+ }
733
+
734
+ private async loadPersistedUserData(): Promise<void> {
735
+ try {
736
+ const [userId, userProperties] = await Promise.all([
737
+ Storage.getItem<string>(STORAGE_KEYS.USER_ID),
738
+ Storage.getItem<UserProperties>(STORAGE_KEYS.USER_PROPERTIES),
739
+ ]);
740
+
741
+ if (userId) {
742
+ this.state.currentUserId = userId;
743
+ }
744
+
745
+ if (userProperties) {
746
+ this.state.userProperties = userProperties;
747
+ }
748
+
749
+ debugLog('Loaded persisted user data:', {
750
+ userId: this.state.currentUserId,
751
+ userProperties: this.state.userProperties,
752
+ });
753
+
754
+ } catch (error) {
755
+ errorLog('Error loading persisted user data:', error as Error);
756
+ }
757
+ }
758
+
759
+ private async persistUserData(): Promise<void> {
760
+ try {
761
+ await Promise.all([
762
+ this.state.currentUserId
763
+ ? Storage.setItem(STORAGE_KEYS.USER_ID, this.state.currentUserId)
764
+ : Storage.removeItem(STORAGE_KEYS.USER_ID),
765
+ Storage.setItem(STORAGE_KEYS.USER_PROPERTIES, this.state.userProperties),
766
+ ]);
767
+ } catch (error) {
768
+ errorLog('Error persisting user data:', error as Error);
769
+ }
770
+ }
771
+
772
+ private setupAppStateMonitoring(): void {
773
+ try {
774
+ this.appStateSubscription = AppState.addEventListener('change', (nextAppState) => {
775
+ debugLog('App state changed:', nextAppState);
776
+
777
+ if (nextAppState === 'background') {
778
+ this.flush();
779
+ if (this.autoEventsManager) {
780
+ this.autoEventsManager.handleAppBackground();
781
+ }
782
+ } else if (nextAppState === 'active') {
783
+ this.refreshSession();
784
+ if (this.autoEventsManager) {
785
+ this.autoEventsManager.handleAppForeground();
786
+ }
787
+ }
788
+ });
789
+
790
+ } catch (error) {
791
+ errorLog('Error setting up app state monitoring:', error as Error);
792
+ }
793
+ }
794
+
795
+ private async refreshSession(): Promise<void> {
796
+ try {
797
+ const newSessionId = await getOrCreateSessionId();
798
+ if (newSessionId !== this.state.sessionId) {
799
+ this.state.sessionId = newSessionId;
800
+ debugLog('Session refreshed:', newSessionId);
801
+ }
802
+ } catch (error) {
803
+ errorLog('Error refreshing session:', error as Error);
804
+ }
805
+ }
806
+
807
+ destroy(): void {
808
+ try {
809
+ debugLog('Destroying Datalyr SDK (Expo)');
810
+
811
+ if (this.appStateSubscription) {
812
+ this.appStateSubscription.remove();
813
+ this.appStateSubscription = null;
814
+ }
815
+
816
+ this.eventQueue.destroy();
817
+ this.state.initialized = false;
818
+
819
+ debugLog('Datalyr SDK (Expo) destroyed');
820
+
821
+ } catch (error) {
822
+ errorLog('Error destroying SDK:', error as Error);
823
+ }
824
+ }
825
+ }
826
+
827
+ // Create singleton instance for Expo
828
+ const datalyrExpo = new DatalyrSDKExpo();
829
+
830
+ // Export static class wrapper for Expo
831
+ export class DatalyrExpo {
832
+ static async initialize(config: DatalyrConfig): Promise<void> {
833
+ await datalyrExpo.initialize(config);
834
+ }
835
+
836
+ static async trackWithSKAdNetwork(event: string, properties?: Record<string, any>): Promise<void> {
837
+ await datalyrExpo.trackWithSKAdNetwork(event, properties);
838
+ }
839
+
840
+ static async trackPurchase(value: number, currency = 'USD', productId?: string): Promise<void> {
841
+ await datalyrExpo.trackPurchase(value, currency, productId);
842
+ }
843
+
844
+ static async trackSubscription(value: number, currency = 'USD', plan?: string): Promise<void> {
845
+ await datalyrExpo.trackSubscription(value, currency, plan);
846
+ }
847
+
848
+ static getConversionValue(event: string, properties?: Record<string, any>): number | null {
849
+ return datalyrExpo.getConversionValue(event, properties);
850
+ }
851
+
852
+ static async track(eventName: string, eventData?: EventData): Promise<void> {
853
+ await datalyrExpo.track(eventName, eventData);
854
+ }
855
+
856
+ static async screen(screenName: string, properties?: EventData): Promise<void> {
857
+ await datalyrExpo.screen(screenName, properties);
858
+ }
859
+
860
+ static async identify(userId: string, properties?: UserProperties): Promise<void> {
861
+ await datalyrExpo.identify(userId, properties);
862
+ }
863
+
864
+ static async alias(newUserId: string, previousId?: string): Promise<void> {
865
+ await datalyrExpo.alias(newUserId, previousId);
866
+ }
867
+
868
+ static async reset(): Promise<void> {
869
+ await datalyrExpo.reset();
870
+ }
871
+
872
+ static async flush(): Promise<void> {
873
+ await datalyrExpo.flush();
874
+ }
875
+
876
+ static getStatus() {
877
+ return datalyrExpo.getStatus();
878
+ }
879
+
880
+ static getAnonymousId(): string {
881
+ return datalyrExpo.getAnonymousId();
882
+ }
883
+
884
+ static getAttributionData(): AttributionData {
885
+ return datalyrExpo.getAttributionData();
886
+ }
887
+
888
+ static async setAttributionData(data: Partial<AttributionData>): Promise<void> {
889
+ await datalyrExpo.setAttributionData(data);
890
+ }
891
+
892
+ static getCurrentSession() {
893
+ return datalyrExpo.getCurrentSession();
894
+ }
895
+
896
+ static async endSession(): Promise<void> {
897
+ await datalyrExpo.endSession();
898
+ }
899
+
900
+ static async trackAppUpdate(previousVersion: string, currentVersion: string): Promise<void> {
901
+ await datalyrExpo.trackAppUpdate(previousVersion, currentVersion);
902
+ }
903
+
904
+ static async trackRevenue(eventName: string, properties?: EventData): Promise<void> {
905
+ await datalyrExpo.trackRevenue(eventName, properties);
906
+ }
907
+
908
+ static updateAutoEventsConfig(config: Partial<AutoEventConfig>): void {
909
+ datalyrExpo.updateAutoEventsConfig(config);
910
+ }
911
+
912
+ // Standard e-commerce events with platform forwarding
913
+
914
+ static async trackAddToCart(value: number, currency: string, contentId?: string, contentName?: string): Promise<void> {
915
+ await datalyrExpo.trackAddToCart(value, currency, contentId, contentName);
916
+ }
917
+
918
+ static async trackViewContent(contentId: string, contentName?: string, contentType?: string, value?: number, currency?: string): Promise<void> {
919
+ await datalyrExpo.trackViewContent(contentId, contentName, contentType, value, currency);
920
+ }
921
+
922
+ static async trackInitiateCheckout(value?: number, currency?: string, numItems?: number, contentIds?: string[]): Promise<void> {
923
+ await datalyrExpo.trackInitiateCheckout(value, currency, numItems, contentIds);
924
+ }
925
+
926
+ static async trackCompleteRegistration(registrationMethod?: string): Promise<void> {
927
+ await datalyrExpo.trackCompleteRegistration(registrationMethod);
928
+ }
929
+
930
+ static async trackSearch(searchString: string, contentIds?: string[]): Promise<void> {
931
+ await datalyrExpo.trackSearch(searchString, contentIds);
932
+ }
933
+
934
+ static async trackLead(value?: number, currency?: string): Promise<void> {
935
+ await datalyrExpo.trackLead(value, currency);
936
+ }
937
+
938
+ static async trackAddPaymentInfo(success?: boolean): Promise<void> {
939
+ await datalyrExpo.trackAddPaymentInfo(success);
940
+ }
941
+
942
+ // Platform integration methods
943
+
944
+ static getDeferredAttributionData(): DeferredDeepLinkResult | null {
945
+ return datalyrExpo.getDeferredAttributionData();
946
+ }
947
+
948
+ static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
949
+ return datalyrExpo.getPlatformIntegrationStatus();
950
+ }
951
+
952
+ static async updateTrackingAuthorization(authorized: boolean): Promise<void> {
953
+ await datalyrExpo.updateTrackingAuthorization(authorized);
954
+ }
955
+ }
956
+
957
+ export default datalyrExpo;