@advenue/react-native 0.9.0 → 1.0.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 +8 -7
- package/android/src/main/java/expo/modules/advenue/AdvenueAndroidModule.kt +118 -368
- package/android/src/main/kotlin/io/advenue/Advenue.kt +549 -0
- package/android/src/main/kotlin/io/advenue/AdvenueConfig.kt +114 -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 +186 -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 +414 -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 +47 -0
- package/android/src/main/kotlin/io/advenue/core/MetaReferrer.kt +80 -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 +149 -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 +91 -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 +183 -675
- package/dist/index.d.cts +199 -336
- package/dist/index.d.ts +199 -336
- package/dist/index.js +182 -681
- package/ios/AdvenueIosModule.swift +144 -446
- package/ios/vendor/Advenue/Advenue.swift +596 -0
- package/ios/vendor/Advenue/AdvenueConfig.swift +77 -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 +587 -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/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 +65 -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 +143 -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 +9 -11
- 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 +266 -830
- package/src/native-types.ts +74 -181
- package/src/native.ts +0 -23
- package/src/types.ts +81 -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,148 @@
|
|
|
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
|
-
|
|
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
|
+
appVersion = options["appVersion"] as? String,
|
|
48
|
+
requireConsent = (options["requireConsent"] as? Boolean) ?: false,
|
|
49
|
+
batchSize = (options["batchSize"] as? Number)?.toInt() ?: 20,
|
|
50
|
+
flushIntervalMs = (options["flushIntervalMs"] as? Number)?.toLong() ?: 15_000L,
|
|
51
|
+
signingSecret = options["signingSecret"] as? String,
|
|
52
|
+
onError = { context, error ->
|
|
53
|
+
sendEvent("onAdvenueError", mapOf("context" to context, "message" to error.toString()))
|
|
54
|
+
},
|
|
55
|
+
// The wrapper's version, not this SDK's: a React Native release pins
|
|
56
|
+
// the native snapshot inside it, so its version identifies both.
|
|
57
|
+
sdkVersion = options["sdkVersion"] as? String,
|
|
58
|
+
// The JavaScript has sent this since before the inversion; nothing
|
|
59
|
+
// read it, so Meta install-referrer attribution never ran.
|
|
60
|
+
fbAppId = options["fbAppId"] as? String,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
186
63
|
}
|
|
187
64
|
|
|
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()
|
|
65
|
+
Function("bridgeTrack") { name: String, properties: Map<String, Any?>? ->
|
|
66
|
+
Advenue.track(name, properties)
|
|
204
67
|
}
|
|
205
68
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
69
|
+
Function("bridgeFlush") { Advenue.flush() }
|
|
70
|
+
Function("bridgeSetUserId") { id: String? -> Advenue.setUserId(id) }
|
|
71
|
+
Function("bridgeSetConsent") { granted: Boolean -> Advenue.setConsent(granted) }
|
|
72
|
+
Function("bridgeForgetMe") { Advenue.forgetMe() }
|
|
73
|
+
Function("bridgeShutdown") { Advenue.shutdown() }
|
|
74
|
+
Function("bridgeResolvedAppId") { Advenue.resolvedAppId() }
|
|
75
|
+
Function("bridgeDeviceId") { Advenue.deviceId() }
|
|
76
|
+
|
|
77
|
+
Function("bridgeSetConsentData") { data: Map<String, Any?> ->
|
|
78
|
+
val gdpr = data["isUserSubjectToGDPR"] as? Boolean ?: return@Function
|
|
79
|
+
Advenue.setConsentData(
|
|
80
|
+
gdpr,
|
|
81
|
+
data["hasConsentForDataUsage"] as? Boolean,
|
|
82
|
+
data["hasConsentForAdsPersonalization"] as? Boolean,
|
|
83
|
+
data["hasConsentForAdStorage"] as? Boolean,
|
|
84
|
+
)
|
|
214
85
|
}
|
|
215
86
|
|
|
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
|
|
87
|
+
// Read back, for the JavaScript consent cache. The cache is seeded from
|
|
88
|
+
// this at initialize: consent is persisted and survives restarts, so a
|
|
89
|
+
// facade defaulting to false on every cold start would silently discard a
|
|
90
|
+
// preference the user gave.
|
|
91
|
+
Function("bridgeGetConsent") { Advenue.trackingConsent() }
|
|
92
|
+
|
|
93
|
+
Function("bridgeGetConsentData") {
|
|
94
|
+
val consent = Advenue.consentData()
|
|
95
|
+
if (consent == null) {
|
|
96
|
+
null
|
|
97
|
+
} else {
|
|
98
|
+
// Written only when stated: "not asked" is not "denied", and a false
|
|
99
|
+
// invented here is a recorded refusal that never happened.
|
|
100
|
+
buildMap<String, Any> {
|
|
101
|
+
put("isUserSubjectToGDPR", consent.isUserSubjectToGDPR)
|
|
102
|
+
consent.hasConsentForDataUsage?.let { put("hasConsentForDataUsage", it) }
|
|
103
|
+
consent.hasConsentForAdsPersonalization?.let {
|
|
104
|
+
put("hasConsentForAdsPersonalization", it)
|
|
253
105
|
}
|
|
106
|
+
consent.hasConsentForAdStorage?.let { put("hasConsentForAdStorage", it) }
|
|
254
107
|
}
|
|
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
108
|
}
|
|
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
109
|
}
|
|
273
110
|
|
|
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
|
-
}
|
|
111
|
+
Function("bridgeSetAppInstanceId") { id: String? -> Advenue.setAppInstanceId(id) }
|
|
336
112
|
|
|
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()
|
|
113
|
+
Function("bridgeSetPushToken") { token: String?, provider: String? ->
|
|
114
|
+
Advenue.setPushToken(token, provider)
|
|
349
115
|
}
|
|
350
116
|
|
|
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)
|
|
117
|
+
Function("bridgeProcessDeepLink") { url: String ->
|
|
118
|
+
val intent = android.content.Intent(android.content.Intent.ACTION_VIEW)
|
|
119
|
+
intent.data = android.net.Uri.parse(url)
|
|
120
|
+
Advenue.processDeepLink(intent)
|
|
372
121
|
}
|
|
373
122
|
|
|
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
|
-
)
|
|
123
|
+
// Blocking by nature — it polls with backoff — so it is async across the
|
|
124
|
+
// bridge and runs on its own thread. Calling it on the JS thread would
|
|
125
|
+
// freeze the UI for up to eight seconds.
|
|
126
|
+
AsyncFunction("bridgeResolveDeferredDeepLink") { promise: Promise ->
|
|
127
|
+
Thread {
|
|
128
|
+
val link = Advenue.resolveDeferredDeepLink()
|
|
129
|
+
if (link == null) {
|
|
130
|
+
promise.resolve(null)
|
|
386
131
|
} else {
|
|
387
|
-
|
|
132
|
+
promise.resolve(
|
|
133
|
+
mapOf(
|
|
134
|
+
"deepLinkValue" to link.deepLinkValue,
|
|
135
|
+
"campaign" to link.campaign,
|
|
136
|
+
"network" to link.network,
|
|
137
|
+
"influencerId" to link.influencerId,
|
|
138
|
+
"isDeferred" to link.isDeferred,
|
|
139
|
+
"isFirstLaunch" to link.isFirstLaunch,
|
|
140
|
+
),
|
|
141
|
+
)
|
|
388
142
|
}
|
|
389
|
-
} catch (_: Exception) {
|
|
390
|
-
null // best-effort: a missing SSAID never blocks anything
|
|
391
143
|
}
|
|
144
|
+
.apply { isDaemon = true }
|
|
145
|
+
.start()
|
|
392
146
|
}
|
|
393
147
|
}
|
|
394
|
-
|
|
395
|
-
private fun sdkPrefs(): android.content.SharedPreferences? =
|
|
396
|
-
appContext.reactContext?.applicationContext
|
|
397
|
-
?.getSharedPreferences("io.advenue.sdk", android.content.Context.MODE_PRIVATE)
|
|
398
148
|
}
|