@advenue/react-native 0.7.0 → 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 +80 -6
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +81 -7
- package/ios/AdvenueIosModule.swift +17 -0
- package/package.json +2 -2
- package/src/aem.ts +33 -0
- package/src/deep-links.ts +11 -2
- package/src/index.ts +123 -3
- package/src/native-types.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();
|
|
@@ -458,6 +475,28 @@ async function resolveAdvertisingId() {
|
|
|
458
475
|
}
|
|
459
476
|
client.setAdvertisingId({});
|
|
460
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
|
+
}
|
|
461
500
|
function collectDeviceInfo(natives) {
|
|
462
501
|
try {
|
|
463
502
|
if (natives.platform === "android" && natives.android?.getDeviceInfo) {
|
|
@@ -486,9 +525,12 @@ var INSTALL_SENT_KEY = "advenue.install_sent";
|
|
|
486
525
|
var installInFlight = false;
|
|
487
526
|
var autoInstallPending = false;
|
|
488
527
|
var autoInstallEnabled = true;
|
|
528
|
+
var manualConsentSet = false;
|
|
529
|
+
var tcfCollectionEnabled = false;
|
|
489
530
|
var configuredAppId = null;
|
|
490
531
|
var configuredApiKey = null;
|
|
491
532
|
var configuredEndpoint = import_sdk_core.DEFAULT_ENDPOINT;
|
|
533
|
+
var aemSeenUrlHashes = /* @__PURE__ */ new Set();
|
|
492
534
|
function active() {
|
|
493
535
|
if (!instance) {
|
|
494
536
|
throw new Error("Advenue.initialize() must be called before tracking events.");
|
|
@@ -525,8 +567,18 @@ function fireAutoInstallIfPending() {
|
|
|
525
567
|
}
|
|
526
568
|
var Advenue = {
|
|
527
569
|
async initialize(config) {
|
|
528
|
-
const {
|
|
570
|
+
const {
|
|
571
|
+
mmkv,
|
|
572
|
+
secureStore: secureMod,
|
|
573
|
+
onDeepLink,
|
|
574
|
+
appId,
|
|
575
|
+
autoTrackInstall,
|
|
576
|
+
tcfDataCollection,
|
|
577
|
+
...rest
|
|
578
|
+
} = config;
|
|
529
579
|
autoInstallPending = false;
|
|
580
|
+
manualConsentSet = false;
|
|
581
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
530
582
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
531
583
|
const platform = resolvePlatform(rest.platform);
|
|
532
584
|
configuredAppId = appId ?? null;
|
|
@@ -552,6 +604,7 @@ var Advenue = {
|
|
|
552
604
|
...osVersion ? { osVersion } : {}
|
|
553
605
|
});
|
|
554
606
|
instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
|
|
607
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
555
608
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
556
609
|
const deviceInfo = collectDeviceInfo({
|
|
557
610
|
platform,
|
|
@@ -568,10 +621,12 @@ var Advenue = {
|
|
|
568
621
|
if (!instance) return;
|
|
569
622
|
if (status === "background") {
|
|
570
623
|
seenBackground = true;
|
|
624
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
571
625
|
applyAppState(instance, status);
|
|
572
626
|
} else if (status === "active") {
|
|
573
627
|
if (!seenBackground) return;
|
|
574
628
|
seenBackground = false;
|
|
629
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
575
630
|
applyAppState(instance, status);
|
|
576
631
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
577
632
|
}
|
|
@@ -582,7 +637,7 @@ var Advenue = {
|
|
|
582
637
|
instance.notifyAppActive();
|
|
583
638
|
}
|
|
584
639
|
}
|
|
585
|
-
|
|
640
|
+
{
|
|
586
641
|
const linking = getLinking();
|
|
587
642
|
if (linking) {
|
|
588
643
|
const client = instance;
|
|
@@ -592,7 +647,19 @@ var Advenue = {
|
|
|
592
647
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
593
648
|
// where available, probabilistic otherwise).
|
|
594
649
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
595
|
-
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
|
+
}
|
|
596
663
|
});
|
|
597
664
|
deepLinkSub.ready.catch((err) => reportError("deepLink.resolve", err));
|
|
598
665
|
}
|
|
@@ -645,12 +712,15 @@ var Advenue = {
|
|
|
645
712
|
}
|
|
646
713
|
},
|
|
647
714
|
setConsentData(consent) {
|
|
715
|
+
manualConsentSet = true;
|
|
648
716
|
active().setConsentData(consent);
|
|
649
717
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
650
718
|
},
|
|
651
719
|
getConsentData: () => active().getConsentData(),
|
|
652
720
|
flush: () => active().flush(),
|
|
653
721
|
getDeviceId: () => active().getDeviceId(),
|
|
722
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
723
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
654
724
|
shutdown() {
|
|
655
725
|
appStateSub?.remove();
|
|
656
726
|
appStateSub = null;
|
|
@@ -662,9 +732,12 @@ var Advenue = {
|
|
|
662
732
|
installInFlight = false;
|
|
663
733
|
autoInstallPending = false;
|
|
664
734
|
autoInstallEnabled = true;
|
|
735
|
+
manualConsentSet = false;
|
|
736
|
+
tcfCollectionEnabled = false;
|
|
665
737
|
configuredAppId = null;
|
|
666
738
|
configuredApiKey = null;
|
|
667
739
|
configuredEndpoint = import_sdk_core.DEFAULT_ENDPOINT;
|
|
740
|
+
aemSeenUrlHashes.clear();
|
|
668
741
|
}
|
|
669
742
|
};
|
|
670
743
|
async function trackInstall() {
|
|
@@ -815,5 +888,6 @@ async function trackInstall() {
|
|
|
815
888
|
getIosNative,
|
|
816
889
|
parseDirectLink,
|
|
817
890
|
resolveAdvertisingId,
|
|
891
|
+
resolveDmaConsent,
|
|
818
892
|
trackInstall
|
|
819
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();
|
|
@@ -454,6 +471,28 @@ async function resolveAdvertisingId() {
|
|
|
454
471
|
}
|
|
455
472
|
client.setAdvertisingId({});
|
|
456
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
|
+
}
|
|
457
496
|
function collectDeviceInfo(natives) {
|
|
458
497
|
try {
|
|
459
498
|
if (natives.platform === "android" && natives.android?.getDeviceInfo) {
|
|
@@ -482,9 +521,12 @@ var INSTALL_SENT_KEY = "advenue.install_sent";
|
|
|
482
521
|
var installInFlight = false;
|
|
483
522
|
var autoInstallPending = false;
|
|
484
523
|
var autoInstallEnabled = true;
|
|
524
|
+
var manualConsentSet = false;
|
|
525
|
+
var tcfCollectionEnabled = false;
|
|
485
526
|
var configuredAppId = null;
|
|
486
527
|
var configuredApiKey = null;
|
|
487
528
|
var configuredEndpoint = DEFAULT_ENDPOINT;
|
|
529
|
+
var aemSeenUrlHashes = /* @__PURE__ */ new Set();
|
|
488
530
|
function active() {
|
|
489
531
|
if (!instance) {
|
|
490
532
|
throw new Error("Advenue.initialize() must be called before tracking events.");
|
|
@@ -521,8 +563,18 @@ function fireAutoInstallIfPending() {
|
|
|
521
563
|
}
|
|
522
564
|
var Advenue = {
|
|
523
565
|
async initialize(config) {
|
|
524
|
-
const {
|
|
566
|
+
const {
|
|
567
|
+
mmkv,
|
|
568
|
+
secureStore: secureMod,
|
|
569
|
+
onDeepLink,
|
|
570
|
+
appId,
|
|
571
|
+
autoTrackInstall,
|
|
572
|
+
tcfDataCollection,
|
|
573
|
+
...rest
|
|
574
|
+
} = config;
|
|
525
575
|
autoInstallPending = false;
|
|
576
|
+
manualConsentSet = false;
|
|
577
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
526
578
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
527
579
|
const platform = resolvePlatform(rest.platform);
|
|
528
580
|
configuredAppId = appId ?? null;
|
|
@@ -548,6 +600,7 @@ var Advenue = {
|
|
|
548
600
|
...osVersion ? { osVersion } : {}
|
|
549
601
|
});
|
|
550
602
|
instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
|
|
603
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
551
604
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
552
605
|
const deviceInfo = collectDeviceInfo({
|
|
553
606
|
platform,
|
|
@@ -564,10 +617,12 @@ var Advenue = {
|
|
|
564
617
|
if (!instance) return;
|
|
565
618
|
if (status === "background") {
|
|
566
619
|
seenBackground = true;
|
|
620
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
567
621
|
applyAppState(instance, status);
|
|
568
622
|
} else if (status === "active") {
|
|
569
623
|
if (!seenBackground) return;
|
|
570
624
|
seenBackground = false;
|
|
625
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
571
626
|
applyAppState(instance, status);
|
|
572
627
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
573
628
|
}
|
|
@@ -578,7 +633,7 @@ var Advenue = {
|
|
|
578
633
|
instance.notifyAppActive();
|
|
579
634
|
}
|
|
580
635
|
}
|
|
581
|
-
|
|
636
|
+
{
|
|
582
637
|
const linking = getLinking();
|
|
583
638
|
if (linking) {
|
|
584
639
|
const client = instance;
|
|
@@ -588,7 +643,19 @@ var Advenue = {
|
|
|
588
643
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
589
644
|
// where available, probabilistic otherwise).
|
|
590
645
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
591
|
-
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
|
+
}
|
|
592
659
|
});
|
|
593
660
|
deepLinkSub.ready.catch((err) => reportError("deepLink.resolve", err));
|
|
594
661
|
}
|
|
@@ -641,12 +708,15 @@ var Advenue = {
|
|
|
641
708
|
}
|
|
642
709
|
},
|
|
643
710
|
setConsentData(consent) {
|
|
711
|
+
manualConsentSet = true;
|
|
644
712
|
active().setConsentData(consent);
|
|
645
713
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
646
714
|
},
|
|
647
715
|
getConsentData: () => active().getConsentData(),
|
|
648
716
|
flush: () => active().flush(),
|
|
649
717
|
getDeviceId: () => active().getDeviceId(),
|
|
718
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
719
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
650
720
|
shutdown() {
|
|
651
721
|
appStateSub?.remove();
|
|
652
722
|
appStateSub = null;
|
|
@@ -658,9 +728,12 @@ var Advenue = {
|
|
|
658
728
|
installInFlight = false;
|
|
659
729
|
autoInstallPending = false;
|
|
660
730
|
autoInstallEnabled = true;
|
|
731
|
+
manualConsentSet = false;
|
|
732
|
+
tcfCollectionEnabled = false;
|
|
661
733
|
configuredAppId = null;
|
|
662
734
|
configuredApiKey = null;
|
|
663
735
|
configuredEndpoint = DEFAULT_ENDPOINT;
|
|
736
|
+
aemSeenUrlHashes.clear();
|
|
664
737
|
}
|
|
665
738
|
};
|
|
666
739
|
async function trackInstall() {
|
|
@@ -810,5 +883,6 @@ export {
|
|
|
810
883
|
getIosNative,
|
|
811
884
|
parseDirectLink,
|
|
812
885
|
resolveAdvertisingId,
|
|
886
|
+
resolveDmaConsent,
|
|
813
887
|
trackInstall
|
|
814
888
|
};
|
|
@@ -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),
|
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 {
|
|
@@ -47,6 +49,7 @@ export type {
|
|
|
47
49
|
AdvenueIosNativeModule,
|
|
48
50
|
InstallReferrerResult,
|
|
49
51
|
MetaInstallReferrerResult,
|
|
52
|
+
NativeTcfData,
|
|
50
53
|
TrackingAuthorizationStatus,
|
|
51
54
|
} from './native-types';
|
|
52
55
|
|
|
@@ -122,6 +125,50 @@ export async function resolveAdvertisingId(): Promise<void> {
|
|
|
122
125
|
client.setAdvertisingId({});
|
|
123
126
|
}
|
|
124
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
|
+
|
|
125
172
|
/**
|
|
126
173
|
* Collects native device metadata (model, locale, screen, cpu, storage, timezone)
|
|
127
174
|
* used to populate Meta CAPI `extinfo`. Reads from the platform-appropriate
|
|
@@ -197,6 +244,14 @@ export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platfo
|
|
|
197
244
|
* install path deterministically.
|
|
198
245
|
*/
|
|
199
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;
|
|
200
255
|
}
|
|
201
256
|
|
|
202
257
|
/**
|
|
@@ -237,6 +292,13 @@ let autoInstallPending = false;
|
|
|
237
292
|
// deferral when an in-flight install is dropped by a mid-flight consent
|
|
238
293
|
// revocation — manual-mode integrators retry trackInstall() themselves.
|
|
239
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;
|
|
240
302
|
// G3.1n: app-scoped id for attestation requestHash. Set from RNAdvenueConfig.appId
|
|
241
303
|
// in initialize(). Null when the integrator has not configured attestation.
|
|
242
304
|
let configuredAppId: string | null = null;
|
|
@@ -251,6 +313,12 @@ let configuredAppId: string | null = null;
|
|
|
251
313
|
// attestation silently never works.
|
|
252
314
|
let configuredApiKey: string | null = null;
|
|
253
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>();
|
|
254
322
|
|
|
255
323
|
function active(): AdvenueClient {
|
|
256
324
|
if (!instance) {
|
|
@@ -314,10 +382,20 @@ function fireAutoInstallIfPending(): void {
|
|
|
314
382
|
*/
|
|
315
383
|
export const Advenue = {
|
|
316
384
|
async initialize(config: RNAdvenueConfig): Promise<AdvenueClient> {
|
|
317
|
-
const {
|
|
385
|
+
const {
|
|
386
|
+
mmkv,
|
|
387
|
+
secureStore: secureMod,
|
|
388
|
+
onDeepLink,
|
|
389
|
+
appId,
|
|
390
|
+
autoTrackInstall,
|
|
391
|
+
tcfDataCollection,
|
|
392
|
+
...rest
|
|
393
|
+
} = config;
|
|
318
394
|
// A deferral armed by a previous initialize() must not leak into this one —
|
|
319
395
|
// a stale pending would override this config's autoTrackInstall opt-out.
|
|
320
396
|
autoInstallPending = false;
|
|
397
|
+
manualConsentSet = false;
|
|
398
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
321
399
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
322
400
|
const platform = resolvePlatform(rest.platform);
|
|
323
401
|
configuredAppId = appId ?? null;
|
|
@@ -353,6 +431,9 @@ export const Advenue = {
|
|
|
353
431
|
// One-line ground truth for "which SDK build talks to which host" — the
|
|
354
432
|
// exact questions a silent integration always raises first.
|
|
355
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();
|
|
356
437
|
// (a) resolve ad-id at init time
|
|
357
438
|
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
358
439
|
// Collect native device info (Meta CAPI extinfo) once at init time; a
|
|
@@ -377,10 +458,21 @@ export const Advenue = {
|
|
|
377
458
|
if (!instance) return;
|
|
378
459
|
if (status === 'background') {
|
|
379
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();
|
|
380
467
|
applyAppState(instance, status);
|
|
381
468
|
} else if (status === 'active') {
|
|
382
469
|
if (!seenBackground) return; // suppress: platform initial 'active' after cold-start
|
|
383
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();
|
|
384
476
|
applyAppState(instance, status);
|
|
385
477
|
// (c) re-resolve ad-id on foreground (after a real background→active transition)
|
|
386
478
|
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
@@ -392,7 +484,12 @@ export const Advenue = {
|
|
|
392
484
|
instance.notifyAppActive(); // non-RN / test env fallback
|
|
393
485
|
}
|
|
394
486
|
}
|
|
395
|
-
|
|
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.
|
|
396
493
|
const linking = getLinking();
|
|
397
494
|
if (linking) {
|
|
398
495
|
const client = instance;
|
|
@@ -402,7 +499,24 @@ export const Advenue = {
|
|
|
402
499
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
403
500
|
// where available, probabilistic otherwise).
|
|
404
501
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
405
|
-
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
|
+
},
|
|
406
520
|
});
|
|
407
521
|
// Cold-start resolution is fire-and-forget: a failure (Linking or the
|
|
408
522
|
// deferred poll rejecting) must surface via diagnostics, never as an
|
|
@@ -469,6 +583,7 @@ export const Advenue = {
|
|
|
469
583
|
}
|
|
470
584
|
},
|
|
471
585
|
setConsentData(consent: Consent): void {
|
|
586
|
+
manualConsentSet = true;
|
|
472
587
|
active().setConsentData(consent);
|
|
473
588
|
// (d) re-resolve ad-id after a consent change
|
|
474
589
|
void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
|
|
@@ -476,6 +591,8 @@ export const Advenue = {
|
|
|
476
591
|
getConsentData: () => active().getConsentData(),
|
|
477
592
|
flush: () => active().flush(),
|
|
478
593
|
getDeviceId: () => active().getDeviceId(),
|
|
594
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
595
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
479
596
|
shutdown() {
|
|
480
597
|
appStateSub?.remove();
|
|
481
598
|
appStateSub = null;
|
|
@@ -487,9 +604,12 @@ export const Advenue = {
|
|
|
487
604
|
installInFlight = false; // reset so re-initialize works correctly
|
|
488
605
|
autoInstallPending = false;
|
|
489
606
|
autoInstallEnabled = true;
|
|
607
|
+
manualConsentSet = false;
|
|
608
|
+
tcfCollectionEnabled = false;
|
|
490
609
|
configuredAppId = null;
|
|
491
610
|
configuredApiKey = null;
|
|
492
611
|
configuredEndpoint = DEFAULT_ENDPOINT;
|
|
612
|
+
aemSeenUrlHashes.clear();
|
|
493
613
|
},
|
|
494
614
|
};
|
|
495
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
|
}
|