@datalyr/react-native 1.2.0 → 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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +30 -1
  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/DatalyrNative.m +4 -0
  10. package/ios/DatalyrNative.swift +58 -1
  11. package/ios/DatalyrSKAdNetwork.m +52 -1
  12. package/lib/ConversionValueEncoder.d.ts +13 -1
  13. package/lib/ConversionValueEncoder.js +57 -23
  14. package/lib/datalyr-sdk.d.ts +34 -2
  15. package/lib/datalyr-sdk.js +90 -8
  16. package/lib/index.d.ts +4 -1
  17. package/lib/index.js +2 -1
  18. package/lib/integrations/apple-search-ads-integration.d.ts +43 -0
  19. package/lib/integrations/apple-search-ads-integration.js +106 -0
  20. package/lib/integrations/index.d.ts +4 -1
  21. package/lib/integrations/index.js +3 -1
  22. package/lib/integrations/meta-integration.d.ts +1 -0
  23. package/lib/integrations/meta-integration.js +4 -3
  24. package/lib/integrations/play-install-referrer.d.ts +74 -0
  25. package/lib/integrations/play-install-referrer.js +156 -0
  26. package/lib/integrations/tiktok-integration.d.ts +1 -0
  27. package/lib/integrations/tiktok-integration.js +4 -3
  28. package/lib/journey.d.ts +106 -0
  29. package/lib/journey.js +258 -0
  30. package/lib/native/DatalyrNativeBridge.d.ts +67 -2
  31. package/lib/native/DatalyrNativeBridge.js +80 -7
  32. package/lib/native/SKAdNetworkBridge.d.ts +21 -0
  33. package/lib/native/SKAdNetworkBridge.js +54 -0
  34. package/package.json +9 -3
  35. package/src/ConversionValueEncoder.ts +67 -26
  36. package/src/datalyr-sdk-expo.ts +98 -9
  37. package/src/datalyr-sdk.ts +109 -14
  38. package/src/expo.ts +8 -0
  39. package/src/index.ts +6 -1
  40. package/src/integrations/apple-search-ads-integration.ts +119 -0
  41. package/src/integrations/index.ts +4 -1
  42. package/src/integrations/meta-integration.ts +4 -3
  43. package/src/integrations/play-install-referrer.ts +203 -0
  44. package/src/integrations/tiktok-integration.ts +4 -3
  45. package/src/journey.ts +338 -0
  46. package/src/native/DatalyrNativeBridge.ts +137 -9
  47. package/src/native/SKAdNetworkBridge.ts +86 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datalyr/react-native",
3
- "version": "1.2.0",
4
- "description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking with bundled Meta and TikTok SDKs",
3
+ "version": "1.3.0",
4
+ "description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking with bundled Meta and TikTok SDKs for iOS and Android",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "scripts": {
@@ -15,6 +15,7 @@
15
15
  "lib/",
16
16
  "src/",
17
17
  "ios/",
18
+ "android/",
18
19
  "README.md",
19
20
  "CHANGELOG.md",
20
21
  "LICENSE",
@@ -38,10 +39,15 @@
38
39
  "tiktok-attribution",
39
40
  "google-attribution",
40
41
  "ios-attribution",
42
+ "android-attribution",
43
+ "apple-search-ads",
44
+ "play-install-referrer",
45
+ "gclid",
41
46
  "conversion-tracking",
42
47
  "revenue-optimization",
43
48
  "deferred-deep-linking",
44
- "fbclid"
49
+ "fbclid",
50
+ "ttclid"
45
51
  ],
46
52
  "author": "Datalyr",
47
53
  "license": "MIT",
@@ -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,11 +30,13 @@ 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';
36
- import { metaIntegration, tiktokIntegration } from './integrations';
37
+ import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
37
38
  import { DeferredDeepLinkResult } from './types';
39
+ import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
38
40
 
