@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.
package/README.md CHANGED
@@ -8,11 +8,11 @@ engine.
8
8
  ## Install
9
9
 
10
10
  ```sh
11
- npx expo install @advenue/react-native @advenue/sdk-android @advenue/sdk-ios \
11
+ npx expo install @advenue/react-native \
12
12
  react-native-mmkv expo-secure-store
13
13
  ```
14
14
 
15
- `@advenue/sdk-android` and `@advenue/sdk-ios` are Expo native modules built by
15
+ The iOS and Android Expo native modules ship inside this package (`ios/`, `android/`) and are built by
16
16
  your app's own Gradle/Xcode build — no manual linking.
17
17
 
18
18
  ## Quick start
@@ -0,0 +1,22 @@
1
+ apply plugin: 'com.android.library'
2
+ apply plugin: 'org.jetbrains.kotlin.android'
3
+
4
+ group = 'expo.modules.advenue'
5
+ version = '0.0.0'
6
+
7
+ android {
8
+ namespace = 'expo.modules.advenue'
9
+ compileSdk = 35
10
+ defaultConfig {
11
+ minSdk = 24
12
+ }
13
+ }
14
+
15
+ dependencies {
16
+ implementation 'com.android.installreferrer:installreferrer:2.2'
17
+ implementation("com.google.android.gms:play-services-ads-identifier:18.0.1")
18
+ // App Set ID — non-advertising, developer-scoped id for first-party fraud/dedup (not ads).
19
+ implementation("com.google.android.gms:play-services-appset:16.0.2")
20
+ // Play Integrity Standard API — device attestation (G3.1n).
21
+ implementation("com.google.android.play:integrity:1.4.0")
22
+ }
@@ -0,0 +1,8 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- Android 11+ package visibility for the Meta Install Referrer providers. -->
3
+ <queries>
4
+ <package android:name="com.facebook.katana" />
5
+ <package android:name="com.instagram.android" />
6
+ <package android:name="com.facebook.lite" />
7
+ </queries>
8
+ </manifest>
@@ -0,0 +1,354 @@
1
+ package expo.modules.advenue
2
+
3
+ import android.content.pm.ApplicationInfo
4
+ import com.android.installreferrer.api.InstallReferrerClient
5
+ import com.android.installreferrer.api.InstallReferrerStateListener
6
+ import com.google.android.gms.ads.identifier.AdvertisingIdClient
7
+ import com.google.android.gms.appset.AppSet
8
+ import com.google.android.play.core.integrity.IntegrityManagerFactory
9
+ import com.google.android.play.core.integrity.StandardIntegrityManager
10
+ import com.google.android.play.core.integrity.StandardIntegrityManager.PrepareIntegrityTokenRequest
11
+ import com.google.android.play.core.integrity.StandardIntegrityManager.StandardIntegrityTokenRequest
12
+ import expo.modules.kotlin.modules.Module
13
+ import expo.modules.kotlin.modules.ModuleDefinition
14
+ import expo.modules.kotlin.Promise
15
+
16
+ // Advenue Android native module. Fetches the Google Play Install Referrer; the
17
+ // referrer string is parsed in TS (@advenue/install-referrer).
18
+ class AdvenueAndroidModule : Module() {
19
+ // Standard Integrity token provider, warmed once at module init. Null when
20
+ // Play Integrity is unavailable (side-loaded APK, emulator, no Play Services).
21
+ // Warming is best-effort — a failure leaves this null and getIntegrityToken
22
+ // will reject with a clear error.
23
+ private var integrityProvider: StandardIntegrityManager.StandardIntegrityTokenProvider? = null
24
+
25
+ override fun definition() = ModuleDefinition {
26
+ Name("AdvenueAndroid")
27
+
28
+ // Warm the Standard Integrity token provider when the module is created.
29
+ // The Google Cloud project number is read from the application's <meta-data>
30
+ // tag in AndroidManifest.xml:
31
+ //
32
+ // <meta-data
33
+ // android:name="io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER"
34
+ // android:value="<your-GCP-project-number>" />
35
+ //
36
+ // This is the numeric project ID shown in the Google Cloud Console URL:
37
+ // https://console.cloud.google.com/home/dashboard?project=<project-id>
38
+ // The number itself lives in Settings → Project info → Project number.
39
+ OnCreate {
40
+ val ctx = appContext.reactContext?.applicationContext ?: return@OnCreate
41
+ try {
42
+ val ai: ApplicationInfo = ctx.packageManager.getApplicationInfo(
43
+ ctx.packageName,
44
+ android.content.pm.PackageManager.GET_META_DATA,
45
+ )
46
+ // GCP project numbers are large longs (12+ digits). Android meta-data
47
+ // stores them as String when declared as android:value="123..." (no type
48
+ // suffix). We try getLong() first (works when stored as an integer
49
+ // resource), then fall back to getString() + toLong() for string values.
50
+ val metaBundle = ai.metaData
51
+ val cloudProjectNumber: Long = metaBundle?.getLong(
52
+ "io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER", 0L
53
+ )?.takeIf { it != 0L }
54
+ ?: metaBundle?.getString("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER")
55
+ ?.toLongOrNull()
56
+ ?: 0L
57
+ if (cloudProjectNumber == 0L) {
58
+ // Project number not configured — attestation will be unavailable.
59
+ // Callers can still call getIntegrityToken(); it will reject with ECONFIG.
60
+ return@OnCreate
61
+ }
62
+ val manager = IntegrityManagerFactory.createStandard(ctx)
63
+ manager.prepareIntegrityToken(
64
+ PrepareIntegrityTokenRequest.builder()
65
+ .setCloudProjectNumber(cloudProjectNumber)
66
+ .build()
67
+ )
68
+ .addOnSuccessListener { provider -> integrityProvider = provider }
69
+ .addOnFailureListener { /* provider stays null — getIntegrityToken will reject */ }
70
+ } catch (_: Exception) {
71
+ // Play Integrity unavailable (no Play Services, side-loaded build, etc.)
72
+ }
73
+ }
74
+
75
+ AsyncFunction("getInstallReferrer") { promise: Promise ->
76
+ val context = appContext.reactContext ?: run {
77
+ promise.reject("ECTX", "No React context", null)
78
+ return@AsyncFunction
79
+ }
80
+ val client = InstallReferrerClient.newBuilder(context).build()
81
+ // The Play Install Referrer callback can fire setup-finished, disconnect, or
82
+ // never respond (offline / unavailable). Settle the promise EXACTLY once and
83
+ // release the service binding on every exit path — otherwise a disconnect or
84
+ // a hung connection leaks the binding and hangs trackInstall forever.
85
+ val settled = java.util.concurrent.atomic.AtomicBoolean(false)
86
+ val handler = android.os.Handler(android.os.Looper.getMainLooper())
87
+ fun release() {
88
+ try { client.endConnection() } catch (_: Exception) {}
89
+ }
90
+ // Watchdog: if the service never calls back, resolve null (referrer is
91
+ // best-effort enrichment) so the JS promise always settles.
92
+ handler.postDelayed({
93
+ if (settled.compareAndSet(false, true)) {
94
+ release()
95
+ promise.resolve(null)
96
+ }
97
+ }, 5_000)
98
+
99
+ client.startConnection(object : InstallReferrerStateListener {
100
+ override fun onInstallReferrerSetupFinished(responseCode: Int) {
101
+ if (!settled.compareAndSet(false, true)) return
102
+ handler.removeCallbacksAndMessages(null)
103
+ try {
104
+ if (responseCode == InstallReferrerClient.InstallReferrerResponse.OK) {
105
+ val response = client.installReferrer
106
+ promise.resolve(mapOf(
107
+ "referrer" to response.installReferrer,
108
+ "clickTimestamp" to response.referrerClickTimestampSeconds,
109
+ "installTimestamp" to response.installBeginTimestampSeconds,
110
+ // Server-clock getters (G1): Play returns 0 when the install is
111
+ // unverified. The JS layer forwards these as server-trusted keys
112
+ // only when non-zero. Do NOT confuse with the device-clock
113
+ // *TimestampSeconds getters above.
114
+ "clickServerTimestamp" to response.referrerClickTimestampServerSeconds,
115
+ "installServerTimestamp" to response.installBeginTimestampServerSeconds,
116
+ "googlePlayInstant" to response.googlePlayInstantParam,
117
+ ))
118
+ } else {
119
+ promise.resolve(null)
120
+ }
121
+ } catch (e: Exception) {
122
+ promise.reject("EREFERRER", e.message, e)
123
+ } finally {
124
+ release()
125
+ }
126
+ }
127
+
128
+ override fun onInstallReferrerServiceDisconnected() {
129
+ // Service dropped before setup finished (e.g. low memory). Release the
130
+ // binding and settle as best-effort null instead of hanging forever.
131
+ if (settled.compareAndSet(false, true)) {
132
+ handler.removeCallbacksAndMessages(null)
133
+ release()
134
+ promise.resolve(null)
135
+ }
136
+ }
137
+ })
138
+ }
139
+
140
+ // Meta (Facebook/Instagram) install referrer via content provider. The
141
+ // payload is parsed in TS and the encrypted blob decrypted server-side
142
+ // (@advenue/install-referrer). Resolves null when no Meta app is installed.
143
+ AsyncFunction("getMetaInstallReferrer") { promise: Promise ->
144
+ val context = appContext.reactContext ?: run {
145
+ promise.reject("ECTX", "No React context", null)
146
+ return@AsyncFunction
147
+ }
148
+ val providers = listOf("com.facebook.katana", "com.instagram.android", "com.facebook.lite")
149
+ try {
150
+ for (pkg in providers) {
151
+ val uri = android.net.Uri.parse(
152
+ "content://$pkg.provider.InstallReferrerProvider/${context.packageName}"
153
+ )
154
+ val cursor = try {
155
+ context.contentResolver.query(
156
+ uri,
157
+ arrayOf("install_referrer", "is_ct", "actual_timestamp"),
158
+ null,
159
+ null,
160
+ null,
161
+ )
162
+ } catch (_: Exception) {
163
+ null
164
+ } ?: continue
165
+ cursor.use {
166
+ if (it.moveToFirst()) {
167
+ val referrerIdx = it.getColumnIndex("install_referrer")
168
+ val ctIdx = it.getColumnIndex("is_ct")
169
+ val tsIdx = it.getColumnIndex("actual_timestamp")
170
+ if (referrerIdx >= 0) {
171
+ promise.resolve(mapOf(
172
+ "installReferrer" to it.getString(referrerIdx),
173
+ "isClickThrough" to (ctIdx >= 0 && it.getInt(ctIdx) == 1),
174
+ "actualTimestamp" to (if (tsIdx >= 0) it.getLong(tsIdx) else 0L),
175
+ "provider" to pkg,
176
+ ))
177
+ return@AsyncFunction
178
+ }
179
+ }
180
+ }
181
+ }
182
+ promise.resolve(null)
183
+ } catch (e: Exception) {
184
+ promise.reject("EMETAREF", e.message, e)
185
+ }
186
+ }
187
+
188
+ // Google Advertising ID (GAID) + limit-ad-tracking flag. The blocking
189
+ // Play Services call is run on a background thread.
190
+ AsyncFunction("getAdvertisingId") { promise: Promise ->
191
+ Thread {
192
+ try {
193
+ val ctx = appContext.reactContext?.applicationContext
194
+ if (ctx == null) { promise.resolve(null); return@Thread }
195
+ val info = AdvertisingIdClient.getAdvertisingIdInfo(ctx)
196
+ val id = info.id
197
+ val zeroGaid = "00000000-0000-0000-0000-000000000000"
198
+ if (id.isNullOrEmpty() || id == zeroGaid) promise.resolve(null)
199
+ else promise.resolve(mapOf("id" to id, "limitAdTracking" to info.isLimitAdTrackingEnabled))
200
+ } catch (e: Exception) {
201
+ promise.resolve(null)
202
+ }
203
+ }.start()
204
+ }
205
+
206
+ // App Set ID — non-advertising, developer-scoped id. First-party fraud/dedup
207
+ // use only; Google forbids using it for ads or measurement.
208
+ AsyncFunction("getAppSetId") { promise: Promise ->
209
+ val ctx = appContext.reactContext?.applicationContext
210
+ if (ctx == null) { promise.resolve(null); return@AsyncFunction }
211
+ AppSet.getClient(ctx).appSetIdInfo
212
+ .addOnSuccessListener { info -> promise.resolve(info.id) }
213
+ .addOnFailureListener { promise.resolve(null) }
214
+ }
215
+
216
+ // Play Integrity Standard API — device attestation (G3.1n).
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.
222
+ //
223
+ // The provider is warmed in OnCreate (above). If it is null — because the
224
+ // manifest meta-data key is absent, Play Integrity is unsupported, or warming
225
+ // failed — this rejects with ECONFIG / EUNSUPPORTED. The JS layer treats any
226
+ // rejection as "no attestation" and omits the token from the install event.
227
+ AsyncFunction("getIntegrityToken") { requestHash: String, promise: Promise ->
228
+ val provider = integrityProvider
229
+ if (provider == null) {
230
+ // Distinguish "project number not set" from "Play Integrity not available".
231
+ val ctx = appContext.reactContext?.applicationContext
232
+ if (ctx != null) {
233
+ val ai = try {
234
+ ctx.packageManager.getApplicationInfo(
235
+ ctx.packageName,
236
+ android.content.pm.PackageManager.GET_META_DATA,
237
+ )
238
+ } catch (_: Exception) { null }
239
+ val mb = ai?.metaData
240
+ val cfgNum: Long = mb?.getLong("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER", 0L)
241
+ ?.takeIf { it != 0L }
242
+ ?: mb?.getString("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER")?.toLongOrNull()
243
+ ?: 0L
244
+ if (cfgNum == 0L) {
245
+ promise.reject(
246
+ "ECONFIG",
247
+ "Play Integrity not configured: add io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER to AndroidManifest.xml meta-data",
248
+ null,
249
+ )
250
+ return@AsyncFunction
251
+ }
252
+ }
253
+ promise.reject(
254
+ "EUNSUPPORTED",
255
+ "Play Integrity token provider not ready (Play Services may be unavailable or warming failed)",
256
+ null,
257
+ )
258
+ return@AsyncFunction
259
+ }
260
+
261
+ val request = StandardIntegrityTokenRequest.builder()
262
+ .setRequestHash(requestHash)
263
+ .build()
264
+
265
+ provider.request(request)
266
+ .addOnSuccessListener { response -> promise.resolve(response.token()) }
267
+ .addOnFailureListener { e ->
268
+ promise.reject("EINTEGRITY", e.message ?: "Play Integrity request failed", e)
269
+ }
270
+ }
271
+
272
+ // Device metadata for Meta CAPI `extinfo` (Task 3n). Synchronous — every
273
+ // API used here is a cheap local read (no network, no blocking I/O beyond
274
+ // a local StatFs stat call). Best-effort per field: a field is simply
275
+ // omitted (not defaulted) when its source is unavailable, so the JS-side
276
+ // fail-safe (collectDeviceInfo) degrades gracefully rather than sending
277
+ // misleading zeros.
278
+ Function("getDeviceInfo") {
279
+ val map = mutableMapOf<String, Any?>()
280
+ map["model"] = android.os.Build.MODEL
281
+ map["locale"] = java.util.Locale.getDefault().toString()
282
+ map["timezone"] = java.util.TimeZone.getDefault().id
283
+ map["cpuCores"] = Runtime.getRuntime().availableProcessors()
284
+
285
+ val ctx = appContext.reactContext?.applicationContext
286
+ if (ctx != null) {
287
+ try {
288
+ val dm = ctx.resources.displayMetrics
289
+ map["screenWidth"] = dm.widthPixels
290
+ map["screenHeight"] = dm.heightPixels
291
+ map["screenDensity"] = dm.density.toDouble()
292
+ } catch (_: Exception) {
293
+ // screen metrics best-effort
294
+ }
295
+ try {
296
+ val statFs = android.os.StatFs(ctx.filesDir.path)
297
+ val blockSize = statFs.blockSizeLong
298
+ val gb = 1024.0 * 1024.0 * 1024.0
299
+ map["totalStorageGb"] = (statFs.blockCountLong * blockSize).toDouble() / gb
300
+ map["freeStorageGb"] = (statFs.availableBlocksLong * blockSize).toDouble() / gb
301
+ } catch (_: Exception) {
302
+ // storage stat best-effort
303
+ }
304
+ try {
305
+ val tm = ctx.getSystemService(android.content.Context.TELEPHONY_SERVICE)
306
+ as? android.telephony.TelephonyManager
307
+ val carrier = tm?.networkOperatorName
308
+ if (!carrier.isNullOrEmpty()) map["carrier"] = carrier
309
+ } catch (_: Exception) {
310
+ // carrier best-effort
311
+ }
312
+ }
313
+ map
314
+ }
315
+
316
+ // Built-in SDK storage (Adjust-style: the app supplies nothing). A
317
+ // dedicated SharedPreferences file keeps SDK keys out of the app's prefs.
318
+ // Wiped on uninstall — Android has no reinstall-durable store, so a
319
+ // reinstall legitimately re-fires the install (backend window dedups).
320
+ Function("storageGetItem") { key: String ->
321
+ sdkPrefs()?.getString(key, null)
322
+ }
323
+ Function("storageSetItem") { key: String, value: String ->
324
+ sdkPrefs()?.edit()?.putString(key, value)?.apply()
325
+ }
326
+ Function("storageRemoveItem") { key: String ->
327
+ sdkPrefs()?.edit()?.remove(key)?.apply()
328
+ }
329
+
330
+ // SSAID (ANDROID_ID): signing-key-scoped, stable across reinstalls, reset
331
+ // only by factory reset. Read by the JS facade ONLY when no usable GAID
332
+ // exists, as a first-party fraud/dedup + reinstall-detection signal (Play
333
+ // policy: never linked to the advertising id, never used for ads).
334
+ Function("getAndroidId") { ->
335
+ try {
336
+ val ctx = appContext.reactContext?.applicationContext
337
+ if (ctx != null) {
338
+ android.provider.Settings.Secure.getString(
339
+ ctx.contentResolver,
340
+ android.provider.Settings.Secure.ANDROID_ID,
341
+ )
342
+ } else {
343
+ null
344
+ }
345
+ } catch (_: Exception) {
346
+ null // best-effort: a missing SSAID never blocks anything
347
+ }
348
+ }
349
+ }
350
+
351
+ private fun sdkPrefs(): android.content.SharedPreferences? =
352
+ appContext.reactContext?.applicationContext
353
+ ?.getSharedPreferences("io.advenue.sdk", android.content.Context.MODE_PRIVATE)
354
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  import { StorageAdapter, SecureStorageAdapter, DeepLink, AdvenueConfig, AdvenueClient, TrackOptions, Consent, DeviceInfo } from '@advenue/sdk-core';
2
2
  export { AdvenueConfig, Consent, DeepLink, TrackOptions } from '@advenue/sdk-core';
3
- import { AdvenueIosNativeModule, TrackingAuthorizationStatus } from '@advenue/sdk-ios';
4
- export { TrackingAuthorizationStatus } from '@advenue/sdk-ios';
5
- import { AdvenueAndroidNativeModule } from '@advenue/sdk-android';
6
3
 
7
4
  /**
8
5
  * Minimal structural type for the bits of `react-native-mmkv`'s MMKV instance
@@ -17,6 +14,172 @@ interface MMKVLike {
17
14
  /** Wraps an MMKV instance as an Advenue StorageAdapter (synchronous, fast). */
18
15
  declare function createMMKVStorage(mmkv: MMKVLike): StorageAdapter;
19
16
 
17
+ interface InstallReferrerResult {
18
+ referrer: string;
19
+ /** Device-clock: referrerClickTimestampSeconds. */
20
+ clickTimestamp: number;
21
+ /** Device-clock: installBeginTimestampSeconds. */
22
+ installTimestamp: number;
23
+ /**
24
+ * Server-clock: referrerClickTimestampServerSeconds (G1 fraud trust —
25
+ * referrerTrust:'server'). 0 = unverified; absent on older native builds.
26
+ */
27
+ clickServerTimestamp?: number;
28
+ /** Server-clock: installBeginTimestampServerSeconds. 0 = unverified. */
29
+ installServerTimestamp?: number;
30
+ googlePlayInstant: boolean;
31
+ }
32
+ interface MetaInstallReferrerResult {
33
+ installReferrer: string;
34
+ isClickThrough: boolean;
35
+ actualTimestamp: number;
36
+ /** Which Meta app served the referrer (e.g. com.facebook.katana). */
37
+ provider: string;
38
+ }
39
+ /**
40
+ * Device metadata for Meta CAPI `extinfo` (Task 3n). Structurally mirrors
41
+ * `DeviceInfo` from `@advenue/shared` — kept as a local subset type (rather
42
+ * than importing that package) so this native-module-only package stays
43
+ * dependency-free; the RN facade's `collectDeviceInfo()` merges it into the
44
+ * real `DeviceInfo` shape.
45
+ */
46
+ interface AdvenueDeviceInfo {
47
+ model?: string;
48
+ locale?: string;
49
+ carrier?: string;
50
+ timezone?: string;
51
+ screenWidth?: number;
52
+ screenHeight?: number;
53
+ screenDensity?: number;
54
+ cpuCores?: number;
55
+ totalStorageGb?: number;
56
+ freeStorageGb?: number;
57
+ }
58
+ interface AdvenueAndroidNativeModule {
59
+ /** Raw Play Install Referrer; parse with @advenue/install-referrer. */
60
+ getInstallReferrer(): Promise<InstallReferrerResult | null>;
61
+ /**
62
+ * Meta install referrer (null when no Facebook/Instagram app is installed);
63
+ * parse with parseMetaInstallReferrer from @advenue/install-referrer.
64
+ */
65
+ getMetaInstallReferrer(): Promise<MetaInstallReferrerResult | null>;
66
+ /** Google Advertising ID (GAID) + limit-ad-tracking flag; null when unavailable. */
67
+ getAdvertisingId(): Promise<{
68
+ id: string;
69
+ limitAdTracking: boolean;
70
+ } | null>;
71
+ /** App Set ID — non-advertising, developer-scoped fallback id; null when unavailable. */
72
+ getAppSetId(): Promise<string | null>;
73
+ /**
74
+ * Play Integrity Standard API token (G3.1n).
75
+ *
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.
79
+ *
80
+ * Rejects when:
81
+ * - ECONFIG: `io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER` meta-data is absent.
82
+ * - EUNSUPPORTED: Play Services unavailable or provider warming failed.
83
+ * - EINTEGRITY: Play returned an API-level error (network, quota, etc.).
84
+ *
85
+ * The JS bridge treats any rejection as "no attestation" and omits the token
86
+ * from the install event — rejection must never block the install track call.
87
+ */
88
+ getIntegrityToken(requestHash: string): Promise<string>;
89
+ /**
90
+ * Device metadata (model, locale, screen, cpu, storage, timezone) used to
91
+ * populate Meta CAPI `extinfo` (Task 3n). Synchronous, best-effort — the JS
92
+ * facade (`collectDeviceInfo`) treats a thrown error as "unavailable" and
93
+ * degrades to `{}` rather than blocking anything.
94
+ */
95
+ getDeviceInfo(): AdvenueDeviceInfo;
96
+ /**
97
+ * Built-in SDK storage (SharedPreferences `io.advenue.sdk`, MODE_PRIVATE) —
98
+ * the durable offline buffer + SDK state, so the app never supplies storage.
99
+ * Optional: absent on native builds predating this capability; the RN facade
100
+ * feature-detects and falls back to in-memory buffering. (No secure variant:
101
+ * Android has no reinstall-durable store — a reinstall legitimately re-fires
102
+ * the install, which the backend per-device window deduplicates.)
103
+ */
104
+ storageGetItem?(key: string): string | null;
105
+ storageSetItem?(key: string, value: string): void;
106
+ storageRemoveItem?(key: string): void;
107
+ /**
108
+ * Android SSAID (Settings.Secure.ANDROID_ID) — signing-key-scoped, stable
109
+ * across reinstalls. The RN facade reads it ONLY when no usable GAID exists
110
+ * (Adjust-style fallback) and it is used strictly as a first-party
111
+ * fraud/dedup + reinstall-detection signal, never for ad attribution (Play
112
+ * policy). Optional: absent on older native builds; feature-detected.
113
+ */
114
+ getAndroidId?(): string | null;
115
+ }
116
+ type TrackingAuthorizationStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
117
+ interface AdvenueIosNativeModule {
118
+ registerAppForAttribution(): void;
119
+ /**
120
+ * Conversion value: fine 0–63, coarse 'low'|'medium'|'high'. Updates the
121
+ * SKAN 4.0 postback, and on iOS 17.4+ the AdAttributionKit postback too.
122
+ */
123
+ updateConversionValue(fineValue: number, coarse: string, lockWindow: boolean): Promise<void>;
124
+ /** Apple Search Ads attribution token (resolve server-side). */
125
+ getAttributionToken(): Promise<string | null>;
126
+ /**
127
+ * Shows the ATT consent prompt; requires NSUserTrackingUsageDescription in
128
+ * the app's Info.plist. Resolves with the resulting status.
129
+ */
130
+ requestTrackingAuthorization(): Promise<TrackingAuthorizationStatus>;
131
+ getTrackingAuthorizationStatus(): TrackingAuthorizationStatus;
132
+ getAdvertisingId(): Promise<string | null>;
133
+ /** Vendor-scoped device id (IDFV) — available without ATT; a fallback anchor. */
134
+ getIdentifierForVendor(): string | null;
135
+ /** Clipboard match token (advmatch:<id>) for deterministic deferred deep linking; null if absent. */
136
+ getPasteboardMatchToken(): string | null;
137
+ /**
138
+ * G3.2n — App Attest. Generates (or reuses a keychain-cached) DCAppAttest
139
+ * key, computes clientDataHash = SHA-256(challenge), and calls
140
+ * DCAppAttestService.attestKey. Resolves { keyId, attestationObject } where
141
+ * attestationObject is a base64-encoded CBOR attestation for the server.
142
+ * Rejects with code EUNSUPPORTED on simulators (isSupported == false).
143
+ */
144
+ attestKey(challenge: string): Promise<{
145
+ keyId: string;
146
+ attestationObject: string;
147
+ }>;
148
+ /**
149
+ * G3.3n — DeviceCheck. Calls DCDevice.current.generateToken to obtain the
150
+ * opaque base64 token that identifies this physical device to the Apple
151
+ * DeviceCheck API. The server queries/updates the persistent 2-bit per-device
152
+ * flag to detect reinstall abuse. Rejects with code EUNSUPPORTED on devices
153
+ * where DCDevice.current.isSupported is false (simulator, Catalyst, etc.).
154
+ */
155
+ getDeviceCheckToken(): Promise<string>;
156
+ /**
157
+ * Device metadata (model, locale, screen, cpu, storage, timezone) used to
158
+ * populate Meta CAPI `extinfo` (Task 3n). Synchronous, best-effort — the JS
159
+ * facade (`collectDeviceInfo`) treats a thrown error as "unavailable" and
160
+ * degrades to `{}` rather than blocking anything.
161
+ */
162
+ getDeviceInfo(): AdvenueDeviceInfo;
163
+ /**
164
+ * Built-in SDK storage (UserDefaults suite `io.advenue.sdk`) — the durable
165
+ * offline buffer + SDK state, so the app never supplies storage. Optional:
166
+ * absent on native builds predating this capability; the RN facade
167
+ * feature-detects and falls back to in-memory buffering.
168
+ */
169
+ storageGetItem?(key: string): string | null;
170
+ storageSetItem?(key: string, value: string): void;
171
+ storageRemoveItem?(key: string): void;
172
+ /**
173
+ * Built-in secure storage (Keychain, kSecAttrAccessibleAfterFirstUnlock,
174
+ * ThisDeviceOnly) — survives app reinstall, which is what makes the device
175
+ * identity and the install guard reinstall-resilient (Adjust-style).
176
+ * Optional: absent on older native builds; feature-detected.
177
+ */
178
+ secureGetItem?(key: string): string | null;
179
+ secureSetItem?(key: string, value: string): void;
180
+ secureRemoveItem?(key: string): void;
181
+ }
182
+
20
183
  /**
21
184
  * Minimal structural type for the bits of `expo-secure-store` we use. Declared
22
185
  * locally so this package needs no expo types at compile time — the app passes
@@ -40,9 +203,11 @@ declare function createSecureStore(secureStore: SecureStoreLike): SecureStorageA
40
203
  declare function parseDirectLink(url: string): DeepLink;
41
204
 
42
205
  /**
43
- * Lazy access to the optional native modules. `requireNativeModule` throws at
44
- * import time when the native side isn't installed (Expo Go, web, missing
45
- * pods), so resolution happens inside try/catch — never at module scope.
206
+ * Lazy access to the native modules that ship inside this package (ios/ and
207
+ * android/, registered via expo-module.config.json). `requireNativeModule`
208
+ * throws when the native side isn't built (Expo Go, web, missing pods), so
209
+ * resolution happens inside try/catch — never at module scope.
210
+ * expo-modules-core is resolved from the consuming app's Expo install.
46
211
  */
47
212
  declare function getIosNative(): AdvenueIosNativeModule | null;
48
213
  declare function getAndroidNative(): AdvenueAndroidNativeModule | null;
@@ -99,7 +264,7 @@ interface RNAdvenueConfig extends Omit<AdvenueConfig, 'storage' | 'platform'> {
99
264
  /**
100
265
  * G3.1n — Play Integrity (Android) / App Attest (iOS) attestation.
101
266
  *
102
- * Your app's Advenue appId (the UUID shown in the dashboard). When set on
267
+ * Your app's Advenue appId (the UUID in Settings → API Keys). When set on
103
268
  * Android, the automatic install obtains a Play Integrity Standard token and
104
269
  * attaches it to the install event. The requestHash is computed as
105
270
  * `sha256Hex("<appId>:<deviceId>")`, matching the server's verification
@@ -178,4 +343,4 @@ declare const Advenue: {
178
343
  */
179
344
  declare function trackInstall(): Promise<void>;
180
345
 
181
- export { Advenue, type MMKVLike, type RNAdvenueConfig, type SecureStoreLike, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveClipboardMatch, trackInstall };
346
+ export { Advenue, type AdvenueAndroidNativeModule, type AdvenueDeviceInfo, type AdvenueIosNativeModule, type InstallReferrerResult, type MMKVLike, type MetaInstallReferrerResult, type RNAdvenueConfig, type SecureStoreLike, type TrackingAuthorizationStatus, applyAppState, collectDeviceInfo, createMMKVStorage, createSecureStore, getAndroidNative, getIosNative, parseDirectLink, resolveAdvertisingId, resolveClipboardMatch, trackInstall };
package/dist/index.js CHANGED
@@ -174,10 +174,10 @@ function parseDirectLink(url) {
174
174
  const path = parsed.pathname.replace(/^\/+/, "");
175
175
  const isHttp = parsed.protocol === "http:" || parsed.protocol === "https:";
176
176
  deepLinkValue = isHttp ? path : [parsed.host, path].filter(Boolean).join("/");
177
- for (const [k, v] of parsed.searchParams.entries()) {
177
+ parsed.searchParams.forEach((v, k) => {
178
178
  params ??= {};
179
179
  params[k] = v;
180
- }
180
+ });
181
181
  } catch {
182
182
  deepLinkValue = url;
183
183
  }
@@ -217,14 +217,16 @@ function createMMKVStorage(mmkv) {
217
217
  // src/native.ts
218
218
  function getIosNative() {
219
219
  try {
220
- return __require("@advenue/sdk-ios").AdvenueIos;
220
+ const core = __require("expo-modules-core");
221
+ return core.requireNativeModule("AdvenueIos");
221
222
  } catch {
222
223
  return null;
223
224
  }
224
225
  }
225
226
  function getAndroidNative() {
226
227
  try {
227
- return __require("@advenue/sdk-android").AdvenueAndroid;
228
+ const core = __require("expo-modules-core");
229
+ return core.requireNativeModule("AdvenueAndroid");
228
230
  } catch {
229
231
  return null;
230
232
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "platforms": ["apple", "android"],
3
+ "apple": {
4
+ "modules": ["AdvenueIosModule"]
5
+ },
6
+ "android": {
7
+ "modules": ["expo.modules.advenue.AdvenueAndroidModule"]
8
+ }
9
+ }
@@ -0,0 +1,20 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'AdvenueIos'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.author = 'Advenue'
11
+ s.homepage = 'https://github.com/broverse/advenue'
12
+ s.platforms = { :ios => '15.1' }
13
+ s.swift_version = '5.4'
14
+ s.source = { git: 'https://github.com/broverse/advenue.git' }
15
+ s.static_framework = true
16
+
17
+ s.dependency 'ExpoModulesCore'
18
+
19
+ s.source_files = '**/*.{h,m,swift}'
20
+ end