@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
@@ -0,0 +1,30 @@
1
+ package com.datalyr.reactnative;
2
+
3
+ import com.facebook.react.ReactPackage;
4
+ import com.facebook.react.bridge.NativeModule;
5
+ import com.facebook.react.bridge.ReactApplicationContext;
6
+ import com.facebook.react.uimanager.ViewManager;
7
+
8
+ import java.util.ArrayList;
9
+ import java.util.Collections;
10
+ import java.util.List;
11
+
12
+ /**
13
+ * React Native Package for Datalyr SDK
14
+ * Registers all native modules (Meta, TikTok, Play Install Referrer)
15
+ */
16
+ public class DatalyrPackage implements ReactPackage {
17
+
18
+ @Override
19
+ public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
20
+ List<NativeModule> modules = new ArrayList<>();
21
+ modules.add(new DatalyrNativeModule(reactContext));
22
+ modules.add(new DatalyrPlayInstallReferrerModule(reactContext));
23
+ return modules;
24
+ }
25
+
26
+ @Override
27
+ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
28
+ return Collections.emptyList();
29
+ }
30
+ }
@@ -0,0 +1,229 @@
1
+ package com.datalyr.reactnative;
2
+
3
+ import android.os.RemoteException;
4
+ import android.util.Log;
5
+
6
+ import com.facebook.react.bridge.Arguments;
7
+ import com.facebook.react.bridge.Promise;
8
+ import com.facebook.react.bridge.ReactApplicationContext;
9
+ import com.facebook.react.bridge.ReactContextBaseJavaModule;
10
+ import com.facebook.react.bridge.ReactMethod;
11
+ import com.facebook.react.bridge.WritableMap;
12
+
13
+ import com.android.installreferrer.api.InstallReferrerClient;
14
+ import com.android.installreferrer.api.InstallReferrerStateListener;
15
+ import com.android.installreferrer.api.ReferrerDetails;
16
+
17
+ import java.io.UnsupportedEncodingException;
18
+ import java.net.URLDecoder;
19
+ import java.util.HashMap;
20
+ import java.util.Map;
21
+
22
+ /**
23
+ * Google Play Install Referrer Module for Android
24
+ *
25
+ * Captures install attribution data from Google Play Store:
26
+ * - UTM parameters (utm_source, utm_medium, utm_campaign, etc.)
27
+ * - Google Ads click ID (gclid)
28
+ * - Referrer timestamps
29
+ *
30
+ * This data is critical for attributing installs to marketing campaigns.
31
+ */
32
+ public class DatalyrPlayInstallReferrerModule extends ReactContextBaseJavaModule {
33
+ private static final String TAG = "DatalyrPlayReferrer";
34
+ private static final String MODULE_NAME = "DatalyrPlayInstallReferrer";
35
+
36
+ private final ReactApplicationContext reactContext;
37
+ private InstallReferrerClient referrerClient;
38
+
39
+ public DatalyrPlayInstallReferrerModule(ReactApplicationContext context) {
40
+ super(context);
41
+ this.reactContext = context;
42
+ }
43
+
44
+ @Override
45
+ public String getName() {
46
+ return MODULE_NAME;
47
+ }
48
+
49
+ /**
50
+ * Check if Play Install Referrer is available
51
+ */
52
+ @ReactMethod
53
+ public void isAvailable(Promise promise) {
54
+ try {
55
+ // Check if the Play Install Referrer library is available
56
+ Class.forName("com.android.installreferrer.api.InstallReferrerClient");
57
+ promise.resolve(true);
58
+ } catch (ClassNotFoundException e) {
59
+ promise.resolve(false);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Get install referrer data from Google Play
65
+ *
66
+ * Returns an object with:
67
+ * - referrerUrl: The full referrer URL
68
+ * - referrerClickTimestamp: When the referrer link was clicked (ms)
69
+ * - installBeginTimestamp: When the install began (ms)
70
+ * - installCompleteTimestamp: When install was completed (ms) - Android 10+
71
+ * - gclid: Google Ads click ID (if present)
72
+ * - utmSource, utmMedium, utmCampaign, etc.
73
+ */
74
+ @ReactMethod
75
+ public void getInstallReferrer(final Promise promise) {
76
+ try {
77
+ referrerClient = InstallReferrerClient.newBuilder(reactContext.getApplicationContext()).build();
78
+
79
+ referrerClient.startConnection(new InstallReferrerStateListener() {
80
+ @Override
81
+ public void onInstallReferrerSetupFinished(int responseCode) {
82
+ switch (responseCode) {
83
+ case InstallReferrerClient.InstallReferrerResponse.OK:
84
+ try {
85
+ ReferrerDetails details = referrerClient.getInstallReferrer();
86
+ WritableMap result = parseReferrerDetails(details);
87
+ promise.resolve(result);
88
+ } catch (RemoteException e) {
89
+ Log.e(TAG, "Failed to get install referrer", e);
90
+ promise.resolve(null);
91
+ } finally {
92
+ referrerClient.endConnection();
93
+ }
94
+ break;
95
+
96
+ case InstallReferrerClient.InstallReferrerResponse.FEATURE_NOT_SUPPORTED:
97
+ Log.d(TAG, "Install referrer not supported on this device");
98
+ promise.resolve(null);
99
+ referrerClient.endConnection();
100
+ break;
101
+
102
+ case InstallReferrerClient.InstallReferrerResponse.SERVICE_UNAVAILABLE:
103
+ Log.d(TAG, "Install referrer service unavailable");
104
+ promise.resolve(null);
105
+ referrerClient.endConnection();
106
+ break;
107
+
108
+ default:
109
+ Log.d(TAG, "Install referrer unknown response: " + responseCode);
110
+ promise.resolve(null);
111
+ referrerClient.endConnection();
112
+ break;
113
+ }
114
+ }
115
+
116
+ @Override
117
+ public void onInstallReferrerServiceDisconnected() {
118
+ Log.d(TAG, "Install referrer service disconnected");
119
+ // Connection lost - can try to reconnect if needed
120
+ }
121
+ });
122
+ } catch (Exception e) {
123
+ Log.e(TAG, "Failed to start install referrer client", e);
124
+ promise.resolve(null);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Parse ReferrerDetails into a WritableMap with UTM parameters extracted
130
+ */
131
+ private WritableMap parseReferrerDetails(ReferrerDetails details) {
132
+ WritableMap result = Arguments.createMap();
133
+
134
+ try {
135
+ String referrerUrl = details.getInstallReferrer();
136
+ result.putString("referrerUrl", referrerUrl);
137
+ result.putDouble("referrerClickTimestamp", details.getReferrerClickTimestampSeconds() * 1000.0);
138
+ result.putDouble("installBeginTimestamp", details.getInstallBeginTimestampSeconds() * 1000.0);
139
+
140
+ // Android 10+ has install complete timestamp
141
+ try {
142
+ long installCompleteTs = details.getInstallBeginTimestampServerSeconds();
143
+ if (installCompleteTs > 0) {
144
+ result.putDouble("installCompleteTimestamp", installCompleteTs * 1000.0);
145
+ }
146
+ } catch (NoSuchMethodError e) {
147
+ // Method not available on older Android versions
148
+ }
149
+
150
+ // Parse UTM parameters from referrer URL
151
+ if (referrerUrl != null && !referrerUrl.isEmpty()) {
152
+ Map<String, String> params = parseReferrerUrl(referrerUrl);
153
+
154
+ // UTM parameters
155
+ if (params.containsKey("utm_source")) {
156
+ result.putString("utmSource", params.get("utm_source"));
157
+ }
158
+ if (params.containsKey("utm_medium")) {
159
+ result.putString("utmMedium", params.get("utm_medium"));
160
+ }
161
+ if (params.containsKey("utm_campaign")) {
162
+ result.putString("utmCampaign", params.get("utm_campaign"));
163
+ }
164
+ if (params.containsKey("utm_term")) {
165
+ result.putString("utmTerm", params.get("utm_term"));
166
+ }
167
+ if (params.containsKey("utm_content")) {
168
+ result.putString("utmContent", params.get("utm_content"));
169
+ }
170
+
171
+ // Google Ads click ID
172
+ if (params.containsKey("gclid")) {
173
+ result.putString("gclid", params.get("gclid"));
174
+ }
175
+
176
+ // Other potential click IDs
177
+ if (params.containsKey("fbclid")) {
178
+ result.putString("fbclid", params.get("fbclid"));
179
+ }
180
+ if (params.containsKey("ttclid")) {
181
+ result.putString("ttclid", params.get("ttclid"));
182
+ }
183
+
184
+ // App referrer (used by some attribution providers)
185
+ if (params.containsKey("referrer")) {
186
+ result.putString("referrer", params.get("referrer"));
187
+ }
188
+ }
189
+
190
+ Log.d(TAG, "Install referrer parsed successfully");
191
+
192
+ } catch (Exception e) {
193
+ Log.e(TAG, "Failed to parse referrer details", e);
194
+ }
195
+
196
+ return result;
197
+ }
198
+
199
+ /**
200
+ * Parse URL-encoded referrer string into key-value pairs
201
+ */
202
+ private Map<String, String> parseReferrerUrl(String referrerUrl) {
203
+ Map<String, String> params = new HashMap<>();
204
+
205
+ if (referrerUrl == null || referrerUrl.isEmpty()) {
206
+ return params;
207
+ }
208
+
209
+ try {
210
+ // Decode the URL-encoded referrer
211
+ String decoded = URLDecoder.decode(referrerUrl, "UTF-8");
212
+
213
+ // Split by & to get key=value pairs
214
+ String[] pairs = decoded.split("&");
215
+ for (String pair : pairs) {
216
+ int idx = pair.indexOf('=');
217
+ if (idx > 0 && idx < pair.length() - 1) {
218
+ String key = pair.substring(0, idx);
219
+ String value = pair.substring(idx + 1);
220
+ params.put(key, value);
221
+ }
222
+ }
223
+ } catch (UnsupportedEncodingException e) {
224
+ Log.e(TAG, "Failed to decode referrer URL", e);
225
+ }
226
+
227
+ return params;
228
+ }
229
+ }
@@ -20,8 +20,8 @@ Pod::Spec.new do |s|
20
20
  s.swift_version = "5.0"
21
21
 
22
22
  s.dependency "React-Core"
23
- s.dependency "FBSDKCoreKit", "~> 17.0"
24
- s.dependency "TikTokBusinessSDK", "~> 1.4"
23
+ s.dependency "FBSDKCoreKit", "~> 18.0"
24
+ s.dependency "TikTokBusinessSDK", "~> 1.6"
25
25
 
26
26
  # Disable bitcode (required for TikTok SDK)
27
27
  s.pod_target_xcconfig = {
@@ -8,6 +8,7 @@
8
8
 
9
9
  RCT_EXPORT_MODULE();
10
10
 
11
+ // SKAN 3.0 - Legacy method for iOS 14.0-16.0
11
12
  RCT_EXPORT_METHOD(updateConversionValue:(NSInteger)value
12
13
  resolve:(RCTPromiseResolveBlock)resolve
13
14
  reject:(RCTPromiseRejectBlock)reject) {
@@ -23,4 +24,402 @@ RCT_EXPORT_METHOD(updateConversionValue:(NSInteger)value
23
24
  }
24
25
  }
25
26
 
26
- @end
27
+ // SKAN 4.0 / AdAttributionKit - Method for iOS 16.1+ with coarse value and lock window support
28
+ // On iOS 17.4+, this uses AdAttributionKit under the hood
29
+ RCT_EXPORT_METHOD(updatePostbackConversionValue:(NSInteger)fineValue
30
+ coarseValue:(NSString *)coarseValue
31
+ lockWindow:(BOOL)lockWindow
32
+ resolve:(RCTPromiseResolveBlock)resolve
33
+ reject:(RCTPromiseRejectBlock)reject) {
34
+ // Validate fine value range
35
+ if (fineValue < 0 || fineValue > 63) {
36
+ reject(@"invalid_value", @"Conversion value must be between 0 and 63", nil);
37
+ return;
38
+ }
39
+
40
+ if (@available(iOS 16.1, *)) {
41
+ // Convert string to SKAdNetwork.CoarseConversionValue
42
+ SKAdNetworkCoarseConversionValue coarse;
43
+ if ([coarseValue isEqualToString:@"high"]) {
44
+ coarse = SKAdNetworkCoarseConversionValueHigh;
45
+ } else if ([coarseValue isEqualToString:@"medium"]) {
46
+ coarse = SKAdNetworkCoarseConversionValueMedium;
47
+ } else {
48
+ coarse = SKAdNetworkCoarseConversionValueLow;
49
+ }
50
+
51
+ [SKAdNetwork updatePostbackConversionValue:fineValue
52
+ coarseValue:coarse
53
+ lockWindow:lockWindow
54
+ completionHandler:^(NSError * _Nullable error) {
55
+ if (error) {
56
+ reject(@"skadnetwork_error", error.localizedDescription, error);
57
+ } else {
58
+ // Return framework info along with success
59
+ NSString *framework = @"SKAdNetwork";
60
+ if (@available(iOS 17.4, *)) {
61
+ framework = @"AdAttributionKit";
62
+ }
63
+ resolve(@{
64
+ @"success": @(YES),
65
+ @"framework": framework,
66
+ @"fineValue": @(fineValue),
67
+ @"coarseValue": coarseValue,
68
+ @"lockWindow": @(lockWindow)
69
+ });
70
+ }
71
+ }];
72
+ } else if (@available(iOS 14.0, *)) {
73
+ // Fallback to SKAN 3.0 for iOS 14.0-16.0
74
+ @try {
75
+ [SKAdNetwork updateConversionValue:fineValue];
76
+ resolve(@{
77
+ @"success": @(YES),
78
+ @"framework": @"SKAdNetwork",
79
+ @"fineValue": @(fineValue),
80
+ @"coarseValue": @"n/a",
81
+ @"lockWindow": @(NO)
82
+ });
83
+ } @catch (NSException *exception) {
84
+ reject(@"skadnetwork_error", exception.reason, nil);
85
+ }
86
+ } else {
87
+ reject(@"ios_version_error", @"SKAdNetwork requires iOS 14.0+", nil);
88
+ }
89
+ }
90
+
91
+ // Check if SKAN 4.0 is available (iOS 16.1+)
92
+ RCT_EXPORT_METHOD(isSKAN4Available:(RCTPromiseResolveBlock)resolve
93
+ reject:(RCTPromiseRejectBlock)reject) {
94
+ if (@available(iOS 16.1, *)) {
95
+ resolve(@(YES));
96
+ } else {
97
+ resolve(@(NO));
98
+ }
99
+ }
100
+
101
+ // Check if AdAttributionKit is available (iOS 17.4+)
102
+ RCT_EXPORT_METHOD(isAdAttributionKitAvailable:(RCTPromiseResolveBlock)resolve
103
+ reject:(RCTPromiseRejectBlock)reject) {
104
+ if (@available(iOS 17.4, *)) {
105
+ resolve(@(YES));
106
+ } else {
107
+ resolve(@(NO));
108
+ }
109
+ }
110
+
111
+ // Check if overlapping windows are available (iOS 18.4+)
112
+ RCT_EXPORT_METHOD(isOverlappingWindowsAvailable:(RCTPromiseResolveBlock)resolve
113
+ reject:(RCTPromiseRejectBlock)reject) {
114
+ if (@available(iOS 18.4, *)) {
115
+ resolve(@(YES));
116
+ } else {
117
+ resolve(@(NO));
118
+ }
119
+ }
120
+
121
+ // Register for ad network attribution (supports both AdAttributionKit and SKAdNetwork)
122
+ RCT_EXPORT_METHOD(registerForAttribution:(RCTPromiseResolveBlock)resolve
123
+ reject:(RCTPromiseRejectBlock)reject) {
124
+ if (@available(iOS 17.4, *)) {
125
+ // AdAttributionKit registration via initial conversion value update
126
+ [SKAdNetwork updatePostbackConversionValue:0
127
+ coarseValue:SKAdNetworkCoarseConversionValueLow
128
+ lockWindow:NO
129
+ completionHandler:^(NSError * _Nullable error) {
130
+ if (error) {
131
+ reject(@"attribution_error", error.localizedDescription, error);
132
+ } else {
133
+ resolve(@{@"framework": @"AdAttributionKit", @"registered": @(YES)});
134
+ }
135
+ }];
136
+ } else if (@available(iOS 14.0, *)) {
137
+ // Legacy SKAdNetwork registration
138
+ [SKAdNetwork registerAppForAdNetworkAttribution];
139
+ resolve(@{@"framework": @"SKAdNetwork", @"registered": @(YES)});
140
+ } else {
141
+ reject(@"ios_version_error", @"Attribution requires iOS 14.0+", nil);
142
+ }
143
+ }
144
+
145
+ // Get attribution framework info
146
+ RCT_EXPORT_METHOD(getAttributionInfo:(RCTPromiseResolveBlock)resolve
147
+ reject:(RCTPromiseRejectBlock)reject) {
148
+ NSMutableDictionary *info = [NSMutableDictionary dictionary];
149
+
150
+ if (@available(iOS 17.4, *)) {
151
+ info[@"framework"] = @"AdAttributionKit";
152
+ info[@"version"] = @"1.0";
153
+ info[@"reengagement_available"] = @(YES);
154
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
155
+ info[@"coarse_values"] = @[@"low", @"medium", @"high"];
156
+ if (@available(iOS 18.4, *)) {
157
+ info[@"overlapping_windows"] = @(YES);
158
+ } else {
159
+ info[@"overlapping_windows"] = @(NO);
160
+ }
161
+ } else if (@available(iOS 16.1, *)) {
162
+ info[@"framework"] = @"SKAdNetwork";
163
+ info[@"version"] = @"4.0";
164
+ info[@"reengagement_available"] = @(NO);
165
+ info[@"overlapping_windows"] = @(NO);
166
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
167
+ info[@"coarse_values"] = @[@"low", @"medium", @"high"];
168
+ } else if (@available(iOS 14.0, *)) {
169
+ info[@"framework"] = @"SKAdNetwork";
170
+ info[@"version"] = @"3.0";
171
+ info[@"reengagement_available"] = @(NO);
172
+ info[@"overlapping_windows"] = @(NO);
173
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
174
+ info[@"coarse_values"] = @[];
175
+ } else {
176
+ info[@"framework"] = @"none";
177
+ info[@"version"] = @"0";
178
+ info[@"reengagement_available"] = @(NO);
179
+ info[@"overlapping_windows"] = @(NO);
180
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @0};
181
+ info[@"coarse_values"] = @[];
182
+ }
183
+
184
+ resolve(info);
185
+ }
186
+
187
+ // Update conversion value for re-engagement (AdAttributionKit iOS 17.4+ only)
188
+ // Re-engagement tracks users who return to the app via an ad after initial install
189
+ RCT_EXPORT_METHOD(updateReengagementConversionValue:(NSInteger)fineValue
190
+ coarseValue:(NSString *)coarseValue
191
+ lockWindow:(BOOL)lockWindow
192
+ resolve:(RCTPromiseResolveBlock)resolve
193
+ reject:(RCTPromiseRejectBlock)reject) {
194
+ if (@available(iOS 17.4, *)) {
195
+ // Validate fine value range
196
+ if (fineValue < 0 || fineValue > 63) {
197
+ reject(@"invalid_value", @"Conversion value must be between 0 and 63", nil);
198
+ return;
199
+ }
200
+
201
+ // Convert string to SKAdNetwork.CoarseConversionValue
202
+ SKAdNetworkCoarseConversionValue coarse;
203
+ if ([coarseValue isEqualToString:@"high"]) {
204
+ coarse = SKAdNetworkCoarseConversionValueHigh;
205
+ } else if ([coarseValue isEqualToString:@"medium"]) {
206
+ coarse = SKAdNetworkCoarseConversionValueMedium;
207
+ } else {
208
+ coarse = SKAdNetworkCoarseConversionValueLow;
209
+ }
210
+
211
+ // In AdAttributionKit, re-engagement uses the same API
212
+ // The framework distinguishes based on user attribution state
213
+ [SKAdNetwork updatePostbackConversionValue:fineValue
214
+ coarseValue:coarse
215
+ lockWindow:lockWindow
216
+ completionHandler:^(NSError * _Nullable error) {
217
+ if (error) {
218
+ reject(@"reengagement_error", error.localizedDescription, error);
219
+ } else {
220
+ resolve(@{
221
+ @"success": @(YES),
222
+ @"type": @"reengagement",
223
+ @"framework": @"AdAttributionKit",
224
+ @"fineValue": @(fineValue),
225
+ @"coarseValue": coarseValue,
226
+ @"lockWindow": @(lockWindow)
227
+ });
228
+ }
229
+ }];
230
+ } else {
231
+ reject(@"unsupported", @"Re-engagement attribution requires iOS 17.4+ (AdAttributionKit)", nil);
232
+ }
233
+ }
234
+
235
+ // iOS 18.4+ - Check if geo-level postback data is available
236
+ RCT_EXPORT_METHOD(isGeoPostbackAvailable:(RCTPromiseResolveBlock)resolve
237
+ reject:(RCTPromiseRejectBlock)reject) {
238
+ if (@available(iOS 18.4, *)) {
239
+ resolve(@(YES));
240
+ } else {
241
+ resolve(@(NO));
242
+ }
243
+ }
244
+
245
+ // iOS 18.4+ - Set postback environment for testing
246
+ // environment: "production" or "sandbox"
247
+ RCT_EXPORT_METHOD(setPostbackEnvironment:(NSString *)environment
248
+ resolve:(RCTPromiseResolveBlock)resolve
249
+ reject:(RCTPromiseRejectBlock)reject) {
250
+ if (@available(iOS 18.4, *)) {
251
+ // Note: In iOS 18.4+, development postbacks are controlled via
252
+ // the developer mode setting in device settings, not programmatically.
253
+ // This method validates the environment string and logs for debugging.
254
+ BOOL isSandbox = [environment isEqualToString:@"sandbox"];
255
+ NSLog(@"[Datalyr] Postback environment set to: %@ (note: actual sandbox mode is controlled via device Developer Mode)", environment);
256
+ resolve(@{
257
+ @"environment": environment,
258
+ @"isSandbox": @(isSandbox),
259
+ @"note": @"Enable Developer Mode in iOS Settings for sandbox postbacks"
260
+ });
261
+ } else {
262
+ reject(@"unsupported", @"Development postbacks require iOS 18.4+", nil);
263
+ }
264
+ }
265
+
266
+ // iOS 18.4+ - Get enhanced attribution info including geo availability
267
+ RCT_EXPORT_METHOD(getEnhancedAttributionInfo:(RCTPromiseResolveBlock)resolve
268
+ reject:(RCTPromiseRejectBlock)reject) {
269
+ NSMutableDictionary *info = [NSMutableDictionary dictionary];
270
+
271
+ if (@available(iOS 18.4, *)) {
272
+ info[@"framework"] = @"AdAttributionKit";
273
+ info[@"version"] = @"2.0";
274
+ info[@"reengagement_available"] = @(YES);
275
+ info[@"overlapping_windows"] = @(YES);
276
+ info[@"geo_postback_available"] = @(YES);
277
+ info[@"development_postbacks"] = @(YES);
278
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
279
+ info[@"coarse_values"] = @[@"low", @"medium", @"high"];
280
+ info[@"features"] = @[
281
+ @"overlapping_windows",
282
+ @"geo_level_postbacks",
283
+ @"development_postbacks",
284
+ @"reengagement"
285
+ ];
286
+ } else if (@available(iOS 17.4, *)) {
287
+ info[@"framework"] = @"AdAttributionKit";
288
+ info[@"version"] = @"1.0";
289
+ info[@"reengagement_available"] = @(YES);
290
+ info[@"overlapping_windows"] = @(NO);
291
+ info[@"geo_postback_available"] = @(NO);
292
+ info[@"development_postbacks"] = @(NO);
293
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
294
+ info[@"coarse_values"] = @[@"low", @"medium", @"high"];
295
+ info[@"features"] = @[@"reengagement"];
296
+ } else if (@available(iOS 16.1, *)) {
297
+ info[@"framework"] = @"SKAdNetwork";
298
+ info[@"version"] = @"4.0";
299
+ info[@"reengagement_available"] = @(NO);
300
+ info[@"overlapping_windows"] = @(NO);
301
+ info[@"geo_postback_available"] = @(NO);
302
+ info[@"development_postbacks"] = @(NO);
303
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
304
+ info[@"coarse_values"] = @[@"low", @"medium", @"high"];
305
+ info[@"features"] = @[];
306
+ } else if (@available(iOS 14.0, *)) {
307
+ info[@"framework"] = @"SKAdNetwork";
308
+ info[@"version"] = @"3.0";
309
+ info[@"reengagement_available"] = @(NO);
310
+ info[@"overlapping_windows"] = @(NO);
311
+ info[@"geo_postback_available"] = @(NO);
312
+ info[@"development_postbacks"] = @(NO);
313
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @63};
314
+ info[@"coarse_values"] = @[];
315
+ info[@"features"] = @[];
316
+ } else {
317
+ info[@"framework"] = @"none";
318
+ info[@"version"] = @"0";
319
+ info[@"reengagement_available"] = @(NO);
320
+ info[@"overlapping_windows"] = @(NO);
321
+ info[@"geo_postback_available"] = @(NO);
322
+ info[@"development_postbacks"] = @(NO);
323
+ info[@"fine_value_range"] = @{@"min": @0, @"max": @0};
324
+ info[@"coarse_values"] = @[];
325
+ info[@"features"] = @[];
326
+ }
327
+
328
+ resolve(info);
329
+ }
330
+
331
+ // iOS 18.4+ - Update postback with overlapping window support
332
+ // windowIndex: 0 = first window (0-2 days), 1 = second window (3-7 days), 2 = third window (8-35 days)
333
+ RCT_EXPORT_METHOD(updatePostbackWithWindow:(NSInteger)fineValue
334
+ coarseValue:(NSString *)coarseValue
335
+ lockWindow:(BOOL)lockWindow
336
+ windowIndex:(NSInteger)windowIndex
337
+ resolve:(RCTPromiseResolveBlock)resolve
338
+ reject:(RCTPromiseRejectBlock)reject) {
339
+ // Validate fine value range
340
+ if (fineValue < 0 || fineValue > 63) {
341
+ reject(@"invalid_value", @"Conversion value must be between 0 and 63", nil);
342
+ return;
343
+ }
344
+
345
+ // Validate window index
346
+ if (windowIndex < 0 || windowIndex > 2) {
347
+ reject(@"invalid_window", @"Window index must be 0, 1, or 2", nil);
348
+ return;
349
+ }
350
+
351
+ if (@available(iOS 18.4, *)) {
352
+ // Convert string to SKAdNetwork.CoarseConversionValue
353
+ SKAdNetworkCoarseConversionValue coarse;
354
+ if ([coarseValue isEqualToString:@"high"]) {
355
+ coarse = SKAdNetworkCoarseConversionValueHigh;
356
+ } else if ([coarseValue isEqualToString:@"medium"]) {
357
+ coarse = SKAdNetworkCoarseConversionValueMedium;
358
+ } else {
359
+ coarse = SKAdNetworkCoarseConversionValueLow;
360
+ }
361
+
362
+ // iOS 18.4 uses the same API but handles overlapping windows automatically
363
+ // based on timing. The windowIndex is for SDK tracking purposes.
364
+ [SKAdNetwork updatePostbackConversionValue:fineValue
365
+ coarseValue:coarse
366
+ lockWindow:lockWindow
367
+ completionHandler:^(NSError * _Nullable error) {
368
+ if (error) {
369
+ reject(@"postback_error", error.localizedDescription, error);
370
+ } else {
371
+ resolve(@{
372
+ @"success": @(YES),
373
+ @"framework": @"AdAttributionKit",
374
+ @"version": @"2.0",
375
+ @"fineValue": @(fineValue),
376
+ @"coarseValue": coarseValue,
377
+ @"lockWindow": @(lockWindow),
378
+ @"windowIndex": @(windowIndex),
379
+ @"overlappingWindows": @(YES)
380
+ });
381
+ }
382
+ }];
383
+ } else if (@available(iOS 16.1, *)) {
384
+ // Fallback for iOS 16.1-18.3 (no overlapping windows)
385
+ SKAdNetworkCoarseConversionValue coarse;
386
+ if ([coarseValue isEqualToString:@"high"]) {
387
+ coarse = SKAdNetworkCoarseConversionValueHigh;
388
+ } else if ([coarseValue isEqualToString:@"medium"]) {
389
+ coarse = SKAdNetworkCoarseConversionValueMedium;
390
+ } else {
391
+ coarse = SKAdNetworkCoarseConversionValueLow;
392
+ }
393
+
394
+ [SKAdNetwork updatePostbackConversionValue:fineValue
395
+ coarseValue:coarse
396
+ lockWindow:lockWindow
397
+ completionHandler:^(NSError * _Nullable error) {
398
+ if (error) {
399
+ reject(@"postback_error", error.localizedDescription, error);
400
+ } else {
401
+ NSString *framework = @"SKAdNetwork";
402
+ NSString *version = @"4.0";
403
+ if (@available(iOS 17.4, *)) {
404
+ framework = @"AdAttributionKit";
405
+ version = @"1.0";
406
+ }
407
+ resolve(@{
408
+ @"success": @(YES),
409
+ @"framework": framework,
410
+ @"version": version,
411
+ @"fineValue": @(fineValue),
412
+ @"coarseValue": coarseValue,
413
+ @"lockWindow": @(lockWindow),
414
+ @"windowIndex": @(windowIndex),
415
+ @"overlappingWindows": @(NO),
416
+ @"note": @"Overlapping windows require iOS 18.4+"
417
+ });
418
+ }
419
+ }];
420
+ } else {
421
+ reject(@"unsupported", @"This method requires iOS 16.1+", nil);
422
+ }
423
+ }
424
+
425
+ @end