@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,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,103 @@
1
1
  import { NativeModules, Platform } from 'react-native';
2
2
 
3
+ // SKAN 4.0 / AdAttributionKit coarse value type
4
+ export type SKANCoarseValue = 'low' | 'medium' | 'high';
5
+
6
+ // SKAN 4.0 / AdAttributionKit conversion result
7
+ export interface SKANConversionResult {
8
+ fineValue: number; // 0-63
9
+ coarseValue: SKANCoarseValue;
10
+ lockWindow: boolean;
11
+ priority: number;
12
+ }
13
+
14
+ // Response from native postback update
15
+ export interface PostbackUpdateResponse {
16
+ success: boolean;
17
+ framework: 'AdAttributionKit' | 'SKAdNetwork';
18
+ fineValue: number;
19
+ coarseValue: string;
20
+ lockWindow: boolean;
21
+ type?: 'reengagement'; // Only for re-engagement updates
22
+ }
23
+
24
+ // Attribution framework info returned by native module
25
+ export interface AttributionFrameworkInfo {
26
+ framework: 'AdAttributionKit' | 'SKAdNetwork' | 'none';
27
+ version: string;
28
+ reengagement_available: boolean;
29
+ overlapping_windows: boolean;
30
+ fine_value_range: { min: number; max: number };
31
+ coarse_values: string[];
32
+ }
33
+
34
+ // Enhanced attribution info for iOS 18.4+ with geo and dev postback support
35
+ export interface EnhancedAttributionInfo extends AttributionFrameworkInfo {
36
+ geo_postback_available: boolean;
37
+ development_postbacks: boolean;
38
+ features: string[];
39
+ }
40
+
41
+ // Postback environment configuration response
42
+ export interface PostbackEnvironmentResponse {
43
+ environment: 'production' | 'sandbox';
44
+ isSandbox: boolean;
45
+ note: string;
46
+ }
47
+
48
+ // Overlapping window postback response (iOS 18.4+)
49
+ export interface OverlappingWindowPostbackResponse {
50
+ success: boolean;
51
+ framework: string;
52
+ version: string;
53
+ fineValue: number;
54
+ coarseValue: string;
55
+ lockWindow: boolean;
56
+ windowIndex: number;
57
+ overlappingWindows: boolean;
58
+ note?: string;
59
+ }
60
+
3
61
  interface SKAdNetworkModule {
4
62
  updateConversionValue(value: number): Promise<boolean>;
63
+ updatePostbackConversionValue(
64
+ fineValue: number,
65
+ coarseValue: string,
66
+ lockWindow: boolean
67
+ ): Promise<PostbackUpdateResponse>;
68
+ updateReengagementConversionValue(
69
+ fineValue: number,
70
+ coarseValue: string,
71
+ lockWindow: boolean
72
+ ): Promise<PostbackUpdateResponse>;
73
+ isSKAN4Available(): Promise<boolean>;
74
+ isAdAttributionKitAvailable(): Promise<boolean>;
75
+ isOverlappingWindowsAvailable(): Promise<boolean>;
76
+ registerForAttribution(): Promise<{ framework: string; registered: boolean }>;
77
+ getAttributionInfo(): Promise<AttributionFrameworkInfo>;
78
+ // iOS 18.4+ methods
79
+ isGeoPostbackAvailable(): Promise<boolean>;
80
+ setPostbackEnvironment(environment: string): Promise<PostbackEnvironmentResponse>;
81
+ getEnhancedAttributionInfo(): Promise<EnhancedAttributionInfo>;
82
+ updatePostbackWithWindow(
83
+ fineValue: number,
84
+ coarseValue: string,
85
+ lockWindow: boolean,
86
+ windowIndex: number
87
+ ): Promise<OverlappingWindowPostbackResponse>;
5
88
  }
6
89
 
