@advenue/react-native 0.8.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.
@@ -215,10 +215,12 @@ class AdvenueAndroidModule : Module() {
215
215
 
216
216
  // Play Integrity Standard API — device attestation (G3.1n).
217
217
  //
218
- // `requestHash` MUST be sha256_hex("<appId>:<deviceId>") — lowercase hex of
219
- // SHA-256 over the literal string "<appId>:<deviceId>". The server's
220
- // mapIntegrityVerdict (Task 3) re-derives the same string and compares it to
221
- // the token's requestHash field; the strings must match byte-for-byte.
218
+ // `requestHash` MUST be sha256_hex("<deviceId>") — lowercase hex of SHA-256
219
+ // over the device id alone. The server's mapIntegrityVerdict re-derives the
220
+ // same string and compares it to the token's requestHash field; the strings
221
+ // must match byte-for-byte. No app id is hashed in: the token is decoded
222
+ // against this app's Play package name and must be PLAY_RECOGNIZED, which
223
+ // binds the app already.
222
224
  //
223
225
  // The provider is warmed in OnCreate (above). If it is null — because the
224
226
  // manifest meta-data key is absent, Play Integrity is unsupported, or warming
@@ -284,6 +286,25 @@ class AdvenueAndroidModule : Module() {
284
286
 
285
287
  val ctx = appContext.reactContext?.applicationContext
286
288
  if (ctx != null) {
289
+ // Meta CAPI extinfo slots 1-3. versionName is the user-facing version
290
+ // (slot 2, surfaced as the event's appVersion); the version code is the
291
+ // build identifier (slot 3). Best-effort: a missing PackageInfo must
292
+ // not cost the caller the rest of the device metadata.
293
+ map["packageName"] = ctx.packageName
294
+ try {
295
+ val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
296
+ pi.versionName?.let { map["shortVersion"] = it }
297
+ @Suppress("DEPRECATION")
298
+ val code =
299
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
300
+ pi.longVersionCode
301
+ } else {
302
+ pi.versionCode.toLong()
303
+ }
304
+ map["longVersion"] = code.toString()
305
+ } catch (_: Exception) {
306
+ // package info best-effort
307
+ }
287
308
  try {
288
309
  val dm = ctx.resources.displayMetrics
289
310
  map["screenWidth"] = dm.widthPixels
package/dist/index.cjs CHANGED
@@ -222,7 +222,7 @@ function sha256Hex(message) {
222
222
  // package.json
223
223
  var package_default = {
224
224
  name: "@advenue/react-native",
225
- version: "0.8.0",
225
+ version: "0.9.0",
226
226
  description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
227
227
  type: "module",
228
228
  license: "MIT",
@@ -497,7 +497,7 @@ function resolveDmaConsent() {
497
497
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
498
498
  return consent;
499
499
  }
500
- function collectDeviceInfo(natives) {
500
+ function readNativeDeviceInfo(natives) {
501
501
  try {
502
502
  if (natives.platform === "android" && natives.android?.getDeviceInfo) {
503
503
  return natives.android.getDeviceInfo();
@@ -509,6 +509,10 @@ function collectDeviceInfo(natives) {
509
509
  }
510
510
  return {};
511
511
  }
512
+ function collectDeviceInfo(natives) {
513
+ const { shortVersion: _shortVersion, ...info } = readNativeDeviceInfo(natives);
514
+ return info;
515
+ }
512
516
  function applyAppState(client, status) {
513
517
  if (status === "active") {
514
518
  client.notifyAppActive();
@@ -527,7 +531,7 @@ var autoInstallPending = false;
527
531
  var autoInstallEnabled = true;
528
532
  var manualConsentSet = false;
529
533
  var tcfCollectionEnabled = false;
530
- var configuredAppId = null;
534
+ var ATTEST_CHALLENGE_TIMEOUT_MS = 5e3;
531
535
  var configuredApiKey = null;
532
536
  var configuredEndpoint = import_sdk_core.DEFAULT_ENDPOINT;
533
537
  var aemSeenUrlHashes = /* @__PURE__ */ new Set();
@@ -571,7 +575,6 @@ var Advenue = {
571
575
  mmkv,
572
576
  secureStore: secureMod,
573
577
  onDeepLink,
574
- appId,
575
578
  autoTrackInstall,
576
579
  tcfDataCollection,
577
580
  ...rest
@@ -581,7 +584,6 @@ var Advenue = {
581
584
  tcfCollectionEnabled = tcfDataCollection === true;
582
585
  autoInstallEnabled = autoTrackInstall !== false;
583
586
  const platform = resolvePlatform(rest.platform);
584
- configuredAppId = appId ?? null;
585
587
  configuredApiKey = config.apiKey;
586
588
  configuredEndpoint = config.endpoint ?? import_sdk_core.DEFAULT_ENDPOINT;
587
589
  appStateSub?.remove();
@@ -596,21 +598,26 @@ var Advenue = {
596
598
  secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
597
599
  const deviceId = rest.deviceId ?? await (0, import_sdk_core.resolveDurableDeviceId)(storage, secureStore ?? void 0);
598
600
  const osVersion = rest.osVersion ?? getOsVersion() ?? void 0;
601
+ const nativeDeviceInfo = readNativeDeviceInfo({
602
+ platform,
603
+ ios: getIosNative(),
604
+ android: getAndroidNative()
605
+ });
606
+ const { shortVersion, ...deviceInfo } = nativeDeviceInfo;
607
+ const appVersion = rest.appVersion ?? shortVersion ?? void 0;
599
608
  instance = new import_sdk_core.AdvenueClient({
600
609
  ...rest,
601
610
  platform,
602
611
  deviceId,
603
612
  storage,
604
- ...osVersion ? { osVersion } : {}
613
+ ...osVersion ? { osVersion } : {},
614
+ ...appVersion ? { appVersion } : {},
615
+ // Stamped by the SDK, never by the host app — see AdvenueConfig.sdkVersion.
616
+ sdkVersion: package_default.version
605
617
  });
606
618
  instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
607
619
  if (tcfCollectionEnabled) resolveDmaConsent();
608
620
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
609
- const deviceInfo = collectDeviceInfo({
610
- platform,
611
- ios: getIosNative(),
612
- android: getAndroidNative()
613
- });
614
621
  if (Object.keys(deviceInfo).length > 0) instance.setDeviceInfo(deviceInfo);
615
622
  if (!config.disableAutoSessions) {
616
623
  const appState = getAppState();
@@ -721,6 +728,9 @@ var Advenue = {
721
728
  getDeviceId: () => active().getDeviceId(),
722
729
  /** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
723
730
  getAdvenueId: () => active().getAdvenueId(),
731
+ /** Which app the server resolved this API key to; null until a batch lands
732
+ * (see AdvenueClient.getResolvedAppId). */
733
+ getResolvedAppId: () => active().getResolvedAppId(),
724
734
  shutdown() {
725
735
  appStateSub?.remove();
726
736
  appStateSub = null;
@@ -734,7 +744,6 @@ var Advenue = {
734
744
  autoInstallEnabled = true;
735
745
  manualConsentSet = false;
736
746
  tcfCollectionEnabled = false;
737
- configuredAppId = null;
738
747
  configuredApiKey = null;
739
748
  configuredEndpoint = import_sdk_core.DEFAULT_ENDPOINT;
740
749
  aemSeenUrlHashes.clear();
@@ -812,10 +821,10 @@ async function trackInstall() {
812
821
  }
813
822
  await resolveAdvertisingId();
814
823
  const trackOpts = { type: "install", ...opts };
815
- if (configuredAppId && android && typeof android.getIntegrityToken === "function") {
824
+ if (android && typeof android.getIntegrityToken === "function") {
816
825
  try {
817
826
  const deviceId = client.getDeviceId();
818
- const requestHash = sha256Hex(`${configuredAppId}:${deviceId}`);
827
+ const requestHash = sha256Hex(deviceId);
819
828
  const token = await android.getIntegrityToken(requestHash);
820
829
  if (token) {
821
830
  trackOpts.attestationToken = token;
@@ -825,13 +834,24 @@ async function trackInstall() {
825
834
  reportError("attest.playIntegrity", err);
826
835
  }
827
836
  }
828
- if (configuredAppId && configuredApiKey && ios && typeof ios.attestKey === "function") {
837
+ if (configuredApiKey && ios && typeof ios.attestKey === "function") {
829
838
  try {
830
839
  const deviceId = client.getDeviceId();
831
840
  const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
832
- const challengeRes = await fetch(challengeUrl, {
833
- headers: { "x-api-key": configuredApiKey }
834
- });
841
+ const ac = new AbortController();
842
+ const timer = setTimeout(
843
+ () => ac.abort(new Error("attest challenge timed out")),
844
+ ATTEST_CHALLENGE_TIMEOUT_MS
845
+ );
846
+ let challengeRes;
847
+ try {
848
+ challengeRes = await fetch(challengeUrl, {
849
+ headers: { "x-api-key": configuredApiKey },
850
+ signal: ac.signal
851
+ });
852
+ } finally {
853
+ clearTimeout(timer);
854
+ }
835
855
  if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
836
856
  const { challenge } = await challengeRes.json();
837
857
  const { keyId, attestationObject } = await ios.attestKey(challenge);
package/dist/index.d.cts 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;
@@ -83,9 +91,11 @@ interface AdvenueAndroidNativeModule {
83
91
  /**
84
92
  * Play Integrity Standard API token (G3.1n).
85
93
  *
86
- * `requestHash` MUST be sha256_hex("<appId>:<deviceId>") — lowercase hex SHA-256
87
- * of the literal string "<appId>:<deviceId>". The server's mapIntegrityVerdict
88
- * re-derives the same string and compares it byte-for-byte.
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.
89
99
  *
90
100
  * Rejects when:
91
101
  * - ECONFIG: `io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER` meta-data is absent.
@@ -264,15 +274,19 @@ declare function resolveDmaConsent(): Consent | null;
264
274
  * native-side error degrades to an empty object so callers can unconditionally
265
275
  * pass the result through.
266
276
  */
267
- declare function collectDeviceInfo(natives: {
277
+ type NativeDeviceInfo = Partial<DeviceInfo> & {
278
+ shortVersion?: string;
279
+ };
280
+ type DeviceInfoNatives = {
268
281
  platform: 'ios' | 'android' | string;
269
282
  ios?: {
270
- getDeviceInfo?: () => Partial<DeviceInfo>;
283
+ getDeviceInfo?: () => NativeDeviceInfo;
271
284
  } | null;
272
285
  android?: {
273
- getDeviceInfo?: () => Partial<DeviceInfo>;
286
+ getDeviceInfo?: () => NativeDeviceInfo;
274
287
  } | null;
275
- }): DeviceInfo;
288
+ };
289
+ declare function collectDeviceInfo(natives: DeviceInfoNatives): DeviceInfo;
276
290
  interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
277
291
  /**
278
292
  * Auto-detected from React Native's `Platform.OS` — omit it. Pass explicitly
@@ -287,18 +301,6 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
287
301
  * supply storage. When set, it replaces the built-in native storage.
288
302
  */
289
303
  mmkv?: MMKVLike;
290
- /**
291
- * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
292
- *
293
- * Your app's Advenue appId (the UUID in Settings → API Keys). When set on
294
- * Android, the automatic install obtains a Play Integrity Standard token and
295
- * attaches it to the install event. The requestHash is computed as
296
- * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
297
- * formula exactly (see `makeDecodeAndroid` in the worker).
298
- *
299
- * Omit to run without attestation (the server field stays `unavailable`).
300
- */
301
- appId?: string;
302
304
  /**
303
305
  * @internal Secure-store override (tests / custom embeddings). The SDK uses
304
306
  * its own Keychain-backed native storage on iOS for the reinstall-resilient
@@ -363,6 +365,9 @@ declare const Advenue: {
363
365
  getDeviceId: () => string;
364
366
  /** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
365
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;
366
371
  shutdown(): void;
367
372
  };
368
373
  /**
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;
@@ -83,9 +91,11 @@ interface AdvenueAndroidNativeModule {
83
91
  /**
84
92
  * Play Integrity Standard API token (G3.1n).
85
93
  *
86
- * `requestHash` MUST be sha256_hex("<appId>:<deviceId>") — lowercase hex SHA-256
87
- * of the literal string "<appId>:<deviceId>". The server's mapIntegrityVerdict
88
- * re-derives the same string and compares it byte-for-byte.
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.
89
99
  *
90
100
  * Rejects when:
91
101
  * - ECONFIG: `io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER` meta-data is absent.
@@ -264,15 +274,19 @@ declare function resolveDmaConsent(): Consent | null;
264
274
  * native-side error degrades to an empty object so callers can unconditionally
265
275
  * pass the result through.
266
276
  */
267
- declare function collectDeviceInfo(natives: {
277
+ type NativeDeviceInfo = Partial<DeviceInfo> & {
278
+ shortVersion?: string;
279
+ };
280
+ type DeviceInfoNatives = {
268
281
  platform: 'ios' | 'android' | string;
269
282
  ios?: {
270
- getDeviceInfo?: () => Partial<DeviceInfo>;
283
+ getDeviceInfo?: () => NativeDeviceInfo;
271
284
  } | null;
272
285
  android?: {
273
- getDeviceInfo?: () => Partial<DeviceInfo>;
286
+ getDeviceInfo?: () => NativeDeviceInfo;
274
287
  } | null;
275
- }): DeviceInfo;
288
+ };
289
+ declare function collectDeviceInfo(natives: DeviceInfoNatives): DeviceInfo;
276
290
  interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
277
291
  /**
278
292
  * Auto-detected from React Native's `Platform.OS` — omit it. Pass explicitly
@@ -287,18 +301,6 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
287
301
  * supply storage. When set, it replaces the built-in native storage.
288
302
  */
289
303
  mmkv?: MMKVLike;
290
- /**
291
- * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
292
- *
293
- * Your app's Advenue appId (the UUID in Settings → API Keys). When set on
294
- * Android, the automatic install obtains a Play Integrity Standard token and
295
- * attaches it to the install event. The requestHash is computed as
296
- * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
297
- * formula exactly (see `makeDecodeAndroid` in the worker).
298
- *
299
- * Omit to run without attestation (the server field stays `unavailable`).
300
- */
301
- appId?: string;
302
304
  /**
303
305
  * @internal Secure-store override (tests / custom embeddings). The SDK uses
304
306
  * its own Keychain-backed native storage on iOS for the reinstall-resilient
@@ -363,6 +365,9 @@ declare const Advenue: {
363
365
  getDeviceId: () => string;
364
366
  /** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
365
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;
366
371
  shutdown(): void;
367
372
  };
368
373
  /**
package/dist/index.js CHANGED
@@ -218,7 +218,7 @@ function sha256Hex(message) {
218
218
  // package.json
219
219
  var package_default = {
220
220
  name: "@advenue/react-native",
221
- version: "0.8.0",
221
+ version: "0.9.0",
222
222
  description: "Advenue MMP React Native SDK \u2014 attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
223
223
  type: "module",
224
224
  license: "MIT",
@@ -493,7 +493,7 @@ function resolveDmaConsent() {
493
493
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
494
494
  return consent;
495
495
  }
496
- function collectDeviceInfo(natives) {
496
+ function readNativeDeviceInfo(natives) {
497
497
  try {
498
498
  if (natives.platform === "android" && natives.android?.getDeviceInfo) {
499
499
  return natives.android.getDeviceInfo();
@@ -505,6 +505,10 @@ function collectDeviceInfo(natives) {
505
505
  }
506
506
  return {};
507
507
  }
508
+ function collectDeviceInfo(natives) {
509
+ const { shortVersion: _shortVersion, ...info } = readNativeDeviceInfo(natives);
510
+ return info;
511
+ }
508
512
  function applyAppState(client, status) {
509
513
  if (status === "active") {
510
514
  client.notifyAppActive();
@@ -523,7 +527,7 @@ var autoInstallPending = false;
523
527
  var autoInstallEnabled = true;
524
528
  var manualConsentSet = false;
525
529
  var tcfCollectionEnabled = false;
526
- var configuredAppId = null;
530
+ var ATTEST_CHALLENGE_TIMEOUT_MS = 5e3;
527
531
  var configuredApiKey = null;
528
532
  var configuredEndpoint = DEFAULT_ENDPOINT;
529
533
  var aemSeenUrlHashes = /* @__PURE__ */ new Set();
@@ -567,7 +571,6 @@ var Advenue = {
567
571
  mmkv,
568
572
  secureStore: secureMod,
569
573
  onDeepLink,
570
- appId,
571
574
  autoTrackInstall,
572
575
  tcfDataCollection,
573
576
  ...rest
@@ -577,7 +580,6 @@ var Advenue = {
577
580
  tcfCollectionEnabled = tcfDataCollection === true;
578
581
  autoInstallEnabled = autoTrackInstall !== false;
579
582
  const platform = resolvePlatform(rest.platform);
580
- configuredAppId = appId ?? null;
581
583
  configuredApiKey = config.apiKey;
582
584
  configuredEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
583
585
  appStateSub?.remove();
@@ -592,21 +594,26 @@ var Advenue = {
592
594
  secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
593
595
  const deviceId = rest.deviceId ?? await resolveDurableDeviceId(storage, secureStore ?? void 0);
594
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;
595
604
  instance = new AdvenueClient({
596
605
  ...rest,
597
606
  platform,
598
607
  deviceId,
599
608
  storage,
600
- ...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
601
613
  });
602
614
  instance.debugLog(`SDK v${package_default.version} \u2014 events -> ${configuredEndpoint}`);
603
615
  if (tcfCollectionEnabled) resolveDmaConsent();
604
616
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
605
- const deviceInfo = collectDeviceInfo({
606
- platform,
607
- ios: getIosNative(),
608
- android: getAndroidNative()
609
- });
610
617
  if (Object.keys(deviceInfo).length > 0) instance.setDeviceInfo(deviceInfo);
611
618
  if (!config.disableAutoSessions) {
612
619
  const appState = getAppState();
@@ -717,6 +724,9 @@ var Advenue = {
717
724
  getDeviceId: () => active().getDeviceId(),
718
725
  /** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
719
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(),
720
730
  shutdown() {
721
731
  appStateSub?.remove();
722
732
  appStateSub = null;
@@ -730,7 +740,6 @@ var Advenue = {
730
740
  autoInstallEnabled = true;
731
741
  manualConsentSet = false;
732
742
  tcfCollectionEnabled = false;
733
- configuredAppId = null;
734
743
  configuredApiKey = null;
735
744
  configuredEndpoint = DEFAULT_ENDPOINT;
736
745
  aemSeenUrlHashes.clear();
@@ -808,10 +817,10 @@ async function trackInstall() {
808
817
  }
809
818
  await resolveAdvertisingId();
810
819
  const trackOpts = { type: "install", ...opts };
811
- if (configuredAppId && android && typeof android.getIntegrityToken === "function") {
820
+ if (android && typeof android.getIntegrityToken === "function") {
812
821
  try {
813
822
  const deviceId = client.getDeviceId();
814
- const requestHash = sha256Hex(`${configuredAppId}:${deviceId}`);
823
+ const requestHash = sha256Hex(deviceId);
815
824
  const token = await android.getIntegrityToken(requestHash);
816
825
  if (token) {
817
826
  trackOpts.attestationToken = token;
@@ -821,13 +830,24 @@ async function trackInstall() {
821
830
  reportError("attest.playIntegrity", err);
822
831
  }
823
832
  }
824
- if (configuredAppId && configuredApiKey && ios && typeof ios.attestKey === "function") {
833
+ if (configuredApiKey && ios && typeof ios.attestKey === "function") {
825
834
  try {
826
835
  const deviceId = client.getDeviceId();
827
836
  const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
828
- const challengeRes = await fetch(challengeUrl, {
829
- headers: { "x-api-key": configuredApiKey }
830
- });
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
+ }
831
851
  if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
832
852
  const { challenge } = await challengeRes.json();
833
853
  const { keyId, attestationObject } = await ios.attestKey(challenge);
@@ -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 (
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@advenue/react-native",
3
- "version": "0.8.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.5.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.2.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/index.ts CHANGED
@@ -178,11 +178,18 @@ export function resolveDmaConsent(): Consent | null {
178
178
  * native-side error degrades to an empty object so callers can unconditionally
179
179
  * pass the result through.
180
180
  */
181
- export function collectDeviceInfo(natives: {
181
+ type NativeDeviceInfo = Partial<DeviceInfo> & { shortVersion?: string };
182
+
183
+ type DeviceInfoNatives = {
182
184
  platform: 'ios' | 'android' | string;
183
- ios?: { getDeviceInfo?: () => Partial<DeviceInfo> } | null;
184
- android?: { getDeviceInfo?: () => Partial<DeviceInfo> } | null;
185
- }): 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 {
186
193
  try {
187
194
  if (natives.platform === 'android' && natives.android?.getDeviceInfo) {
188
195
  return natives.android.getDeviceInfo();
@@ -196,6 +203,13 @@ export function collectDeviceInfo(natives: {
196
203
  return {};
197
204
  }
198
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
+
199
213
  export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
200
214
  /**
201
215
  * Auto-detected from React Native's `Platform.OS` — omit it. Pass explicitly
@@ -210,18 +224,6 @@ export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platfo
210
224
  * supply storage. When set, it replaces the built-in native storage.
211
225
  */
212
226
  mmkv?: MMKVLike;
213
- /**
214
- * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
215
- *
216
- * Your app's Advenue appId (the UUID in Settings → API Keys). When set on
217
- * Android, the automatic install obtains a Play Integrity Standard token and
218
- * attaches it to the install event. The requestHash is computed as
219
- * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
220
- * formula exactly (see `makeDecodeAndroid` in the worker).
221
- *
222
- * Omit to run without attestation (the server field stays `unavailable`).
223
- */
224
- appId?: string;
225
227
  /**
226
228
  * @internal Secure-store override (tests / custom embeddings). The SDK uses
227
229
  * its own Keychain-backed native storage on iOS for the reinstall-resilient
@@ -299,9 +301,10 @@ let manualConsentSet = false;
299
301
  // Whether the current initialize() opted into TCF auto-collection
300
302
  // (RNAdvenueConfig.tcfDataCollection). Reset by initialize()/shutdown().
301
303
  let tcfCollectionEnabled = false;
302
- // G3.1n: app-scoped id for attestation requestHash. Set from RNAdvenueConfig.appId
303
- // in initialize(). Null when the integrator has not configured attestation.
304
- let configuredAppId: string | null = null;
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;
305
308
  // G3.2n: API key + endpoint needed to fetch the App Attest challenge from the
306
309
  // server. Stored at module level so trackInstall() can issue the challenge
307
310
  // request without reaching into the private AdvenueClient fields.
@@ -386,7 +389,6 @@ export const Advenue = {
386
389
  mmkv,
387
390
  secureStore: secureMod,
388
391
  onDeepLink,
389
- appId,
390
392
  autoTrackInstall,
391
393
  tcfDataCollection,
392
394
  ...rest
@@ -398,7 +400,6 @@ export const Advenue = {
398
400
  tcfCollectionEnabled = tcfDataCollection === true;
399
401
  autoInstallEnabled = autoTrackInstall !== false;
400
402
  const platform = resolvePlatform(rest.platform);
401
- configuredAppId = appId ?? null;
402
403
  configuredApiKey = config.apiKey;
403
404
  configuredEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
404
405
  appStateSub?.remove();
@@ -421,12 +422,29 @@ export const Advenue = {
421
422
  // Auto-collect the OS release version — it feeds the server's normalized
422
423
  // probabilistic fingerprint (spec D1); an explicit config value wins.
423
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;
424
439
  instance = new AdvenueClient({
425
440
  ...rest,
426
441
  platform,
427
442
  deviceId,
428
443
  storage,
429
444
  ...(osVersion ? { osVersion } : {}),
445
+ ...(appVersion ? { appVersion } : {}),
446
+ // Stamped by the SDK, never by the host app — see AdvenueConfig.sdkVersion.
447
+ sdkVersion: pkg.version,
430
448
  });
431
449
  // One-line ground truth for "which SDK build talks to which host" — the
432
450
  // exact questions a silent integration always raises first.
@@ -436,13 +454,6 @@ export const Advenue = {
436
454
  if (tcfCollectionEnabled) resolveDmaConsent();
437
455
  // (a) resolve ad-id at init time
438
456
  void resolveAdvertisingId().catch((err) => reportError('enrich.advertisingId', err));
439
- // Collect native device info (Meta CAPI extinfo) once at init time; a
440
- // best-effort, synchronous read, so no fire-and-forget needed.
441
- const deviceInfo = collectDeviceInfo({
442
- platform,
443
- ios: getIosNative(),
444
- android: getAndroidNative(),
445
- });
446
457
  if (Object.keys(deviceInfo).length > 0) instance.setDeviceInfo(deviceInfo);
447
458
  if (!config.disableAutoSessions) {
448
459
  const appState = getAppState();
@@ -593,6 +604,9 @@ export const Advenue = {
593
604
  getDeviceId: () => active().getDeviceId(),
594
605
  /** The Advenue ID — alias of getDeviceId (see AdvenueClient.getAdvenueId). */
595
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(),
596
610
  shutdown() {
597
611
  appStateSub?.remove();
598
612
  appStateSub = null;
@@ -606,7 +620,6 @@ export const Advenue = {
606
620
  autoInstallEnabled = true;
607
621
  manualConsentSet = false;
608
622
  tcfCollectionEnabled = false;
609
- configuredAppId = null;
610
623
  configuredApiKey = null;
611
624
  configuredEndpoint = DEFAULT_ENDPOINT;
612
625
  aemSeenUrlHashes.clear();
@@ -735,18 +748,20 @@ export async function trackInstall(): Promise<void> {
735
748
  // leaves attestation absent; the install is never blocked.
736
749
  const trackOpts: TrackOptions = { type: 'install', ...opts };
737
750
  if (
738
- configuredAppId &&
739
751
  android &&
740
752
  typeof (android as { getIntegrityToken?: unknown }).getIntegrityToken === 'function'
741
753
  ) {
742
754
  try {
743
- // requestHash = sha256_hex("<appId>:<deviceId>") — must match the server's
755
+ // requestHash = sha256_hex("<deviceId>") — must match the server's
744
756
  // mapIntegrityVerdict computation byte-for-byte (see makeDecodeAndroid in
745
757
  // apps/worker/src/attestation-decoders.ts):
746
- // expectedRequestHash = sha256(`${event.appId}:${event.deviceId}`)
747
- // 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.
748
763
  const deviceId = client.getDeviceId();
749
- const requestHash = sha256Hex(`${configuredAppId}:${deviceId}`);
764
+ const requestHash = sha256Hex(deviceId);
750
765
  const token = await (
751
766
  android as { getIntegrityToken(h: string): Promise<string> }
752
767
  ).getIntegrityToken(requestHash);
@@ -763,19 +778,32 @@ export async function trackInstall(): Promise<void> {
763
778
  // G3.2n — App Attest attestation (iOS only). Best-effort: any error (no
764
779
  // support on simulator, challenge fetch failure, attest failure) leaves
765
780
  // attestation absent; the install is NEVER blocked.
766
- if (
767
- configuredAppId &&
768
- configuredApiKey &&
769
- ios &&
770
- typeof (ios as { attestKey?: unknown }).attestKey === 'function'
771
- ) {
781
+ if (configuredApiKey && ios && typeof (ios as { attestKey?: unknown }).attestKey === 'function') {
772
782
  try {
773
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.
774
791
  const deviceId = client.getDeviceId();
775
792
  const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
776
- const challengeRes = await fetch(challengeUrl, {
777
- headers: { 'x-api-key': configuredApiKey },
778
- });
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
+ }
779
807
  if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
780
808
  const { challenge } = (await challengeRes.json()) as { challenge: string };
781
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;
@@ -73,9 +81,11 @@ export interface AdvenueAndroidNativeModule {
73
81
  /**
74
82
  * Play Integrity Standard API token (G3.1n).
75
83
  *
76
- * `requestHash` MUST be sha256_hex("<appId>:<deviceId>") — lowercase hex SHA-256
77
- * of the literal string "<appId>:<deviceId>". The server's mapIntegrityVerdict
78
- * 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.
79
89
  *
80
90
  * Rejects when:
81
91
  * - ECONFIG: `io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER` meta-data is absent.