@datalyr/react-native 1.4.7 → 1.4.9
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.
- package/README.md +19 -2
- package/android/build.gradle +3 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +44 -0
- package/ios/DatalyrNativeModule.swift +224 -80
- package/ios/DatalyrObjCExceptionCatcher.h +14 -0
- package/ios/DatalyrObjCExceptionCatcher.m +30 -0
- package/lib/datalyr-sdk.d.ts +1 -0
- package/lib/datalyr-sdk.js +38 -2
- package/lib/event-queue.js +1 -1
- package/lib/http-client.js +3 -3
- package/lib/integrations/tiktok-integration.js +4 -1
- package/lib/native/DatalyrNativeBridge.d.ts +15 -0
- package/lib/native/DatalyrNativeBridge.js +18 -0
- package/lib/native/index.d.ts +2 -1
- package/lib/native/index.js +1 -1
- package/lib/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/datalyr-sdk-expo.ts +61 -7
- package/src/datalyr-sdk.ts +39 -3
- package/src/event-queue.ts +1 -1
- package/src/http-client.ts +3 -3
- package/src/integrations/tiktok-integration.ts +3 -1
- package/src/native/DatalyrNativeBridge.ts +37 -0
- package/src/native/index.ts +2 -0
- package/src/types.ts +2 -2
- package/src/utils-expo.ts +55 -19
package/README.md
CHANGED
|
@@ -478,13 +478,25 @@ await Datalyr.initialize({
|
|
|
478
478
|
await Datalyr.initialize({
|
|
479
479
|
apiKey: 'dk_your_api_key',
|
|
480
480
|
tiktok: {
|
|
481
|
-
appId: 'your_app_id',
|
|
482
|
-
tiktokAppId: '7123456789',
|
|
481
|
+
appId: 'your_app_id', // Events API App ID
|
|
482
|
+
tiktokAppId: '7123456789', // TikTok App ID (Developer Portal)
|
|
483
|
+
accessToken: 'your_access_token', // Events API Access Token
|
|
483
484
|
enableAppEvents: true,
|
|
484
485
|
},
|
|
485
486
|
});
|
|
486
487
|
```
|
|
487
488
|
|
|
489
|
+
**Where to find your TikTok credentials:**
|
|
490
|
+
|
|
491
|
+
| Credential | Where to get it |
|
|
492
|
+
|------------|----------------|
|
|
493
|
+
| `tiktokAppId` | [TikTok Developer Portal](https://developers.tiktok.com) → Your App → App ID |
|
|
494
|
+
| `appId` | TikTok Business Center → Assets → Events → Your App → App ID |
|
|
495
|
+
| `accessToken` | TikTok Business Center → Assets → Events → Your App → Settings → Access Token |
|
|
496
|
+
|
|
497
|
+
> **Note:** The `accessToken` enables client-side TikTok SDK features (enhanced attribution, real-time event forwarding). Without it, events are still tracked server-side via Datalyr postbacks — you'll see a warning in debug mode.
|
|
498
|
+
```
|
|
499
|
+
|
|
488
500
|
### Apple Search Ads
|
|
489
501
|
|
|
490
502
|
Attribution for users who install from Apple Search Ads (iOS 14.3+). Automatically fetched on initialization.
|
|
@@ -667,12 +679,17 @@ Check status: `Datalyr.getPlatformIntegrationStatus()`
|
|
|
667
679
|
|
|
668
680
|
### TikTok SDK Not Working
|
|
669
681
|
|
|
682
|
+
1. Make sure you have all three TikTok credentials (see [TikTok setup](#tiktok))
|
|
683
|
+
2. The `accessToken` is required for client-side SDK — without it, you'll see a warning but server-side tracking still works
|
|
684
|
+
3. Check status: `Datalyr.getPlatformIntegrationStatus()`
|
|
685
|
+
|
|
670
686
|
```typescript
|
|
671
687
|
await Datalyr.initialize({
|
|
672
688
|
apiKey: 'dk_your_api_key',
|
|
673
689
|
tiktok: {
|
|
674
690
|
appId: 'your_app_id',
|
|
675
691
|
tiktokAppId: '7123456789012345',
|
|
692
|
+
accessToken: 'your_access_token',
|
|
676
693
|
},
|
|
677
694
|
});
|
|
678
695
|
```
|
package/android/build.gradle
CHANGED
|
@@ -26,6 +26,9 @@ import com.tiktok.TikTokBusinessSdk.TTConfig;
|
|
|
26
26
|
import com.tiktok.appevents.TikTokAppEvent;
|
|
27
27
|
import com.tiktok.appevents.TikTokAppEventLogger;
|
|
28
28
|
|
|
29
|
+
import com.google.android.gms.ads.identifier.AdvertisingIdClient;
|
|
30
|
+
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
|
|
31
|
+
|
|
29
32
|
import java.math.BigDecimal;
|
|
30
33
|
import java.util.Currency;
|
|
31
34
|
import java.util.HashMap;
|
|
@@ -391,6 +394,47 @@ public class DatalyrNativeModule extends ReactContextBaseJavaModule {
|
|
|
391
394
|
promise.resolve(result);
|
|
392
395
|
}
|
|
393
396
|
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Advertiser Info (GAID on Android)
|
|
399
|
+
// ============================================================================
|
|
400
|
+
|
|
401
|
+
@ReactMethod
|
|
402
|
+
public void getAdvertiserInfo(Promise promise) {
|
|
403
|
+
// GAID must be fetched on a background thread
|
|
404
|
+
new Thread(() -> {
|
|
405
|
+
try {
|
|
406
|
+
WritableMap result = Arguments.createMap();
|
|
407
|
+
|
|
408
|
+
// Fetch Google Advertising ID
|
|
409
|
+
try {
|
|
410
|
+
AdvertisingIdClient.Info adInfo = AdvertisingIdClient.getAdvertisingIdInfo(reactContext.getApplicationContext());
|
|
411
|
+
boolean limitAdTracking = adInfo.isLimitAdTrackingEnabled();
|
|
412
|
+
result.putBoolean("advertiser_tracking_enabled", !limitAdTracking);
|
|
413
|
+
result.putInt("att_status", limitAdTracking ? 2 : 3); // 2=denied, 3=authorized
|
|
414
|
+
|
|
415
|
+
if (!limitAdTracking && adInfo.getId() != null) {
|
|
416
|
+
result.putString("gaid", adInfo.getId());
|
|
417
|
+
}
|
|
418
|
+
} catch (GooglePlayServicesNotAvailableException e) {
|
|
419
|
+
// Google Play Services not available (e.g., Huawei devices)
|
|
420
|
+
result.putInt("att_status", 3);
|
|
421
|
+
result.putBoolean("advertiser_tracking_enabled", true);
|
|
422
|
+
Log.d(TAG, "Google Play Services not available for GAID");
|
|
423
|
+
} catch (Exception e) {
|
|
424
|
+
// Fallback — GAID not available but not blocking
|
|
425
|
+
result.putInt("att_status", 3);
|
|
426
|
+
result.putBoolean("advertiser_tracking_enabled", true);
|
|
427
|
+
Log.d(TAG, "GAID not available: " + e.getMessage());
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
promise.resolve(result);
|
|
431
|
+
} catch (Exception e) {
|
|
432
|
+
Log.e(TAG, "Failed to get advertiser info: " + e.getMessage());
|
|
433
|
+
promise.resolve(null);
|
|
434
|
+
}
|
|
435
|
+
}).start();
|
|
436
|
+
}
|
|
437
|
+
|
|
394
438
|
// ============================================================================
|
|
395
439
|
// Helper Methods
|
|
396
440
|
// ============================================================================
|
|
@@ -2,15 +2,20 @@ import ExpoModulesCore
|
|
|
2
2
|
import FBSDKCoreKit
|
|
3
3
|
import TikTokBusinessSDK
|
|
4
4
|
import AdServices
|
|
5
|
+
import AppTrackingTransparency
|
|
6
|
+
import AdSupport
|
|
5
7
|
|
|
6
8
|
public class DatalyrNativeModule: Module {
|
|
9
|
+
private var tiktokInitialized = false
|
|
10
|
+
private var metaInitialized = false
|
|
11
|
+
|
|
7
12
|
public func definition() -> ModuleDefinition {
|
|
8
13
|
Name("DatalyrNative")
|
|
9
14
|
|
|
10
15
|
// MARK: - Meta (Facebook) SDK Methods
|
|
11
16
|
|
|
12
17
|
AsyncFunction("initializeMetaSDK") { (appId: String, clientToken: String?, advertiserTrackingEnabled: Bool, promise: Promise) in
|
|
13
|
-
DispatchQueue.main.async {
|
|
18
|
+
DispatchQueue.main.async { [weak self] in
|
|
14
19
|
Settings.shared.appID = appId
|
|
15
20
|
|
|
16
21
|
if let token = clientToken, !token.isEmpty {
|
|
@@ -20,145 +25,246 @@ public class DatalyrNativeModule: Module {
|
|
|
20
25
|
Settings.shared.isAdvertiserTrackingEnabled = advertiserTrackingEnabled
|
|
21
26
|
Settings.shared.isAdvertiserIDCollectionEnabled = advertiserTrackingEnabled
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
var initError: NSError?
|
|
29
|
+
let success = DatalyrObjCExceptionCatcher.tryBlock({
|
|
30
|
+
ApplicationDelegate.shared.application(
|
|
31
|
+
UIApplication.shared,
|
|
32
|
+
didFinishLaunchingWithOptions: nil
|
|
33
|
+
)
|
|
34
|
+
}, error: &initError)
|
|
27
35
|
|
|
28
|
-
|
|
36
|
+
if success {
|
|
37
|
+
self?.metaInitialized = true
|
|
38
|
+
promise.resolve(true)
|
|
39
|
+
} else {
|
|
40
|
+
let message = initError?.localizedDescription ?? "Unknown ObjC exception during Meta SDK init"
|
|
41
|
+
promise.reject("meta_init_error", message)
|
|
42
|
+
}
|
|
29
43
|
}
|
|
30
44
|
}
|
|
31
45
|
|
|
32
46
|
AsyncFunction("fetchDeferredAppLink") { (promise: Promise) in
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
DispatchQueue.main.async {
|
|
48
|
+
AppLinkUtility.fetchDeferredAppLink { url, error in
|
|
49
|
+
if error != nil {
|
|
50
|
+
promise.resolve(nil)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
38
53
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
if let url = url {
|
|
55
|
+
promise.resolve(url.absoluteString)
|
|
56
|
+
} else {
|
|
57
|
+
promise.resolve(nil)
|
|
58
|
+
}
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
}
|
|
46
62
|
|
|
47
63
|
AsyncFunction("logMetaEvent") { (eventName: String, valueToSum: Double?, parameters: [String: Any]?, promise: Promise) in
|
|
48
|
-
|
|
64
|
+
guard self.metaInitialized else {
|
|
65
|
+
promise.reject("meta_not_initialized", "Meta SDK not initialized. Call initializeMetaSDK first.")
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
DispatchQueue.main.async {
|
|
70
|
+
var params: [AppEvents.ParameterName: Any] = [:]
|
|
49
71
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
72
|
+
if let dict = parameters {
|
|
73
|
+
for (key, value) in dict {
|
|
74
|
+
params[AppEvents.ParameterName(key)] = value
|
|
75
|
+
}
|
|
53
76
|
}
|
|
54
|
-
}
|
|
55
77
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
var logError: NSError?
|
|
79
|
+
DatalyrObjCExceptionCatcher.tryBlock({
|
|
80
|
+
if let value = valueToSum {
|
|
81
|
+
AppEvents.shared.logEvent(AppEvents.Name(eventName), valueToSum: value, parameters: params)
|
|
82
|
+
} else if params.isEmpty {
|
|
83
|
+
AppEvents.shared.logEvent(AppEvents.Name(eventName))
|
|
84
|
+
} else {
|
|
85
|
+
AppEvents.shared.logEvent(AppEvents.Name(eventName), parameters: params)
|
|
86
|
+
}
|
|
87
|
+
}, error: &logError)
|
|
63
88
|
|
|
64
|
-
|
|
89
|
+
if let logError = logError {
|
|
90
|
+
promise.reject("meta_event_error", logError.localizedDescription)
|
|
91
|
+
} else {
|
|
92
|
+
promise.resolve(true)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
AsyncFunction("logMetaPurchase") { (amount: Double, currency: String, parameters: [String: Any]?, promise: Promise) in
|
|
68
|
-
|
|
98
|
+
guard self.metaInitialized else {
|
|
99
|
+
promise.reject("meta_not_initialized", "Meta SDK not initialized. Call initializeMetaSDK first.")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
DispatchQueue.main.async {
|
|
104
|
+
var params: [AppEvents.ParameterName: Any] = [:]
|
|
69
105
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
if let dict = parameters {
|
|
107
|
+
for (key, value) in dict {
|
|
108
|
+
params[AppEvents.ParameterName(key)] = value
|
|
109
|
+
}
|
|
73
110
|
}
|
|
74
|
-
}
|
|
75
111
|
|
|
76
|
-
|
|
77
|
-
|
|
112
|
+
var logError: NSError?
|
|
113
|
+
DatalyrObjCExceptionCatcher.tryBlock({
|
|
114
|
+
AppEvents.shared.logPurchase(amount: amount, currency: currency, parameters: params)
|
|
115
|
+
}, error: &logError)
|
|
116
|
+
|
|
117
|
+
if let logError = logError {
|
|
118
|
+
promise.reject("meta_event_error", logError.localizedDescription)
|
|
119
|
+
} else {
|
|
120
|
+
promise.resolve(true)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
78
123
|
}
|
|
79
124
|
|
|
80
125
|
AsyncFunction("setMetaUserData") { (userData: [String: Any], promise: Promise) in
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
AppEvents.shared.setUserData(userData["dateOfBirth"] as? String, forType: .dateOfBirth)
|
|
86
|
-
AppEvents.shared.setUserData(userData["gender"] as? String, forType: .gender)
|
|
87
|
-
AppEvents.shared.setUserData(userData["city"] as? String, forType: .city)
|
|
88
|
-
AppEvents.shared.setUserData(userData["state"] as? String, forType: .state)
|
|
89
|
-
AppEvents.shared.setUserData(userData["zip"] as? String, forType: .zip)
|
|
90
|
-
AppEvents.shared.setUserData(userData["country"] as? String, forType: .country)
|
|
126
|
+
guard self.metaInitialized else {
|
|
127
|
+
promise.reject("meta_not_initialized", "Meta SDK not initialized. Call initializeMetaSDK first.")
|
|
128
|
+
return
|
|
129
|
+
}
|
|
91
130
|
|
|
92
|
-
|
|
131
|
+
DispatchQueue.main.async {
|
|
132
|
+
if let email = userData["email"] as? String { AppEvents.shared.setUserData(email, forType: .email) }
|
|
133
|
+
if let firstName = userData["firstName"] as? String { AppEvents.shared.setUserData(firstName, forType: .firstName) }
|
|
134
|
+
if let lastName = userData["lastName"] as? String { AppEvents.shared.setUserData(lastName, forType: .lastName) }
|
|
135
|
+
if let phone = userData["phone"] as? String { AppEvents.shared.setUserData(phone, forType: .phone) }
|
|
136
|
+
if let dateOfBirth = userData["dateOfBirth"] as? String { AppEvents.shared.setUserData(dateOfBirth, forType: .dateOfBirth) }
|
|
137
|
+
if let gender = userData["gender"] as? String { AppEvents.shared.setUserData(gender, forType: .gender) }
|
|
138
|
+
if let city = userData["city"] as? String { AppEvents.shared.setUserData(city, forType: .city) }
|
|
139
|
+
if let state = userData["state"] as? String { AppEvents.shared.setUserData(state, forType: .state) }
|
|
140
|
+
if let zip = userData["zip"] as? String { AppEvents.shared.setUserData(zip, forType: .zip) }
|
|
141
|
+
if let country = userData["country"] as? String { AppEvents.shared.setUserData(country, forType: .country) }
|
|
142
|
+
|
|
143
|
+
promise.resolve(true)
|
|
144
|
+
}
|
|
93
145
|
}
|
|
94
146
|
|
|
95
147
|
AsyncFunction("clearMetaUserData") { (promise: Promise) in
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
guard self.metaInitialized else {
|
|
149
|
+
promise.reject("meta_not_initialized", "Meta SDK not initialized. Call initializeMetaSDK first.")
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
DispatchQueue.main.async {
|
|
154
|
+
AppEvents.shared.clearUserData()
|
|
155
|
+
promise.resolve(true)
|
|
156
|
+
}
|
|
98
157
|
}
|
|
99
158
|
|
|
100
159
|
AsyncFunction("updateMetaTrackingAuthorization") { (enabled: Bool, promise: Promise) in
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
160
|
+
guard self.metaInitialized else {
|
|
161
|
+
promise.reject("meta_not_initialized", "Meta SDK not initialized. Call initializeMetaSDK first.")
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
DispatchQueue.main.async {
|
|
166
|
+
Settings.shared.isAdvertiserTrackingEnabled = enabled
|
|
167
|
+
Settings.shared.isAdvertiserIDCollectionEnabled = enabled
|
|
168
|
+
promise.resolve(true)
|
|
169
|
+
}
|
|
104
170
|
}
|
|
105
171
|
|
|
106
172
|
// MARK: - TikTok SDK Methods
|
|
107
173
|
|
|
108
174
|
AsyncFunction("initializeTikTokSDK") { (appId: String, tiktokAppId: String, accessToken: String?, debug: Bool, promise: Promise) in
|
|
109
|
-
DispatchQueue.main.async {
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} else {
|
|
114
|
-
config = TikTokConfig(appId: appId, tiktokAppId: tiktokAppId)
|
|
175
|
+
DispatchQueue.main.async { [weak self] in
|
|
176
|
+
guard let token = accessToken, !token.isEmpty else {
|
|
177
|
+
promise.reject("tiktok_init_error", "TikTok accessToken is required. The deprecated init without accessToken has been removed.")
|
|
178
|
+
return
|
|
115
179
|
}
|
|
116
180
|
|
|
181
|
+
let config = TikTokConfig(accessToken: token, appId: appId, tiktokAppId: tiktokAppId)
|
|
182
|
+
|
|
117
183
|
if debug {
|
|
118
184
|
config?.setLogLevel(TikTokLogLevelDebug)
|
|
119
185
|
}
|
|
120
186
|
|
|
121
|
-
|
|
187
|
+
guard let validConfig = config else {
|
|
188
|
+
promise.reject("tiktok_init_error", "Failed to create TikTok config")
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var initError: NSError?
|
|
193
|
+
let success = DatalyrObjCExceptionCatcher.tryBlock({
|
|
122
194
|
TikTokBusiness.initializeSdk(validConfig)
|
|
195
|
+
}, error: &initError)
|
|
196
|
+
|
|
197
|
+
if success {
|
|
198
|
+
self?.tiktokInitialized = true
|
|
123
199
|
promise.resolve(true)
|
|
124
200
|
} else {
|
|
125
|
-
|
|
201
|
+
let message = initError?.localizedDescription ?? "Unknown ObjC exception during TikTok SDK init"
|
|
202
|
+
promise.reject("tiktok_init_error", message)
|
|
126
203
|
}
|
|
127
204
|
}
|
|
128
205
|
}
|
|
129
206
|
|
|
130
207
|
AsyncFunction("trackTikTokEvent") { (eventName: String, eventId: String?, properties: [String: Any]?, promise: Promise) in
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
event = TikTokBaseEvent(eventName: eventName, eventId: eid)
|
|
135
|
-
} else {
|
|
136
|
-
event = TikTokBaseEvent(eventName: eventName)
|
|
208
|
+
guard self.tiktokInitialized else {
|
|
209
|
+
promise.reject("tiktok_not_initialized", "TikTok SDK not initialized. Call initializeTikTokSDK first.")
|
|
210
|
+
return
|
|
137
211
|
}
|
|
138
212
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
213
|
+
DispatchQueue.main.async {
|
|
214
|
+
let event: TikTokBaseEvent
|
|
215
|
+
|
|
216
|
+
if let eid = eventId, !eid.isEmpty {
|
|
217
|
+
event = TikTokBaseEvent(eventName: eventName, eventId: eid)
|
|
218
|
+
} else {
|
|
219
|
+
event = TikTokBaseEvent(eventName: eventName)
|
|
142
220
|
}
|
|
143
|
-
}
|
|
144
221
|
|
|
145
|
-
|
|
146
|
-
|
|
222
|
+
if let dict = properties {
|
|
223
|
+
for (key, value) in dict {
|
|
224
|
+
event.addProperty(withKey: key, value: value)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
var trackError: NSError?
|
|
229
|
+
DatalyrObjCExceptionCatcher.tryBlock({
|
|
230
|
+
TikTokBusiness.trackTTEvent(event)
|
|
231
|
+
}, error: &trackError)
|
|
232
|
+
|
|
233
|
+
if let trackError = trackError {
|
|
234
|
+
promise.reject("tiktok_event_error", trackError.localizedDescription)
|
|
235
|
+
} else {
|
|
236
|
+
promise.resolve(true)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
147
239
|
}
|
|
148
240
|
|
|
149
241
|
AsyncFunction("identifyTikTokUser") { (externalId: String, externalUserName: String, phoneNumber: String, email: String, promise: Promise) in
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
242
|
+
guard self.tiktokInitialized else {
|
|
243
|
+
promise.reject("tiktok_not_initialized", "TikTok SDK not initialized. Call initializeTikTokSDK first.")
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
DispatchQueue.main.async {
|
|
248
|
+
TikTokBusiness.identify(
|
|
249
|
+
withExternalID: externalId.isEmpty ? nil : externalId,
|
|
250
|
+
externalUserName: externalUserName.isEmpty ? nil : externalUserName,
|
|
251
|
+
phoneNumber: phoneNumber.isEmpty ? nil : phoneNumber,
|
|
252
|
+
email: email.isEmpty ? nil : email
|
|
253
|
+
)
|
|
254
|
+
promise.resolve(true)
|
|
255
|
+
}
|
|
157
256
|
}
|
|
158
257
|
|
|
159
258
|
AsyncFunction("logoutTikTok") { (promise: Promise) in
|
|
160
|
-
|
|
161
|
-
|
|
259
|
+
guard self.tiktokInitialized else {
|
|
260
|
+
promise.reject("tiktok_not_initialized", "TikTok SDK not initialized. Call initializeTikTokSDK first.")
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
DispatchQueue.main.async {
|
|
265
|
+
TikTokBusiness.logout()
|
|
266
|
+
promise.resolve(true)
|
|
267
|
+
}
|
|
162
268
|
}
|
|
163
269
|
|
|
164
270
|
AsyncFunction("updateTikTokTrackingAuthorization") { (enabled: Bool, promise: Promise) in
|
|
@@ -176,6 +282,44 @@ public class DatalyrNativeModule: Module {
|
|
|
176
282
|
])
|
|
177
283
|
}
|
|
178
284
|
|
|
285
|
+
// MARK: - Advertiser Info (IDFA, IDFV, ATT Status)
|
|
286
|
+
|
|
287
|
+
AsyncFunction("getAdvertiserInfo") { (promise: Promise) in
|
|
288
|
+
var result: [String: Any] = [:]
|
|
289
|
+
|
|
290
|
+
// IDFV is always available
|
|
291
|
+
if let idfv = UIDevice.current.identifierForVendor?.uuidString {
|
|
292
|
+
result["idfv"] = idfv
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ATT status
|
|
296
|
+
if #available(iOS 14, *) {
|
|
297
|
+
let status = ATTrackingManager.trackingAuthorizationStatus
|
|
298
|
+
result["att_status"] = status.rawValue
|
|
299
|
+
result["advertiser_tracking_enabled"] = status == .authorized
|
|
300
|
+
|
|
301
|
+
// IDFA only if ATT authorized
|
|
302
|
+
if status == .authorized {
|
|
303
|
+
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
|
|
304
|
+
let zeroUUID = "00000000-0000-0000-0000-000000000000"
|
|
305
|
+
if idfa != zeroUUID {
|
|
306
|
+
result["idfa"] = idfa
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
// Pre-iOS 14, tracking allowed by default
|
|
311
|
+
result["att_status"] = 3 // .authorized equivalent
|
|
312
|
+
result["advertiser_tracking_enabled"] = true
|
|
313
|
+
let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
|
|
314
|
+
let zeroUUID = "00000000-0000-0000-0000-000000000000"
|
|
315
|
+
if idfa != zeroUUID {
|
|
316
|
+
result["idfa"] = idfa
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
promise.resolve(result)
|
|
321
|
+
}
|
|
322
|
+
|
|
179
323
|
// MARK: - Apple Search Ads Attribution
|
|
180
324
|
|
|
181
325
|
AsyncFunction("getAppleSearchAdsAttribution") { (promise: Promise) in
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Catches ObjC NSExceptions (from Meta/TikTok SDKs) and converts them to NSErrors.
|
|
6
|
+
/// Swift's do/try/catch cannot catch NSExceptions — they propagate through Hermes
|
|
7
|
+
/// and cause EXC_BAD_ACCESS (SIGSEGV) crashes.
|
|
8
|
+
@interface DatalyrObjCExceptionCatcher : NSObject
|
|
9
|
+
|
|
10
|
+
+ (BOOL)tryBlock:(void(NS_NOESCAPE ^)(void))block error:(NSError *_Nullable *_Nullable)error;
|
|
11
|
+
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#import "DatalyrObjCExceptionCatcher.h"
|
|
2
|
+
|
|
3
|
+
@implementation DatalyrObjCExceptionCatcher
|
|
4
|
+
|
|
5
|
+
+ (BOOL)tryBlock:(void(NS_NOESCAPE ^)(void))block error:(NSError **)error {
|
|
6
|
+
@try {
|
|
7
|
+
block();
|
|
8
|
+
return YES;
|
|
9
|
+
}
|
|
10
|
+
@catch (NSException *exception) {
|
|
11
|
+
if (error) {
|
|
12
|
+
NSString *description = exception.reason ?: exception.name;
|
|
13
|
+
NSDictionary *userInfo = @{
|
|
14
|
+
NSLocalizedDescriptionKey: description,
|
|
15
|
+
@"ExceptionName": exception.name ?: @"Unknown",
|
|
16
|
+
};
|
|
17
|
+
if (exception.userInfo) {
|
|
18
|
+
NSMutableDictionary *merged = [userInfo mutableCopy];
|
|
19
|
+
[merged addEntriesFromDictionary:exception.userInfo];
|
|
20
|
+
userInfo = merged;
|
|
21
|
+
}
|
|
22
|
+
*error = [NSError errorWithDomain:@"com.datalyr.objc-exception"
|
|
23
|
+
code:-1
|
|
24
|
+
userInfo:userInfo];
|
|
25
|
+
}
|
|
26
|
+
return NO;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@end
|
package/lib/datalyr-sdk.d.ts
CHANGED
package/lib/datalyr-sdk.js
CHANGED
|
@@ -8,12 +8,14 @@ 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 { AdvertiserInfoBridge } from './native/DatalyrNativeBridge';
|
|
11
12
|
import { networkStatusManager } from './network-status';
|
|
12
13
|
export class DatalyrSDK {
|
|
13
14
|
constructor() {
|
|
14
15
|
this.autoEventsManager = null;
|
|
15
16
|
this.appStateSubscription = null;
|
|
16
17
|
this.networkStatusUnsubscribe = null;
|
|
18
|
+
this.cachedAdvertiserInfo = null;
|
|
17
19
|
// Initialize state with defaults
|
|
18
20
|
this.state = {
|
|
19
21
|
initialized: false,
|
|
@@ -26,9 +28,11 @@ export class DatalyrSDK {
|
|
|
26
28
|
maxRetries: 3,
|
|
27
29
|
retryDelay: 1000,
|
|
28
30
|
batchSize: 10,
|
|
29
|
-
flushInterval:
|
|
31
|
+
flushInterval: 30000,
|
|
30
32
|
maxQueueSize: 100,
|
|
31
33
|
respectDoNotTrack: true,
|
|
34
|
+
enableAutoEvents: true,
|
|
35
|
+
enableAttribution: true,
|
|
32
36
|
},
|
|
33
37
|
visitorId: '',
|
|
34
38
|
anonymousId: '', // Persistent anonymous identifier
|
|
@@ -166,6 +170,13 @@ export class DatalyrSDK {
|
|
|
166
170
|
}
|
|
167
171
|
// Wait for all platform integrations to complete
|
|
168
172
|
await Promise.all(platformInitPromises);
|
|
173
|
+
// Cache advertiser info (IDFA/GAID, ATT status) once at init to avoid per-event native bridge calls
|
|
174
|
+
try {
|
|
175
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
errorLog('Failed to cache advertiser info:', error);
|
|
179
|
+
}
|
|
169
180
|
debugLog('Platform integrations initialized', {
|
|
170
181
|
meta: metaIntegration.isAvailable(),
|
|
171
182
|
tiktok: tiktokIntegration.isAvailable(),
|
|
@@ -179,7 +190,7 @@ export class DatalyrSDK {
|
|
|
179
190
|
const installData = await attributionManager.trackInstall();
|
|
180
191
|
await this.track('app_install', {
|
|
181
192
|
platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
|
|
182
|
-
sdk_version: '1.
|
|
193
|
+
sdk_version: '1.4.9',
|
|
183
194
|
...installData,
|
|
184
195
|
});
|
|
185
196
|
}
|
|
@@ -760,6 +771,13 @@ export class DatalyrSDK {
|
|
|
760
771
|
}
|
|
761
772
|
metaIntegration.updateTrackingAuthorization(enabled);
|
|
762
773
|
tiktokIntegration.updateTrackingAuthorization(enabled);
|
|
774
|
+
// Refresh cached advertiser info after ATT status change
|
|
775
|
+
try {
|
|
776
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
errorLog('Failed to refresh advertiser info:', error);
|
|
780
|
+
}
|
|
763
781
|
// Track ATT status event
|
|
764
782
|
await this.track('$att_status', {
|
|
765
783
|
authorized: enabled,
|
|
@@ -835,6 +853,8 @@ export class DatalyrSDK {
|
|
|
835
853
|
asa_conversion_type: asaAttribution.conversionType,
|
|
836
854
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
837
855
|
} : {};
|
|
856
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
857
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
838
858
|
const payload = {
|
|
839
859
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
840
860
|
visitorId: this.state.visitorId,
|
|
@@ -852,8 +872,24 @@ export class DatalyrSDK {
|
|
|
852
872
|
device_model: deviceInfo.model,
|
|
853
873
|
app_version: deviceInfo.appVersion,
|
|
854
874
|
app_build: deviceInfo.buildNumber,
|
|
875
|
+
app_name: deviceInfo.bundleId, // Best available app name
|
|
876
|
+
app_namespace: deviceInfo.bundleId,
|
|
877
|
+
screen_width: deviceInfo.screenWidth,
|
|
878
|
+
screen_height: deviceInfo.screenHeight,
|
|
879
|
+
locale: deviceInfo.locale,
|
|
880
|
+
timezone: deviceInfo.timezone,
|
|
881
|
+
carrier: deviceInfo.carrier,
|
|
855
882
|
network_type: getNetworkType(),
|
|
856
883
|
timestamp: Date.now(),
|
|
884
|
+
sdk_version: '1.4.9',
|
|
885
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
886
|
+
...(advertiserInfo ? {
|
|
887
|
+
idfa: advertiserInfo.idfa,
|
|
888
|
+
idfv: advertiserInfo.idfv,
|
|
889
|
+
gaid: advertiserInfo.gaid,
|
|
890
|
+
att_status: advertiserInfo.att_status,
|
|
891
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
892
|
+
} : {}),
|
|
857
893
|
// Attribution data
|
|
858
894
|
...attributionData,
|
|
859
895
|
// Apple Search Ads attribution
|
package/lib/event-queue.js
CHANGED
|
@@ -216,7 +216,7 @@ export const createEventQueue = (httpClient, config) => {
|
|
|
216
216
|
const defaultConfig = {
|
|
217
217
|
maxQueueSize: 100,
|
|
218
218
|
batchSize: 10,
|
|
219
|
-
flushInterval:
|
|
219
|
+
flushInterval: 30000, // 30 seconds — matches SDK constructor defaults and docs
|
|
220
220
|
maxRetryCount: 3,
|
|
221
221
|
};
|
|
222
222
|
return new EventQueue(httpClient, { ...defaultConfig, ...config });
|