@advenue/react-native 0.3.1 → 0.4.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/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ declare function createMMKVStorage(mmkv: MMKVLike): StorageAdapter;
25
25
  interface SecureStoreLike {
26
26
  getItemAsync(key: string): Promise<string | null>;
27
27
  setItemAsync(key: string, value: string): Promise<void>;
28
+ /** Optional — forwarded so GDPR erasure can wipe the durable entries. */
29
+ deleteItemAsync?(key: string): Promise<void>;
28
30
  }
29
31
  /** Wraps expo-secure-store as an Advenue SecureStorageAdapter (async, durable). */
30
32
  declare function createSecureStore(secureStore: SecureStoreLike): SecureStorageAdapter;
@@ -80,20 +82,26 @@ declare function collectDeviceInfo(natives: {
80
82
  * paste banner — the user-visible, privacy-forward deferred path.)
81
83
  */
82
84
  declare function resolveClipboardMatch(): Promise<DeepLink | null>;
83
- interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage'> {
85
+ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
84
86
  /**
85
- * An MMKV instance for the durable offline buffer:
86
- * import { MMKV } from 'react-native-mmkv';
87
- * Advenue.initialize({ apiKey, platform: 'ios', mmkv: new MMKV() });
88
- * Omit to fall back to in-memory buffering (events lost on cold start).
87
+ * Auto-detected from React Native's `Platform.OS` — omit it. Pass explicitly
88
+ * only outside a React Native runtime (tests, custom embeddings); `ios` and
89
+ * `android` map verbatim, any other RN target (web, desktop) reports as
90
+ * `web`.
91
+ */
92
+ platform?: AdvenueConfig['platform'];
93
+ /**
94
+ * @internal Storage override (tests / custom embeddings). The SDK persists
95
+ * via its own native modules (UserDefaults/SharedPreferences) — apps never
96
+ * supply storage. When set, it replaces the built-in native storage.
89
97
  */
90
98
  mmkv?: MMKVLike;
91
99
  /**
92
100
  * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
93
101
  *
94
102
  * Your app's Advenue appId (the UUID shown in the dashboard). When set on
95
- * Android, `trackInstall()` will obtain a Play Integrity Standard token and
96
- * attach it to the install event. The requestHash is computed as
103
+ * Android, the automatic install obtains a Play Integrity Standard token and
104
+ * attaches it to the install event. The requestHash is computed as
97
105
  * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
98
106
  * formula exactly (see `makeDecodeAndroid` in the worker).
99
107
  *
@@ -101,10 +109,10 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage'> {
101
109
  */
102
110
  appId?: string;
103
111
  /**
104
- * An expo-secure-store module for a reinstall-resilient device identity:
105
- * import * as SecureStore from 'expo-secure-store';
106
- * await Advenue.initialize({ apiKey, platform: 'ios', secureStore: SecureStore });
107
- * Omit to fall back to sync storage (deviceId is not reinstall-resilient).
112
+ * @internal Secure-store override (tests / custom embeddings). The SDK uses
113
+ * its own Keychain-backed native storage on iOS for the reinstall-resilient
114
+ * device identity — apps never supply a secure store. When set, it replaces
115
+ * the built-in Keychain adapter.
108
116
  */
109
117
  secureStore?: SecureStoreLike;
110
118
  /**
@@ -115,6 +123,13 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage'> {
115
123
  * to distinguish the two. Omit to disable deep-link handling entirely.
116
124
  */
117
125
  onDeepLink?: (link: DeepLink) => void;
126
+ /**
127
+ * @internal Test hook — disables the automatic install fired by
128
+ * `initialize()`. Install tracking is always automatic in production (there
129
+ * is no public `trackInstall`); tests turn it off to exercise the internal
130
+ * install path deterministically.
131
+ */
132
+ autoTrackInstall?: boolean;
118
133
  }
