@datalyr/react-native 1.2.1 → 1.3.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/android/build.gradle +54 -0
- package/android/src/main/AndroidManifest.xml +14 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrNativeModule.java +423 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrPackage.java +30 -0
- package/android/src/main/java/com/datalyr/reactnative/DatalyrPlayInstallReferrerModule.java +229 -0
- package/datalyr-react-native.podspec +2 -2
- package/ios/DatalyrSKAdNetwork.m +52 -1
- package/lib/ConversionValueEncoder.d.ts +13 -1
- package/lib/ConversionValueEncoder.js +57 -23
- package/lib/datalyr-sdk.d.ts +25 -2
- package/lib/datalyr-sdk.js +59 -8
- package/lib/index.d.ts +2 -0
- package/lib/index.js +1 -0
- package/lib/integrations/index.d.ts +3 -1
- package/lib/integrations/index.js +2 -1
- package/lib/integrations/meta-integration.d.ts +1 -0
- package/lib/integrations/meta-integration.js +4 -3
- package/lib/integrations/play-install-referrer.d.ts +74 -0
- package/lib/integrations/play-install-referrer.js +156 -0
- package/lib/integrations/tiktok-integration.d.ts +1 -0
- package/lib/integrations/tiktok-integration.js +4 -3
- package/lib/journey.d.ts +106 -0
- package/lib/journey.js +258 -0
- package/lib/native/DatalyrNativeBridge.d.ts +42 -3
- package/lib/native/DatalyrNativeBridge.js +63 -9
- package/lib/native/SKAdNetworkBridge.d.ts +21 -0
- package/lib/native/SKAdNetworkBridge.js +54 -0
- package/package.json +8 -3
- package/src/ConversionValueEncoder.ts +67 -26
- package/src/datalyr-sdk-expo.ts +55 -6
- package/src/datalyr-sdk.ts +72 -13
- package/src/expo.ts +4 -0
- package/src/index.ts +2 -0
- package/src/integrations/index.ts +3 -1
- package/src/integrations/meta-integration.ts +4 -3
- package/src/integrations/play-install-referrer.ts +203 -0
- package/src/integrations/tiktok-integration.ts +4 -3
- package/src/journey.ts +338 -0
- package/src/native/DatalyrNativeBridge.ts +99 -13
- package/src/native/SKAdNetworkBridge.ts +86 -2
|
@@ -46,14 +46,15 @@ export class TikTokIntegration {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Initialize TikTok SDK with configuration
|
|
49
|
+
* Supported on both iOS and Android via native modules
|
|
49
50
|
*/
|
|
50
51
|
async initialize(config: TikTokConfig, debug: boolean = false): Promise<void> {
|
|
51
52
|
this.debug = debug;
|
|
52
53
|
this.config = config;
|
|
53
54
|
|
|
54
|
-
// Only available on iOS via native
|
|
55
|
-
if (Platform.OS !== 'ios') {
|
|
56
|
-
this.log('TikTok SDK only available on iOS');
|
|
55
|
+
// Only available on iOS and Android via native modules
|
|
56
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
57
|
+
this.log('TikTok SDK only available on iOS and Android');
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
60
|
|
package/src/journey.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journey Tracking Module for React Native
|
|
3
|
+
* Mirrors the Web SDK's journey tracking capabilities:
|
|
4
|
+
* - First-touch attribution with 90-day expiration
|
|
5
|
+
* - Last-touch attribution with 90-day expiration
|
|
6
|
+
* - Up to 30 touchpoints stored
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Storage, debugLog, errorLog } from './utils';
|
|
10
|
+
|
|
11
|
+
// Storage keys for journey data
|
|
12
|
+
const JOURNEY_STORAGE_KEYS = {
|
|
13
|
+
FIRST_TOUCH: '@datalyr/first_touch',
|
|
14
|
+
LAST_TOUCH: '@datalyr/last_touch',
|
|
15
|
+
JOURNEY: '@datalyr/journey',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// 90-day attribution window (matching web SDK)
|
|
19
|
+
const ATTRIBUTION_WINDOW_MS = 90 * 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
// Maximum touchpoints to store
|
|
22
|
+
const MAX_TOUCHPOINTS = 30;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Attribution data for a touch
|
|
26
|
+
*/
|
|
27
|
+
export interface TouchAttribution {
|
|
28
|
+
timestamp: number;
|
|
29
|
+
expires_at: number;
|
|
30
|
+
captured_at: number;
|
|
31
|
+
|
|
32
|
+
// Source attribution
|
|
33
|
+
source?: string;
|
|
34
|
+
medium?: string;
|
|
35
|
+
campaign?: string;
|
|
36
|
+
term?: string;
|
|
37
|
+
content?: string;
|
|
38
|
+
|
|
39
|
+
// Click IDs
|
|
40
|
+
clickId?: string;
|
|
41
|
+
clickIdType?: string;
|
|
42
|
+
fbclid?: string;
|
|
43
|
+
gclid?: string;
|
|
44
|
+
ttclid?: string;
|
|
45
|
+
gbraid?: string;
|
|
46
|
+
wbraid?: string;
|
|
47
|
+
|
|
48
|
+
// LYR tag
|
|
49
|
+
lyr?: string;
|
|
50
|
+
|
|
51
|
+
// Context
|
|
52
|
+
landingPage?: string;
|
|
53
|
+
referrer?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A single touchpoint in the customer journey
|
|
58
|
+
*/
|
|
59
|
+
export interface TouchPoint {
|
|
60
|
+
timestamp: number;
|
|
61
|
+
sessionId: string;
|
|
62
|
+
source?: string;
|
|
63
|
+
medium?: string;
|
|
64
|
+
campaign?: string;
|
|
65
|
+
clickIdType?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Journey manager for tracking customer touchpoints
|
|
70
|
+
*/
|
|
71
|
+
export class JourneyManager {
|
|
72
|
+
private firstTouch: TouchAttribution | null = null;
|
|
73
|
+
private lastTouch: TouchAttribution | null = null;
|
|
74
|
+
private journey: TouchPoint[] = [];
|
|
75
|
+
private initialized = false;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Initialize journey tracking by loading persisted data
|
|
79
|
+
*/
|
|
80
|
+
async initialize(): Promise<void> {
|
|
81
|
+
if (this.initialized) return;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
debugLog('Initializing journey manager...');
|
|
85
|
+
|
|
86
|
+
// Load first touch
|
|
87
|
+
const savedFirstTouch = await Storage.getItem<TouchAttribution>(JOURNEY_STORAGE_KEYS.FIRST_TOUCH);
|
|
88
|
+
if (savedFirstTouch && !this.isExpired(savedFirstTouch)) {
|
|
89
|
+
this.firstTouch = savedFirstTouch;
|
|
90
|
+
} else if (savedFirstTouch) {
|
|
91
|
+
// Expired, clear it
|
|
92
|
+
await Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Load last touch
|
|
96
|
+
const savedLastTouch = await Storage.getItem<TouchAttribution>(JOURNEY_STORAGE_KEYS.LAST_TOUCH);
|
|
97
|
+
if (savedLastTouch && !this.isExpired(savedLastTouch)) {
|
|
98
|
+
this.lastTouch = savedLastTouch;
|
|
99
|
+
} else if (savedLastTouch) {
|
|
100
|
+
await Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Load journey
|
|
104
|
+
const savedJourney = await Storage.getItem<TouchPoint[]>(JOURNEY_STORAGE_KEYS.JOURNEY);
|
|
105
|
+
if (savedJourney) {
|
|
106
|
+
this.journey = savedJourney;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.initialized = true;
|
|
110
|
+
debugLog('Journey manager initialized', {
|
|
111
|
+
hasFirstTouch: !!this.firstTouch,
|
|
112
|
+
hasLastTouch: !!this.lastTouch,
|
|
113
|
+
touchpointCount: this.journey.length,
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
errorLog('Failed to initialize journey manager:', error as Error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if attribution has expired
|
|
122
|
+
*/
|
|
123
|
+
private isExpired(attribution: TouchAttribution): boolean {
|
|
124
|
+
return Date.now() >= attribution.expires_at;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Store first touch attribution (only if not already set or expired)
|
|
129
|
+
*/
|
|
130
|
+
async storeFirstTouch(attribution: Partial<TouchAttribution>): Promise<void> {
|
|
131
|
+
try {
|
|
132
|
+
// Only store if no valid first touch exists
|
|
133
|
+
if (this.firstTouch && !this.isExpired(this.firstTouch)) {
|
|
134
|
+
debugLog('First touch already exists, not overwriting');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
this.firstTouch = {
|
|
140
|
+
...attribution,
|
|
141
|
+
timestamp: attribution.timestamp || now,
|
|
142
|
+
captured_at: now,
|
|
143
|
+
expires_at: now + ATTRIBUTION_WINDOW_MS,
|
|
144
|
+
} as TouchAttribution;
|
|
145
|
+
|
|
146
|
+
await Storage.setItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH, this.firstTouch);
|
|
147
|
+
debugLog('First touch stored:', this.firstTouch);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
errorLog('Failed to store first touch:', error as Error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get first touch attribution (null if expired)
|
|
155
|
+
*/
|
|
156
|
+
getFirstTouch(): TouchAttribution | null {
|
|
157
|
+
if (this.firstTouch && this.isExpired(this.firstTouch)) {
|
|
158
|
+
this.firstTouch = null;
|
|
159
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH).catch(() => {});
|
|
160
|
+
}
|
|
161
|
+
return this.firstTouch;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Store last touch attribution (always updates)
|
|
166
|
+
*/
|
|
167
|
+
async storeLastTouch(attribution: Partial<TouchAttribution>): Promise<void> {
|
|
168
|
+
try {
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
this.lastTouch = {
|
|
171
|
+
...attribution,
|
|
172
|
+
timestamp: attribution.timestamp || now,
|
|
173
|
+
captured_at: now,
|
|
174
|
+
expires_at: now + ATTRIBUTION_WINDOW_MS,
|
|
175
|
+
} as TouchAttribution;
|
|
176
|
+
|
|
177
|
+
await Storage.setItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH, this.lastTouch);
|
|
178
|
+
debugLog('Last touch stored:', this.lastTouch);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
errorLog('Failed to store last touch:', error as Error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Get last touch attribution (null if expired)
|
|
186
|
+
*/
|
|
187
|
+
getLastTouch(): TouchAttribution | null {
|
|
188
|
+
if (this.lastTouch && this.isExpired(this.lastTouch)) {
|
|
189
|
+
this.lastTouch = null;
|
|
190
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH).catch(() => {});
|
|
191
|
+
}
|
|
192
|
+
return this.lastTouch;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Add a touchpoint to the customer journey
|
|
197
|
+
*/
|
|
198
|
+
async addTouchpoint(sessionId: string, attribution: Partial<TouchAttribution>): Promise<void> {
|
|
199
|
+
try {
|
|
200
|
+
const touchpoint: TouchPoint = {
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
sessionId,
|
|
203
|
+
source: attribution.source,
|
|
204
|
+
medium: attribution.medium,
|
|
205
|
+
campaign: attribution.campaign,
|
|
206
|
+
clickIdType: attribution.clickIdType,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
this.journey.push(touchpoint);
|
|
210
|
+
|
|
211
|
+
// Keep only last MAX_TOUCHPOINTS
|
|
212
|
+
if (this.journey.length > MAX_TOUCHPOINTS) {
|
|
213
|
+
this.journey = this.journey.slice(-MAX_TOUCHPOINTS);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
await Storage.setItem(JOURNEY_STORAGE_KEYS.JOURNEY, this.journey);
|
|
217
|
+
debugLog('Touchpoint added, total:', this.journey.length);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
errorLog('Failed to add touchpoint:', error as Error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Get customer journey (all touchpoints)
|
|
225
|
+
*/
|
|
226
|
+
getJourney(): TouchPoint[] {
|
|
227
|
+
return [...this.journey];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Record attribution from a deep link or install
|
|
232
|
+
* Updates first-touch (if not set), last-touch, and adds touchpoint
|
|
233
|
+
*/
|
|
234
|
+
async recordAttribution(sessionId: string, attribution: Partial<TouchAttribution>): Promise<void> {
|
|
235
|
+
// Only process if we have meaningful attribution data
|
|
236
|
+
const hasAttribution = attribution.source || attribution.clickId || attribution.campaign || attribution.lyr;
|
|
237
|
+
|
|
238
|
+
if (!hasAttribution) {
|
|
239
|
+
debugLog('No attribution data to record');
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Store first touch if not set
|
|
244
|
+
if (!this.getFirstTouch()) {
|
|
245
|
+
await this.storeFirstTouch(attribution);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Always update last touch
|
|
249
|
+
await this.storeLastTouch(attribution);
|
|
250
|
+
|
|
251
|
+
// Add touchpoint
|
|
252
|
+
await this.addTouchpoint(sessionId, attribution);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get attribution data for events (mirrors Web SDK format)
|
|
257
|
+
*/
|
|
258
|
+
getAttributionData(): Record<string, any> {
|
|
259
|
+
const firstTouch = this.getFirstTouch();
|
|
260
|
+
const lastTouch = this.getLastTouch();
|
|
261
|
+
const journey = this.getJourney();
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
// First touch (with snake_case and camelCase aliases)
|
|
265
|
+
first_touch_source: firstTouch?.source,
|
|
266
|
+
first_touch_medium: firstTouch?.medium,
|
|
267
|
+
first_touch_campaign: firstTouch?.campaign,
|
|
268
|
+
first_touch_timestamp: firstTouch?.timestamp,
|
|
269
|
+
firstTouchSource: firstTouch?.source,
|
|
270
|
+
firstTouchMedium: firstTouch?.medium,
|
|
271
|
+
firstTouchCampaign: firstTouch?.campaign,
|
|
272
|
+
|
|
273
|
+
// Last touch
|
|
274
|
+
last_touch_source: lastTouch?.source,
|
|
275
|
+
last_touch_medium: lastTouch?.medium,
|
|
276
|
+
last_touch_campaign: lastTouch?.campaign,
|
|
277
|
+
last_touch_timestamp: lastTouch?.timestamp,
|
|
278
|
+
lastTouchSource: lastTouch?.source,
|
|
279
|
+
lastTouchMedium: lastTouch?.medium,
|
|
280
|
+
lastTouchCampaign: lastTouch?.campaign,
|
|
281
|
+
|
|
282
|
+
// Journey metrics
|
|
283
|
+
touchpoint_count: journey.length,
|
|
284
|
+
touchpointCount: journey.length,
|
|
285
|
+
days_since_first_touch: firstTouch?.timestamp
|
|
286
|
+
? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
|
|
287
|
+
: 0,
|
|
288
|
+
daysSinceFirstTouch: firstTouch?.timestamp
|
|
289
|
+
? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
|
|
290
|
+
: 0,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Clear all journey data (for testing/reset)
|
|
296
|
+
*/
|
|
297
|
+
async clearJourney(): Promise<void> {
|
|
298
|
+
this.firstTouch = null;
|
|
299
|
+
this.lastTouch = null;
|
|
300
|
+
this.journey = [];
|
|
301
|
+
|
|
302
|
+
await Promise.all([
|
|
303
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.FIRST_TOUCH),
|
|
304
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.LAST_TOUCH),
|
|
305
|
+
Storage.removeItem(JOURNEY_STORAGE_KEYS.JOURNEY),
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
debugLog('Journey data cleared');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Get journey summary for debugging
|
|
313
|
+
*/
|
|
314
|
+
getJourneySummary(): {
|
|
315
|
+
hasFirstTouch: boolean;
|
|
316
|
+
hasLastTouch: boolean;
|
|
317
|
+
touchpointCount: number;
|
|
318
|
+
daysSinceFirstTouch: number;
|
|
319
|
+
sources: string[];
|
|
320
|
+
} {
|
|
321
|
+
const firstTouch = this.getFirstTouch();
|
|
322
|
+
const journey = this.getJourney();
|
|
323
|
+
const sources = [...new Set(journey.map(t => t.source).filter(Boolean))] as string[];
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
hasFirstTouch: !!firstTouch,
|
|
327
|
+
hasLastTouch: !!this.getLastTouch(),
|
|
328
|
+
touchpointCount: journey.length,
|
|
329
|
+
daysSinceFirstTouch: firstTouch?.timestamp
|
|
330
|
+
? Math.floor((Date.now() - firstTouch.timestamp) / 86400000)
|
|
331
|
+
: 0,
|
|
332
|
+
sources,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Export singleton instance
|
|
338
|
+
export const journeyManager = new JourneyManager();
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Native Bridge for Meta, TikTok,
|
|
2
|
+
* Native Bridge for Meta, TikTok, Apple Search Ads, and Play Install Referrer
|
|
3
3
|
* Uses bundled native modules instead of separate npm packages
|
|
4
|
+
*
|
|
5
|
+
* Supported Platforms:
|
|
6
|
+
* - iOS: Meta SDK, TikTok SDK, Apple Search Ads (AdServices)
|
|
7
|
+
* - Android: Meta SDK, TikTok SDK, Play Install Referrer
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
import { NativeModules, Platform } from 'react-native';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
|
-
* Apple Search Ads attribution data returned from AdServices API
|
|
13
|
+
* Apple Search Ads attribution data returned from AdServices API (iOS only)
|
|
10
14
|
*/
|
|
11
15
|
export interface AppleSearchAdsAttribution {
|
|
12
16
|
attribution: boolean;
|
|
@@ -23,6 +27,25 @@ export interface AppleSearchAdsAttribution {
|
|
|
23
27
|
countryOrRegion?: string;
|
|
24
28
|
}
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Play Install Referrer data (Android only)
|
|
32
|
+
*/
|
|
33
|
+
export interface PlayInstallReferrerData {
|
|
34
|
+
referrerUrl: string;
|
|
35
|
+
referrerClickTimestamp: number;
|
|
36
|
+
installBeginTimestamp: number;
|
|
37
|
+
installCompleteTimestamp?: number;
|
|
38
|
+
gclid?: string;
|
|
39
|
+
fbclid?: string;
|
|
40
|
+
ttclid?: string;
|
|
41
|
+
utmSource?: string;
|
|
42
|
+
utmMedium?: string;
|
|
43
|
+
utmCampaign?: string;
|
|
44
|
+
utmTerm?: string;
|
|
45
|
+
utmContent?: string;
|
|
46
|
+
referrer?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
interface DatalyrNativeModule {
|
|
27
50
|
// Meta SDK Methods
|
|
28
51
|
initializeMetaSDK(
|
|
@@ -66,16 +89,29 @@ interface DatalyrNativeModule {
|
|
|
66
89
|
logoutTikTok(): Promise<boolean>;
|
|
67
90
|
updateTikTokTrackingAuthorization(enabled: boolean): Promise<boolean>;
|
|
68
91
|
|
|
69
|
-
// Apple Search Ads Methods
|
|
92
|
+
// Apple Search Ads Methods (iOS only)
|
|
70
93
|
getAppleSearchAdsAttribution(): Promise<AppleSearchAdsAttribution | null>;
|
|
71
94
|
|
|
72
95
|
// SDK Availability
|
|
73
|
-
getSDKAvailability(): Promise<{
|
|
96
|
+
getSDKAvailability(): Promise<{
|
|
97
|
+
meta: boolean;
|
|
98
|
+
tiktok: boolean;
|
|
99
|
+
appleSearchAds: boolean;
|
|
100
|
+
playInstallReferrer?: boolean;
|
|
101
|
+
}>;
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
104
|
+
interface PlayInstallReferrerModule {
|
|
105
|
+
isAvailable(): Promise<boolean>;
|
|
106
|
+
getInstallReferrer(): Promise<PlayInstallReferrerData | null>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Native modules - available on both iOS and Android
|
|
110
|
+
const DatalyrNative: DatalyrNativeModule | null = NativeModules.DatalyrNative ?? null;
|
|
111
|
+
|
|
112
|
+
// Play Install Referrer - Android only
|
|
113
|
+
const DatalyrPlayInstallReferrer: PlayInstallReferrerModule | null =
|
|
114
|
+
Platform.OS === 'android' ? NativeModules.DatalyrPlayInstallReferrer : null;
|
|
79
115
|
|
|
80
116
|
/**
|
|
81
117
|
* Check if native module is available
|
|
@@ -85,21 +121,34 @@ export const isNativeModuleAvailable = (): boolean => {
|
|
|
85
121
|
};
|
|
86
122
|
|
|
87
123
|
/**
|
|
88
|
-
* Get SDK availability status
|
|
124
|
+
* Get SDK availability status for all platforms
|
|
89
125
|
*/
|
|
90
126
|
export const getSDKAvailability = async (): Promise<{
|
|
91
127
|
meta: boolean;
|
|
92
128
|
tiktok: boolean;
|
|
93
129
|
appleSearchAds: boolean;
|
|
130
|
+
playInstallReferrer: boolean;
|
|
94
131
|
}> => {
|
|
132
|
+
const defaultAvailability = {
|
|
133
|
+
meta: false,
|
|
134
|
+
tiktok: false,
|
|
135
|
+
appleSearchAds: false,
|
|
136
|
+
playInstallReferrer: false,
|
|
137
|
+
};
|
|
138
|
+
|
|
95
139
|
if (!DatalyrNative) {
|
|
96
|
-
return
|
|
140
|
+
return defaultAvailability;
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
try {
|
|
100
|
-
|
|
144
|
+
const result = await DatalyrNative.getSDKAvailability();
|
|
145
|
+
return {
|
|
146
|
+
...defaultAvailability,
|
|
147
|
+
...result,
|
|
148
|
+
playInstallReferrer: Platform.OS === 'android' && DatalyrPlayInstallReferrer !== null,
|
|
149
|
+
};
|
|
101
150
|
} catch {
|
|
102
|
-
return
|
|
151
|
+
return defaultAvailability;
|
|
103
152
|
}
|
|
104
153
|
};
|
|
105
154
|
|
|
@@ -292,7 +341,7 @@ export const TikTokNativeBridge = {
|
|
|
292
341
|
},
|
|
293
342
|
};
|
|
294
343
|
|
|
295
|
-
// MARK: - Apple Search Ads Bridge
|
|
344
|
+
// MARK: - Apple Search Ads Bridge (iOS only)
|
|
296
345
|
|
|
297
346
|
export const AppleSearchAdsNativeBridge = {
|
|
298
347
|
/**
|
|
@@ -301,7 +350,7 @@ export const AppleSearchAdsNativeBridge = {
|
|
|
301
350
|
* Returns null if user didn't come from Apple Search Ads or on older iOS
|
|
302
351
|
*/
|
|
303
352
|
async getAttribution(): Promise<AppleSearchAdsAttribution | null> {
|
|
304
|
-
if (!DatalyrNative) return null;
|
|
353
|
+
if (!DatalyrNative || Platform.OS !== 'ios') return null;
|
|
305
354
|
|
|
306
355
|
try {
|
|
307
356
|
return await DatalyrNative.getAppleSearchAdsAttribution();
|
|
@@ -311,3 +360,40 @@ export const AppleSearchAdsNativeBridge = {
|
|
|
311
360
|
}
|
|
312
361
|
},
|
|
313
362
|
};
|
|
363
|
+
|
|
364
|
+
// MARK: - Play Install Referrer Bridge (Android only)
|
|
365
|
+
|
|
366
|
+
export const PlayInstallReferrerNativeBridge = {
|
|
367
|
+
/**
|
|
368
|
+
* Check if Play Install Referrer is available
|
|
369
|
+
* Only available on Android with Google Play Services
|
|
370
|
+
*/
|
|
371
|
+
async isAvailable(): Promise<boolean> {
|
|
372
|
+
if (!DatalyrPlayInstallReferrer || Platform.OS !== 'android') return false;
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
return await DatalyrPlayInstallReferrer.isAvailable();
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Get install referrer data from Google Play
|
|
383
|
+
*
|
|
384
|
+
* Returns UTM parameters, click IDs (gclid, fbclid, ttclid), and timestamps
|
|
385
|
+
* from the Google Play Store referrer.
|
|
386
|
+
*
|
|
387
|
+
* Call this on first app launch to capture install attribution.
|
|
388
|
+
*/
|
|
389
|
+
async getInstallReferrer(): Promise<PlayInstallReferrerData | null> {
|
|
390
|
+
if (!DatalyrPlayInstallReferrer || Platform.OS !== 'android') return null;
|
|
391
|
+
|
|
392
|
+
try {
|
|
393
|
+
return await DatalyrPlayInstallReferrer.getInstallReferrer();
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.error('[Datalyr/PlayInstallReferrer] Get referrer failed:', error);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
};
|
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
import { NativeModules, Platform } from 'react-native';
|
|
2
2
|
|
|
3
|
+
// SKAN 4.0 coarse value type
|
|
4
|
+
export type SKANCoarseValue = 'low' | 'medium' | 'high';
|
|
5
|
+
|
|
6
|
+
// SKAN 4.0 conversion result
|
|
7
|
+
export interface SKANConversionResult {
|
|
8
|
+
fineValue: number; // 0-63
|
|
9
|
+
coarseValue: SKANCoarseValue;
|
|
10
|
+
lockWindow: boolean;
|
|
11
|
+
priority: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
3
14
|
interface SKAdNetworkModule {
|
|
4
15
|
updateConversionValue(value: number): Promise<boolean>;
|
|
16
|
+
updatePostbackConversionValue(
|
|
17
|
+
fineValue: number,
|
|
18
|
+
coarseValue: string,
|
|
19
|
+
lockWindow: boolean
|
|
20
|
+
): Promise<boolean>;
|
|
21
|
+
isSKAN4Available(): Promise<boolean>;
|
|
5
22
|
}
|
|
6
23
|
|
|
7
|
-
const { DatalyrSKAdNetwork } = NativeModules as {
|
|
8
|
-
DatalyrSKAdNetwork?: SKAdNetworkModule
|
|
24
|
+
const { DatalyrSKAdNetwork } = NativeModules as {
|
|
25
|
+
DatalyrSKAdNetwork?: SKAdNetworkModule
|
|
9
26
|
};
|
|
10
27
|
|
|
11
28
|
export class SKAdNetworkBridge {
|
|
29
|
+
private static _isSKAN4Available: boolean | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* SKAN 3.0 - Update conversion value (0-63)
|
|
33
|
+
* @deprecated Use updatePostbackConversionValue for iOS 16.1+
|
|
34
|
+
*/
|
|
12
35
|
static async updateConversionValue(value: number): Promise<boolean> {
|
|
13
36
|
if (Platform.OS !== 'ios') {
|
|
14
37
|
return false; // Android doesn't support SKAdNetwork
|
|
@@ -29,6 +52,67 @@ export class SKAdNetworkBridge {
|
|
|
29
52
|
}
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* SKAN 4.0 - Update postback conversion value with coarse value and lock window
|
|
57
|
+
* Falls back to SKAN 3.0 on iOS 14.0-16.0
|
|
58
|
+
*/
|
|
59
|
+
static async updatePostbackConversionValue(
|
|
60
|
+
result: SKANConversionResult
|
|
61
|
+
): Promise<boolean> {
|
|
62
|
+
if (Platform.OS !== 'ios') {
|
|
63
|
+
return false; // Android doesn't support SKAdNetwork
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!DatalyrSKAdNetwork) {
|
|
67
|
+
console.warn('[Datalyr] SKAdNetwork native module not found. Ensure native bridge is properly configured.');
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const success = await DatalyrSKAdNetwork.updatePostbackConversionValue(
|
|
73
|
+
result.fineValue,
|
|
74
|
+
result.coarseValue,
|
|
75
|
+
result.lockWindow
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const isSKAN4 = await this.isSKAN4Available();
|
|
79
|
+
if (isSKAN4) {
|
|
80
|
+
console.log(`[Datalyr] SKAN 4.0 postback updated: fineValue=${result.fineValue}, coarseValue=${result.coarseValue}, lockWindow=${result.lockWindow}`);
|
|
81
|
+
} else {
|
|
82
|
+
console.log(`[Datalyr] SKAN 3.0 fallback: conversionValue=${result.fineValue}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return success;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.warn('[Datalyr] Failed to update SKAdNetwork postback conversion value:', error);
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if SKAN 4.0 is available (iOS 16.1+)
|
|
94
|
+
*/
|
|
95
|
+
static async isSKAN4Available(): Promise<boolean> {
|
|
96
|
+
if (Platform.OS !== 'ios') {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (this._isSKAN4Available !== null) {
|
|
101
|
+
return this._isSKAN4Available;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!DatalyrSKAdNetwork?.isSKAN4Available) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
this._isSKAN4Available = await DatalyrSKAdNetwork.isSKAN4Available();
|
|
110
|
+
return this._isSKAN4Available;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
32
116
|
static isAvailable(): boolean {
|
|
33
117
|
return Platform.OS === 'ios' && !!DatalyrSKAdNetwork;
|
|
34
118
|
}
|