@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/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
|
-
|
|
181
|
+
type NativeDeviceInfo = Partial<DeviceInfo> & { shortVersion?: string };
|
|
182
|
+
|
|
183
|
+
type DeviceInfoNatives = {
|
|
135
184
|
platform: 'ios' | 'android' | string;
|
|
136
|
-
ios?: { getDeviceInfo?: () =>
|
|
137
|
-
android?: { getDeviceInfo?: () =>
|
|
138
|
-
}
|
|
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
|
-
//
|
|
241
|
-
//
|
|
242
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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("<
|
|
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(
|
|
627
|
-
//
|
|
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(
|
|
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
|
|
657
|
-
|
|
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
|
|
package/src/native-types.ts
CHANGED
|
@@ -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("<
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
}
|