119
134
  /**
120
135
  * Maps a React Native AppState status to the client's session lifecycle hooks.
@@ -134,18 +149,10 @@ declare const Advenue: {
134
149
  * where ATT doesn't exist) and forwards the result to the consent gate.
135
150
  */
136
151
  requestTrackingAuthorization(): Promise<TrackingAuthorizationStatus>;
137
- /**
138
- * Tracks the install event enriched with native attribution data: Play +
139
- * Meta install referrers on Android, SKAdNetwork registration on iOS.
140
- * Safe to call on every launch — when a `secureStore` is wired it fires at
141
- * most once per device (reinstall-resilient on iOS via Keychain; Android
142
- * re-fires after a reinstall, which is unavoidable post-GAID).
143
- */
144
- trackInstall(properties?: Record<string, unknown>): Promise<void>;
145
- setTrackingConsent: (granted: boolean) => void;
152
+ setTrackingConsent(granted: boolean): void;
146
153
  getTrackingConsent: () => boolean;
147
154
  /** GDPR/CCPA erasure: stop tracking and wipe local identifiers/queue. */
148
- forgetMe: () => void;
155
+ forgetMe(): void;
149
156
  setConsentData(consent: Consent): void;
150
157
  getConsentData: () => {
151
158
  isUserSubjectToGDPR: boolean;
@@ -157,5 +164,18 @@ declare const Advenue: {
157
164
  getDeviceId: () => string;
158
165
  shutdown(): void;
159
166
  };
167
+ /**
168
+ * Tracks the install event enriched with native attribution data: Play + Meta
169
+ * install referrers on Android, SKAdNetwork registration on iOS. Fired
170
+ * automatically by `initialize()` (or its consent deferral); when a
171
+ * `secureStore` is wired it fires at most once per device (reinstall-resilient
172
+ * on iOS via Keychain; Android re-fires after a reinstall, which is
173
+ * unavoidable post-GAID).
174
+ *
175
+ * @internal Exported for tests only — install tracking is automatic and this
176
+ * function is deliberately NOT on the `Advenue` facade. Do not call it from
177
+ * app code.
178
+ */
179
+ declare function trackInstall(): Promise<void>;
160
180
 
161
- export { Advenue, type MMKVLike, type RNAdvenueConfig, type SecureStoreLike, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveClipboardMatch };
181
+ export { Advenue, type MMKVLike, type RNAdvenueConfig, type SecureStoreLike, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveClipboardMatch, trackInstall };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { parseInstallReferrer, extractReferrerProperties, parseMetaInstallReferrer } from '@advenue/install-referrer';
2
- import { DEFAULT_ENDPOINT, MemoryStorage, resolveDurableDeviceId, AdvenueClient } from '@advenue/sdk-core';
2
+ import { DEVICE_ID_KEY, DEFAULT_ENDPOINT, MemoryStorage, resolveDurableDeviceId, AdvenueClient } from '@advenue/sdk-core';
3
3
 
4
4
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
5
5
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -229,6 +229,13 @@ function getAndroidNative() {
229
229
  return null;
230
230
  }
231
231
  }
