@datalyr/react-native 1.3.0 → 1.4.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.
@@ -0,0 +1,333 @@
1
+ import ExpoModulesCore
2
+ import StoreKit
3
+
4
+ public class DatalyrSKAdNetworkModule: Module {
5
+ public func definition() -> ModuleDefinition {
6
+ Name("DatalyrSKAdNetwork")
7
+
8
+ // SKAN 3.0 - Legacy method for iOS 14.0-16.0
9
+ AsyncFunction("updateConversionValue") { (value: Int, promise: Promise) in
10
+ if #available(iOS 14.0, *) {
11
+ SKAdNetwork.updateConversionValue(value)
12
+ promise.resolve(true)
13
+ } else {
14
+ promise.reject("ios_version_error", "SKAdNetwork requires iOS 14.0+")
15
+ }
16
+ }
17
+
18
+ // SKAN 4.0 / AdAttributionKit - Method for iOS 16.1+ with coarse value and lock window support
19
+ // On iOS 17.4+, this uses AdAttributionKit under the hood
20
+ AsyncFunction("updatePostbackConversionValue") { (fineValue: Int, coarseValue: String, lockWindow: Bool, promise: Promise) in
21
+ guard fineValue >= 0 && fineValue <= 63 else {
22
+ promise.reject("invalid_value", "Conversion value must be between 0 and 63")
23
+ return
24
+ }
25
+
26
+ if #available(iOS 16.1, *) {
27
+ let coarse = Self.mapCoarseValue(coarseValue)
28
+
29
+ SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: coarse, lockWindow: lockWindow) { error in
30
+ if let error = error {
31
+ promise.reject("skadnetwork_error", error.localizedDescription)
32
+ } else {
33
+ var framework = "SKAdNetwork"
34
+ if #available(iOS 17.4, *) {
35
+ framework = "AdAttributionKit"
36
+ }
37
+ promise.resolve([
38
+ "success": true,
39
+ "framework": framework,
40
+ "fineValue": fineValue,
41
+ "coarseValue": coarseValue,
42
+ "lockWindow": lockWindow
43
+ ] as [String: Any])
44
+ }
45
+ }
46
+ } else if #available(iOS 14.0, *) {
47
+ // Fallback to SKAN 3.0 for iOS 14.0-16.0
48
+ SKAdNetwork.updateConversionValue(fineValue)
49
+ promise.resolve([
50
+ "success": true,
51
+ "framework": "SKAdNetwork",
52
+ "fineValue": fineValue,
53
+ "coarseValue": "n/a",
54
+ "lockWindow": false
55
+ ] as [String: Any])
56
+ } else {
57
+ promise.reject("ios_version_error", "SKAdNetwork requires iOS 14.0+")
58
+ }
59
+ }
60
+
61
+ // Check if SKAN 4.0 is available (iOS 16.1+)
62
+ AsyncFunction("isSKAN4Available") { (promise: Promise) in
63
+ if #available(iOS 16.1, *) {
64
+ promise.resolve(true)
65
+ } else {
66
+ promise.resolve(false)
67
+ }
68
+ }
69
+
70
+ // Check if AdAttributionKit is available (iOS 17.4+)
71
+ AsyncFunction("isAdAttributionKitAvailable") { (promise: Promise) in
72
+ if #available(iOS 17.4, *) {
73
+ promise.resolve(true)
74
+ } else {
75
+ promise.resolve(false)
76
+ }
77
+ }
78
+
79
+ // Check if overlapping windows are available (iOS 18.4+)
80
+ AsyncFunction("isOverlappingWindowsAvailable") { (promise: Promise) in
81
+ if #available(iOS 18.4, *) {
82
+ promise.resolve(true)
83
+ } else {
84
+ promise.resolve(false)
85
+ }
86
+ }
87
+
88
+ // Register for ad network attribution (supports both AdAttributionKit and SKAdNetwork)
89
+ AsyncFunction("registerForAttribution") { (promise: Promise) in
90
+ if #available(iOS 17.4, *) {
91
+ SKAdNetwork.updatePostbackConversionValue(0, coarseValue: .low, lockWindow: false) { error in
92
+ if let error = error {
93
+ promise.reject("attribution_error", error.localizedDescription)
94
+ } else {
95
+ promise.resolve(["framework": "AdAttributionKit", "registered": true] as [String: Any])
96
+ }
97
+ }
98
+ } else if #available(iOS 14.0, *) {
99
+ SKAdNetwork.registerAppForAdNetworkAttribution()
100
+ promise.resolve(["framework": "SKAdNetwork", "registered": true] as [String: Any])
101
+ } else {
102
+ promise.reject("ios_version_error", "Attribution requires iOS 14.0+")
103
+ }
104
+ }
105
+
106
+ // Get attribution framework info
107
+ AsyncFunction("getAttributionInfo") { (promise: Promise) in
108
+ promise.resolve(Self.buildAttributionInfo())
109
+ }
110
+
111
+ // Update conversion value for re-engagement (AdAttributionKit iOS 17.4+ only)
112
+ AsyncFunction("updateReengagementConversionValue") { (fineValue: Int, coarseValue: String, lockWindow: Bool, promise: Promise) in
113
+ if #available(iOS 17.4, *) {
114
+ guard fineValue >= 0 && fineValue <= 63 else {
115
+ promise.reject("invalid_value", "Conversion value must be between 0 and 63")
116
+ return
117
+ }
118
+
119
+ let coarse = Self.mapCoarseValue(coarseValue)
120
+
121
+ SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: coarse, lockWindow: lockWindow) { error in
122
+ if let error = error {
123
+ promise.reject("reengagement_error", error.localizedDescription)
124
+ } else {
125
+ promise.resolve([
126
+ "success": true,
127
+ "type": "reengagement",
128
+ "framework": "AdAttributionKit",
129
+ "fineValue": fineValue,
130
+ "coarseValue": coarseValue,
131
+ "lockWindow": lockWindow
132
+ ] as [String: Any])
133
+ }
134
+ }
135
+ } else {
136
+ promise.reject("unsupported", "Re-engagement attribution requires iOS 17.4+ (AdAttributionKit)")
137
+ }
138
+ }
139
+
140
+ // iOS 18.4+ - Check if geo-level postback data is available
141
+ AsyncFunction("isGeoPostbackAvailable") { (promise: Promise) in
142
+ if #available(iOS 18.4, *) {
143
+ promise.resolve(true)
144
+ } else {
145
+ promise.resolve(false)
146
+ }
147
+ }
148
+
149
+ // iOS 18.4+ - Set postback environment for testing
150
+ AsyncFunction("setPostbackEnvironment") { (environment: String, promise: Promise) in
151
+ if #available(iOS 18.4, *) {
152
+ let isSandbox = environment == "sandbox"
153
+ NSLog("[Datalyr] Postback environment set to: %@ (note: actual sandbox mode is controlled via device Developer Mode)", environment)
154
+ promise.resolve([
155
+ "environment": environment,
156
+ "isSandbox": isSandbox,
157
+ "note": "Enable Developer Mode in iOS Settings for sandbox postbacks"
158
+ ] as [String: Any])
159
+ } else {
160
+ promise.reject("unsupported", "Development postbacks require iOS 18.4+")
161
+ }
162
+ }
163
+
164
+ // iOS 18.4+ - Get enhanced attribution info including geo availability
165
+ AsyncFunction("getEnhancedAttributionInfo") { (promise: Promise) in
166
+ promise.resolve(Self.buildEnhancedAttributionInfo())
167
+ }
168
+
169
+ // iOS 18.4+ - Update postback with overlapping window support
170
+ AsyncFunction("updatePostbackWithWindow") { (fineValue: Int, coarseValue: String, lockWindow: Bool, windowIndex: Int, promise: Promise) in
171
+ guard fineValue >= 0 && fineValue <= 63 else {
172
+ promise.reject("invalid_value", "Conversion value must be between 0 and 63")
173
+ return
174
+ }
175
+
176
+ guard windowIndex >= 0 && windowIndex <= 2 else {
177
+ promise.reject("invalid_window", "Window index must be 0, 1, or 2")
178
+ return
179
+ }
180
+
181
+ if #available(iOS 16.1, *) {
182
+ let coarse = Self.mapCoarseValue(coarseValue)
183
+
184
+ SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: coarse, lockWindow: lockWindow) { error in
185
+ if let error = error {
186
+ promise.reject("postback_error", error.localizedDescription)
187
+ } else {
188
+ var framework = "SKAdNetwork"
189
+ var version = "4.0"
190
+ var overlapping = false
191
+
192
+ if #available(iOS 18.4, *) {
193
+ framework = "AdAttributionKit"
194
+ version = "2.0"
195
+ overlapping = true
196
+ } else if #available(iOS 17.4, *) {
197
+ framework = "AdAttributionKit"
198
+ version = "1.0"
199
+ }
200
+
201
+ promise.resolve([
202
+ "success": true,
203
+ "framework": framework,
204
+ "version": version,
205
+ "fineValue": fineValue,
206
+ "coarseValue": coarseValue,
207
+ "lockWindow": lockWindow,
208
+ "windowIndex": windowIndex,
209
+ "overlappingWindows": overlapping,
210
+ ] as [String: Any])
211
+ }
212
+ }
213
+ } else {
214
+ promise.reject("unsupported", "This method requires iOS 16.1+")
215
+ }
216
+ }
217
+ }
218
+
219
+ // MARK: - Helper Methods
220
+
221
+ @available(iOS 16.1, *)
222
+ private static func mapCoarseValue(_ value: String) -> SKAdNetwork.CoarseConversionValue {
223
+ switch value {
224
+ case "high": return .high
225
+ case "medium": return .medium
226
+ default: return .low
227
+ }
228
+ }
229
+
230
+ private static func buildAttributionInfo() -> [String: Any] {
231
+ var info: [String: Any] = [:]
232
+
233
+ if #available(iOS 17.4, *) {
234
+ info["framework"] = "AdAttributionKit"
235
+ info["version"] = "1.0"
236
+ info["reengagement_available"] = true
237
+ info["fine_value_range"] = ["min": 0, "max": 63]
238
+ info["coarse_values"] = ["low", "medium", "high"]
239
+ if #available(iOS 18.4, *) {
240
+ info["overlapping_windows"] = true
241
+ } else {
242
+ info["overlapping_windows"] = false
243
+ }
244
+ } else if #available(iOS 16.1, *) {
245
+ info["framework"] = "SKAdNetwork"
246
+ info["version"] = "4.0"
247
+ info["reengagement_available"] = false
248
+ info["overlapping_windows"] = false
249
+ info["fine_value_range"] = ["min": 0, "max": 63]
250
+ info["coarse_values"] = ["low", "medium", "high"]
251
+ } else if #available(iOS 14.0, *) {
252
+ info["framework"] = "SKAdNetwork"
253
+ info["version"] = "3.0"
254
+ info["reengagement_available"] = false
255
+ info["overlapping_windows"] = false
256
+ info["fine_value_range"] = ["min": 0, "max": 63]
257
+ info["coarse_values"] = [] as [String]
258
+ } else {
259
+ info["framework"] = "none"
260
+ info["version"] = "0"
261
+ info["reengagement_available"] = false
262
+ info["overlapping_windows"] = false
263
+ info["fine_value_range"] = ["min": 0, "max": 0]
264
+ info["coarse_values"] = [] as [String]
265
+ }
266
+
267
+ return info
268
+ }
269
+
270
+ private static func buildEnhancedAttributionInfo() -> [String: Any] {
271
+ if #available(iOS 18.4, *) {
272
+ return [
273
+ "framework": "AdAttributionKit",
274
+ "version": "2.0",
275
+ "reengagement_available": true,
276
+ "overlapping_windows": true,
277
+ "geo_postback_available": true,
278
+ "development_postbacks": true,
279
+ "fine_value_range": ["min": 0, "max": 63],
280
+ "coarse_values": ["low", "medium", "high"],
281
+ "features": ["overlapping_windows", "geo_level_postbacks", "development_postbacks", "reengagement"]
282
+ ] as [String: Any]
283
+ } else if #available(iOS 17.4, *) {
284
+ return [
285
+ "framework": "AdAttributionKit",
286
+ "version": "1.0",
287
+ "reengagement_available": true,
288
+ "overlapping_windows": false,
289
+ "geo_postback_available": false,
290
+ "development_postbacks": false,
291
+ "fine_value_range": ["min": 0, "max": 63],
292
+ "coarse_values": ["low", "medium", "high"],
293
+ "features": ["reengagement"]
294
+ ] as [String: Any]
295
+ } else if #available(iOS 16.1, *) {
296
+ return [
297
+ "framework": "SKAdNetwork",
298
+ "version": "4.0",
299
+ "reengagement_available": false,
300
+ "overlapping_windows": false,
301
+ "geo_postback_available": false,
302
+ "development_postbacks": false,
303
+ "fine_value_range": ["min": 0, "max": 63],
304
+ "coarse_values": ["low", "medium", "high"],
305
+ "features": [] as [String]
306
+ ] as [String: Any]
307
+ } else if #available(iOS 14.0, *) {
308
+ return [
309
+ "framework": "SKAdNetwork",
310
+ "version": "3.0",
311
+ "reengagement_available": false,
312
+ "overlapping_windows": false,
313
+ "geo_postback_available": false,
314
+ "development_postbacks": false,
315
+ "fine_value_range": ["min": 0, "max": 63],
316
+ "coarse_values": [] as [String],
317
+ "features": [] as [String]
318
+ ] as [String: Any]
319
+ } else {
320
+ return [
321
+ "framework": "none",
322
+ "version": "0",
323
+ "reengagement_available": false,
324
+ "overlapping_windows": false,
325
+ "geo_postback_available": false,
326
+ "development_postbacks": false,
327
+ "fine_value_range": ["min": 0, "max": 0],
328
+ "coarse_values": [] as [String],
329
+ "features": [] as [String]
330
+ ] as [String: Any]
331
+ }
332
+ }
333
+ }
@@ -0,0 +1,48 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSPrivacyTracking</key>
6
+ <false/>
7
+ <key>NSPrivacyTrackingDomains</key>
8
+ <array/>
9
+ <key>NSPrivacyCollectedDataTypes</key>
10
+ <array>
11
+ <dict>
12
+ <key>NSPrivacyCollectedDataType</key>
13
+ <string>NSPrivacyCollectedDataTypeDeviceID</string>
14
+ <key>NSPrivacyCollectedDataTypeLinked</key>
15
+ <false/>
16
+ <key>NSPrivacyCollectedDataTypeTracking</key>
17
+ <false/>
18
+ <key>NSPrivacyCollectedDataTypePurposes</key>
19
+ <array>
20
+ <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
21
+ </array>
22
+ </dict>
23
+ <dict>
24
+ <key>NSPrivacyCollectedDataType</key>
25
+ <string>NSPrivacyCollectedDataTypeProductInteraction</string>
26
+ <key>NSPrivacyCollectedDataTypeLinked</key>
27
+ <false/>
28
+ <key>NSPrivacyCollectedDataTypeTracking</key>
29
+ <false/>
30
+ <key>NSPrivacyCollectedDataTypePurposes</key>
31
+ <array>
32
+ <string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
33
+ </array>
34
+ </dict>
35
+ </array>
36
+ <key>NSPrivacyAccessedAPITypes</key>
37
+ <array>
38
+ <dict>
39
+ <key>NSPrivacyAccessedAPIType</key>
40
+ <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
41
+ <key>NSPrivacyAccessedAPITypeReasons</key>
42
+ <array>
43
+ <string>CA92.1</string>
44
+ </array>
45
+ </dict>
46
+ </array>
47
+ </dict>
48
+ </plist>
@@ -8,6 +8,7 @@ export declare class DatalyrSDK {
8
8
  private eventQueue;
9
9
  private autoEventsManager;
10
10
  private appStateSubscription;
11
+ private networkStatusUnsubscribe;
11
12
  private static conversionEncoder?;
12
13
  private static debugEnabled;
13
14
  constructor();
@@ -193,6 +194,11 @@ export declare class DatalyrSDK {
193
194
  * Persist user data to storage
194
195
  */
195
196
  private persistUserData;
197
+ /**
198
+ * Initialize network status monitoring
199
+ * Automatically updates event queue when network status changes
200
+ */
201
+ private initializeNetworkMonitoring;
196
202
  /**
197
203
  * Set up app state monitoring for lifecycle events (optimized)
198
204
  */
@@ -8,10 +8,12 @@ import { AutoEventsManager } from './auto-events';
8
8
  import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
9
9
  import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
10
10
  import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
11
+ import { networkStatusManager } from './network-status';
11
12
  export class DatalyrSDK {
12
13
  constructor() {
13
14
  this.autoEventsManager = null;
14
15
  this.appStateSubscription = null;
16
+ this.networkStatusUnsubscribe = null;
15
17
  // Initialize state with defaults
16
18
  this.state = {
17
19
  initialized: false,
@@ -73,18 +75,20 @@ export class DatalyrSDK {
73
75
  flushInterval: this.state.config.flushInterval || 30000,
74
76
  maxRetryCount: this.state.config.maxRetries || 3,
75
77
  });
76
- // Initialize visitor ID, anonymous ID and session
77
- this.state.visitorId = await getOrCreateVisitorId();
78
- this.state.anonymousId = await getOrCreateAnonymousId();
79
- this.state.sessionId = await getOrCreateSessionId();
80
- // Load persisted user data
81
- await this.loadPersistedUserData();
82
- // Initialize attribution manager
83
- if (this.state.config.enableAttribution) {
84
- await attributionManager.initialize();
85
- }
86
- // Initialize journey tracking (for first-touch, last-touch, touchpoints)
87
- await journeyManager.initialize();
78
+ // PARALLEL INITIALIZATION: IDs and core managers
79
+ // Run ID creation and core manager initialization in parallel for faster startup
80
+ const [visitorId, anonymousId, sessionId] = await Promise.all([
81
+ getOrCreateVisitorId(),
82
+ getOrCreateAnonymousId(),
83
+ getOrCreateSessionId(),
84
+ // These run concurrently but don't return values we need to capture
85
+ this.loadPersistedUserData(),
86
+ this.state.config.enableAttribution ? attributionManager.initialize() : Promise.resolve(),
87
+ journeyManager.initialize(),
88
+ ]);
89
+ this.state.visitorId = visitorId;
90
+ this.state.anonymousId = anonymousId;
91
+ this.state.sessionId = sessionId;
88
92
  // Record initial attribution to journey if this is a new session with attribution
89
93
  const initialAttribution = attributionManager.getAttributionData();
90
94
  if (initialAttribution.utm_source || initialAttribution.fbclid || initialAttribution.gclid || initialAttribution.lyr) {
@@ -122,7 +126,7 @@ export class DatalyrSDK {
122
126
  errorLog('Error setting up app state monitoring (non-blocking):', error);
123
127
  }
124
128
  }, 50);
125
- // Initialize SKAdNetwork conversion encoder
129
+ // Initialize SKAdNetwork conversion encoder (synchronous, no await needed)
126
130
  if (config.skadTemplate) {
127
131
  const template = ConversionTemplates[config.skadTemplate];
128
132
  if (template) {
@@ -134,25 +138,34 @@ export class DatalyrSDK {
134
138
  }
135
139
  }
136
140
  }
137
- // Initialize Meta SDK if configured
141
+ // PARALLEL INITIALIZATION: Network monitoring and platform integrations
142
+ // These are independent and can run concurrently for faster startup
143
+ const platformInitPromises = [
144
+ // Network monitoring
145
+ this.initializeNetworkMonitoring(),
146
+ // Apple Search Ads (iOS only)
147
+ appleSearchAdsIntegration.initialize(config.debug),
148
+ // Google Play Install Referrer (Android only)
149
+ playInstallReferrerIntegration.initialize(),
150
+ ];
151
+ // Add Meta initialization if configured
138
152
  if ((_b = config.meta) === null || _b === void 0 ? void 0 : _b.appId) {
139
- await metaIntegration.initialize(config.meta, config.debug);
140
- // Fetch deferred deep link and merge with attribution
141
- if (config.enableAttribution !== false) {
142
- const deferredLink = await metaIntegration.fetchDeferredDeepLink();
143
- if (deferredLink) {
144
- await this.handleDeferredDeepLink(deferredLink);
153
+ platformInitPromises.push(metaIntegration.initialize(config.meta, config.debug).then(async () => {
154
+ // After Meta initializes, fetch deferred deep link
155
+ if (config.enableAttribution !== false) {
156
+ const deferredLink = await metaIntegration.fetchDeferredDeepLink();
157
+ if (deferredLink) {
158
+ await this.handleDeferredDeepLink(deferredLink);
159
+ }
145
160
  }
146
- }
161
+ }));
147
162
  }
148
- // Initialize TikTok SDK if configured
163
+ // Add TikTok initialization if configured
149
164
  if (((_c = config.tiktok) === null || _c === void 0 ? void 0 : _c.appId) && ((_d = config.tiktok) === null || _d === void 0 ? void 0 : _d.tiktokAppId)) {
150
- await tiktokIntegration.initialize(config.tiktok, config.debug);
165
+ platformInitPromises.push(tiktokIntegration.initialize(config.tiktok, config.debug));
151
166
  }
152
- // Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
153
- await appleSearchAdsIntegration.initialize(config.debug);
154
- // Initialize Google Play Install Referrer (Android only)
155
- await playInstallReferrerIntegration.initialize();
167
+ // Wait for all platform integrations to complete
168
+ await Promise.all(platformInitPromises);
156
169
  debugLog('Platform integrations initialized', {
157
170
  meta: metaIntegration.isAvailable(),
158
171
  tiktok: tiktokIntegration.isAvailable(),
@@ -900,6 +913,41 @@ export class DatalyrSDK {
900
913
  errorLog('Error persisting user data:', error);
901
914
  }
902
915
  }
916
+ /**
917
+ * Initialize network status monitoring
918
+ * Automatically updates event queue when network status changes
919
+ */
920
+ async initializeNetworkMonitoring() {
921
+ try {
922
+ await networkStatusManager.initialize();
923
+ // Update event queue with current network status
924
+ this.state.isOnline = networkStatusManager.isOnline();
925
+ this.eventQueue.setOnlineStatus(this.state.isOnline);
926
+ // Subscribe to network changes
927
+ this.networkStatusUnsubscribe = networkStatusManager.subscribe((state) => {
928
+ const isOnline = state.isConnected && (state.isInternetReachable !== false);
929
+ this.state.isOnline = isOnline;
930
+ this.eventQueue.setOnlineStatus(isOnline);
931
+ // Track network status change event (only if SDK is fully initialized)
932
+ if (this.state.initialized) {
933
+ this.track('$network_status_change', {
934
+ is_online: isOnline,
935
+ network_type: state.type,
936
+ is_internet_reachable: state.isInternetReachable,
937
+ }).catch(() => {
938
+ // Ignore errors for network status events
939
+ });
940
+ }
941
+ });
942
+ debugLog(`Network monitoring initialized, online: ${this.state.isOnline}`);
943
+ }
944
+ catch (error) {
945
+ errorLog('Error initializing network monitoring (non-blocking):', error);
946
+ // Default to online if monitoring fails
947
+ this.state.isOnline = true;
948
+ this.eventQueue.setOnlineStatus(true);
949
+ }
950
+ }
903
951
  /**
904
952
  * Set up app state monitoring for lifecycle events (optimized)
905
953
  */
@@ -920,6 +968,8 @@ export class DatalyrSDK {
920
968
  else if (nextAppState === 'active') {
921
969
  // App became active, ensure we have fresh session if needed
922
970
  this.refreshSession();
971
+ // Refresh network status when coming back from background
972
+ networkStatusManager.refresh();
923
973
  // Notify auto-events manager for session handling
924
974
  if (this.autoEventsManager) {
925
975
  this.autoEventsManager.handleAppForeground();
@@ -957,6 +1007,13 @@ export class DatalyrSDK {
957
1007
  this.appStateSubscription.remove();
958
1008
  this.appStateSubscription = null;
959
1009
  }
1010
+ // Remove network status listener
1011
+ if (this.networkStatusUnsubscribe) {
1012
+ this.networkStatusUnsubscribe();
1013
+ this.networkStatusUnsubscribe = null;
1014
+ }
1015
+ // Destroy network status manager
1016
+ networkStatusManager.destroy();
960
1017
  // Destroy event queue
961
1018
  this.eventQueue.destroy();
962
1019
  // Reset state
package/lib/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export * from './event-queue';
12
12
  export { DatalyrSDK };
13
13
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
14
14
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
15
- export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
15
+ export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
16
+ export { networkStatusManager } from './network-status';
17
+ export type { NetworkState, NetworkStateListener } from './network-status';
16
18
  export type { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
17
19
  export default DatalyrSDK;
package/lib/index.js CHANGED
@@ -20,6 +20,8 @@ export { DatalyrSDK };
20
20
  export { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
21
21
  export { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
22
22
  // Export platform integrations
23
- export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
23
+ export { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
24
+ // Export network status manager
25
+ export { networkStatusManager } from './network-status';
24
26
  // Default export for compatibility
25
27
  export default DatalyrSDK;
@@ -19,7 +19,9 @@
19
19
  * - referrer_url: Full referrer URL from Play Store
20
20
  * - referrer_click_timestamp: When the referrer link was clicked
21
21
  * - install_begin_timestamp: When the install began
22
- * - gclid: Google Ads click ID (if present)
22
+ * - gclid: Google Ads click ID (standard)
23
+ * - gbraid: Google Ads privacy-safe click ID (iOS App campaigns)
24
+ * - wbraid: Google Ads privacy-safe click ID (Web-to-App campaigns)
23
25
  * - utm_source, utm_medium, utm_campaign, etc.
24
26
  */
25
27
  export interface PlayInstallReferrer {
@@ -28,6 +30,8 @@ export interface PlayInstallReferrer {
28
30
  installBeginTimestamp: number;
29
31
  installCompleteTimestamp?: number;
30
32
  gclid?: string;
33
+ gbraid?: string;
34
+ wbraid?: string;
31
35
  utmSource?: string;
32
36
  utmMedium?: string;
33
37
  utmCampaign?: string;
@@ -19,7 +19,9 @@
19
19
  * - referrer_url: Full referrer URL from Play Store
20
20
  * - referrer_click_timestamp: When the referrer link was clicked
21
21
  * - install_begin_timestamp: When the install began
22
- * - gclid: Google Ads click ID (if present)
22
+ * - gclid: Google Ads click ID (standard)
23
+ * - gbraid: Google Ads privacy-safe click ID (iOS App campaigns)
24
+ * - wbraid: Google Ads privacy-safe click ID (Web-to-App campaigns)
23
25
  * - utm_source, utm_medium, utm_campaign, etc.
24
26
  */
25
27
  import { Platform, NativeModules } from 'react-native';
@@ -61,6 +63,8 @@ class PlayInstallReferrerIntegration {
61
63
  utmSource: this.referrerData.utmSource,
62
64
  utmMedium: this.referrerData.utmMedium,
63
65
  hasGclid: !!this.referrerData.gclid,
66
+ hasGbraid: !!this.referrerData.gbraid,
67
+ hasWbraid: !!this.referrerData.wbraid,
64
68
  });
65
69
  }
66
70
  }
@@ -109,11 +113,14 @@ class PlayInstallReferrerIntegration {
109
113
  params.utmCampaign = searchParams.get('utm_campaign') || undefined;
110
114
  params.utmTerm = searchParams.get('utm_term') || undefined;
111
115
  params.utmContent = searchParams.get('utm_content') || undefined;
112
- // Extract click IDs
116
+ // Extract click IDs (gclid, gbraid, wbraid)
113
117
  params.gclid = searchParams.get('gclid') || undefined;
118
+ params.gbraid = searchParams.get('gbraid') || undefined;
119
+ params.wbraid = searchParams.get('wbraid') || undefined;
114
120
  // Store any additional parameters
121
+ const knownParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'gclid', 'gbraid', 'wbraid'];
115
122
  searchParams.forEach((value, key) => {
116
- if (!key.startsWith('utm_') && key !== 'gclid') {
123
+ if (!knownParams.includes(key) && !key.startsWith('utm_')) {
117
124
  params[key] = value;
118
125
  }
119
126
  });
@@ -141,8 +148,11 @@ class PlayInstallReferrerIntegration {
141
148
  install_referrer_url: this.referrerData.referrerUrl,
142
149
  referrer_click_timestamp: this.referrerData.referrerClickTimestamp,
143
150
  install_begin_timestamp: this.referrerData.installBeginTimestamp,
144
- // Standard attribution fields
151
+ // Google Ads click IDs (gclid is standard, gbraid/wbraid are privacy-safe alternatives)
145
152
  gclid: this.referrerData.gclid,
153
+ gbraid: this.referrerData.gbraid,
154
+ wbraid: this.referrerData.wbraid,
155
+ // UTM parameters
146
156
  utm_source: this.referrerData.utmSource,
147
157
  utm_medium: this.referrerData.utmMedium,
148
158
  utm_campaign: this.referrerData.utmCampaign,