@advenue/react-native 0.4.0 → 0.5.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.
@@ -0,0 +1,461 @@
1
+ import ExpoModulesCore
2
+ import StoreKit
3
+ import AdServices
4
+ import AdSupport
5
+ import AppTrackingTransparency
6
+ import UIKit
7
+ import DeviceCheck
8
+ import CryptoKit
9
+ #if canImport(AdAttributionKit)
10
+ import AdAttributionKit
11
+ #endif
12
+
13
+ // Advenue iOS native module. Exposes SKAdNetwork conversion-value updates
14
+ // (SKAN 4.0 fine + coarse) and the Apple Search Ads attribution token to JS.
15
+ // The conversion-value POLICY lives in TS (@advenue/skadnetwork); this module
16
+ // only performs the privileged StoreKit/AdServices calls.
17
+ public class AdvenueIosModule: Module {
18
+ // UIKit (UIScreen/UIDevice/UIPasteboard) is main-thread-only, but synchronous
19
+ // `Function` handlers run on whatever thread invoked them (the JS thread on
20
+ // RN's bridge). Hop to main when needed; `sync` is safe because these are
21
+ // cheap local reads and the guard prevents a same-thread deadlock.
22
+ private static func onMainSync<T>(_ work: () -> T) -> T {
23
+ if Thread.isMainThread {
24
+ return work()
25
+ }
26
+ return DispatchQueue.main.sync(execute: work)
27
+ }
28
+
29
+ public func definition() -> ModuleDefinition {
30
+ Name("AdvenueIos")
31
+
32
+ // Call once at launch so SKAdNetwork starts the measurement window.
33
+ Function("registerAppForAttribution") {
34
+ if #available(iOS 16.1, *) {
35
+ // No-op: updatePostbackConversionValue implicitly registers.
36
+ } else {
37
+ SKAdNetwork.registerAppForAdNetworkAttribution()
38
+ }
39
+ }
40
+
41
+ // SKAN 4.0: fine value 0–63 + coarse "low" | "medium" | "high". On
42
+ // iOS 17.4+ the AdAttributionKit postback is updated as well, since an
43
+ // impression may be signed for either rail.
44
+ AsyncFunction("updateConversionValue") { (fineValue: Int, coarse: String, lockWindow: Bool, promise: Promise) in
45
+ #if canImport(AdAttributionKit)
46
+ if #available(iOS 17.4, *) {
47
+ let aakCoarse: CoarseConversionValue = {
48
+ switch coarse {
49
+ case "high": return .high
50
+ case "medium": return .medium
51
+ default: return .low
52
+ }
53
+ }()
54
+ Task {
55
+ do {
56
+ try await Postback.updateConversionValue(fineValue, coarseConversionValue: aakCoarse, lockPostback: lockWindow)
57
+ } catch {
58
+ // Expected when no AAK-signed impression exists; logged so SKAN/AAK
59
+ // postback divergence on iOS 17.4+ is diagnosable. The SKAN call
60
+ // below still resolves the promise.
61
+ NSLog("[Advenue] AdAttributionKit conversion update skipped: %@", error.localizedDescription)
62
+ }
63
+ }
64
+ }
65
+ #endif
66
+ if #available(iOS 16.1, *) {
67
+ let coarseValue: SKAdNetwork.CoarseConversionValue = {
68
+ switch coarse {
69
+ case "high": return .high
70
+ case "medium": return .medium
71
+ default: return .low
72
+ }
73
+ }()
74
+ SKAdNetwork.updatePostbackConversionValue(fineValue, coarseValue: coarseValue, lockWindow: lockWindow) { error in
75
+ if let error = error { promise.reject("ESKAN", error.localizedDescription) }
76
+ else { promise.resolve(nil) }
77
+ }
78
+ } else if #available(iOS 15.4, *) {
79
+ SKAdNetwork.updatePostbackConversionValue(fineValue) { error in
80
+ if let error = error { promise.reject("ESKAN", error.localizedDescription) }
81
+ else { promise.resolve(nil) }
82
+ }
83
+ } else {
84
+ SKAdNetwork.updateConversionValue(fineValue)
85
+ promise.resolve(nil)
86
+ }
87
+ }
88
+
89
+ // Apple Search Ads attribution token; resolved server-side via @advenue/skadnetwork.
90
+ AsyncFunction("getAttributionToken") { (promise: Promise) in
91
+ if #available(iOS 14.3, *) {
92
+ do {
93
+ let token = try AAAttribution.attributionToken()
94
+ promise.resolve(token)
95
+ } catch {
96
+ promise.reject("EASA", error.localizedDescription)
97
+ }
98
+ } else {
99
+ promise.resolve(nil)
100
+ }
101
+ }
102
+
103
+ // ATT consent prompt. The consumer app must declare
104
+ // NSUserTrackingUsageDescription in Info.plist, or iOS rejects the call.
105
+ AsyncFunction("requestTrackingAuthorization") { (promise: Promise) in
106
+ if #available(iOS 14, *) {
107
+ ATTrackingManager.requestTrackingAuthorization { status in
108
+ promise.resolve(Self.attStatusString(status))
109
+ }
110
+ } else {
111
+ promise.resolve("authorized")
112
+ }
113
+ }
114
+
115
+ AsyncFunction("getAdvertisingId") { (promise: Promise) in
116
+ if #available(iOS 14, *) {
117
+ guard ATTrackingManager.trackingAuthorizationStatus == .authorized else {
118
+ promise.resolve(nil)
119
+ return
120
+ }
121
+ }
122
+ let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
123
+ promise.resolve(idfa == "00000000-0000-0000-0000-000000000000" ? nil : idfa)
124
+ }
125
+
126
+ Function("getTrackingAuthorizationStatus") { () -> String in
127
+ if #available(iOS 14, *) {
128
+ return Self.attStatusString(ATTrackingManager.trackingAuthorizationStatus)
129
+ }
130
+ return "authorized"
131
+ }
132
+
133
+ // Vendor-scoped device id (identifierForVendor). Available WITHOUT ATT because
134
+ // it is NOT an advertising identifier. First-party fraud/dedup use only — it
135
+ // must not be used for cross-network attribution, which would be "tracking".
136
+ Function("getIdentifierForVendor") { () -> String? in
137
+ return Self.onMainSync { UIDevice.current.identifierForVendor?.uuidString }
138
+ }
139
+
140
+ // Reads our clipboard match token (advmatch:<id>) written by the link
141
+ // redirector, for deterministic deferred deep linking. Returns ONLY our
142
+ // token, never arbitrary clipboard contents. (Reading the pasteboard shows
143
+ // the iOS paste notification — expected, user-visible, deterministic path.)
144
+ Function("getPasteboardMatchToken") { () -> String? in
145
+ return Self.onMainSync { () -> String? in
146
+ guard let s = UIPasteboard.general.string, s.hasPrefix("advmatch:") else { return nil }
147
+ return s
148
+ }
149
+ }
150
+
151
+ // G3.3 — DeviceCheck token. Calls DCDevice.current.generateToken to obtain
152
+ // the opaque base64 token that identifies this physical device to the Apple
153
+ // DeviceCheck API. The server uses it (with the per-app DeviceCheck key) to
154
+ // query/update the persistent 2-bit per-device flag. Unlike App Attest, this
155
+ // requires no challenge/server round-trip — just generate and attach.
156
+ //
157
+ // Rejects with EUNSUPPORTED when DCDevice.current.isSupported is false (iOS
158
+ // simulator, Catalyst, etc.) so the JS layer can fail-safe and omit the token.
159
+ // IMPORTANT: NOT compile/on-device verified here; reviewed against the
160
+ // DCDevice API. The JS fail-safe in sdk-react-native ensures missing token
161
+ // never blocks attribution.
162
+ AsyncFunction("getDeviceCheckToken") { (promise: Promise) in
163
+ guard DCDevice.current.isSupported else {
164
+ promise.reject("EUNSUPPORTED", "DCDevice is not supported on this device")
165
+ return
166
+ }
167
+ DCDevice.current.generateToken { tokenData, error in
168
+ if let error = error {
169
+ promise.reject("EDEVICECHECK", error.localizedDescription)
170
+ return
171
+ }
172
+ guard let tokenData = tokenData else {
173
+ promise.reject("EDEVICECHECK", "generateToken returned nil data")
174
+ return
175
+ }
176
+ promise.resolve(tokenData.base64EncodedString())
177
+ }
178
+ }
179
+
180
+ // G3.2n — App Attest. Takes the server-supplied challenge string, computes
181
+ // clientDataHash = SHA-256(challenge UTF-8), generates (or reuses a
182
+ // keychain-cached) DCAppAttest key, and calls attestKey to obtain the
183
+ // CBOR attestation object. Resolves { keyId, attestationObject } where
184
+ // attestationObject is base64-encoded — pass directly to the server's
185
+ // verifyAppAttest endpoint. Rejects with a code on any failure so the JS
186
+ // layer can fail-safe (omit attestation rather than blocking the install).
187
+ // IMPORTANT: DCAppAttestService.isSupported is false on simulators; this
188
+ // causes the JS layer to skip attestation entirely (server sees 'unavailable').
189
+ AsyncFunction("attestKey") { (challenge: String, promise: Promise) in
190
+ guard #available(iOS 14.0, *) else {
191
+ promise.reject("EUNSUPPORTED", "App Attest requires iOS 14.0+")
192
+ return
193
+ }
194
+ let service = DCAppAttestService.shared
195
+ guard service.isSupported else {
196
+ promise.reject("EUNSUPPORTED", "DCAppAttestService is not supported on this device")
197
+ return
198
+ }
199
+
200
+ // Compute clientDataHash = SHA-256 of the UTF-8 challenge bytes.
201
+ guard let challengeData = challenge.data(using: .utf8) else {
202
+ promise.reject("EBADCHALLENGE", "Failed to encode challenge as UTF-8")
203
+ return
204
+ }
205
+ let clientDataHash = Data(SHA256.hash(data: challengeData))
206
+
207
+ // Load a previously generated key from Keychain, or generate a new one.
208
+ // Key is cached per-install/device so we only call generateKey once.
209
+ Self.resolveKeyId(service: service) { result in
210
+ switch result {
211
+ case .failure(let error):
212
+ promise.reject("EKEYID", error.localizedDescription)
213
+ case .success(let keyId):
214
+ service.attestKey(keyId, clientDataHash: clientDataHash) { attestationObject, error in
215
+ if let error = error {
216
+ // A key can be attested only once. If Apple rejects this keyId as
217
+ // invalid (e.g. it was already attested but the server never recorded
218
+ // it, or the key became invalid), the cached id is permanently
219
+ // unusable — drop it so the NEXT call generates a fresh key instead
220
+ // of looping on the dead id forever.
221
+ if (error as? DCError)?.code == .invalidKey {
222
+ Self.deleteKeyId()
223
+ }
224
+ promise.reject("EATTEST", error.localizedDescription)
225
+ return
226
+ }
227
+ guard let attestationObject = attestationObject else {
228
+ promise.reject("EATTEST", "attestKey returned nil attestation object")
229
+ return
230
+ }
231
+ let base64Attest = attestationObject.base64EncodedString()
232
+ promise.resolve(["keyId": keyId, "attestationObject": base64Attest])
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ // Device metadata for Meta CAPI `extinfo` (Task 3n). Synchronous — every
239
+ // API used here is a cheap local read (no network I/O). Best-effort per
240
+ // field: a field is simply omitted when its source is unavailable, so the
241
+ // JS-side fail-safe (collectDeviceInfo) degrades gracefully rather than
242
+ // sending misleading placeholder values.
243
+ // IMPORTANT: NOT compile/on-device verified here; reviewed against the
244
+ // UIKit/Foundation APIs. The JS fail-safe in sdk-react-native ensures a
245
+ // missing/partial result never blocks attribution.
246
+ Function("getDeviceInfo") { () -> [String: Any] in
247
+ var info: [String: Any] = [:]
248
+
249
+ // Hardware model identifier (e.g. "iPhone16,1") via utsname, not the
250
+ // marketing name — matches the Android side's Build.MODEL granularity.
251
+ var systemInfo = utsname()
252
+ uname(&systemInfo)
253
+ let model = withUnsafePointer(to: &systemInfo.machine) {
254
+ $0.withMemoryRebound(to: CChar.self, capacity: 1) { ptr in
255
+ String(cString: ptr)
256
+ }
257
+ }
258
+ info["model"] = model
259
+ info["locale"] = Locale.current.identifier
260
+ info["timezone"] = TimeZone.current.identifier
261
+ info["cpuCores"] = ProcessInfo.processInfo.activeProcessorCount
262
+
263
+ let (screenWidth, screenHeight, screenDensity) = Self.onMainSync { () -> (Int, Int, Double) in
264
+ let screen = UIScreen.main
265
+ return (
266
+ Int(screen.bounds.width * screen.scale),
267
+ Int(screen.bounds.height * screen.scale),
268
+ Double(screen.scale)
269
+ )
270
+ }
271
+ info["screenWidth"] = screenWidth
272
+ info["screenHeight"] = screenHeight
273
+ info["screenDensity"] = screenDensity
274
+
275
+ if let attrs = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory()) {
276
+ let gb = 1024.0 * 1024.0 * 1024.0
277
+ if let total = attrs[.systemSize] as? NSNumber {
278
+ info["totalStorageGb"] = total.doubleValue / gb
279
+ }
280
+ if let free = attrs[.systemFreeSize] as? NSNumber {
281
+ info["freeStorageGb"] = free.doubleValue / gb
282
+ }
283
+ }
284
+
285
+ return info
286
+ }
287
+
288
+ // MARK: - Built-in SDK storage (Adjust-style: the app supplies nothing)
289
+
290
+ // Sync key/value storage backing the SDK's offline buffer + state. A
291
+ // dedicated UserDefaults suite keeps SDK keys out of the app's defaults.
292
+ Function("storageGetItem") { (key: String) -> String? in
293
+ Self.sdkDefaults.string(forKey: key)
294
+ }
295
+ Function("storageSetItem") { (key: String, value: String) in
296
+ Self.sdkDefaults.set(value, forKey: key)
297
+ }
298
+ Function("storageRemoveItem") { (key: String) in
299
+ Self.sdkDefaults.removeObject(forKey: key)
300
+ }
301
+
302
+ // Keychain-backed secure storage for the reinstall-durable identifiers
303
+ // (device id, install guard). kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly:
304
+ // survives app reinstall, readable after first unlock (background events),
305
+ // and never migrates via device-to-device backup restore — a restored
306
+ // backup must NOT clone another device's attribution identity.
307
+ Function("secureGetItem") { (key: String) -> String? in
308
+ Self.keychainGet(key)
309
+ }
310
+ Function("secureSetItem") { (key: String, value: String) in
311
+ Self.keychainSet(key, value)
312
+ }
313
+ Function("secureRemoveItem") { (key: String) in
314
+ Self.keychainRemove(key)
315
+ }
316
+ }
317
+
318
+ // MARK: - Built-in storage helpers
319
+
320
+ private static let sdkDefaults: UserDefaults =
321
+ UserDefaults(suiteName: "io.advenue.sdk") ?? .standard
322
+
323
+ // Shares `keychainService` with the App Attest helpers below — same service,
324
+ // distinct accounts (storage keys vs "app-attest-key-id"), so neither can
325
+ // read or delete the other's entries.
326
+ private static func keychainBaseQuery(_ key: String) -> [String: Any] {
327
+ [
328
+ kSecClass as String: kSecClassGenericPassword,
329
+ kSecAttrService as String: keychainService,
330
+ kSecAttrAccount as String: key,
331
+ ]
332
+ }
333
+
334
+ private static func keychainGet(_ key: String) -> String? {
335
+ var query = keychainBaseQuery(key)
336
+ query[kSecReturnData as String] = true
337
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
338
+ var result: AnyObject?
339
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
340
+ guard status == errSecSuccess, let data = result as? Data else {
341
+ return nil
342
+ }
343
+ return String(data: data, encoding: .utf8)
344
+ }
345
+
346
+ private static func keychainSet(_ key: String, _ value: String) {
347
+ guard let data = value.data(using: .utf8) else { return }
348
+ let base = keychainBaseQuery(key)
349
+ let update: [String: Any] = [kSecValueData as String: data]
350
+ let status = SecItemUpdate(base as CFDictionary, update as CFDictionary)
351
+ if status == errSecItemNotFound {
352
+ var add = base
353
+ add[kSecValueData as String] = data
354
+ add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
355
+ SecItemAdd(add as CFDictionary, nil)
356
+ }
357
+ }
358
+
359
+ private static func keychainRemove(_ key: String) {
360
+ SecItemDelete(keychainBaseQuery(key) as CFDictionary)
361
+ }
362
+
363
+ @available(iOS 14, *)
364
+ private static func attStatusString(_ status: ATTrackingManager.AuthorizationStatus) -> String {
365
+ switch status {
366
+ case .authorized: return "authorized"
367
+ case .denied: return "denied"
368
+ case .restricted: return "restricted"
369
+ default: return "notDetermined"
370
+ }
371
+ }
372
+
373
+ // MARK: - App Attest Keychain helpers
374
+
375
+ private static let keychainService = "io.advenue.sdk"
376
+ private static let keychainAccount = "app-attest-key-id"
377
+
378
+ /// Loads the persisted App Attest keyId from the Keychain. Returns nil when
379
+ /// no key has been generated yet; the caller must then call generateKey and
380
+ /// persist the result via saveKeyId(_:).
381
+ @available(iOS 14.0, *)
382
+ private static func loadKeyId() -> String? {
383
+ let query: [CFString: Any] = [
384
+ kSecClass: kSecClassGenericPassword,
385
+ kSecAttrService: keychainService,
386
+ kSecAttrAccount: keychainAccount,
387
+ kSecReturnData: true,
388
+ kSecMatchLimit: kSecMatchLimitOne,
389
+ ]
390
+ var item: CFTypeRef?
391
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
392
+ guard status == errSecSuccess, let data = item as? Data else { return nil }
393
+ return String(data: data, encoding: .utf8)
394
+ }
395
+
396
+ /// Persists the App Attest keyId to the Keychain, replacing any prior value.
397
+ @available(iOS 14.0, *)
398
+ private static func saveKeyId(_ keyId: String) {
399
+ guard let data = keyId.data(using: .utf8) else { return }
400
+ // Try to update an existing item first; fall back to adding a new one.
401
+ let query: [CFString: Any] = [
402
+ kSecClass: kSecClassGenericPassword,
403
+ kSecAttrService: keychainService,
404
+ kSecAttrAccount: keychainAccount,
405
+ ]
406
+ let attributes: [CFString: Any] = [kSecValueData: data]
407
+ var status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
408
+ if status == errSecItemNotFound {
409
+ var addQuery = query
410
+ addQuery[kSecValueData] = data
411
+ status = SecItemAdd(addQuery as CFDictionary, nil)
412
+ }
413
+ if status != errSecSuccess {
414
+ NSLog("[Advenue] Keychain write for App Attest keyId failed: %d", status)
415
+ }
416
+ }
417
+
418
+ /// Removes the persisted App Attest keyId so the next resolveKeyId generates a
419
+ /// fresh key. Called when Apple reports the cached key as invalid.
420
+ @available(iOS 14.0, *)
421
+ private static func deleteKeyId() {
422
+ let query: [CFString: Any] = [
423
+ kSecClass: kSecClassGenericPassword,
424
+ kSecAttrService: keychainService,
425
+ kSecAttrAccount: keychainAccount,
426
+ ]
427
+ let status = SecItemDelete(query as CFDictionary)
428
+ if status != errSecSuccess && status != errSecItemNotFound {
429
+ NSLog("[Advenue] Keychain delete for App Attest keyId failed: %d", status)
430
+ }
431
+ }
432
+
433
+ /// Resolves a DCAppAttest keyId: loads from Keychain if present, otherwise
434
+ /// generates a new one via DCAppAttestService.generateKey and persists it.
435
+ @available(iOS 14.0, *)
436
+ private static func resolveKeyId(
437
+ service: DCAppAttestService,
438
+ completion: @escaping (Result<String, Error>) -> Void
439
+ ) {
440
+ if let cached = loadKeyId() {
441
+ completion(.success(cached))
442
+ return
443
+ }
444
+ service.generateKey { keyId, error in
445
+ if let error = error {
446
+ completion(.failure(error))
447
+ return
448
+ }
449
+ guard let keyId = keyId else {
450
+ completion(.failure(NSError(
451
+ domain: "io.advenue.appattest",
452
+ code: -1,
453
+ userInfo: [NSLocalizedDescriptionKey: "generateKey returned nil keyId"]
454
+ )))
455
+ return
456
+ }
457
+ saveKeyId(keyId)
458
+ completion(.success(keyId))
459
+ }
460
+ }
461
+ }
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@advenue/react-native",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
+ "description": "Advenue MMP React Native SDK — attribution, install tracking, and deep links (bundled iOS/Android Expo native modules)",
4
5
  "type": "module",