232
+ function getPlatformOS() {
233
+ try {
234
+ return __require("react-native").Platform.OS ?? null;
235
+ } catch {
236
+ return null;
237
+ }
238
+ }
232
239
  function getAppState() {
233
240
  try {
234
241
  return __require("react-native").AppState;
@@ -244,11 +251,38 @@ function getLinking() {
244
251
  }
245
252
  }
246
253
 
254
+ // src/native-storage.ts
255
+ function createNativeStorage(mod) {
256
+ const m = mod;
257
+ if (!m || typeof m.storageGetItem !== "function" || typeof m.storageSetItem !== "function" || typeof m.storageRemoveItem !== "function") {
258
+ return null;
259
+ }
260
+ return {
261
+ getItem: (key) => m.storageGetItem?.(key) ?? null,
262
+ setItem: (key, value) => m.storageSetItem?.(key, value),
263
+ removeItem: (key) => m.storageRemoveItem?.(key)
264
+ };
265
+ }
266
+ function createNativeSecureStore(mod) {
267
+ const m = mod;
268
+ if (!m || typeof m.secureGetItem !== "function" || typeof m.secureSetItem !== "function" || typeof m.secureRemoveItem !== "function") {
269
+ return null;
270
+ }
271
+ return {
272
+ getItemAsync: async (key) => m.secureGetItem?.(key) ?? null,
273
+ setItemAsync: async (key, value) => m.secureSetItem?.(key, value),
274
+ deleteItemAsync: async (key) => m.secureRemoveItem?.(key)
275
+ };
276
+ }
277
+
247
278
  // src/secure-store.ts
248
279
  function createSecureStore(secureStore2) {
249
280
  return {
250
281
  getItemAsync: async (key) => await secureStore2.getItemAsync(key) ?? null,
251
- setItemAsync: (key, value) => secureStore2.setItemAsync(key, value)
282
+ setItemAsync: (key, value) => secureStore2.setItemAsync(key, value),
283
+ ...secureStore2.deleteItemAsync ? {
284
+ deleteItemAsync: (key) => secureStore2.deleteItemAsync?.(key) ?? Promise.resolve()
285
+ } : {}
252
286
  };
253
287
  }
254
288
 
@@ -276,7 +310,14 @@ async function resolveAdvertisingId() {
276
310
  const r = await android.getAdvertisingId();
277
311
  const vendorId = typeof android.getAppSetId === "function" ? await android.getAppSetId() : null;
278
312
  const adFields = r ? r.limitAdTracking ? { limitAdTracking: true } : { gaid: r.id } : {};
279
- client.setAdvertisingId({ ...adFields, ...vendorId ? { vendorId } : {} });
313
+ const hasUsableGaid = !!r && !r.limitAdTracking;
314
+ const rawAndroidId = !hasUsableGaid && typeof android.getAndroidId === "function" ? android.getAndroidId() : null;
315
+ const androidId = rawAndroidId && /^[0-9a-fA-F]{8,32}$/.test(rawAndroidId) ? rawAndroidId : null;
316
+ client.setAdvertisingId({
317
+ ...adFields,
318
+ ...vendorId ? { vendorId } : {},
319
+ ...androidId ? { androidId } : {}
320
+ });
280
321
  return;
281
322
  }
282
323
  client.setAdvertisingId({});
@@ -300,7 +341,14 @@ async function resolveClipboardMatch() {
300
341
  if (!ios || typeof ios.getPasteboardMatchToken !== "function") {
301
342
  return null;
302
343
  }
303
- const matchId = parseMatchToken(ios.getPasteboardMatchToken());
344
+ let token;
345
+ try {
346
+ token = ios.getPasteboardMatchToken();
347
+ } catch (err) {
348
+ reportError("enrich.clipboardMatch", err);
349
+ return null;
350
+ }
351
+ const matchId = parseMatchToken(token);
304
352
  if (!matchId) return null;
305
353
  return client.exchangeClipboardMatch(matchId);
306
354
  }
@@ -318,6 +366,8 @@ var secureStore = null;
318
366
  var installFlagStore = null;
319
367
  var INSTALL_SENT_KEY = "advenue.install_sent";
320
368
  var installInFlight = false;
369
+ var autoInstallPending = false;
370
+ var autoInstallEnabled = true;
321
371
  var configuredAppId = null;
322
372
  var configuredApiKey = null;
323
373
  var configuredEndpoint = DEFAULT_ENDPOINT;
@@ -330,9 +380,37 @@ function active() {
330
380
  function reportError(context, cause) {
331
381
  instance?.report(context, cause);
332
382
  }
383
+ function fireAutoInstall() {
384
+ void trackInstall().catch((err) => reportError("install.auto", err));
385
+ }
386
+ function resolvePlatform(explicit) {
387
+ if (explicit) {
388
+ return explicit;
389
+ }
390
+ const os = getPlatformOS();
391
+ if (os === "ios" || os === "android") {
392
+ return os;
393
+ }
394
+ if (os) {
395
+ return "web";
396
+ }
397
+ throw new Error(
398
+ "Advenue.initialize(): platform could not be detected (react-native unavailable) \u2014 pass platform: 'ios' | 'android' | 'web'."
399
+ );
400
+ }
401
+ function fireAutoInstallIfPending() {
402
+ if (!autoInstallPending) {
403
+ return;
404
+ }
405
+ autoInstallPending = false;
406
+ fireAutoInstall();
407
+ }
333
408
  var Advenue = {
334
409
  async initialize(config) {
335
- const { mmkv, secureStore: secureMod, onDeepLink, appId, ...rest } = config;
410
+ const { mmkv, secureStore: secureMod, onDeepLink, appId, autoTrackInstall, ...rest } = config;
411
+ autoInstallPending = false;
412
+ autoInstallEnabled = autoTrackInstall !== false;
413
+ const platform = resolvePlatform(rest.platform);
336
414
  configuredAppId = appId ?? null;
337
415
  configuredApiKey = config.apiKey;
338
416
  configuredEndpoint = config.endpoint ?? DEFAULT_ENDPOINT;
@@ -341,14 +419,16 @@ var Advenue = {
341
419
  deepLinkSub?.remove();
342
420
  deepLinkSub = null;
343
421
  instance?.shutdown();
344
- const storage = mmkv ? createMMKVStorage(mmkv) : new MemoryStorage();
422
+ const iosMod = getIosNative();
423
+ const androidMod = getAndroidNative();
424
+ const storage = mmkv ? createMMKVStorage(mmkv) : createNativeStorage(iosMod ?? androidMod) ?? new MemoryStorage();
345
425
  installFlagStore = storage;
346
- secureStore = secureMod ? createSecureStore(secureMod) : null;
426
+ secureStore = secureMod ? createSecureStore(secureMod) : createNativeSecureStore(iosMod);
347
427
  const deviceId = rest.deviceId ?? await resolveDurableDeviceId(storage, secureStore ?? void 0);
348
- instance = new AdvenueClient({ ...rest, deviceId, storage });
428
+ instance = new AdvenueClient({ ...rest, platform, deviceId, storage });
349
429
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
350
430
  const deviceInfo = collectDeviceInfo({
351
- platform: rest.platform,
431
+ platform,
352
432
  ios: getIosNative(),
353
433
  android: getAndroidNative()
354
434
  });
@@ -388,6 +468,14 @@ var Advenue = {
388
468
  resolveDeferred: async () => await resolveClipboardMatch() ?? client.resolveDeferredDeepLink(),
389
469
  onDeepLink
390
470
  });
471
+ deepLinkSub.ready.catch((err) => reportError("deepLink.resolve", err));
472
+ }
473
+ }
474
+ if (autoTrackInstall !== false) {
475
+ if (instance.getRequireConsent() && !instance.getTrackingConsent()) {
476
+ autoInstallPending = true;
477
+ } else {
478
+ fireAutoInstall();
391
479
  }
392
480
  }
393
481
  return instance;
@@ -402,142 +490,33 @@ var Advenue = {
402
490
  const status = ios ? await ios.requestTrackingAuthorization() : "authorized";
403
491
  active().setTrackingConsent(status === "authorized");
404
492
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
493
+ if (status === "authorized") {
494
+ fireAutoInstallIfPending();
495
+ }
405
496
  return status;
406
497
  },
407
- /**
408
- * Tracks the install event enriched with native attribution data: Play +
409
- * Meta install referrers on Android, SKAdNetwork registration on iOS.
410
- * Safe to call on every launch — when a `secureStore` is wired it fires at
411
- * most once per device (reinstall-resilient on iOS via Keychain; Android
412
- * re-fires after a reinstall, which is unavoidable post-GAID).
413
- */
414
- async trackInstall(properties) {
415
- const client = active();
416
- let sent = installFlagStore?.getItem(INSTALL_SENT_KEY) === "1";
417
- if (!sent && secureStore) {
418
- try {
419
- sent = await secureStore.getItemAsync(INSTALL_SENT_KEY) === "1";
420
- } catch (err) {
421
- reportError("install.guardRead", err);
422
- }
423
- }
424
- if (sent) {
425
- return;
426
- }
427
- if (installInFlight) {
428
- return;
429
- }
430
- installInFlight = true;
431
- const opts = {};
432
- const props = { ...properties };
433
- const android = getAndroidNative();
434
- if (android) {
435
- try {
436
- const result = await android.getInstallReferrer();
437
- if (result) {
438
- const parsed = parseInstallReferrer(result.referrer);
439
- if (parsed.network) opts.network = parsed.network;
440
- if (parsed.campaign) opts.campaign = parsed.campaign;
441
- if (parsed.gclid) props.gclid = parsed.gclid;
442
- if (parsed.fbclid) props.fbclid = parsed.fbclid;
443
- Object.assign(props, extractReferrerProperties(result));
444
- }
445
- } catch (err) {
446
- reportError("enrich.installReferrer", err);
447
- }
448
- try {
449
- const meta = await android.getMetaInstallReferrer();
450
- const parsedMeta = meta ? parseMetaInstallReferrer(meta) : null;
451
- if (parsedMeta) {
452
- props.metaInstallReferrer = parsedMeta.raw;
453
- if (parsedMeta.encryptedData) props.metaReferrerData = parsedMeta.encryptedData;
454
- if (parsedMeta.nonce) props.metaReferrerNonce = parsedMeta.nonce;
455
- if (!opts.network && parsedMeta.isClickThrough) {
456
- opts.network = "meta";
457
- if (parsedMeta.campaign) opts.campaign = parsedMeta.campaign;
458
- }
459
- }
460
- } catch (err) {
461
- reportError("enrich.metaReferrer", err);
462
- }
463
- }
464
- const ios = getIosNative();
465
- if (ios) {
466
- try {
467
- ios.registerAppForAttribution();
468
- } catch (err) {
469
- reportError("enrich.skanRegister", err);
470
- }
471
- try {
472
- const asaToken = await ios.getAttributionToken();
473
- if (asaToken) {
474
- opts.adservicesToken = asaToken;
475
- }
476
- } catch (err) {
477
- reportError("enrich.adservicesToken", err);
478
- }
498
+ setTrackingConsent(granted) {
499
+ active().setTrackingConsent(granted);
500
+ if (granted) {
501
+ fireAutoInstallIfPending();
479
502
  }
480
- await resolveAdvertisingId();
481
- const trackOpts = { type: "install", ...opts };
482
- if (configuredAppId && android && typeof android.getIntegrityToken === "function") {
483
- try {
484
- const deviceId = client.getDeviceId();
485
- const requestHash = sha256Hex(`${configuredAppId}:${deviceId}`);
486
- const token = await android.getIntegrityToken(requestHash);
487
- if (token) {
488
- trackOpts.attestationToken = token;
489
- trackOpts.attestationType = "play-integrity";
490
- }
491
- } catch (err) {
492
- reportError("attest.playIntegrity", err);
493
- }
494
- }
495
- if (configuredAppId && configuredApiKey && ios && typeof ios.attestKey === "function") {
496
- try {
497
- const deviceId = client.getDeviceId();
498
- const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
499
- const challengeRes = await fetch(challengeUrl, {
500
- headers: { "x-api-key": configuredApiKey }
501
- });
502
- if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
503
- const { challenge } = await challengeRes.json();
504
- const { keyId, attestationObject } = await ios.attestKey(challenge);
505
- trackOpts.attestationToken = attestationObject;
506
- trackOpts.attestationType = "app-attest";
507
- trackOpts.attestationKeyId = keyId;
508
- trackOpts.attestationChallenge = challenge;
509
- } catch (err) {
510
- reportError("attest.appAttest", err);
511
- }
512
- }
513
- if (ios && typeof ios.getDeviceCheckToken === "function") {
514
- try {
515
- const token = await ios.getDeviceCheckToken();
516
- if (token) {
517
- trackOpts.deviceCheckToken = token;
518
- }
519
- } catch (err) {
520
- reportError("attest.deviceCheck", err);
521
- }
522
- }
523
- client.track("install", props, trackOpts);
503
+ },
504
+ getTrackingConsent: () => active().getTrackingConsent(),
505
+ /** GDPR/CCPA erasure: stop tracking and wipe local identifiers/queue. */
506
+ forgetMe() {
507
+ active().forgetMe();
524
508
  try {
525
- installFlagStore?.setItem(INSTALL_SENT_KEY, "1");
509
+ installFlagStore?.removeItem(INSTALL_SENT_KEY);
526
510
  } catch (err) {
527
- reportError("install.flagPersist", err);
511
+ reportError("forget.wipe", err);
528
512
  }
529
- if (secureStore) {
530
- try {
531
- await secureStore.setItemAsync(INSTALL_SENT_KEY, "1");
532
- } catch (err) {
533
- reportError("install.flagPersistSecure", err);
513
+ const secure = secureStore;
514
+ if (secure?.deleteItemAsync) {
515
+ for (const key of [DEVICE_ID_KEY, INSTALL_SENT_KEY]) {
516
+ void secure.deleteItemAsync(key).catch((err) => reportError("forget.secureWipe", err));
534
517
  }
535
518
  }
536
519
  },
537
- setTrackingConsent: (granted) => active().setTrackingConsent(granted),
538
- getTrackingConsent: () => active().getTrackingConsent(),
539
- /** GDPR/CCPA erasure: stop tracking and wipe local identifiers/queue. */
540
- forgetMe: () => active().forgetMe(),
541
520
  setConsentData(consent) {
542
521
  active().setConsentData(consent);
543
522
  void resolveAdvertisingId().catch((err) => reportError("enrich.advertisingId", err));
@@ -554,10 +533,146 @@ var Advenue = {
554
533
  instance = null;
555
534
  secureStore = null;
556
535
  installInFlight = false;
536
+ autoInstallPending = false;
537
+ autoInstallEnabled = true;
557
538
  configuredAppId = null;
558
539
  configuredApiKey = null;
559
540
  configuredEndpoint = DEFAULT_ENDPOINT;
560
541
  }
561
542
  };
543
+ async function trackInstall() {
544
+ const client = active();
545
+ if (client.getRequireConsent() && !client.getTrackingConsent()) {
546
+ return;
547
+ }
548
+ let sent = installFlagStore?.getItem(INSTALL_SENT_KEY) === "1";
549
+ if (!sent && secureStore) {
550
+ try {
551
+ sent = await secureStore.getItemAsync(INSTALL_SENT_KEY) === "1";
552
+ } catch (err) {
553
+ reportError("install.guardRead", err);
554
+ }
555
+ }
556
+ if (sent) {
557
+ return;
558
+ }
559
+ if (installInFlight) {
560
+ return;
561
+ }
562
+ installInFlight = true;
563
+ const opts = {};
564
+ const props = {};
565
+ const android = getAndroidNative();
566
+ if (android) {
567
+ try {
568
+ const result = await android.getInstallReferrer();
569
+ if (result) {
570
+ const parsed = parseInstallReferrer(result.referrer);
571
+ if (parsed.network) opts.network = parsed.network;
572
+ if (parsed.campaign) opts.campaign = parsed.campaign;
573
+ if (parsed.gclid) props.gclid = parsed.gclid;
574
+ if (parsed.fbclid) props.fbclid = parsed.fbclid;
575
+ Object.assign(props, extractReferrerProperties(result));
576
+ }
577
+ } catch (err) {
578
+ reportError("enrich.installReferrer", err);
579
+ }
580
+ try {
581
+ const meta = await android.getMetaInstallReferrer();
582
+ const parsedMeta = meta ? parseMetaInstallReferrer(meta) : null;
583
+ if (parsedMeta) {
584
+ props.metaInstallReferrer = parsedMeta.raw;
585
+ if (parsedMeta.encryptedData) props.metaReferrerData = parsedMeta.encryptedData;
586
+ if (parsedMeta.nonce) props.metaReferrerNonce = parsedMeta.nonce;
587
+ if (!opts.network && parsedMeta.isClickThrough) {
588
+ opts.network = "meta";
589
+ if (parsedMeta.campaign) opts.campaign = parsedMeta.campaign;
590
+ }
591
+ }
592
+ } catch (err) {
593
+ reportError("enrich.metaReferrer", err);
594
+ }
595
+ }
596
+ const ios = getIosNative();
597
+ if (ios) {
598
+ try {
599
+ ios.registerAppForAttribution();
600
+ } catch (err) {
601
+ reportError("enrich.skanRegister", err);
602
+ }
603
+ try {
604
+ const asaToken = await ios.getAttributionToken();
605
+ if (asaToken) {
606
+ opts.adservicesToken = asaToken;
607
+ }
608
+ } catch (err) {
609
+ reportError("enrich.adservicesToken", err);
610
+ }
611
+ }
612
+ await resolveAdvertisingId();
613
+ const trackOpts = { type: "install", ...opts };
614
+ if (configuredAppId && android && typeof android.getIntegrityToken === "function") {
615
+ try {
616
+ const deviceId = client.getDeviceId();
617
+ const requestHash = sha256Hex(`${configuredAppId}:${deviceId}`);
618
+ const token = await android.getIntegrityToken(requestHash);
619
+ if (token) {
620
+ trackOpts.attestationToken = token;
621
+ trackOpts.attestationType = "play-integrity";
622
+ }
623
+ } catch (err) {
624
+ reportError("attest.playIntegrity", err);
625
+ }
626
+ }
627
+ if (configuredAppId && configuredApiKey && ios && typeof ios.attestKey === "function") {
628
+ try {
629
+ const deviceId = client.getDeviceId();
630
+ const challengeUrl = `${configuredEndpoint}/v1/attest/challenge?deviceId=${encodeURIComponent(deviceId)}`;
631
+ const challengeRes = await fetch(challengeUrl, {
632
+ headers: { "x-api-key": configuredApiKey }
633
+ });
634
+ if (!challengeRes.ok) throw new Error(`challenge HTTP ${challengeRes.status}`);
635
+ const { challenge } = await challengeRes.json();
636
+ const { keyId, attestationObject } = await ios.attestKey(challenge);
637
+ trackOpts.attestationToken = attestationObject;
638
+ trackOpts.attestationType = "app-attest";
639
+ trackOpts.attestationKeyId = keyId;
640
+ trackOpts.attestationChallenge = challenge;
641
+ } catch (err) {
642
+ reportError("attest.appAttest", err);
643
+ }
644
+ }
645
+ if (ios && typeof ios.getDeviceCheckToken === "function") {
646
+ try {
647
+ const token = await ios.getDeviceCheckToken();
648
+ if (token) {
649
+ trackOpts.deviceCheckToken = token;
650
+ }
651
+ } catch (err) {
652
+ reportError("attest.deviceCheck", err);
653
+ }
654
+ }
655
+ const pendingBefore = client.pendingCount;
656
+ client.track("install", props, trackOpts);
657
+ if (client.pendingCount === pendingBefore) {
658
+ installInFlight = false;
659
+ if (autoInstallEnabled && client.getRequireConsent() && !client.getTrackingConsent()) {
660
+ autoInstallPending = true;
661
+ }
662
+ return;
663
+ }
664
+ try {
665
+ installFlagStore?.setItem(INSTALL_SENT_KEY, "1");
666
+ } catch (err) {
667
+ reportError("install.flagPersist", err);
668
+ }
669
+ if (secureStore) {
670
+ try {
671
+ await secureStore.setItemAsync(INSTALL_SENT_KEY, "1");
672
+ } catch (err) {
673
+ reportError("install.flagPersistSecure", err);
674
+ }
675
+ }
676
+ }
562
677
 
563
- export { Advenue, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveClipboardMatch };
678
+ export { Advenue, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveClipboardMatch, trackInstall };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@advenue/react-native",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -23,16 +23,14 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@advenue/install-referrer": "0.1.1",
26
- "@advenue/sdk-core": "0.2.0"
26
+ "@advenue/sdk-core": "0.3.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@advenue/sdk-android": "*",
30
30
  "@advenue/sdk-ios": "*",
31
31
  "@expo/config-plugins": ">=8",
32
- "expo-secure-store": ">=12",
33
32
  "react": ">=18.2",
34
- "react-native": ">=0.74",
35
- "react-native-mmkv": ">=3"
33
+ "react-native": ">=0.74"
36
34
  },
37
35
  "peerDependenciesMeta": {
38
36
  "@expo/config-plugins": {
@@ -43,21 +41,15 @@
43
41
  },
44
42
  "@advenue/sdk-ios": {
45
43
  "optional": true
46
- },
47
- "expo-secure-store": {
48
- "optional": true
49
- },
50
- "react-native-mmkv": {
51
- "optional": true
52
44
  }
53
45
  },
54
46
  "devDependencies": {
55
47
  "tsup": "^8.5.1",
56
48
  "typescript": "^5.7.2",
57
49
  "vitest": "^2.1.8",
58
- "@advenue/sdk-ios": "0.2.0",
59
- "@advenue/sdk-android": "0.2.0",
60
- "@advenue/shared": "0.0.0",
50
+ "@advenue/sdk-android": "0.3.0",
51
+ "@advenue/sdk-ios": "0.3.0",
52
+ "@advenue/shared": "0.1.0",
61
53
  "@advenue/signing": "0.0.0"
62
54
  },
63
55
  "scripts": {
package/plugin/index.js CHANGED
@@ -5,23 +5,19 @@
5
5
  // backend already serves the matching apple-app-site-association / assetlinks.json.
6
6
  //
7
7
  // Usage (app.json / app.config.js):
8
- // ["@advenue/react-native/plugin", { "domains": ["go.advenue.io"] }]
8
+ // "@advenue/react-native/plugin" — Advenue cloud (go.advenue.io)
9
+ // ["@advenue/react-native/plugin", { "domains": ["go.acme.com"] }] — self-host / custom link domain
9
10
  //
10
11
  // @expo/config-plugins is resolved from the consuming app's Expo install.
11
12
  const { withEntitlementsPlist, withAndroidManifest } = require('@expo/config-plugins');
12
- const { addAssociatedDomains, addAppLinksIntentFilter } = require('./links');
13
+ const { addAssociatedDomains, addAppLinksIntentFilter, resolveDomains } = require('./links');
13
14
 
14
15
  /**
15
16
  * @param {any} config
16
17
  * @param {{ domains?: string[] } | undefined} props
17
18
  */
18
19
  function withAdvenueLinks(config, props) {
19
- const domains = props?.domains ?? [];
20
- if (domains.length === 0) {
21
- throw new Error(
22
- '@advenue/react-native/plugin: provide at least one link domain, e.g. { "domains": ["go.advenue.io"] }',
23
- );
24
- }
20
+ const domains = resolveDomains(props);
25
21
 
26
22
  let result = withEntitlementsPlist(config, (cfg) => {
27
23
  cfg.modResults = addAssociatedDomains(cfg.modResults, domains);