39
41
  export class DatalyrSDKExpo {
40
42
  private state: SDKState;
@@ -114,6 +116,24 @@ export class DatalyrSDKExpo {
114
116
  await attributionManager.initialize();
115
117
  }
116
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
+
117
137
  if (this.state.config.enableAutoEvents) {
118
138
  this.autoEventsManager = new AutoEventsManager(
119
139
  this.track.bind(this),
@@ -176,6 +196,14 @@ export class DatalyrSDKExpo {
176
196
  }
177
197
  }
178
198
 
199
+ // Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
200
+ await appleSearchAdsIntegration.initialize(config.debug);
201
+ debugLog('Platform integrations initialized', {
202
+ meta: metaIntegration.isAvailable(),
203
+ tiktok: tiktokIntegration.isAvailable(),
204
+ appleSearchAds: appleSearchAdsIntegration.isAvailable(),
205
+ });
206
+
179
207
  this.state.initialized = true;
180
208
 
181
209
  if (attributionManager.isInstall()) {
@@ -432,8 +460,32 @@ export class DatalyrSDKExpo {
432
460
  return this.state.anonymousId;
433
461
  }
434
462
 
435
- getAttributionData(): AttributionData {
436
- 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();
437
489
  }
438
490
 
439
491
  async setAttributionData(data: Partial<AttributionData>): Promise<void> {
@@ -468,6 +520,10 @@ export class DatalyrSDKExpo {
468
520
  }
469
521
  }
470
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
+ */
471
527
  async trackWithSKAdNetwork(event: string, properties?: EventData): Promise<void> {
472
528
  await this.track(event, properties);
473
529
 
@@ -478,13 +534,15 @@ export class DatalyrSDKExpo {
478
534
  return;
479
535
  }
480
536
 
481
- 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);
482
539
 
483
- if (conversionValue > 0) {
484
- 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);
485
543
 
486
544
  if (DatalyrSDKExpo.debugEnabled) {
487
- 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);
488
546
  }
489
547
  }
490
548
  }
@@ -636,13 +694,22 @@ export class DatalyrSDKExpo {
636
694
  return null;
637
695
  }
638
696
 
639
- getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
697
+ getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
640
698
  return {
641
699
  meta: metaIntegration.isAvailable(),
642
700
  tiktok: tiktokIntegration.isAvailable(),
701
+ appleSearchAds: appleSearchAdsIntegration.isAvailable(),
643
702
  };
644
703
  }
645
704
 
