@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.
- package/CHANGELOG.md +19 -0
- package/README.md +145 -9
- package/datalyr-react-native.podspec +1 -1
- package/expo-module.config.json +6 -0
- package/ios/DatalyrNativeModule.swift +221 -0
- package/ios/DatalyrSKAdNetworkModule.swift +333 -0
- package/ios/PrivacyInfo.xcprivacy +48 -0
- package/lib/datalyr-sdk.d.ts +6 -0
- package/lib/datalyr-sdk.js +84 -27
- package/lib/index.d.ts +3 -1
- package/lib/index.js +3 -1
- package/lib/integrations/play-install-referrer.d.ts +5 -1
- package/lib/integrations/play-install-referrer.js +14 -4
- package/lib/native/DatalyrNativeBridge.js +20 -4
- package/lib/native/SKAdNetworkBridge.d.ts +121 -0
- package/lib/native/SKAdNetworkBridge.js +288 -4
- package/lib/network-status.d.ts +84 -0
- package/lib/network-status.js +281 -0
- package/lib/types.d.ts +51 -0
- package/lib/utils.d.ts +6 -1
- package/lib/utils.js +52 -2
- package/package.json +12 -2
- package/src/datalyr-sdk.ts +96 -32
- package/src/index.ts +5 -1
- package/src/integrations/play-install-referrer.ts +19 -4
- package/src/native/DatalyrNativeBridge.ts +17 -4
- package/src/native/SKAdNetworkBridge.ts +411 -9
- package/src/network-status.ts +312 -0
- package/src/types.ts +74 -6
- package/src/utils.ts +62 -6
- package/ios/DatalyrNative.m +0 -74
- package/ios/DatalyrNative.swift +0 -332
- package/ios/DatalyrSKAdNetwork.m +0 -77
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to the Datalyr React Native SDK will be documented in this f
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.1] - 2026-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **iOS 18.4+ Features** - Geo-level postbacks, development postbacks, overlapping windows
|
|
12
|
+
- **Privacy Manifest** (`ios/PrivacyInfo.xcprivacy`) - Required for App Store compliance
|
|
13
|
+
- **Network Status Detection** - Automatic online/offline handling with queue sync
|
|
14
|
+
- `SKAdNetworkBridge` iOS 18.4+ methods:
|
|
15
|
+
- `isGeoPostbackAvailable()` - Check for geo-level postback support
|
|
16
|
+
- `setPostbackEnvironment()` - Configure sandbox/production mode
|
|
17
|
+
- `getEnhancedAttributionInfo()` - Full feature matrix by iOS version
|
|
18
|
+
- `updatePostbackWithWindow()` - Overlapping window support
|
|
19
|
+
- `enableDevelopmentMode()` / `disableDevelopmentMode()` - Convenience methods
|
|
20
|
+
- Migration guides from AppsFlyer and Adjust
|
|
21
|
+
- Comprehensive troubleshooting section in README
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Parallel SDK initialization for faster startup
|
|
25
|
+
- Enhanced TypeScript types for iOS 18.4+ responses
|
|
26
|
+
|
|
8
27
|
## [1.2.1] - 2025-01
|
|
9
28
|
|
|
10
29
|
### Added
|
package/README.md
CHANGED
|
@@ -553,24 +553,160 @@ import {
|
|
|
553
553
|
|
|
554
554
|
---
|
|
555
555
|
|
|
556
|
+
## Migrating from AppsFlyer / Adjust
|
|
557
|
+
|
|
558
|
+
Datalyr provides similar functionality with a simpler integration.
|
|
559
|
+
|
|
560
|
+
### From AppsFlyer
|
|
561
|
+
|
|
562
|
+
```typescript
|
|
563
|
+
// BEFORE: AppsFlyer
|
|
564
|
+
import appsFlyer from 'react-native-appsflyer';
|
|
565
|
+
appsFlyer.logEvent('af_purchase', { af_revenue: 99.99, af_currency: 'USD' });
|
|
566
|
+
|
|
567
|
+
// AFTER: Datalyr
|
|
568
|
+
import { Datalyr } from '@datalyr/react-native';
|
|
569
|
+
await Datalyr.trackPurchase(99.99, 'USD', 'product_id');
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### From Adjust
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
// BEFORE: Adjust
|
|
576
|
+
import { Adjust, AdjustEvent } from 'react-native-adjust';
|
|
577
|
+
const event = new AdjustEvent('abc123');
|
|
578
|
+
event.setRevenue(99.99, 'USD');
|
|
579
|
+
Adjust.trackEvent(event);
|
|
580
|
+
|
|
581
|
+
// AFTER: Datalyr
|
|
582
|
+
import { Datalyr } from '@datalyr/react-native';
|
|
583
|
+
await Datalyr.trackPurchase(99.99, 'USD');
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Event Mapping
|
|
587
|
+
|
|
588
|
+
| AppsFlyer | Adjust | Datalyr |
|
|
589
|
+
|-----------|--------|---------|
|
|
590
|
+
| `af_purchase` | `PURCHASE` | `trackPurchase()` |
|
|
591
|
+
| `af_add_to_cart` | `ADD_TO_CART` | `trackAddToCart()` |
|
|
592
|
+
| `af_initiated_checkout` | `INITIATE_CHECKOUT` | `trackInitiateCheckout()` |
|
|
593
|
+
| `af_complete_registration` | `COMPLETE_REGISTRATION` | `trackCompleteRegistration()` |
|
|
594
|
+
| `af_content_view` | `VIEW_CONTENT` | `trackViewContent()` |
|
|
595
|
+
| `af_subscribe` | `SUBSCRIBE` | `trackSubscription()` |
|
|
596
|
+
|
|
597
|
+
### Migration Checklist
|
|
598
|
+
|
|
599
|
+
- [ ] Remove old SDK: `npm uninstall react-native-appsflyer`
|
|
600
|
+
- [ ] Install Datalyr: `npm install @datalyr/react-native`
|
|
601
|
+
- [ ] Run `cd ios && pod install`
|
|
602
|
+
- [ ] Replace initialization and event tracking code
|
|
603
|
+
- [ ] Verify events in Datalyr dashboard
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
556
607
|
## Troubleshooting
|
|
557
608
|
|
|
558
|
-
### Events
|
|
609
|
+
### Events Not Appearing
|
|
610
|
+
|
|
611
|
+
**1. Check SDK Status**
|
|
612
|
+
```typescript
|
|
613
|
+
const status = Datalyr.getStatus();
|
|
614
|
+
console.log('Initialized:', status.initialized);
|
|
615
|
+
console.log('Queue size:', status.queueStats.queueSize);
|
|
616
|
+
console.log('Online:', status.queueStats.isOnline);
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**2. Enable Debug Mode**
|
|
620
|
+
```typescript
|
|
621
|
+
await Datalyr.initialize({
|
|
622
|
+
apiKey: 'dk_your_api_key',
|
|
623
|
+
debug: true,
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
**3. Force Flush**
|
|
628
|
+
```typescript
|
|
629
|
+
await Datalyr.flush();
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
**4. Verify API Key** - Should start with `dk_`
|
|
559
633
|
|
|
560
|
-
|
|
561
|
-
2. Enable `debug: true`
|
|
562
|
-
3. Check `Datalyr.getStatus()` for queue info
|
|
563
|
-
4. Verify network connectivity
|
|
634
|
+
### iOS Build Errors
|
|
564
635
|
|
|
565
|
-
|
|
636
|
+
```bash
|
|
637
|
+
cd ios
|
|
638
|
+
pod deintegrate
|
|
639
|
+
pod cache clean --all
|
|
640
|
+
pod install
|
|
641
|
+
```
|
|
566
642
|
|
|
643
|
+
**Clean Reset**
|
|
567
644
|
```bash
|
|
568
|
-
|
|
645
|
+
rm -rf node_modules ios/Pods ios/Podfile.lock
|
|
646
|
+
npm install && cd ios && pod install
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### Android Build Errors
|
|
650
|
+
|
|
651
|
+
```bash
|
|
652
|
+
cd android && ./gradlew clean
|
|
653
|
+
npx react-native run-android
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
### Meta SDK Not Working
|
|
657
|
+
|
|
658
|
+
Verify Info.plist:
|
|
659
|
+
```xml
|
|
660
|
+
<key>FacebookAppID</key>
|
|
661
|
+
<string>YOUR_APP_ID</string>
|
|
662
|
+
<key>FacebookClientToken</key>
|
|
663
|
+
<string>YOUR_CLIENT_TOKEN</string>
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Check status: `Datalyr.getPlatformIntegrationStatus()`
|
|
667
|
+
|
|
668
|
+
### TikTok SDK Not Working
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
await Datalyr.initialize({
|
|
672
|
+
apiKey: 'dk_your_api_key',
|
|
673
|
+
tiktok: {
|
|
674
|
+
appId: 'your_app_id',
|
|
675
|
+
tiktokAppId: '7123456789012345',
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### SKAdNetwork Not Updating
|
|
681
|
+
|
|
682
|
+
1. iOS 14.0+ required (16.1+ for SKAN 4.0)
|
|
683
|
+
2. Set `skadTemplate` in config
|
|
684
|
+
3. Use `trackWithSKAdNetwork()` instead of `track()`
|
|
685
|
+
|
|
686
|
+
### Attribution Not Captured
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
await Datalyr.initialize({
|
|
690
|
+
apiKey: 'dk_your_api_key',
|
|
691
|
+
enableAttribution: true,
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// Check data
|
|
695
|
+
const attribution = Datalyr.getAttributionData();
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### App Tracking Transparency (iOS 14.5+)
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
import { requestTrackingPermissionsAsync } from 'expo-tracking-transparency';
|
|
702
|
+
|
|
703
|
+
const { status } = await requestTrackingPermissionsAsync();
|
|
704
|
+
await Datalyr.updateTrackingAuthorization(status === 'granted');
|
|
569
705
|
```
|
|
570
706
|
|
|
571
|
-
###
|
|
707
|
+
### Debug Logging
|
|
572
708
|
|
|
573
|
-
|
|
709
|
+
Look for `[Datalyr]` prefixed messages in console.
|
|
574
710
|
|
|
575
711
|
---
|
|
576
712
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import FBSDKCoreKit
|
|
3
|
+
import TikTokBusinessSDK
|
|
4
|
+
import AdServices
|
|
5
|
+
|
|
6
|
+
public class DatalyrNativeModule: Module {
|
|
7
|
+
public func definition() -> ModuleDefinition {
|
|
8
|
+
Name("DatalyrNative")
|
|
9
|
+
|
|
10
|
+
// MARK: - Meta (Facebook) SDK Methods
|
|
11
|
+
|
|
12
|
+
AsyncFunction("initializeMetaSDK") { (appId: String, clientToken: String?, advertiserTrackingEnabled: Bool, promise: Promise) in
|
|
13
|
+
DispatchQueue.main.async {
|
|
14
|
+
Settings.shared.appID = appId
|
|
15
|
+
|
|
16
|
+
if let token = clientToken, !token.isEmpty {
|
|
17
|
+
Settings.shared.clientToken = token
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Settings.shared.isAdvertiserTrackingEnabled = advertiserTrackingEnabled
|
|
21
|
+
Settings.shared.isAdvertiserIDCollectionEnabled = advertiserTrackingEnabled
|
|
22
|
+
|
|
23
|
+
ApplicationDelegate.shared.application(
|
|
24
|
+
UIApplication.shared,
|
|
25
|
+
didFinishLaunchingWithOptions: nil
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
promise.resolve(true)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
AsyncFunction("fetchDeferredAppLink") { (promise: Promise) in
|
|
33
|
+
AppLinkUtility.fetchDeferredAppLink { url, error in
|
|
34
|
+
if error != nil {
|
|
35
|
+
promise.resolve(nil)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if let url = url {
|
|
40
|
+
promise.resolve(url.absoluteString)
|
|
41
|
+
} else {
|
|
42
|
+
promise.resolve(nil)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
AsyncFunction("logMetaEvent") { (eventName: String, valueToSum: Double?, parameters: [String: Any]?, promise: Promise) in
|
|
48
|
+
var params: [AppEvents.ParameterName: Any] = [:]
|
|
49
|
+
|
|
50
|
+
if let dict = parameters {
|
|
51
|
+
for (key, value) in dict {
|
|
52
|
+
params[AppEvents.ParameterName(key)] = value
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if let value = valueToSum {
|
|
57
|
+
AppEvents.shared.logEvent(AppEvents.Name(eventName), valueToSum: value, parameters: params)
|
|
58
|
+
} else if params.isEmpty {
|
|
59
|
+
AppEvents.shared.logEvent(AppEvents.Name(eventName))
|
|
60
|
+
} else {
|
|
61
|
+
AppEvents.shared.logEvent(AppEvents.Name(eventName), parameters: params)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
promise.resolve(true)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
AsyncFunction("logMetaPurchase") { (amount: Double, currency: String, parameters: [String: Any]?, promise: Promise) in
|
|
68
|
+
var params: [AppEvents.ParameterName: Any] = [:]
|
|
69
|
+
|
|
70
|
+
if let dict = parameters {
|
|
71
|
+
for (key, value) in dict {
|
|
72
|
+
params[AppEvents.ParameterName(key)] = value
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
AppEvents.shared.logPurchase(amount: amount, currency: currency, parameters: params)
|
|
77
|
+
promise.resolve(true)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
AsyncFunction("setMetaUserData") { (userData: [String: Any], promise: Promise) in
|
|
81
|
+
AppEvents.shared.setUserData(userData["email"] as? String, forType: .email)
|
|
82
|
+
AppEvents.shared.setUserData(userData["firstName"] as? String, forType: .firstName)
|
|
83
|
+
AppEvents.shared.setUserData(userData["lastName"] as? String, forType: .lastName)
|
|
84
|
+
AppEvents.shared.setUserData(userData["phone"] as? String, forType: .phone)
|
|
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)
|
|
91
|
+
|
|
92
|
+
promise.resolve(true)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
AsyncFunction("clearMetaUserData") { (promise: Promise) in
|
|
96
|
+
AppEvents.shared.clearUserData()
|
|
97
|
+
promise.resolve(true)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
AsyncFunction("updateMetaTrackingAuthorization") { (enabled: Bool, promise: Promise) in
|
|
101
|
+
Settings.shared.isAdvertiserTrackingEnabled = enabled
|
|
102
|
+
Settings.shared.isAdvertiserIDCollectionEnabled = enabled
|
|
103
|
+
promise.resolve(true)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: - TikTok SDK Methods
|
|
107
|
+
|
|
108
|
+
AsyncFunction("initializeTikTokSDK") { (appId: String, tiktokAppId: String, accessToken: String?, debug: Bool, promise: Promise) in
|
|
109
|
+
DispatchQueue.main.async {
|
|
110
|
+
let config = TikTokConfig(appId: appId, tiktokAppId: tiktokAppId)
|
|
111
|
+
|
|
112
|
+
if let token = accessToken, !token.isEmpty {
|
|
113
|
+
config?.accessToken = token
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if debug {
|
|
117
|
+
config?.setLogLevel(.debug)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if let validConfig = config {
|
|
121
|
+
TikTokBusiness.initializeSdk(validConfig)
|
|
122
|
+
promise.resolve(true)
|
|
123
|
+
} else {
|
|
124
|
+
promise.reject("tiktok_init_error", "Failed to create TikTok config")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
AsyncFunction("trackTikTokEvent") { (eventName: String, eventId: String?, properties: [String: Any]?, promise: Promise) in
|
|
130
|
+
let event: TikTokBaseEvent
|
|
131
|
+
|
|
132
|
+
if let eid = eventId, !eid.isEmpty {
|
|
133
|
+
event = TikTokBaseEvent(eventName: eventName, eventId: eid)
|
|
134
|
+
} else {
|
|
135
|
+
event = TikTokBaseEvent(eventName: eventName)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if let dict = properties {
|
|
139
|
+
for (key, value) in dict {
|
|
140
|
+
event.addProperty(withKey: key, value: value)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
TikTokBusiness.trackTTEvent(event)
|
|
145
|
+
promise.resolve(true)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
AsyncFunction("identifyTikTokUser") { (externalId: String, externalUserName: String, phoneNumber: String, email: String, promise: Promise) in
|
|
149
|
+
TikTokBusiness.identify(
|
|
150
|
+
withExternalID: externalId.isEmpty ? nil : externalId,
|
|
151
|
+
externalUserName: externalUserName.isEmpty ? nil : externalUserName,
|
|
152
|
+
phoneNumber: phoneNumber.isEmpty ? nil : phoneNumber,
|
|
153
|
+
email: email.isEmpty ? nil : email
|
|
154
|
+
)
|
|
155
|
+
promise.resolve(true)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
AsyncFunction("logoutTikTok") { (promise: Promise) in
|
|
159
|
+
TikTokBusiness.logout()
|
|
160
|
+
promise.resolve(true)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
AsyncFunction("updateTikTokTrackingAuthorization") { (enabled: Bool, promise: Promise) in
|
|
164
|
+
// TikTok SDK handles ATT automatically, but we track the change
|
|
165
|
+
promise.resolve(true)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// MARK: - SDK Availability Check
|
|
169
|
+
|
|
170
|
+
AsyncFunction("getSDKAvailability") { (promise: Promise) in
|
|
171
|
+
promise.resolve([
|
|
172
|
+
"meta": true,
|
|
173
|
+
"tiktok": true,
|
|
174
|
+
"appleSearchAds": true
|
|
175
|
+
])
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// MARK: - Apple Search Ads Attribution
|
|
179
|
+
|
|
180
|
+
AsyncFunction("getAppleSearchAdsAttribution") { (promise: Promise) in
|
|
181
|
+
if #available(iOS 14.3, *) {
|
|
182
|
+
do {
|
|
183
|
+
let token = try AAAttribution.attributionToken()
|
|
184
|
+
|
|
185
|
+
var request = URLRequest(url: URL(string: "https://api-adservices.apple.com/api/v1/")!)
|
|
186
|
+
request.httpMethod = "POST"
|
|
187
|
+
request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
|
|
188
|
+
request.httpBody = token.data(using: .utf8)
|
|
189
|
+
|
|
190
|
+
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
|
191
|
+
if error != nil {
|
|
192
|
+
promise.resolve(nil)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
guard let data = data else {
|
|
197
|
+
promise.resolve(nil)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
do {
|
|
202
|
+
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
203
|
+
promise.resolve(json)
|
|
204
|
+
} else {
|
|
205
|
+
promise.resolve(nil)
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
promise.resolve(nil)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
task.resume()
|
|
212
|
+
|
|
213
|
+
} catch {
|
|
214
|
+
promise.resolve(nil)
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
promise.resolve(nil)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|