@datalyr/react-native 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +145 -9
  3. package/android/build.gradle +54 -0
  4. package/android/src/main/AndroidManifest.xml +14 -0
  5. package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +423 -0
  6. package/android/src/main/java/com/datalyr/reactnative/DatalyrPackage.java +30 -0
  7. package/android/src/main/java/com/datalyr/reactnative/DatalyrPlayInstallReferrerModule.java +229 -0
  8. package/datalyr-react-native.podspec +2 -2
  9. package/ios/DatalyrSKAdNetwork.m +400 -1
  10. package/ios/PrivacyInfo.xcprivacy +48 -0
  11. package/lib/ConversionValueEncoder.d.ts +13 -1
  12. package/lib/ConversionValueEncoder.js +57 -23
  13. package/lib/datalyr-sdk.d.ts +31 -2
  14. package/lib/datalyr-sdk.js +138 -30
  15. package/lib/index.d.ts +5 -1
  16. package/lib/index.js +4 -1
  17. package/lib/integrations/index.d.ts +3 -1
  18. package/lib/integrations/index.js +2 -1
  19. package/lib/integrations/meta-integration.d.ts +1 -0
  20. package/lib/integrations/meta-integration.js +4 -3
  21. package/lib/integrations/play-install-referrer.d.ts +78 -0
  22. package/lib/integrations/play-install-referrer.js +166 -0
  23. package/lib/integrations/tiktok-integration.d.ts +1 -0
  24. package/lib/integrations/tiktok-integration.js +4 -3
  25. package/lib/journey.d.ts +106 -0
  26. package/lib/journey.js +258 -0
  27. package/lib/native/DatalyrNativeBridge.d.ts +42 -3
  28. package/lib/native/DatalyrNativeBridge.js +63 -9
  29. package/lib/native/SKAdNetworkBridge.d.ts +142 -0
  30. package/lib/native/SKAdNetworkBridge.js +328 -0
  31. package/lib/network-status.d.ts +84 -0
  32. package/lib/network-status.js +281 -0
  33. package/lib/types.d.ts +51 -0
  34. package/lib/utils.d.ts +6 -1
  35. package/lib/utils.js +52 -2
  36. package/package.json +13 -4
  37. package/src/ConversionValueEncoder.ts +67 -26
  38. package/src/datalyr-sdk-expo.ts +55 -6
  39. package/src/datalyr-sdk.ts +161 -38
  40. package/src/expo.ts +4 -0
  41. package/src/index.ts +7 -1
  42. package/src/integrations/index.ts +3 -1
  43. package/src/integrations/meta-integration.ts +4 -3
  44. package/src/integrations/play-install-referrer.ts +218 -0
  45. package/src/integrations/tiktok-integration.ts +4 -3
  46. package/src/journey.ts +338 -0
  47. package/src/native/DatalyrNativeBridge.ts +99 -13
  48. package/src/native/SKAdNetworkBridge.ts +481 -2
  49. package/src/network-status.ts +312 -0
  50. package/src/types.ts +74 -6
  51. package/src/utils.ts +62 -6
@@ -1,8 +1,12 @@
1
- // Interface definitions
1
+ import { SKANCoarseValue, SKANConversionResult } from './native/SKAdNetworkBridge';
2
+
3
+ // SKAN 4.0 compatible event mapping
2
4
  interface EventMapping {
3
5
  bits: number[];
4
6
  revenueBits?: number[];
5
7
  priority: number;
8
+ coarseValue?: SKANCoarseValue; // SKAN 4.0: low, medium, high
9
+ lockWindow?: boolean; // SKAN 4.0: lock the conversion window after this event
6
10
  }
7
11
 
