@advenue/react-native 0.6.6 → 0.8.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/README.md +5 -0
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +23 -0
- package/dist/index.cjs +101 -7
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +102 -8
- package/ios/AdvenueIos.podspec +4 -0
- package/ios/AdvenueIosModule.swift +17 -0
- package/ios/PrivacyInfo.xcprivacy +56 -0
- package/package.json +2 -2
- package/src/aem.ts +33 -0
- package/src/deep-links.ts +11 -2
- package/src/index.ts +134 -4
- package/src/native-types.ts +23 -0
- package/src/native.ts +23 -0
package/README.md
CHANGED
|
@@ -33,6 +33,11 @@ await Advenue.initialize({
|
|
|
33
33
|
Advenue.track('signup_completed', { plan: 'pro' });
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
> The `adv_` event-name prefix (e.g. `adv_meta_aem`) is reserved for
|
|
37
|
+
> Advenue-internal signals — don't name your own custom events with it. Such
|
|
38
|
+
> events are still accepted by `track()` (the pipeline relies on them) but are
|
|
39
|
+
> hidden from analytics surfaces like the dashboard's funnel event picker.
|
|
40
|
+
|
|
36
41
|
## Deep links (Universal Links / App Links)
|
|
37
42
|
|
|
38
43
|
Add the config plugin so install-time deep links resolve on device:
|
|
@@ -327,6 +327,29 @@ class AdvenueAndroidModule : Module() {
|
|
|
327
327
|
sdkPrefs()?.edit()?.remove(key)?.apply()
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
// IAB TCF v2 data a TCF-compliant CMP (e.g. Google UMP) writes to the
|
|
331
|
+
// *default* SharedPreferences ("<package>_preferences", per the TCF spec) —
|
|
332
|
+
// deliberately not our io.advenue.sdk file. Raw values pass through; the
|
|
333
|
+
// TS side (tcfToConsent) owns the policy mapping.
|
|
334
|
+
Function("getTcfData") { ->
|
|
335
|
+
val ctx = appContext.reactContext?.applicationContext ?: return@Function null
|
|
336
|
+
val prefs = ctx.getSharedPreferences(
|
|
337
|
+
"${ctx.packageName}_preferences", android.content.Context.MODE_PRIVATE
|
|
338
|
+
)
|
|
339
|
+
// prefs.all + map lookup: a missing key reads as null (no default value
|
|
340
|
+
// involved), so "missing" and "0" stay distinguishable. `as? Number`
|
|
341
|
+
// tolerates a CMP writing the value as Int OR Long (SharedPreferences
|
|
342
|
+
// has no fixed numeric type per the TCF spec's own contract) without
|
|
343
|
+
// degrading a Long writer to unknown. runCatching stays as the
|
|
344
|
+
// last-resort crash guard.
|
|
345
|
+
val gdprApplies = runCatching {
|
|
346
|
+
(prefs.all["IABTCF_gdprApplies"] as? Number)?.toInt()
|
|
347
|
+
}.getOrNull()
|
|
348
|
+
val purposeConsents = runCatching { prefs.getString("IABTCF_PurposeConsents", null) }.getOrNull()
|
|
349
|
+
if (gdprApplies == null && purposeConsents == null) return@Function null
|
|
350
|
+
mapOf("gdprApplies" to gdprApplies, "purposeConsents" to purposeConsents)
|
|
351
|
+
}
|
|
352
|
+
|
|
330
353
|
// SSAID (ANDROID_ID): signing-key-scoped, stable across reinstalls, reset
|
|
331
354
|
// only by factory reset. Read by the JS facade ONLY when no usable GAID
|
|
332
355
|
// exists, as a first-party fraud/dedup + reinstall-detection signal (Play
|
package/dist/index.cjs
CHANGED
|
@@ -63,6 +63,7 @@ __export(index_exports, {
|
|
|
63
63
|
getIosNative: () => getIosNative,
|
|
64
64
|
parseDirectLink: () => parseDirectLink,
|
|
65
65
|
resolveAdvertisingId: () => resolveAdvertisingId,
|
|
66
|
+
resolveDmaConsent: () => resolveDmaConsent,
|
|
66
67
|
trackInstall: () => trackInstall
|
|
67
68
|
});
|
|
68
69
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -221,7 +222,7 @@ function sha256Hex(message) {
|
|
|
221
222
|
// package.json
|
|
222
223
|
var package_default = {
|
|
223
224
|
name: "@advenue/react-native",
|
|
224
|
-
version: "0.
|
|
225
|
+
version: "0.8.0",
|
|
225
226
|
description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
226
227
|
type: "module",
|
|
227
228
|
license: "MIT",
|
|
@@ -282,6 +283,17 @@ var package_default = {
|
|
|
282
283
|
"react-native": "./dist/index.cjs"
|
|
283
284
|
};
|
|
284
285
|
|
|
286
|
+
// src/aem.ts
|
|
287
|
+
function extractAemCampaignIds(alApplinkData) {
|
|
288
|
+
try {
|
|
289
|
+
const parsed = JSON.parse(alApplinkData);
|
|
290
|
+
const value = typeof parsed.campaign_ids === "string" && parsed.campaign_ids ? parsed.campaign_ids : typeof parsed.extras?.campaign_ids === "string" && parsed.extras.campaign_ids ? parsed.extras.campaign_ids : null;
|
|
291
|
+
return value && value.length <= 2048 ? value : null;
|
|
292
|
+
} catch {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
285
297
|
// src/deep-links.ts
|
|
286
298
|
function parseDirectLink(url) {
|
|
287
299
|
let deepLinkValue = "";
|
|
@@ -307,11 +319,16 @@ function parseDirectLink(url) {
|
|
|
307
319
|
};
|
|
308
320
|
}
|
|
309
321
|
function setupDeepLinks(setup) {
|
|
310
|
-
const
|
|
322
|
+
const deliverDirect = (url) => {
|
|
323
|
+
const link = parseDirectLink(url);
|
|
324
|
+
setup.onDirectUrl?.(url, link);
|
|
325
|
+
setup.onDeepLink(link);
|
|
326
|
+
};
|
|
327
|
+
const listener = setup.addUrlListener(deliverDirect);
|
|
311
328
|
const ready = (async () => {
|
|
312
329
|
const initial = await setup.getInitialURL();
|
|
313
330
|
if (initial) {
|
|
314
|
-
|
|
331
|
+
deliverDirect(initial);
|
|
315
332
|
return;
|
|
316
333
|
}
|
|
317
334
|
const deferred = await setup.resolveDeferred();
|
|
@@ -357,6 +374,19 @@ function getPlatformOS() {
|
|
|
357
374
|
return null;
|
|
358
375
|
}
|
|
359
376
|
}
|
|
377
|
+
function getOsVersion() {
|
|
378
|
+
try {
|
|
379
|
+
const rn = (0, import_native_require.requireReactNative)();
|
|
380
|
+
if (!rn) return null;
|
|
381
|
+
if (rn.Platform.OS === "android") {
|
|
382
|
+
const release = rn.Platform.constants?.Release;
|
|
383
|
+
return release != null ? String(release) : null;
|
|
384
|
+
}
|
|
385
|
+
return rn.Platform.Version != null ? String(rn.Platform.Version) : null;
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
360
390
|
function getAppState() {
|
|
361
391
|
try {
|
|
362
392
|
const rn = (0, import_native_require.requireReactNative)();
|
|
@@ -445,6 +475,28 @@ async function resolveAdvertisingId() {
|
|
|
445
475
|
}
|
|
446
476
|
client.setAdvertisingId({});
|
|
447
477
|
}
|
|
478
|
+
function consentEquals(a, b) {
|
|
479
|
+
return a.isUserSubjectToGDPR === b.isUserSubjectToGDPR && a.hasConsentForDataUsage === b.hasConsentForDataUsage && a.hasConsentForAdsPersonalization === b.hasConsentForAdsPersonalization && a.hasConsentForAdStorage === b.hasConsentForAdStorage;
|
|
480
|
+
}
|
|
481
|
+
function resolveDmaConsent() {
|
|
482
|
+
if (manualConsentSet) return null;
|
|
483
|
+
const native = getIosNative() ?? getAndroidNative();
|
|
484
|
+
if (!native || typeof native.getTcfData !== "function") return null;
|
|
485
|
+
let consent = null;
|
|
486
|
+
try {
|
|
487
|
+
consent = (0, import_sdk_core.tcfToConsent)(native.getTcfData());
|
|
488
|
+
} catch (err) {
|
|
489
|
+
reportError("consent.tcfRead", err);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
if (!consent) return null;
|
|
493
|
+
const client = active();
|
|
494
|
+
const current = client.getConsentData();
|
|
495
|
+
if (current && consentEquals(current, consent)) return null;
|
|
496
|
+
client.setConsentData(consent);
|
|
497
|
+
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
498
|
+
return consent;
|
|
499
|
+
}
|
|
448
500
|
function collectDeviceInfo(natives) {
|
|
449
501
|
try {
|
|
450
502
|
if (natives.platform === "android" && natives.android?.getDeviceInfo) {
|
|
@@ -473,9 +525,12 @@ var INSTALL_SENT_KEY = "advenue.install_sent";
|
|
|
473
525
|
var installInFlight = false;
|
|
474
526
|
var autoInstallPending = false;
|
|
475
527
|
var autoInstallEnabled = true;
|
|
528
|
+
var manualConsentSet = false;
|
|
529
|
+
var tcfCollectionEnabled = false;
|
|
476
530
|
var configuredAppId = null;
|
|
477
531
|
var configuredApiKey = null;
|
|
478
532
|
var configuredEndpoint = import_sdk_core.DEFAULT_ENDPOINT;
|
|
533
|
+
var aemSeenUrlHashes = /* @__PURE__ */ new Set();
|
|
479
534
|
function active() {
|
|
480
535
|
if (!instance) {
|
|
481
536
|
throw new Error("Advenue.initialize() must be called before tracking events.");
|
|
@@ -512,8 +567,18 @@ function fireAutoInstallIfPending() {
|
|
|
512
567
|
}
|
|
513
568
|
var Advenue = {
|
|
514
569
|
async initialize(config) {
|
|
515
|
-
const {
|
|
570
|
+
const {
|
|
571
|
+
mmkv,
|
|
572
|
+
secureStore: secureMod,
|
|
573
|
+
onDeepLink,
|
|
574
|
+
appId,
|
|
575
|
+
autoTrackInstall,
|
|
576
|
+
tcfDataCollection,
|
|
577
|
+
...rest
|
|
578
|
+
} = config;
|
|
516
579
|
autoInstallPending = false;
|
|
580
|
+
manualConsentSet = false;
|
|
581
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
517
582
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
518
583
|
const platform = resolvePlatform(rest.platform);
|
|
519
584
|
configuredAppId = appId ?? null;
|
|
@@ -530,8 +595,16 @@ var Advenue = {
|
|
|
530
595
|
installFlagStore = storage;
|
|
531
596
|
secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
|
|
532
597
|
const deviceId = rest.deviceId ?? await (0, import_sdk_core.resolveDurableDeviceId)(storage, secureStore ?? void 0);
|
|
533
|
-
|
|
598
|
+
const osVersion = rest.osVersion ?? getOsVersion() ?? void 0;
|
|
599
|
+
instance = new import_sdk_core.AdvenueClient({
|
|
600
|
+
...rest,
|
|
601
|
+
platform,
|
|
602
|
+
deviceId,
|
|
603
|
+
storage,
|
|
604
|
+
...osVersion ? { osVersion } : {}
|
|
605
|
+
});
|
|
534
606
|
instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
|
|
607
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
535
608
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
536
609
|
const deviceInfo = collectDeviceInfo({
|
|
537
610
|
platform,
|
|
@@ -548,10 +621,12 @@ var Advenue = {
|
|
|
548
621
|
if (!instance) return;
|
|
549
622
|
if (status === "background") {
|
|
550
623
|
seenBackground = true;
|
|
624
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
551
625
|
applyAppState(instance, status);
|
|
552
626
|
} else if (status === "active") {
|
|
553
627
|
if (!seenBackground) return;
|
|
554
628
|
seenBackground = false;
|
|
629
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
555
630
|
applyAppState(instance, status);
|
|
556
631
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
557
632
|
}
|
|
@@ -562,7 +637,7 @@ var Advenue = {
|
|
|
562
637
|
instance.notifyAppActive();
|
|
563
638
|
}
|
|
564
639
|
}
|
|
565
|
-
|
|
640
|
+
{
|
|
566
641
|
const linking = getLinking();
|
|
567
642
|
if (linking) {
|
|
568
643
|
const client = instance;
|
|
@@ -572,7 +647,19 @@ var Advenue = {
|
|
|
572
647
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
573
648
|
// where available, probabilistic otherwise).
|
|
574
649
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
575
|
-
onDeepLink
|
|
650
|
+
onDeepLink: onDeepLink ?? (() => {
|
|
651
|
+
}),
|
|
652
|
+
onDirectUrl: (url, link) => {
|
|
653
|
+
const raw = link.params?.al_applink_data;
|
|
654
|
+
if (typeof raw !== "string") return;
|
|
655
|
+
const campaignIds = extractAemCampaignIds(raw);
|
|
656
|
+
if (!campaignIds) return;
|
|
657
|
+
const sourceUrlHash = sha256Hex(url);
|
|
658
|
+
if (aemSeenUrlHashes.has(sourceUrlHash)) return;
|
|
659
|
+
if (client.getRequireConsent() && !client.getTrackingConsent()) return;
|
|
660
|
+
client.track("adv_meta_aem", { campaignIds, sourceUrlHash });
|
|
661
|
+
aemSeenUrlHashes.add(sourceUrlHash);
|
|
662
|
+
}
|
|
576
663
|
});
|
|
577
664
|
deepLinkSub.ready.catch((err) => reportError("deepLink.resolve", err));
|
|
578
665
|
}
|
|
@@ -625,12 +712,15 @@ var Advenue = {
|
|
|
625
712
|
}
|
|
626
713
|
},
|
|
627
714
|
setConsentData(consent) {
|
|
715
|
+
manualConsentSet = true;
|
|
628
716
|
active().setConsentData(consent);
|
|
629
717
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
630
718
|
},
|
|
631
719
|
getConsentData: () => active().getConsentData(),
|
|
632
720
|
flush: () => active().flush(),
|
|
633
721
|
getDeviceId: () => active().getDeviceId(),
|
|
722
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
723
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
634
724
|
shutdown() {
|
|
635
725
|
appStateSub?.remove();
|
|
636
726
|
appStateSub = null;
|
|
@@ -642,9 +732,12 @@ var Advenue = {
|
|
|
642
732
|
installInFlight = false;
|
|
643
733
|
autoInstallPending = false;
|
|
644
734
|
autoInstallEnabled = true;
|
|
735
|
+
manualConsentSet = false;
|
|
736
|
+
tcfCollectionEnabled = false;
|
|
645
737
|
configuredAppId = null;
|
|
646
738
|
configuredApiKey = null;
|
|
647
739
|
configuredEndpoint = import_sdk_core.DEFAULT_ENDPOINT;
|
|
740
|
+
aemSeenUrlHashes.clear();
|
|
648
741
|
}
|
|
649
742
|
};
|
|
650
743
|
async function trackInstall() {
|
|
@@ -795,5 +888,6 @@ async function trackInstall() {
|
|
|
795
888
|
getIosNative,
|
|
796
889
|
parseDirectLink,
|
|
797
890
|
resolveAdvertisingId,
|
|
891
|
+
resolveDmaConsent,
|
|
798
892
|
trackInstall
|
|
799
893
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -55,6 +55,16 @@ interface AdvenueDeviceInfo {
|
|
|
55
55
|
totalStorageGb?: number;
|
|
56
56
|
freeStorageGb?: number;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Raw IAB TCF v2 CMP values from the platform default store
|
|
60
|
+
* (`UserDefaults.standard` / default SharedPreferences) — where TCF-compliant
|
|
61
|
+
* CMPs (e.g. Google UMP) write `IABTCF_*` keys. Structurally mirrors
|
|
62
|
+
* `TcfData` from `@advenue/sdk-core`.
|
|
63
|
+
*/
|
|
64
|
+
interface NativeTcfData {
|
|
65
|
+
gdprApplies?: number | null;
|
|
66
|
+
purposeConsents?: string | null;
|
|
67
|
+
}
|
|
58
68
|
interface AdvenueAndroidNativeModule {
|
|
59
69
|
/** Raw Play Install Referrer; parse with @advenue/install-referrer. */
|
|
60
70
|
getInstallReferrer(): Promise<InstallReferrerResult | null>;
|
|
@@ -112,6 +122,12 @@ interface AdvenueAndroidNativeModule {
|
|
|
112
122
|
* policy). Optional: absent on older native builds; feature-detected.
|
|
113
123
|
*/
|
|
114
124
|
getAndroidId?(): string | null;
|
|
125
|
+
/**
|
|
126
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
127
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
128
|
+
* precedent).
|
|
129
|
+
*/
|
|
130
|
+
getTcfData?(): NativeTcfData | null;
|
|
115
131
|
}
|
|
116
132
|
type TrackingAuthorizationStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
|
|
117
133
|
interface AdvenueIosNativeModule {
|
|
@@ -176,6 +192,12 @@ interface AdvenueIosNativeModule {
|
|
|
176
192
|
secureGetItem?(key: string): string | null;
|
|
177
193
|
secureSetItem?(key: string, value: string): void;
|
|
178
194
|
secureRemoveItem?(key: string): void;
|
|
195
|
+
/**
|
|
196
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
197
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
198
|
+
* precedent).
|
|
199
|
+
*/
|
|
200
|
+
getTcfData?(): NativeTcfData | null;
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
/**
|
|
@@ -219,6 +241,20 @@ declare function getAndroidNative(): AdvenueAndroidNativeModule | null;
|
|
|
219
241
|
* Fire-and-forget at call sites; errors are suppressed internally.
|
|
220
242
|
*/
|
|
221
243
|
declare function resolveAdvertisingId(): Promise<void>;
|
|
244
|
+
/**
|
|
245
|
+
* Reads IAB TCF v2 consent (written by a TCF-compliant CMP, e.g. Google UMP,
|
|
246
|
+
* to the platform default store), maps it to the DMA Consent shape, and
|
|
247
|
+
* pushes it into the active client. No-op — returning null — when consent was
|
|
248
|
+
* set manually this session, the native module or its getter is absent, no
|
|
249
|
+
* TCF data exists, or the freshly-mapped consent is identical to what the
|
|
250
|
+
* client already has (skip-if-unchanged: avoids re-persisting to storage and
|
|
251
|
+
* re-firing the ad-id resolve on every foreground when nothing changed):
|
|
252
|
+
* absent/unchanged consent must stay untouched, never fabricated or re-churned.
|
|
253
|
+
*
|
|
254
|
+
* Must be called after `Advenue.initialize()` — like every facade method, it
|
|
255
|
+
* throws via the uninitialized guard otherwise.
|
|
256
|
+
*/
|
|
257
|
+
declare function resolveDmaConsent(): Consent | null;
|
|
222
258
|
/**
|
|
223
259
|
* Collects native device metadata (model, locale, screen, cpu, storage, timezone)
|
|
224
260
|
* used to populate Meta CAPI `extinfo`. Reads from the platform-appropriate
|
|
@@ -285,6 +321,14 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
|
|
|
285
321
|
* install path deterministically.
|
|
286
322
|
*/
|
|
287
323
|
autoTrackInstall?: boolean;
|
|
324
|
+
/**
|
|
325
|
+
* Auto-collect IAB TCF v2 consent written by a TCF-compliant CMP (e.g.
|
|
326
|
+
* Google UMP) and attach it to every event as DMA consent data (AppsFlyer
|
|
327
|
+
* `enableTCFDataCollection` parity). Read at init and re-read on each
|
|
328
|
+
* foreground, so a CMP decision made after startup lands on later events.
|
|
329
|
+
* A manual `setConsentData()` call always wins. Default false.
|
|
330
|
+
*/
|
|
331
|
+
tcfDataCollection?: boolean;
|
|
288
332
|
}
|
|
289
333
|
/**
|
|
290
334
|
* Maps a React Native AppState status to the client's session lifecycle hooks.
|
|
@@ -317,6 +361,8 @@ declare const Advenue: {
|
|
|
317
361
|
} | null;
|
|
318
362
|
flush: () => Promise<void>;
|
|
319
363
|
getDeviceId: () => string;
|
|
364
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
365
|
+
getAdvenueId: () => string;
|
|
320
366
|
shutdown(): void;
|
|
321
367
|
};
|
|
322
368
|
/**
|
|
@@ -333,4 +379,4 @@ declare const Advenue: {
|
|
|
333
379
|
*/
|
|
334
380
|
declare function trackInstall(): Promise<void>;
|
|
335
381
|
|
|
336
|
-
export { Advenue, type AdvenueAndroidNativeModule, type AdvenueDeviceInfo, type AdvenueIosNativeModule, type InstallReferrerResult, type MMKVLike, type MetaInstallReferrerResult, type RNAdvenueConfig, type SecureStoreLike, type TrackingAuthorizationStatus, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, trackInstall };
|
|
382
|
+
export { Advenue, type AdvenueAndroidNativeModule, type AdvenueDeviceInfo, type AdvenueIosNativeModule, type InstallReferrerResult, type MMKVLike, type MetaInstallReferrerResult, type NativeTcfData, type RNAdvenueConfig, type SecureStoreLike, type TrackingAuthorizationStatus, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveDmaConsent, trackInstall };
|
package/dist/index.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ interface AdvenueDeviceInfo {
|
|
|
55
55
|
totalStorageGb?: number;
|
|
56
56
|
freeStorageGb?: number;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Raw IAB TCF v2 CMP values from the platform default store
|
|
60
|
+
* (`UserDefaults.standard` / default SharedPreferences) — where TCF-compliant
|
|
61
|
+
* CMPs (e.g. Google UMP) write `IABTCF_*` keys. Structurally mirrors
|
|
62
|
+
* `TcfData` from `@advenue/sdk-core`.
|
|
63
|
+
*/
|
|
64
|
+
interface NativeTcfData {
|
|
65
|
+
gdprApplies?: number | null;
|
|
66
|
+
purposeConsents?: string | null;
|
|
67
|
+
}
|
|
58
68
|
interface AdvenueAndroidNativeModule {
|
|
59
69
|
/** Raw Play Install Referrer; parse with @advenue/install-referrer. */
|
|
60
70
|
getInstallReferrer(): Promise<InstallReferrerResult | null>;
|
|
@@ -112,6 +122,12 @@ interface AdvenueAndroidNativeModule {
|
|
|
112
122
|
* policy). Optional: absent on older native builds; feature-detected.
|
|
113
123
|
*/
|
|
114
124
|
getAndroidId?(): string | null;
|
|
125
|
+
/**
|
|
126
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
127
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
128
|
+
* precedent).
|
|
129
|
+
*/
|
|
130
|
+
getTcfData?(): NativeTcfData | null;
|
|
115
131
|
}
|
|
116
132
|
type TrackingAuthorizationStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
|
|
117
133
|
interface AdvenueIosNativeModule {
|
|
@@ -176,6 +192,12 @@ interface AdvenueIosNativeModule {
|
|
|
176
192
|
secureGetItem?(key: string): string | null;
|
|
177
193
|
secureSetItem?(key: string, value: string): void;
|
|
178
194
|
secureRemoveItem?(key: string): void;
|
|
195
|
+
/**
|
|
196
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
197
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
198
|
+
* precedent).
|
|
199
|
+
*/
|
|
200
|
+
getTcfData?(): NativeTcfData | null;
|
|
179
201
|
}
|
|
180
202
|
|
|
181
203
|
/**
|
|
@@ -219,6 +241,20 @@ declare function getAndroidNative(): AdvenueAndroidNativeModule | null;
|
|
|
219
241
|
* Fire-and-forget at call sites; errors are suppressed internally.
|
|
220
242
|
*/
|
|
221
243
|
declare function resolveAdvertisingId(): Promise<void>;
|
|
244
|
+
/**
|
|
245
|
+
* Reads IAB TCF v2 consent (written by a TCF-compliant CMP, e.g. Google UMP,
|
|
246
|
+
* to the platform default store), maps it to the DMA Consent shape, and
|
|
247
|
+
* pushes it into the active client. No-op — returning null — when consent was
|
|
248
|
+
* set manually this session, the native module or its getter is absent, no
|
|
249
|
+
* TCF data exists, or the freshly-mapped consent is identical to what the
|
|
250
|
+
* client already has (skip-if-unchanged: avoids re-persisting to storage and
|
|
251
|
+
* re-firing the ad-id resolve on every foreground when nothing changed):
|
|
252
|
+
* absent/unchanged consent must stay untouched, never fabricated or re-churned.
|
|
253
|
+
*
|
|
254
|
+
* Must be called after `Advenue.initialize()` — like every facade method, it
|
|
255
|
+
* throws via the uninitialized guard otherwise.
|
|
256
|
+
*/
|
|
257
|
+
declare function resolveDmaConsent(): Consent | null;
|
|
222
258
|
/**
|
|
223
259
|
* Collects native device metadata (model, locale, screen, cpu, storage, timezone)
|
|
224
260
|
* used to populate Meta CAPI `extinfo`. Reads from the platform-appropriate
|
|
@@ -285,6 +321,14 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
|
|
|
285
321
|
* install path deterministically.
|
|
286
322
|
*/
|
|
287
323
|
autoTrackInstall?: boolean;
|
|
324
|
+
/**
|
|
325
|
+
* Auto-collect IAB TCF v2 consent written by a TCF-compliant CMP (e.g.
|
|
326
|
+
* Google UMP) and attach it to every event as DMA consent data (AppsFlyer
|
|
327
|
+
* `enableTCFDataCollection` parity). Read at init and re-read on each
|
|
328
|
+
* foreground, so a CMP decision made after startup lands on later events.
|
|
329
|
+
* A manual `setConsentData()` call always wins. Default false.
|
|
330
|
+
*/
|
|
331
|
+
tcfDataCollection?: boolean;
|
|
288
332
|
}
|
|
289
333
|
/**
|
|
290
334
|
* Maps a React Native AppState status to the client's session lifecycle hooks.
|
|
@@ -317,6 +361,8 @@ declare const Advenue: {
|
|
|
317
361
|
} | null;
|
|
318
362
|
flush: () => Promise<void>;
|
|
319
363
|
getDeviceId: () => string;
|
|
364
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
365
|
+
getAdvenueId: () => string;
|
|
320
366
|
shutdown(): void;
|
|
321
367
|
};
|
|
322
368
|
/**
|
|
@@ -333,4 +379,4 @@ declare const Advenue: {
|
|
|
333
379
|
*/
|
|
334
380
|
declare function trackInstall(): Promise<void>;
|
|
335
381
|
|
|
336
|
-
export { Advenue, type AdvenueAndroidNativeModule, type AdvenueDeviceInfo, type AdvenueIosNativeModule, type InstallReferrerResult, type MMKVLike, type MetaInstallReferrerResult, type RNAdvenueConfig, type SecureStoreLike, type TrackingAuthorizationStatus, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, trackInstall };
|
|
382
|
+
export { Advenue, type AdvenueAndroidNativeModule, type AdvenueDeviceInfo, type AdvenueIosNativeModule, type InstallReferrerResult, type MMKVLike, type MetaInstallReferrerResult, type NativeTcfData, type RNAdvenueConfig, type SecureStoreLike, type TrackingAuthorizationStatus, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveDmaConsent, trackInstall };
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,8 @@ import {
|
|
|
62
62
|
DEFAULT_ENDPOINT,
|
|
63
63
|
DEVICE_ID_KEY,
|
|
64
64
|
MemoryStorage,
|
|
65
|
-
resolveDurableDeviceId
|
|
65
|
+
resolveDurableDeviceId,
|
|
66
|
+
tcfToConsent
|
|
66
67
|
} from "@advenue/sdk-core";
|
|
67
68
|
|
|
68
69
|
// ../signing/src/index.ts
|
|
@@ -217,7 +218,7 @@ function sha256Hex(message) {
|
|
|
217
218
|
// package.json
|
|
218
219
|
var package_default = {
|
|
219
220
|
name: "@advenue/react-native",
|
|
220
|
-
version: "0.
|
|
221
|
+
version: "0.8.0",
|
|
221
222
|
description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
222
223
|
type: "module",
|
|
223
224
|
license: "MIT",
|
|
@@ -278,6 +279,17 @@ var package_default = {
|
|
|
278
279
|
"react-native": "./dist/index.cjs"
|
|
279
280
|
};
|
|
280
281
|
|
|
282
|
+
// src/aem.ts
|
|
283
|
+
function extractAemCampaignIds(alApplinkData) {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(alApplinkData);
|
|
286
|
+
const value = typeof parsed.campaign_ids === "string" && parsed.campaign_ids ? parsed.campaign_ids : typeof parsed.extras?.campaign_ids === "string" && parsed.extras.campaign_ids ? parsed.extras.campaign_ids : null;
|
|
287
|
+
return value && value.length <= 2048 ? value : null;
|
|
288
|
+
} catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
// src/deep-links.ts
|
|
282
294
|
function parseDirectLink(url) {
|
|
283
295
|
let deepLinkValue = "";
|
|
@@ -303,11 +315,16 @@ function parseDirectLink(url) {
|
|
|
303
315
|
};
|
|
304
316
|
}
|
|
305
317
|
function setupDeepLinks(setup) {
|
|
306
|
-
const
|
|
318
|
+
const deliverDirect = (url) => {
|
|
319
|
+
const link = parseDirectLink(url);
|
|
320
|
+
setup.onDirectUrl?.(url, link);
|
|
321
|
+
setup.onDeepLink(link);
|
|
322
|
+
};
|
|
323
|
+
const listener = setup.addUrlListener(deliverDirect);
|
|
307
324
|
const ready = (async () => {
|
|
308
325
|
const initial = await setup.getInitialURL();
|
|
309
326
|
if (initial) {
|
|
310
|
-
|
|
327
|
+
deliverDirect(initial);
|
|
311
328
|
return;
|
|
312
329
|
}
|
|
313
330
|
const deferred = await setup.resolveDeferred();
|
|
@@ -353,6 +370,19 @@ function getPlatformOS() {
|
|
|
353
370
|
return null;
|
|
354
371
|
}
|
|
355
372
|
}
|
|
373
|
+
function getOsVersion() {
|
|
374
|
+
try {
|
|
375
|
+
const rn = (0, import_native_require.requireReactNative)();
|
|
376
|
+
if (!rn) return null;
|
|
377
|
+
if (rn.Platform.OS === "android") {
|
|
378
|
+
const release = rn.Platform.constants?.Release;
|
|
379
|
+
return release != null ? String(release) : null;
|
|
380
|
+
}
|
|
381
|
+
return rn.Platform.Version != null ? String(rn.Platform.Version) : null;
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
356
386
|
function getAppState() {
|
|
357
387
|
try {
|
|
358
388
|
const rn = (0, import_native_require.requireReactNative)();
|
|
@@ -441,6 +471,28 @@ async function resolveAdvertisingId() {
|
|
|
441
471
|
}
|
|
442
472
|
client.setAdvertisingId({});
|
|
443
473
|
}
|
|
474
|
+
function consentEquals(a, b) {
|
|
475
|
+
return a.isUserSubjectToGDPR === b.isUserSubjectToGDPR && a.hasConsentForDataUsage === b.hasConsentForDataUsage && a.hasConsentForAdsPersonalization === b.hasConsentForAdsPersonalization && a.hasConsentForAdStorage === b.hasConsentForAdStorage;
|
|
476
|
+
}
|
|
477
|
+
function resolveDmaConsent() {
|
|
478
|
+
if (manualConsentSet) return null;
|
|
479
|
+
const native = getIosNative() ?? getAndroidNative();
|
|
480
|
+
if (!native || typeof native.getTcfData !== "function") return null;
|
|
481
|
+
let consent = null;
|
|
482
|
+
try {
|
|
483
|
+
consent = tcfToConsent(native.getTcfData());
|
|
484
|
+
} catch (err) {
|
|
485
|
+
reportError("consent.tcfRead", err);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
if (!consent) return null;
|
|
489
|
+
const client = active();
|
|
490
|
+
const current = client.getConsentData();
|
|
491
|
+
if (current && consentEquals(current, consent)) return null;
|
|
492
|
+
client.setConsentData(consent);
|
|
493
|
+
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
494
|
+
return consent;
|
|
495
|
+
}
|
|
444
496
|
function collectDeviceInfo(natives) {
|
|
445
497
|
try {
|
|
446
498
|
if (natives.platform === "android" && natives.android?.getDeviceInfo) {
|
|
@@ -469,9 +521,12 @@ var INSTALL_SENT_KEY = "advenue.install_sent";
|
|
|
469
521
|
var installInFlight = false;
|
|
470
522
|
var autoInstallPending = false;
|
|
471
523
|
var autoInstallEnabled = true;
|
|
524
|
+
var manualConsentSet = false;
|
|
525
|
+
var tcfCollectionEnabled = false;
|
|
472
526
|
var configuredAppId = null;
|
|
473
527
|
var configuredApiKey = null;
|
|
474
528
|
var configuredEndpoint = DEFAULT_ENDPOINT;
|
|
529
|
+
var aemSeenUrlHashes = /* @__PURE__ */ new Set();
|
|
475
530
|
function active() {
|
|
476
531
|
if (!instance) {
|
|
477
532
|
throw new Error("Advenue.initialize() must be called before tracking events.");
|
|
@@ -508,8 +563,18 @@ function fireAutoInstallIfPending() {
|
|
|
508
563
|
}
|
|
509
564
|
var Advenue = {
|
|
510
565
|
async initialize(config) {
|
|
511
|
-
const {
|
|
566
|
+
const {
|
|
567
|
+
mmkv,
|
|
568
|
+
secureStore: secureMod,
|
|
569
|
+
onDeepLink,
|
|
570
|
+
appId,
|
|
571
|
+
autoTrackInstall,
|
|
572
|
+
tcfDataCollection,
|
|
573
|
+
...rest
|
|
574
|
+
} = config;
|
|
512
575
|
autoInstallPending = false;
|
|
576
|
+
manualConsentSet = false;
|
|
577
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
513
578
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
514
579
|
const platform = resolvePlatform(rest.platform);
|
|
515
580
|
configuredAppId = appId ?? null;
|
|
@@ -526,8 +591,16 @@ var Advenue = {
|
|
|
526
591
|
installFlagStore = storage;
|
|
527
592
|
secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
|
|
528
593
|
const deviceId = rest.deviceId ?? await resolveDurableDeviceId(storage, secureStore ?? void 0);
|
|
529
|
-
|
|
594
|
+
const osVersion = rest.osVersion ?? getOsVersion() ?? void 0;
|
|
595
|
+
instance = new AdvenueClient({
|
|
596
|
+
...rest,
|
|
597
|
+
platform,
|
|
598
|
+
deviceId,
|
|
599
|
+
storage,
|
|
600
|
+
...osVersion ? { osVersion } : {}
|
|
601
|
+
});
|
|
530
602
|
instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
|
|
603
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
531
604
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
532
605
|
const deviceInfo = collectDeviceInfo({
|
|
533
606
|
platform,
|
|
@@ -544,10 +617,12 @@ var Advenue = {
|
|
|
544
617
|
if (!instance) return;
|
|
545
618
|
if (status === "background") {
|
|
546
619
|
seenBackground = true;
|
|
620
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
547
621
|
applyAppState(instance, status);
|
|
548
622
|
} else if (status === "active") {
|
|
549
623
|
if (!seenBackground) return;
|
|
550
624
|
seenBackground = false;
|
|
625
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
551
626
|
applyAppState(instance, status);
|
|
552
627
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
553
628
|
}
|
|
@@ -558,7 +633,7 @@ var Advenue = {
|
|
|
558
633
|
instance.notifyAppActive();
|
|
559
634
|
}
|
|
560
635
|
}
|
|
561
|
-
|
|
636
|
+
{
|
|
562
637
|
const linking = getLinking();
|
|
563
638
|
if (linking) {
|
|
564
639
|
const client = instance;
|
|
@@ -568,7 +643,19 @@ var Advenue = {
|
|
|
568
643
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
569
644
|
// where available, probabilistic otherwise).
|
|
570
645
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
571
|
-
onDeepLink
|
|
646
|
+
onDeepLink: onDeepLink ?? (() => {
|
|
647
|
+
}),
|
|
648
|
+
onDirectUrl: (url, link) => {
|
|
649
|
+
const raw = link.params?.al_applink_data;
|
|
650
|
+
if (typeof raw !== "string") return;
|
|
651
|
+
const campaignIds = extractAemCampaignIds(raw);
|
|
652
|
+
if (!campaignIds) return;
|
|
653
|
+
const sourceUrlHash = sha256Hex(url);
|
|
654
|
+
if (aemSeenUrlHashes.has(sourceUrlHash)) return;
|
|
655
|
+
if (client.getRequireConsent() && !client.getTrackingConsent()) return;
|
|
656
|
+
client.track("adv_meta_aem", { campaignIds, sourceUrlHash });
|
|
657
|
+
aemSeenUrlHashes.add(sourceUrlHash);
|
|
658
|
+
}
|
|
572
659
|
});
|
|
573
660
|
deepLinkSub.ready.catch((err) => reportError("deepLink.resolve", err));
|
|
574
661
|
}
|
|
@@ -621,12 +708,15 @@ var Advenue = {
|
|
|
621
708
|
}
|
|
622
709
|
},
|
|
623
710
|
setConsentData(consent) {
|
|
711
|
+
manualConsentSet = true;
|
|
624
712
|
active().setConsentData(consent);
|
|
625
713
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
626
714
|
},
|
|
627
715
|
getConsentData: () => active().getConsentData(),
|
|
628
716
|
flush: () => active().flush(),
|
|
629
717
|
getDeviceId: () => active().getDeviceId(),
|
|
718
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
719
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
630
720
|
shutdown() {
|
|
631
721
|
appStateSub?.remove();
|
|
632
722
|
appStateSub = null;
|
|
@@ -638,9 +728,12 @@ var Advenue = {
|
|
|
638
728
|
installInFlight = false;
|
|
639
729
|
autoInstallPending = false;
|
|
640
730
|
autoInstallEnabled = true;
|
|
731
|
+
manualConsentSet = false;
|
|
732
|
+
tcfCollectionEnabled = false;
|
|
641
733
|
configuredAppId = null;
|
|
642
734
|
configuredApiKey = null;
|
|
643
735
|
configuredEndpoint = DEFAULT_ENDPOINT;
|
|
736
|
+
aemSeenUrlHashes.clear();
|
|
644
737
|
}
|
|
645
738
|
};
|
|
646
739
|
async function trackInstall() {
|
|
@@ -790,5 +883,6 @@ export {
|
|
|
790
883
|
getIosNative,
|
|
791
884
|
parseDirectLink,
|
|
792
885
|
resolveAdvertisingId,
|
|
886
|
+
resolveDmaConsent,
|
|
793
887
|
trackInstall
|
|
794
888
|
};
|
package/ios/AdvenueIos.podspec
CHANGED
|
@@ -17,4 +17,8 @@ Pod::Spec.new do |s|
|
|
|
17
17
|
s.dependency 'ExpoModulesCore'
|
|
18
18
|
|
|
19
19
|
s.source_files = '**/*.{h,m,swift}'
|
|
20
|
+
# Apple privacy manifest (required since May 2024 for SDKs that persist
|
|
21
|
+
# state / collect device info) — shipped as a resource bundle so Xcode
|
|
22
|
+
# aggregates it into the app's privacy report.
|
|
23
|
+
s.resource_bundles = { 'AdvenueIos_Privacy' => ['PrivacyInfo.xcprivacy'] }
|
|
20
24
|
end
|
|
@@ -288,6 +288,23 @@ public class AdvenueIosModule: Module {
|
|
|
288
288
|
Self.sdkDefaults.removeObject(forKey: key)
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// MARK: - DMA / TCF consent (read-only)
|
|
292
|
+
|
|
293
|
+
// IAB TCF v2 data a TCF-compliant CMP (e.g. Google UMP) writes to the
|
|
294
|
+
// app's *standard* UserDefaults — deliberately not our io.advenue.sdk
|
|
295
|
+
// suite. Raw values pass through; the TS side (tcfToConsent) owns the
|
|
296
|
+
// policy mapping. UserDefaults is thread-safe — no main-thread hop.
|
|
297
|
+
Function("getTcfData") { () -> [String: Any?]? in
|
|
298
|
+
let std = UserDefaults.standard
|
|
299
|
+
// object(forKey:), not integer(forKey:): a missing key must read as
|
|
300
|
+
// unknown, not 0 ("GDPR does not apply") — a defaulted 0 would strip
|
|
301
|
+
// consent handling from EEA traffic.
|
|
302
|
+
let gdprApplies = std.object(forKey: "IABTCF_gdprApplies") as? Int
|
|
303
|
+
let purposeConsents = std.string(forKey: "IABTCF_PurposeConsents")
|
|
304
|
+
if gdprApplies == nil && purposeConsents == nil { return nil }
|
|
305
|
+
return ["gdprApplies": gdprApplies, "purposeConsents": purposeConsents]
|
|
306
|
+
}
|
|
307
|
+
|
|
291
308
|
// Keychain-backed secure storage for the reinstall-durable identifiers
|
|
292
309
|
// (device id, install guard). kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
|
|
293
310
|
// survives app reinstall, readable after first unlock (background events),
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<!-- The SDK itself does not track (no fingerprinting; IDFA collection is
|
|
6
|
+
app-mediated via the ATT prompt) and calls no tracking domains. -->
|
|
7
|
+
<key>NSPrivacyTracking</key>
|
|
8
|
+
<false/>
|
|
9
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
10
|
+
<array/>
|
|
11
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
12
|
+
<array>
|
|
13
|
+
<!-- Durable SDK device id (Keychain-backed) — attribution/analytics. -->
|
|
14
|
+
<dict>
|
|
15
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
16
|
+
<string>NSPrivacyCollectedDataTypeDeviceID</string>
|
|
17
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
18
|
+
<true/>
|
|
19
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
20
|
+
<false/>
|
|
21
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
22
|
+
<array>
|
|
23
|
+
<string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
|
|
24
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
25
|
+
</array>
|
|
26
|
+
</dict>
|
|
27
|
+
<!-- Coarse device characteristics (model, OS version, locale, screen) —
|
|
28
|
+
event enrichment / attribution corroboration. -->
|
|
29
|
+
<dict>
|
|
30
|
+
<key>NSPrivacyCollectedDataType</key>
|
|
31
|
+
<string>NSPrivacyCollectedDataTypeOtherDataTypes</string>
|
|
32
|
+
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
33
|
+
<true/>
|
|
34
|
+
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
35
|
+
<false/>
|
|
36
|
+
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
37
|
+
<array>
|
|
38
|
+
<string>NSPrivacyCollectedDataTypePurposeAnalytics</string>
|
|
39
|
+
</array>
|
|
40
|
+
</dict>
|
|
41
|
+
</array>
|
|
42
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
43
|
+
<array>
|
|
44
|
+
<!-- CA92.1: UserDefaults is read/written only for this app's own SDK
|
|
45
|
+
state (device id mirror, install flag, consent). -->
|
|
46
|
+
<dict>
|
|
47
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
48
|
+
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
49
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
50
|
+
<array>
|
|
51
|
+
<string>CA92.1</string>
|
|
52
|
+
</array>
|
|
53
|
+
</dict>
|
|
54
|
+
</array>
|
|
55
|
+
</dict>
|
|
56
|
+
</plist>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@advenue/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Advenue MMP React Native SDK — attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@advenue/install-referrer": "0.3.0",
|
|
33
|
-
"@advenue/sdk-core": "0.
|
|
33
|
+
"@advenue/sdk-core": "0.5.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@expo/config-plugins": ">=8",
|
package/src/aem.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meta AEM campaign_ids extraction from an al_applink_data payload.
|
|
3
|
+
*
|
|
4
|
+
* The input string is the ALREADY percent-decoded query-param value
|
|
5
|
+
* (URLSearchParams decodes; decoding again would corrupt blobs containing
|
|
6
|
+
* '%'). Meta does not publicly document the al_applink_data schema for AEM,
|
|
7
|
+
* so we accept the two shapes observed in the wild — top-level
|
|
8
|
+
* `campaign_ids` and `extras.campaign_ids` — top level first. VERIFY against
|
|
9
|
+
* a real captured payload when one is available and update this comment
|
|
10
|
+
* with what production links actually show (spec: implementation-time
|
|
11
|
+
* verification).
|
|
12
|
+
*
|
|
13
|
+
* The blob is opaque, Meta-encrypted, non-user metadata: returned verbatim,
|
|
14
|
+
* never trimmed/re-encoded. ≤ 2048 chars as URL hygiene (the worker
|
|
15
|
+
* re-validates — SDK caps are advisory at the trust boundary).
|
|
16
|
+
*/
|
|
17
|
+
export function extractAemCampaignIds(alApplinkData: string): string | null {
|
|
18
|
+
try {
|
|
19
|
+
const parsed = JSON.parse(alApplinkData) as {
|
|
20
|
+
campaign_ids?: unknown;
|
|
21
|
+
extras?: { campaign_ids?: unknown };
|
|
22
|
+
};
|
|
23
|
+
const value =
|
|
24
|
+
typeof parsed.campaign_ids === 'string' && parsed.campaign_ids
|
|
25
|
+
? parsed.campaign_ids
|
|
26
|
+
: typeof parsed.extras?.campaign_ids === 'string' && parsed.extras.campaign_ids
|
|
27
|
+
? parsed.extras.campaign_ids
|
|
28
|
+
: null;
|
|
29
|
+
return value && value.length <= 2048 ? value : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/deep-links.ts
CHANGED
|
@@ -58,6 +58,10 @@ export interface DeepLinkSetup {
|
|
|
58
58
|
resolveDeferred: () => Promise<DeepLink | null>;
|
|
59
59
|
/** Fired for every resolved deep link (direct or deferred). The app navigates. */
|
|
60
60
|
onDeepLink: (link: DeepLink) => void;
|
|
61
|
+
/** Fired for every DIRECT OS-delivered URL (cold or warm), before the
|
|
62
|
+
* navigation callback. Internal hook (AEM capture); never fires for
|
|
63
|
+
* deferred links, which are our own server's data, not an OS URL. */
|
|
64
|
+
onDirectUrl?: (url: string, link: DeepLink) => void;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/**
|
|
@@ -70,12 +74,17 @@ export interface DeepLinkSetup {
|
|
|
70
74
|
* `deepLinkValue`. Await `handle.ready` to observe completion (used in tests).
|
|
71
75
|
*/
|
|
72
76
|
export function setupDeepLinks(setup: DeepLinkSetup): DeepLinkHandle {
|
|
73
|
-
const
|
|
77
|
+
const deliverDirect = (url: string) => {
|
|
78
|
+
const link = parseDirectLink(url);
|
|
79
|
+
setup.onDirectUrl?.(url, link);
|
|
80
|
+
setup.onDeepLink(link);
|
|
81
|
+
};
|
|
82
|
+
const listener = setup.addUrlListener(deliverDirect);
|
|
74
83
|
|
|
75
84
|
const ready = (async () => {
|
|
76
85
|
const initial = await setup.getInitialURL();
|
|
77
86
|
if (initial) {
|
|
78
|
-
|
|
87
|
+
deliverDirect(initial);
|
|
79
88
|
return;
|
|
80
89
|
}
|
|
81
90
|
const deferred = await setup.resolveDeferred();
|
package/src/index.ts
CHANGED
|
@@ -16,9 +16,11 @@ import {
|
|
|
16
16
|
type StorageAdapter,
|
|
17
17
|
type TrackOptions,
|
|
18
18
|
resolveDurableDeviceId,
|
|
19
|
+
tcfToConsent,
|
|
19
20
|
} from '@advenue/sdk-core';
|
|
20
21
|
import { sha256Hex } from '@advenue/signing';
|
|
21
22
|
import pkg from '../package.json' with { type: 'json' };
|
|
23
|
+
import { extractAemCampaignIds } from './aem';
|
|
22
24
|
import { type DeepLinkHandle, setupDeepLinks } from './deep-links';
|
|
23
25
|
import { type MMKVLike, createMMKVStorage } from './mmkv-storage';
|
|
24
26
|
import {
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
getAppState,
|
|
28
30
|
getIosNative,
|
|
29
31
|
getLinking,
|
|
32
|
+
getOsVersion,
|
|
30
33
|
getPlatformOS,
|
|
31
34
|
} from './native';
|
|
32
35
|
import { createNativeSecureStore, createNativeStorage } from './native-storage';
|
|
@@ -46,6 +49,7 @@ export type {
|
|
|
46
49
|
AdvenueIosNativeModule,
|
|
47
50
|
InstallReferrerResult,
|
|
48
51
|
MetaInstallReferrerResult,
|
|
52
|
+
NativeTcfData,
|
|
49
53
|
TrackingAuthorizationStatus,
|
|
50
54
|
} from './native-types';
|
|
51
55
|
|
|
@@ -121,6 +125,50 @@ export async function resolveAdvertisingId(): Promise<void> {
|
|
|
121
125
|
client.setAdvertisingId({});
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
/** Field-wise equality for the small, flat Consent shape. */
|
|
129
|
+
function consentEquals(a: Consent, b: Consent): boolean {
|
|
130
|
+
return (
|
|
131
|
+
a.isUserSubjectToGDPR === b.isUserSubjectToGDPR &&
|
|
132
|
+
a.hasConsentForDataUsage === b.hasConsentForDataUsage &&
|
|
133
|
+
a.hasConsentForAdsPersonalization === b.hasConsentForAdsPersonalization &&
|
|
134
|
+
a.hasConsentForAdStorage === b.hasConsentForAdStorage
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reads IAB TCF v2 consent (written by a TCF-compliant CMP, e.g. Google UMP,
|
|
140
|
+
* to the platform default store), maps it to the DMA Consent shape, and
|
|
141
|
+
* pushes it into the active client. No-op — returning null — when consent was
|
|
142
|
+
* set manually this session, the native module or its getter is absent, no
|
|
143
|
+
* TCF data exists, or the freshly-mapped consent is identical to what the
|
|
144
|
+
* client already has (skip-if-unchanged: avoids re-persisting to storage and
|
|
145
|
+
* re-firing the ad-id resolve on every foreground when nothing changed):
|
|
146
|
+
* absent/unchanged consent must stay untouched, never fabricated or re-churned.
|
|
147
|
+
*
|
|
148
|
+
* Must be called after `Advenue.initialize()` — like every facade method, it
|
|
149
|
+
* throws via the uninitialized guard otherwise.
|
|
150
|
+
*/
|
|
151
|
+
export function resolveDmaConsent(): Consent | null {
|
|
152
|
+
if (manualConsentSet) return null;
|
|
153
|
+
const native = getIosNative() ?? getAndroidNative();
|
|
154
|
+
if (!native || typeof native.getTcfData !== 'function') return null;
|
|
155
|
+
let consent: Consent | null = null;
|
|
156
|
+
try {
|
|
157
|
+
consent = tcfToConsent(native.getTcfData());
|
|
158
|
+
} catch (err) {
|
|
159
|
+
reportError('consent.tcfRead', err);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (!consent) return null;
|
|
163
|
+
const client = active();
|
|
164
|
+
const current = client.getConsentData();
|
|
165
|
+
if (current && consentEquals(current, consent)) return null;
|
|
166
|
+
client.setConsentData(consent);
|
|
167
|
+
// Consent feeds the ad-id gate — re-resolve, same as manual setConsentData.
|
|
168
|
+
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
169
|
+
return consent;
|
|
170
|
+
}
|
|
171
|
+
|
|
124
172
|
/**
|
|
125
173
|
* Collects native device metadata (model, locale, screen, cpu, storage, timezone)
|
|
126
174
|
* used to populate Meta CAPI `extinfo`. Reads from the platform-appropriate
|
|
@@ -196,6 +244,14 @@ export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platfo
|
|
|
196
244
|
* install path deterministically.
|
|
197
245
|
*/
|
|
198
246
|
autoTrackInstall?: boolean;
|
|
247
|
+
/**
|
|
248
|
+
* Auto-collect IAB TCF v2 consent written by a TCF-compliant CMP (e.g.
|
|
249
|
+
* Google UMP) and attach it to every event as DMA consent data (AppsFlyer
|
|
250
|
+
* `enableTCFDataCollection` parity). Read at init and re-read on each
|
|
251
|
+
* foreground, so a CMP decision made after startup lands on later events.
|
|
252
|
+
* A manual `setConsentData()` call always wins. Default false.
|
|
253
|
+
*/
|
|
254
|
+
tcfDataCollection?: boolean;
|
|
199
255
|
}
|
|
200
256
|
|
|
201
257
|
/**
|
|
@@ -236,6 +292,13 @@ let autoInstallPending = false;
|
|
|
236
292
|
// deferral when an in-flight install is dropped by a mid-flight consent
|
|
237
293
|
// revocation — manual-mode integrators retry trackInstall() themselves.
|
|
238
294
|
let autoInstallEnabled = true;
|
|
295
|
+
// D5: an explicit setConsentData() call disables auto-TCF application for the
|
|
296
|
+
// rest of the session (AppsFlyer semantics: manual wins). Reset by
|
|
297
|
+
// initialize()/shutdown().
|
|
298
|
+
let manualConsentSet = false;
|
|
299
|
+
// Whether the current initialize() opted into TCF auto-collection
|
|
300
|
+
// (RNAdvenueConfig.tcfDataCollection). Reset by initialize()/shutdown().
|
|
301
|
+
let tcfCollectionEnabled = false;
|
|
239
302
|
// G3.1n: app-scoped id for attestation requestHash. Set from RNAdvenueConfig.appId
|
|
240
303
|
// in initialize(). Null when the integrator has not configured attestation.
|
|
241
304
|
let configuredAppId: string | null = null;
|
|
@@ -250,6 +313,12 @@ let configuredAppId: string | null = null;
|
|
|
250
313
|
// attestation silently never works.
|
|
251
314
|
let configuredApiKey: string | null = null;
|
|
252
315
|
let configuredEndpoint = DEFAULT_ENDPOINT;
|
|
316
|
+
// Meta AEM campaign_ids capture: per-process dedup so a URL delivered twice in
|
|
317
|
+
// one open (cold-start + a redundant warm listener re-delivery) enqueues only
|
|
318
|
+
// one adv_meta_aem event. Keyed on the sourceUrlHash (sha256 of the raw URL),
|
|
319
|
+
// not the campaign_ids blob, so two distinct URLs carrying the same blob
|
|
320
|
+
// still each get their own event. Cleared in shutdown().
|
|
321
|
+
const aemSeenUrlHashes = new Set<string>();
|
|
253
322
|
|
|
254
323
|
function active(): AdvenueClient {
|
|
255
324
|
if (!instance) {
|
|
@@ -313,10 +382,20 @@ function fireAutoInstallIfPending(): void {
|
|
|
313
382
|
*/
|
|
314
383
|
export const Advenue = {
|
|
315
384
|
async initialize(config: RNAdvenueConfig): Promise<AdvenueClient> {
|
|
316
|
-
const {
|
|
385
|
+
const {
|
|
386
|
+
mmkv,
|
|
387
|
+
secureStore: secureMod,
|
|
388
|
+
onDeepLink,
|
|
389
|
+
appId,
|
|
390
|
+
autoTrackInstall,
|
|
391
|
+
tcfDataCollection,
|
|
392
|
+
...rest
|
|
393
|
+
} = config;
|
|
317
394
|
// A deferral armed by a previous initialize() must not leak into this one —
|
|
318
395
|
// a stale pending would override this config's autoTrackInstall opt-out.
|
|
319
396
|
autoInstallPending = false;
|
|
397
|
+
manualConsentSet = false;
|
|
398
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
320
399
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
321
400
|
const platform = resolvePlatform(rest.platform);
|
|
322
401
|
configuredAppId = appId ?? null;
|
|
@@ -339,10 +418,22 @@ export const Advenue = {
|
|
|
339
418
|
secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
|
|
340
419
|
const deviceId =
|
|
341
420
|
rest.deviceId ?? (await resolveDurableDeviceId(storage, secureStore ?? undefined));
|
|
342
|
-
|
|
421
|
+
// Auto-collect the OS release version — it feeds the server's normalized
|
|
422
|
+
// probabilistic fingerprint (spec D1); an explicit config value wins.
|
|
423
|
+
const osVersion = rest.osVersion ?? getOsVersion() ?? undefined;
|
|
424
|
+
instance = new AdvenueClient({
|
|
425
|
+
...rest,
|
|
426
|
+
platform,
|
|
427
|
+
deviceId,
|
|
428
|
+
storage,
|
|
429
|
+
...(osVersion ? { osVersion } : {}),
|
|
430
|
+
});
|
|
343
431
|
// One-line ground truth for "which SDK build talks to which host" — the
|
|
344
432
|
// exact questions a silent integration always raises first.
|
|
345
433
|
instance.debugLog(`SDK v${pkg.version} — events -> ${configuredEndpoint}`);
|
|
434
|
+
// TCF auto-collection (opt-in): populate DMA consent before the ad-id
|
|
435
|
+
// resolve below and before any event is enqueued.
|
|
436
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
346
437
|
// (a) resolve ad-id at init time
|
|
347
438
|
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
348
439
|
// Collect native device info (Meta CAPI extinfo) once at init time; a
|
|
@@ -367,10 +458,21 @@ export const Advenue = {
|
|
|
367
458
|
if (!instance) return;
|
|
368
459
|
if (status === 'background') {
|
|
369
460
|
seenBackground = true;
|
|
461
|
+
// The CMP dialog usually completes mid-foreground, so the
|
|
462
|
+
// background flush is the first moment that decision can leave
|
|
463
|
+
// the device — refresh TCF consent BEFORE applyAppState()
|
|
464
|
+
// enqueues session_end, so it ships with the flush rather than
|
|
465
|
+
// one session late.
|
|
466
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
370
467
|
applyAppState(instance, status);
|
|
371
468
|
} else if (status === 'active') {
|
|
372
469
|
if (!seenBackground) return; // suppress: platform initial 'active' after cold-start
|
|
373
470
|
seenBackground = false;
|
|
471
|
+
// CMP dialogs usually complete after init — refresh TCF consent
|
|
472
|
+
// BEFORE applyAppState() enqueues session_start, so that event (and
|
|
473
|
+
// the ad-id re-resolve below) see the up-to-date consent instead of
|
|
474
|
+
// a stale snapshot from before the CMP decision changed.
|
|
475
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
374
476
|
applyAppState(instance, status);
|
|
375
477
|
// (c) re-resolve ad-id on foreground (after a real background→active transition)
|
|
376
478
|
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
@@ -382,7 +484,12 @@ export const Advenue = {
|
|
|
382
484
|
instance.notifyAppActive(); // non-RN / test env fallback
|
|
383
485
|
}
|
|
384
486
|
}
|
|
385
|
-
|
|
487
|
+
{
|
|
488
|
+
// The OS-link listener is set up unconditionally — Meta AEM capture
|
|
489
|
+
// (onDirectUrl below) must run regardless of whether the integrator
|
|
490
|
+
// wants navigation callbacks; the old `if (onDeepLink)` gate silently
|
|
491
|
+
// disabled capture whenever onDeepLink was omitted. `onDeepLink` itself
|
|
492
|
+
// stays optional: a no-op stands in when the integrator didn't pass one.
|
|
386
493
|
const linking = getLinking();
|
|
387
494
|
if (linking) {
|
|
388
495
|
const client = instance;
|
|
@@ -392,7 +499,24 @@ export const Advenue = {
|
|
|
392
499
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
393
500
|
// where available, probabilistic otherwise).
|
|
394
501
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
395
|
-
onDeepLink,
|
|
502
|
+
onDeepLink: onDeepLink ?? (() => {}),
|
|
503
|
+
onDirectUrl: (url, link) => {
|
|
504
|
+
const raw = link.params?.al_applink_data;
|
|
505
|
+
if (typeof raw !== 'string') return;
|
|
506
|
+
const campaignIds = extractAemCampaignIds(raw);
|
|
507
|
+
if (!campaignIds) return;
|
|
508
|
+
const sourceUrlHash = sha256Hex(url);
|
|
509
|
+
// Cold-start + warm-listener can deliver the same URL twice in
|
|
510
|
+
// one open — one marker per distinct URL per process.
|
|
511
|
+
if (aemSeenUrlHashes.has(sourceUrlHash)) return;
|
|
512
|
+
// Consent gate closed: client.track() silently drops the event.
|
|
513
|
+
// Don't burn the dedup slot on a URL that never actually got
|
|
514
|
+
// enqueued — otherwise it could never fire even after consent is
|
|
515
|
+
// later granted. Mirrors the auto-install consent gate check.
|
|
516
|
+
if (client.getRequireConsent() && !client.getTrackingConsent()) return;
|
|
517
|
+
client.track('adv_meta_aem', { campaignIds, sourceUrlHash });
|
|
518
|
+
aemSeenUrlHashes.add(sourceUrlHash);
|
|
519
|
+
},
|
|
396
520
|
});
|
|
397
521
|
// Cold-start resolution is fire-and-forget: a failure (Linking or the
|
|
398
522
|
// deferred poll rejecting) must surface via diagnostics, never as an
|
|
@@ -459,6 +583,7 @@ export const Advenue = {
|
|
|
459
583
|
}
|
|
460
584
|
},
|
|
461
585
|
setConsentData(consent: Consent): void {
|
|
586
|
+
manualConsentSet = true;
|
|
462
587
|
active().setConsentData(consent);
|
|
463
588
|
// (d) re-resolve ad-id after a consent change
|
|
464
589
|
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
@@ -466,6 +591,8 @@ export const Advenue = {
|
|
|
466
591
|
getConsentData: () => active().getConsentData(),
|
|
467
592
|
flush: () => active().flush(),
|
|
468
593
|
getDeviceId: () => active().getDeviceId(),
|
|
594
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
595
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
469
596
|
shutdown() {
|
|
470
597
|
appStateSub?.remove();
|
|
471
598
|
appStateSub = null;
|
|
@@ -477,9 +604,12 @@ export const Advenue = {
|
|
|
477
604
|
installInFlight = false; // reset so re-initialize works correctly
|
|
478
605
|
autoInstallPending = false;
|
|
479
606
|
autoInstallEnabled = true;
|
|
607
|
+
manualConsentSet = false;
|
|
608
|
+
tcfCollectionEnabled = false;
|
|
480
609
|
configuredAppId = null;
|
|
481
610
|
configuredApiKey = null;
|
|
482
611
|
configuredEndpoint = DEFAULT_ENDPOINT;
|
|
612
|
+
aemSeenUrlHashes.clear();
|
|
483
613
|
},
|
|
484
614
|
};
|
|
485
615
|
|
package/src/native-types.ts
CHANGED
|
@@ -47,6 +47,17 @@ export interface AdvenueDeviceInfo {
|
|
|
47
47
|
freeStorageGb?: number;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Raw IAB TCF v2 CMP values from the platform default store
|
|
52
|
+
* (`UserDefaults.standard` / default SharedPreferences) — where TCF-compliant
|
|
53
|
+
* CMPs (e.g. Google UMP) write `IABTCF_*` keys. Structurally mirrors
|
|
54
|
+
* `TcfData` from `@advenue/sdk-core`.
|
|
55
|
+
*/
|
|
56
|
+
export interface NativeTcfData {
|
|
57
|
+
gdprApplies?: number | null;
|
|
58
|
+
purposeConsents?: string | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
export interface AdvenueAndroidNativeModule {
|
|
51
62
|
/** Raw Play Install Referrer; parse with @advenue/install-referrer. */
|
|
52
63
|
getInstallReferrer(): Promise<InstallReferrerResult | null>;
|
|
@@ -101,6 +112,12 @@ export interface AdvenueAndroidNativeModule {
|
|
|
101
112
|
* policy). Optional: absent on older native builds; feature-detected.
|
|
102
113
|
*/
|
|
103
114
|
getAndroidId?(): string | null;
|
|
115
|
+
/**
|
|
116
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
117
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
118
|
+
* precedent).
|
|
119
|
+
*/
|
|
120
|
+
getTcfData?(): NativeTcfData | null;
|
|
104
121
|
}
|
|
105
122
|
|
|
106
123
|
export type TrackingAuthorizationStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
|
|
@@ -164,4 +181,10 @@ export interface AdvenueIosNativeModule {
|
|
|
164
181
|
secureGetItem?(key: string): string | null;
|
|
165
182
|
secureSetItem?(key: string, value: string): void;
|
|
166
183
|
secureRemoveItem?(key: string): void;
|
|
184
|
+
/**
|
|
185
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
186
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
187
|
+
* precedent).
|
|
188
|
+
*/
|
|
189
|
+
getTcfData?(): NativeTcfData | null;
|
|
167
190
|
}
|
package/src/native.ts
CHANGED
|
@@ -44,6 +44,29 @@ export function getPlatformOS(): string | null {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* OS *release* version for attribution matching: iOS `Platform.Version` is the
|
|
49
|
+
* system version string ('18.2'); Android `Platform.Version` is the API LEVEL
|
|
50
|
+
* (34) — sending it would break the server's normalized fingerprint (the
|
|
51
|
+
* click-side browser UA says "Android 14"), so Android reads
|
|
52
|
+
* `Platform.constants.Release` and degrades to null when it's absent.
|
|
53
|
+
*/
|
|
54
|
+
export function getOsVersion(): string | null {
|
|
55
|
+
try {
|
|
56
|
+
const rn = requireReactNative() as {
|
|
57
|
+
Platform: { OS: string; Version?: unknown; constants?: { Release?: unknown } };
|
|
58
|
+
} | null;
|
|
59
|
+
if (!rn) return null;
|
|
60
|
+
if (rn.Platform.OS === 'android') {
|
|
61
|
+
const release = rn.Platform.constants?.Release;
|
|
62
|
+
return release != null ? String(release) : null;
|
|
63
|
+
}
|
|
64
|
+
return rn.Platform.Version != null ? String(rn.Platform.Version) : null;
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
/** Subscription handle returned by AppState.addEventListener. */
|
|
48
71
|
export interface AppStateSubscription {
|
|
49
72
|
remove(): void;
|