@datalyr/react-native 1.2.1 → 1.3.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 (40) hide show
  1. package/android/build.gradle +54 -0
  2. package/android/src/main/AndroidManifest.xml +14 -0
  3. package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +423 -0
  4. package/android/src/main/java/com/datalyr/reactnative/DatalyrPackage.java +30 -0
  5. package/android/src/main/java/com/datalyr/reactnative/DatalyrPlayInstallReferrerModule.java +229 -0
  6. package/datalyr-react-native.podspec +2 -2
  7. package/ios/DatalyrSKAdNetwork.m +52 -1
  8. package/lib/ConversionValueEncoder.d.ts +13 -1
  9. package/lib/ConversionValueEncoder.js +57 -23
  10. package/lib/datalyr-sdk.d.ts +25 -2
  11. package/lib/datalyr-sdk.js +59 -8
  12. package/lib/index.d.ts +2 -0
  13. package/lib/index.js +1 -0
  14. package/lib/integrations/index.d.ts +3 -1
  15. package/lib/integrations/index.js +2 -1
  16. package/lib/integrations/meta-integration.d.ts +1 -0
  17. package/lib/integrations/meta-integration.js +4 -3
  18. package/lib/integrations/play-install-referrer.d.ts +74 -0
  19. package/lib/integrations/play-install-referrer.js +156 -0
  20. package/lib/integrations/tiktok-integration.d.ts +1 -0
  21. package/lib/integrations/tiktok-integration.js +4 -3
  22. package/lib/journey.d.ts +106 -0
  23. package/lib/journey.js +258 -0
  24. package/lib/native/DatalyrNativeBridge.d.ts +42 -3
  25. package/lib/native/DatalyrNativeBridge.js +63 -9
  26. package/lib/native/SKAdNetworkBridge.d.ts +21 -0
  27. package/lib/native/SKAdNetworkBridge.js +54 -0
  28. package/package.json +8 -3
  29. package/src/ConversionValueEncoder.ts +67 -26
  30. package/src/datalyr-sdk-expo.ts +55 -6
  31. package/src/datalyr-sdk.ts +72 -13
  32. package/src/expo.ts +4 -0
  33. package/src/index.ts +2 -0
  34. package/src/integrations/index.ts +3 -1
  35. package/src/integrations/meta-integration.ts +4 -3
  36. package/src/integrations/play-install-referrer.ts +203 -0
  37. package/src/integrations/tiktok-integration.ts +4 -3
  38. package/src/journey.ts +338 -0
  39. package/src/native/DatalyrNativeBridge.ts +99 -13
  40. package/src/native/SKAdNetworkBridge.ts +86 -2
@@ -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,10 +27,11 @@ 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';
35
36
 
36
37
  export class DatalyrSDK {
@@ -124,6 +125,24 @@ export class DatalyrSDK {
124
125
  await attributionManager.initialize();
125
126
  }
126
127
 
128
+ // Initialize journey tracking (for first-touch, last-touch, touchpoints)
129
+ await journeyManager.initialize();
130
+
131
+ // Record initial attribution to journey if this is a new session with attribution
132
+ const initialAttribution = attributionManager.getAttributionData();
133
+ if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
134
+ await journeyManager.recordAttribution(this.state.sessionId, {
135
+ source: initialAttribution.utm_source || initialAttribution.campaign_source,
136
+ medium: initialAttribution.utm_medium || initialAttribution.campaign_medium,
137
+ campaign: initialAttribution.utm_campaign || initialAttribution.campaign_name,
138
+ fbclid: initialAttribution.fbclid,
139
+ gclid: initialAttribution.gclid,
140
+ ttclid: initialAttribution.ttclid,
141
+ clickIdType: initialAttribution.fbclid ? 'fbclid' : initialAttribution.gclid ? 'gclid' : initialAttribution.ttclid ? 'ttclid' : undefined,
142
+ lyr: initialAttribution.lyr,
143
+ });
144
+ }
145
+
127
146
  // Initialize auto-events manager (asynchronously to avoid blocking)