8
12
  interface ConversionTemplate {
@@ -18,11 +22,21 @@ export class ConversionValueEncoder {
18
22
  }
19
23
 
20
24
  /**
21
- * Encode an event into Apple's 0-63 conversion value format
25
+ * Encode an event into Apple's 0-63 conversion value format (SKAN 3.0 compatible)
26
+ * @deprecated Use encodeWithSKAN4 for iOS 16.1+
22
27
  */
23
28
  encode(event: string, properties?: Record<string, any>): number {
29
+ return this.encodeWithSKAN4(event, properties).fineValue;
30
+ }
31
+
32
+ /**
33
+ * Encode an event with full SKAN 4.0 support (fine value, coarse value, lock window)
34
+ */
35
+ encodeWithSKAN4(event: string, properties?: Record<string, any>): SKANConversionResult {
24
36
  const mapping = this.template.events[event];
25
- if (!mapping) return 0;
37
+ if (!mapping) {
38
+ return { fineValue: 0, coarseValue: 'low', lockWindow: false, priority: 0 };
39
+ }
26
40
 
27
41
  let conversionValue = 0;
28
42
 
@@ -32,18 +46,30 @@ export class ConversionValueEncoder {
32
46
  }
33
47
 
34
48
  // Set revenue bits if revenue is provided
49
+ let coarseValue: SKANCoarseValue = mapping.coarseValue || 'medium';
35
50
  if (mapping.revenueBits && properties) {
36
51
  const revenue = properties.revenue || properties.value || 0;
37
52
  const revenueTier = this.getRevenueTier(revenue);
38
-
53
+
39
54
  for (let i = 0; i < Math.min(mapping.revenueBits.length, 3); i++) {
40
55
  if ((revenueTier >> i) & 1) {
41
56
  conversionValue |= (1 << mapping.revenueBits[i]);
42
57
  }
43
58
  }
59
+
60
+ // Upgrade coarse value based on revenue
61
+ coarseValue = this.getCoarseValueForRevenue(revenue);
44
62
  }
45
63
 
46
- return Math.min(conversionValue, 63);
64
+ // Ensure value is within 0-63 range
65
+ const fineValue = Math.min(conversionValue, 63);
66
+
67
+ return {
68
+ fineValue,
69
+ coarseValue,
70
+ lockWindow: mapping.lockWindow || false,
71
+ priority: mapping.priority
72
+ };
47
73
  }
48
74
 
49
75
  /**
@@ -59,43 +85,58 @@ export class ConversionValueEncoder {
59
85
  if (revenue < 250) return 6; // $100-250
60
86
  return 7; // $250+
61
87
  }
88
+
89
+ /**
90
+ * Map revenue to SKAN 4.0 coarse value
91
+ */
92
+ private getCoarseValueForRevenue(revenue: number): SKANCoarseValue {
93
+ if (revenue < 10) return 'low'; // $0-10 = low value
94
+ if (revenue < 50) return 'medium'; // $10-50 = medium value
95
+ return 'high'; // $50+ = high value
96
+ }
62
97
  }
63
98
 