705
+ /**
706
+ * Get Apple Search Ads attribution data
707
+ * Returns attribution if user installed via Apple Search Ads, null otherwise
708
+ */
709
+ getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
710
+ return appleSearchAdsIntegration.getAttributionData();
711
+ }
712
+
646
713
  async updateTrackingAuthorization(authorized: boolean): Promise<void> {
647
714
  if (metaIntegration.isAvailable()) {
648
715
  metaIntegration.updateTrackingAuthorization(authorized);
@@ -694,6 +761,22 @@ export class DatalyrSDKExpo {
694
761
  const attributionData = attributionManager.getAttributionData();
695
762
  const networkType = await getNetworkType();
696
763
 
764
+ // Get Apple Search Ads attribution if available
765
+ const asaAttribution = appleSearchAdsIntegration.getAttributionData();
766
+ const asaData = asaAttribution?.attribution ? {
767
+ asa_campaign_id: asaAttribution.campaignId,
768
+ asa_campaign_name: asaAttribution.campaignName,
769
+ asa_ad_group_id: asaAttribution.adGroupId,
770
+ asa_ad_group_name: asaAttribution.adGroupName,
771
+ asa_keyword_id: asaAttribution.keywordId,
772
+ asa_keyword: asaAttribution.keyword,
773
+ asa_org_id: asaAttribution.orgId,
774
+ asa_org_name: asaAttribution.orgName,
775
+ asa_click_date: asaAttribution.clickDate,
776
+ asa_conversion_type: asaAttribution.conversionType,
777
+ asa_country_or_region: asaAttribution.countryOrRegion,
778
+ } : {};
779
+
697
780
  const payload: EventPayload = {
698
781
  workspaceId: this.state.config.workspaceId || 'mobile_sdk',
699
782
  visitorId: this.state.visitorId,
@@ -713,6 +796,8 @@ export class DatalyrSDKExpo {
713
796
  timestamp: Date.now(),
714
797
  sdk_variant: 'expo',
715
798
  ...attributionData,
799
+ // Apple Search Ads attribution
800
+ ...asaData,
716
801
  },
717
802
  fingerprintData,
718
803
  source: 'mobile_app',
@@ -945,10 +1030,14 @@ export class DatalyrExpo {
945
1030
  return datalyrExpo.getDeferredAttributionData();
946
1031
  }
947
1032
 
948
- static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
1033
+ static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
949
1034
  return datalyrExpo.getPlatformIntegrationStatus();
950
1035
  }
951
1036
 
1037
+ static getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
1038
+ return datalyrExpo.getAppleSearchAdsAttribution();
1039
+ }
1040
+
952
1041
  static async updateTrackingAuthorization(authorized: boolean): Promise<void> {
953
1042
  await datalyrExpo.updateTrackingAuthorization(authorized);
954
1043
  }
@@ -27,10 +27,12 @@ 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 } from './integrations';
34
+ import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
35
+ import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
34
36
 
35
37
  export class DatalyrSDK {
36
38
  private state: SDKState;
@@ -123,6 +125,24 @@ export class DatalyrSDK {
123
125
  await attributionManager.initialize();
124
126
  }
125
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
+
126
146
  // Initialize auto-events manager (asynchronously to avoid blocking)
127
147
  if (this.state.config.enableAutoEvents) {
128
148
  this.autoEventsManager = new AutoEventsManager(
@@ -181,9 +201,17 @@ export class DatalyrSDK {
181
201
  await tiktokIntegration.initialize(config.tiktok, config.debug);
182
202
  }
183
203
 
204
+ // Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
205
+ await appleSearchAdsIntegration.initialize(config.debug);
206
+
207
+ // Initialize Google Play Install Referrer (Android only)
208
+ await playInstallReferrerIntegration.initialize();
209
+
184
210
  debugLog('Platform integrations initialized', {
185
211
  meta: metaIntegration.isAvailable(),
186
212
  tiktok: tiktokIntegration.isAvailable(),
213
+ appleSearchAds: appleSearchAdsIntegration.isAvailable(),
214
+ playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
187
215
  });
188
216
 
189
217
  // SDK initialized successfully - set state before tracking install event
@@ -485,6 +513,7 @@ export class DatalyrSDK {
485
513
  currentUserId?: string;
486
514
  queueStats: any;
487
515
  attribution: any;
516
+ journey: any;
488
517
  } {
489
518
  return {
490
519
  initialized: this.state.initialized,
@@ -495,6 +524,7 @@ export class DatalyrSDK {
495
524
  currentUserId: this.state.currentUserId,
496
525
  queueStats: this.eventQueue.getStats(),
497
526
  attribution: attributionManager.getAttributionSummary(),
527
+ journey: journeyManager.getJourneySummary(),
498
528
  };
499
529
  }
500
530
 
@@ -506,10 +536,31 @@ export class DatalyrSDK {
506
536
  }
507
537
 
508
538
  /**
509
- * 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
554
+ */
555
+ getJourneySummary() {
556
+ return journeyManager.getJourneySummary();
557
+ }
558
+
559
+ /**
560
+ * Get full customer journey (all touchpoints)
510
561
  */
511
- getAttributionData(): AttributionData {
512
- return attributionManager.getAttributionData();
562
+ getJourney() {
563
+ return journeyManager.getJourney();
513
564
  }
514
565
 
515
566
  /**
@@ -566,15 +617,16 @@ export class DatalyrSDK {
566
617
 
567
618
  /**
568
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
569
621
  */
570
622
  async trackWithSKAdNetwork(
571
- event: string,
623
+ event: string,
572
624
  properties?: EventData
573
625
  ): Promise<void> {
574
626
  // Existing tracking (keep exactly as-is)
575
627
  await this.track(event, properties);
576
628
 
577
- // NEW: Automatic SKAdNetwork encoding
629
+ // Automatic SKAdNetwork encoding with SKAN 4.0 support
578
630
  if (!DatalyrSDK.conversionEncoder) {
579
631
  if (DatalyrSDK.debugEnabled) {
580
632
  errorLog('SKAdNetwork encoder not initialized. Pass skadTemplate in initialize()');
@@ -582,13 +634,15 @@ export class DatalyrSDK {
582
634
  return;
583
635
  }
584
636
 
585
- const conversionValue = DatalyrSDK.conversionEncoder.encode(event, properties);
586
-
587
- if (conversionValue > 0) {
588
- const success = await SKAdNetworkBridge.updateConversionValue(conversionValue);
589
-
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
+
590
644
  if (DatalyrSDK.debugEnabled) {
591
- 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);
592
646
  }
593
647
  } else if (DatalyrSDK.debugEnabled) {
594
648
  debugLog(`No conversion value generated for event: ${event}`);
@@ -832,13 +886,32 @@ export class DatalyrSDK {
832
886
  /**
833
887
  * Get platform integration status
834
888
  */
835
- getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
889
+ getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
836
890
  return {
837
891
  meta: metaIntegration.isAvailable(),
838
892
  tiktok: tiktokIntegration.isAvailable(),
893
+ appleSearchAds: appleSearchAdsIntegration.isAvailable(),
894
+ playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
839
895
  };
840
896
  }
841
897
 
898
+ /**
899
+ * Get Apple Search Ads attribution data
900
+ * Returns attribution if user installed via Apple Search Ads, null otherwise
901
+ */
902
+ getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
903
+ return appleSearchAdsIntegration.getAttributionData();
904
+ }
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
+
842
915
  /**
843
916
  * Update tracking authorization status on all platform SDKs
844
917
  * Call this AFTER the user responds to the ATT permission dialog
@@ -919,6 +992,22 @@ export class DatalyrSDK {
919
992
  const fingerprintData = await createFingerprintData();
920
993
  const attributionData = attributionManager.getAttributionData();
921
994
 
995
+ // Get Apple Search Ads attribution if available
996
+ const asaAttribution = appleSearchAdsIntegration.getAttributionData();
997
+ const asaData = asaAttribution?.attribution ? {
998
+ asa_campaign_id: asaAttribution.campaignId,
999
+ asa_campaign_name: asaAttribution.campaignName,
1000
+ asa_ad_group_id: asaAttribution.adGroupId,
1001
+ asa_ad_group_name: asaAttribution.adGroupName,
1002
+ asa_keyword_id: asaAttribution.keywordId,
1003
+ asa_keyword: asaAttribution.keyword,
1004
+ asa_org_id: asaAttribution.orgId,
1005
+ asa_org_name: asaAttribution.orgName,
1006
+ asa_click_date: asaAttribution.clickDate,
1007
+ asa_conversion_type: asaAttribution.conversionType,
1008
+ asa_country_or_region: asaAttribution.countryOrRegion,
1009
+ } : {};
1010
+
922
1011
  const payload: EventPayload = {
923
1012
  workspaceId: this.state.config.workspaceId || 'mobile_sdk',
924
1013
  visitorId: this.state.visitorId,
@@ -940,6 +1029,8 @@ export class DatalyrSDK {
940
1029
  timestamp: Date.now(),
941
1030
  // Attribution data
942
1031
  ...attributionData,
1032
+ // Apple Search Ads attribution
1033
+ ...asaData,
943
1034
  },
944
1035
  fingerprintData,
945
1036
  source: 'mobile_app',
@@ -1239,10 +1330,14 @@ export class Datalyr {
1239
1330
  return datalyr.getDeferredAttributionData();
1240
1331
  }
1241
1332
 
1242
- static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean } {
1333
+ static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
1243
1334
  return datalyr.getPlatformIntegrationStatus();
1244
1335
  }
1245
1336
 
1337
+ static getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
1338
+ return datalyr.getAppleSearchAdsAttribution();
1339
+ }
1340
+
1246
1341
  static async updateTrackingAuthorization(enabled: boolean): Promise<void> {
1247
1342
  await datalyr.updateTrackingAuthorization(enabled);
1248
1343
  }
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
 
@@ -52,5 +56,9 @@ export * from './event-queue';
52
56
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
53
57
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
54
58
 
59
+ // Export platform integrations
60
+ export { appleSearchAdsIntegration } from './integrations';
61
+ export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
62
+
55
63
  // Default export
56
64
  export default datalyrExpo;
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,10 @@ export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEn
27
29
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
28
30
 
29
31
  // Export platform integrations
30
- export { metaIntegration, tiktokIntegration } from './integrations';
32
+ export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
33
+
34
+ // Export native bridge types
35
+ export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
31
36
 
32
37
  // Default export for compatibility
33
38
  export default DatalyrSDK;