@advenue/react-native 0.7.0 → 0.9.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/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
@@ -131,11 +178,18 @@ export async function resolveAdvertisingId(): Promise<void> {
131
178
  * native-side error degrades to an empty object so callers can unconditionally
132
179
  * pass the result through.
133
180
  */
134
- export function collectDeviceInfo(natives: {
181
+ type NativeDeviceInfo = Partial<DeviceInfo> & { shortVersion?: string };
182
+
183
+ type DeviceInfoNatives = {
135
184
  platform: 'ios' | 'android' | string;
136
- ios?: { getDeviceInfo?: () => Partial<DeviceInfo> } | null;
137
- android?: { getDeviceInfo?: () => Partial<DeviceInfo> } | null;
138
- }): DeviceInfo {
185
+ ios?: { getDeviceInfo?: () => NativeDeviceInfo } | null;
186
+ android?: { getDeviceInfo?: () => NativeDeviceInfo } | null;
187
+ };
188
+
189
+ /** The raw native map, `shortVersion` included. Internal: `initialize` needs
190
+ * that field for the event's top-level `appVersion`, everyone else wants the
191
+ * deviceInfo blob that `collectDeviceInfo` returns. */
192
+ function readNativeDeviceInfo(natives: DeviceInfoNatives): NativeDeviceInfo {
139
193
  try {
140
194
  if (natives.platform === 'android' && natives.android?.getDeviceInfo) {
141
195
  return natives.android.getDeviceInfo();
@@ -149,6 +203,13 @@ export function collectDeviceInfo(natives: {
149
203
  return {};
150
204
  }
151
205
 
206
+ export function collectDeviceInfo(natives: DeviceInfoNatives): DeviceInfo {
207
+ // shortVersion is the app's own version, carried by the event's `appVersion`
208
+ // field (extinfo slot 2) — it is not part of the persisted deviceInfo blob.
209
+ const { shortVersion: _shortVersion, ...info } = readNativeDeviceInfo(natives);
210
+ return info;
211
+ }
212
+
152
213
  export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
153
214
  /**
154
215
  * Auto-detected from React Native's `Platform.OS` — omit it. Pass explicitly
@@ -163,18 +224,6 @@ export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platfo
163
224
  * supply storage. When set, it replaces the built-in native storage.
164
225
  */
165
226
  mmkv?: MMKVLike;
166
- /**
167
- * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
168
- *
169
- * Your app's Advenue appId (the UUID in Settings → API Keys). When set on
170
- * Android, the automatic install obtains a Play Integrity Standard token and
171
- * attaches it to the install event. The requestHash is computed as
172
- * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
173
- * formula exactly (see `makeDecodeAndroid` in the worker).
174
- *
175
- * Omit to run without attestation (the server field stays `unavailable`).
176
- */
177
- appId?: string;
178
227
  /**
179
228
  * @internal Secure-store override (tests / custom embeddings). The SDK uses
180
229
  * its own Keychain-backed native storage on iOS for the reinstall-resilient
@@ -197,6 +246,14 @@ export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platfo
197
246
  * install path deterministically.
198
247
  */
199
248
  autoTrackInstall?: boolean;
249
+ /**
250
+ * Auto-collect IAB TCF v2 consent written by a TCF-compliant CMP (e.g.
251
+ * Google UMP) and attach it to every event as DMA consent data (AppsFlyer
252
+ * `enableTCFDataCollection` parity). Read at init and re-read on each
253
+ * foreground, so a CMP decision made after startup lands on later events.
254
+ * A manual `setConsentData()` call always wins. Default false.
255
+ */
256
+ tcfDataCollection?: boolean;
200
257
  }
201
258
 
202
259
  /**
@@ -237,9 +294,17 @@ let autoInstallPending = false;
237
294
  // deferral when an in-flight install is dropped by a mid-flight consent
238
295
  // revocation — manual-mode integrators retry trackInstall() themselves.
239
296
  let autoInstallEnabled = true;
240
- // G3.1n: app-scoped id for attestation requestHash. Set from RNAdvenueConfig.appId
241
- // in initialize(). Null when the integrator has not configured attestation.
242
- let configuredAppId: string | null = null;
297
+ // D5: an explicit setConsentData() call disables auto-TCF application for the
298
+ // rest of the session (AppsFlyer semantics: manual wins). Reset by
299
+ // initialize()/shutdown().
300
+ let manualConsentSet = false;
301
+ // Whether the current initialize() opted into TCF auto-collection
302
+ // (RNAdvenueConfig.tcfDataCollection). Reset by initialize()/shutdown().
303
+ let tcfCollectionEnabled = false;
304
+ /** Deadline for the App Attest challenge fetch. Enrichment on the install path:
305
+ * bounded well under the transport's 15s so a slow host costs the install a
306
+ * moment, never the platform socket timeout. */
307
+ const ATTEST_CHALLENGE_TIMEOUT_MS = 5_000;
243
308
  // G3.2n: API key + endpoint needed to fetch the App Attest challenge from the
244
309
  // server. Stored at module level so trackInstall() can issue the challenge
245
310
  // request without reaching into the private AdvenueClient fields.
@@ -251,6 +316,12 @@ let configuredAppId: string | null = null;
251
316
  // attestation silently never works.
252
317
  let configuredApiKey: string | null = null;
253
318
  let configuredEndpoint = DEFAULT_ENDPOINT;
319
+ // Meta AEM campaign_ids capture: per-process dedup so a URL delivered twice in
320
+ // one open (cold-start + a redundant warm listener re-delivery) enqueues only
321
+ // one adv_meta_aem event. Keyed on the sourceUrlHash (sha256 of the raw URL),
322
+ // not the campaign_ids blob, so two distinct URLs carrying the same blob
323
+ // still each get their own event. Cleared in shutdown().
324
+ const aemSeenUrlHashes = new Set<string>();
254
325
 
255
326
  function active(): AdvenueClient {
256
327
  if (!instance) {
@@ -314,13 +385,21 @@ function fireAutoInstallIfPending(): void {
314
385
  */
315
386
  export const Advenue = {
316
387
  async initialize(config: RNAdvenueConfig): Promise<AdvenueClient> {
317
- const { mmkv, secureStore: secureMod, onDeepLink, appId, autoTrackInstall, ...rest } = config;
388
+ const {
389
+ mmkv,
390
+ secureStore: secureMod,
391
+ onDeepLink,
392
+ autoTrackInstall,
393
+ tcfDataCollection,
394
+ ...rest
395
+ } = config;
318
396
  // A deferral armed by a previous initialize() must not leak into this one —
319
397
  // a stale pending would override this config's autoTrackInstall opt-out.
320
398
  autoInstallPending = false;
399
+ manualConsentSet = false;
400
+ tcfCollectionEnabled = tcfDataCollection === true;
321
401
  autoInstallEnabled = autoTrackInstall !== false;
322
402
  const platform = resolvePlatform(rest.platform);
323
- configuredAppId = appId ?? null;
324
403
  configuredApiKey = config.apiKey;
325
404
  configuredEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
326
405
  appStateSub?.remove();
@@ -343,25 +422,38 @@ export const Advenue = {
343
422
  // Auto-collect the OS release version — it feeds the server's normalized
344
423
  // probabilistic fingerprint (spec D1); an explicit config value wins.
345
424
  const osVersion = rest.osVersion ?? getOsVersion() ?? undefined;
425
+ // Native device metadata (Meta CAPI extinfo) — read BEFORE the client is
426
+ // constructed because the app's short version becomes the event's
427
+ // `appVersion`, which the client takes at construction time. Best-effort
428
+ // and synchronous, so no fire-and-forget needed.
429
+ const nativeDeviceInfo = readNativeDeviceInfo({
430
+ platform,
431
+ ios: getIosNative(),
432
+ android: getAndroidNative(),
433
+ });
434
+ const { shortVersion, ...deviceInfo } = nativeDeviceInfo;
435
+ // Same contract as osVersion: the config wins, the platform fills the gap.
436
+ // Without this fallback nothing ever set it — every event, and every
437
+ // device_profiles row, recorded an empty app version.
438
+ const appVersion = rest.appVersion ?? shortVersion ?? undefined;
346
439
  instance = new AdvenueClient({
347
440
  ...rest,
348
441
  platform,
349
442
  deviceId,
350
443
  storage,
351
444
  ...(osVersion ? { osVersion } : {}),
445
+ ...(appVersion ? { appVersion } : {}),
446
+ // Stamped by the SDK, never by the host app — see AdvenueConfig.sdkVersion.
447
+ sdkVersion: pkg.version,
352
448
  });
353
449
  // One-line ground truth for "which SDK build talks to which host" — the
354
450
  // exact questions a silent integration always raises first.
355
451
  instance.debugLog(`SDK v${pkg.version} — events -> ${configuredEndpoint}`);
452
+ // TCF auto-collection (opt-in): populate DMA consent before the ad-id
453
+ // resolve below and before any event is enqueued.
454
+ if (tcfCollectionEnabled) resolveDmaConsent();
356
455
  // (a) resolve ad-id at init time
357
456
  void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
358
- // Collect native device info (Meta CAPI extinfo) once at init time; a
359
- // best-effort, synchronous read, so no fire-and-forget needed.
360
- const deviceInfo = collectDeviceInfo({
361
- platform,
362
- ios: getIosNative(),
363
- android: getAndroidNative(),
364
- });
365
457
  if (Object.keys(deviceInfo).length > 0) instance.setDeviceInfo(deviceInfo);
366
458
  if (!config.disableAutoSessions) {
367
459
  const appState = getAppState();
@@ -377,10 +469,21 @@ export const Advenue = {
377
469
  if (!instance) return;
378
470
  if (status === 'background') {
379
471
  seenBackground = true;
472
+ // The CMP dialog usually completes mid-foreground, so the
473
+ // background flush is the first moment that decision can leave
474
+ // the device — refresh TCF consent BEFORE applyAppState()
475
+ // enqueues session_end, so it ships with the flush rather than
476
+ // one session late.
477
+ if (tcfCollectionEnabled) resolveDmaConsent();
380
478
  applyAppState(instance, status);
381
479
  } else if (status === 'active') {
382
480
  if (!seenBackground) return; // suppress: platform initial 'active' after cold-start
383
481
  seenBackground = false;
482
+ // CMP dialogs usually complete after init — refresh TCF consent
483
+ // BEFORE applyAppState() enqueues session_start, so that event (and
484
+ // the ad-id re-resolve below) see the up-to-date consent instead of
485
+ // a stale snapshot from before the CMP decision changed.
486
+ if (tcfCollectionEnabled) resolveDmaConsent();
384
487
  applyAppState(instance, status);
385
488
  // (c) re-resolve ad-id on foreground (after a real background→active transition)
386
489
  void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
@@ -392,7 +495,12 @@ export const Advenue = {
392
495
  instance.notifyAppActive(); // non-RN / test env fallback
393
496
  }
394
497
  }
395
- if (onDeepLink) {
498
+ {
499
+ // The OS-link listener is set up unconditionally — Meta AEM capture
500
+ // (onDirectUrl below) must run regardless of whether the integrator
501
+ // wants navigation callbacks; the old `if (onDeepLink)` gate silently
502
+ // disabled capture whenever onDeepLink was omitted. `onDeepLink` itself
503
+ // stays optional: a no-op stands in when the integrator didn't pass one.
396
504
  const linking = getLinking();
397
505
  if (linking) {
398
506
  const client = instance;
@@ -402,7 +510,24 @@ export const Advenue = {
402
510
  // Deferred deep link: server-side conversion poll (deterministic ids
403
511
  // where available, probabilistic otherwise).
404
512
  resolveDeferred: () => client.resolveDeferredDeepLink(),
405
- onDeepLink,
513
+ onDeepLink: onDeepLink ?? (() => {}),
514
+ onDirectUrl: (url, link) => {
515
+ const raw = link.params?.al_applink_data;
516
+ if (typeof raw !== 'string') return;
517
+ const campaignIds = extractAemCampaignIds(raw);
518
+ if (!campaignIds) return;
519
+ const sourceUrlHash = sha256Hex(url);
520
+ // Cold-start + warm-listener can deliver the same URL twice in
521
+ // one open — one marker per distinct URL per process.
522
+ if (aemSeenUrlHashes.has(sourceUrlHash)) return;
523
+ // Consent gate closed: client.track() silently drops the event.
524
+ // Don't burn the dedup slot on a URL that never actually got
525
+ // enqueued — otherwise it could never fire even after consent is
526
+ // later granted. Mirrors the auto-install consent gate check.
527
+ if (client.getRequireConsent() && !client.getTrackingConsent()) return;
528
+ client.track('adv_meta_aem', { campaignIds, sourceUrlHash });
529
+ aemSeenUrlHashes.add(sourceUrlHash);
530
+ },
406
531
  });
407
532
  // Cold-start resolution is fire-and-forget: a failure (Linking or the
408
533
  // deferred poll rejecting) must surface via diagnostics, never as an
@@ -469,6 +594,7 @@ export const Advenue = {
469
594
  }
470
595
  },
471
596
  setConsentData(consent: Consent): void {
597
+ manualConsentSet = true;
472
598
  active().setConsentData(consent);
473
599
  // (d) re-resolve ad-id after a consent change
474
600
  void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
@@ -476,6 +602,11 @@ export const Advenue = {
476
602
  getConsentData: () => active().getConsentData(),
477
603
  flush: () => active().flush(),
478
604
  getDeviceId: () => active().getDeviceId(),
605
+ /** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
606
+ getAdvenueId: () => active().getAdvenueId(),
607
+ /** Which app the server resolved this API key to; null until a batch lands
608
+ * (see AdvenueClient.getResolvedAppId). */
609
+ getResolvedAppId: () => active().getResolvedAppId(),
479
610
  shutdown() {
480
611
  appStateSub?.remove();
481
612
  appStateSub = null;
@@ -487,9 +618,11 @@ export const Advenue = {
487
618
  installInFlight = false; // reset so re-initialize works correctly
488
619
  autoInstallPending = false;
489
620
  autoInstallEnabled = true;
490
- configuredAppId = null;
621
+ manualConsentSet = false;
622
+ tcfCollectionEnabled = false;
491
623
  configuredApiKey = null;
492
624
  configuredEndpoint = DEFAULT_ENDPOINT;
625
+ aemSeenUrlHashes.clear();
493
626
  },
494
627
  };
495
628
 
@@ -615,18 +748,20 @@ export async function trackInstall(): Promise<void> {
615
748
  // leaves attestation absent; the install is never blocked.
616
749
  const trackOpts: TrackOptions = { type: 'install', ...opts };
617
750
  if (
618
- configuredAppId &&
619
751
  android &&
620
752
  typeof (android as { getIntegrityToken?: unknown }).getIntegrityToken === 'function'
621
753
  ) {
622
754
  try {
623
- // requestHash = sha256_hex("<appId>:<deviceId>") — must match the server's
755
+ // requestHash = sha256_hex("<deviceId>") — must match the server's
624
756
  // mapIntegrityVerdict computation byte-for-byte (see makeDecodeAndroid in
625
757
  // apps/worker/src/attestation-decoders.ts):
626
- // expectedRequestHash = sha256(`${event.appId}:${event.deviceId}`)
627
- // configuredAppId must be the app's UUID from the Advenue dashboard.
758
+ // expectedRequestHash = sha256(event.deviceId)
759
+ // No app id is hashed in, deliberately: the token is already decoded
760
+ // against the app's Play package name and must be PLAY_RECOGNIZED, which
761
+ // binds the app more tightly than an internal UUID could — and it keeps
762
+ // the API key as the SDK's only app identity.
628
763
  const deviceId = client.getDeviceId();
629
- const requestHash = sha256Hex(`${configuredAppId}:${deviceId}`);
764
+ const requestHash = sha256Hex(deviceId);
630
765
  const token = await (
631
766
  android as { getIntegrityToken(h: string): Promise<string> }
632
767
  ).getIntegrityToken(requestHash);
@@ -643,19 +778,32 @@ export async function trackInstall(): Promise<void> {
643
778
  // G3.2n — App Attest attestation (iOS only). Best-effort: any error (no
644
779
  // support on simulator, challenge fetch failure, attest failure) leaves
645
780
  // attestation absent; the install is NEVER blocked.
646
- if (
647
- configuredAppId &&
648
- configuredApiKey &&
649
- ios &&
650
- typeof (ios as { attestKey?: unknown }).attestKey === 'function'
651
- ) {
781
+ if (configuredApiKey && ios && typeof (ios as { attestKey?: unknown }).attestKey === 'function') {
652
782
  try {
653
783
  // Step 1: fetch a one-time server challenge bound to this device.
784
+ //
785
+ // Deadline-bounded, like every other SDK fetch (see sdk-core transport):
786
+ // the install event is only enqueued after this await, and attestation is
787
+ // attempted on every install — so an unreachable or hung host would
788
+ // otherwise delay the install by the platform socket timeout, shifting the
789
+ // recorded install time and the deferred deep link the user is waiting on.
790
+ // Shorter than the transport's 15s: this is enrichment, not the payload.
654
791
  const deviceId = client.getDeviceId();
655
792
  const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
656
- const challengeRes = await fetch(challengeUrl, {
657
- headers: { 'x-api-key': configuredApiKey },
658
- });
793
+ const ac = new AbortController();
794
+ const timer = setTimeout(
795
+ () => ac.abort(new Error('attest challenge timed out')),
796
+ ATTEST_CHALLENGE_TIMEOUT_MS,
797
+ );
798
+ let challengeRes: Response;
799
+ try {
800
+ challengeRes = await fetch(challengeUrl, {
801
+ headers: { 'x-api-key': configuredApiKey },
802
+ signal: ac.signal,
803
+ });
804
+ } finally {
805
+ clearTimeout(timer);
806
+ }
659
807
  if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
660
808
  const { challenge } = (await challengeRes.json()) as { challenge: string };
661
809
 
@@ -35,6 +35,14 @@ export interface MetaInstallReferrerResult {
35
35
  * real `DeviceInfo` shape.
36
36
  */
37
37
  export interface AdvenueDeviceInfo {
38
+ /** Bundle id / package name — Meta CAPI extinfo slot 1. */
39
+ packageName?: string;
40
+ /** User-facing app version (CFBundleShortVersionString / versionName) —
41
+ * extinfo slot 2. Surfaced as the event's top-level `appVersion`, not as
42
+ * part of the deviceInfo blob. */
43
+ shortVersion?: string;
44
+ /** Build identifier (CFBundleVersion / versionCode) — extinfo slot 3. */
45
+ longVersion?: string;
38
46
  model?: string;
39
47
  locale?: string;
40
48
  carrier?: string;
@@ -47,6 +55,17 @@ export interface AdvenueDeviceInfo {
47
55
  freeStorageGb?: number;
48
56
  }
49
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
+ export interface NativeTcfData {
65
+ gdprApplies?: number | null;
66
+ purposeConsents?: string | null;
67
+ }
68
+
50
69
  export interface AdvenueAndroidNativeModule {
51
70
  /** Raw Play Install Referrer; parse with @advenue/install-referrer. */
52
71
  getInstallReferrer(): Promise<InstallReferrerResult | null>;
@@ -62,9 +81,11 @@ export interface AdvenueAndroidNativeModule {
62
81
  /**
63
82
  * Play Integrity Standard API token (G3.1n).
64
83
  *
65
- * `requestHash` MUST be sha256_hex("<appId>:<deviceId>") — lowercase hex SHA-256
66
- * of the literal string "<appId>:<deviceId>". The server's mapIntegrityVerdict
67
- * re-derives the same string and compares it byte-for-byte.
84
+ * `requestHash` MUST be sha256_hex("<deviceId>") — lowercase hex SHA-256 of the
85
+ * device id alone. The server's mapIntegrityVerdict re-derives the same string
86
+ * and compares it byte-for-byte. No app id is hashed in: the token is decoded
87
+ * against the app's Play package name and must be PLAY_RECOGNIZED, which binds
88
+ * the app already.
68
89
  *
69
90
  * Rejects when:
70
91
  * - ECONFIG: `io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER` meta-data is absent.
@@ -101,6 +122,12 @@ export interface AdvenueAndroidNativeModule {
101
122
  * policy). Optional: absent on older native builds; feature-detected.
102
123
  */
103
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;
104
131
  }
105
132
 
106
133
  export type TrackingAuthorizationStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
@@ -164,4 +191,10 @@ export interface AdvenueIosNativeModule {
164
191
  secureGetItem?(key: string): string | null;
165
192
  secureSetItem?(key: string, value: string): void;
166
193
  secureRemoveItem?(key: string): void;
194
+ /**
195
+ * IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
196
+ * older native builds lack it — callers feature-detect (getAndroidId
197
+ * precedent).
198
+ */
199
+ getTcfData?(): NativeTcfData | null;
167
200
  }