@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 +2 -2
- package/android/build.gradle +22 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +354 -0
- package/dist/index.d.ts +173 -8
- package/dist/index.js +6 -4
- package/expo-module.config.json +9 -0
- package/ios/AdvenueIos.podspec +20 -0
- package/ios/AdvenueIosModule.swift +461 -0
- package/package.json +8 -10
- package/src/deep-links.ts +5 -2
- package/src/index.ts +10 -3
- package/src/native-types.ts +169 -0
- package/src/native.ts +14 -8
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ engine.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npx expo install @advenue/react-native
|
|
11
|
+
npx expo install @advenue/react-native \
|
|
12
12
|
react-native-mmkv expo-secure-store
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
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
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
+
const core = __require("expo-modules-core");
|
|
229
|
+
return core.requireNativeModule("AdvenueAndroid");
|
|
228
230
|
} catch {
|
|
229
231
|
return null;
|
|
230
232
|
}
|
|
@@ -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
|