@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
@@ -46,14 +46,15 @@ export class TikTokIntegration {
46
46
 
47
47
  /**
48
48
  * Initialize TikTok SDK with configuration
49
+ * Supported on both iOS and Android via native modules
49
50
  */
50
51
  async initialize(config: TikTokConfig, debug: boolean = false): Promise<void> {
51
52
  this.debug = debug;
52
53
  this.config = config;
53
54
 
54
- // Only available on iOS via native module
55
- if (Platform.OS !== 'ios') {
56
- this.log('TikTok SDK only available on iOS');
55
+ // Only available on iOS and Android via native modules
56
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
57
+ this.log('TikTok SDK only available on iOS and Android');
57
58
  return;
58
59
  }
59
60
 
package/src/journey.ts ADDED
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Journey Tracking Module for React Native
3
+ * Mirrors the Web SDK's journey tracking capabilities:
4
+ * - First-touch attribution with 90-day expiration
5
+ * - Last-touch attribution with 90-day expiration
6
+ * - Up to 30 touchpoints stored
7
+ */
8
+
9
+ import { Storage, debugLog, errorLog } from './utils';
10
+
11
+ // Storage keys for journey data
12
+ const JOURNEY_STORAGE_KEYS = {
13
+ FIRST_TOUCH: '@datalyr/first_touch',
14
+ LAST_TOUCH: '@datalyr/last_touch',
15
+ JOURNEY: '@datalyr/journey',
16
+ };
17
+
18
+ // 90-day attribution window (matching web SDK)
19
+ const ATTRIBUTION_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
20
+
21
+ // Maximum touchpoints to store
22
+ const MAX_TOUCHPOINTS = 30;
23
+
24
+ /**
25
+ * Attribution data for a touch
26
+ */
27
+ export interface TouchAttribution {
28
+ timestamp: number;
29
+ expires_at: number;
30
+ captured_at: number;
31
+
32
+ // Source attribution
33
+ source?: string;
34
+ medium?: string;
35
+ campaign?: string;
36
+ term?: string;
37
+ content?: string;
38
+
39
+ // Click IDs
40
+ clickId?: string;
41
+ clickIdType?: string;
42
+ fbclid?: string;
43
+ gclid?: string;
44
+ ttclid?: string;
45
+ gbraid?: string;
46
+ wbraid?: string;
47
+
48
+ // LYR tag
49
+ lyr?: string;
50
+
51
+ // Context
52
+ landingPage?: string;
53
+ referrer?: string;
54
+ }
55
+
56
+ /**
57
+ * A single touchpoint in the customer journey
58
+ */
59
+ export interface TouchPoint {
60
+ timestamp: number;
61
+ sessionId: string;
62
+ source?: string;
63
+ medium?: string;
64
+ campaign?: string;
65
+ clickIdType?: string;
66
+ }
67
+
68
+ /**
69
+ * Journey manager for tracking customer touchpoints
70
+ */
71
+ export class JourneyManager {
72
+ private firstTouch: TouchAttribution | null = null;
73
+ private lastTouch: TouchAttribution | null = null;
74
+ private journey: TouchPoint[] = [];
75
+ private initialized = false;
76
+
77
+ /**
78
+ * Initialize journey tracking by loading persisted data
79
+ */
80
+ async initialize(): Promise<void> {
81
+ if (this.initialized) return;
82
+
83
+ try {
84
+ debugLog('Initializing journey manager...');
85
+
86
+ // Load first touch
87
+ const savedFirstTouch = await Storage.getItem<TouchAttribution>(JOURNEY_STORAGE_KEYS.FIRST_TOUCH);
88
+ if (savedFirstTouch && !this.isExpired(savedFirstTouch)) {
89
+ this.firstTouch = savedFirstTouch;
90
+ } else if (savedFirstTouch) {
91
+ // Expired, clear it
92
+ await Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH);
93
+ }
94
+
95
+ // Load last touch
96
+ const savedLastTouch = await Storage.getItem<TouchAttribution>(JOURNEY_STORAGE_KEYS.LAST_TOUCH);
97
+ if (savedLastTouch && !this.isExpired(savedLastTouch)) {
98
+ this.lastTouch = savedLastTouch;
99
+ } else if (savedLastTouch) {
100
+ await Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH);
101
+ }
102
+
103
+ // Load journey
104
+ const savedJourney = await Storage.getItem<TouchPoint[]>(JOURNEY_STORAGE_KEYS.JOURNEY);
105
+ if (savedJourney) {
106
+ this.journey = savedJourney;
107
+ }
108
+
109
+ this.initialized = true;
110
+ debugLog('Journey manager initialized', {
111
+ hasFirstTouch: !!this.firstTouch,
112
+ hasLastTouch: !!this.lastTouch,
113
+ touchpointCount: this.journey.length,
114
+ });
115
+ } catch (error) {
116
+ errorLog('Failed to initialize journey manager:', error as Error);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check if attribution has expired
122
+ */
123
+ private isExpired(attribution: TouchAttribution): boolean {
124
+ return Date.now() >= attribution.expires_at;
125
+ }
126
+
127
+ /**
128
+ * Store first touch attribution (only if not already set or expired)
129
+ */
130
+ async storeFirstTouch(attribution: Partial<TouchAttribution>): Promise<void> {
131
+ try {
132
+ // Only store if no valid first touch exists
133
+ if (this.firstTouch && !this.isExpired(this.firstTouch)) {
134
+ debugLog('First touch already exists, not overwriting');
135
+ return;
136
+ }
137
+
138
+ const now = Date.now();
139
+ this.firstTouch = {
140
+ ...attribution,
141
+ timestamp: attribution.timestamp || now,
142
+ captured_at: now,
143
+ expires_at: now + ATTRIBUTION_WINDOW_MS,
144
+ } as TouchAttribution;
145
+
146
+ await Storage.setItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH, this.firstTouch);
147
+ debugLog('First touch stored:', this.firstTouch);
148
+ } catch (error) {
149
+ errorLog('Failed to store first touch:', error as Error);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Get first touch attribution (null if expired)
155
+ */
156
+ getFirstTouch(): TouchAttribution | null {
157
+ if (this.firstTouch && this.isExpired(this.firstTouch)) {
158
+ this.firstTouch = null;
159
+ Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH).catch(() => {});
160
+ }
161
+ return this.firstTouch;
162
+ }
163
+
164
+ /**
165
+ * Store last touch attribution (always updates)
166
+ */
167
+ async storeLastTouch(attribution: Partial<TouchAttribution>): Promise<void> {
168
+ try {
169
+ const now = Date.now();
170
+ this.lastTouch = {
171
+ ...attribution,
172
+ timestamp: attribution.timestamp || now,
173
+ captured_at: now,
174
+ expires_at: now + ATTRIBUTION_WINDOW_MS,
175
+ } as TouchAttribution;
176
+
177
+ await Storage.setItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH, this.lastTouch);
178
+ debugLog('Last touch stored:', this.lastTouch);
179
+ } catch (error) {
180
+ errorLog('Failed to store last touch:', error as Error);
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get last touch attribution (null if expired)
186
+ */
187
+ getLastTouch(): TouchAttribution | null {
188
+ if (this.lastTouch && this.isExpired(this.lastTouch)) {
189
+ this.lastTouch = null;
190
+ Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH).catch(() => {});
191
+ }
192
+ return this.lastTouch;
193
+ }
194
+
195
+ /**
196
+ * Add a touchpoint to the customer journey
197
+ */
198
+ async addTouchpoint(sessionId: string, attribution: Partial<TouchAttribution>): Promise<void> {
199
+ try {
200
+ const touchpoint: TouchPoint = {
201
+ timestamp: Date.now(),
202
+ sessionId,
203
+ source: attribution.source,
204
+ medium: attribution.medium,
205
+ campaign: attribution.campaign,
206
+ clickIdType: attribution.clickIdType,
207
+ };
208
+
209
+ this.journey.push(touchpoint);
210
+
211
+ // Keep only last MAX_TOUCHPOINTS
212
+ if (this.journey.length > MAX_TOUCHPOINTS) {
213
+ this.journey = this.journey.slice(-MAX_TOUCHPOINTS);
214
+ }
215
+
216
+ await Storage.setItem(JOURNEY_STORAGE_KEYS.JOURNEY, this.journey);
217
+ debugLog('Touchpoint added, total:', this.journey.length);
218
+ } catch (error) {
219
+ errorLog('Failed to add touchpoint:', error as Error);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Get customer journey (all touchpoints)
225
+ */
226
+ getJourney(): TouchPoint[] {
227
+ return [...this.journey];
228
+ }
229
+
230
+ /**
231
+ * Record attribution from a deep link or install
232
+ * Updates first-touch (if not set), last-touch, and adds touchpoint
233
+ */
234
+ async recordAttribution(sessionId: string, attribution: Partial<TouchAttribution>): Promise<void> {
235
+ // Only process if we have meaningful attribution data
236
+ const hasAttribution = attribution.source || attribution.clickId || attribution.campaign || attribution.lyr;
237
+
238
+ if (!hasAttribution) {
239
+ debugLog('No attribution data to record');
240
+ return;
241
+ }
242
+
243
+ // Store first touch if not set
244
+ if (!this.getFirstTouch()) {
245
+ await this.storeFirstTouch(attribution);
246
+ }
247
+
248
+ // Always update last touch
249
+ await this.storeLastTouch(attribution);
250
+
251
+ // Add touchpoint
252
+ await this.addTouchpoint(sessionId, attribution);
253
+ }
254
+
255
+ /**
256
+ * Get attribution data for events (mirrors Web SDK format)
257
+ */
258
+ getAttributionData(): Record<string, any> {
259
+ const firstTouch = this.getFirstTouch();
260
+ const lastTouch = this.getLastTouch();
261
+ const journey = this.getJourney();
262
+
263
+ return {
264
+ // First touch (with snake_case and camelCase aliases)
265
+ first_touch_source: firstTouch?.source,
266
+ first_touch_medium: firstTouch?.medium,
267
+ first_touch_campaign: firstTouch?.campaign,
268
+ first_touch_timestamp: firstTouch?.timestamp,
269
+ firstTouchSource: firstTouch?.source,
270
+ firstTouchMedium: firstTouch?.medium,
271
+ firstTouchCampaign: firstTouch?.campaign,
272
+
273
+ // Last touch
274
+ last_touch_source: lastTouch?.source,
275
+ last_touch_medium: lastTouch?.medium,
276
+ last_touch_campaign: lastTouch?.campaign,
277
+ last_touch_timestamp: lastTouch?.timestamp,
278
+ lastTouchSource: lastTouch?.source,
279
+ lastTouchMedium: lastTouch?.medium,
280
+ lastTouchCampaign: lastTouch?.campaign,
281
+
282
+ // Journey metrics
283
+ touchpoint_count: journey.length,
284
+ touchpointCount: journey.length,
285
+ days_since_first_touch: firstTouch?.timestamp
286
+ ? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
287
+ : 0,
288
+ daysSinceFirstTouch: firstTouch?.timestamp
289
+ ? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
290
+ : 0,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Clear all journey data (for testing/reset)
296
+ */
297
+ async clearJourney(): Promise<void> {
298
+ this.firstTouch = null;
299
+ this.lastTouch = null;
300
+ this.journey = [];
301
+
302
+ await Promise.all([
303
+ Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH),
304
+ Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH),
305
+ Storage.removeItem(JOURNEY_STORAGE_KEYS.JOURNEY),
306
+ ]);
307
+
308
+ debugLog('Journey data cleared');
309
+ }
310
+
311
+ /**
312
+ * Get journey summary for debugging
313
+ */
314
+ getJourneySummary(): {
315
+ hasFirstTouch: boolean;
316
+ hasLastTouch: boolean;
317
+ touchpointCount: number;
318
+ daysSinceFirstTouch: number;
319
+ sources: string[];
320
+ } {
321
+ const firstTouch = this.getFirstTouch();
322
+ const journey = this.getJourney();
323
+ const sources = [...new Set(journey.map(t => t.source).filter(Boolean))] as string[];
324
+
325
+ return {
326
+ hasFirstTouch: !!firstTouch,
327
+ hasLastTouch: !!this.getLastTouch(),
328
+ touchpointCount: journey.length,
329
+ daysSinceFirstTouch: firstTouch?.timestamp
330
+ ? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
331
+ : 0,
332
+ sources,
333
+ };
334
+ }
335
+ }
336
+
337
+ // Export singleton instance
338
+ export const journeyManager = new JourneyManager();
@@ -1,12 +1,16 @@
1
1
  /**
2
- * Native Bridge for Meta, TikTok, and Apple Search Ads
2
+ * Native Bridge for Meta, TikTok, Apple Search Ads, and Play Install Referrer
3
3
  * Uses bundled native modules instead of separate npm packages
4
+ *
5
+ * Supported Platforms:
6
+ * - iOS: Meta SDK, TikTok SDK, Apple Search Ads (AdServices)
7
+ * - Android: Meta SDK, TikTok SDK, Play Install Referrer
4
8
  */
5
9
 
6
10
  import { NativeModules, Platform } from 'react-native';
7
11
 
8
12
  /**
9
- * Apple Search Ads attribution data returned from AdServices API
13
+ * Apple Search Ads attribution data returned from AdServices API (iOS only)
10
14
  */
11
15
  export interface AppleSearchAdsAttribution {
12
16
  attribution: boolean;
@@ -23,6 +27,25 @@ export interface AppleSearchAdsAttribution {
23
27
  countryOrRegion?: string;
24
28
  }
25
29
 
30
+ /**
31
+ * Play Install Referrer data (Android only)
32
+ */
33
+ export interface PlayInstallReferrerData {
34
+ referrerUrl: string;
35
+ referrerClickTimestamp: number;
36
+ installBeginTimestamp: number;
37
+ installCompleteTimestamp?: number;
38
+ gclid?: string;
39
+ fbclid?: string;
40
+ ttclid?: string;
41
+ utmSource?: string;
42
+ utmMedium?: string;
43
+ utmCampaign?: string;
44
+ utmTerm?: string;
45
+ utmContent?: string;
46
+ referrer?: string;
47
+ }
48
+
26
49
  interface DatalyrNativeModule {
27
50
  // Meta SDK Methods
28
51
  initializeMetaSDK(
@@ -66,16 +89,29 @@ interface DatalyrNativeModule {
66
89
  logoutTikTok(): Promise<boolean>;
67
90
  updateTikTokTrackingAuthorization(enabled: boolean): Promise<boolean>;
68
91
 
69
- // Apple Search Ads Methods
92
+ // Apple Search Ads Methods (iOS only)
70
93
  getAppleSearchAdsAttribution(): Promise<AppleSearchAdsAttribution | null>;
71
94
 
72
95
  // SDK Availability
73
- getSDKAvailability(): Promise<{ meta: boolean; tiktok: boolean; appleSearchAds: boolean }>;
96
+ getSDKAvailability(): Promise<{
97
+ meta: boolean;
98
+ tiktok: boolean;
99
+ appleSearchAds: boolean;
100
+ playInstallReferrer?: boolean;
101
+ }>;
74
102
  }
75
103
 
76
- // Native module is only available on iOS
77
- const DatalyrNative: DatalyrNativeModule | null =
78
- Platform.OS === 'ios' ? NativeModules.DatalyrNative : null;
104
+ interface PlayInstallReferrerModule {
105
+ isAvailable(): Promise<boolean>;
106
+ getInstallReferrer(): Promise<PlayInstallReferrerData | null>;
107
+ }
108
+
109
+ // Native modules - available on both iOS and Android
110
+ const DatalyrNative: DatalyrNativeModule | null = NativeModules.DatalyrNative ?? null;
111
+
112
+ // Play Install Referrer - Android only
113
+ const DatalyrPlayInstallReferrer: PlayInstallReferrerModule | null =
114
+ Platform.OS === 'android' ? NativeModules.DatalyrPlayInstallReferrer : null;
79
115
 
80
116
  /**
81
117
  * Check if native module is available
@@ -85,21 +121,34 @@ export const isNativeModuleAvailable = (): boolean => {
85
121
  };
86
122
 
87
123
  /**
88
- * Get SDK availability status
124
+ * Get SDK availability status for all platforms
89
125
  */
90
126
  export const getSDKAvailability = async (): Promise<{
91
127
  meta: boolean;
92
128
  tiktok: boolean;
93
129
  appleSearchAds: boolean;
130
+ playInstallReferrer: boolean;
94
131
  }> => {
132
+ const defaultAvailability = {
133
+ meta: false,
134
+ tiktok: false,
135
+ appleSearchAds: false,
136
+ playInstallReferrer: false,
137
+ };
138
+
95
139
  if (!DatalyrNative) {
96
- return { meta: false, tiktok: false, appleSearchAds: false };
140
+ return defaultAvailability;
97
141
  }
98
142
 
99
143
  try {
100
- return await DatalyrNative.getSDKAvailability();
144
+ const result = await DatalyrNative.getSDKAvailability();
145
+ return {
146
+ ...defaultAvailability,
147
+ ...result,
148
+ playInstallReferrer: Platform.OS === 'android' && DatalyrPlayInstallReferrer !== null,
149
+ };
101
150
  } catch {
102
- return { meta: false, tiktok: false, appleSearchAds: false };
151
+ return defaultAvailability;
103
152
  }
104
153
  };
105
154
 
@@ -292,7 +341,7 @@ export const TikTokNativeBridge = {
292
341
  },
293
342
  };
294
343
 
295
- // MARK: - Apple Search Ads Bridge
344
+ // MARK: - Apple Search Ads Bridge (iOS only)
296
345
 
297
346
  export const AppleSearchAdsNativeBridge = {
298
347
  /**
@@ -301,7 +350,7 @@ export const AppleSearchAdsNativeBridge = {
301
350
  * Returns null if user didn't come from Apple Search Ads or on older iOS
302
351
  */
303
352
  async getAttribution(): Promise<AppleSearchAdsAttribution | null> {
304
- if (!DatalyrNative) return null;
353
+ if (!DatalyrNative || Platform.OS !== 'ios') return null;
305
354
 
306
355
  try {
307
356
  return await DatalyrNative.getAppleSearchAdsAttribution();
@@ -311,3 +360,40 @@ export const AppleSearchAdsNativeBridge = {
311
360
  }
312
361
  },
313
362
  };
363
+
364
+ // MARK: - Play Install Referrer Bridge (Android only)
365
+
366
+ export const PlayInstallReferrerNativeBridge = {
367
+ /**
368
+ * Check if Play Install Referrer is available
369
+ * Only available on Android with Google Play Services
370
+ */
371
+ async isAvailable(): Promise<boolean> {
372
+ if (!DatalyrPlayInstallReferrer || Platform.OS !== 'android') return false;
373
+
374
+ try {
375
+ return await DatalyrPlayInstallReferrer.isAvailable();
376
+ } catch {
377
+ return false;
378
+ }
379
+ },
380
+
381
+ /**
382
+ * Get install referrer data from Google Play
383
+ *
384
+ * Returns UTM parameters, click IDs (gclid, fbclid, ttclid), and timestamps
385
+ * from the Google Play Store referrer.
386
+ *
387
+ * Call this on first app launch to capture install attribution.
388
+ */
389
+ async getInstallReferrer(): Promise<PlayInstallReferrerData | null> {
390
+ if (!DatalyrPlayInstallReferrer || Platform.OS !== 'android') return null;
391
+
392
+ try {
393
+ return await DatalyrPlayInstallReferrer.getInstallReferrer();
394
+ } catch (error) {
395
+ console.error('[Datalyr/PlayInstallReferrer] Get referrer failed:', error);
396
+ return null;
397
+ }
398
+ },
399
+ };
@@ -1,14 +1,37 @@
1
1
  import { NativeModules, Platform } from 'react-native';
2
2
 
3
+ // SKAN 4.0 coarse value type
4
+ export type SKANCoarseValue = 'low' | 'medium' | 'high';
5
+
6
+ // SKAN 4.0 conversion result
7
+ export interface SKANConversionResult {
8
+ fineValue: number; // 0-63
9
+ coarseValue: SKANCoarseValue;
10
+ lockWindow: boolean;
11
+ priority: number;
12
+ }
13
+
3
14
  interface SKAdNetworkModule {
4
15
  updateConversionValue(value: number): Promise<boolean>;
16
+ updatePostbackConversionValue(
17
+ fineValue: number,
18
+ coarseValue: string,
19
+ lockWindow: boolean
20
+ ): Promise<boolean>;
21
+ isSKAN4Available(): Promise<boolean>;
5
22
  }
6
23
 
7
- const { DatalyrSKAdNetwork } = NativeModules as {
8
- DatalyrSKAdNetwork?: SKAdNetworkModule
24
+ const { DatalyrSKAdNetwork } = NativeModules as {
25
+ DatalyrSKAdNetwork?: SKAdNetworkModule
9
26
  };
10
27
 
11
28
  export class SKAdNetworkBridge {
29
+ private static _isSKAN4Available: boolean | null = null;
30
+
31
+ /**
32
+ * SKAN 3.0 - Update conversion value (0-63)
33
+ * @deprecated Use updatePostbackConversionValue for iOS 16.1+
34
+ */
12
35
  static async updateConversionValue(value: number): Promise<boolean> {
13
36
  if (Platform.OS !== 'ios') {
14
37
  return false; // Android doesn't support SKAdNetwork
@@ -29,6 +52,67 @@ export class SKAdNetworkBridge {
29
52
  }
30
53
  }
31
54
 
55
+ /**
56
+ * SKAN 4.0 - Update postback conversion value with coarse value and lock window
57
+ * Falls back to SKAN 3.0 on iOS 14.0-16.0
58
+ */
59
+ static async updatePostbackConversionValue(
60
+ result: SKANConversionResult
61
+ ): Promise<boolean> {
62
+ if (Platform.OS !== 'ios') {
63
+ return false; // Android doesn't support SKAdNetwork
64
+ }
65
+
66
+ if (!DatalyrSKAdNetwork) {
67
+ console.warn('[Datalyr] SKAdNetwork native module not found. Ensure native bridge is properly configured.');
68
+ return false;
69
+ }
70
+
71
+ try {
72
+ const success = await DatalyrSKAdNetwork.updatePostbackConversionValue(
73
+ result.fineValue,
74
+ result.coarseValue,
75
+ result.lockWindow
76
+ );
77
+
78
+ const isSKAN4 = await this.isSKAN4Available();
79
+ if (isSKAN4) {
80
+ console.log(`[Datalyr] SKAN 4.0 postback updated: fineValue=${result.fineValue}, coarseValue=${result.coarseValue}, lockWindow=${result.lockWindow}`);
81
+ } else {
82
+ console.log(`[Datalyr] SKAN 3.0 fallback: conversionValue=${result.fineValue}`);
83
+ }
84
+
85
+ return success;
86
+ } catch (error) {
87
+ console.warn('[Datalyr] Failed to update SKAdNetwork postback conversion value:', error);
88
+ return false;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Check if SKAN 4.0 is available (iOS 16.1+)
94
+ */
95
+ static async isSKAN4Available(): Promise<boolean> {
96
+ if (Platform.OS !== 'ios') {
97
+ return false;
98
+ }
99
+
100
+ if (this._isSKAN4Available !== null) {
101
+ return this._isSKAN4Available;
102
+ }
103
+
104
+ if (!DatalyrSKAdNetwork?.isSKAN4Available) {
105
+ return false;
106
+ }
107
+
108
+ try {
109
+ this._isSKAN4Available = await DatalyrSKAdNetwork.isSKAN4Available();
110
+ return this._isSKAN4Available;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
32
116
  static isAvailable(): boolean {
33
117
  return Platform.OS === 'ios' && !!DatalyrSKAdNetwork;
34
118
  }