7
- const { DatalyrSKAdNetwork } = NativeModules as {
8
- DatalyrSKAdNetwork?: SKAdNetworkModule
90
+ const { DatalyrSKAdNetwork } = NativeModules as {
91
+ DatalyrSKAdNetwork?: SKAdNetworkModule
9
92
  };
10
93
 
11
94
  export class SKAdNetworkBridge {
95
+ private static _isSKAN4Available: boolean | null = null;
96
+
97
+ /**
98
+ * SKAN 3.0 - Update conversion value (0-63)
99
+ * @deprecated Use updatePostbackConversionValue for iOS 16.1+
100
+ */
12
101
  static async updateConversionValue(value: number): Promise<boolean> {
13
102
  if (Platform.OS !== 'ios') {
14
103
  return false; // Android doesn't support SKAdNetwork
@@ -29,7 +118,397 @@ export class SKAdNetworkBridge {
29
118
  }
30
119
  }
31
120
 
121
+ /**
122
+ * SKAN 4.0 - Update postback conversion value with coarse value and lock window
123
+ * Falls back to SKAN 3.0 on iOS 14.0-16.0
124
+ */
125
+ static async updatePostbackConversionValue(
126
+ result: SKANConversionResult
127
+ ): Promise<boolean> {
128
+ if (Platform.OS !== 'ios') {
129
+ return false; // Android doesn't support SKAdNetwork
130
+ }
131
+
132
+ if (!DatalyrSKAdNetwork) {
133
+ console.warn('[Datalyr] SKAdNetwork native module not found. Ensure native bridge is properly configured.');
134
+ return false;
135
+ }
136
+
137
+ try {
138
+ const response = await DatalyrSKAdNetwork.updatePostbackConversionValue(
139
+ result.fineValue,
140
+ result.coarseValue,
141
+ result.lockWindow
142
+ );
143
+
144
+ const isSKAN4 = await this.isSKAN4Available();
145
+ if (isSKAN4) {
146
+ console.log(`[Datalyr] SKAN 4.0 postback updated: fineValue=${result.fineValue}, coarseValue=${result.coarseValue}, lockWindow=${result.lockWindow}`);
147
+ } else {
148
+ console.log(`[Datalyr] SKAN 3.0 fallback: conversionValue=${result.fineValue}`);
149
+ }
150
+
151
+ return response.success;
152
+ } catch (error) {
153
+ console.warn('[Datalyr] Failed to update SKAdNetwork postback conversion value:', error);
154
+ return false;
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Check if SKAN 4.0 is available (iOS 16.1+)
160
+ */
161
+ static async isSKAN4Available(): Promise<boolean> {
162
+ if (Platform.OS !== 'ios') {
163
+ return false;
164
+ }
165
+
166
+ if (this._isSKAN4Available !== null) {
167
+ return this._isSKAN4Available;
168
+ }
169
+
170
+ if (!DatalyrSKAdNetwork?.isSKAN4Available) {
171
+ return false;
172
+ }
173
+
174
+ try {
175
+ this._isSKAN4Available = await DatalyrSKAdNetwork.isSKAN4Available();
176
+ return this._isSKAN4Available;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+
32
182
  static isAvailable(): boolean {
33
183
  return Platform.OS === 'ios' && !!DatalyrSKAdNetwork;
34
184
  }
185
+
186
+ /**
187
+ * Check if AdAttributionKit is available (iOS 17.4+)
188
+ * AdAttributionKit is Apple's replacement for SKAdNetwork with enhanced features
189
+ */
190
+ private static _isAdAttributionKitAvailable: boolean | null = null;
191
+
192
+ static async isAdAttributionKitAvailable(): Promise<boolean> {
193
+ if (Platform.OS !== 'ios') {
194
+ return false;
195
+ }
196
+
197
+ if (this._isAdAttributionKitAvailable !== null) {
198
+ return this._isAdAttributionKitAvailable;
199
+ }
200
+
201
+ if (!DatalyrSKAdNetwork?.isAdAttributionKitAvailable) {
202
+ return false;
203
+ }
204
+
205
+ try {
206
+ this._isAdAttributionKitAvailable = await DatalyrSKAdNetwork.isAdAttributionKitAvailable();
207
+ return this._isAdAttributionKitAvailable;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Register for ad network attribution
215
+ * Uses AdAttributionKit on iOS 17.4+, SKAdNetwork on earlier versions
216
+ */
217
+ static async registerForAttribution(): Promise<{ framework: string; registered: boolean } | null> {
218
+ if (Platform.OS !== 'ios') {
219
+ return null;
220
+ }
221
+
222
+ if (!DatalyrSKAdNetwork?.registerForAttribution) {
223
+ console.warn('[Datalyr] Attribution registration not available');
224
+ return null;
225
+ }
226
+
227
+ try {
228
+ const result = await DatalyrSKAdNetwork.registerForAttribution();
229
+ console.log(`[Datalyr] Registered for attribution: ${result.framework}`);
230
+ return result;
231
+ } catch (error) {
232
+ console.warn('[Datalyr] Failed to register for attribution:', error);
233
+ return null;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get attribution framework info
239
+ * Returns details about which framework is being used and its capabilities
240
+ */
241
+ static async getAttributionInfo(): Promise<AttributionFrameworkInfo | null> {
242
+ if (Platform.OS !== 'ios') {
243
+ return {
244
+ framework: 'none',
245
+ version: '0',
246
+ reengagement_available: false,
247
+ overlapping_windows: false,
248
+ fine_value_range: { min: 0, max: 0 },
249
+ coarse_values: [],
250
+ };
251
+ }
252
+
253
+ if (!DatalyrSKAdNetwork?.getAttributionInfo) {
254
+ return null;
255
+ }
256
+
257
+ try {
258
+ return await DatalyrSKAdNetwork.getAttributionInfo();
259
+ } catch (error) {
260
+ console.warn('[Datalyr] Failed to get attribution info:', error);
261
+ return null;
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Check if overlapping conversion windows are available (iOS 18.4+)
267
+ * Overlapping windows allow multiple conversion windows to be active simultaneously
268
+ */
269
+ private static _isOverlappingWindowsAvailable: boolean | null = null;
270
+
271
+ static async isOverlappingWindowsAvailable(): Promise<boolean> {
272
+ if (Platform.OS !== 'ios') {
273
+ return false;
274
+ }
275
+
276
+ if (this._isOverlappingWindowsAvailable !== null) {
277
+ return this._isOverlappingWindowsAvailable;
278
+ }
279
+
280
+ if (!DatalyrSKAdNetwork?.isOverlappingWindowsAvailable) {
281
+ return false;
282
+ }
283
+
284
+ try {
285
+ this._isOverlappingWindowsAvailable = await DatalyrSKAdNetwork.isOverlappingWindowsAvailable();
286
+ return this._isOverlappingWindowsAvailable;
287
+ } catch {
288
+ return false;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Update conversion value for re-engagement attribution (AdAttributionKit iOS 17.4+ only)
294
+ * Re-engagement tracks users who return to the app via an ad after initial install.
295
+ *
296
+ * @param result - Conversion result with fine value (0-63), coarse value, and lock window
297
+ * @returns Response with framework info, or null if not supported
298
+ */
299
+ static async updateReengagementConversionValue(
300
+ result: SKANConversionResult
301
+ ): Promise<PostbackUpdateResponse | null> {
302
+ if (Platform.OS !== 'ios') {
303
+ return null; // Android doesn't support AdAttributionKit
304
+ }
305
+
306
+ // Check if AdAttributionKit is available (required for re-engagement)
307
+ const isAAKAvailable = await this.isAdAttributionKitAvailable();
308
+ if (!isAAKAvailable) {
309
+ console.warn('[Datalyr] Re-engagement attribution requires iOS 17.4+ (AdAttributionKit)');
310
+ return null;
311
+ }
312
+
313
+ if (!DatalyrSKAdNetwork?.updateReengagementConversionValue) {
314
+ console.warn('[Datalyr] Re-engagement native module not available');
315
+ return null;
316
+ }
317
+
318
+ try {
319
+ const response = await DatalyrSKAdNetwork.updateReengagementConversionValue(
320
+ result.fineValue,
321
+ result.coarseValue,
322
+ result.lockWindow
323
+ );
324
+
325
+ console.log(`[Datalyr] AdAttributionKit re-engagement updated: fineValue=${result.fineValue}, coarseValue=${result.coarseValue}, lockWindow=${result.lockWindow}`);
326
+ return response;
327
+ } catch (error) {
328
+ console.warn('[Datalyr] Failed to update re-engagement conversion value:', error);
329
+ return null;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Get a summary of attribution capabilities for the current device
335
+ */
336
+ static async getCapabilitiesSummary(): Promise<{
337
+ skadnetwork3: boolean;
338
+ skadnetwork4: boolean;
339
+ adAttributionKit: boolean;
340
+ reengagement: boolean;
341
+ overlappingWindows: boolean;
342
+ geoPostback: boolean;
343
+ developmentPostbacks: boolean;
344
+ framework: string;
345
+ }> {
346
+ const info = await this.getAttributionInfo();
347
+ const isSKAN4 = await this.isSKAN4Available();
348
+ const isAAK = await this.isAdAttributionKitAvailable();
349
+ const isOverlapping = await this.isOverlappingWindowsAvailable();
350
+ const isGeo = await this.isGeoPostbackAvailable();
351
+
352
+ return {
353
+ skadnetwork3: Platform.OS === 'ios',
354
+ skadnetwork4: isSKAN4,
355
+ adAttributionKit: isAAK,
356
+ reengagement: info?.reengagement_available ?? false,
357
+ overlappingWindows: isOverlapping,
358
+ geoPostback: isGeo,
359
+ developmentPostbacks: isGeo, // Same iOS version requirement
360
+ framework: info?.framework ?? 'none',
361
+ };
362
+ }
363
+
364
+ // ===== iOS 18.4+ Features =====
365
+
366
+ /**
367
+ * Check if geo-level postback data is available (iOS 18.4+)
368
+ * Geo postbacks include country code information for regional analytics
369
+ */
370
+ private static _isGeoPostbackAvailable: boolean | null = null;
371
+
372
+ static async isGeoPostbackAvailable(): Promise<boolean> {
373
+ if (Platform.OS !== 'ios') {
374
+ return false;
375
+ }
376
+
377
+ if (this._isGeoPostbackAvailable !== null) {
378
+ return this._isGeoPostbackAvailable;
379
+ }
380
+
381
+ if (!DatalyrSKAdNetwork?.isGeoPostbackAvailable) {
382
+ return false;
383
+ }
384
+
385
+ try {
386
+ this._isGeoPostbackAvailable = await DatalyrSKAdNetwork.isGeoPostbackAvailable();
387
+ return this._isGeoPostbackAvailable;
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Set postback environment for testing (iOS 18.4+)
395
+ * Note: Actual sandbox mode requires Developer Mode enabled in iOS Settings
396
+ *
397
+ * @param environment - 'production' or 'sandbox'
398
+ */
399
+ static async setPostbackEnvironment(
400
+ environment: 'production' | 'sandbox'
401
+ ): Promise<PostbackEnvironmentResponse | null> {
402
+ if (Platform.OS !== 'ios') {
403
+ return null;
404
+ }
405
+
406
+ if (!DatalyrSKAdNetwork?.setPostbackEnvironment) {
407
+ console.warn('[Datalyr] Development postbacks require iOS 18.4+');
408
+ return null;
409
+ }
410
+
411
+ try {
412
+ const result = await DatalyrSKAdNetwork.setPostbackEnvironment(environment);
413
+ console.log(`[Datalyr] Postback environment: ${result.environment}`);
414
+ return result;
415
+ } catch (error) {
416
+ console.warn('[Datalyr] Failed to set postback environment:', error);
417
+ return null;
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Get enhanced attribution info including iOS 18.4+ features
423
+ * Returns details about geo postbacks, development mode, and all available features
424
+ */
425
+ static async getEnhancedAttributionInfo(): Promise<EnhancedAttributionInfo | null> {
426
+ if (Platform.OS !== 'ios') {
427
+ return {
428
+ framework: 'none',
429
+ version: '0',
430
+ reengagement_available: false,
431
+ overlapping_windows: false,
432
+ geo_postback_available: false,
433
+ development_postbacks: false,
434
+ fine_value_range: { min: 0, max: 0 },
435
+ coarse_values: [],
436
+ features: [],
437
+ };
438
+ }
439
+
440
+ if (!DatalyrSKAdNetwork?.getEnhancedAttributionInfo) {
441
+ // Fallback to basic info if enhanced not available
442
+ const basicInfo = await this.getAttributionInfo();
443
+ if (basicInfo) {
444
+ return {
445
+ ...basicInfo,
446
+ geo_postback_available: false,
447
+ development_postbacks: false,
448
+ features: [],
449
+ };
450
+ }
451
+ return null;
452
+ }
453
+
454
+ try {
455
+ return await DatalyrSKAdNetwork.getEnhancedAttributionInfo();
456
+ } catch (error) {
457
+ console.warn('[Datalyr] Failed to get enhanced attribution info:', error);
458
+ return null;
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Update postback with overlapping window support (iOS 18.4+)
464
+ * Allows tracking conversions across multiple time windows simultaneously
465
+ *
466
+ * @param result - Conversion result with fine value, coarse value, and lock window
467
+ * @param windowIndex - Window index: 0 (0-2 days), 1 (3-7 days), 2 (8-35 days)
468
+ */
469
+ static async updatePostbackWithWindow(
470
+ result: SKANConversionResult,
471
+ windowIndex: 0 | 1 | 2
472
+ ): Promise<OverlappingWindowPostbackResponse | null> {
473
+ if (Platform.OS !== 'ios') {
474
+ return null;
475
+ }
476
+
477
+ if (!DatalyrSKAdNetwork?.updatePostbackWithWindow) {
478
+ console.warn('[Datalyr] Overlapping windows require iOS 16.1+ (full support on iOS 18.4+)');
479
+ return null;
480
+ }
481
+
482
+ try {
483
+ const response = await DatalyrSKAdNetwork.updatePostbackWithWindow(
484
+ result.fineValue,
485
+ result.coarseValue,
486
+ result.lockWindow,
487
+ windowIndex
488
+ );
489
+
490
+ console.log(`[Datalyr] Postback updated for window ${windowIndex}: fineValue=${result.fineValue}, overlapping=${response.overlappingWindows}`);
491
+ return response;
492
+ } catch (error) {
493
+ console.warn('[Datalyr] Failed to update postback with window:', error);
494
+ return null;
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Enable development/sandbox mode for testing attribution
500
+ * Convenience method that sets sandbox environment
501
+ */
502
+ static async enableDevelopmentMode(): Promise<boolean> {
503
+ const result = await this.setPostbackEnvironment('sandbox');
504
+ return result?.isSandbox ?? false;
505
+ }
506
+
507
+ /**
508
+ * Disable development mode (switch to production)
509
+ */
510
+ static async disableDevelopmentMode(): Promise<boolean> {
511
+ const result = await this.setPostbackEnvironment('production');
512
+ return result !== null && !result.isSandbox;
513
+ }
35
514
  }