@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/README.md +5 -0
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +48 -4
- package/dist/index.cjs +116 -22
- package/dist/index.d.cts +71 -20
- package/dist/index.d.ts +71 -20
- package/dist/index.js +117 -23
- package/ios/AdvenueIosModule.swift +31 -0
- package/package.json +3 -3
- package/src/aem.ts +33 -0
- package/src/deep-links.ts +11 -2
- package/src/index.ts +193 -45
- package/src/native-types.ts +36 -3
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,14 @@ interface MetaInstallReferrerResult {
|
|
|
44
44
|
* real `DeviceInfo` shape.
|
|
45
45
|
*/
|
|
46
46
|
interface AdvenueDeviceInfo {
|
|
47
|
+
/** Bundle id / package name — Meta CAPI extinfo slot 1. */
|
|
48
|
+
packageName?: string;
|
|
49
|
+
/** User-facing app version (CFBundleShortVersionString / versionName) —
|
|
50
|
+
* extinfo slot 2. Surfaced as the event's top-level `appVersion`, not as
|
|
51
|
+
* part of the deviceInfo blob. */
|
|
52
|
+
shortVersion?: string;
|
|
53
|
+
/** Build identifier (CFBundleVersion / versionCode) — extinfo slot 3. */
|
|
54
|
+
longVersion?: string;
|
|
47
55
|
model?: string;
|
|
48
56
|
locale?: string;
|
|
49
57
|
carrier?: string;
|
|
@@ -55,6 +63,16 @@ interface AdvenueDeviceInfo {
|
|
|
55
63
|
totalStorageGb?: number;
|
|
56
64
|
freeStorageGb?: number;
|
|
57
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Raw IAB TCF v2 CMP values from the platform default store
|
|
68
|
+
* (`UserDefaults.standard` / default SharedPreferences) — where TCF-compliant
|
|
69
|
+
* CMPs (e.g. Google UMP) write `IABTCF_*` keys. Structurally mirrors
|
|
70
|
+
* `TcfData` from `@advenue/sdk-core`.
|
|
71
|
+
*/
|
|
72
|
+
interface NativeTcfData {
|
|
73
|
+
gdprApplies?: number | null;
|
|
74
|
+
purposeConsents?: string | null;
|
|
75
|
+
}
|
|
58
76
|
interface AdvenueAndroidNativeModule {
|
|
59
77
|
/** Raw Play Install Referrer; parse with @advenue/install-referrer. */
|
|
60
78
|
getInstallReferrer(): Promise<InstallReferrerResult | null>;
|
|
@@ -73,9 +91,11 @@ interface AdvenueAndroidNativeModule {
|
|
|
73
91
|
/**
|
|
74
92
|
* Play Integrity Standard API token (G3.1n).
|
|
75
93
|
*
|
|
76
|
-
* `requestHash` MUST be sha256_hex("<
|
|
77
|
-
*
|
|
78
|
-
*
|
|
94
|
+
* `requestHash` MUST be sha256_hex("<deviceId>") — lowercase hex SHA-256 of the
|
|
95
|
+
* device id alone. The server's mapIntegrityVerdict re-derives the same string
|
|
96
|
+
* and compares it byte-for-byte. No app id is hashed in: the token is decoded
|
|
97
|
+
* against the app's Play package name and must be PLAY_RECOGNIZED, which binds
|
|
98
|
+
* the app already.
|
|
79
99
|
*
|
|
80
100
|
* Rejects when:
|
|
81
101
|
* - ECONFIG: `io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER` meta-data is absent.
|
|
@@ -112,6 +132,12 @@ interface AdvenueAndroidNativeModule {
|
|
|
112
132
|
* policy). Optional: absent on older native builds; feature-detected.
|
|
113
133
|
*/
|
|
114
134
|
getAndroidId?(): string | null;
|
|
135
|
+
/**
|
|
136
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
137
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
138
|
+
* precedent).
|
|
139
|
+
*/
|
|
140
|
+
getTcfData?(): NativeTcfData | null;
|
|
115
141
|
}
|
|
116
142
|
type TrackingAuthorizationStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
|
|
117
143
|
interface AdvenueIosNativeModule {
|
|
@@ -176,6 +202,12 @@ interface AdvenueIosNativeModule {
|
|
|
176
202
|
secureGetItem?(key: string): string | null;
|
|
177
203
|
secureSetItem?(key: string, value: string): void;
|
|
178
204
|
secureRemoveItem?(key: string): void;
|
|
205
|
+
/**
|
|
206
|
+
* IAB TCF v2 CMP data, null when no CMP has written TCF keys. Optional:
|
|
207
|
+
* older native builds lack it — callers feature-detect (getAndroidId
|
|
208
|
+
* precedent).
|
|
209
|
+
*/
|
|
210
|
+
getTcfData?(): NativeTcfData | null;
|
|
179
211
|
}
|
|
180
212
|
|
|
181
213
|
/**
|
|
@@ -219,6 +251,20 @@ declare function getAndroidNative(): AdvenueAndroidNativeModule | null;
|
|
|
219
251
|
* Fire-and-forget at call sites; errors are suppressed internally.
|
|
220
252
|
*/
|
|
221
253
|
declare function resolveAdvertisingId(): Promise<void>;
|
|
254
|
+
/**
|
|
255
|
+
* Reads IAB TCF v2 consent (written by a TCF-compliant CMP, e.g. Google UMP,
|
|
256
|
+
* to the platform default store), maps it to the DMA Consent shape, and
|
|
257
|
+
* pushes it into the active client. No-op — returning null — when consent was
|
|
258
|
+
* set manually this session, the native module or its getter is absent, no
|
|
259
|
+
* TCF data exists, or the freshly-mapped consent is identical to what the
|
|
260
|
+
* client already has (skip-if-unchanged: avoids re-persisting to storage and
|
|
261
|
+
* re-firing the ad-id resolve on every foreground when nothing changed):
|
|
262
|
+
* absent/unchanged consent must stay untouched, never fabricated or re-churned.
|
|
263
|
+
*
|
|
264
|
+
* Must be called after `Advenue.initialize()` — like every facade method, it
|
|
265
|
+
* throws via the uninitialized guard otherwise.
|
|
266
|
+
*/
|
|
267
|
+
declare function resolveDmaConsent(): Consent | null;
|
|
222
268
|
/**
|
|
223
269
|
* Collects native device metadata (model, locale, screen, cpu, storage, timezone)
|
|
224
270
|
* used to populate Meta CAPI `extinfo`. Reads from the platform-appropriate
|
|
@@ -228,15 +274,19 @@ declare function resolveAdvertisingId(): Promise<void>;
|
|
|
228
274
|
* native-side error degrades to an empty object so callers can unconditionally
|
|
229
275
|
* pass the result through.
|
|
230
276
|
*/
|
|
231
|
-
|
|
277
|
+
type NativeDeviceInfo = Partial<DeviceInfo> & {
|
|
278
|
+
shortVersion?: string;
|
|
279
|
+
};
|
|
280
|
+
type DeviceInfoNatives = {
|
|
232
281
|
platform: 'ios' | 'android' | string;
|
|
233
282
|
ios?: {
|
|
234
|
-
getDeviceInfo?: () =>
|
|
283
|
+
getDeviceInfo?: () => NativeDeviceInfo;
|
|
235
284
|
} | null;
|
|
236
285
|
android?: {
|
|
237
|
-
getDeviceInfo?: () =>
|
|
286
|
+
getDeviceInfo?: () => NativeDeviceInfo;
|
|
238
287
|
} | null;
|
|
239
|
-
}
|
|
288
|
+
};
|
|
289
|
+
declare function collectDeviceInfo(natives: DeviceInfoNatives): DeviceInfo;
|
|
240
290
|
interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
|
|
241
291
|
/**
|
|
242
292
|
* Auto-detected from React Native's `Platform.OS` — omit it. Pass explicitly
|
|
@@ -251,18 +301,6 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
|
|
|
251
301
|
* supply storage. When set, it replaces the built-in native storage.
|
|
252
302
|
*/
|
|
253
303
|
mmkv?: MMKVLike;
|
|
254
|
-
/**
|
|
255
|
-
* G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
|
|
256
|
-
*
|
|
257
|
-
* Your app's Advenue appId (the UUID in Settings → API Keys). When set on
|
|
258
|
-
* Android, the automatic install obtains a Play Integrity Standard token and
|
|
259
|
-
* attaches it to the install event. The requestHash is computed as
|
|
260
|
-
* `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
|
|
261
|
-
* formula exactly (see `makeDecodeAndroid` in the worker).
|
|
262
|
-
*
|
|
263
|
-
* Omit to run without attestation (the server field stays `unavailable`).
|
|
264
|
-
*/
|
|
265
|
-
appId?: string;
|
|
266
304
|
/**
|
|
267
305
|
* @internal Secure-store override (tests / custom embeddings). The SDK uses
|
|
268
306
|
* its own Keychain-backed native storage on iOS for the reinstall-resilient
|
|
@@ -285,6 +323,14 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
|
|
|
285
323
|
* install path deterministically.
|
|
286
324
|
*/
|
|
287
325
|
autoTrackInstall?: boolean;
|
|
326
|
+
/**
|
|
327
|
+
* Auto-collect IAB TCF v2 consent written by a TCF-compliant CMP (e.g.
|
|
328
|
+
* Google UMP) and attach it to every event as DMA consent data (AppsFlyer
|
|
329
|
+
* `enableTCFDataCollection` parity). Read at init and re-read on each
|
|
330
|
+
* foreground, so a CMP decision made after startup lands on later events.
|
|
331
|
+
* A manual `setConsentData()` call always wins. Default false.
|
|
332
|
+
*/
|
|
333
|
+
tcfDataCollection?: boolean;
|
|
288
334
|
}
|
|
289
335
|
/**
|
|
290
336
|
* Maps a React Native AppState status to the client's session lifecycle hooks.
|
|
@@ -317,6 +363,11 @@ declare const Advenue: {
|
|
|
317
363
|
} | null;
|
|
318
364
|
flush: () => Promise<void>;
|
|
319
365
|
getDeviceId: () => string;
|
|
366
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
367
|
+
getAdvenueId: () => string;
|
|
368
|
+
/** Which app the server resolved this API key to; null until a batch lands
|
|
369
|
+
* (see AdvenueClient.getResolvedAppId). */
|
|
370
|
+
getResolvedAppId: () => string | null;
|
|
320
371
|
shutdown(): void;
|
|
321
372
|
};
|
|
322
373
|
/**
|
|
@@ -333,4 +384,4 @@ declare const Advenue: {
|
|
|
333
384
|
*/
|
|
334
385
|
declare function trackInstall(): Promise<void>;
|
|
335
386
|
|
|
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 };
|
|
387
|
+
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.9.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,7 +471,29 @@ async function resolveAdvertisingId() {
|
|
|
454
471
|
}
|
|
455
472
|
client.setAdvertisingId({});
|
|
456
473
|
}
|
|
457
|
-
function
|
|
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
|
+
}
|
|
496
|
+
function readNativeDeviceInfo(natives) {
|
|
458
497
|
try {
|
|
459
498
|
if (natives.platform === "android" && natives.android?.getDeviceInfo) {
|
|
460
499
|
return natives.android.getDeviceInfo();
|
|
@@ -466,6 +505,10 @@ function collectDeviceInfo(natives) {
|
|
|
466
505
|
}
|
|
467
506
|
return {};
|
|
468
507
|
}
|
|
508
|
+
function collectDeviceInfo(natives) {
|
|
509
|
+
const { shortVersion: _shortVersion, ...info } = readNativeDeviceInfo(natives);
|
|
510
|
+
return info;
|
|
511
|
+
}
|
|
469
512
|
function applyAppState(client, status) {
|
|
470
513
|
if (status === "active") {
|
|
471
514
|
client.notifyAppActive();
|
|
@@ -482,9 +525,12 @@ var INSTALL_SENT_KEY = "advenue.install_sent";
|
|
|
482
525
|
var installInFlight = false;
|
|
483
526
|
var autoInstallPending = false;
|
|
484
527
|
var autoInstallEnabled = true;
|
|
485
|
-
var
|
|
528
|
+
var manualConsentSet = false;
|
|
529
|
+
var tcfCollectionEnabled = false;
|
|
530
|
+
var ATTEST_CHALLENGE_TIMEOUT_MS = 5e3;
|
|
486
531
|
var configuredApiKey = null;
|
|
487
532
|
var configuredEndpoint = DEFAULT_ENDPOINT;
|
|
533
|
+
var aemSeenUrlHashes = /* @__PURE__ */ new Set();
|
|
488
534
|
function active() {
|
|
489
535
|
if (!instance) {
|
|
490
536
|
throw new Error("Advenue.initialize() must be called before tracking events.");
|
|
@@ -521,11 +567,19 @@ function fireAutoInstallIfPending() {
|
|
|
521
567
|
}
|
|
522
568
|
var Advenue = {
|
|
523
569
|
async initialize(config) {
|
|
524
|
-
const {
|
|
570
|
+
const {
|
|
571
|
+
mmkv,
|
|
572
|
+
secureStore: secureMod,
|
|
573
|
+
onDeepLink,
|
|
574
|
+
autoTrackInstall,
|
|
575
|
+
tcfDataCollection,
|
|
576
|
+
...rest
|
|
577
|
+
} = config;
|
|
525
578
|
autoInstallPending = false;
|
|
579
|
+
manualConsentSet = false;
|
|
580
|
+
tcfCollectionEnabled = tcfDataCollection === true;
|
|
526
581
|
autoInstallEnabled = autoTrackInstall !== false;
|
|
527
582
|
const platform = resolvePlatform(rest.platform);
|
|
528
|
-
configuredAppId = appId ?? null;
|
|
529
583
|
configuredApiKey = config.apiKey;
|
|
530
584
|
configuredEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
|
|
531
585
|
appStateSub?.remove();
|
|
@@ -540,20 +594,26 @@ var Advenue = {
|
|
|
540
594
|
secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
|
|
541
595
|
const deviceId = rest.deviceId ?? await resolveDurableDeviceId(storage, secureStore ?? void 0);
|
|
542
596
|
const osVersion = rest.osVersion ?? getOsVersion() ?? void 0;
|
|
597
|
+
const nativeDeviceInfo = readNativeDeviceInfo({
|
|
598
|
+
platform,
|
|
599
|
+
ios: getIosNative(),
|
|
600
|
+
android: getAndroidNative()
|
|
601
|
+
});
|
|
602
|
+
const { shortVersion, ...deviceInfo } = nativeDeviceInfo;
|
|
603
|
+
const appVersion = rest.appVersion ?? shortVersion ?? void 0;
|
|
543
604
|
instance = new AdvenueClient({
|
|
544
605
|
...rest,
|
|
545
606
|
platform,
|
|
546
607
|
deviceId,
|
|
547
608
|
storage,
|
|
548
|
-
...osVersion ? { osVersion } : {}
|
|
609
|
+
...osVersion ? { osVersion } : {},
|
|
610
|
+
...appVersion ? { appVersion } : {},
|
|
611
|
+
// Stamped by the SDK, never by the host app — see AdvenueConfig.sdkVersion.
|
|
612
|
+
sdkVersion: package_default.version
|
|
549
613
|
});
|
|
550
614
|
instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
|
|
615
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
551
616
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
552
|
-
const deviceInfo = collectDeviceInfo({
|
|
553
|
-
platform,
|
|
554
|
-
ios: getIosNative(),
|
|
555
|
-
android: getAndroidNative()
|
|
556
|
-
});
|
|
557
617
|
if (Object.keys(deviceInfo).length > 0) instance.setDeviceInfo(deviceInfo);
|
|
558
618
|
if (!config.disableAutoSessions) {
|
|
559
619
|
const appState = getAppState();
|
|
@@ -564,10 +624,12 @@ var Advenue = {
|
|
|
564
624
|
if (!instance) return;
|
|
565
625
|
if (status === "background") {
|
|
566
626
|
seenBackground = true;
|
|
627
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
567
628
|
applyAppState(instance, status);
|
|
568
629
|
} else if (status === "active") {
|
|
569
630
|
if (!seenBackground) return;
|
|
570
631
|
seenBackground = false;
|
|
632
|
+
if (tcfCollectionEnabled) resolveDmaConsent();
|
|
571
633
|
applyAppState(instance, status);
|
|
572
634
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
573
635
|
}
|
|
@@ -578,7 +640,7 @@ var Advenue = {
|
|
|
578
640
|
instance.notifyAppActive();
|
|
579
641
|
}
|
|
580
642
|
}
|
|
581
|
-
|
|
643
|
+
{
|
|
582
644
|
const linking = getLinking();
|
|
583
645
|
if (linking) {
|
|
584
646
|
const client = instance;
|
|
@@ -588,7 +650,19 @@ var Advenue = {
|
|
|
588
650
|
// Deferred deep link: server-side conversion poll (deterministic ids
|
|
589
651
|
// where available, probabilistic otherwise).
|
|
590
652
|
resolveDeferred: () => client.resolveDeferredDeepLink(),
|
|
591
|
-
onDeepLink
|
|
653
|
+
onDeepLink: onDeepLink ?? (() => {
|
|
654
|
+
}),
|
|
655
|
+
onDirectUrl: (url, link) => {
|
|
656
|
+
const raw = link.params?.al_applink_data;
|
|
657
|
+
if (typeof raw !== "string") return;
|
|
658
|
+
const campaignIds = extractAemCampaignIds(raw);
|
|
659
|
+
if (!campaignIds) return;
|
|
660
|
+
const sourceUrlHash = sha256Hex(url);
|
|
661
|
+
if (aemSeenUrlHashes.has(sourceUrlHash)) return;
|
|
662
|
+
if (client.getRequireConsent() && !client.getTrackingConsent()) return;
|
|
663
|
+
client.track("adv_meta_aem", { campaignIds, sourceUrlHash });
|
|
664
|
+
aemSeenUrlHashes.add(sourceUrlHash);
|
|
665
|
+
}
|
|
592
666
|
});
|
|
593
667
|
deepLinkSub.ready.catch((err) => reportError("deepLink.resolve", err));
|
|
594
668
|
}
|
|
@@ -641,12 +715,18 @@ var Advenue = {
|
|
|
641
715
|
}
|
|
642
716
|
},
|
|
643
717
|
setConsentData(consent) {
|
|
718
|
+
manualConsentSet = true;
|
|
644
719
|
active().setConsentData(consent);
|
|
645
720
|
void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
|
|
646
721
|
},
|
|
647
722
|
getConsentData: () => active().getConsentData(),
|
|
648
723
|
flush: () => active().flush(),
|
|
649
724
|
getDeviceId: () => active().getDeviceId(),
|
|
725
|
+
/** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
|
|
726
|
+
getAdvenueId: () => active().getAdvenueId(),
|
|
727
|
+
/** Which app the server resolved this API key to; null until a batch lands
|
|
728
|
+
* (see AdvenueClient.getResolvedAppId). */
|
|
729
|
+
getResolvedAppId: () => active().getResolvedAppId(),
|
|
650
730
|
shutdown() {
|
|
651
731
|
appStateSub?.remove();
|
|
652
732
|
appStateSub = null;
|
|
@@ -658,9 +738,11 @@ var Advenue = {
|
|
|
658
738
|
installInFlight = false;
|
|
659
739
|
autoInstallPending = false;
|
|
660
740
|
autoInstallEnabled = true;
|
|
661
|
-
|
|
741
|
+
manualConsentSet = false;
|
|
742
|
+
tcfCollectionEnabled = false;
|
|
662
743
|
configuredApiKey = null;
|
|
663
744
|
configuredEndpoint = DEFAULT_ENDPOINT;
|
|
745
|
+
aemSeenUrlHashes.clear();
|
|
664
746
|
}
|
|
665
747
|
};
|
|
666
748
|
async function trackInstall() {
|
|
@@ -735,10 +817,10 @@ async function trackInstall() {
|
|
|
735
817
|
}
|
|
736
818
|
await resolveAdvertisingId();
|
|
737
819
|
const trackOpts = { type: "install", ...opts };
|
|
738
|
-
if (
|
|
820
|
+
if (android && typeof android.getIntegrityToken === "function") {
|
|
739
821
|
try {
|
|
740
822
|
const deviceId = client.getDeviceId();
|
|
741
|
-
const requestHash = sha256Hex(
|
|
823
|
+
const requestHash = sha256Hex(deviceId);
|
|
742
824
|
const token = await android.getIntegrityToken(requestHash);
|
|
743
825
|
if (token) {
|
|
744
826
|
trackOpts.attestationToken = token;
|
|
@@ -748,13 +830,24 @@ async function trackInstall() {
|
|
|
748
830
|
reportError("attest.playIntegrity", err);
|
|
749
831
|
}
|
|
750
832
|
}
|
|
751
|
-
if (
|
|
833
|
+
if (configuredApiKey && ios && typeof ios.attestKey === "function") {
|
|
752
834
|
try {
|
|
753
835
|
const deviceId = client.getDeviceId();
|
|
754
836
|
const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
837
|
+
const ac = new AbortController();
|
|
838
|
+
const timer = setTimeout(
|
|
839
|
+
() => ac.abort(new Error("attest challenge timed out")),
|
|
840
|
+
ATTEST_CHALLENGE_TIMEOUT_MS
|
|
841
|
+
);
|
|
842
|
+
let challengeRes;
|
|
843
|
+
try {
|
|
844
|
+
challengeRes = await fetch(challengeUrl, {
|
|
845
|
+
headers: { "x-api-key": configuredApiKey },
|
|
846
|
+
signal: ac.signal
|
|
847
|
+
});
|
|
848
|
+
} finally {
|
|
849
|
+
clearTimeout(timer);
|
|
850
|
+
}
|
|
758
851
|
if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
|
|
759
852
|
const { challenge } = await challengeRes.json();
|
|
760
853
|
const { keyId, attestationObject } = await ios.attestKey(challenge);
|
|
@@ -810,5 +903,6 @@ export {
|
|
|
810
903
|
getIosNative,
|
|
811
904
|
parseDirectLink,
|
|
812
905
|
resolveAdvertisingId,
|
|
906
|
+
resolveDmaConsent,
|
|
813
907
|
trackInstall
|
|
814
908
|
};
|
|
@@ -249,6 +249,20 @@ public class AdvenueIosModule: Module {
|
|
|
249
249
|
info["timezone"] = TimeZone.current.identifier
|
|
250
250
|
info["cpuCores"] = ProcessInfo.processInfo.activeProcessorCount
|
|
251
251
|
|
|
252
|
+
// Meta CAPI extinfo slots 1-3. CFBundleShortVersionString is the
|
|
253
|
+
// user-facing version (slot 2, surfaced as the event's appVersion);
|
|
254
|
+
// CFBundleVersion is the build identifier (slot 3).
|
|
255
|
+
let bundle = Bundle.main
|
|
256
|
+
if let bundleId = bundle.bundleIdentifier {
|
|
257
|
+
info["packageName"] = bundleId
|
|
258
|
+
}
|
|
259
|
+
if let short = bundle.infoDictionary?["CFBundleShortVersionString"] as? String {
|
|
260
|
+
info["shortVersion"] = short
|
|
261
|
+
}
|
|
262
|
+
if let build = bundle.infoDictionary?["CFBundleVersion"] as? String {
|
|
263
|
+
info["longVersion"] = build
|
|
264
|
+
}
|
|
265
|
+
|
|
252
266
|
let (screenWidth, screenHeight, screenDensity) = Self.onMainSync { () -> (Int, Int, Double) in
|
|
253
267
|
let screen = UIScreen.main
|
|
254
268
|
return (
|
|
@@ -288,6 +302,23 @@ public class AdvenueIosModule: Module {
|
|
|
288
302
|
Self.sdkDefaults.removeObject(forKey: key)
|
|
289
303
|
}
|
|
290
304
|
|
|
305
|
+
// MARK: - DMA / TCF consent (read-only)
|
|
306
|
+
|
|
307
|
+
// IAB TCF v2 data a TCF-compliant CMP (e.g. Google UMP) writes to the
|
|
308
|
+
// app's *standard* UserDefaults — deliberately not our io.advenue.sdk
|
|
309
|
+
// suite. Raw values pass through; the TS side (tcfToConsent) owns the
|
|
310
|
+
// policy mapping. UserDefaults is thread-safe — no main-thread hop.
|
|
311
|
+
Function("getTcfData") { () -> [String: Any?]? in
|
|
312
|
+
let std = UserDefaults.standard
|
|
313
|
+
// object(forKey:), not integer(forKey:): a missing key must read as
|
|
314
|
+
// unknown, not 0 ("GDPR does not apply") — a defaulted 0 would strip
|
|
315
|
+
// consent handling from EEA traffic.
|
|
316
|
+
let gdprApplies = std.object(forKey: "IABTCF_gdprApplies") as? Int
|
|
317
|
+
let purposeConsents = std.string(forKey: "IABTCF_PurposeConsents")
|
|
318
|
+
if gdprApplies == nil && purposeConsents == nil { return nil }
|
|
319
|
+
return ["gdprApplies": gdprApplies, "purposeConsents": purposeConsents]
|
|
320
|
+
}
|
|
321
|
+
|
|
291
322
|
// Keychain-backed secure storage for the reinstall-durable identifiers
|
|
292
323
|
// (device id, install guard). kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
|
|
293
324
|
// 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.9.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.6.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@expo/config-plugins": ">=8",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"tsup": "^8.5.1",
|
|
51
51
|
"typescript": "^5.7.2",
|
|
52
52
|
"vitest": "^2.1.8",
|
|
53
|
-
"@advenue/shared": "0.
|
|
53
|
+
"@advenue/shared": "0.3.0",
|
|
54
54
|
"@advenue/signing": "0.0.0"
|
|
55
55
|
},
|
|
56
56
|
"react-native": "./dist/index.cjs",
|
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();
|