@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 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.7.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 listener = setup.addUrlListener((url) => setup.onDeepLink(parseDirectLink(url)));
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
- setup.onDeepLink(parseDirectLink(initial));
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 { mmkv, secureStore: secureMod, onDeepLink, appId, autoTrackInstall, ...rest } = config;
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
- if (onDeepLink) {
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.7.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 listener = setup.addUrlListener((url) => setup.onDeepLink(parseDirectLink(url)));
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
- setup.onDeepLink(parseDirectLink(initial));
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 { mmkv, secureStore: secureMod, onDeepLink, appId, autoTrackInstall, ...rest } = config;
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
- if (onDeepLink) {
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.7.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.4.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 listener = setup.addUrlListener((url) => setup.onDeepLink(parseDirectLink(url)));
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
- setup.onDeepLink(parseDirectLink(initial));
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 { mmkv, secureStore: secureMod, onDeepLink, appId, autoTrackInstall, ...rest } = config;
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
- if (onDeepLink) {
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
 
@@ -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
  }