5
6
  "license": "MIT",
6
7
  "main": "./dist/index.js",
@@ -16,6 +17,9 @@
16
17
  "dist",
17
18
  "src",
18
19
  "plugin",
20
+ "ios",
21
+ "android",
22
+ "expo-module.config.json",
19
23
  "!**/*.test.ts"
20
24
  ],
21
25
  "publishConfig": {
@@ -26,20 +30,16 @@
26
30
  "@advenue/sdk-core": "0.3.0"
27
31
  },
28
32
  "peerDependencies": {
29
- "@advenue/sdk-android": "*",
30
- "@advenue/sdk-ios": "*",
31
33
  "@expo/config-plugins": ">=8",
32
34
  "react": ">=18.2",
33
- "react-native": ">=0.74"
35
+ "react-native": ">=0.74",
36
+ "expo-modules-core": "*"
34
37
  },
35
38
  "peerDependenciesMeta": {
36
39
  "@expo/config-plugins": {
37
40
  "optional": true
38
41
  },
39
- "@advenue/sdk-android": {
40
- "optional": true
41
- },
42
- "@advenue/sdk-ios": {
42
+ "expo-modules-core": {
43
43
  "optional": true
44
44
  }
45
45
  },
@@ -47,8 +47,6 @@
47
47
  "tsup": "^8.5.1",
48
48
  "typescript": "^5.7.2",
49
49
  "vitest": "^2.1.8",
50
- "@advenue/sdk-android": "0.3.0",
51
- "@advenue/sdk-ios": "0.3.0",
52
50
  "@advenue/shared": "0.1.0",
53
51
  "@advenue/signing": "0.0.0"
54
52
  },