64
- // Industry templates
99
+ // Industry templates with SKAN 4.0 support
65
100
  export const ConversionTemplates = {
101
+ // E-commerce template - optimized for online stores
102
+ // SKAN 4.0: purchase locks window, high-value events get "high" coarse value
66
103
  ecommerce: {
67
104
  name: 'ecommerce',
68
105
  events: {
69
- purchase: { bits: [0], revenueBits: [1, 2, 3], priority: 100 },
70
- add_to_cart: { bits: [4], priority: 30 },
71
- begin_checkout: { bits: [5], priority: 50 },
72
- signup: { bits: [6], priority: 20 },
73
- subscribe: { bits: [0, 1], revenueBits: [2, 3, 4], priority: 90 },
74
- view_item: { bits: [7], priority: 10 }
106
+ purchase: { bits: [0], revenueBits: [1, 2, 3], priority: 100, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
107
+ add_to_cart: { bits: [4], priority: 30, coarseValue: 'low' as SKANCoarseValue },
108
+ begin_checkout: { bits: [5], priority: 50, coarseValue: 'medium' as SKANCoarseValue },
109
+ signup: { bits: [6], priority: 20, coarseValue: 'low' as SKANCoarseValue },
110
+ subscribe: { bits: [0, 1], revenueBits: [2, 3, 4], priority: 90, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
111
+ view_item: { bits: [7], priority: 10, coarseValue: 'low' as SKANCoarseValue }
75
112
  }
76
113
  } as ConversionTemplate,
77
-
114
+
115
+ // Gaming template - optimized for mobile games
116
+ // SKAN 4.0: purchase locks window, tutorial completion is medium value
78
117
  gaming: {
79
118
  name: 'gaming',
80
119
  events: {
81
- level_complete: { bits: [0], priority: 40 },
82
- tutorial_complete: { bits: [1], priority: 60 },
83
- purchase: { bits: [2], revenueBits: [3, 4, 5], priority: 100 },
84
- achievement_unlocked: { bits: [6], priority: 30 },
85
- session_start: { bits: [7], priority: 10 },
86
- ad_watched: { bits: [0, 6], priority: 20 }
120
+ level_complete: { bits: [0], priority: 40, coarseValue: 'medium' as SKANCoarseValue },
121
+ tutorial_complete: { bits: [1], priority: 60, coarseValue: 'medium' as SKANCoarseValue },
122
+ purchase: { bits: [2], revenueBits: [3, 4, 5], priority: 100, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
123
+ achievement_unlocked: { bits: [6], priority: 30, coarseValue: 'low' as SKANCoarseValue },
124
+ session_start: { bits: [7], priority: 10, coarseValue: 'low' as SKANCoarseValue },
125
+ ad_watched: { bits: [0, 6], priority: 20, coarseValue: 'low' as SKANCoarseValue }
87
126
  }
88
127
  } as ConversionTemplate,
89
-
128
+
129
+ // Subscription template - optimized for subscription apps
130
+ // SKAN 4.0: subscribe/upgrade lock window, trial is medium value
90
131
  subscription: {
91
132
  name: 'subscription',
92
133
  events: {
93
- trial_start: { bits: [0], priority: 70 },
94
- subscribe: { bits: [1], revenueBits: [2, 3, 4], priority: 100 },
95
- upgrade: { bits: [1, 5], revenueBits: [2, 3, 4], priority: 90 },
96
- cancel: { bits: [6], priority: 20 },
97
- signup: { bits: [7], priority: 30 },
98
- payment_method_added: { bits: [0, 7], priority: 50 }
134
+ trial_start: { bits: [0], priority: 70, coarseValue: 'medium' as SKANCoarseValue },
135
+ subscribe: { bits: [1], revenueBits: [2, 3, 4], priority: 100, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
136
+ upgrade: { bits: [1, 5], revenueBits: [2, 3, 4], priority: 90, coarseValue: 'high' as SKANCoarseValue, lockWindow: true },
137
+ cancel: { bits: [6], priority: 20, coarseValue: 'low' as SKANCoarseValue },
138
+ signup: { bits: [7], priority: 30, coarseValue: 'low' as SKANCoarseValue },
139
+ payment_method_added: { bits: [0, 7], priority: 50, coarseValue: 'medium' as SKANCoarseValue }
99
140
  }
100
141
  } as ConversionTemplate
101
142
  };
@@ -30,6 +30,7 @@ import {
30
30
  import { createHttpClient, HttpClient } from './http-client';
31
31
  import { createEventQueue, EventQueue } from './event-queue';
32
32
  import { attributionManager, AttributionData } from './attribution';
33
+ import { journeyManager } from './journey';
33
34
  import { createAutoEventsManager, AutoEventsManager } from './auto-events';
34
35
  import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
35
36
  import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
@@ -115,6 +116,24 @@ export class DatalyrSDKExpo {
115
116
  await attributionManager.initialize();
116
117
  }
117
118
 
119
+ // Initialize journey tracking (for first-touch, last-touch, touchpoints)
120
+ await journeyManager.initialize();
121
+
122
+ // Record initial attribution to journey if this is a new session with attribution
123
+ const initialAttribution = attributionManager.getAttributionData();
124
+ if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
125
+ await journeyManager.recordAttribution(this.state.sessionId, {
126
+ source: initialAttribution.utm_source || initialAttribution.campaign_source,
127
+ medium: initialAttribution.utm_medium || initialAttribution.campaign_medium,
128
+ campaign: initialAttribution.utm_campaign || initialAttribution.campaign_name,
129
+ fbclid: initialAttribution.fbclid,
130
+ gclid: initialAttribution.gclid,
131
+ ttclid: initialAttribution.ttclid,
132
+ clickIdType: initialAttribution.fbclid ? 'fbclid' : initialAttribution.gclid ? 'gclid' : initialAttribution.ttclid ? 'ttclid' : undefined,
133
+ lyr: initialAttribution.lyr,
134
+ });
135
+ }
136
+
118
137
  if (this.state.config.enableAutoEvents) {
119
138
  this.autoEventsManager = new AutoEventsManager(
120
139
  this.track.bind(this),
@@ -441,8 +460,32 @@ export class DatalyrSDKExpo {
441
460
  return this.state.anonymousId;
442
461
  }
443
462
 
444
- getAttributionData(): AttributionData {
445
- return attributionManager.getAttributionData();
463
+ /**
464
+ * Get detailed attribution data (includes journey tracking data)
465
+ */
466
+ getAttributionData(): AttributionData & Record<string, any> {
467
+ const attribution = attributionManager.getAttributionData();
468
+ const journeyData = journeyManager.getAttributionData();
469
+
470
+ // Merge attribution with journey data
471
+ return {
472
+ ...attribution,
473
+ ...journeyData,
474
+ };
475
+ }
476
+
477
+ /**
478
+ * Get journey tracking summary
479
+ */
480
+ getJourneySummary() {
481
+ return journeyManager.getJourneySummary();
482
+ }
483
+
484
+ /**
485
+ * Get full customer journey (all touchpoints)
486
+ */
487
+ getJourney() {
488
+ return journeyManager.getJourney();
446
489
  }
447
490
 
448
491
  async setAttributionData(data: Partial<AttributionData>): Promise<void> {
@@ -477,6 +520,10 @@ export class DatalyrSDKExpo {
477
520
  }
478
521
  }
479
522
 
523
+ /**
524
+ * Track event with automatic SKAdNetwork conversion value encoding
525
+ * Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
526
+ */
480
527
  async trackWithSKAdNetwork(event: string, properties?: EventData): Promise<void> {
481
528
  await this.track(event, properties);
482
529
 
@@ -487,13 +534,15 @@ export class DatalyrSDKExpo {
487
534
  return;
488
535
  }
489
536
 
490
- const conversionValue = DatalyrSDKExpo.conversionEncoder.encode(event, properties);
537
+ // Use SKAN 4.0 encoding (includes coarse value and lock window)
538
+ const result = DatalyrSDKExpo.conversionEncoder.encodeWithSKAN4(event, properties);
491
539
 
492
- if (conversionValue > 0) {
493
- const success = await SKAdNetworkBridge.updateConversionValue(conversionValue);
540
+ if (result.fineValue > 0 || result.priority > 0) {
541
+ // Use SKAN 4.0 method (automatically falls back to SKAN 3.0 on older iOS)
542
+ const success = await SKAdNetworkBridge.updatePostbackConversionValue(result);
494
543
 
495
544
  if (DatalyrSDKExpo.debugEnabled) {
496
- debugLog(`Event: ${event}, Conversion Value: ${conversionValue}, Success: ${success}`, properties);
545
+ debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
497
546
  }
498
547
  }
499
548
  }
@@ -27,11 +27,13 @@ import {
27
27
  import { createHttpClient, HttpClient } from './http-client';
28
28
  import { createEventQueue, EventQueue } from './event-queue';
29
29
  import { attributionManager, AttributionData } from './attribution';
30
+ import { journeyManager } from './journey';
30
31
  import { createAutoEventsManager, AutoEventsManager, SessionData } from './auto-events';
31
32
  import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
32
33
  import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
33
- import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
34
+ import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
34
35
  import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
36
+ import { networkStatusManager } from './network-status';
35
37
 
36
38
  export class DatalyrSDK {
37
39
  private state: SDKState;
@@ -39,6 +41,7 @@ export class DatalyrSDK {
39
41
  private eventQueue: EventQueue;
40
42
  private autoEventsManager: AutoEventsManager | null = null;
41
43
  private appStateSubscription: any = null;
44
+ private networkStatusUnsubscribe: (() => void) | null = null;
42
45
  private static conversionEncoder?: ConversionValueEncoder;
43
46
  private static debugEnabled = false;
44
47
 
@@ -111,17 +114,35 @@ export class DatalyrSDK {
111
114
  maxRetryCount: this.state.config.maxRetries || 3,
112
115
  });
113
116
 
114
- // Initialize visitor ID, anonymous ID and session
115
- this.state.visitorId = await getOrCreateVisitorId();
116
- this.state.anonymousId = await getOrCreateAnonymousId();
117
- this.state.sessionId = await getOrCreateSessionId();
118
-
119
- // Load persisted user data
120
- await this.loadPersistedUserData();
117
+ // PARALLEL INITIALIZATION: IDs and core managers
118
+ // Run ID creation and core manager initialization in parallel for faster startup
119
+ const [visitorId, anonymousId, sessionId] = await Promise.all([
120
+ getOrCreateVisitorId(),
121
+ getOrCreateAnonymousId(),
122
+ getOrCreateSessionId(),
123
+ // These run concurrently but don't return values we need to capture
124
+ this.loadPersistedUserData(),
125
+ this.state.config.enableAttribution ? attributionManager.initialize() : Promise.resolve(),
126
+ journeyManager.initialize(),
127
+ ]);
121
128
 
122
- // Initialize attribution manager
123
- if (this.state.config.enableAttribution) {
124
- await attributionManager.initialize();
129
+ this.state.visitorId = visitorId;
130
+ this.state.anonymousId = anonymousId;
131
+ this.state.sessionId = sessionId;
132
+
133
+ // Record initial attribution to journey if this is a new session with attribution
134
+ const initialAttribution = attributionManager.getAttributionData();
135
+ if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
136
+ await journeyManager.recordAttribution(this.state.sessionId, {
137
+ source: initialAttribution.utm_source || initialAttribution.campaign_source,
138
+ medium: initialAttribution.utm_medium || initialAttribution.campaign_medium,
139
+ campaign: initialAttribution.utm_campaign || initialAttribution.campaign_name,
140
+ fbclid: initialAttribution.fbclid,
141
+ gclid: initialAttribution.gclid,
142
+ ttclid: initialAttribution.ttclid,
143
+ clickIdType: initialAttribution.fbclid ? 'fbclid' : initialAttribution.gclid ? 'gclid' : initialAttribution.ttclid ? 'ttclid' : undefined,
144
+ lyr: initialAttribution.lyr,
145
+ });
125
146
  }
126
147
 
127
148
  // Initialize auto-events manager (asynchronously to avoid blocking)
@@ -150,7 +171,7 @@ export class DatalyrSDK {
150
171
  }
151
172
  }, 50);
152
173
 
153
- // Initialize SKAdNetwork conversion encoder
174
+ // Initialize SKAdNetwork conversion encoder (synchronous, no await needed)
154
175
  if (config.skadTemplate) {
155
176
  const template = ConversionTemplates[config.skadTemplate];
156
177
  if (template) {
@@ -164,31 +185,47 @@ export class DatalyrSDK {
164
185
  }
165
186
  }
166
187
 
167
- // Initialize Meta SDK if configured
188
+ // PARALLEL INITIALIZATION: Network monitoring and platform integrations
189
+ // These are independent and can run concurrently for faster startup
190
+ const platformInitPromises: Promise<void>[] = [
191
+ // Network monitoring
192
+ this.initializeNetworkMonitoring(),
193
+ // Apple Search Ads (iOS only)
194
+ appleSearchAdsIntegration.initialize(config.debug),
195
+ // Google Play Install Referrer (Android only)
196
+ playInstallReferrerIntegration.initialize(),
197
+ ];
198
+
199
+ // Add Meta initialization if configured
168
200
  if (config.meta?.appId) {
169
- await metaIntegration.initialize(config.meta, config.debug);
170
-
171
- // Fetch deferred deep link and merge with attribution
172
- if (config.enableAttribution !== false) {
173
- const deferredLink = await metaIntegration.fetchDeferredDeepLink();
174
- if (deferredLink) {
175
- await this.handleDeferredDeepLink(deferredLink);
176
- }
177
- }
201
+ platformInitPromises.push(
202
+ metaIntegration.initialize(config.meta, config.debug).then(async () => {
203
+ // After Meta initializes, fetch deferred deep link
204
+ if (config.enableAttribution !== false) {
205
+ const deferredLink = await metaIntegration.fetchDeferredDeepLink();
206
+ if (deferredLink) {
207
+ await this.handleDeferredDeepLink(deferredLink);
208
+ }
209
+ }
210
+ })
211
+ );
178
212
  }
179
213
 
180
- // Initialize TikTok SDK if configured
214
+ // Add TikTok initialization if configured
181
215
  if (config.tiktok?.appId && config.tiktok?.tiktokAppId) {
182
- await tiktokIntegration.initialize(config.tiktok, config.debug);
216
+ platformInitPromises.push(
217
+ tiktokIntegration.initialize(config.tiktok, config.debug)
218
+ );
183
219
  }
184
220
 
185
- // Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
186
- await appleSearchAdsIntegration.initialize(config.debug);
221
+ // Wait for all platform integrations to complete
222
+ await Promise.all(platformInitPromises);
187
223
 
188
224
  debugLog('Platform integrations initialized', {
189
225
  meta: metaIntegration.isAvailable(),
190
226
  tiktok: tiktokIntegration.isAvailable(),
191
227
  appleSearchAds: appleSearchAdsIntegration.isAvailable(),
228
+ playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
192
229
  });
193
230
 
194
231
  // SDK initialized successfully - set state before tracking install event
@@ -490,6 +527,7 @@ export class DatalyrSDK {
490
527
  currentUserId?: string;
491
528
  queueStats: any;
492
529
  attribution: any;
530
+ journey: any;
493
531
  } {
494
532
  return {
495
533
  initialized: this.state.initialized,
@@ -500,6 +538,7 @@ export class DatalyrSDK {
500
538
  currentUserId: this.state.currentUserId,
501
539
  queueStats: this.eventQueue.getStats(),
502
540
  attribution: attributionManager.getAttributionSummary(),
541
+ journey: journeyManager.getJourneySummary(),
503
542
  };
504
543
  }
505
544
 
@@ -511,10 +550,31 @@ export class DatalyrSDK {
511
550
  }
512
551
 
513
552
  /**
514
- * Get detailed attribution data
553
+ * Get detailed attribution data (includes journey tracking data)
515
554
  */
516
- getAttributionData(): AttributionData {
517
- return attributionManager.getAttributionData();
555
+ getAttributionData(): AttributionData & Record<string, any> {
556
+ const attribution = attributionManager.getAttributionData();
557
+ const journeyData = journeyManager.getAttributionData();
558
+
559
+ // Merge attribution with journey data
560
+ return {
561
+ ...attribution,
562
+ ...journeyData,
563
+ };
564
+ }
565
+
566
+ /**
567
+ * Get journey tracking summary
568
+ */
569
+ getJourneySummary() {
570
+ return journeyManager.getJourneySummary();
571
+ }
572
+
573
+ /**
574
+ * Get full customer journey (all touchpoints)
575
+ */
576
+ getJourney() {
577
+ return journeyManager.getJourney();
518
578
  }
519
579
 
520
580
  /**
@@ -571,15 +631,16 @@ export class DatalyrSDK {
571
631
 
572
632
  /**
573
633
  * Track event with automatic SKAdNetwork conversion value encoding
634
+ * Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
574
635
  */
575
636
  async trackWithSKAdNetwork(
576
- event: string,
637
+ event: string,
577
638
  properties?: EventData
578
639
  ): Promise<void> {
579
640
  // Existing tracking (keep exactly as-is)
580
641
  await this.track(event, properties);
581
642
 
582
- // NEW: Automatic SKAdNetwork encoding
643
+ // Automatic SKAdNetwork encoding with SKAN 4.0 support
583
644
  if (!DatalyrSDK.conversionEncoder) {
584
645
  if (DatalyrSDK.debugEnabled) {
585
646
  errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
@@ -587,13 +648,15 @@ export class DatalyrSDK {
587
648
  return;
588
649
  }
589
650
 
590
- const conversionValue = DatalyrSDK.conversionEncoder.encode(event, properties);
591
-
592
- if (conversionValue > 0) {
593
- const success = await SKAdNetworkBridge.updateConversionValue(conversionValue);
594
-
651
+ // Use SKAN 4.0 encoding (includes coarse value and lock window)
652
+ const result = DatalyrSDK.conversionEncoder.encodeWithSKAN4(event, properties);
653
+
654
+ if (result.fineValue > 0 || result.priority > 0) {
655
+ // Use SKAN 4.0 method (automatically falls back to SKAN 3.0 on older iOS)
656
+ const success = await SKAdNetworkBridge.updatePostbackConversionValue(result);
657
+
595
658
  if (DatalyrSDK.debugEnabled) {
596
- debugLog(`Event: ${event}, Conversion Value: ${conversionValue}, Success: ${success}`, properties);
659
+ debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
597
660
  }
598
661
  } else if (DatalyrSDK.debugEnabled) {
599
662
  debugLog(`No conversion value generated for event: ${event}`);
@@ -837,11 +900,12 @@ export class DatalyrSDK {
837
900
  /**
838
901
  * Get platform integration status
839
902
  */
840
- getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
903
+ getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
841
904
  return {
842
905
  meta: metaIntegration.isAvailable(),
843
906
  tiktok: tiktokIntegration.isAvailable(),
844
907
  appleSearchAds: appleSearchAdsIntegration.isAvailable(),
908
+ playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
845
909
  };
846
910
  }
847
911
 
@@ -853,6 +917,15 @@ export class DatalyrSDK {
853
917
  return appleSearchAdsIntegration.getAttributionData();
854
918
  }
855
919
 
920
+ /**
921
+ * Get Google Play Install Referrer attribution data (Android only)
922
+ * Returns referrer data if available, null otherwise
923
+ */
924
+ getPlayInstallReferrer(): Record<string, any> | null {
925
+ const data = playInstallReferrerIntegration.getReferrerData();
926
+ return data ? playInstallReferrerIntegration.getAttributionData() : null;
927
+ }
928
+
856
929
  /**
857
930
  * Update tracking authorization status on all platform SDKs
858
931
  * Call this AFTER the user responds to the ATT permission dialog
@@ -1035,6 +1108,45 @@ export class DatalyrSDK {
1035
1108
  }
1036
1109
  }
1037
1110
 
1111
+ /**
1112
+ * Initialize network status monitoring
1113
+ * Automatically updates event queue when network status changes
1114
+ */
1115
+ private async initializeNetworkMonitoring(): Promise<void> {
1116
+ try {
1117
+ await networkStatusManager.initialize();
1118
+
1119
+ // Update event queue with current network status
1120
+ this.state.isOnline = networkStatusManager.isOnline();
1121
+ this.eventQueue.setOnlineStatus(this.state.isOnline);
1122
+
1123
+ // Subscribe to network changes
1124
+ this.networkStatusUnsubscribe = networkStatusManager.subscribe((state) => {
1125
+ const isOnline = state.isConnected && (state.isInternetReachable !== false);
1126
+ this.state.isOnline = isOnline;
1127
+ this.eventQueue.setOnlineStatus(isOnline);
1128
+
1129
+ // Track network status change event (only if SDK is fully initialized)
1130
+ if (this.state.initialized) {
1131
+ this.track('$network_status_change', {
1132
+ is_online: isOnline,
1133
+ network_type: state.type,
1134
+ is_internet_reachable: state.isInternetReachable,
1135
+ }).catch(() => {
1136
+ // Ignore errors for network status events
1137
+ });
1138
+ }
1139
+ });
1140
+
1141
+ debugLog(`Network monitoring initialized, online: ${this.state.isOnline}`);
1142
+ } catch (error) {
1143
+ errorLog('Error initializing network monitoring (non-blocking):', error as Error);
1144
+ // Default to online if monitoring fails
1145
+ this.state.isOnline = true;
1146
+ this.eventQueue.setOnlineStatus(true);
1147
+ }
1148
+ }
1149
+
1038
1150
  /**
1039
1151
  * Set up app state monitoring for lifecycle events (optimized)
1040
1152
  */
@@ -1055,6 +1167,8 @@ export class DatalyrSDK {
1055
1167
  } else if (nextAppState === 'active') {
1056
1168
  // App became active, ensure we have fresh session if needed
1057
1169
  this.refreshSession();
1170
+ // Refresh network status when coming back from background
1171
+ networkStatusManager.refresh();
1058
1172
  // Notify auto-events manager for session handling
1059
1173
  if (this.autoEventsManager) {
1060
1174
  this.autoEventsManager.handleAppForeground();
@@ -1095,6 +1209,15 @@ export class DatalyrSDK {
1095
1209
  this.appStateSubscription = null;
1096
1210
  }
1097
1211
 
1212
+ // Remove network status listener
1213
+ if (this.networkStatusUnsubscribe) {
1214
+ this.networkStatusUnsubscribe();
1215
+ this.networkStatusUnsubscribe = null;
1216
+ }
1217
+
1218
+ // Destroy network status manager
1219
+ networkStatusManager.destroy();
1220
+
1098
1221
  // Destroy event queue
1099
1222
  this.eventQueue.destroy();
1100
1223
 
package/src/expo.ts CHANGED
@@ -22,6 +22,10 @@ export * from './types';
22
22
  // Export attribution manager
23
23
  export { attributionManager } from './attribution';
24
24
 
25
+ // Export journey tracking
26
+ export { journeyManager } from './journey';
27
+ export type { TouchAttribution, TouchPoint } from './journey';
28
+
25
29
  // Export auto-events
26
30
  export { createAutoEventsManager, AutoEventsManager } from './auto-events';
27
31
 
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ export { Datalyr };
12
12
  // Export types and utilities
13
13
  export * from './types';
14
14
  export { attributionManager } from './attribution';
15
+ export { journeyManager } from './journey';
16
+ export type { TouchAttribution, TouchPoint } from './journey';
15
17
  export { createAutoEventsManager, AutoEventsManager } from './auto-events';
16
18
 
17
19
  // Re-export utilities for advanced usage
@@ -27,7 +29,11 @@ export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEn
27
29
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
28
30
 
29
31
  // Export platform integrations
30
- export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
32
+ export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
33
+
34
+ // Export network status manager
35
+ export { networkStatusManager } from './network-status';
36
+ export type { NetworkState, NetworkStateListener } from './network-status';
31
37
 
32
38
  // Export native bridge types
33
39
  export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Platform SDK Integrations
3
- * Meta (Facebook), TikTok, and Apple Search Ads SDK wrappers
3
+ * Meta (Facebook), TikTok, Apple Search Ads, and Google Play Install Referrer SDK wrappers
4
4
  */
5
5
 
6
6
  export { MetaIntegration, metaIntegration } from './meta-integration';
7
7
  export { TikTokIntegration, tiktokIntegration } from './tiktok-integration';
8
8
  export { AppleSearchAdsIntegration, appleSearchAdsIntegration } from './apple-search-ads-integration';
9
+ export { playInstallReferrerIntegration } from './play-install-referrer';
10
+ export type { PlayInstallReferrer } from './play-install-referrer';
@@ -20,14 +20,15 @@ export class MetaIntegration {
20
20
 
21
21
  /**
22
22
  * Initialize Meta SDK with configuration
23
+ * Supported on both iOS and Android via native modules
23
24
  */
24
25
  async initialize(config: MetaConfig, debug: boolean = false): Promise<void> {
25
26
  this.debug = debug;
26
27
  this.config = config;
27
28
 
28
- // Only available on iOS via native module
29
- if (Platform.OS !== 'ios') {
30
- this.log('Meta SDK only available on iOS');
29
+ // Only available on iOS and Android via native modules
30
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
31
+ this.log('Meta SDK only available on iOS and Android');
31
32
  return;
32
33
  }
33
34