@datalyr/react-native 1.4.8 → 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 +40 -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,6 +2,8 @@ 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 {
|
|
7
9
|
private var tiktokInitialized = false
|
|
@@ -280,6 +282,44 @@ public class DatalyrNativeModule: Module {
|
|
|
280
282
|
])
|
|
281
283
|
}
|
|
282
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
|
+
|
|
283
323
|
// MARK: - Apple Search Ads Attribution
|
|
284
324
|
|
|
285
325
|
AsyncFunction("getAppleSearchAdsAttribution") { (promise: Promise) in
|
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 });
|
package/lib/http-client.js
CHANGED
|
@@ -45,7 +45,7 @@ export class HttpClient {
|
|
|
45
45
|
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
46
46
|
const headers = {
|
|
47
47
|
'Content-Type': 'application/json',
|
|
48
|
-
'User-Agent': `@datalyr/react-native/1.
|
|
48
|
+
'User-Agent': `@datalyr/react-native/1.4.8`,
|
|
49
49
|
};
|
|
50
50
|
// Server-side tracking uses X-API-Key header
|
|
51
51
|
if (this.config.useServerTracking !== false) {
|
|
@@ -143,7 +143,7 @@ export class HttpClient {
|
|
|
143
143
|
return {
|
|
144
144
|
event: payload.eventName,
|
|
145
145
|
userId: payload.userId || payload.visitorId,
|
|
146
|
-
anonymousId: payload.visitorId,
|
|
146
|
+
anonymousId: payload.anonymousId || payload.visitorId,
|
|
147
147
|
properties: {
|
|
148
148
|
...payload.eventData,
|
|
149
149
|
sessionId: payload.sessionId,
|
|
@@ -152,7 +152,7 @@ export class HttpClient {
|
|
|
152
152
|
},
|
|
153
153
|
context: {
|
|
154
154
|
library: '@datalyr/react-native',
|
|
155
|
-
version: '1.
|
|
155
|
+
version: '1.4.8',
|
|
156
156
|
source: 'mobile_app', // Explicitly set source for mobile
|
|
157
157
|
userProperties: payload.userProperties,
|
|
158
158
|
},
|
|
@@ -62,9 +62,12 @@ export class TikTokIntegration {
|
|
|
62
62
|
this.initialized = true;
|
|
63
63
|
this.log(`TikTok SDK initialized with App ID: ${config.tiktokAppId}`);
|
|
64
64
|
}
|
|
65
|
+
else {
|
|
66
|
+
console.warn('[Datalyr/TikTok] TikTok SDK not initialized (accessToken may be missing). Events will still be sent server-side via Datalyr postbacks.');
|
|
67
|
+
}
|
|
65
68
|
}
|
|
66
69
|
catch (error) {
|
|
67
|
-
|
|
70
|
+
console.warn('[Datalyr/TikTok] TikTok SDK init failed. Events will still be sent server-side via Datalyr postbacks.', error);
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
/**
|
|
@@ -78,6 +78,21 @@ export declare const AppleSearchAdsNativeBridge: {
|
|
|
78
78
|
*/
|
|
79
79
|
getAttribution(): Promise<AppleSearchAdsAttribution | null>;
|
|
80
80
|
};
|
|
81
|
+
export interface AdvertiserInfo {
|
|
82
|
+
idfa?: string;
|
|
83
|
+
idfv?: string;
|
|
84
|
+
gaid?: string;
|
|
85
|
+
att_status: number;
|
|
86
|
+
advertiser_tracking_enabled: boolean;
|
|
87
|
+
}
|
|
88
|
+
export declare const AdvertiserInfoBridge: {
|
|
89
|
+
/**
|
|
90
|
+
* Get advertiser info (IDFA, IDFV, ATT status)
|
|
91
|
+
* IDFA is only available when ATT is authorized (iOS 14+)
|
|
92
|
+
* IDFV is always available on iOS
|
|
93
|
+
*/
|
|
94
|
+
getAdvertiserInfo(): Promise<AdvertiserInfo | null>;
|
|
95
|
+
};
|
|
81
96
|
export declare const PlayInstallReferrerNativeBridge: {
|
|
82
97
|
/**
|
|
83
98
|
* Check if Play Install Referrer is available
|
|
@@ -219,6 +219,24 @@ export const AppleSearchAdsNativeBridge = {
|
|
|
219
219
|
}
|
|
220
220
|
},
|
|
221
221
|
};
|
|
222
|
+
export const AdvertiserInfoBridge = {
|
|
223
|
+
/**
|
|
224
|
+
* Get advertiser info (IDFA, IDFV, ATT status)
|
|
225
|
+
* IDFA is only available when ATT is authorized (iOS 14+)
|
|
226
|
+
* IDFV is always available on iOS
|
|
227
|
+
*/
|
|
228
|
+
async getAdvertiserInfo() {
|
|
229
|
+
if (!DatalyrNative)
|
|
230
|
+
return null;
|
|
231
|
+
try {
|
|
232
|
+
return await DatalyrNative.getAdvertiserInfo();
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('[Datalyr/AdvertiserInfo] Get advertiser info failed:', error);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
222
240
|
// MARK: - Play Install Referrer Bridge (Android only)
|
|
223
241
|
export const PlayInstallReferrerNativeBridge = {
|
|
224
242
|
/**
|
package/lib/native/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
* Native Module Exports
|
|
3
3
|
*/
|
|
4
4
|
export { SKAdNetworkBridge } from './SKAdNetworkBridge';
|
|
5
|
-
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, } from './DatalyrNativeBridge';
|
|
5
|
+
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, AdvertiserInfoBridge, } from './DatalyrNativeBridge';
|
|
6
|
+
export type { AdvertiserInfo } from './DatalyrNativeBridge';
|
package/lib/native/index.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
* Native Module Exports
|
|
3
3
|
*/
|
|
4
4
|
export { SKAdNetworkBridge } from './SKAdNetworkBridge';
|
|
5
|
-
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, } from './DatalyrNativeBridge';
|
|
5
|
+
export { isNativeModuleAvailable, getSDKAvailability, MetaNativeBridge, TikTokNativeBridge, AdvertiserInfoBridge, } from './DatalyrNativeBridge';
|
package/lib/types.d.ts
CHANGED
|
@@ -83,9 +83,9 @@ export interface DatalyrConfig {
|
|
|
83
83
|
maxEventQueueSize?: number;
|
|
84
84
|
/** Respect browser Do Not Track setting. Default: true */
|
|
85
85
|
respectDoNotTrack?: boolean;
|
|
86
|
-
/** Enable automatic event tracking (sessions, app lifecycle). Default:
|
|
86
|
+
/** Enable automatic event tracking (sessions, app lifecycle). Default: true */
|
|
87
87
|
enableAutoEvents?: boolean;
|
|
88
|
-
/** Enable attribution tracking (deep links, install referrer). Default:
|
|
88
|
+
/** Enable attribution tracking (deep links, install referrer). Default: true */
|
|
89
89
|
enableAttribution?: boolean;
|
|
90
90
|
/** Enable web-to-app attribution matching via email. Default: true */
|
|
91
91
|
enableWebToAppAttribution?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalyr/react-native",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.9",
|
|
4
4
|
"description": "Datalyr SDK for React Native & Expo - Server-side attribution tracking with bundled Meta and TikTok SDKs for iOS and Android",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
package/src/datalyr-sdk-expo.ts
CHANGED
|
@@ -34,9 +34,9 @@ import { journeyManager } from './journey';
|
|
|
34
34
|
import { createAutoEventsManager, AutoEventsManager } from './auto-events';
|
|
35
35
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
36
36
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
37
|
-
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration } from './integrations';
|
|
37
|
+
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
38
38
|
import { DeferredDeepLinkResult } from './types';
|
|
39
|
-
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
39
|
+
import { AppleSearchAdsAttribution, AdvertiserInfoBridge } from './native/DatalyrNativeBridge';
|
|
40
40
|
|
|
41
41
|
export class DatalyrSDKExpo {
|
|
42
42
|
private state: SDKState;
|
|
@@ -44,6 +44,7 @@ export class DatalyrSDKExpo {
|
|
|
44
44
|
private eventQueue: EventQueue;
|
|
45
45
|
private autoEventsManager: AutoEventsManager | null = null;
|
|
46
46
|
private appStateSubscription: any = null;
|
|
47
|
+
private cachedAdvertiserInfo: any = null;
|
|
47
48
|
private static conversionEncoder?: ConversionValueEncoder;
|
|
48
49
|
private static debugEnabled = false;
|
|
49
50
|
|
|
@@ -59,9 +60,11 @@ export class DatalyrSDKExpo {
|
|
|
59
60
|
maxRetries: 3,
|
|
60
61
|
retryDelay: 1000,
|
|
61
62
|
batchSize: 10,
|
|
62
|
-
flushInterval:
|
|
63
|
+
flushInterval: 30000,
|
|
63
64
|
maxQueueSize: 100,
|
|
64
65
|
respectDoNotTrack: true,
|
|
66
|
+
enableAutoEvents: true,
|
|
67
|
+
enableAttribution: true,
|
|
65
68
|
},
|
|
66
69
|
visitorId: '',
|
|
67
70
|
anonymousId: '',
|
|
@@ -198,10 +201,22 @@ export class DatalyrSDKExpo {
|
|
|
198
201
|
|
|
199
202
|
// Initialize Apple Search Ads attribution (iOS only, auto-fetches on init)
|
|
200
203
|
await appleSearchAdsIntegration.initialize(config.debug);
|
|
204
|
+
|
|
205
|
+
// Initialize Play Install Referrer (Android only)
|
|
206
|
+
await playInstallReferrerIntegration.initialize();
|
|
207
|
+
|
|
208
|
+
// Cache advertiser info (IDFA/GAID, ATT status) once at init to avoid per-event native bridge calls
|
|
209
|
+
try {
|
|
210
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
211
|
+
} catch (error) {
|
|
212
|
+
errorLog('Failed to cache advertiser info:', error as Error);
|
|
213
|
+
}
|
|
214
|
+
|
|
201
215
|
debugLog('Platform integrations initialized', {
|
|
202
216
|
meta: metaIntegration.isAvailable(),
|
|
203
217
|
tiktok: tiktokIntegration.isAvailable(),
|
|
204
218
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
219
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
205
220
|
});
|
|
206
221
|
|
|
207
222
|
this.state.initialized = true;
|
|
@@ -210,7 +225,7 @@ export class DatalyrSDKExpo {
|
|
|
210
225
|
const installData = await attributionManager.trackInstall();
|
|
211
226
|
await this.track('app_install', {
|
|
212
227
|
platform: Platform.OS,
|
|
213
|
-
sdk_version: '1.
|
|
228
|
+
sdk_version: '1.4.9',
|
|
214
229
|
sdk_variant: 'expo',
|
|
215
230
|
...installData,
|
|
216
231
|
});
|
|
@@ -694,14 +709,23 @@ export class DatalyrSDKExpo {
|
|
|
694
709
|
return null;
|
|
695
710
|
}
|
|
696
711
|
|
|
697
|
-
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
712
|
+
getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
|
|
698
713
|
return {
|
|
699
714
|
meta: metaIntegration.isAvailable(),
|
|
700
715
|
tiktok: tiktokIntegration.isAvailable(),
|
|
701
716
|
appleSearchAds: appleSearchAdsIntegration.isAvailable(),
|
|
717
|
+
playInstallReferrer: playInstallReferrerIntegration.isAvailable(),
|
|
702
718
|
};
|
|
703
719
|
}
|
|
704
720
|
|
|
721
|
+
/**
|
|
722
|
+
* Get Play Install Referrer data (Android only)
|
|
723
|
+
*/
|
|
724
|
+
getPlayInstallReferrer(): Record<string, any> | null {
|
|
725
|
+
const data = playInstallReferrerIntegration.getReferrerData();
|
|
726
|
+
return data ? playInstallReferrerIntegration.getAttributionData() : null;
|
|
727
|
+
}
|
|
728
|
+
|
|
705
729
|
/**
|
|
706
730
|
* Get Apple Search Ads attribution data
|
|
707
731
|
* Returns attribution if user installed via Apple Search Ads, null otherwise
|
|
@@ -717,6 +741,13 @@ export class DatalyrSDKExpo {
|
|
|
717
741
|
if (tiktokIntegration.isAvailable()) {
|
|
718
742
|
tiktokIntegration.updateTrackingAuthorization(authorized);
|
|
719
743
|
}
|
|
744
|
+
|
|
745
|
+
// Refresh cached advertiser info after ATT status change
|
|
746
|
+
try {
|
|
747
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
748
|
+
} catch (error) {
|
|
749
|
+
errorLog('Failed to refresh advertiser info:', error as Error);
|
|
750
|
+
}
|
|
720
751
|
}
|
|
721
752
|
|
|
722
753
|
private async handleDeferredDeepLink(data: DeferredDeepLinkResult): Promise<void> {
|
|
@@ -759,7 +790,7 @@ export class DatalyrSDKExpo {
|
|
|
759
790
|
const deviceInfo = await getDeviceInfo();
|
|
760
791
|
const fingerprintData = await createFingerprintData();
|
|
761
792
|
const attributionData = attributionManager.getAttributionData();
|
|
762
|
-
const networkType =
|
|
793
|
+
const networkType = getNetworkType();
|
|
763
794
|
|
|
764
795
|
// Get Apple Search Ads attribution if available
|
|
765
796
|
const asaAttribution = appleSearchAdsIntegration.getAttributionData();
|
|
@@ -777,6 +808,9 @@ export class DatalyrSDKExpo {
|
|
|
777
808
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
778
809
|
} : {};
|
|
779
810
|
|
|
811
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
812
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
813
|
+
|
|
780
814
|
const payload: EventPayload = {
|
|
781
815
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
782
816
|
visitorId: this.state.visitorId,
|
|
@@ -792,9 +826,25 @@ export class DatalyrSDKExpo {
|
|
|
792
826
|
device_model: deviceInfo.model,
|
|
793
827
|
app_version: deviceInfo.appVersion,
|
|
794
828
|
app_build: deviceInfo.buildNumber,
|
|
829
|
+
app_name: deviceInfo.bundleId,
|
|
830
|
+
app_namespace: deviceInfo.bundleId,
|
|
831
|
+
screen_width: deviceInfo.screenWidth,
|
|
832
|
+
screen_height: deviceInfo.screenHeight,
|
|
833
|
+
locale: deviceInfo.locale,
|
|
834
|
+
timezone: deviceInfo.timezone,
|
|
835
|
+
carrier: deviceInfo.carrier,
|
|
795
836
|
network_type: networkType,
|
|
796
837
|
timestamp: Date.now(),
|
|
838
|
+
sdk_version: '1.4.9',
|
|
797
839
|
sdk_variant: 'expo',
|
|
840
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
841
|
+
...(advertiserInfo ? {
|
|
842
|
+
idfa: advertiserInfo.idfa,
|
|
843
|
+
idfv: advertiserInfo.idfv,
|
|
844
|
+
gaid: advertiserInfo.gaid,
|
|
845
|
+
att_status: advertiserInfo.att_status,
|
|
846
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
847
|
+
} : {}),
|
|
798
848
|
...attributionData,
|
|
799
849
|
// Apple Search Ads attribution
|
|
800
850
|
...asaData,
|
|
@@ -1030,10 +1080,14 @@ export class DatalyrExpo {
|
|
|
1030
1080
|
return datalyrExpo.getDeferredAttributionData();
|
|
1031
1081
|
}
|
|
1032
1082
|
|
|
1033
|
-
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean } {
|
|
1083
|
+
static getPlatformIntegrationStatus(): { meta: boolean; tiktok: boolean; appleSearchAds: boolean; playInstallReferrer: boolean } {
|
|
1034
1084
|
return datalyrExpo.getPlatformIntegrationStatus();
|
|
1035
1085
|
}
|
|
1036
1086
|
|
|
1087
|
+
static getPlayInstallReferrer(): Record<string, any> | null {
|
|
1088
|
+
return datalyrExpo.getPlayInstallReferrer();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1037
1091
|
static getAppleSearchAdsAttribution(): AppleSearchAdsAttribution | null {
|
|
1038
1092
|
return datalyrExpo.getAppleSearchAdsAttribution();
|
|
1039
1093
|
}
|
package/src/datalyr-sdk.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { createAutoEventsManager, AutoEventsManager, SessionData } from './auto-
|
|
|
32
32
|
import { ConversionValueEncoder, ConversionTemplates } from './ConversionValueEncoder';
|
|
33
33
|
import { SKAdNetworkBridge } from './native/SKAdNetworkBridge';
|
|
34
34
|
import { metaIntegration, tiktokIntegration, appleSearchAdsIntegration, playInstallReferrerIntegration } from './integrations';
|
|
35
|
-
import { AppleSearchAdsAttribution } from './native/DatalyrNativeBridge';
|
|
35
|
+
import { AppleSearchAdsAttribution, AdvertiserInfoBridge } from './native/DatalyrNativeBridge';
|
|
36
36
|
import { networkStatusManager } from './network-status';
|
|
37
37
|
|
|
38
38
|
export class DatalyrSDK {
|
|
@@ -42,6 +42,7 @@ export class DatalyrSDK {
|
|
|
42
42
|
private autoEventsManager: AutoEventsManager | null = null;
|
|
43
43
|
private appStateSubscription: any = null;
|
|
44
44
|
private networkStatusUnsubscribe: (() => void) | null = null;
|
|
45
|
+
private cachedAdvertiserInfo: any = null;
|
|
45
46
|
private static conversionEncoder?: ConversionValueEncoder;
|
|
46
47
|
private static debugEnabled = false;
|
|
47
48
|
|
|
@@ -58,9 +59,11 @@ export class DatalyrSDK {
|
|
|
58
59
|
maxRetries: 3,
|
|
59
60
|
retryDelay: 1000,
|
|
60
61
|
batchSize: 10,
|
|
61
|
-
flushInterval:
|
|
62
|
+
flushInterval: 30000,
|
|
62
63
|
maxQueueSize: 100,
|
|
63
64
|
respectDoNotTrack: true,
|
|
65
|
+
enableAutoEvents: true,
|
|
66
|
+
enableAttribution: true,
|
|
64
67
|
},
|
|
65
68
|
visitorId: '',
|
|
66
69
|
anonymousId: '', // Persistent anonymous identifier
|
|
@@ -221,6 +224,13 @@ export class DatalyrSDK {
|
|
|
221
224
|
// Wait for all platform integrations to complete
|
|
222
225
|
await Promise.all(platformInitPromises);
|
|
223
226
|
|
|
227
|
+
// Cache advertiser info (IDFA/GAID, ATT status) once at init to avoid per-event native bridge calls
|
|
228
|
+
try {
|
|
229
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
230
|
+
} catch (error) {
|
|
231
|
+
errorLog('Failed to cache advertiser info:', error as Error);
|
|
232
|
+
}
|
|
233
|
+
|
|
224
234
|
debugLog('Platform integrations initialized', {
|
|
225
235
|
meta: metaIntegration.isAvailable(),
|
|
226
236
|
tiktok: tiktokIntegration.isAvailable(),
|
|
@@ -236,7 +246,7 @@ export class DatalyrSDK {
|
|
|
236
246
|
const installData = await attributionManager.trackInstall();
|
|
237
247
|
await this.track('app_install', {
|
|
238
248
|
platform: Platform.OS === 'ios' || Platform.OS === 'android' ? Platform.OS : 'android',
|
|
239
|
-
sdk_version: '1.
|
|
249
|
+
sdk_version: '1.4.9',
|
|
240
250
|
...installData,
|
|
241
251
|
});
|
|
242
252
|
}
|
|
@@ -939,6 +949,13 @@ export class DatalyrSDK {
|
|
|
939
949
|
metaIntegration.updateTrackingAuthorization(enabled);
|
|
940
950
|
tiktokIntegration.updateTrackingAuthorization(enabled);
|
|
941
951
|
|
|
952
|
+
// Refresh cached advertiser info after ATT status change
|
|
953
|
+
try {
|
|
954
|
+
this.cachedAdvertiserInfo = await AdvertiserInfoBridge.getAdvertiserInfo();
|
|
955
|
+
} catch (error) {
|
|
956
|
+
errorLog('Failed to refresh advertiser info:', error as Error);
|
|
957
|
+
}
|
|
958
|
+
|
|
942
959
|
// Track ATT status event
|
|
943
960
|
await this.track('$att_status', {
|
|
944
961
|
authorized: enabled,
|
|
@@ -1022,6 +1039,9 @@ export class DatalyrSDK {
|
|
|
1022
1039
|
asa_country_or_region: asaAttribution.countryOrRegion,
|
|
1023
1040
|
} : {};
|
|
1024
1041
|
|
|
1042
|
+
// Use cached advertiser info (IDFA/GAID, ATT status) — cached at init, refreshed on ATT change
|
|
1043
|
+
const advertiserInfo = this.cachedAdvertiserInfo;
|
|
1044
|
+
|
|
1025
1045
|
const payload: EventPayload = {
|
|
1026
1046
|
workspaceId: this.state.config.workspaceId || 'mobile_sdk',
|
|
1027
1047
|
visitorId: this.state.visitorId,
|
|
@@ -1039,8 +1059,24 @@ export class DatalyrSDK {
|
|
|
1039
1059
|
device_model: deviceInfo.model,
|
|
1040
1060
|
app_version: deviceInfo.appVersion,
|
|
1041
1061
|
app_build: deviceInfo.buildNumber,
|
|
1062
|
+
app_name: deviceInfo.bundleId, // Best available app name
|
|
1063
|
+
app_namespace: deviceInfo.bundleId,
|
|
1064
|
+
screen_width: deviceInfo.screenWidth,
|
|
1065
|
+
screen_height: deviceInfo.screenHeight,
|
|
1066
|
+
locale: deviceInfo.locale,
|
|
1067
|
+
timezone: deviceInfo.timezone,
|
|
1068
|
+
carrier: deviceInfo.carrier,
|
|
1042
1069
|
network_type: getNetworkType(),
|
|
1043
1070
|
timestamp: Date.now(),
|
|
1071
|
+
sdk_version: '1.4.9',
|
|
1072
|
+
// Advertiser data (IDFA/GAID, ATT status) for Meta CAPI / TikTok Events API
|
|
1073
|
+
...(advertiserInfo ? {
|
|
1074
|
+
idfa: advertiserInfo.idfa,
|
|
1075
|
+
idfv: advertiserInfo.idfv,
|
|
1076
|
+
gaid: advertiserInfo.gaid,
|
|
1077
|
+
att_status: advertiserInfo.att_status,
|
|
1078
|
+
advertiser_tracking_enabled: advertiserInfo.advertiser_tracking_enabled,
|
|
1079
|
+
} : {}),
|
|
1044
1080
|
// Attribution data
|
|
1045
1081
|
...attributionData,
|
|
1046
1082
|
// Apple Search Ads attribution
|
package/src/event-queue.ts
CHANGED
|
@@ -262,7 +262,7 @@ export const createEventQueue = (httpClient: HttpClient, config?: Partial<QueueC
|
|
|
262
262
|
const defaultConfig: QueueConfig = {
|
|
263
263
|
maxQueueSize: 100,
|
|
264
264
|
batchSize: 10,
|
|
265
|
-
flushInterval:
|
|
265
|
+
flushInterval: 30000, // 30 seconds — matches SDK constructor defaults and docs
|
|
266
266
|
maxRetryCount: 3,
|
|
267
267
|
};
|
|
268
268
|
|
package/src/http-client.ts
CHANGED
|
@@ -72,7 +72,7 @@ export class HttpClient {
|
|
|
72
72
|
|
|
73
73
|
const headers: Record<string, string> = {
|
|
74
74
|
'Content-Type': 'application/json',
|
|
75
|
-
'User-Agent': `@datalyr/react-native/1.
|
|
75
|
+
'User-Agent': `@datalyr/react-native/1.4.8`,
|
|
76
76
|
};
|
|
77
77
|
|
|
78
78
|
// Server-side tracking uses X-API-Key header
|
|
@@ -188,7 +188,7 @@ export class HttpClient {
|
|
|
188
188
|
return {
|
|
189
189
|
event: payload.eventName,
|
|
190
190
|
userId: payload.userId || payload.visitorId,
|
|
191
|
-
anonymousId: payload.visitorId,
|
|
191
|
+
anonymousId: payload.anonymousId || payload.visitorId,
|
|
192
192
|
properties: {
|
|
193
193
|
...payload.eventData,
|
|
194
194
|
sessionId: payload.sessionId,
|
|
@@ -197,7 +197,7 @@ export class HttpClient {
|
|
|
197
197
|
},
|
|
198
198
|
context: {
|
|
199
199
|
library: '@datalyr/react-native',
|
|
200
|
-
version: '1.
|
|
200
|
+
version: '1.4.8',
|
|
201
201
|
source: 'mobile_app', // Explicitly set source for mobile
|
|
202
202
|
userProperties: payload.userProperties,
|
|
203
203
|
},
|
|
@@ -76,9 +76,11 @@ export class TikTokIntegration {
|
|
|
76
76
|
if (success) {
|
|
77
77
|
this.initialized = true;
|
|
78
78
|
this.log(`TikTok SDK initialized with App ID: ${config.tiktokAppId}`);
|
|
79
|
+
} else {
|
|
80
|
+
console.warn('[Datalyr/TikTok] TikTok SDK not initialized (accessToken may be missing). Events will still be sent server-side via Datalyr postbacks.');
|
|
79
81
|
}
|
|
80
82
|
} catch (error) {
|
|
81
|
-
|
|
83
|
+
console.warn('[Datalyr/TikTok] TikTok SDK init failed. Events will still be sent server-side via Datalyr postbacks.', error);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -90,6 +90,15 @@ interface DatalyrNativeModule {
|
|
|
90
90
|
logoutTikTok(): Promise<boolean>;
|
|
91
91
|
updateTikTokTrackingAuthorization(enabled: boolean): Promise<boolean>;
|
|
92
92
|
|
|
93
|
+
// Advertiser Info (IDFA, IDFV, GAID, ATT Status)
|
|
94
|
+
getAdvertiserInfo(): Promise<{
|
|
95
|
+
idfa?: string;
|
|
96
|
+
idfv?: string;
|
|
97
|
+
gaid?: string;
|
|
98
|
+
att_status: number;
|
|
99
|
+
advertiser_tracking_enabled: boolean;
|
|
100
|
+
} | null>;
|
|
101
|
+
|
|
93
102
|
// Apple Search Ads Methods (iOS only)
|
|
94
103
|
getAppleSearchAdsAttribution(): Promise<AppleSearchAdsAttribution | null>;
|
|
95
104
|
|
|
@@ -374,6 +383,34 @@ export const AppleSearchAdsNativeBridge = {
|
|
|
374
383
|
},
|
|
375
384
|
};
|
|
376
385
|
|
|
386
|
+
// MARK: - Advertiser Info Bridge
|
|
387
|
+
|
|
388
|
+
export interface AdvertiserInfo {
|
|
389
|
+
idfa?: string;
|
|
390
|
+
idfv?: string;
|
|
391
|
+
gaid?: string;
|
|
392
|
+
att_status: number;
|
|
393
|
+
advertiser_tracking_enabled: boolean;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export const AdvertiserInfoBridge = {
|
|
397
|
+
/**
|
|
398
|
+
* Get advertiser info (IDFA, IDFV, ATT status)
|
|
399
|
+
* IDFA is only available when ATT is authorized (iOS 14+)
|
|
400
|
+
* IDFV is always available on iOS
|
|
401
|
+
*/
|
|
402
|
+
async getAdvertiserInfo(): Promise<AdvertiserInfo | null> {
|
|
403
|
+
if (!DatalyrNative) return null;
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
return await DatalyrNative.getAdvertiserInfo();
|
|
407
|
+
} catch (error) {
|
|
408
|
+
console.error('[Datalyr/AdvertiserInfo] Get advertiser info failed:', error);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
377
414
|
// MARK: - Play Install Referrer Bridge (Android only)
|
|
378
415
|
|
|
379
416
|
export const PlayInstallReferrerNativeBridge = {
|
package/src/native/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -105,10 +105,10 @@ export interface DatalyrConfig {
|
|
|
105
105
|
/** Respect browser Do Not Track setting. Default: true */
|
|
106
106
|
respectDoNotTrack?: boolean;
|
|
107
107
|
|
|
108
|
-
/** Enable automatic event tracking (sessions, app lifecycle). Default:
|
|
108
|
+
/** Enable automatic event tracking (sessions, app lifecycle). Default: true */
|
|
109
109
|
enableAutoEvents?: boolean;
|
|
110
110
|
|
|
111
|
-
/** Enable attribution tracking (deep links, install referrer). Default:
|
|
111
|
+
/** Enable attribution tracking (deep links, install referrer). Default: true */
|
|
112
112
|
enableAttribution?: boolean;
|
|
113
113
|
|
|
114
114
|
/** Enable web-to-app attribution matching via email. Default: true */
|
package/src/utils-expo.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
-
import { Platform } from 'react-native';
|
|
2
|
+
import { Platform, Dimensions } from 'react-native';
|
|
3
3
|
import * as Application from 'expo-application';
|
|
4
4
|
import * as Device from 'expo-device';
|
|
5
5
|
import * as Network from 'expo-network';
|
|
@@ -84,10 +84,14 @@ export interface DeviceInfo {
|
|
|
84
84
|
isEmulator: boolean;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
// Cached device info to avoid repeated async calls (matches utils.ts pattern)
|
|
88
|
+
let cachedDeviceInfo: DeviceInfo | null = null;
|
|
89
|
+
let deviceInfoPromise: Promise<DeviceInfo> | null = null;
|
|
90
|
+
|
|
91
|
+
const fetchDeviceInfoInternal = async (): Promise<DeviceInfo> => {
|
|
88
92
|
try {
|
|
89
93
|
const deviceId = await getOrCreateDeviceId();
|
|
90
|
-
|
|
94
|
+
|
|
91
95
|
return {
|
|
92
96
|
deviceId,
|
|
93
97
|
model: Device.modelName || Device.deviceName || 'Unknown',
|
|
@@ -96,10 +100,10 @@ export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
|
96
100
|
appVersion: Application.nativeApplicationVersion || '1.0.0',
|
|
97
101
|
buildNumber: Application.nativeBuildVersion || '1',
|
|
98
102
|
bundleId: Application.applicationId || 'unknown.bundle.id',
|
|
99
|
-
screenWidth:
|
|
100
|
-
screenHeight:
|
|
103
|
+
screenWidth: Dimensions.get('window').width,
|
|
104
|
+
screenHeight: Dimensions.get('window').height,
|
|
101
105
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
102
|
-
locale: 'en-US',
|
|
106
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale || 'en-US',
|
|
103
107
|
carrier: undefined, // Not available in Expo managed workflow
|
|
104
108
|
isEmulator: !Device.isDevice,
|
|
105
109
|
};
|
|
@@ -113,8 +117,8 @@ export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
|
113
117
|
appVersion: '1.0.0',
|
|
114
118
|
buildNumber: '1',
|
|
115
119
|
bundleId: 'unknown.bundle.id',
|
|
116
|
-
screenWidth:
|
|
117
|
-
screenHeight:
|
|
120
|
+
screenWidth: Dimensions.get('window').width,
|
|
121
|
+
screenHeight: Dimensions.get('window').height,
|
|
118
122
|
timezone: 'UTC',
|
|
119
123
|
locale: 'en-US',
|
|
120
124
|
isEmulator: true,
|
|
@@ -122,6 +126,19 @@ export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
|
122
126
|
}
|
|
123
127
|
};
|
|
124
128
|
|
|
129
|
+
export const getDeviceInfo = async (): Promise<DeviceInfo> => {
|
|
130
|
+
if (cachedDeviceInfo) return cachedDeviceInfo;
|
|
131
|
+
if (deviceInfoPromise) return deviceInfoPromise;
|
|
132
|
+
|
|
133
|
+
deviceInfoPromise = fetchDeviceInfoInternal();
|
|
134
|
+
try {
|
|
135
|
+
cachedDeviceInfo = await deviceInfoPromise;
|
|
136
|
+
return cachedDeviceInfo;
|
|
137
|
+
} finally {
|
|
138
|
+
deviceInfoPromise = null;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
125
142
|
// Device ID management
|
|
126
143
|
const getOrCreateDeviceId = async (): Promise<string> => {
|
|
127
144
|
try {
|
|
@@ -246,30 +263,49 @@ export const createFingerprintData = async () => {
|
|
|
246
263
|
// IDFA/GAID collection has been removed for privacy compliance
|
|
247
264
|
// Modern attribution tracking relies on privacy-safe methods:
|
|
248
265
|
|
|
249
|
-
//
|
|
250
|
-
|
|
266
|
+
// Cached network type to avoid per-event native bridge calls
|
|
267
|
+
let cachedNetworkType = 'unknown';
|
|
268
|
+
let networkTypeLastFetched = 0;
|
|
269
|
+
const NETWORK_TYPE_CACHE_MS = 30000; // Refresh every 30s
|
|
270
|
+
|
|
271
|
+
// Network type detection using Expo Network — cached to avoid per-event async calls
|
|
272
|
+
export const getNetworkType = (): string => {
|
|
273
|
+
// Trigger background refresh if stale, but always return cached value synchronously
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
if (now - networkTypeLastFetched > NETWORK_TYPE_CACHE_MS) {
|
|
276
|
+
networkTypeLastFetched = now;
|
|
277
|
+
refreshNetworkType();
|
|
278
|
+
}
|
|
279
|
+
return cachedNetworkType;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const refreshNetworkType = async (): Promise<void> => {
|
|
251
283
|
try {
|
|
252
284
|
const networkState = await Network.getNetworkStateAsync();
|
|
253
|
-
|
|
285
|
+
|
|
254
286
|
if (!networkState.isConnected) {
|
|
255
|
-
|
|
287
|
+
cachedNetworkType = 'none';
|
|
288
|
+
return;
|
|
256
289
|
}
|
|
257
|
-
|
|
290
|
+
|
|
258
291
|
switch (networkState.type) {
|
|
259
292
|
case Network.NetworkStateType.WIFI:
|
|
260
|
-
|
|
293
|
+
cachedNetworkType = 'wifi';
|
|
294
|
+
break;
|
|
261
295
|
case Network.NetworkStateType.CELLULAR:
|
|
262
|
-
|
|
296
|
+
cachedNetworkType = 'cellular';
|
|
297
|
+
break;
|
|
263
298
|
case Network.NetworkStateType.ETHERNET:
|
|
264
|
-
|
|
299
|
+
cachedNetworkType = 'ethernet';
|
|
300
|
+
break;
|
|
265
301
|
case Network.NetworkStateType.BLUETOOTH:
|
|
266
|
-
|
|
302
|
+
cachedNetworkType = 'bluetooth';
|
|
303
|
+
break;
|
|
267
304
|
default:
|
|
268
|
-
|
|
305
|
+
cachedNetworkType = 'unknown';
|
|
269
306
|
}
|
|
270
307
|
} catch (error) {
|
|
271
308
|
debugLog('Error getting network type:', error);
|
|
272
|
-
return 'unknown';
|
|
273
309
|
}
|
|
274
310
|
};
|
|
275
311
|
|