package/src/deep-links.ts CHANGED
@@ -28,10 +28,13 @@ export function parseDirectLink(url: string): DeepLink {
28
28
  const isHttp = parsed.protocol === 'http:' || parsed.protocol === 'https:';
29
29
  deepLinkValue = isHttp ? path : [parsed.host, path].filter(Boolean).join('/');
30
30
 
31
- for (const [k, v] of parsed.searchParams.entries()) {
31
+ // forEach, not entries(): the iterable protocol types live in
32
+ // lib.dom.iterable / @types/node, neither of which is guaranteed in the
33
+ // dts build's lib set — forEach is in the base lib everywhere.
34
+ parsed.searchParams.forEach((v, k) => {
32
35
  params ??= {};
33
36
  params[k] = v;
34
- }
37
+ });
35
38
  } catch {
36
39
  // Unparseable URL — surface the raw value so the app can still route.
37
40
  deepLinkValue = url;
package/src/index.ts CHANGED
@@ -17,7 +17,6 @@ import {
17
17
  type TrackOptions,
18
18
  resolveDurableDeviceId,
19
19
  } from '@advenue/sdk-core';
20
- import type { TrackingAuthorizationStatus } from '@advenue/sdk-ios';
21
20
  import { parseMatchToken } from '@advenue/shared/match-token';
22
21
  import { sha256Hex } from '@advenue/signing';
23
22
  import { type DeepLinkHandle, setupDeepLinks } from './deep-links';
@@ -31,6 +30,7 @@ import {
31
30
  getPlatformOS,
32
31
  } from './native';
33
32
  import { createNativeSecureStore, createNativeStorage } from './native-storage';
33
+ import type { TrackingAuthorizationStatus } from './native-types';
34
34
  import { type SecureStoreLike, createSecureStore } from './secure-store';
35
35
 
36
36
  export { createMMKVStorage } from './mmkv-storage';
@@ -40,7 +40,14 @@ export type { SecureStoreLike } from './secure-store';
40
40
  export { parseDirectLink } from './deep-links';
41
41
  export type { AdvenueConfig, Consent, DeepLink, TrackOptions } from '@advenue/sdk-core';
42
42
  export { getAndroidNative, getIosNative } from './native';
43
- export type { TrackingAuthorizationStatus } from '@advenue/sdk-ios';
43
+ export type {
44
+ AdvenueAndroidNativeModule,
45
+ AdvenueDeviceInfo,
46
+ AdvenueIosNativeModule,
47
+ InstallReferrerResult,
48
+ MetaInstallReferrerResult,
49
+ TrackingAuthorizationStatus,
50
+ } from './native-types';
44
51
 
45
52
  /**
46
53
  * Resolves the device advertising id (IDFA on iOS, GAID on Android), gates it
@@ -189,7 +196,7 @@ export interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platfo
189
196
  /**
190
197
  * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
191
198
  *
192
- * Your app's Advenue appId (the UUID shown in the dashboard). When set on
199
+ * Your app's Advenue appId (the UUID in Settings → API Keys). When set on
193
200
  * Android, the automatic install obtains a Play Integrity Standard token and
194
201
  * attaches it to the install event. The requestHash is computed as
195
202
  * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification