@advenue/react-native 0.3.1 → 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 +214 -29
- package/dist/index.js +258 -141
- package/expo-module.config.json +9 -0
- package/ios/AdvenueIos.podspec +20 -0
- package/ios/AdvenueIosModule.swift +461 -0
- package/package.json +9 -19
- package/plugin/index.js +4 -8
- package/plugin/links.js +23 -1
- package/src/deep-links.ts +5 -2
- package/src/index.ts +413 -233
- package/src/native-storage.ts +63 -0
- package/src/native-types.ts +169 -0
- package/src/native.ts +27 -8
- package/src/secure-store.ts +7 -0
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
|
+
}
|