@advenue/react-native 0.9.0 → 1.0.1
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 +8 -7
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +117 -368
- package/android/src/main/kotlin/io/advenue/Advenue.kt +574 -0
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +141 -0
- package/android/src/main/kotlin/io/advenue/core/Backoff.kt +28 -0
- package/android/src/main/kotlin/io/advenue/core/ClientEvent.kt +136 -0
- package/android/src/main/kotlin/io/advenue/core/CommandPipe.kt +193 -0
- package/android/src/main/kotlin/io/advenue/core/Consent.kt +52 -0
- package/android/src/main/kotlin/io/advenue/core/Contracts.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/Conversion.kt +69 -0
- package/android/src/main/kotlin/io/advenue/core/Engine.kt +436 -0
- package/android/src/main/kotlin/io/advenue/core/EventQueue.kt +89 -0
- package/android/src/main/kotlin/io/advenue/core/HmacSigner.kt +31 -0
- package/android/src/main/kotlin/io/advenue/core/InstallReferrer.kt +146 -0
- package/android/src/main/kotlin/io/advenue/core/Json.kt +272 -0
- package/android/src/main/kotlin/io/advenue/core/Limits.kt +50 -0
- package/android/src/main/kotlin/io/advenue/core/MetaReferrer.kt +80 -0
- package/android/src/main/kotlin/io/advenue/core/PiiScrub.kt +77 -0
- package/android/src/main/kotlin/io/advenue/core/SessionTracker.kt +189 -0
- package/android/src/main/kotlin/io/advenue/core/SystemServices.kt +58 -0
- package/android/src/main/kotlin/io/advenue/core/Tcf.kt +48 -0
- package/android/src/main/kotlin/io/advenue/core/Time.kt +56 -0
- package/android/src/main/kotlin/io/advenue/platform/Collectors.kt +163 -0
- package/android/src/main/kotlin/io/advenue/platform/CompositeStore.kt +33 -0
- package/android/src/main/kotlin/io/advenue/platform/ConversionFetcher.kt +68 -0
- package/android/src/main/kotlin/io/advenue/platform/ForegroundTracker.kt +90 -0
- package/android/src/main/kotlin/io/advenue/platform/HttpUrlTransport.kt +101 -0
- package/android/src/main/kotlin/io/advenue/platform/Identity.kt +79 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallEnrichment.kt +197 -0
- package/android/src/main/kotlin/io/advenue/platform/InstallScopedStore.kt +96 -0
- package/android/src/main/kotlin/io/advenue/platform/LifecycleBridge.kt +72 -0
- package/android/src/main/kotlin/io/advenue/platform/PreferencesStore.kt +32 -0
- package/android/src/main/kotlin/io/advenue/plugin/Contracts.kt +67 -0
- package/android/src/main/kotlin/io/advenue/plugin/FirebaseAppInstanceIdSource.kt +73 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayAdvertisingIdSource.kt +47 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayInstallReferrerSource.kt +85 -0
- package/android/src/main/kotlin/io/advenue/plugin/PlayIntegritySource.kt +86 -0
- package/android/src/main/kotlin/io/advenue/plugin/PluginRegistry.kt +61 -0
- package/dist/index.cjs +188 -675
- package/dist/index.d.cts +203 -336
- package/dist/index.d.ts +203 -336
- package/dist/index.js +187 -681
- package/ios/AdvenueIosModule.swift +143 -446
- package/ios/vendor/Advenue/Advenue.swift +632 -0
- package/ios/vendor/Advenue/AdvenueConfig.swift +89 -0
- package/ios/vendor/AdvenueCore/AdvenueValue.swift +103 -0
- package/ios/vendor/AdvenueCore/Attestation.swift +52 -0
- package/ios/vendor/AdvenueCore/Backoff.swift +28 -0
- package/ios/vendor/AdvenueCore/ClientEvent.swift +154 -0
- package/ios/vendor/AdvenueCore/Consent.swift +40 -0
- package/ios/vendor/AdvenueCore/Contracts.swift +59 -0
- package/ios/vendor/AdvenueCore/Conversion.swift +74 -0
- package/ios/vendor/AdvenueCore/ConversionValue.swift +217 -0
- package/ios/vendor/AdvenueCore/Engine.swift +607 -0
- package/ios/vendor/AdvenueCore/EventQueue.swift +89 -0
- package/ios/vendor/AdvenueCore/Limits.swift +41 -0
- package/ios/vendor/AdvenueCore/PIIScrub.swift +102 -0
- package/ios/vendor/AdvenueCore/SessionTracker.swift +139 -0
- package/ios/vendor/AdvenueCore/SkanConfig.swift +76 -0
- package/ios/vendor/AdvenueCore/SkanReporter.swift +25 -0
- package/ios/vendor/AdvenueCore/SkanState.swift +258 -0
- package/ios/vendor/AdvenueCore/Tcf.swift +48 -0
- package/ios/vendor/AdvenueCore/Transport.swift +14 -0
- package/ios/vendor/AdvenueFirebase/FirebaseAppInstanceId.swift +33 -0
- package/ios/vendor/AdvenuePlatform/AdvertisingIdentity.swift +87 -0
- package/ios/vendor/AdvenuePlatform/CacheFileStore.swift +90 -0
- package/ios/vendor/AdvenuePlatform/ChallengeFetcher.swift +42 -0
- package/ios/vendor/AdvenuePlatform/ConversionFetcher.swift +63 -0
- package/ios/vendor/AdvenuePlatform/CryptoKitSigner.swift +19 -0
- package/ios/vendor/AdvenuePlatform/DeviceCheckAttestation.swift +91 -0
- package/ios/vendor/AdvenuePlatform/DeviceInfo.swift +77 -0
- package/ios/vendor/AdvenuePlatform/ForegroundTracker.swift +55 -0
- package/ios/vendor/AdvenuePlatform/HttpTransport.swift +96 -0
- package/ios/vendor/AdvenuePlatform/Identity.swift +52 -0
- package/ios/vendor/AdvenuePlatform/InstallEnrichment.swift +155 -0
- package/ios/vendor/AdvenuePlatform/KeychainStore.swift +95 -0
- package/ios/vendor/AdvenuePlatform/SearchAdsToken.swift +76 -0
- package/ios/vendor/AdvenuePlatform/SkanConfigFetcher.swift +70 -0
- package/ios/vendor/AdvenuePlatform/StoreKitSkanReporter.swift +140 -0
- package/ios/vendor/AdvenuePlatform/SystemServices.swift +55 -0
- package/ios/vendor/AdvenuePlatform/TcfReader.swift +18 -0
- package/ios/vendor/AdvenuePlatform/UserDefaultsStore.swift +26 -0
- package/package.json +13 -15
- package/scripts/check-dist.mjs +17 -0
- package/scripts/check-vendored-swift.mjs +148 -0
- package/scripts/vendor-natives.mjs +149 -0
- package/scripts/vendor-natives.test.mjs +112 -0
- package/src/deep-links.ts +19 -1
- package/src/index.ts +273 -830
- package/src/native-types.ts +73 -181
- package/src/native.ts +0 -23
- package/src/types.ts +86 -0
- package/src/aem.ts +0 -33
- package/src/mmkv-storage.ts +0 -21
- package/src/native-storage.ts +0 -63
- package/src/secure-store.ts +0 -26
package/README.md
CHANGED
|
@@ -8,8 +8,7 @@ engine.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npx expo install @advenue/react-native
|
|
12
|
-
react-native-mmkv expo-secure-store
|
|
11
|
+
npx expo install @advenue/react-native
|
|
13
12
|
```
|
|
14
13
|
|
|
15
14
|
The iOS and Android Expo native modules ship inside this package (`ios/`, `android/`) and are built by
|
|
@@ -19,20 +18,22 @@ your app's own Gradle/Xcode build — no manual linking.
|
|
|
19
18
|
|
|
20
19
|
```ts
|
|
21
20
|
import { Advenue } from '@advenue/react-native';
|
|
22
|
-
import { MMKV } from 'react-native-mmkv';
|
|
23
|
-
import * as SecureStore from 'expo-secure-store';
|
|
24
21
|
|
|
25
22
|
await Advenue.initialize({
|
|
26
23
|
apiKey: 'apk_live_...', // from your Advenue dashboard
|
|
27
|
-
platform: 'ios', // or 'android'
|
|
28
|
-
mmkv: new MMKV(), // offline event buffering
|
|
29
|
-
secureStore: SecureStore, // durable device identity
|
|
30
24
|
// endpoint: 'https://ingest.your-domain.com', // self-hosted only
|
|
31
25
|
});
|
|
32
26
|
|
|
33
27
|
Advenue.track('signup_completed', { plan: 'pro' });
|
|
34
28
|
```
|
|
35
29
|
|
|
30
|
+
Storage and platform detection are built in. `track()` properties never become
|
|
31
|
+
financial revenue; connect a verified S2S store source for revenue/LTV/ROAS.
|
|
32
|
+
|
|
33
|
+
On iOS, the SDK fetches the versioned SKAN config and persists monotonic state.
|
|
34
|
+
Use `Advenue.recordSkanRevenue({ amount: '9.99', currency: 'USD' })` only to
|
|
35
|
+
advance Apple's local conversion value. It does not record Advenue revenue.
|
|
36
|
+
|
|
36
37
|
> The `adv_` event-name prefix (e.g. `adv_meta_aem`) is reserved for
|
|
37
38
|
> Advenue-internal signals — don't name your own custom events with it. Such
|
|
38
39
|
> events are still accepted by `track()` (the pipeline relies on them) but are
|
|
@@ -1,398 +1,147 @@
|
|
|
1
1
|
package expo.modules.advenue
|
|
2
2
|
|
|
3
|
-
import
|
|
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
|
|
3
|
+
import expo.modules.kotlin.Promise
|
|
12
4
|
import expo.modules.kotlin.modules.Module
|
|
13
5
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
import io.advenue.Advenue
|
|
7
|
+
import io.advenue.AdvenueConfig
|
|
8
|
+
import io.advenue.platform.DEFAULT_ENDPOINT
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The React Native bridge to the Advenue Android SDK.
|
|
12
|
+
*
|
|
13
|
+
* It used to be a toolbox: the Play Install Referrer, the Meta install
|
|
14
|
+
* referrer, the GAID, the App Set ID, Play Integrity, the SSAID, TCF, device
|
|
15
|
+
* info and two storage backends, each its own function, because a TypeScript
|
|
16
|
+
* core drove them one at a time. That core is retired and none of them had a
|
|
17
|
+
* caller left.
|
|
18
|
+
*
|
|
19
|
+
* They are deleted rather than left in place. One of them was actively harmful:
|
|
20
|
+
* an `OnCreate` block warmed a Play Integrity token provider for nobody, and
|
|
21
|
+
* the SDK's own `PlayIntegritySource` warms another — two `prepareIntegrityToken`
|
|
22
|
+
* calls per launch, against a quota, for one token. The rest were merely dead,
|
|
23
|
+
* which in an SDK means code someone later assumes is live.
|
|
24
|
+
*
|
|
25
|
+
* Everything they did, the vendored SDK now does through its own plugins.
|
|
26
|
+
*/
|
|
18
27
|
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
28
|
override fun definition() = ModuleDefinition {
|
|
26
29
|
Name("AdvenueAndroid")
|
|
27
30
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
}
|
|
31
|
+
// Diagnostics travel back to JavaScript. `onError` is a JS closure and
|
|
32
|
+
// cannot cross the channel, so what crosses is the event: a stable context
|
|
33
|
+
// identifier and a message, never PII. Without this the SDK's own swallowed
|
|
34
|
+
// failures reach nobody, which is what happened between the inversion and
|
|
35
|
+
// this commit.
|
|
36
|
+
Events("onAdvenueError")
|
|
37
|
+
|
|
38
|
+
Function("bridgeInitialize") { options: Map<String, Any?> ->
|
|
39
|
+
val application = appContext.reactContext?.applicationContext as? android.app.Application
|
|
40
|
+
val apiKey = options["apiKey"] as? String
|
|
41
|
+
if (application == null || apiKey == null) return@Function
|
|
42
|
+
Advenue.initialize(
|
|
43
|
+
application,
|
|
44
|
+
AdvenueConfig(
|
|
45
|
+
apiKey = apiKey,
|
|
46
|
+
endpoint = (options["endpoint"] as? String) ?: DEFAULT_ENDPOINT,
|
|
47
|
+
requireConsent = (options["requireConsent"] as? Boolean) ?: false,
|
|
48
|
+
batchSize = (options["batchSize"] as? Number)?.toInt() ?: 20,
|
|
49
|
+
flushIntervalMs = (options["flushIntervalMs"] as? Number)?.toLong() ?: 15_000L,
|
|
50
|
+
signingSecret = options["signingSecret"] as? String,
|
|
51
|
+
onError = { context, error ->
|
|
52
|
+
sendEvent("onAdvenueError", mapOf("context" to context, "message" to error.toString()))
|
|
53
|
+
},
|
|
54
|
+
// The wrapper's version, not this SDK's: a React Native release pins
|
|
55
|
+
// the native snapshot inside it, so its version identifies both.
|
|
56
|
+
sdkVersion = options["sdkVersion"] as? String,
|
|
57
|
+
// The JavaScript has sent this since before the inversion; nothing
|
|
58
|
+
// read it, so Meta install-referrer attribution never ran.
|
|
59
|
+
fbAppId = options["fbAppId"] as? String,
|
|
60
|
+
),
|
|
61
|
+
)
|
|
186
62
|
}
|
|
187
63
|
|
|
188
|
-
|
|
189
|
-
|
|
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()
|
|
64
|
+
Function("bridgeTrack") { name: String, properties: Map<String, Any?>? ->
|
|
65
|
+
Advenue.track(name, properties)
|
|
204
66
|
}
|
|
205
67
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
68
|
+
Function("bridgeFlush") { Advenue.flush() }
|
|
69
|
+
Function("bridgeSetUserId") { id: String? -> Advenue.setUserId(id) }
|
|
70
|
+
Function("bridgeSetConsent") { granted: Boolean -> Advenue.setConsent(granted) }
|
|
71
|
+
Function("bridgeForgetMe") { Advenue.forgetMe() }
|
|
72
|
+
Function("bridgeShutdown") { Advenue.shutdown() }
|
|
73
|
+
Function("bridgeResolvedAppId") { Advenue.resolvedAppId() }
|
|
74
|
+
Function("bridgeDeviceId") { Advenue.deviceId() }
|
|
75
|
+
|
|
76
|
+
Function("bridgeSetConsentData") { data: Map<String, Any?> ->
|
|
77
|
+
val gdpr = data["isUserSubjectToGDPR"] as? Boolean ?: return@Function
|
|
78
|
+
Advenue.setConsentData(
|
|
79
|
+
gdpr,
|
|
80
|
+
data["hasConsentForDataUsage"] as? Boolean,
|
|
81
|
+
data["hasConsentForAdsPersonalization"] as? Boolean,
|
|
82
|
+
data["hasConsentForAdStorage"] as? Boolean,
|
|
83
|
+
)
|
|
214
84
|
}
|
|
215
85
|
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (ctx != null) {
|
|
235
|
-
val ai = try {
|
|
236
|
-
ctx.packageManager.getApplicationInfo(
|
|
237
|
-
ctx.packageName,
|
|
238
|
-
android.content.pm.PackageManager.GET_META_DATA,
|
|
239
|
-
)
|
|
240
|
-
} catch (_: Exception) { null }
|
|
241
|
-
val mb = ai?.metaData
|
|
242
|
-
val cfgNum: Long = mb?.getLong("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER", 0L)
|
|
243
|
-
?.takeIf { it != 0L }
|
|
244
|
-
?: mb?.getString("io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER")?.toLongOrNull()
|
|
245
|
-
?: 0L
|
|
246
|
-
if (cfgNum == 0L) {
|
|
247
|
-
promise.reject(
|
|
248
|
-
"ECONFIG",
|
|
249
|
-
"Play Integrity not configured: add io.advenue.INTEGRITY_CLOUD_PROJECT_NUMBER to AndroidManifest.xml meta-data",
|
|
250
|
-
null,
|
|
251
|
-
)
|
|
252
|
-
return@AsyncFunction
|
|
86
|
+
// Read back, for the JavaScript consent cache. The cache is seeded from
|
|
87
|
+
// this at initialize: consent is persisted and survives restarts, so a
|
|
88
|
+
// facade defaulting to false on every cold start would silently discard a
|
|
89
|
+
// preference the user gave.
|
|
90
|
+
Function("bridgeGetConsent") { Advenue.trackingConsent() }
|
|
91
|
+
|
|
92
|
+
Function("bridgeGetConsentData") {
|
|
93
|
+
val consent = Advenue.consentData()
|
|
94
|
+
if (consent == null) {
|
|
95
|
+
null
|
|
96
|
+
} else {
|
|
97
|
+
// Written only when stated: "not asked" is not "denied", and a false
|
|
98
|
+
// invented here is a recorded refusal that never happened.
|
|
99
|
+
buildMap<String, Any> {
|
|
100
|
+
put("isUserSubjectToGDPR", consent.isUserSubjectToGDPR)
|
|
101
|
+
consent.hasConsentForDataUsage?.let { put("hasConsentForDataUsage", it) }
|
|
102
|
+
consent.hasConsentForAdsPersonalization?.let {
|
|
103
|
+
put("hasConsentForAdsPersonalization", it)
|
|
253
104
|
}
|
|
105
|
+
consent.hasConsentForAdStorage?.let { put("hasConsentForAdStorage", it) }
|
|
254
106
|
}
|
|
255
|
-
promise.reject(
|
|
256
|
-
"EUNSUPPORTED",
|
|
257
|
-
"Play Integrity token provider not ready (Play Services may be unavailable or warming failed)",
|
|
258
|
-
null,
|
|
259
|
-
)
|
|
260
|
-
return@AsyncFunction
|
|
261
107
|
}
|
|
262
|
-
|
|
263
|
-
val request = StandardIntegrityTokenRequest.builder()
|
|
264
|
-
.setRequestHash(requestHash)
|
|
265
|
-
.build()
|
|
266
|
-
|
|
267
|
-
provider.request(request)
|
|
268
|
-
.addOnSuccessListener { response -> promise.resolve(response.token()) }
|
|
269
|
-
.addOnFailureListener { e ->
|
|
270
|
-
promise.reject("EINTEGRITY", e.message ?: "Play Integrity request failed", e)
|
|
271
|
-
}
|
|
272
108
|
}
|
|
273
109
|
|
|
274
|
-
|
|
275
|
-
// API used here is a cheap local read (no network, no blocking I/O beyond
|
|
276
|
-
// a local StatFs stat call). Best-effort per field: a field is simply
|
|
277
|
-
// omitted (not defaulted) when its source is unavailable, so the JS-side
|
|
278
|
-
// fail-safe (collectDeviceInfo) degrades gracefully rather than sending
|
|
279
|
-
// misleading zeros.
|
|
280
|
-
Function("getDeviceInfo") {
|
|
281
|
-
val map = mutableMapOf<String, Any?>()
|
|
282
|
-
map["model"] = android.os.Build.MODEL
|
|
283
|
-
map["locale"] = java.util.Locale.getDefault().toString()
|
|
284
|
-
map["timezone"] = java.util.TimeZone.getDefault().id
|
|
285
|
-
map["cpuCores"] = Runtime.getRuntime().availableProcessors()
|
|
286
|
-
|
|
287
|
-
val ctx = appContext.reactContext?.applicationContext
|
|
288
|
-
if (ctx != null) {
|
|
289
|
-
// Meta CAPI extinfo slots 1-3. versionName is the user-facing version
|
|
290
|
-
// (slot 2, surfaced as the event's appVersion); the version code is the
|
|
291
|
-
// build identifier (slot 3). Best-effort: a missing PackageInfo must
|
|
292
|
-
// not cost the caller the rest of the device metadata.
|
|
293
|
-
map["packageName"] = ctx.packageName
|
|
294
|
-
try {
|
|
295
|
-
val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
|
|
296
|
-
pi.versionName?.let { map["shortVersion"] = it }
|
|
297
|
-
@Suppress("DEPRECATION")
|
|
298
|
-
val code =
|
|
299
|
-
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
300
|
-
pi.longVersionCode
|
|
301
|
-
} else {
|
|
302
|
-
pi.versionCode.toLong()
|
|
303
|
-
}
|
|
304
|
-
map["longVersion"] = code.toString()
|
|
305
|
-
} catch (_: Exception) {
|
|
306
|
-
// package info best-effort
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
val dm = ctx.resources.displayMetrics
|
|
310
|
-
map["screenWidth"] = dm.widthPixels
|
|
311
|
-
map["screenHeight"] = dm.heightPixels
|
|
312
|
-
map["screenDensity"] = dm.density.toDouble()
|
|
313
|
-
} catch (_: Exception) {
|
|
314
|
-
// screen metrics best-effort
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
val statFs = android.os.StatFs(ctx.filesDir.path)
|
|
318
|
-
val blockSize = statFs.blockSizeLong
|
|
319
|
-
val gb = 1024.0 * 1024.0 * 1024.0
|
|
320
|
-
map["totalStorageGb"] = (statFs.blockCountLong * blockSize).toDouble() / gb
|
|
321
|
-
map["freeStorageGb"] = (statFs.availableBlocksLong * blockSize).toDouble() / gb
|
|
322
|
-
} catch (_: Exception) {
|
|
323
|
-
// storage stat best-effort
|
|
324
|
-
}
|
|
325
|
-
try {
|
|
326
|
-
val tm = ctx.getSystemService(android.content.Context.TELEPHONY_SERVICE)
|
|
327
|
-
as? android.telephony.TelephonyManager
|
|
328
|
-
val carrier = tm?.networkOperatorName
|
|
329
|
-
if (!carrier.isNullOrEmpty()) map["carrier"] = carrier
|
|
330
|
-
} catch (_: Exception) {
|
|
331
|
-
// carrier best-effort
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
map
|
|
335
|
-
}
|
|
110
|
+
Function("bridgeSetAppInstanceId") { id: String? -> Advenue.setAppInstanceId(id) }
|
|
336
111
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// Wiped on uninstall — Android has no reinstall-durable store, so a
|
|
340
|
-
// reinstall legitimately re-fires the install (backend window dedups).
|
|
341
|
-
Function("storageGetItem") { key: String ->
|
|
342
|
-
sdkPrefs()?.getString(key, null)
|
|
343
|
-
}
|
|
344
|
-
Function("storageSetItem") { key: String, value: String ->
|
|
345
|
-
sdkPrefs()?.edit()?.putString(key, value)?.apply()
|
|
346
|
-
}
|
|
347
|
-
Function("storageRemoveItem") { key: String ->
|
|
348
|
-
sdkPrefs()?.edit()?.remove(key)?.apply()
|
|
112
|
+
Function("bridgeSetPushToken") { token: String?, provider: String? ->
|
|
113
|
+
Advenue.setPushToken(token, provider)
|
|
349
114
|
}
|
|
350
115
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
Function("getTcfData") { ->
|
|
356
|
-
val ctx = appContext.reactContext?.applicationContext ?: return@Function null
|
|
357
|
-
val prefs = ctx.getSharedPreferences(
|
|
358
|
-
"${ctx.packageName}_preferences", android.content.Context.MODE_PRIVATE
|
|
359
|
-
)
|
|
360
|
-
// prefs.all + map lookup: a missing key reads as null (no default value
|
|
361
|
-
// involved), so "missing" and "0" stay distinguishable. `as? Number`
|
|
362
|
-
// tolerates a CMP writing the value as Int OR Long (SharedPreferences
|
|
363
|
-
// has no fixed numeric type per the TCF spec's own contract) without
|
|
364
|
-
// degrading a Long writer to unknown. runCatching stays as the
|
|
365
|
-
// last-resort crash guard.
|
|
366
|
-
val gdprApplies = runCatching {
|
|
367
|
-
(prefs.all["IABTCF_gdprApplies"] as? Number)?.toInt()
|
|
368
|
-
}.getOrNull()
|
|
369
|
-
val purposeConsents = runCatching { prefs.getString("IABTCF_PurposeConsents", null) }.getOrNull()
|
|
370
|
-
if (gdprApplies == null && purposeConsents == null) return@Function null
|
|
371
|
-
mapOf("gdprApplies" to gdprApplies, "purposeConsents" to purposeConsents)
|
|
116
|
+
Function("bridgeProcessDeepLink") { url: String ->
|
|
117
|
+
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
|
118
|
+
intent.data = android.net.Uri.parse(url)
|
|
119
|
+
Advenue.processDeepLink(intent)
|
|
372
120
|
}
|
|
373
121
|
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
android.provider.Settings.Secure.getString(
|
|
383
|
-
ctx.contentResolver,
|
|
384
|
-
android.provider.Settings.Secure.ANDROID_ID,
|
|
385
|
-
)
|
|
122
|
+
// Blocking by nature — it polls with backoff — so it is async across the
|
|
123
|
+
// bridge and runs on its own thread. Calling it on the JS thread would
|
|
124
|
+
// freeze the UI for up to eight seconds.
|
|
125
|
+
AsyncFunction("bridgeResolveDeferredDeepLink") { promise: Promise ->
|
|
126
|
+
Thread {
|
|
127
|
+
val link = Advenue.resolveDeferredDeepLink()
|
|
128
|
+
if (link == null) {
|
|
129
|
+
promise.resolve(null)
|
|
386
130
|
} else {
|
|
387
|
-
|
|
131
|
+
promise.resolve(
|
|
132
|
+
mapOf(
|
|
133
|
+
"deepLinkValue" to link.deepLinkValue,
|
|
134
|
+
"campaign" to link.campaign,
|
|
135
|
+
"network" to link.network,
|
|
136
|
+
"influencerId" to link.influencerId,
|
|
137
|
+
"isDeferred" to link.isDeferred,
|
|
138
|
+
"isFirstLaunch" to link.isFirstLaunch,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
388
141
|
}
|
|
389
|
-
} catch (_: Exception) {
|
|
390
|
-
null // best-effort: a missing SSAID never blocks anything
|
|
391
142
|
}
|
|
143
|
+
.apply { isDaemon = true }
|
|
144
|
+
.start()
|
|
392
145
|
}
|
|
393
146
|
}
|
|
394
|
-
|
|
395
|
-
private fun sdkPrefs(): android.content.SharedPreferences? =
|
|
396
|
-
appContext.reactContext?.applicationContext
|
|
397
|
-
?.getSharedPreferences("io.advenue.sdk", android.content.Context.MODE_PRIVATE)
|
|
398
147
|
}
|