128
147
  if (this.state.config.enableAutoEvents) {
129
148
  this.autoEventsManager = new AutoEventsManager(
@@ -185,10 +204,14 @@ export class DatalyrSDK {
185
204
  // Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
186
205
  await appleSearchAdsIntegration.initialize(config.debug);
187
206
 
207
+ // Initialize Google Play Install Referrer (Android only)
208
+ await playInstallReferrerIntegration.initialize();
209
+
188
210
  debugLog('Platform integrations initialized', {
189
211
  meta: metaIntegration.isAvailable(),
190
212
  tiktok: tiktokIntegration.isAvailable(),
191
213
  appleSearchAds: appleSearchAdsIntegration.isAvailable(),
214
+ playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
192
215
  });
193
216
 
194
217
  // SDK initialized successfully - set state before tracking install event
@@ -490,6 +513,7 @@ export class DatalyrSDK {
490
513
  currentUserId?: string;
491
514
  queueStats: any;
492
515
  attribution: any;
516
+ journey: any;
493
517
  } {
494
518
  return {
495
519
  initialized: this.state.initialized,
@@ -500,6 +524,7 @@ export class DatalyrSDK {
500
524
  currentUserId: this.state.currentUserId,
501
525
  queueStats: this.eventQueue.getStats(),
502
526
  attribution: attributionManager.getAttributionSummary(),
527
+ journey: journeyManager.getJourneySummary(),
503
528
  };
504
529
  }
505
530
 
@@ -511,10 +536,31 @@ export class DatalyrSDK {
511
536
  }
512
537
 
513
538
  /**
514
- * Get detailed attribution data
539
+ * Get detailed attribution data (includes journey tracking data)
540
+ */
541
+ getAttributionData(): AttributionData & Record<string, any> {
542
+ const attribution = attributionManager.getAttributionData();
543
+ const journeyData = journeyManager.getAttributionData();
544
+
545
+ // Merge attribution with journey data
546
+ return {
547
+ ...attribution,
548
+ ...journeyData,
549
+ };
550
+ }
551
+
552
+ /**
553
+ * Get journey tracking summary
515
554
  */
516
- getAttributionData(): AttributionData {
517
- return attributionManager.getAttributionData();
555
+ getJourneySummary() {
556
+ return journeyManager.getJourneySummary();
557
+ }
558
+
559
+ /**
560
+ * Get full customer journey (all touchpoints)
561
+ */
562
+ getJourney() {
563
+ return journeyManager.getJourney();
518
564
  }
519
565
 
520
566
  /**
@@ -571,15 +617,16 @@ export class DatalyrSDK {
571
617
 
572
618
  /**
573
619
  * Track event with automatic SKAdNetwork conversion value encoding
620
+ * Uses SKAN 4.0 on iOS 16.1+ with coarse values and lock window support
574
621
  */
575
622
  async trackWithSKAdNetwork(
576
- event: string,
623
+ event: string,
577
624
  properties?: EventData
578
625
  ): Promise<void> {
579
626
  // Existing tracking (keep exactly as-is)
580
627
  await this.track(event, properties);
581
628
 
582
- // NEW: Automatic SKAdNetwork encoding
629
+ // Automatic SKAdNetwork encoding with SKAN 4.0 support
583
630
  if (!DatalyrSDK.conversionEncoder) {
584
631
  if (DatalyrSDK.debugEnabled) {
585
632
  errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
@@ -587,13 +634,15 @@ export class DatalyrSDK {
587
634
  return;
588
635
  }
589
636
 
590
- const conversionValue = DatalyrSDK.conversionEncoder.encode(event, properties);
591
-
592
- if (conversionValue > 0) {
593
- const success = await SKAdNetworkBridge.updateConversionValue(conversionValue);
594
-
637
+ // Use SKAN 4.0 encoding (includes coarse value and lock window)
638
+ const result = DatalyrSDK.conversionEncoder.encodeWithSKAN4(event, properties);
639
+
640
+ if (result.fineValue > 0 || result.priority > 0) {
641
+ // Use SKAN 4.0 method (automatically falls back to SKAN 3.0 on older iOS)
642
+ const success = await SKAdNetworkBridge.updatePostbackConversionValue(result);
643
+
595
644
  if (DatalyrSDK.debugEnabled) {
596
- debugLog(`Event: ${event}, Conversion Value: ${conversionValue}, Success: ${success}`, properties);
645
+ debugLog(`SKAN: event=${event}, fine=${result.fineValue}, coarse=${result.coarseValue}, lock=${result.lockWindow}, success=${success}`, properties);
597
646
  }
598
647
  } else if (DatalyrSDK.debugEnabled) {
599
648
  debugLog(`No conversion value generated for event: ${event}`);
@@ -837,11 +886,12 @@ export class DatalyrSDK {
837
886
  /**
838
887
  * Get platform integration status
839
888
  */
840
- getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
889
+ getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
841
890
  return {
842
891
  meta: metaIntegration.isAvailable(),
843
892
  tiktok: tiktokIntegration.isAvailable(),
844
893
  appleSearchAds: appleSearchAdsIntegration.isAvailable(),
894
+ playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
845
895
  };
846
896
  }
847
897
 
@@ -853,6 +903,15 @@ export class DatalyrSDK {
853
903
  return appleSearchAdsIntegration.getAttributionData();
854
904
  }
855
905
 
906
+ /**
907
+ * Get Google Play Install Referrer attribution data (Android only)
908
+ * Returns referrer data if available, null otherwise
909
+ */
910
+ getPlayInstallReferrer(): Record<string, any> | null {
911
+ const data = playInstallReferrerIntegration.getReferrerData();
912
+ return data ? playInstallReferrerIntegration.getAttributionData() : null;
913
+ }
914
+
856
915
  /**
857
916
  * Update tracking authorization status on all platform SDKs
858
917
  * Call this AFTER the user responds to the ATT permission dialog
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
@@ -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
 
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Google Play Install Referrer Integration
3
+ *
4
+ * Provides Android install attribution via the Google Play Install Referrer API.
5
+ * This captures UTM parameters and click IDs passed through Google Play Store.
6
+ *
7
+ * How it works:
8
+ * 1. User clicks ad/link with UTM parameters
9
+ * 2. Google Play Store stores the referrer URL
10
+ * 3. On first app launch, SDK retrieves the referrer
11
+ * 4. Attribution data (utm_source, utm_medium, gclid, etc.) is extracted
12
+ *
13
+ * Requirements:
14
+ * - Android only (returns null on iOS)
15
+ * - Requires Google Play Install Referrer Library in build.gradle:
16
+ * implementation 'com.android.installreferrer:installreferrer:2.2'
17
+ *
18
+ * Attribution data captured:
19
+ * - referrer_url: Full referrer URL from Play Store
20
+ * - referrer_click_timestamp: When the referrer link was clicked
21
+ * - install_begin_timestamp: When the install began
22
+ * - gclid: Google Ads click ID (if present)
23
+ * - utm_source, utm_medium, utm_campaign, etc.
24
+ */
25
+
26
+ import { Platform, NativeModules } from 'react-native';
27
+ import { debugLog, errorLog } from '../utils';
28
+
29
+ export interface PlayInstallReferrer {
30
+ // Raw referrer URL from Play Store
31
+ referrerUrl: string;
32
+ // Timestamp when the referrer link was clicked (ms)
33
+ referrerClickTimestamp: number;
34
+ // Timestamp when the install began (ms)
35
+ installBeginTimestamp: number;
36
+ // Timestamp when install was completed (ms)
37
+ installCompleteTimestamp?: number;
38
+ // Google Ads click ID
39
+ gclid?: string;
40
+ // UTM Parameters
41
+ utmSource?: string;
42
+ utmMedium?: string;
43
+ utmCampaign?: string;
44
+ utmTerm?: string;
45
+ utmContent?: string;
46
+ // Additional parameters
47
+ [key: string]: any;
48
+ }
49
+
50
+ interface PlayInstallReferrerModule {
51
+ getInstallReferrer(): Promise<PlayInstallReferrer | null>;
52
+ isAvailable(): Promise<boolean>;
53
+ }
54
+
55
+ const { DatalyrPlayInstallReferrer } = NativeModules as {
56
+ DatalyrPlayInstallReferrer?: PlayInstallReferrerModule;
57
+ };
58
+
59
+ /**
60
+ * Google Play Install Referrer Integration
61
+ *
62
+ * Retrieves install attribution data from Google Play Store.
63
+ * Only available on Android.
64
+ */
65
+ class PlayInstallReferrerIntegration {
66
+ private referrerData: PlayInstallReferrer | null = null;
67
+ private initialized = false;
68
+
69
+ /**
70
+ * Check if Play Install Referrer is available
71
+ */
72
+ isAvailable(): boolean {
73
+ return Platform.OS === 'android' && !!DatalyrPlayInstallReferrer;
74
+ }
75
+
76
+ /**
77
+ * Initialize and fetch install referrer data
78
+ * Should be called once on first app launch
79
+ */
80
+ async initialize(): Promise<void> {
81
+ if (this.initialized) return;
82
+
83
+ if (!this.isAvailable()) {
84
+ debugLog('[PlayInstallReferrer] Not available (iOS or native module missing)');
85
+ return;
86
+ }
87
+
88
+ try {
89
+ this.referrerData = await this.fetchInstallReferrer();
90
+ this.initialized = true;
91
+
92
+ if (this.referrerData) {
93
+ debugLog('[PlayInstallReferrer] Install referrer fetched:', {
94
+ utmSource: this.referrerData.utmSource,
95
+ utmMedium: this.referrerData.utmMedium,
96
+ hasGclid: !!this.referrerData.gclid,
97
+ });
98
+ }
99
+ } catch (error) {
100
+ errorLog('[PlayInstallReferrer] Failed to initialize:', error as Error);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Fetch install referrer from Play Store
106
+ */
107
+ async fetchInstallReferrer(): Promise<PlayInstallReferrer | null> {
108
+ if (!this.isAvailable()) {
109
+ return null;
110
+ }
111
+
112
+ try {
113
+ const referrer = await DatalyrPlayInstallReferrer!.getInstallReferrer();
114
+
115
+ if (!referrer) {
116
+ return null;
117
+ }
118
+
119
+ // Parse UTM parameters from referrer URL
120
+ const parsed = this.parseReferrerUrl(referrer.referrerUrl);
121
+
122
+ return {
123
+ ...referrer,
124
+ ...parsed,
125
+ };
126
+ } catch (error) {
127
+ errorLog('[PlayInstallReferrer] Error fetching referrer:', error as Error);
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Parse referrer URL to extract UTM parameters and click IDs
134
+ */
135
+ private parseReferrerUrl(referrerUrl: string): Partial<PlayInstallReferrer> {
136
+ const params: Partial<PlayInstallReferrer> = {};
137
+
138
+ if (!referrerUrl) return params;
139
+
140
+ try {
141
+ // Referrer URL is URL-encoded, decode it first
142
+ const decoded = decodeURIComponent(referrerUrl);
143
+ const searchParams = new URLSearchParams(decoded);
144
+
145
+ // Extract UTM parameters
146
+ params.utmSource = searchParams.get('utm_source') || undefined;
147
+ params.utmMedium = searchParams.get('utm_medium') || undefined;
148
+ params.utmCampaign = searchParams.get('utm_campaign') || undefined;
149
+ params.utmTerm = searchParams.get('utm_term') || undefined;
150
+ params.utmContent = searchParams.get('utm_content') || undefined;
151
+
152
+ // Extract click IDs
153
+ params.gclid = searchParams.get('gclid') || undefined;
154
+
155
+ // Store any additional parameters
156
+ searchParams.forEach((value, key) => {
157
+ if (!key.startsWith('utm_') && key !== 'gclid') {
158
+ params[key] = value;
159
+ }
160
+ });
161
+
162
+ debugLog('[PlayInstallReferrer] Parsed referrer URL:', params);
163
+ } catch (error) {
164
+ errorLog('[PlayInstallReferrer] Error parsing referrer URL:', error as Error);
165
+ }
166
+
167
+ return params;
168
+ }
169
+
170
+ /**
171
+ * Get cached install referrer data
172
+ */
173
+ getReferrerData(): PlayInstallReferrer | null {
174
+ return this.referrerData;
175
+ }
176
+
177
+ /**
178
+ * Get attribution data in standard format
179
+ */
180
+ getAttributionData(): Record<string, any> {
181
+ if (!this.referrerData) return {};
182
+
183
+ return {
184
+ // Install referrer specific
185
+ install_referrer_url: this.referrerData.referrerUrl,
186
+ referrer_click_timestamp: this.referrerData.referrerClickTimestamp,
187
+ install_begin_timestamp: this.referrerData.installBeginTimestamp,
188
+
189
+ // Standard attribution fields
190
+ gclid: this.referrerData.gclid,
191
+ utm_source: this.referrerData.utmSource,
192
+ utm_medium: this.referrerData.utmMedium,
193
+ utm_campaign: this.referrerData.utmCampaign,
194
+ utm_term: this.referrerData.utmTerm,
195
+ utm_content: this.referrerData.utmContent,
196
+
197
+ // Source indicators
198
+ attribution_source: 'play_install_referrer',
199
+ };
200
+ }
201
+ }
202
+
203
+ export const playInstallReferrerIntegration = new PlayInstallReferrerIntegration();